From 11d0054fcc64b865b55666471a109d06508b0c28 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Thu, 7 Dec 2023 11:29:27 +0000 Subject: [PATCH] ENT-11055: Basic external verification (#7545) * ENT-11055: Basic external verification Introduction of the external transaction verifier, a separate JVM process for verifying `SignedTransaction`s. The end goal is for this verifier to be built with Kotlin 1.2 so that it creates a compatible verification environment for transactions with 4.11 contracts. For now however the verifier is built against Kotlin 1.8, same as the node. External verification is enabled when the the system property `net.corda.node.verification.external` is set to `true`. When enabled, all verification requests made via `SignedTransaction.verify` are sent to the external verifier, regardless of the transaction content. It will do the vast bulk of the verification and then send the result back, namely if an exception occurred. If it did, then it's re-thrown in the node. The external verifier is a stateless process, with no connection to the node's database. All transaction resolution information needed to create the relevant ledger transaction object are made to the node, which waits in a loop servicing these requests until it receives the result. The verifier Jar is embedded in the Corda node Jar, and is extracted and run when needed for the first time. The node opens up a local port for the verifier to communicate with, which is specified to the verifier in the process command line. This all means there is no extra configuration or deployment required to support external verification. The existing code had some initial attempts and abstractions to support a future external verification feature. However, they were either incorrect or didn't quite fit. One such example was `TransactionVerifierService`. It incorrectly operated on the `LedgerTransaction` level, which doesn't work since the transaction needs to be first serialised. Instead a new abstraction, `VerificationSupport` has been introduced, which represents all the operations needed to resolve and verify a `SignedTransaction`, essentially replacing `ServicesForResolution` (a lot of the changes are due to this). The external verifier implements this with a simple RPC mechanism, whilst the node needed a new (internal) `ServiceHub` abstraction, `VerifyingServiceHub`. `ServicesForResolution` hasn't been deleted since it's public API, however all classes implementing it must also implement `VerifyingServiceHub`. This is possible to do without breaking compatibility since `ServicesForResolution` is annotated with `@DoNotImplement`. Changes to `api-current.txt` were made due to the removal of `TransactionVerifierService`, which was clearly indicated as an internal class, and returning `TransactionBuilder.toLedgerTransactionWithContext` back to an internal method. * Address review comments * One bulk load states method * Merge fix --- .ci/api-current.txt | 11 - build.gradle | 2 +- client/jackson/build.gradle | 4 +- .../client/jackson/JacksonSupportTest.kt | 99 +-- .../flows/ContractUpgradeFlowRPCTest.kt | 4 +- .../flows/ContractUpgradeFlowTest.kt | 42 +- .../net/corda/coretests/flows/WithFinality.kt | 5 +- .../verification/AttachmentFixupsTest.kt | 137 ++++ .../transactions/TransactionBuilderTest.kt | 137 ++-- core/build.gradle | 8 +- .../core/contracts/ContractAttachment.kt | 1 + .../crypto/internal/DigestAlgorithmFactory.kt | 9 +- .../corda/core/flows/SendTransactionFlow.kt | 15 +- .../corda/core/internal/ClassLoadingUtils.kt | 19 +- .../net/corda/core/internal/CordaUtils.kt | 56 +- .../core/internal/CordappFixupInternal.kt | 7 - .../net/corda/core/internal/InternalUtils.kt | 42 +- .../net/corda/core/internal/NamedCache.kt | 14 +- .../core/internal/ServiceHubCoreInternal.kt | 10 +- .../corda/core/internal/TransactionUtils.kt | 4 +- .../cordapp/CordappProviderInternal.kt | 13 + .../internal/verification/AttachmentFixups.kt | 79 +++ .../verification/VerificationSupport.kt | 50 ++ .../Verifier.kt} | 39 +- .../verification/VerifyingServiceHub.kt | 221 ++++++ .../kotlin/net/corda/core/node/ServiceHub.kt | 14 +- .../services/TransactionVerifierService.kt | 18 - .../ContractUpgradeTransactions.kt | 161 ++--- .../core/transactions/LedgerTransaction.kt | 94 +-- .../transactions/NotaryChangeTransactions.kt | 85 ++- .../core/transactions/SignedTransaction.kt | 169 ++++- .../core/transactions/TransactionBuilder.kt | 73 +- .../core/transactions/WireTransaction.kt | 171 +++-- ...elpers.kt => InternalAccessTestHelpers.kt} | 38 +- .../finance/contracts/CommercialPaperTests.kt | 5 +- ...tachmentsClassLoaderStaticContractTests.kt | 56 +- .../persistence/HibernateConfiguration.kt | 3 +- .../CustomSerializationSchemeAdapterTests.kt | 1 + node/build.gradle | 4 + .../contracts/mutator/MutatorContract.kt | 2 +- .../CustomSerializationSchemeDriverTest.kt | 29 +- .../verification/ExternalVerificationTest.kt | 219 ++++++ .../net/corda/node/internal/AbstractNode.kt | 64 +- .../corda/node/internal/AppServiceHubImpl.kt | 9 +- .../kotlin/net/corda/node/internal/Node.kt | 59 +- .../internal/NodeServicesForResolution.kt | 15 - .../internal/ServicesForResolutionImpl.kt | 85 --- .../corda/node/internal/classloading/Utils.kt | 25 - .../internal/cordapp/CordappProviderImpl.kt | 136 +--- .../cordapp/CordappProviderInternal.kt | 14 - .../cordapp/JarScanningCordappLoader.kt | 30 +- .../security/RPCSecurityManagerImpl.kt | 4 +- .../corda/node/migration/CordaMigration.kt | 15 - .../MigrationServicesForResolution.kt | 175 ----- .../node/migration/VaultStateMigration.kt | 239 +------ .../node/services/api/ServiceHubInternal.kt | 19 +- .../NodeAttachmentTrustCalculator.kt | 19 +- .../PublicKeyToOwningIdentityCacheImpl.kt | 6 +- .../statemachine/FlowLogicRefFactoryImpl.kt | 10 +- .../InMemoryTransactionVerifierService.kt | 120 ---- .../node/services/vault/NodeVaultService.kt | 43 +- .../utilities/InfrequentlyMutatedCache.kt | 3 +- .../node/utilities/NonInvalidatingCache.kt | 3 +- .../verification/ExternalVerifierHandle.kt | 229 +++++++ .../node/verification/NoDbAccessVerifier.kt | 12 + .../CustomSerializationSchemeScanningTest.kt | 17 +- .../node/migration/VaultStateMigrationTest.kt | 637 ------------------ .../corda/node/services/NotaryChangeTests.kt | 5 +- .../persistence/HibernateConfigurationTest.kt | 38 +- .../vault/VaultSoftLockManagerTest.kt | 4 +- .../raft/RaftNotaryServiceTests.kt | 21 +- serialization/build.gradle | 2 - .../internal/model/RemoteTypeCarpenter.kt | 8 +- .../CustomSerializationSchemeAdapter.kt | 28 +- .../verifier/ExternalVerifierTypes.kt | 87 +++ settings.gradle | 1 + .../InternalSerializationTestHelpers.kt | 2 +- .../net/corda/testing/node/MockServices.kt | 84 ++- .../node/internal/InternalMockNetwork.kt | 1 - .../kotlin/net/corda/testing/dsl/TestDSL.kt | 16 +- verifier/build.gradle | 35 + .../verifier/ExternalVerificationContext.kt | 42 ++ .../net/corda/verifier/ExternalVerifier.kt | 237 +++++++ .../ExternalVerifierNamedCacheFactory.kt | 35 + .../main/kotlin/net/corda/verifier/Main.kt | 46 ++ verifier/src/main/resources/log4j2.xml | 44 ++ 86 files changed, 2520 insertions(+), 2374 deletions(-) create mode 100644 core-tests/src/test/kotlin/net/corda/coretests/internal/verification/AttachmentFixupsTest.kt delete mode 100644 core/src/main/kotlin/net/corda/core/internal/CordappFixupInternal.kt create mode 100644 core/src/main/kotlin/net/corda/core/internal/cordapp/CordappProviderInternal.kt create mode 100644 core/src/main/kotlin/net/corda/core/internal/verification/AttachmentFixups.kt create mode 100644 core/src/main/kotlin/net/corda/core/internal/verification/VerificationSupport.kt rename core/src/main/kotlin/net/corda/core/internal/{TransactionVerifierServiceInternal.kt => verification/Verifier.kt} (95%) create mode 100644 core/src/main/kotlin/net/corda/core/internal/verification/VerifyingServiceHub.kt delete mode 100644 core/src/main/kotlin/net/corda/core/node/services/TransactionVerifierService.kt rename core/src/test/kotlin/net/corda/core/internal/{internalAccessTestHelpers.kt => InternalAccessTestHelpers.kt} (68%) create mode 100644 node/src/integration-test/kotlin/net/corda/node/verification/ExternalVerificationTest.kt delete mode 100644 node/src/main/kotlin/net/corda/node/internal/NodeServicesForResolution.kt delete mode 100644 node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt delete mode 100644 node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderInternal.kt delete mode 100644 node/src/main/kotlin/net/corda/node/migration/MigrationServicesForResolution.kt delete mode 100644 node/src/main/kotlin/net/corda/node/services/transactions/InMemoryTransactionVerifierService.kt create mode 100644 node/src/main/kotlin/net/corda/node/verification/ExternalVerifierHandle.kt create mode 100644 node/src/main/kotlin/net/corda/node/verification/NoDbAccessVerifier.kt delete mode 100644 node/src/test/kotlin/net/corda/node/migration/VaultStateMigrationTest.kt rename {node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization => serialization/src/main/kotlin/net/corda/serialization/internal/verifier}/CustomSerializationSchemeAdapter.kt (66%) create mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/verifier/ExternalVerifierTypes.kt create mode 100644 verifier/build.gradle create mode 100644 verifier/src/main/kotlin/net/corda/verifier/ExternalVerificationContext.kt create mode 100644 verifier/src/main/kotlin/net/corda/verifier/ExternalVerifier.kt create mode 100644 verifier/src/main/kotlin/net/corda/verifier/ExternalVerifierNamedCacheFactory.kt create mode 100644 verifier/src/main/kotlin/net/corda/verifier/Main.kt create mode 100644 verifier/src/main/resources/log4j2.xml diff --git a/.ci/api-current.txt b/.ci/api-current.txt index a2703c32b2..363b258082 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -4735,8 +4735,6 @@ public interface net.corda.core.node.ServiceHub extends net.corda.core.node.Serv @NotNull public abstract net.corda.core.node.services.TelemetryService getTelemetryService() @NotNull - public abstract net.corda.core.node.services.TransactionVerifierService getTransactionVerifierService() - @NotNull public abstract net.corda.core.node.services.TransactionStorage getValidatedTransactions() @NotNull public abstract net.corda.core.node.services.VaultService getVaultService() @@ -5087,11 +5085,6 @@ public interface net.corda.core.node.services.TransactionStorage @NotNull public abstract net.corda.core.concurrent.CordaFuture trackTransaction(net.corda.core.crypto.SecureHash) ## -@DoNotImplement -public interface net.corda.core.node.services.TransactionVerifierService - @NotNull - public abstract net.corda.core.concurrent.CordaFuture verify(net.corda.core.transactions.LedgerTransaction) -## @CordaSerializable public final class net.corda.core.node.services.UnknownAnonymousPartyException extends net.corda.core.CordaException public (String) @@ -7846,8 +7839,6 @@ public class net.corda.core.transactions.TransactionBuilder extends java.lang.Ob @NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServiceHub) @NotNull - public final net.corda.core.transactions.LedgerTransaction toLedgerTransactionWithContext(net.corda.core.node.ServicesForResolution, net.corda.core.serialization.SerializationContext) - @NotNull public final net.corda.core.transactions.SignedTransaction toSignedTransaction(net.corda.core.node.services.KeyManagementService, java.security.PublicKey, net.corda.core.crypto.SignatureMetadata, net.corda.core.node.ServicesForResolution) @NotNull public final net.corda.core.transactions.WireTransaction toWireTransaction(net.corda.core.node.ServicesForResolution) @@ -9890,8 +9881,6 @@ public class net.corda.testing.node.MockServices extends java.lang.Object implem @NotNull public net.corda.core.internal.telemetry.TelemetryServiceImpl getTelemetryService() @NotNull - public net.corda.core.node.services.TransactionVerifierService getTransactionVerifierService() - @NotNull public net.corda.core.node.services.TransactionStorage getValidatedTransactions() @NotNull public net.corda.core.node.services.VaultService getVaultService() diff --git a/build.gradle b/build.gradle index 7bdd1871cb..c239bed37a 100644 --- a/build.gradle +++ b/build.gradle @@ -215,7 +215,7 @@ plugins { id 'org.jetbrains.kotlin.jvm' apply false id 'org.jetbrains.kotlin.plugin.allopen' apply false id 'org.jetbrains.kotlin.plugin.jpa' apply false - id 'com.github.johnrengelman.shadow' version '2.0.4' apply false + id 'com.github.johnrengelman.shadow' version '7.1.2' apply false id "org.ajoberstar.grgit" version "4.0.0" id 'corda.root-publish' id "org.jetbrains.dokka" version "1.8.20" diff --git a/client/jackson/build.gradle b/client/jackson/build.gradle index eb4cca280e..47b9b51ac3 100644 --- a/client/jackson/build.gradle +++ b/client/jackson/build.gradle @@ -4,7 +4,8 @@ apply plugin: 'net.corda.plugins.api-scanner' apply plugin: 'corda.common-publishing' dependencies { - implementation project(':core') + api project(':core') + implementation project(':serialization') // Jackson and its plugins: parsing to/from JSON and other textual formats. @@ -27,6 +28,7 @@ dependencies { testImplementation project(':test-common') testImplementation project(':core-test-utils') testImplementation project(':test-utils') + testImplementation project(":node-driver") testImplementation project(path: ':core', configuration: 'testArtifacts') testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}" diff --git a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt index f753375b5a..fddafc6547 100644 --- a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt +++ b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt @@ -8,27 +8,36 @@ import com.fasterxml.jackson.databind.node.ObjectNode import com.fasterxml.jackson.databind.node.TextNode import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import com.fasterxml.jackson.module.kotlin.convertValue -import org.mockito.kotlin.any -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import org.mockito.kotlin.whenever -import org.mockito.kotlin.spy import net.corda.client.jackson.internal.childrenAs import net.corda.client.jackson.internal.valueAs -import net.corda.core.contracts.* +import net.corda.core.contracts.Amount +import net.corda.core.contracts.Command +import net.corda.core.contracts.LinearState +import net.corda.core.contracts.PrivacySalt +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TimeWindow +import net.corda.core.contracts.TransactionState +import net.corda.core.contracts.UniqueIdentifier import net.corda.core.cordapp.CordappProvider -import net.corda.core.crypto.* import net.corda.core.crypto.CompositeKey +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.DigestService +import net.corda.core.crypto.DigitalSignature +import net.corda.core.crypto.PartialMerkleTree import net.corda.core.crypto.PartialMerkleTree.PartialTree -import net.corda.core.identity.* -import net.corda.core.internal.AbstractAttachment +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.SignatureMetadata +import net.corda.core.crypto.SignatureScheme +import net.corda.core.crypto.TransactionSignature +import net.corda.core.crypto.secureRandomBytes +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.DigitalSignatureWithCert import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub -import net.corda.core.node.services.AttachmentStorage -import net.corda.core.node.services.IdentityService -import net.corda.core.node.services.NetworkParametersService -import net.corda.core.node.services.TransactionStorage import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize @@ -37,14 +46,27 @@ import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.* +import net.corda.core.utilities.ByteSequence +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.days +import net.corda.core.utilities.hours +import net.corda.core.utilities.toBase58String +import net.corda.core.utilities.toBase64 +import net.corda.core.utilities.toHexString +import net.corda.coretesting.internal.createNodeInfoAndSigned +import net.corda.coretesting.internal.rigorousMock import net.corda.finance.USD import net.corda.nodeapi.internal.crypto.x509Certificates import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract -import net.corda.testing.core.* -import net.corda.coretesting.internal.createNodeInfoAndSigned -import net.corda.coretesting.internal.rigorousMock +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.core.DummyCommandData +import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.core.TestIdentity +import net.corda.testing.node.MockServices import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Before @@ -54,15 +76,22 @@ import org.junit.jupiter.api.TestFactory import org.junit.runner.RunWith import org.junit.runners.Parameterized import org.junit.runners.Parameterized.Parameters +import org.mockito.kotlin.spy +import org.mockito.kotlin.whenever import java.math.BigInteger import java.nio.charset.StandardCharsets.UTF_8 import java.security.PublicKey import java.security.cert.CertPath import java.security.cert.X509Certificate import java.time.Instant -import java.util.* +import java.util.Currency +import java.util.Date +import java.util.UUID import javax.security.auth.x500.X500Principal -import kotlin.collections.ArrayList +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.component3 +import kotlin.collections.component4 @RunWith(Parameterized::class) class JacksonSupportTest(@Suppress("unused") private val name: String, factory: JsonFactory) { @@ -90,23 +119,12 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory: @Before fun setup() { - val unsignedAttachment = object : AbstractAttachment({ byteArrayOf() }, "test") { - override val id: SecureHash get() = throw UnsupportedOperationException() - } - - val attachments = rigorousMock().also { - doReturn(unsignedAttachment).whenever(it).openAttachment(any()) - } - services = rigorousMock() + services = MockServices( + listOf("net.corda.testing.contracts"), + MINI_CORP, + testNetworkParameters(minimumPlatformVersion = 4) + ) cordappProvider = rigorousMock() - val networkParameters = testNetworkParameters(minimumPlatformVersion = 4) - val networkParametersService = rigorousMock().also { - doReturn(networkParameters.serialize().hash).whenever(it).currentHash - } - doReturn(networkParametersService).whenever(services).networkParametersService - doReturn(cordappProvider).whenever(services).cordappProvider - doReturn(networkParameters).whenever(services).networkParameters - doReturn(attachments).whenever(services).attachments } @Test(timeout=300_000) @@ -263,17 +281,6 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory: @Test(timeout=300_000) fun `SignedTransaction (WireTransaction)`() { val attachmentId = SecureHash.randomSHA256() - doReturn(attachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID) - val attachmentStorage = rigorousMock() - doReturn(attachmentStorage).whenever(services).attachments - doReturn(mock()).whenever(services).validatedTransactions - doReturn(mock()).whenever(services).identityService - val attachment = rigorousMock() - doReturn(attachment).whenever(attachmentStorage).openAttachment(attachmentId) - doReturn(attachmentId).whenever(attachment).id - doReturn(emptyList()).whenever(attachment).signerKeys - doReturn(setOf(DummyContract.PROGRAM_ID)).whenever(attachment).allContracts - doReturn("app").whenever(attachment).uploader val wtx = TransactionBuilder( notary = DUMMY_NOTARY, diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/ContractUpgradeFlowRPCTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/ContractUpgradeFlowRPCTest.kt index 01e9e81df7..70e0996a9d 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/ContractUpgradeFlowRPCTest.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/ContractUpgradeFlowRPCTest.kt @@ -9,6 +9,7 @@ import net.corda.core.CordaRuntimeException import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateAndRef import net.corda.core.flows.ContractUpgradeFlow +import net.corda.core.internal.getRequiredTransaction import net.corda.core.messaging.CordaRPCOps import net.corda.core.transactions.ContractUpgradeLedgerTransaction import net.corda.core.transactions.SignedTransaction @@ -120,8 +121,7 @@ class ContractUpgradeFlowRPCTest : WithContracts, WithFinality { isUpgrade()) private fun TestStartedNode.getContractUpgradeTransaction(state: StateAndRef) = - services.validatedTransactions.getTransaction(state.ref.txhash)!! - .resolveContractUpgradeTransaction(services) + services.getRequiredTransaction(state.ref.txhash).resolveContractUpgradeTransaction(services) private inline fun isUpgrade() = isUpgradeFrom() and isUpgradeTo() diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/ContractUpgradeFlowTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/ContractUpgradeFlowTest.kt index 1ea3dceb50..0f4dbb7214 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/ContractUpgradeFlowTest.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/ContractUpgradeFlowTest.kt @@ -1,32 +1,55 @@ package net.corda.coretests.flows -import com.natpryce.hamkrest.* +import com.natpryce.hamkrest.Matcher +import com.natpryce.hamkrest.and +import com.natpryce.hamkrest.anything import com.natpryce.hamkrest.assertion.assertThat -import net.corda.core.contracts.* +import com.natpryce.hamkrest.equalTo +import com.natpryce.hamkrest.has +import com.natpryce.hamkrest.isA +import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint +import net.corda.core.contracts.Amount +import net.corda.core.contracts.AttachmentConstraint +import net.corda.core.contracts.BelongsToContract +import net.corda.core.contracts.CommandAndState +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.FungibleAsset +import net.corda.core.contracts.Issued +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.TypeOnlyCommandData +import net.corda.core.contracts.UpgradedContractWithLegacyConstraint import net.corda.core.flows.UnexpectedFlowEndException import net.corda.core.identity.AbstractParty import net.corda.core.internal.Emoji +import net.corda.core.internal.getRequiredTransaction +import net.corda.core.internal.mapToSet import net.corda.core.transactions.ContractUpgradeLedgerTransaction import net.corda.core.transactions.LedgerTransaction import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow +import net.corda.coretesting.internal.matchers.flow.willReturn +import net.corda.coretesting.internal.matchers.flow.willThrow import net.corda.finance.USD -import net.corda.finance.`issued by` import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.CashIssueFlow +import net.corda.finance.`issued by` import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContractV2 import net.corda.testing.contracts.DummyContractV3 import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.singleIdentity -import net.corda.coretesting.internal.matchers.flow.willReturn -import net.corda.coretesting.internal.matchers.flow.willThrow -import net.corda.testing.node.internal.* +import net.corda.testing.node.internal.DUMMY_CONTRACTS_CORDAPP +import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP +import net.corda.testing.node.internal.FINANCE_WORKFLOWS_CORDAPP +import net.corda.testing.node.internal.InternalMockNetwork +import net.corda.testing.node.internal.TestStartedNode +import net.corda.testing.node.internal.enclosedCordapp +import net.corda.testing.node.internal.startFlow import org.junit.AfterClass import org.junit.Ignore import org.junit.Test -import java.util.* +import java.util.Currency @Ignore("TODO JDK17: class cast exception") class ContractUpgradeFlowTest : WithContracts, WithFinality { @@ -161,7 +184,7 @@ class ContractUpgradeFlowTest : WithContracts, WithFinality { @BelongsToContract(CashV2::class) data class State(override val amount: Amount>, val owners: List) : FungibleAsset { override val owner: AbstractParty = owners.first() - override val exitKeys = (owners + amount.token.issuer.party).map { it.owningKey }.toSet() + override val exitKeys = (owners + amount.token.issuer.party).mapToSet { it.owningKey } override val participants = owners override fun withNewOwnerAndAmount(newAmount: Amount>, newOwner: AbstractParty) = copy(amount = amount.copy(newAmount.quantity), owners = listOf(newOwner)) @@ -182,8 +205,7 @@ class ContractUpgradeFlowTest : WithContracts, WithFinality { isUpgrade()) private fun TestStartedNode.getContractUpgradeTransaction(state: StateAndRef) = - services.validatedTransactions.getTransaction(state.ref.txhash)!! - .resolveContractUpgradeTransaction(services) + services.getRequiredTransaction(state.ref.txhash).resolveContractUpgradeTransaction(services) private inline fun isUpgrade() = isUpgradeFrom() and isUpgradeTo() diff --git a/core-tests/src/test/kotlin/net/corda/coretests/flows/WithFinality.kt b/core-tests/src/test/kotlin/net/corda/coretests/flows/WithFinality.kt index 714e8b85fa..127cc1ccf9 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/flows/WithFinality.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/flows/WithFinality.kt @@ -13,6 +13,7 @@ import net.corda.core.flows.ReceiveFinalityFlow import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party import net.corda.core.internal.FlowStateMachineHandle +import net.corda.core.internal.getRequiredTransaction import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.startFlow @@ -26,9 +27,7 @@ interface WithFinality : WithMockNet { return startFlowAndRunNetwork(FinalityInvoker(stx, recipients.toSet(), emptySet())) } - fun TestStartedNode.getValidatedTransaction(stx: SignedTransaction): SignedTransaction { - return services.validatedTransactions.getTransaction(stx.id)!! - } + fun TestStartedNode.getValidatedTransaction(stx: SignedTransaction): SignedTransaction = services.getRequiredTransaction(stx.id) fun CordaRPCOps.finalise(stx: SignedTransaction, vararg recipients: Party): FlowHandle { return startFlow(WithFinality::FinalityInvoker, stx, recipients.toSet(), emptySet()).andRunNetwork() diff --git a/core-tests/src/test/kotlin/net/corda/coretests/internal/verification/AttachmentFixupsTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/internal/verification/AttachmentFixupsTest.kt new file mode 100644 index 0000000000..67dc930fe7 --- /dev/null +++ b/core-tests/src/test/kotlin/net/corda/coretests/internal/verification/AttachmentFixupsTest.kt @@ -0,0 +1,137 @@ +package net.corda.coretests.internal.verification + +import net.corda.core.internal.verification.AttachmentFixups +import net.corda.core.node.services.AttachmentId +import net.corda.node.VersionInfo +import net.corda.node.internal.cordapp.JarScanningCordappLoader +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import java.net.URL +import java.nio.file.Files +import java.nio.file.Path +import java.util.jar.JarOutputStream +import java.util.zip.Deflater +import java.util.zip.ZipEntry +import kotlin.io.path.outputStream +import kotlin.test.assertFailsWith + +class AttachmentFixupsTest { + companion object { + @JvmField + val ID1 = AttachmentId.randomSHA256() + @JvmField + val ID2 = AttachmentId.randomSHA256() + @JvmField + val ID3 = AttachmentId.randomSHA256() + @JvmField + val ID4 = AttachmentId.randomSHA256() + } + + @Test(timeout=300_000) + fun `test fixup rule that adds attachment`() { + val fixupJar = Files.createTempFile("fixup", ".jar") + .writeFixupRules("$ID1 => $ID2, $ID3") + val fixedIDs = with(newFixupService(fixupJar.toUri().toURL())) { + fixupAttachmentIds(listOf(ID1)) + } + assertThat(fixedIDs).containsExactly(ID2, ID3) + } + + @Test(timeout=300_000) + fun `test fixup rule that deletes attachment`() { + val fixupJar = Files.createTempFile("fixup", ".jar") + .writeFixupRules("$ID1 =>") + val fixedIDs = with(newFixupService(fixupJar.toUri().toURL())) { + fixupAttachmentIds(listOf(ID1)) + } + assertThat(fixedIDs).isEmpty() + } + + @Test(timeout=300_000) + fun `test fixup rule with blank LHS`() { + val fixupJar = Files.createTempFile("fixup", ".jar") + .writeFixupRules(" => $ID2") + val ex = assertFailsWith { + newFixupService(fixupJar.toUri().toURL()) + } + assertThat(ex).hasMessageContaining( + "Forbidden empty list of source attachment IDs in '$fixupJar'" + ) + } + + @Test(timeout=300_000) + fun `test fixup rule without arrows`() { + val rule = " $ID1 " + val fixupJar = Files.createTempFile("fixup", ".jar") + .writeFixupRules(rule) + val ex = assertFailsWith { + newFixupService(fixupJar.toUri().toURL()) + } + assertThat(ex).hasMessageContaining( + "Invalid fix-up line '${rule.trim()}' in '$fixupJar'" + ) + } + + @Test(timeout=300_000) + fun `test fixup rule with too many arrows`() { + val rule = " $ID1 => $ID2 => $ID3 " + val fixupJar = Files.createTempFile("fixup", ".jar") + .writeFixupRules(rule) + val ex = assertFailsWith { + newFixupService(fixupJar.toUri().toURL()) + } + assertThat(ex).hasMessageContaining( + "Invalid fix-up line '${rule.trim()}' in '$fixupJar'" + ) + } + + @Test(timeout=300_000) + fun `test fixup file containing multiple rules and comments`() { + val fixupJar = Files.createTempFile("fixup", ".jar").writeFixupRules( + "# Whole line comment", + "\t$ID1,$ID2 => $ID2,, $ID3 # EOl comment", + " # Empty line with comment", + "", + "$ID3 => $ID4" + ) + val fixedIDs = with(newFixupService(fixupJar.toUri().toURL())) { + fixupAttachmentIds(listOf(ID2, ID1)) + } + assertThat(fixedIDs).containsExactlyInAnyOrder(ID2, ID4) + } + + private fun Path.writeFixupRules(vararg lines: String): Path { + JarOutputStream(outputStream()).use { jar -> + jar.setMethod(ZipEntry.DEFLATED) + jar.setLevel(Deflater.NO_COMPRESSION) + jar.putNextEntry(directoryEntry("META-INF")) + jar.putNextEntry(fileEntry("META-INF/Corda-Fixups")) + for (line in lines) { + jar.write(line.toByteArray()) + jar.write('\r'.code) + jar.write('\n'.code) + } + } + return this + } + + private fun directoryEntry(internalName: String): ZipEntry { + return ZipEntry("$internalName/").apply { + method = ZipEntry.STORED + compressedSize = 0 + size = 0 + crc = 0 + } + } + + private fun fileEntry(internalName: String): ZipEntry { + return ZipEntry(internalName).apply { + method = ZipEntry.DEFLATED + } + } + + private fun newFixupService(vararg urls: URL): AttachmentFixups { + val loader = JarScanningCordappLoader.fromJarUrls(urls.toList(), VersionInfo.UNKNOWN) + return AttachmentFixups().apply { load(loader.appClassLoader) } + } +} diff --git a/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderTest.kt index b48199ec47..f1887ed00c 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderTest.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderTest.kt @@ -1,7 +1,6 @@ package net.corda.coretests.transactions import net.corda.core.contracts.Command -import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.HashAttachmentConstraint import net.corda.core.contracts.PrivacySalt import net.corda.core.contracts.SignatureAttachmentConstraint @@ -10,45 +9,33 @@ import net.corda.core.contracts.StateRef import net.corda.core.contracts.TimeWindow import net.corda.core.contracts.TransactionState import net.corda.core.contracts.TransactionVerificationException.UnsupportedHashTypeException -import net.corda.core.cordapp.CordappProvider -import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.DigestService import net.corda.core.crypto.SecureHash -import net.corda.core.identity.Party -import net.corda.core.internal.AbstractAttachment import net.corda.core.internal.HashAgility import net.corda.core.internal.PLATFORM_VERSION import net.corda.core.internal.digestService -import net.corda.core.node.ServicesForResolution import net.corda.core.node.ZoneVersionTooLowException -import net.corda.core.node.services.AttachmentStorage -import net.corda.core.node.services.IdentityService -import net.corda.core.node.services.NetworkParametersService -import net.corda.core.serialization.serialize +import net.corda.core.serialization.internal._driverSerializationEnv import net.corda.core.transactions.TransactionBuilder -import net.corda.coretesting.internal.rigorousMock import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.BOB_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.DummyCommandData import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity +import net.corda.testing.node.MockNetwork +import net.corda.testing.node.MockNetworkParameters +import net.corda.testing.node.MockServices +import net.corda.testing.node.internal.cordappWithPackages import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.assertj.core.api.Assertions.assertThatThrownBy -import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue -import org.junit.Before import org.junit.Ignore import org.junit.Rule import org.junit.Test -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import org.mockito.kotlin.whenever -import java.security.PublicKey import java.time.Instant import kotlin.test.assertFailsWith @@ -58,33 +45,12 @@ class TransactionBuilderTest { val testSerialization = SerializationEnvironmentRule() private val notary = TestIdentity(DUMMY_NOTARY_NAME).party - private val services = rigorousMock() - private val contractAttachmentId = SecureHash.randomSHA256() - private val attachments = rigorousMock() - private val networkParametersService = mock() - - @Before - fun setup() { - val cordappProvider = rigorousMock() - val networkParameters = testNetworkParameters(minimumPlatformVersion = PLATFORM_VERSION) - doReturn(networkParametersService).whenever(services).networkParametersService - doReturn(networkParameters.serialize().hash).whenever(networkParametersService).currentHash - doReturn(cordappProvider).whenever(services).cordappProvider - doReturn(contractAttachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID) - doReturn(networkParameters).whenever(services).networkParameters - doReturn(mock()).whenever(services).identityService - - val attachmentStorage = rigorousMock() - doReturn(attachmentStorage).whenever(services).attachments - val attachment = rigorousMock() - doReturn(attachment).whenever(attachmentStorage).openAttachment(contractAttachmentId) - doReturn(contractAttachmentId).whenever(attachment).id - doReturn(setOf(DummyContract.PROGRAM_ID)).whenever(attachment).allContracts - doReturn("app").whenever(attachment).uploader - doReturn(emptyList()).whenever(attachment).signerKeys - doReturn(listOf(contractAttachmentId)).whenever(attachmentStorage) - .getLatestContractAttachments("net.corda.testing.contracts.DummyContract") - } + private val services = MockServices( + listOf("net.corda.testing.contracts"), + TestIdentity(ALICE_NAME), + testNetworkParameters(minimumPlatformVersion = PLATFORM_VERSION) + ) + private val contractAttachmentId = services.attachments.getLatestContractAttachments(DummyContract.PROGRAM_ID)[0] @Test(timeout=300_000) fun `bare minimum issuance tx`() { @@ -100,13 +66,11 @@ class TransactionBuilderTest { val wtx = builder.toWireTransaction(services) assertThat(wtx.outputs).containsOnly(outputState) assertThat(wtx.commands).containsOnly(Command(DummyCommandData, notary.owningKey)) - assertThat(wtx.networkParametersHash).isEqualTo(networkParametersService.currentHash) + assertThat(wtx.networkParametersHash).isEqualTo(services.networkParametersService.currentHash) } @Test(timeout=300_000) fun `automatic hash constraint`() { - doReturn(unsignedAttachment).whenever(attachments).openAttachment(contractAttachmentId) - val outputState = TransactionState(data = DummyState(), contract = DummyContract.PROGRAM_ID, notary = notary) val builder = TransactionBuilder() .addOutputState(outputState) @@ -117,8 +81,6 @@ class TransactionBuilderTest { @Test(timeout=300_000) fun `reference states`() { - doReturn(unsignedAttachment).whenever(attachments).openAttachment(contractAttachmentId) - val referenceState = TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary) val referenceStateRef = StateRef(SecureHash.randomSHA256(), 1) val builder = TransactionBuilder(notary) @@ -126,54 +88,55 @@ class TransactionBuilderTest { .addOutputState(TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary)) .addCommand(DummyCommandData, notary.owningKey) - doReturn(testNetworkParameters(minimumPlatformVersion = 3)).whenever(services).networkParameters - assertThatThrownBy { builder.toWireTransaction(services) } - .isInstanceOf(ZoneVersionTooLowException::class.java) - .hasMessageContaining("Reference states") + with(testNetworkParameters(minimumPlatformVersion = 3)) { + val services = MockServices(listOf("net.corda.testing.contracts"), TestIdentity(ALICE_NAME), this) + assertThatThrownBy { builder.toWireTransaction(services) } + .isInstanceOf(ZoneVersionTooLowException::class.java) + .hasMessageContaining("Reference states") + } - doReturn(testNetworkParameters(minimumPlatformVersion = 4)).whenever(services).networkParameters - doReturn(referenceState).whenever(services).loadState(referenceStateRef) - val wtx = builder.toWireTransaction(services) - assertThat(wtx.references).containsOnly(referenceStateRef) + with(testNetworkParameters(minimumPlatformVersion = 4)) { + val services = MockServices(listOf("net.corda.testing.contracts"), TestIdentity(ALICE_NAME), this) + val wtx = builder.toWireTransaction(services) + assertThat(wtx.references).containsOnly(referenceStateRef) + } } @Test(timeout=300_000) fun `automatic signature constraint`() { - val aliceParty = TestIdentity(ALICE_NAME).party - val bobParty = TestIdentity(BOB_NAME).party - val compositeKey = CompositeKey.Builder().addKeys(aliceParty.owningKey, bobParty.owningKey).build() - val expectedConstraint = SignatureAttachmentConstraint(compositeKey) - val signedAttachment = signedAttachment(aliceParty, bobParty) + // We need to use a MockNetwork so that we can create a signed attachment. However, SerializationEnvironmentRule and MockNetwork + // don't work well together, so we temporarily clear out the driverSerializationEnv for this test. + val driverSerializationEnv = _driverSerializationEnv.get() + _driverSerializationEnv.set(null) + val mockNetwork = MockNetwork( + MockNetworkParameters( + networkParameters = testNetworkParameters(minimumPlatformVersion = PLATFORM_VERSION), + cordappsForAllNodes = listOf(cordappWithPackages("net.corda.testing.contracts").signed()) + ) + ) - assertTrue(expectedConstraint.isSatisfiedBy(signedAttachment)) - assertFalse(expectedConstraint.isSatisfiedBy(unsignedAttachment)) + try { + val services = mockNetwork.notaryNodes[0].services - doReturn(attachments).whenever(services).attachments - doReturn(signedAttachment).whenever(attachments).openAttachment(contractAttachmentId) - doReturn(listOf(contractAttachmentId)).whenever(attachments) - .getLatestContractAttachments("net.corda.testing.contracts.DummyContract") + val attachment = services.attachments.openAttachment(services.attachments.getLatestContractAttachments(DummyContract.PROGRAM_ID)[0]) + val attachmentSigner = attachment!!.signerKeys.single() - val outputState = TransactionState(data = DummyState(), contract = DummyContract.PROGRAM_ID, notary = notary) - val builder = TransactionBuilder() - .addOutputState(outputState) - .addCommand(DummyCommandData, notary.owningKey) - val wtx = builder.toWireTransaction(services) + val expectedConstraint = SignatureAttachmentConstraint(attachmentSigner) + assertTrue(expectedConstraint.isSatisfiedBy(attachment)) - assertThat(wtx.outputs).containsOnly(outputState.copy(constraint = expectedConstraint)) + val outputState = TransactionState(data = DummyState(), contract = DummyContract.PROGRAM_ID, notary = notary) + val builder = TransactionBuilder() + .addOutputState(outputState) + .addCommand(DummyCommandData, notary.owningKey) + val wtx = builder.toWireTransaction(services) + + assertThat(wtx.outputs).containsOnly(outputState.copy(constraint = expectedConstraint)) + } finally { + mockNetwork.stopNodes() + _driverSerializationEnv.set(driverSerializationEnv) + } } - private val unsignedAttachment = ContractAttachment(object : AbstractAttachment({ byteArrayOf() }, "test") { - override val id: SecureHash get() = throw UnsupportedOperationException() - - override val signerKeys: List get() = emptyList() - }, DummyContract.PROGRAM_ID) - - private fun signedAttachment(vararg parties: Party) = ContractAttachment.create(object : AbstractAttachment({ byteArrayOf() }, "test") { - override val id: SecureHash get() = contractAttachmentId - - override val signerKeys: List get() = parties.map { it.owningKey } - }, DummyContract.PROGRAM_ID, signerKeys = parties.map { it.owningKey }) - @Test(timeout=300_000) fun `list accessors are mutable copies`() { val inputState1 = TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary) diff --git a/core/build.gradle b/core/build.gradle index 9916f83039..f2fe5c1ea1 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -40,9 +40,6 @@ dependencies { // Hamkrest, for fluent, composable matchers testImplementation "com.natpryce:hamkrest:$hamkrest_version" - // Thread safety annotations - implementation "com.google.code.findbugs:jsr305:$jsr305_version" - // SLF4J: commons-logging bindings for a SLF4J back end implementation "org.slf4j:jcl-over-slf4j:$slf4j_version" implementation "org.slf4j:slf4j-api:$slf4j_version" @@ -66,10 +63,7 @@ dependencies { // Bouncy castle support needed for X509 certificate manipulation implementation "org.bouncycastle:bcprov-jdk18on:${bouncycastle_version}" - implementation "org.bouncycastle:bcpkix-jdk18on:${bouncycastle_version}" - - // JPA 2.2 annotations. - implementation "javax.persistence:javax.persistence-api:2.2" + testImplementation "org.bouncycastle:bcpkix-jdk18on:${bouncycastle_version}" // required to use @Type annotation implementation "org.hibernate:hibernate-core:$hibernate_version" diff --git a/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt b/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt index 0b56707ee1..24852fad3c 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ContractAttachment.kt @@ -27,6 +27,7 @@ class ContractAttachment private constructor( companion object { @CordaInternal + @JvmSynthetic fun create(attachment: Attachment, contract: ContractClassName, additionalContracts: Set = emptySet(), diff --git a/core/src/main/kotlin/net/corda/core/crypto/internal/DigestAlgorithmFactory.kt b/core/src/main/kotlin/net/corda/core/crypto/internal/DigestAlgorithmFactory.kt index 892506aa76..a2f2396607 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/internal/DigestAlgorithmFactory.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/internal/DigestAlgorithmFactory.kt @@ -1,10 +1,10 @@ package net.corda.core.crypto.internal import net.corda.core.crypto.DigestAlgorithm -import java.lang.reflect.Constructor +import net.corda.core.internal.loadClassOfType import java.security.MessageDigest import java.security.NoSuchAlgorithmException -import java.util.* +import java.util.Collections import java.util.concurrent.ConcurrentHashMap sealed class DigestAlgorithmFactory { @@ -28,9 +28,8 @@ sealed class DigestAlgorithmFactory { } private class CustomAlgorithmFactory(className: String) : DigestAlgorithmFactory() { - val constructor: Constructor = Class.forName(className, false, javaClass.classLoader) - .asSubclass(DigestAlgorithm::class.java) - .getConstructor() + private val constructor = loadClassOfType(className, false, javaClass.classLoader).getConstructor() + override val algorithm: String = constructor.newInstance().algorithm override fun create(): DigestAlgorithm { diff --git a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt index 173107dd5f..91bc460bce 100644 --- a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt @@ -11,6 +11,8 @@ import net.corda.core.internal.NetworkParametersStorage import net.corda.core.internal.PlatformVersionSwitches import net.corda.core.internal.RetrieveAnyTransactionPayload import net.corda.core.internal.ServiceHubCoreInternal +import net.corda.core.internal.getRequiredTransaction +import net.corda.core.internal.mapToSet import net.corda.core.internal.readFully import net.corda.core.node.ServicesForResolution import net.corda.core.node.StatesToRecord @@ -169,13 +171,10 @@ open class DataVendingFlow(val otherSessions: Set, val payload: Any is SignedTransaction -> TransactionAuthorisationFilter().addAuthorised(getInputTransactions(payload)) is RetrieveAnyTransactionPayload -> TransactionAuthorisationFilter(acceptAll = true) is List<*> -> TransactionAuthorisationFilter().addAuthorised(payload.flatMap { someObject -> - if (someObject is StateAndRef<*>) { - getInputTransactions(serviceHub.validatedTransactions.getTransaction(someObject.ref.txhash)!!) + someObject.ref.txhash - } - else if (someObject is NamedByHash) { - setOf(someObject.id) - } else { - throw Exception("Unknown payload type: ${someObject!!::class.java} ?") + when (someObject) { + is StateAndRef<*> -> getInputTransactions(serviceHub.getRequiredTransaction(someObject.ref.txhash)) + someObject.ref.txhash + is NamedByHash -> setOf(someObject.id) + else -> throw Exception("Unknown payload type: ${someObject!!::class.java} ?") } }.toSet()) else -> throw Exception("Unknown payload type: ${payload::class.java} ?") @@ -308,7 +307,7 @@ open class DataVendingFlow(val otherSessions: Set, val payload: Any @Suspendable private fun getInputTransactions(tx: SignedTransaction): Set { - return tx.inputs.map { it.txhash }.toSet() + tx.references.map { it.txhash }.toSet() + return tx.inputs.mapToSet { it.txhash } + tx.references.mapToSet { it.txhash } } private class TransactionAuthorisationFilter(private val authorisedTransactions: MutableSet = mutableSetOf(), val acceptAll: Boolean = false) { diff --git a/core/src/main/kotlin/net/corda/core/internal/ClassLoadingUtils.kt b/core/src/main/kotlin/net/corda/core/internal/ClassLoadingUtils.kt index 2e80d05eb0..4b1fe0b291 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ClassLoadingUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ClassLoadingUtils.kt @@ -21,8 +21,7 @@ import net.corda.core.serialization.internal.AttachmentURLStreamHandlerFactory.a fun createInstancesOfClassesImplementing(classloader: ClassLoader, clazz: Class, classVersionRange: IntRange? = null): Set { return getNamesOfClassesImplementing(classloader, clazz, classVersionRange) - .map { Class.forName(it, false, classloader).asSubclass(clazz) } - .mapTo(LinkedHashSet()) { it.kotlin.objectOrNewInstance() } + .mapToSet { loadClassOfType(clazz, it, false, classloader).kotlin.objectOrNewInstance() } } /** @@ -56,10 +55,23 @@ fun getNamesOfClassesImplementing(classloader: ClassLoader, clazz: Clas } result.getClassesImplementing(clazz.name) .filterNot(ClassInfo::isAbstract) - .mapTo(LinkedHashSet(), ClassInfo::getName) + .mapToSet(ClassInfo::getName) } } +/** + * @throws ClassNotFoundException + * @throws ClassCastException + * @see Class.forName + */ +inline fun loadClassOfType(className: String, initialize: Boolean = true, classLoader: ClassLoader? = null): Class { + return loadClassOfType(T::class.java, className, initialize, classLoader) +} + +fun loadClassOfType(type: Class, className: String, initialize: Boolean = true, classLoader: ClassLoader? = null): Class { + return Class.forName(className, initialize, classLoader).asSubclass(type) +} + fun executeWithThreadContextClassLoader(classloader: ClassLoader, fn: () -> T): T { val threadClassLoader = Thread.currentThread().contextClassLoader try { @@ -68,5 +80,4 @@ fun executeWithThreadContextClassLoader(classloader: ClassLoader, fn: } finally { Thread.currentThread().contextClassLoader = threadClassLoader } - } diff --git a/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt b/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt index 9f315a778d..9531bd512e 100644 --- a/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt @@ -1,29 +1,22 @@ @file:Suppress("TooManyFunctions") package net.corda.core.internal -import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractClassName -import net.corda.core.cordapp.CordappProvider +import net.corda.core.contracts.TransactionResolutionException +import net.corda.core.crypto.SecureHash import net.corda.core.flows.DataVendingFlow import net.corda.core.flows.FlowLogic import net.corda.core.node.NetworkParameters +import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution import net.corda.core.node.ZoneVersionTooLowException -import net.corda.core.node.services.AttachmentId -import net.corda.core.node.services.AttachmentStorage -import net.corda.core.node.services.vault.AttachmentQueryCriteria -import net.corda.core.node.services.vault.AttachmentSort -import net.corda.core.node.services.vault.Builder -import net.corda.core.node.services.vault.Sort import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializationContext -import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction import org.slf4j.MDC import java.security.PublicKey -import java.util.jar.JarInputStream // *Internal* Corda-specific utilities. @@ -68,11 +61,6 @@ fun TransactionBuilder.toWireTransaction(services: ServicesForResolution, serial return toWireTransactionWithContext(services, serializationContext) } -/** Provide access to internal method for AttachmentClassLoaderTests. */ -fun TransactionBuilder.toLedgerTransaction(services: ServicesForResolution, serializationContext: SerializationContext): LedgerTransaction { - return toLedgerTransactionWithContext(services, serializationContext) -} - /** Checks if this flow is an idempotent flow. */ fun Class>.isIdempotentFlow(): Boolean { return IdempotentFlow::class.java.isAssignableFrom(this) @@ -125,40 +113,6 @@ fun noPackageOverlap(packages: Collection): Boolean { return packages.all { outer -> packages.none { inner -> inner != outer && inner.startsWith("$outer.") } } } -/** - * @return The set of [AttachmentId]s after the node's fix-up rules have been applied to [attachmentIds]. - */ -fun CordappProvider.internalFixupAttachmentIds(attachmentIds: Collection): Set { - return (this as CordappFixupInternal).fixupAttachmentIds(attachmentIds) -} - -/** - * Scans trusted (installed locally) attachments to find all that contain the [className]. - * This is required as a workaround until explicit cordapp dependencies are implemented. - * DO NOT USE IN CLIENT code. - * - * @return the attachments with the highest version. - * - * TODO: Should throw when the class is found in multiple contract attachments (not different versions). - */ -fun AttachmentStorage.internalFindTrustedAttachmentForClass(className: String): Attachment? { - val allTrusted = queryAttachments( - AttachmentQueryCriteria.AttachmentsQueryCriteria().withUploader(Builder.`in`(TRUSTED_UPLOADERS)), - AttachmentSort(listOf(AttachmentSort.AttachmentSortColumn(AttachmentSort.AttachmentSortAttribute.VERSION, Sort.Direction.DESC)))) - - // TODO - add caching if performance is affected. - for (attId in allTrusted) { - val attch = openAttachment(attId)!! - if (attch.openAsJAR().use { hasFile(it, "$className.class") }) return attch - } - return null -} - -private fun hasFile(jarStream: JarInputStream, className: String): Boolean { - while (true) { - val e = jarStream.nextJarEntry ?: return false - if (e.name == className) { - return true - } - } +fun ServiceHub.getRequiredTransaction(txhash: SecureHash): SignedTransaction { + return validatedTransactions.getTransaction(txhash) ?: throw TransactionResolutionException(txhash) } diff --git a/core/src/main/kotlin/net/corda/core/internal/CordappFixupInternal.kt b/core/src/main/kotlin/net/corda/core/internal/CordappFixupInternal.kt deleted file mode 100644 index e1ac4e22c6..0000000000 --- a/core/src/main/kotlin/net/corda/core/internal/CordappFixupInternal.kt +++ /dev/null @@ -1,7 +0,0 @@ -package net.corda.core.internal - -import net.corda.core.node.services.AttachmentId - -interface CordappFixupInternal { - fun fixupAttachmentIds(attachmentIds: Collection): Set -} diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index 7ad05fe75c..9f227123ae 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -23,7 +23,6 @@ import rx.subjects.UnicastSubject import java.io.ByteArrayOutputStream import java.io.IOException import java.io.InputStream -import java.io.OutputStream import java.lang.reflect.Field import java.lang.reflect.Member import java.lang.reflect.Modifier @@ -138,10 +137,34 @@ fun List.randomOrNull(): T? { /** Returns the index of the given item or throws [IllegalArgumentException] if not found. */ fun List.indexOfOrThrow(item: T): Int { val i = indexOf(item) - require(i != -1){"No such element"} + require(i != -1) { "No such element" } return i } +/** + * Similar to [Iterable.map] except it maps to a [Set] which preserves the iteration order. + */ +inline fun Iterable.mapToSet(transform: (T) -> R): Set { + if (this is Collection) { + when (size) { + 0 -> return emptySet() + 1 -> return setOf(transform(first())) + } + } + return mapTo(LinkedHashSet(), transform) +} + +/** + * Similar to [Iterable.flatMap] except it maps to a [Set] which preserves the iteration order. + */ +inline fun Iterable.flatMapToSet(transform: (T) -> Iterable): Set { + return if (this is Collection && isEmpty()) { + emptySet() + } else { + flatMapTo(LinkedHashSet(), transform) + } +} + fun InputStream.copyTo(target: Path, vararg options: CopyOption): Long = Files.copy(this, target, *options) /** Same as [InputStream.readBytes] but also closes the stream. */ @@ -165,12 +188,6 @@ fun InputStream.hash(): SecureHash { inline fun InputStream.readObject(): T = readFully().deserialize() -object NullOutputStream : OutputStream() { - override fun write(b: Int) = Unit - override fun write(b: ByteArray) = Unit - override fun write(b: ByteArray, off: Int, len: Int) = Unit -} - fun String.abbreviate(maxWidth: Int): String = if (length <= maxWidth) this else take(maxWidth - 1) + "…" /** Return the sum of an Iterable of [BigDecimal]s. */ @@ -532,11 +549,6 @@ fun ByteBuffer.copyBytes(): ByteArray = ByteArray(remaining()).also { get(it) } val PublicKey.hash: SecureHash get() = Crypto.encodePublicKey(this).sha256() -/** - * Extension method for providing a sumBy method that processes and returns a Long - */ -fun Iterable.sumByLong(selector: (T) -> Long): Long = this.map { selector(it) }.sum() - fun SerializedBytes.checkPayloadIs(type: Class): UntrustworthyData { val payloadData: T = try { val serializer = SerializationDefaults.SERIALIZATION_FACTORY @@ -563,6 +575,10 @@ fun MutableMap.toSynchronised(): MutableMap = Collections.syn /** @see Collections.synchronizedSet */ fun MutableSet.toSynchronised(): MutableSet = Collections.synchronizedSet(this) +fun Collection<*>.equivalent(other: Collection<*>): Boolean { + return this.size == other.size && this.containsAll(other) && other.containsAll(this) +} + /** * List implementation that applies the expensive [transform] function only when the element is accessed and caches calculated values. * Size is very cheap as it doesn't call [transform]. diff --git a/core/src/main/kotlin/net/corda/core/internal/NamedCache.kt b/core/src/main/kotlin/net/corda/core/internal/NamedCache.kt index 82c1c35b66..d192557345 100644 --- a/core/src/main/kotlin/net/corda/core/internal/NamedCache.kt +++ b/core/src/main/kotlin/net/corda/core/internal/NamedCache.kt @@ -9,18 +9,24 @@ import com.github.benmanes.caffeine.cache.LoadingCache * Allow extra functionality to be injected to our caches. */ interface NamedCacheFactory { + companion object { + private val allowedChars = Regex("""^[0-9A-Za-z_.]*$""") + } + /** * Restrict the allowed characters of a cache name - this ensures that each cache has a name, and that * the name can be used to create a file name or a metric name. */ fun checkCacheName(name: String) { - require(!name.isBlank()){"Name must not be empty or only whitespace"} - require(allowedChars.matches(name)){"Invalid characters in cache name"} + require(name.isNotBlank()) { "Name must not be empty or only whitespace" } + require(allowedChars.matches(name)) { "Invalid characters in cache name" } } + fun buildNamed(name: String): Cache = buildNamed(Caffeine.newBuilder(), name) + fun buildNamed(caffeine: Caffeine, name: String): Cache + fun buildNamed(name: String, loader: CacheLoader): LoadingCache = buildNamed(Caffeine.newBuilder(), name, loader) + fun buildNamed(caffeine: Caffeine, name: String, loader: CacheLoader): LoadingCache } - -private val allowedChars = Regex("^[0-9A-Za-z_.]*\$") diff --git a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt index bd7c1142ac..84ba452deb 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ServiceHubCoreInternal.kt @@ -6,19 +6,15 @@ import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.TransactionMetadata import net.corda.core.identity.CordaX500Name import net.corda.core.internal.notary.NotaryService -import net.corda.core.node.ServiceHub +import net.corda.core.internal.verification.VerifyingServiceHub import net.corda.core.node.StatesToRecord -import net.corda.core.serialization.internal.AttachmentsClassLoaderCache import net.corda.core.transactions.SignedTransaction import java.util.concurrent.ExecutorService // TODO: This should really be called ServiceHubInternal but that name is already taken by net.corda.node.services.api.ServiceHubInternal. -interface ServiceHubCoreInternal : ServiceHub { - +interface ServiceHubCoreInternal : VerifyingServiceHub { val externalOperationExecutor: ExecutorService - val attachmentTrustCalculator: AttachmentTrustCalculator - /** * Optional `NotaryService` which will be `null` for all non-Notary nodes. */ @@ -26,8 +22,6 @@ interface ServiceHubCoreInternal : ServiceHub { fun createTransactionsResolver(flow: ResolveTransactionsFlow): TransactionsResolver - val attachmentsClassLoaderCache: AttachmentsClassLoaderCache - /** * Stores [SignedTransaction] and participant signatures without the notary signature in the local transaction storage, * inclusive of flow recovery metadata. diff --git a/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt b/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt index 41e94a236a..a6aa9c2ac4 100644 --- a/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt @@ -172,11 +172,13 @@ fun createComponentGroups(inputs: List, return componentGroupMap } +typealias SerializedTransactionState = SerializedBytes> + /** * A SerializedStateAndRef is a pair (BinaryStateRepresentation, StateRef). * The [serializedState] is the actual component from the original wire transaction. */ -data class SerializedStateAndRef(val serializedState: SerializedBytes>, val ref: StateRef) { +data class SerializedStateAndRef(val serializedState: SerializedTransactionState, val ref: StateRef) { fun toStateAndRef(factory: SerializationFactory, context: SerializationContext) = StateAndRef(serializedState.deserialize(factory, context), ref) fun toStateAndRef(): StateAndRef { val factory = SerializationFactory.defaultFactory diff --git a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappProviderInternal.kt b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappProviderInternal.kt new file mode 100644 index 0000000000..5b94f2571a --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappProviderInternal.kt @@ -0,0 +1,13 @@ +package net.corda.core.internal.cordapp + +import net.corda.core.cordapp.Cordapp +import net.corda.core.cordapp.CordappProvider +import net.corda.core.flows.FlowLogic +import net.corda.core.node.services.AttachmentId + +interface CordappProviderInternal : CordappProvider { + val appClassLoader: ClassLoader + val cordapps: List + fun getCordappForFlow(flowLogic: FlowLogic<*>): Cordapp? + fun fixupAttachmentIds(attachmentIds: Collection): Set +} diff --git a/core/src/main/kotlin/net/corda/core/internal/verification/AttachmentFixups.kt b/core/src/main/kotlin/net/corda/core/internal/verification/AttachmentFixups.kt new file mode 100644 index 0000000000..4e20a46d41 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/verification/AttachmentFixups.kt @@ -0,0 +1,79 @@ +package net.corda.core.internal.verification + +import net.corda.core.crypto.SecureHash +import net.corda.core.internal.mapNotNull +import net.corda.core.node.services.AttachmentFixup +import net.corda.core.node.services.AttachmentId +import net.corda.core.node.services.AttachmentStorage +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.loggerFor +import java.net.JarURLConnection +import java.net.URL + +class AttachmentFixups { + private val fixupRules = ArrayList() + + /** + * Loads the "fixup" rules from all META-INF/Corda-Fixups files. + * These files have the following format: + * ,...=>,,... + * where each is the SHA256 of a CorDapp JAR that [TransactionBuilder] will expect to find inside [AttachmentStorage]. + * + * These rules are for repairing broken CorDapps. A correctly written CorDapp should not require them. + */ + fun load(appClassLoader: ClassLoader) { + for (url in appClassLoader.resources("META-INF/Corda-Fixups")) { + val connection = toValidFixupResource(url) ?: continue + connection.inputStream.bufferedReader().lines().use { lines -> + lines.mapNotNull(::cleanLine).forEach { line -> + val tokens = line.split("=>") + require(tokens.size == 2) { + "Invalid fix-up line '$line' in '${connection.jarFile.name}'" + } + val sourceIds = parseIds(tokens[0]) + require(sourceIds.isNotEmpty()) { + "Forbidden empty list of source attachment IDs in '${connection.jarFile.name}'" + } + val targetIds = parseIds(tokens[1]) + fixupRules += AttachmentFixup(sourceIds, targetIds) + } + } + } + } + + private fun toValidFixupResource(url: URL): JarURLConnection? { + val connection = url.openConnection() as? JarURLConnection ?: return null + val isValid = connection.jarFile.stream().allMatch { it.name.startsWith("META-INF/") } + if (!isValid) { + loggerFor().warn("FixUp '{}' contains files outside META-INF/ - IGNORING!", connection.jarFile.name) + return null + } + return connection + } + + private fun cleanLine(line: String): String? = line.substringBefore('#').trim().takeIf(String::isNotEmpty) + + private fun parseIds(ids: String): Set { + return ids.splitToSequence(",") + .map(String::trim) + .filterNot(String::isEmpty) + .mapTo(LinkedHashSet(), SecureHash.Companion::create) + } + + /** + * Apply this node's attachment fix-up rules to the given attachment IDs. + * + * @param attachmentIds A collection of [AttachmentId]s, e.g. as provided by a transaction. + * @return The [attachmentIds] with the fix-up rules applied. + */ + fun fixupAttachmentIds(attachmentIds: Collection): Set { + val replacementIds = LinkedHashSet(attachmentIds) + for ((sourceIds, targetIds) in fixupRules) { + if (replacementIds.containsAll(sourceIds)) { + replacementIds.removeAll(sourceIds) + replacementIds.addAll(targetIds) + } + } + return replacementIds + } +} diff --git a/core/src/main/kotlin/net/corda/core/internal/verification/VerificationSupport.kt b/core/src/main/kotlin/net/corda/core/internal/verification/VerificationSupport.kt new file mode 100644 index 0000000000..5d1ea265bb --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/verification/VerificationSupport.kt @@ -0,0 +1,50 @@ +package net.corda.core.internal.verification + +import net.corda.core.contracts.Attachment +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.StateRef +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.Party +import net.corda.core.internal.SerializedTransactionState +import net.corda.core.node.NetworkParameters +import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.internal.AttachmentsClassLoaderCache +import net.corda.core.transactions.LedgerTransaction +import net.corda.core.transactions.defaultVerifier +import java.security.PublicKey + +/** + * Represents the operations required to resolve and verify a transaction. + */ +interface VerificationSupport { + val isResolutionLazy: Boolean get() = true + + val appClassLoader: ClassLoader + + val attachmentsClassLoaderCache: AttachmentsClassLoaderCache? get() = null + + // TODO Use SequencedCollection if upgraded to Java 21 + fun getParties(keys: Collection): List + + fun getAttachment(id: SecureHash): Attachment? + + // TODO Use SequencedCollection if upgraded to Java 21 + fun getAttachments(ids: Collection): List = ids.map(::getAttachment) + + fun isAttachmentTrusted(attachment: Attachment): Boolean + + fun getTrustedClassAttachment(className: String): Attachment? + + fun getNetworkParameters(id: SecureHash?): NetworkParameters? + + fun getSerializedState(stateRef: StateRef): SerializedTransactionState + + fun getStateAndRef(stateRef: StateRef): StateAndRef<*> = StateAndRef(getSerializedState(stateRef).deserialize(), stateRef) + + fun fixupAttachmentIds(attachmentIds: Collection): Set + + fun createVerifier(ltx: LedgerTransaction, serializationContext: SerializationContext): Verifier { + return defaultVerifier(ltx, serializationContext) + } +} diff --git a/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt b/core/src/main/kotlin/net/corda/core/internal/verification/Verifier.kt similarity index 95% rename from core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt rename to core/src/main/kotlin/net/corda/core/internal/verification/Verifier.kt index 840163238f..f1e55acc80 100644 --- a/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/verification/Verifier.kt @@ -1,7 +1,5 @@ -package net.corda.core.internal +package net.corda.core.internal.verification -import net.corda.core.concurrent.CordaFuture -import net.corda.core.contracts.Attachment import net.corda.core.contracts.Contract import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.ContractClassName @@ -30,21 +28,26 @@ import net.corda.core.contracts.TransactionVerificationException.TransactionNota import net.corda.core.contracts.TransactionVerificationException.TransactionRequiredContractUnspecifiedException import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.SecureHash +import net.corda.core.internal.AttachmentWithContext +import net.corda.core.internal.MAX_NUMBER_OF_KEYS_IN_SIGNATURE_CONSTRAINT +import net.corda.core.internal.PlatformVersionSwitches +import net.corda.core.internal.canBeTransitionedFrom +import net.corda.core.internal.checkConstraintValidity +import net.corda.core.internal.checkMinimumPlatformVersion +import net.corda.core.internal.checkNotaryWhitelisted +import net.corda.core.internal.checkSupportedHashType +import net.corda.core.internal.contractHasAutomaticConstraintPropagation +import net.corda.core.internal.loadClassOfType +import net.corda.core.internal.mapToSet +import net.corda.core.internal.requiredContractClassName import net.corda.core.internal.rules.StateContractValidationEnforcementRule +import net.corda.core.internal.warnContractWithoutConstraintPropagation +import net.corda.core.internal.warnOnce import net.corda.core.transactions.LedgerTransaction import net.corda.core.utilities.loggerFor import java.util.function.Function import java.util.function.Supplier -interface TransactionVerifierServiceInternal { - fun reverifyWithFixups(transaction: LedgerTransaction, missingClass: String?): CordaFuture<*> -} - -/** - * Defined here for visibility reasons. - */ -fun LedgerTransaction.prepareVerify(attachments: List) = internalPrepareVerify(attachments) - interface Verifier { /** @@ -142,10 +145,12 @@ private class Validator(private val ltx: LedgerTransaction, private val transact */ @Suppress("ThrowsCount") private fun getUniqueContractAttachmentsByContract(): Map { - val contractClasses = allStates.mapTo(LinkedHashSet(), TransactionState<*>::contract) + val contractClasses = allStates.mapToSet { it.contract } // Check that there are no duplicate attachments added. - if (ltx.attachments.size != ltx.attachments.toSet().size) throw DuplicateAttachmentsRejection(ltx.id, ltx.attachments.groupBy { it }.filterValues { it.size > 1 }.keys.first()) + if (ltx.attachments.size != ltx.attachments.toSet().size) { + throw DuplicateAttachmentsRejection(ltx.id, ltx.attachments.groupBy { it }.filterValues { it.size > 1 }.keys.first()) + } // For each attachment this finds all the relevant state contracts that it provides. // And then maps them to the attachment. @@ -393,7 +398,7 @@ private class Validator(private val ltx: LedgerTransaction, private val transact @Suppress("NestedBlockDepth", "MagicNumber") private fun verifyConstraints(contractAttachmentsByContract: Map) { // For each contract/constraint pair check that the relevant attachment is valid. - allStates.mapTo(LinkedHashSet()) { it.contract to it.constraint }.forEach { (contract, constraint) -> + allStates.mapToSet { it.contract to it.constraint }.forEach { (contract, constraint) -> if (constraint is SignatureAttachmentConstraint) { /** * Support for signature constraints has been added on @@ -440,7 +445,7 @@ class TransactionVerifier(private val transactionClassLoader: ClassLoader) : Fun // Loads the contract class from the transactionClassLoader. private fun createContractClass(id: SecureHash, contractClassName: ContractClassName): Class { return try { - Class.forName(contractClassName, false, transactionClassLoader).asSubclass(Contract::class.java) + loadClassOfType(contractClassName, false, transactionClassLoader) } catch (e: Exception) { throw ContractCreationError(id, contractClassName, e) } @@ -448,7 +453,7 @@ class TransactionVerifier(private val transactionClassLoader: ClassLoader) : Fun private fun generateContracts(ltx: LedgerTransaction): List { return (ltx.inputs.map(StateAndRef::state) + ltx.outputs) - .mapTo(LinkedHashSet(), TransactionState<*>::contract) + .mapToSet { it.contract } .map { contractClassName -> createContractClass(ltx.id, contractClassName) }.map { contractClass -> diff --git a/core/src/main/kotlin/net/corda/core/internal/verification/VerifyingServiceHub.kt b/core/src/main/kotlin/net/corda/core/internal/verification/VerifyingServiceHub.kt new file mode 100644 index 0000000000..eba81ca2dc --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/verification/VerifyingServiceHub.kt @@ -0,0 +1,221 @@ +package net.corda.core.internal.verification + +import net.corda.core.contracts.Attachment +import net.corda.core.contracts.AttachmentResolutionException +import net.corda.core.contracts.ComponentGroupEnum +import net.corda.core.contracts.ContractAttachment +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionResolutionException +import net.corda.core.contracts.TransactionState +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.Party +import net.corda.core.internal.AttachmentTrustCalculator +import net.corda.core.internal.SerializedTransactionState +import net.corda.core.internal.TRUSTED_UPLOADERS +import net.corda.core.internal.cordapp.CordappProviderInternal +import net.corda.core.internal.getRequiredTransaction +import net.corda.core.node.NetworkParameters +import net.corda.core.node.ServiceHub +import net.corda.core.node.ServicesForResolution +import net.corda.core.node.services.vault.AttachmentQueryCriteria +import net.corda.core.node.services.vault.AttachmentSort +import net.corda.core.node.services.vault.AttachmentSort.AttachmentSortAttribute +import net.corda.core.node.services.vault.Builder +import net.corda.core.node.services.vault.Sort +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder +import net.corda.core.serialization.serialize +import net.corda.core.transactions.ContractUpgradeLedgerTransaction.Companion.loadUpgradedContract +import net.corda.core.transactions.ContractUpgradeWireTransaction +import net.corda.core.transactions.ContractUpgradeWireTransaction.Companion.calculateUpgradedState +import net.corda.core.transactions.MissingContractAttachments +import net.corda.core.transactions.NotaryChangeLedgerTransaction +import net.corda.core.transactions.NotaryChangeWireTransaction +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.WireTransaction +import java.security.PublicKey +import java.util.jar.JarInputStream + +@Suppress("TooManyFunctions", "ThrowsCount") +interface VerifyingServiceHub : ServiceHub, VerificationSupport { + override val cordappProvider: CordappProviderInternal + + val attachmentTrustCalculator: AttachmentTrustCalculator + + override val appClassLoader: ClassLoader get() = cordappProvider.appClassLoader + + override fun loadContractAttachment(stateRef: StateRef): Attachment { + // We may need to recursively chase transactions if there are notary changes. + return loadContractAttachment(stateRef, null) + } + + private fun loadContractAttachment(stateRef: StateRef, forContractClassName: String?): Attachment { + val stx = getRequiredTransaction(stateRef.txhash) + return when (val ctx = stx.coreTransaction) { + is WireTransaction -> { + val contractClassName = forContractClassName ?: ctx.outRef(stateRef.index).state.contract + ctx.attachments + .asSequence() + .mapNotNull { id -> loadAttachmentContainingContract(id, contractClassName) } + .firstOrNull() ?: throw AttachmentResolutionException(stateRef.txhash) + } + is ContractUpgradeWireTransaction -> { + attachments.openAttachment(ctx.upgradedContractAttachmentId) ?: throw AttachmentResolutionException(stateRef.txhash) + } + is NotaryChangeWireTransaction -> { + val transactionState = getSerializedState(stateRef).deserialize() + val input = ctx.inputs.firstOrNull() ?: throw AttachmentResolutionException(stateRef.txhash) + loadContractAttachment(input, transactionState.contract) + } + else -> throw UnsupportedOperationException("Attempting to resolve attachment for index ${stateRef.index} of a " + + "${ctx.javaClass} transaction. This is not supported.") + } + } + + private fun loadAttachmentContainingContract(id: SecureHash, contractClassName: String): Attachment? { + return attachments.openAttachment(id)?.takeIf { it is ContractAttachment && contractClassName in it.allContracts } + } + + override fun loadState(stateRef: StateRef): TransactionState<*> = getSerializedState(stateRef).deserialize() + + override fun loadStates(stateRefs: Set): Set> = loadStatesInternal(stateRefs, LinkedHashSet()) + + fun >> loadStatesInternal(input: Iterable, output: C): C { + return input.mapTo(output, ::toStateAndRef) + } + + // TODO Bulk party lookup? + override fun getParties(keys: Collection): List = keys.map(identityService::partyFromKey) + + override fun getAttachment(id: SecureHash): Attachment? = attachments.openAttachment(id) + + override fun getNetworkParameters(id: SecureHash?): NetworkParameters? { + return networkParametersService.lookup(id ?: networkParametersService.defaultHash) + } + + /** + * This is the main logic that knows how to retrieve the binary representation of [StateRef]s. + * + * For [ContractUpgradeWireTransaction] or [NotaryChangeWireTransaction] it knows how to recreate the output state in the + * correct classloader independent of the node's classpath. + */ + override fun getSerializedState(stateRef: StateRef): SerializedTransactionState { + val coreTransaction = getRequiredTransaction(stateRef.txhash).coreTransaction + return when (coreTransaction) { + is WireTransaction -> getRegularOutput(coreTransaction, stateRef.index) + is ContractUpgradeWireTransaction -> getContractUpdateOutput(coreTransaction, stateRef.index) + is NotaryChangeWireTransaction -> getNotaryChangeOutput(coreTransaction, stateRef.index) + else -> throw UnsupportedOperationException("Attempting to resolve input ${stateRef.index} of a ${coreTransaction.javaClass} " + + "transaction. This is not supported.") + } + } + + private fun getRegularOutput(coreTransaction: WireTransaction, outputIndex: Int): SerializedTransactionState { + @Suppress("UNCHECKED_CAST") + return coreTransaction.componentGroups + .first { it.groupIndex == ComponentGroupEnum.OUTPUTS_GROUP.ordinal } + .components[outputIndex] as SerializedTransactionState + } + + /** + * Creates a binary serialized component for a virtual output state serialised and executed with the attachments from the transaction. + */ + private fun getContractUpdateOutput(wtx: ContractUpgradeWireTransaction, outputIndex: Int): SerializedTransactionState { + val binaryInput = getSerializedState(wtx.inputs[outputIndex]) + val legacyContractAttachment = getAttachment(wtx.legacyContractAttachmentId) ?: throw MissingContractAttachments(emptyList()) + val upgradedContractAttachment = getAttachment(wtx.upgradedContractAttachmentId) ?: throw MissingContractAttachments(emptyList()) + val networkParameters = getNetworkParameters(wtx.networkParametersHash) ?: throw TransactionResolutionException(wtx.id) + + return AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext( + listOf(legacyContractAttachment, upgradedContractAttachment), + networkParameters, + wtx.id, + ::isAttachmentTrusted, + attachmentsClassLoaderCache = attachmentsClassLoaderCache + ) { serializationContext -> + val upgradedContract = loadUpgradedContract(wtx.upgradedContractClassName, wtx.id, serializationContext.deserializationClassLoader) + val outputState = calculateUpgradedState(binaryInput.deserialize(), upgradedContract, upgradedContractAttachment) + outputState.serialize() + } + } + + /** + * This should return a serialized virtual output state, that will be used to verify spending transactions. + * The binary output should not depend on the classpath of the node that is verifying the transaction. + * + * Ideally the serialization engine would support partial deserialization so that only the Notary ( and the encumbrance can be replaced + * from the binary input state) + */ + // TODO - currently this uses the main classloader. + private fun getNotaryChangeOutput(wtx: NotaryChangeWireTransaction, outputIndex: Int): SerializedTransactionState { + val input = getStateAndRef(wtx.inputs[outputIndex]) + val output = NotaryChangeLedgerTransaction.computeOutput(input, wtx.newNotary) { wtx.inputs } + return output.serialize() + } + + /** + * Scans trusted (installed locally) attachments to find all that contain the [className]. + * This is required as a workaround until explicit cordapp dependencies are implemented. + * + * @return the attachments with the highest version. + */ + // TODO Should throw when the class is found in multiple contract attachments (not different versions). + override fun getTrustedClassAttachment(className: String): Attachment? { + val allTrusted = attachments.queryAttachments( + AttachmentQueryCriteria.AttachmentsQueryCriteria().withUploader(Builder.`in`(TRUSTED_UPLOADERS)), + AttachmentSort(listOf(AttachmentSort.AttachmentSortColumn(AttachmentSortAttribute.VERSION, Sort.Direction.DESC))) + ) + + // TODO - add caching if performance is affected. + for (attId in allTrusted) { + val attch = attachments.openAttachment(attId)!! + if (attch.openAsJAR().use { hasFile(it, "$className.class") }) return attch + } + return null + } + + private fun hasFile(jarStream: JarInputStream, className: String): Boolean { + while (true) { + val e = jarStream.nextJarEntry ?: return false + if (e.name == className) { + return true + } + } + } + + override fun isAttachmentTrusted(attachment: Attachment): Boolean = attachmentTrustCalculator.calculate(attachment) + + override fun fixupAttachmentIds(attachmentIds: Collection): Set { + return cordappProvider.fixupAttachmentIds(attachmentIds) + } + + /** + * Try to verify the given transaction on the external verifier, assuming it is available. It is not required to verify externally even + * if the verifier is available. + * + * The default implementation is to only do internal verification. + * + * @return true if the transaction should (also) be verified internally, regardless of whether it was verified externally. + */ + fun tryExternalVerification(stx: SignedTransaction, checkSufficientSignatures: Boolean): Boolean { + return true + } +} + +fun ServicesForResolution.toVerifyingServiceHub(): VerifyingServiceHub { + if (this is VerifyingServiceHub) { + return this + } + // All ServicesForResolution instances should also implement VerifyingServiceHub, which is something we can enforce with the + // @DoNotImplement annotation. The only exception however is MockServices, which does not since it's public API and VerifyingServiceHub + // is internal. Instead, MockServices has a private VerifyingServiceHub "view" which we get at via reflection. + var clazz: Class<*> = javaClass + while (true) { + if (clazz.name == "net.corda.testing.node.MockServices") { + return clazz.getDeclaredMethod("getVerifyingView").apply { isAccessible = true }.invoke(this) as VerifyingServiceHub + } + clazz = clazz.superclass ?: throw ClassCastException("${javaClass.name} is not a VerifyingServiceHub") + } +} diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index 2f8c097804..2bf4bcdfcd 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -11,6 +11,7 @@ import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.ContractUpgradeFlow import net.corda.core.internal.PlatformVersionSwitches.TWO_PHASE_FINALITY import net.corda.core.internal.telemetry.TelemetryComponent +import net.corda.core.internal.uncheckedCast import net.corda.core.node.services.* import net.corda.core.node.services.diagnostics.DiagnosticsService import net.corda.core.serialization.CordaSerializable @@ -170,12 +171,6 @@ interface ServiceHub : ServicesForResolution { */ val telemetryService: TelemetryService - /** - * INTERNAL. DO NOT USE. - * @suppress - */ - val transactionVerifierService: TransactionVerifierService - /** * A [Clock] representing the node's current time. This should be used in preference to directly accessing the * clock so the current time can be controlled during unit testing. @@ -283,8 +278,7 @@ interface ServiceHub : ServicesForResolution { */ @Throws(TransactionResolutionException::class) fun toStateAndRef(stateRef: StateRef): StateAndRef { - val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash) - return stx.resolveBaseTransaction(this).outRef(stateRef.index) + return StateAndRef(uncheckedCast(loadState(stateRef)), stateRef) } private val legalIdentityKey: PublicKey get() = this.myInfo.legalIdentitiesAndCerts.first().owningKey @@ -424,8 +418,8 @@ interface ServiceHub : ServicesForResolution { * When used within a flow, this session automatically forms part of the enclosing flow transaction boundary, * and thus queryable data will include everything committed as of the last checkpoint. * - * We want to make sure users have a restricted access to administrative functions, this function will return a [RestrictedConnection] instance. - * The following methods are blocked: + * We want to make sure users have a restricted access to administrative functions, this function will return a [Connection] instance + * with the following methods blocked: * - abort(executor: Executor?) * - clearWarnings() * - close() diff --git a/core/src/main/kotlin/net/corda/core/node/services/TransactionVerifierService.kt b/core/src/main/kotlin/net/corda/core/node/services/TransactionVerifierService.kt deleted file mode 100644 index d72eec72f2..0000000000 --- a/core/src/main/kotlin/net/corda/core/node/services/TransactionVerifierService.kt +++ /dev/null @@ -1,18 +0,0 @@ -package net.corda.core.node.services - -import net.corda.core.DoNotImplement -import net.corda.core.concurrent.CordaFuture -import net.corda.core.transactions.LedgerTransaction - -/** - * Provides verification service. The implementation may be a simple in-memory verify() call or perhaps an IPC/RPC. - * @suppress - */ -@DoNotImplement -interface TransactionVerifierService { - /** - * @param transaction The transaction to be verified. - * @return A future that completes successfully if the transaction verified, or sets an exception the verifier threw. - */ - fun verify(transaction: LedgerTransaction): CordaFuture<*> -} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt index 145da6a07c..f26ccd00ad 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt @@ -1,28 +1,44 @@ package net.corda.core.transactions import net.corda.core.CordaInternal -import net.corda.core.contracts.* +import net.corda.core.contracts.Attachment +import net.corda.core.contracts.AttachmentResolutionException +import net.corda.core.contracts.ContractAttachment +import net.corda.core.contracts.ContractClassName +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.HashAttachmentConstraint +import net.corda.core.contracts.PrivacySalt +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionResolutionException +import net.corda.core.contracts.TransactionState +import net.corda.core.contracts.TransactionVerificationException +import net.corda.core.contracts.UpgradedContract +import net.corda.core.contracts.UpgradedContractWithLegacyConstraint +import net.corda.core.contracts.WhitelistedByZoneAttachmentConstraint import net.corda.core.crypto.DigestService import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.identity.Party import net.corda.core.internal.AttachmentWithContext -import net.corda.core.internal.ServiceHubCoreInternal import net.corda.core.internal.combinedHash +import net.corda.core.internal.loadClassOfType +import net.corda.core.internal.mapToSet +import net.corda.core.internal.verification.VerificationSupport +import net.corda.core.internal.verification.toVerifyingServiceHub import net.corda.core.node.NetworkParameters import net.corda.core.node.ServicesForResolution import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.DeprecatedConstructorForDeserialization -import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize -import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder -import net.corda.core.serialization.serialize import net.corda.core.transactions.ContractUpgradeFilteredTransaction.FilteredComponent -import net.corda.core.transactions.ContractUpgradeLedgerTransaction.Companion.loadUpgradedContract -import net.corda.core.transactions.ContractUpgradeLedgerTransaction.Companion.retrieveAppClassLoader import net.corda.core.transactions.ContractUpgradeWireTransaction.Companion.calculateUpgradedState -import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.* -import net.corda.core.transactions.WireTransaction.Companion.resolveStateRefBinaryComponent +import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.INPUTS +import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.LEGACY_ATTACHMENT +import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.NOTARY +import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.PARAMETERS_HASH +import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.UPGRADED_ATTACHMENT +import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.UPGRADED_CONTRACT import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.toBase58String import java.security.PublicKey @@ -52,7 +68,10 @@ data class ContractUpgradeWireTransaction( * Runs the explicit upgrade logic. */ @CordaInternal - internal fun calculateUpgradedState(state: TransactionState, upgradedContract: UpgradedContract, upgradedContractAttachment: Attachment): TransactionState { + @JvmSynthetic + internal fun calculateUpgradedState(state: TransactionState, + upgradedContract: UpgradedContract, + upgradedContractAttachment: Attachment): TransactionState { // TODO: if there are encumbrance states in the inputs, just copy them across without modifying val upgradedState: S = upgradedContract.upgrade(state.data) val inputConstraint = state.constraint @@ -121,60 +140,12 @@ data class ContractUpgradeWireTransaction( /** Resolves input states and contract attachments, and builds a ContractUpgradeLedgerTransaction. */ fun resolve(services: ServicesForResolution, sigs: List): ContractUpgradeLedgerTransaction { - val resolvedInputs = services.loadStates(inputs.toSet()).toList() - val legacyContractAttachment = services.attachments.openAttachment(legacyContractAttachmentId) - ?: throw AttachmentResolutionException(legacyContractAttachmentId) - val upgradedContractAttachment = services.attachments.openAttachment(upgradedContractAttachmentId) - ?: throw AttachmentResolutionException(upgradedContractAttachmentId) - val hashToResolve = networkParametersHash ?: services.networkParametersService.defaultHash - val resolvedNetworkParameters = services.networkParametersService.lookup(hashToResolve) ?: throw TransactionResolutionException(id) - return ContractUpgradeLedgerTransaction.create( - resolvedInputs, - notary, - legacyContractAttachment, - upgradedContractAttachment, - id, - privacySalt, - sigs, - resolvedNetworkParameters, - loadUpgradedContract(upgradedContractClassName, retrieveAppClassLoader(services)) - ) - } - - private fun upgradedContract(className: ContractClassName, classLoader: ClassLoader): UpgradedContract = try { - @Suppress("UNCHECKED_CAST") - Class.forName(className, false, classLoader).asSubclass(UpgradedContract::class.java).getDeclaredConstructor().newInstance() as UpgradedContract - } catch (e: Exception) { - throw TransactionVerificationException.ContractCreationError(id, className, e) - } - - /** - * Creates a binary serialized component for a virtual output state serialised and executed with the attachments from the transaction. - */ - @CordaInternal - internal fun resolveOutputComponent(services: ServicesForResolution, stateRef: StateRef, params: NetworkParameters): SerializedBytes> { - val binaryInput: SerializedBytes> = resolveStateRefBinaryComponent(inputs[stateRef.index], services)!! - val legacyAttachment = services.attachments.openAttachment(legacyContractAttachmentId) - ?: throw MissingContractAttachments(emptyList()) - val upgradedAttachment = services.attachments.openAttachment(upgradedContractAttachmentId) - ?: throw MissingContractAttachments(emptyList()) - - return AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext( - listOf(legacyAttachment, upgradedAttachment), - params, - id, - { (services as ServiceHubCoreInternal).attachmentTrustCalculator.calculate(it) }, - attachmentsClassLoaderCache = (services as ServiceHubCoreInternal).attachmentsClassLoaderCache) { serializationContext -> - val resolvedInput = binaryInput.deserialize() - val upgradedContract = upgradedContract(upgradedContractClassName, serializationContext.deserializationClassLoader) - val outputState = calculateUpgradedState(resolvedInput, upgradedContract, upgradedAttachment) - outputState.serialize() - } + return ContractUpgradeLedgerTransaction.resolve(services.toVerifyingServiceHub(), this, sigs) } /** Constructs a filtered transaction: the inputs, the notary party and network parameters hash are always visible, while the rest are hidden. */ fun buildFilteredTransaction(): ContractUpgradeFilteredTransaction { - val totalComponents = (0 until serializedComponents.size).toSet() + val totalComponents = serializedComponents.indices.toSet() val visibleComponents = mapOf( INPUTS.ordinal to FilteredComponent(serializedComponents[INPUTS.ordinal], nonces[INPUTS.ordinal]), NOTARY.ordinal to FilteredComponent(serializedComponents[NOTARY.ordinal], nonces[NOTARY.ordinal]), @@ -287,39 +258,47 @@ private constructor( get() = upgradedContract::class.java.name companion object { - @CordaInternal - internal fun create( - inputs: List>, - notary: Party, - legacyContractAttachment: Attachment, - upgradedContractAttachment: Attachment, - id: SecureHash, - privacySalt: PrivacySalt, - sigs: List, - networkParameters: NetworkParameters, - upgradedContract: UpgradedContract - ): ContractUpgradeLedgerTransaction { - return ContractUpgradeLedgerTransaction(inputs, notary, legacyContractAttachment, upgradedContractAttachment, id, privacySalt, sigs, networkParameters, upgradedContract) + @JvmSynthetic + @Suppress("ThrowsCount") + internal fun resolve(verificationSupport: VerificationSupport, + wtx: ContractUpgradeWireTransaction, + sigs: List): ContractUpgradeLedgerTransaction { + val inputs = wtx.inputs.map(verificationSupport::getStateAndRef) + val (legacyContractAttachment, upgradedContractAttachment) = verificationSupport.getAttachments(listOf( + wtx.legacyContractAttachmentId, + wtx.upgradedContractAttachmentId + )) + val networkParameters = verificationSupport.getNetworkParameters(wtx.networkParametersHash) + ?: throw TransactionResolutionException(wtx.id) + val upgradedContract = loadUpgradedContract(wtx.upgradedContractClassName, wtx.id, verificationSupport.appClassLoader) + return ContractUpgradeLedgerTransaction( + inputs, + wtx.notary, + legacyContractAttachment ?: throw AttachmentResolutionException(wtx.legacyContractAttachmentId), + upgradedContractAttachment ?: throw AttachmentResolutionException(wtx.upgradedContractAttachmentId), + wtx.id, + wtx.privacySalt, + sigs, + networkParameters, + upgradedContract + ) } - // TODO - this has to use a classloader created from the upgraded attachment. + // TODO There is an inconsistency with the class loader used with this method. Transaction resolution uses the app class loader, + // whilst TransactionStorageVerification.getContractUpdateOutput uses an attachments class loder comprised of the the legacy and + // upgraded attachments @CordaInternal - internal fun loadUpgradedContract(upgradedContractClassName: ContractClassName, classLoader: ClassLoader): UpgradedContract { - @Suppress("UNCHECKED_CAST") - return Class.forName(upgradedContractClassName, false, classLoader) - .asSubclass(Contract::class.java) - .getConstructor() - .newInstance() as UpgradedContract - } - - // This is a "hack" to retrieve the CordappsClassloader from the services without having access to all classes. - @CordaInternal - internal fun retrieveAppClassLoader(services: ServicesForResolution): ClassLoader { - val cordappLoader = services.cordappProvider::class.java.getMethod("getCordappLoader").invoke(services.cordappProvider) - - @Suppress("UNCHECKED_CAST") - return cordappLoader::class.java.getMethod("getAppClassLoader").invoke(cordappLoader) as ClassLoader + @JvmSynthetic + @Suppress("TooGenericExceptionCaught") + internal fun loadUpgradedContract(className: ContractClassName, id: SecureHash, classLoader: ClassLoader): UpgradedContract { + return try { + loadClassOfType>(className, false, classLoader) + .getDeclaredConstructor() + .newInstance() + } catch (e: Exception) { + throw TransactionVerificationException.ContractCreationError(id, className, e) + } } } @@ -366,7 +345,7 @@ private constructor( /** The required signers are the set of all input states' participants. */ override val requiredSigningKeys: Set - get() = inputs.flatMap { it.state.data.participants }.map { it.owningKey }.toSet() + notary.owningKey + get() = inputs.flatMap { it.state.data.participants }.mapToSet { it.owningKey } + notary.owningKey override fun getKeyDescriptions(keys: Set): List { return keys.map { it.toBase58String() } @@ -401,7 +380,7 @@ private constructor( privacySalt: PrivacySalt, sigs: List, networkParameters: NetworkParameters - ) : this(inputs, notary, legacyContractAttachment, upgradedContractAttachment, id, privacySalt, sigs, networkParameters, loadUpgradedContract(upgradedContractClassName, ContractUpgradeLedgerTransaction::class.java.classLoader)) + ) : this(inputs, notary, legacyContractAttachment, upgradedContractAttachment, id, privacySalt, sigs, networkParameters, loadUpgradedContract(upgradedContractClassName, id, ContractUpgradeLedgerTransaction::class.java.classLoader)) @Deprecated("ContractUpgradeLedgerTransaction should not be created directly, use ContractUpgradeWireTransaction.resolve instead.") fun copy( diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index 846799d0b3..8037668b68 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -16,21 +16,21 @@ import net.corda.core.crypto.DigestService import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic import net.corda.core.identity.Party -import net.corda.core.internal.AbstractVerifier import net.corda.core.internal.SerializedStateAndRef -import net.corda.core.internal.Verifier import net.corda.core.internal.castIfPossible import net.corda.core.internal.deserialiseCommands import net.corda.core.internal.deserialiseComponentGroup import net.corda.core.internal.eagerDeserialise import net.corda.core.internal.isUploaderTrusted import net.corda.core.internal.uncheckedCast +import net.corda.core.internal.verification.AbstractVerifier +import net.corda.core.internal.verification.Verifier import net.corda.core.node.NetworkParameters import net.corda.core.serialization.DeprecatedConstructorForDeserialization import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationFactory -import net.corda.core.serialization.internal.AttachmentsClassLoaderCache import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder +import net.corda.core.serialization.internal.AttachmentsClassLoaderCache import net.corda.core.utilities.contextLogger import java.util.Collections.unmodifiableList import java.util.function.Predicate @@ -153,34 +153,35 @@ private constructor( serializedInputs: List? = null, serializedReferences: List? = null, isAttachmentTrusted: (Attachment) -> Boolean, + verifierFactory: (LedgerTransaction, SerializationContext) -> Verifier, attachmentsClassLoaderCache: AttachmentsClassLoaderCache?, digestService: DigestService ): LedgerTransaction { return LedgerTransaction( - inputs = inputs, - outputs = outputs, - commands = commands, - attachments = attachments, - id = id, - notary = notary, - timeWindow = timeWindow, - privacySalt = privacySalt, - networkParameters = networkParameters, - references = references, - componentGroups = protectOrNull(componentGroups), - serializedInputs = protectOrNull(serializedInputs), - serializedReferences = protectOrNull(serializedReferences), - isAttachmentTrusted = isAttachmentTrusted, - verifierFactory = ::BasicVerifier, - attachmentsClassLoaderCache = attachmentsClassLoaderCache, - digestService = digestService + inputs = inputs, + outputs = outputs, + commands = commands, + attachments = attachments, + id = id, + notary = notary, + timeWindow = timeWindow, + privacySalt = privacySalt, + networkParameters = networkParameters, + references = references, + componentGroups = protectOrNull(componentGroups), + serializedInputs = protectOrNull(serializedInputs), + serializedReferences = protectOrNull(serializedReferences), + isAttachmentTrusted = isAttachmentTrusted, + verifierFactory = verifierFactory, + attachmentsClassLoaderCache = attachmentsClassLoaderCache, + digestService = digestService ) } /** * This factory function will create an instance of [LedgerTransaction] * that will be used for contract verification. - * @see BasicVerifier + * @see DefaultVerifier */ @CordaInternal fun createForContractVerify( @@ -243,26 +244,26 @@ private constructor( */ @Throws(TransactionVerificationException::class) fun verify() { - internalPrepareVerify(attachments).verify() + verifyInternal() } /** * This method has to be called in a context where it has access to the database. */ @CordaInternal - internal fun internalPrepareVerify(txAttachments: List): Verifier { + @JvmSynthetic + internal fun verifyInternal(txAttachments: List = this.attachments) { // Switch thread local deserialization context to using a cached attachments classloader. This classloader enforces various rules // like no-overlap, package namespace ownership and (in future) deterministic Java. - return AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext( + val verifier = AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext( txAttachments, getParamsWithGoo(), id, - isAttachmentTrusted = isAttachmentTrusted, - attachmentsClassLoaderCache = attachmentsClassLoaderCache) { serializationContext -> - + isAttachmentTrusted, + attachmentsClassLoaderCache = attachmentsClassLoaderCache + ) { serializationContext -> // Legacy check - warns if the LedgerTransaction was created incorrectly. checkLtxForVerification() - // Create a copy of the outer LedgerTransaction which deserializes all fields using // the serialization context (or its deserializationClassloader). // Only the copy will be used for verification, and the outer shell will be discarded. @@ -270,6 +271,7 @@ private constructor( // NOTE: The Verifier creates the copies of the LedgerTransaction object now. verifierFactory(this, serializationContext) } + verifier.verify() } /** @@ -463,7 +465,7 @@ private constructor( } inline fun filterInputs(crossinline predicate: (T) -> Boolean): List { - return filterInputs(T::class.java, Predicate { predicate(it) }) + return filterInputs(T::class.java) { predicate(it) } } /** @@ -479,7 +481,7 @@ private constructor( } inline fun filterReferenceInputs(crossinline predicate: (T) -> Boolean): List { - return filterReferenceInputs(T::class.java, Predicate { predicate(it) }) + return filterReferenceInputs(T::class.java) { predicate(it) } } /** @@ -495,7 +497,7 @@ private constructor( } inline fun filterInRefs(crossinline predicate: (T) -> Boolean): List> { - return filterInRefs(T::class.java, Predicate { predicate(it) }) + return filterInRefs(T::class.java) { predicate(it) } } /** @@ -511,7 +513,7 @@ private constructor( } inline fun filterReferenceInputRefs(crossinline predicate: (T) -> Boolean): List> { - return filterReferenceInputRefs(T::class.java, Predicate { predicate(it) }) + return filterReferenceInputRefs(T::class.java) { predicate(it) } } /** @@ -528,7 +530,7 @@ private constructor( } inline fun findInput(crossinline predicate: (T) -> Boolean): T { - return findInput(T::class.java, Predicate { predicate(it) }) + return findInput(T::class.java) { predicate(it) } } /** @@ -541,11 +543,11 @@ private constructor( * @throws IllegalArgumentException if no item, or multiple items are found matching the requirements. */ fun findReference(clazz: Class, predicate: Predicate): T { - return referenceInputsOfType(clazz).single { predicate.test(it) } + return referenceInputsOfType(clazz).single(predicate::test) } inline fun findReference(crossinline predicate: (T) -> Boolean): T { - return findReference(T::class.java, Predicate { predicate(it) }) + return findReference(T::class.java) { predicate(it) } } /** @@ -562,7 +564,7 @@ private constructor( } inline fun findInRef(crossinline predicate: (T) -> Boolean): StateAndRef { - return findInRef(T::class.java, Predicate { predicate(it) }) + return findInRef(T::class.java) { predicate(it) } } /** @@ -579,7 +581,7 @@ private constructor( } inline fun findReferenceInputRef(crossinline predicate: (T) -> Boolean): StateAndRef { - return findReferenceInputRef(T::class.java, Predicate { predicate(it) }) + return findReferenceInputRef(T::class.java) { predicate(it) } } /** @@ -614,7 +616,7 @@ private constructor( } inline fun filterCommands(crossinline predicate: (T) -> Boolean): List> { - return filterCommands(T::class.java, Predicate { predicate(it) }) + return filterCommands(T::class.java) { predicate(it) } } /** @@ -631,7 +633,7 @@ private constructor( } inline fun findCommand(crossinline predicate: (T) -> Boolean): Command { - return findCommand(T::class.java, Predicate { predicate(it) }) + return findCommand(T::class.java) { predicate(it) } } /** @@ -706,7 +708,7 @@ private constructor( serializedInputs = null, serializedReferences = null, isAttachmentTrusted = Attachment::isUploaderTrusted, - verifierFactory = ::BasicVerifier, + verifierFactory = ::DefaultVerifier, attachmentsClassLoaderCache = null ) @@ -736,7 +738,7 @@ private constructor( serializedInputs = null, serializedReferences = null, isAttachmentTrusted = Attachment::isUploaderTrusted, - verifierFactory = ::BasicVerifier, + verifierFactory = ::DefaultVerifier, attachmentsClassLoaderCache = null ) @@ -804,14 +806,19 @@ private constructor( } } +@CordaInternal +@JvmSynthetic +fun defaultVerifier(ltx: LedgerTransaction, serializationContext: SerializationContext): Verifier { + return DefaultVerifier(ltx, serializationContext) +} + /** * This is the default [Verifier] that configures Corda * to execute [Contract.verify(LedgerTransaction)]. * * THIS CLASS IS NOT PUBLIC API, AND IS DELIBERATELY PRIVATE! */ -@CordaInternal -private class BasicVerifier( +private class DefaultVerifier( ltx: LedgerTransaction, private val serializationContext: SerializationContext ) : AbstractVerifier(ltx, serializationContext.deserializationClassLoader) { @@ -874,7 +881,6 @@ private class BasicVerifier( * THIS CLASS IS NOT PUBLIC API, AND IS DELIBERATELY PRIVATE! */ @Suppress("unused_parameter") -@CordaInternal private class NoOpVerifier(ltx: LedgerTransaction, serializationContext: SerializationContext) : Verifier { // Invoking LedgerTransaction.verify() from Contract.verify(LedgerTransaction) // will execute this function. But why would anyone do that?! diff --git a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt index ab42260f37..cf5db15911 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt @@ -1,20 +1,31 @@ package net.corda.core.transactions import net.corda.core.CordaInternal -import net.corda.core.contracts.* +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionResolutionException +import net.corda.core.contracts.TransactionState +import net.corda.core.contracts.TransactionVerificationException import net.corda.core.crypto.DigestService import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.identity.Party +import net.corda.core.internal.indexOfOrThrow +import net.corda.core.internal.mapToSet +import net.corda.core.internal.verification.VerificationSupport +import net.corda.core.internal.verification.toVerifyingServiceHub import net.corda.core.node.NetworkParameters import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.DeprecatedConstructorForDeserialization -import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize -import net.corda.core.transactions.NotaryChangeWireTransaction.Component.* +import net.corda.core.transactions.NotaryChangeWireTransaction.Component.INPUTS +import net.corda.core.transactions.NotaryChangeWireTransaction.Component.NEW_NOTARY +import net.corda.core.transactions.NotaryChangeWireTransaction.Component.NOTARY +import net.corda.core.transactions.NotaryChangeWireTransaction.Component.PARAMETERS_HASH import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.toBase58String import java.security.PublicKey @@ -88,32 +99,12 @@ data class NotaryChangeWireTransaction( /** Resolves input states and network parameters and builds a [NotaryChangeLedgerTransaction]. */ fun resolve(services: ServicesForResolution, sigs: List): NotaryChangeLedgerTransaction { - val resolvedInputs = services.loadStates(inputs.toSet()).toList() - val hashToResolve = networkParametersHash ?: services.networkParametersService.defaultHash - val resolvedNetworkParameters = services.networkParametersService.lookup(hashToResolve) - ?: throw TransactionResolutionException(id) - return NotaryChangeLedgerTransaction.create(resolvedInputs, notary, newNotary, id, sigs, resolvedNetworkParameters) + return NotaryChangeLedgerTransaction.resolve(services.toVerifyingServiceHub(), this, sigs) } /** Resolves input states and builds a [NotaryChangeLedgerTransaction]. */ - fun resolve(services: ServiceHub, sigs: List) = resolve(services as ServicesForResolution, sigs) - - /** - * This should return a serialized virtual output state, that will be used to verify spending transactions. - * The binary output should not depend on the classpath of the node that is verifying the transaction. - * - * Ideally the serialization engine would support partial deserialization so that only the Notary ( and the encumbrance can be replaced from the binary input state) - * - * - * TODO - currently this uses the main classloader. - */ - @CordaInternal - internal fun resolveOutputComponent( - services: ServicesForResolution, - stateRef: StateRef, - @Suppress("UNUSED_PARAMETER") params: NetworkParameters - ): SerializedBytes> { - return services.loadState(stateRef).serialize() + fun resolve(services: ServiceHub, sigs: List): NotaryChangeLedgerTransaction { + return resolve(services as ServicesForResolution, sigs) } enum class Component { @@ -140,13 +131,25 @@ private constructor( ) : FullTransaction(), TransactionWithSignatures { companion object { @CordaInternal - internal fun create(inputs: List>, - notary: Party, - newNotary: Party, - id: SecureHash, - sigs: List, - networkParameters: NetworkParameters): NotaryChangeLedgerTransaction { - return NotaryChangeLedgerTransaction(inputs, notary, newNotary, id, sigs, networkParameters) + @JvmSynthetic + internal fun resolve(verificationSupport: VerificationSupport, + wireTx: NotaryChangeWireTransaction, + sigs: List): NotaryChangeLedgerTransaction { + val inputs = wireTx.inputs.map(verificationSupport::getStateAndRef) + val networkParameters = verificationSupport.getNetworkParameters(wireTx.networkParametersHash) + ?: throw TransactionResolutionException(wireTx.id) + return NotaryChangeLedgerTransaction(inputs, wireTx.notary, wireTx.newNotary, wireTx.id, sigs, networkParameters) + } + + @CordaInternal + @JvmSynthetic + internal inline fun computeOutput(input: StateAndRef<*>, newNotary: Party, inputs: () -> List): TransactionState<*> { + val (state, ref) = input + val newEncumbrance = state.encumbrance?.let { + val encumbranceStateRef = ref.copy(index = state.encumbrance) + inputs().indexOfOrThrow(encumbranceStateRef) + } + return state.copy(notary = newNotary, encumbrance = newEncumbrance) } } @@ -174,22 +177,10 @@ private constructor( /** We compute the outputs on demand by applying the notary field modification to the inputs. */ override val outputs: List> - get() = computeOutputs() - - private fun computeOutputs(): List> { - val inputPositionIndex: Map = inputs.mapIndexed { index, stateAndRef -> stateAndRef.ref to index }.toMap() - return inputs.map { (state, ref) -> - if (state.encumbrance != null) { - val encumbranceStateRef = StateRef(ref.txhash, state.encumbrance) - val encumbrancePosition = inputPositionIndex[encumbranceStateRef] - ?: throw IllegalStateException("Unable to generate output states – transaction not constructed correctly.") - state.copy(notary = newNotary, encumbrance = encumbrancePosition) - } else state.copy(notary = newNotary) - } - } + get() = inputs.map { computeOutput(it, newNotary) { inputs.map(StateAndRef::ref) } } override val requiredSigningKeys: Set - get() = inputs.flatMap { it.state.data.participants }.map { it.owningKey }.toSet() + notary.owningKey + get() = inputs.flatMap { it.state.data.participants }.mapToSet { it.owningKey } + notary.owningKey override fun getKeyDescriptions(keys: Set): List { return keys.map { it.toBase58String() } diff --git a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt index 94e2079967..b1d6f91b6b 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -1,27 +1,40 @@ package net.corda.core.transactions import net.corda.core.CordaException +import net.corda.core.CordaInternal import net.corda.core.CordaThrowable -import net.corda.core.contracts.* -import net.corda.core.crypto.* +import net.corda.core.contracts.Attachment +import net.corda.core.contracts.AttachmentResolutionException +import net.corda.core.contracts.NamedByHash +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionResolutionException +import net.corda.core.contracts.TransactionVerificationException +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.SignableData +import net.corda.core.crypto.SignatureMetadata +import net.corda.core.crypto.TransactionSignature +import net.corda.core.crypto.sign +import net.corda.core.crypto.toStringShort import net.corda.core.identity.Party import net.corda.core.internal.TransactionDeserialisationException -import net.corda.core.internal.TransactionVerifierServiceInternal import net.corda.core.internal.VisibleForTesting +import net.corda.core.internal.equivalent +import net.corda.core.internal.isUploaderTrusted +import net.corda.core.internal.verification.VerificationSupport +import net.corda.core.internal.verification.toVerifyingServiceHub import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.MissingAttachmentsException import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize import net.corda.core.serialization.internal.MissingSerializerException import net.corda.core.serialization.serialize import net.corda.core.utilities.contextLogger -import net.corda.core.utilities.getOrThrow import java.io.NotSerializableException import java.security.KeyPair import java.security.PublicKey import java.security.SignatureException -import java.util.* import java.util.function.Predicate /** @@ -142,6 +155,12 @@ data class SignedTransaction(val txBits: SerializedBytes, @JvmOverloads @Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class) fun toLedgerTransaction(services: ServiceHub, checkSufficientSignatures: Boolean = true): LedgerTransaction { + // We need parameters check here, because finality flow calls stx.toLedgerTransaction() and then verify. + resolveAndCheckNetworkParameters(services) + return toLedgerTransactionInternal(services.toVerifyingServiceHub(), checkSufficientSignatures) + } + + private fun toLedgerTransactionInternal(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean): LedgerTransaction { // TODO: We could probably optimise the below by // a) not throwing if threshold is eventually satisfied, but some of the rest of the signatures are failing. // b) omit verifying signatures when threshold requirement is met. @@ -154,9 +173,7 @@ data class SignedTransaction(val txBits: SerializedBytes, } else { checkSignaturesAreValid() } - // We need parameters check here, because finality flow calls stx.toLedgerTransaction() and then verify. - resolveAndCheckNetworkParameters(services) - return tx.toLedgerTransaction(services) + return tx.toLedgerTransactionInternal(verificationSupport) } /** @@ -173,10 +190,19 @@ data class SignedTransaction(val txBits: SerializedBytes, @Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class) fun verify(services: ServiceHub, checkSufficientSignatures: Boolean = true) { resolveAndCheckNetworkParameters(services) + val verifyingServiceHub = services.toVerifyingServiceHub() + if (verifyingServiceHub.tryExternalVerification(this, checkSufficientSignatures)) { + verifyInternal(verifyingServiceHub, checkSufficientSignatures) + } + } + + @CordaInternal + @JvmSynthetic + fun verifyInternal(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean) { when (coreTransaction) { - is NotaryChangeWireTransaction -> verifyNotaryChangeTransaction(services, checkSufficientSignatures) - is ContractUpgradeWireTransaction -> verifyContractUpgradeTransaction(services, checkSufficientSignatures) - else -> verifyRegularTransaction(services, checkSufficientSignatures) + is NotaryChangeWireTransaction -> verifyNotaryChangeTransaction(verificationSupport, checkSufficientSignatures) + is ContractUpgradeWireTransaction -> verifyContractUpgradeTransaction(verificationSupport, checkSufficientSignatures) + else -> verifyRegularTransaction(verificationSupport, checkSufficientSignatures) } } @@ -197,15 +223,15 @@ data class SignedTransaction(val txBits: SerializedBytes, } /** No contract code is run when verifying notary change transactions, it is sufficient to check invariants during initialisation. */ - private fun verifyNotaryChangeTransaction(services: ServiceHub, checkSufficientSignatures: Boolean) { - val ntx = resolveNotaryChangeTransaction(services) + private fun verifyNotaryChangeTransaction(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean) { + val ntx = NotaryChangeLedgerTransaction.resolve(verificationSupport, coreTransaction as NotaryChangeWireTransaction, sigs) if (checkSufficientSignatures) ntx.verifyRequiredSignatures() else checkSignaturesAreValid() } /** No contract code is run when verifying contract upgrade transactions, it is sufficient to check invariants during initialisation. */ - private fun verifyContractUpgradeTransaction(services: ServicesForResolution, checkSufficientSignatures: Boolean) { - val ctx = resolveContractUpgradeTransaction(services) + private fun verifyContractUpgradeTransaction(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean) { + val ctx = ContractUpgradeLedgerTransaction.resolve(verificationSupport, coreTransaction as ContractUpgradeWireTransaction, sigs) if (checkSufficientSignatures) ctx.verifyRequiredSignatures() else checkSignaturesAreValid() } @@ -213,22 +239,21 @@ data class SignedTransaction(val txBits: SerializedBytes, // TODO: Verify contract constraints here as well as in LedgerTransaction to ensure that anything being deserialised // from the attachment is trusted. This will require some partial serialisation work to not load the ContractState // objects from the TransactionState. - private fun verifyRegularTransaction(services: ServiceHub, checkSufficientSignatures: Boolean) { - val ltx = toLedgerTransaction(services, checkSufficientSignatures) + private fun verifyRegularTransaction(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean) { + val ltx = toLedgerTransactionInternal(verificationSupport, checkSufficientSignatures) try { - // TODO: allow non-blocking verification. - services.transactionVerifierService.verify(ltx).getOrThrow() + ltx.verify() } catch (e: NoClassDefFoundError) { checkReverifyAllowed(e) val missingClass = e.message ?: throw e log.warn("Transaction {} has missing class: {}", ltx.id, missingClass) - reverifyWithFixups(ltx, services, missingClass) + reverifyWithFixups(ltx, verificationSupport, missingClass) } catch (e: NotSerializableException) { checkReverifyAllowed(e) - retryVerification(e, e, ltx, services) + retryVerification(e, e, ltx, verificationSupport) } catch (e: TransactionDeserialisationException) { checkReverifyAllowed(e) - retryVerification(e.cause, e, ltx, services) + retryVerification(e.cause, e, ltx, verificationSupport) } } @@ -243,18 +268,18 @@ data class SignedTransaction(val txBits: SerializedBytes, } @Suppress("ThrowsCount") - private fun retryVerification(cause: Throwable?, ex: Throwable, ltx: LedgerTransaction, services: ServiceHub) { + private fun retryVerification(cause: Throwable?, ex: Throwable, ltx: LedgerTransaction, verificationSupport: VerificationSupport) { when (cause) { is MissingSerializerException -> { log.warn("Missing serializers: typeDescriptor={}, typeNames={}", cause.typeDescriptor ?: "", cause.typeNames) - reverifyWithFixups(ltx, services, null) + reverifyWithFixups(ltx, verificationSupport, null) } is NotSerializableException -> { val underlying = cause.cause if (underlying is ClassNotFoundException) { val missingClass = underlying.message?.replace('.', '/') ?: throw ex log.warn("Transaction {} has missing class: {}", ltx.id, missingClass) - reverifyWithFixups(ltx, services, missingClass) + reverifyWithFixups(ltx, verificationSupport, missingClass) } else { throw ex } @@ -266,15 +291,93 @@ data class SignedTransaction(val txBits: SerializedBytes, // Transactions created before Corda 4 can be missing dependencies on other CorDapps. // This code has detected a missing custom serializer - probably located inside a workflow CorDapp. // We need to extract this CorDapp from AttachmentStorage and try verifying this transaction again. - private fun reverifyWithFixups(ltx: LedgerTransaction, services: ServiceHub, missingClass: String?) { + private fun reverifyWithFixups(ltx: LedgerTransaction, verificationSupport: VerificationSupport, missingClass: String?) { log.warn("""Detected that transaction $id does not contain all cordapp dependencies. |This may be the result of a bug in a previous version of Corda. |Attempting to re-verify having applied this node's fix-up rules. |Please check with the originator that this is a valid transaction.""".trimMargin()) - (services.transactionVerifierService as TransactionVerifierServiceInternal) - .reverifyWithFixups(ltx, missingClass) - .getOrThrow() + val replacementAttachments = computeReplacementAttachments(ltx, verificationSupport, missingClass) + log.warn("Reverifying transaction {} with attachments:{}", ltx.id, replacementAttachments) + ltx.verifyInternal(replacementAttachments.toList()) + } + + private fun computeReplacementAttachments(ltx: LedgerTransaction, + verificationSupport: VerificationSupport, + missingClass: String?): Collection { + val replacements = fixupAttachments(verificationSupport, ltx.attachments) + if (!replacements.equivalent(ltx.attachments)) { + return replacements + } + + // We cannot continue unless we have some idea which class is missing from the attachments. + if (missingClass == null) { + throw TransactionVerificationException.BrokenTransactionException( + txId = ltx.id, + message = "No fix-up rules provided for broken attachments: $replacements" + ) + } + + /* + * The Node's fix-up rules have not been able to adjust the transaction's attachments, + * so resort to the original mechanism of trying to find an attachment that contains + * the missing class. + */ + val extraAttachment = requireNotNull(verificationSupport.getTrustedClassAttachment(missingClass)) { + """Transaction $ltx is incorrectly formed. Most likely it was created during version 3 of Corda + |when the verification logic was more lenient. Attempted to find local dependency for class: $missingClass, + |but could not find one. + |If you wish to verify this transaction, please contact the originator of the transaction and install the + |provided missing JAR. + |You can install it using the RPC command: `uploadAttachment` without restarting the node. + |""".trimMargin() + } + + return replacements.toMutableSet().apply { + /* + * Check our transaction doesn't already contain this extra attachment. + * It seems unlikely that we would, but better safe than sorry! + */ + if (!add(extraAttachment)) { + throw TransactionVerificationException.BrokenTransactionException( + txId = ltx.id, + message = "Unlinkable class $missingClass inside broken attachments: $replacements" + ) + } + + log.warn("""Detected that transaction $ltx does not contain all cordapp dependencies. + |This may be the result of a bug in a previous version of Corda. + |Attempting to verify using the additional trusted dependency: $extraAttachment for class $missingClass. + |Please check with the originator that this is a valid transaction. + |YOU ARE ONLY SEEING THIS MESSAGE BECAUSE THE CORDAPPS THAT CREATED THIS TRANSACTION ARE BROKEN! + |WE HAVE TRIED TO REPAIR THE TRANSACTION AS BEST WE CAN, BUT CANNOT GUARANTEE WE HAVE SUCCEEDED! + |PLEASE FIX THE CORDAPPS AND MIGRATE THESE BROKEN TRANSACTIONS AS SOON AS POSSIBLE! + |THIS MESSAGE IS **SUPPOSED** TO BE SCARY!! + |""".trimMargin() + ) + } + } + + /** + * Apply this node's attachment fix-up rules to the given attachments. + * + * @param attachments A collection of [Attachment] objects, e.g. as provided by a transaction. + * @return The [attachments] with the node's fix-up rules applied. + */ + private fun fixupAttachments(verificationSupport: VerificationSupport, attachments: Collection): Collection { + val attachmentsById = attachments.associateByTo(LinkedHashMap(), Attachment::id) + val replacementIds = verificationSupport.fixupAttachmentIds(attachmentsById.keys) + attachmentsById.keys.retainAll(replacementIds) + val extraIds = replacementIds - attachmentsById.keys + val extraAttachments = verificationSupport.getAttachments(extraIds) + for ((index, extraId) in extraIds.withIndex()) { + val extraAttachment = extraAttachments[index] + if (extraAttachment == null || !extraAttachment.isUploaderTrusted()) { + throw MissingAttachmentsException(listOf(extraId)) + } + attachmentsById[extraId] = extraAttachment + } + return attachmentsById.values } /** @@ -306,7 +409,7 @@ data class SignedTransaction(val txBits: SerializedBytes, } /** - * If [transaction] is a [NotaryChangeWireTransaction], loads the input states and resolves it to a + * If [coreTransaction] is a [NotaryChangeWireTransaction], loads the input states and resolves it to a * [NotaryChangeLedgerTransaction] so the signatures can be verified. */ fun resolveNotaryChangeTransaction(services: ServicesForResolution): NotaryChangeLedgerTransaction { @@ -316,10 +419,12 @@ data class SignedTransaction(val txBits: SerializedBytes, } /** - * If [transaction] is a [NotaryChangeWireTransaction], loads the input states and resolves it to a + * If [coreTransaction] is a [NotaryChangeWireTransaction], loads the input states and resolves it to a * [NotaryChangeLedgerTransaction] so the signatures can be verified. */ - fun resolveNotaryChangeTransaction(services: ServiceHub) = resolveNotaryChangeTransaction(services as ServicesForResolution) + fun resolveNotaryChangeTransaction(services: ServiceHub): NotaryChangeLedgerTransaction { + return resolveNotaryChangeTransaction(services as ServicesForResolution) + } /** * If [coreTransaction] is a [ContractUpgradeWireTransaction], loads the input states and resolves it to a diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 6ff76068bd..32ef6351ca 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -9,6 +9,8 @@ import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignatureMetadata import net.corda.core.identity.Party import net.corda.core.internal.* +import net.corda.core.internal.verification.VerifyingServiceHub +import net.corda.core.internal.verification.toVerifyingServiceHub import net.corda.core.node.NetworkParameters import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution @@ -28,7 +30,6 @@ import java.time.Duration import java.time.Instant import java.util.* import java.util.regex.Pattern -import kotlin.collections.ArrayList import kotlin.collections.component1 import kotlin.collections.component2 import kotlin.reflect.KClass @@ -77,9 +78,6 @@ open class TransactionBuilder( private const val ID_PATTERN = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*" private val FQCP: Pattern = Pattern.compile("$ID_PATTERN(/$ID_PATTERN)+") private fun isValidJavaClass(identifier: String) = FQCP.matcher(identifier).matches() - private fun Collection<*>.deepEquals(other: Collection<*>): Boolean { - return (size == other.size) && containsAll(other) && other.containsAll(this) - } private fun Collection.toPrettyString(): String = sorted().joinToString( separator = System.lineSeparator(), prefix = System.lineSeparator() @@ -178,24 +176,25 @@ open class TransactionBuilder( } @CordaInternal - fun toWireTransactionWithContext( + @JvmSynthetic + internal fun toWireTransactionWithContext( services: ServicesForResolution, serializationContext: SerializationContext? - ) : WireTransaction = toWireTransactionWithContext(services, serializationContext, 0) + ) : WireTransaction = toWireTransactionWithContext(services.toVerifyingServiceHub(), serializationContext, 0) private tailrec fun toWireTransactionWithContext( - services: ServicesForResolution, - serializationContext: SerializationContext?, - tryCount: Int + serviceHub: VerifyingServiceHub, + serializationContext: SerializationContext?, + tryCount: Int ): WireTransaction { val referenceStates = referenceStates() if (referenceStates.isNotEmpty()) { - services.ensureMinimumPlatformVersion(4, "Reference states") + serviceHub.ensureMinimumPlatformVersion(4, "Reference states") } - resolveNotary(services) + resolveNotary(serviceHub) val (allContractAttachments: Collection, resolvedOutputs: List>) - = selectContractAttachmentsAndOutputStateConstraints(services, serializationContext) + = selectContractAttachmentsAndOutputStateConstraints(serviceHub, serializationContext) // Final sanity check that all states have the correct constraints. for (state in (inputsWithTransactionState.map { it.state } + resolvedOutputs)) { @@ -213,9 +212,9 @@ open class TransactionBuilder( notary, window, referenceStates, - services.networkParametersService.currentHash), + serviceHub.networkParametersService.currentHash), privacySalt, - services.digestService + serviceHub.digestService ) } @@ -223,10 +222,10 @@ open class TransactionBuilder( // This is a workaround as the current version of Corda does not support cordapp dependencies. // It works by running transaction validation and then scan the attachment storage for missing classes. // TODO - remove once proper support for cordapp dependencies is added. - val addedDependency = addMissingDependency(services, wireTx, tryCount) + val addedDependency = addMissingDependency(serviceHub, wireTx, tryCount) return if (addedDependency) - toWireTransactionWithContext(services, serializationContext, tryCount + 1) + toWireTransactionWithContext(serviceHub, serializationContext, tryCount + 1) else wireTx } @@ -241,9 +240,9 @@ open class TransactionBuilder( /** * @return true if a new dependency was successfully added. */ - private fun addMissingDependency(services: ServicesForResolution, wireTx: WireTransaction, tryCount: Int): Boolean { + private fun addMissingDependency(serviceHub: VerifyingServiceHub, wireTx: WireTransaction, tryCount: Int): Boolean { return try { - wireTx.toLedgerTransaction(services).verify() + wireTx.toLedgerTransactionInternal(serviceHub).verify() // The transaction verified successfully without adding any extra dependency. false } catch (e: Throwable) { @@ -253,12 +252,12 @@ open class TransactionBuilder( // Handle various exceptions that can be thrown during verification and drill down the wrappings. // Note: this is a best effort to preserve backwards compatibility. rootError is ClassNotFoundException -> { - ((tryCount == 0) && fixupAttachments(wireTx.attachments, services, e)) - || addMissingAttachment((rootError.message ?: throw e).replace('.', '/'), services, e) + ((tryCount == 0) && fixupAttachments(wireTx.attachments, serviceHub, e)) + || addMissingAttachment((rootError.message ?: throw e).replace('.', '/'), serviceHub, e) } rootError is NoClassDefFoundError -> { - ((tryCount == 0) && fixupAttachments(wireTx.attachments, services, e)) - || addMissingAttachment(rootError.message ?: throw e, services, e) + ((tryCount == 0) && fixupAttachments(wireTx.attachments, serviceHub, e)) + || addMissingAttachment(rootError.message ?: throw e, serviceHub, e) } // Ignore these exceptions as they will break unit tests. @@ -281,18 +280,18 @@ open class TransactionBuilder( } private fun fixupAttachments( - txAttachments: List, - services: ServicesForResolution, - originalException: Throwable + txAttachments: List, + serviceHub: VerifyingServiceHub, + originalException: Throwable ): Boolean { - val replacementAttachments = services.cordappProvider.internalFixupAttachmentIds(txAttachments) - if (replacementAttachments.deepEquals(txAttachments)) { + val replacementAttachments = serviceHub.fixupAttachmentIds(txAttachments) + if (replacementAttachments.equivalent(txAttachments)) { return false } val extraAttachments = replacementAttachments - txAttachments extraAttachments.forEach { id -> - val attachment = services.attachments.openAttachment(id) + val attachment = serviceHub.attachments.openAttachment(id) if (attachment == null || !attachment.isUploaderTrusted()) { log.warn("""The node's fix-up rules suggest including attachment {}, which cannot be found either. |Please contact the developer of the CorDapp for further instructions. @@ -315,7 +314,7 @@ open class TransactionBuilder( return true } - private fun addMissingAttachment(missingClass: String, services: ServicesForResolution, originalException: Throwable): Boolean { + private fun addMissingAttachment(missingClass: String, serviceHub: VerifyingServiceHub, originalException: Throwable): Boolean { if (!isValidJavaClass(missingClass)) { log.warn("Could not autodetect a valid attachment for the transaction being built.") throw originalException @@ -324,7 +323,7 @@ open class TransactionBuilder( throw originalException } - val attachment = services.attachments.internalFindTrustedAttachmentForClass(missingClass) + val attachment = serviceHub.getTrustedClassAttachment(missingClass) if (attachment == null) { log.error("""The transaction currently built is missing an attachment for class: $missingClass. @@ -475,14 +474,14 @@ open class TransactionBuilder( // Determine if there are any HashConstraints that pin the version of a contract. If there are, check if we trust them. val hashAttachments = inputsAndOutputs .filter { it.constraint is HashAttachmentConstraint } - .map { state -> + .mapToSet { state -> val attachment = services.attachments.openAttachment((state.constraint as HashAttachmentConstraint).attachmentId) if (attachment == null || attachment !is ContractAttachment || !isUploaderTrusted(attachment.uploader)) { // This should never happen because these are input states that should have been validated already. throw MissingContractAttachments(listOf(state)) } attachment - }.toSet() + } // Check that states with the HashConstraint don't conflict between themselves or with an explicitly set attachment. require(hashAttachments.size <= 1) { @@ -490,7 +489,7 @@ open class TransactionBuilder( } if (explicitContractAttachment != null && hashAttachments.singleOrNull() != null) { - require(explicitContractAttachment == (hashAttachments.single() as ContractAttachment).attachment.id) { + require(explicitContractAttachment == hashAttachments.single().attachment.id) { "An attachment has been explicitly set for contract $contractClassName in the transaction builder which conflicts with the HashConstraint of a state." } } @@ -665,10 +664,6 @@ open class TransactionBuilder( @Throws(AttachmentResolutionException::class, TransactionResolutionException::class) fun toLedgerTransaction(services: ServiceHub) = toWireTransaction(services).toLedgerTransaction(services) - fun toLedgerTransactionWithContext(services: ServicesForResolution, serializationContext: SerializationContext): LedgerTransaction { - return toWireTransactionWithContext(services, serializationContext).toLedgerTransaction(services) - } - @Throws(AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class) fun verify(services: ServiceHub) { toLedgerTransaction(services).verify() @@ -692,7 +687,7 @@ open class TransactionBuilder( } // Transaction can combine different identities of the same notary after key rotation. - private fun checkReferencesUseSameNotary() = referencesWithTransactionState.map { it.notary.name }.toSet().size == 1 + private fun checkReferencesUseSameNotary() = referencesWithTransactionState.mapToSet { it.notary.name }.size == 1 // Automatically correct notary after its key rotation private fun resolveNotary(services: ServicesForResolution) { @@ -719,8 +714,6 @@ open class TransactionBuilder( * If this method is called outside the context of a flow, a [ServiceHub] instance must be passed to this method * for it to be able to resolve [StatePointer]s. Usually for a unit test, this will be an instance of mock services. * - * @param serviceHub a [ServiceHub] instance needed for performing vault queries. - * * @throws IllegalStateException if no [ServiceHub] is provided and no flow context is available. */ private fun resolveStatePointers(transactionState: TransactionState<*>) { diff --git a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt index a0fa249240..457b33d246 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -1,21 +1,41 @@ package net.corda.core.transactions import net.corda.core.CordaInternal -import net.corda.core.contracts.* +import net.corda.core.contracts.Attachment +import net.corda.core.contracts.AttachmentResolutionException +import net.corda.core.contracts.Command +import net.corda.core.contracts.CommandWithParties +import net.corda.core.contracts.ComponentGroupEnum import net.corda.core.contracts.ComponentGroupEnum.COMMANDS_GROUP import net.corda.core.contracts.ComponentGroupEnum.OUTPUTS_GROUP -import net.corda.core.crypto.* +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.PrivacySalt +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TimeWindow +import net.corda.core.contracts.TransactionResolutionException +import net.corda.core.contracts.TransactionState +import net.corda.core.crypto.DigestService +import net.corda.core.crypto.MerkleTree +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.TransactionSignature +import net.corda.core.crypto.keys import net.corda.core.identity.Party -import net.corda.core.internal.* +import net.corda.core.internal.Emoji +import net.corda.core.internal.SerializedStateAndRef +import net.corda.core.internal.SerializedTransactionState +import net.corda.core.internal.createComponentGroups +import net.corda.core.internal.flatMapToSet +import net.corda.core.internal.isUploaderTrusted +import net.corda.core.internal.lazyMapped +import net.corda.core.internal.mapToSet +import net.corda.core.internal.verification.VerificationSupport +import net.corda.core.internal.verification.toVerifyingServiceHub import net.corda.core.node.NetworkParameters -import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.AttachmentId import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.DeprecatedConstructorForDeserialization import net.corda.core.serialization.SerializationFactory -import net.corda.core.serialization.SerializedBytes -import net.corda.core.serialization.internal.AttachmentsClassLoaderCache import net.corda.core.serialization.serialize import net.corda.core.utilities.OpaqueBytes import java.security.PublicKey @@ -47,6 +67,7 @@ import java.util.function.Predicate *

*/ @CordaSerializable +@Suppress("ThrowsCount") class WireTransaction(componentGroups: List, val privacySalt: PrivacySalt, digestService: DigestService) : TraversableTransaction(componentGroups, digestService) { constructor(componentGroups: List) : this(componentGroups, PrivacySalt()) @@ -71,7 +92,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr init { check(componentGroups.all { it.components.isNotEmpty() }) { "Empty component groups are not allowed" } - check(componentGroups.map { it.groupIndex }.toSet().size == componentGroups.size) { "Duplicated component groups detected" } + check(componentGroups.mapToSet { it.groupIndex }.size == componentGroups.size) { "Duplicated component groups detected" } checkBaseInvariants() check(inputs.isNotEmpty() || outputs.isNotEmpty()) { "A transaction must contain at least one input or output state" } check(commands.isNotEmpty()) { "A transaction must contain at least one command" } @@ -102,28 +123,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr */ @Throws(AttachmentResolutionException::class, TransactionResolutionException::class) fun toLedgerTransaction(services: ServicesForResolution): LedgerTransaction { - return services.specialise( - toLedgerTransactionInternal( - resolveIdentity = { services.identityService.partyFromKey(it) }, - resolveAttachment = { services.attachments.openAttachment(it) }, - resolveStateRefAsSerialized = { resolveStateRefBinaryComponent(it, services) }, - resolveParameters = { - val hashToResolve = it ?: services.networkParametersService.defaultHash - services.networkParametersService.lookup(hashToResolve) - }, - // `as?` is used due to [MockServices] not implementing [ServiceHubCoreInternal] - isAttachmentTrusted = { (services as? ServiceHubCoreInternal)?.attachmentTrustCalculator?.calculate(it) ?: true }, - attachmentsClassLoaderCache = (services as? ServiceHubCoreInternal)?.attachmentsClassLoaderCache - ) - ) - } - - // Helper for deprecated toLedgerTransaction - @Suppress("UNUSED") // not sure if this field can be removed safely?? - private val missingAttachment: Attachment by lazy { - object : AbstractAttachment({ byteArrayOf() }, DEPLOYED_CORDAPP_UPLOADER ) { - override val id: SecureHash get() = throw UnsupportedOperationException() - } + return toLedgerTransactionInternal(services.toVerifyingServiceHub()) } /** @@ -143,29 +143,37 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr @Suppress("UNUSED_PARAMETER") resolveContractAttachment: (TransactionState) -> AttachmentId? ): LedgerTransaction { // This reverts to serializing the resolved transaction state. - return toLedgerTransactionInternal( - resolveIdentity, - resolveAttachment, - { stateRef -> resolveStateRef(stateRef)?.serialize() }, - { null }, - Attachment::isUploaderTrusted, - null - ) + return toLedgerTransactionInternal(object : VerificationSupport { + override fun getParties(keys: Collection): List = keys.map(resolveIdentity) + override fun getAttachment(id: SecureHash): Attachment? = resolveAttachment(id) + override fun getNetworkParameters(id: SecureHash?): NetworkParameters? = null + override fun isAttachmentTrusted(attachment: Attachment): Boolean = attachment.isUploaderTrusted() + override fun getSerializedState(stateRef: StateRef): SerializedTransactionState { + return resolveStateRef(stateRef)?.serialize() ?: throw TransactionResolutionException(stateRef.txhash) + } + // These are not used + override val appClassLoader: ClassLoader get() = throw AbstractMethodError() + override fun getTrustedClassAttachment(className: String) = throw AbstractMethodError() + override fun fixupAttachmentIds(attachmentIds: Collection) = throw AbstractMethodError() + }) } - @Suppress("LongParameterList", "ThrowsCount") - private fun toLedgerTransactionInternal( - resolveIdentity: (PublicKey) -> Party?, - resolveAttachment: (SecureHash) -> Attachment?, - resolveStateRefAsSerialized: (StateRef) -> SerializedBytes>?, - resolveParameters: (SecureHash?) -> NetworkParameters?, - isAttachmentTrusted: (Attachment) -> Boolean, - attachmentsClassLoaderCache: AttachmentsClassLoaderCache? - ): LedgerTransaction { + @CordaInternal + @JvmSynthetic + internal fun toLedgerTransactionInternal(verificationSupport: VerificationSupport): LedgerTransaction { // Look up public keys to authenticated identities. - val authenticatedCommands = commands.lazyMapped { cmd, _ -> - val parties = cmd.signers.mapNotNull(resolveIdentity) - CommandWithParties(cmd.signers, parties, cmd.value) + val authenticatedCommands = if (verificationSupport.isResolutionLazy) { + commands.lazyMapped { cmd, _ -> + val parties = verificationSupport.getParties(cmd.signers).filterNotNull() + CommandWithParties(cmd.signers, parties, cmd.value) + } + } else { + val allSigners = commands.flatMapToSet { it.signers } + val allParties = verificationSupport.getParties(allSigners) + commands.map { cmd -> + val parties = cmd.signers.mapNotNull { allParties[allSigners.indexOf(it)] } + CommandWithParties(cmd.signers, parties, cmd.value) + } } // Ensure that the lazy mappings will use the correct SerializationContext. @@ -175,19 +183,28 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr ssar.toStateAndRef(serializationFactory, serializationContext) } - val serializedResolvedInputs = inputs.map { ref -> - SerializedStateAndRef(resolveStateRefAsSerialized(ref) ?: throw TransactionResolutionException(ref.txhash), ref) + val serializedResolvedInputs = inputs.map { + SerializedStateAndRef(verificationSupport.getSerializedState(it), it) } val resolvedInputs = serializedResolvedInputs.lazyMapped(toStateAndRef) - val serializedResolvedReferences = references.map { ref -> - SerializedStateAndRef(resolveStateRefAsSerialized(ref) ?: throw TransactionResolutionException(ref.txhash), ref) + val serializedResolvedReferences = references.map { + SerializedStateAndRef(verificationSupport.getSerializedState(it), it) } val resolvedReferences = serializedResolvedReferences.lazyMapped(toStateAndRef) - val resolvedAttachments = attachments.lazyMapped { att, _ -> resolveAttachment(att) ?: throw AttachmentResolutionException(att) } + val resolvedAttachments = if (verificationSupport.isResolutionLazy) { + attachments.lazyMapped { id, _ -> + verificationSupport.getAttachment(id) ?: throw AttachmentResolutionException(id) + } + } else { + verificationSupport.getAttachments(attachments).mapIndexed { index, attachment -> + attachment ?: throw AttachmentResolutionException(attachments[index]) + } + } - val resolvedNetworkParameters = resolveParameters(networkParametersHash) ?: throw TransactionResolutionException.UnknownParametersException(id, networkParametersHash!!) + val resolvedNetworkParameters = verificationSupport.getNetworkParameters(networkParametersHash) + ?: throw TransactionResolutionException.UnknownParametersException(id, networkParametersHash!!) val ltx = LedgerTransaction.create( resolvedInputs, @@ -203,8 +220,9 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr componentGroups, serializedResolvedInputs, serializedResolvedReferences, - isAttachmentTrusted, - attachmentsClassLoaderCache, + verificationSupport::isAttachmentTrusted, + verificationSupport::createVerifier, + verificationSupport.attachmentsClassLoaderCache, digestService ) @@ -230,15 +248,15 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr // This calculates a value that is slightly lower than the actual re-serialized version. But it is stable and does not depend on the classloader. fun componentGroupSize(componentGroup: ComponentGroupEnum): Int { - return this.componentGroups.firstOrNull { it.groupIndex == componentGroup.ordinal }?.let { cg -> cg.components.sumBy { it.size } + 4 } ?: 0 + return this.componentGroups.firstOrNull { it.groupIndex == componentGroup.ordinal }?.let { cg -> cg.components.sumOf { it.size } + 4 } ?: 0 } // Check attachments size first as they are most likely to go over the limit. With ContractAttachment instances // it's likely that the same underlying Attachment CorDapp will occur more than once so we dedup on the attachment id. ltx.attachments.distinctBy { it.id }.forEach { minus(it.size) } - minus(resolvedSerializedInputs.sumBy { it.serializedState.size }) - minus(resolvedSerializedReferences.sumBy { it.serializedState.size }) + minus(resolvedSerializedInputs.sumOf { it.serializedState.size }) + minus(resolvedSerializedReferences.sumOf { it.serializedState.size }) // For Commands and outputs we can use the component groups as they are already serialized. minus(componentGroupSize(COMMANDS_GROUP)) @@ -273,7 +291,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr // Even if empty and not used, we should at least send oneHashes for each known // or received but unknown (thus, bigger than known ordinal) component groups. val allOnesHash = digestService.allOnesHash - for (i in 0..componentGroups.map { it.groupIndex }.max()!!) { + for (i in 0..componentGroups.maxOf { it.groupIndex }) { val root = groupsMerkleRoots[i] ?: allOnesHash listOfLeaves.add(root) } @@ -340,37 +358,6 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr timeWindow: TimeWindow?): List { return createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow, emptyList(), null) } - - /** - * This is the main logic that knows how to retrieve the binary representation of [StateRef]s. - * - * For [ContractUpgradeWireTransaction] or [NotaryChangeWireTransaction] it knows how to recreate the output state in the - * correct classloader independent of the node's classpath. - */ - @CordaInternal - fun resolveStateRefBinaryComponent(stateRef: StateRef, services: ServicesForResolution): SerializedBytes>? { - return if (services is ServiceHub) { - val coreTransaction = services.validatedTransactions.getTransaction(stateRef.txhash)?.coreTransaction - ?: throw TransactionResolutionException(stateRef.txhash) - // Get the network parameters from the tx or whatever the default params are. - val paramsHash = coreTransaction.networkParametersHash ?: services.networkParametersService.defaultHash - val params = services.networkParametersService.lookup(paramsHash) - ?: throw IllegalStateException("Should have been able to fetch parameters by this point: $paramsHash") - @Suppress("UNCHECKED_CAST") - when (coreTransaction) { - is WireTransaction -> coreTransaction.componentGroups - .firstOrNull { it.groupIndex == OUTPUTS_GROUP.ordinal } - ?.components - ?.get(stateRef.index) as SerializedBytes>? - is ContractUpgradeWireTransaction -> coreTransaction.resolveOutputComponent(services, stateRef, params) - is NotaryChangeWireTransaction -> coreTransaction.resolveOutputComponent(services, stateRef, params) - else -> throw UnsupportedOperationException("Attempting to resolve input ${stateRef.index} of a ${coreTransaction.javaClass} transaction. This is not supported.") - } - } else { - // For backwards compatibility revert to using the node classloader. - services.loadState(stateRef).serialize() - } - } } override fun toString(): String { diff --git a/core/src/test/kotlin/net/corda/core/internal/internalAccessTestHelpers.kt b/core/src/test/kotlin/net/corda/core/internal/InternalAccessTestHelpers.kt similarity index 68% rename from core/src/test/kotlin/net/corda/core/internal/internalAccessTestHelpers.kt rename to core/src/test/kotlin/net/corda/core/internal/InternalAccessTestHelpers.kt index 16a6e6bef8..5f9e48bb2e 100644 --- a/core/src/test/kotlin/net/corda/core/internal/internalAccessTestHelpers.kt +++ b/core/src/test/kotlin/net/corda/core/internal/InternalAccessTestHelpers.kt @@ -1,9 +1,19 @@ package net.corda.core.internal -import net.corda.core.contracts.* +import net.corda.core.contracts.Attachment +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.CommandWithParties +import net.corda.core.contracts.Contract +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.PrivacySalt +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.TimeWindow +import net.corda.core.contracts.TransactionState +import net.corda.core.contracts.TransactionVerificationException import net.corda.core.crypto.DigestService import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party +import net.corda.core.internal.verification.AbstractVerifier import net.corda.core.node.NetworkParameters import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.internal.AttachmentsClassLoaderCache @@ -40,15 +50,35 @@ fun createLedgerTransaction( isAttachmentTrusted: (Attachment) -> Boolean, attachmentsClassLoaderCache: AttachmentsClassLoaderCache, digestService: DigestService = DigestService.default -): LedgerTransaction = LedgerTransaction.create( - inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, references, componentGroups, serializedInputs, serializedReferences, isAttachmentTrusted, attachmentsClassLoaderCache, digestService -).specialise(::PassthroughVerifier) +): LedgerTransaction { + return LedgerTransaction.create( + inputs, + outputs, + commands, + attachments, + id, + notary, + timeWindow, + privacySalt, + networkParameters, + references, + componentGroups, + serializedInputs, + serializedReferences, + isAttachmentTrusted, + ::PassthroughVerifier, + attachmentsClassLoaderCache, + digestService + ) +} fun createContractCreationError(txId: SecureHash, contractClass: String, cause: Throwable) = TransactionVerificationException.ContractCreationError(txId, contractClass, cause) fun createContractRejection(txId: SecureHash, contract: Contract, cause: Throwable) = TransactionVerificationException.ContractRejection(txId, contract, cause) /** * Verify the [LedgerTransaction] we already have. + * + * Note, this is not secure! */ private class PassthroughVerifier(ltx: LedgerTransaction, context: SerializationContext) : AbstractVerifier(ltx, context.deserializationClassLoader) { override val transaction: Supplier diff --git a/finance/contracts/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt b/finance/contracts/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt index 67b075dee0..ee1e0584e6 100644 --- a/finance/contracts/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt +++ b/finance/contracts/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt @@ -4,6 +4,7 @@ import net.corda.core.contracts.* import net.corda.core.identity.AnonymousParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.internal.getRequiredTransaction import net.corda.core.node.NotaryInfo import net.corda.core.node.services.Vault import net.corda.core.transactions.SignedTransaction @@ -284,8 +285,8 @@ class CommercialPaperTestsGeneric { } // Propagate the cash transactions to each side. - aliceServices.recordTransactions(bigCorpCash.states.map { megaCorpServices.validatedTransactions.getTransaction(it.ref.txhash)!! }) - megaCorpServices.recordTransactions(aliceCash.states.map { aliceServices.validatedTransactions.getTransaction(it.ref.txhash)!! }) + aliceServices.recordTransactions(bigCorpCash.states.map { megaCorpServices.getRequiredTransaction(it.ref.txhash) }) + megaCorpServices.recordTransactions(aliceCash.states.map { aliceServices.getRequiredTransaction(it.ref.txhash) }) // MegaCorp™ issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself. val faceValue = 10000.DOLLARS `issued by` dummyCashIssuer.ref(1) diff --git a/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/AttachmentsClassLoaderStaticContractTests.kt b/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/AttachmentsClassLoaderStaticContractTests.kt index cbc8211c72..d08375d8aa 100644 --- a/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/AttachmentsClassLoaderStaticContractTests.kt +++ b/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/AttachmentsClassLoaderStaticContractTests.kt @@ -1,34 +1,24 @@ package net.corda.nodeapitests.internal -import org.mockito.kotlin.any -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import org.mockito.kotlin.whenever -import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash +import net.corda.core.contracts.Command +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.Contract +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.PartyAndReference +import net.corda.core.contracts.StateAndContract +import net.corda.core.contracts.TypeOnlyCommandData import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party -import net.corda.core.node.ServicesForResolution -import net.corda.core.node.services.AttachmentStorage -import net.corda.core.node.services.IdentityService -import net.corda.core.node.services.NetworkParametersService import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder -import net.corda.nodeapi.internal.cordapp.CordappLoader -import net.corda.node.internal.cordapp.CordappProviderImpl -import net.corda.node.internal.cordapp.JarScanningCordappLoader import net.corda.nodeapitests.internal.AttachmentsClassLoaderStaticContractTests.AttachmentDummyContract.Companion.ATTACHMENT_PROGRAM_ID -import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity -import net.corda.testing.internal.MockCordappConfigProvider -import net.corda.coretesting.internal.rigorousMock -import net.corda.testing.node.internal.cordappWithPackages -import net.corda.testing.services.MockAttachmentStorage +import net.corda.testing.node.MockServices import org.assertj.core.api.Assertions.assertThat import org.junit.Assert.assertEquals import org.junit.Rule @@ -69,31 +59,7 @@ class AttachmentsClassLoaderStaticContractTests { } } - private val networkParameters = testNetworkParameters() - - private val networkParametersService get() = mock().also { - doReturn(networkParameters.serialize().hash).whenever(it).currentHash - } - - private val serviceHub get() = rigorousMock().also { - val cordappProviderImpl = CordappProviderImpl(cordappLoaderForPackages(listOf("net.corda.nodeapitests.internal")), MockCordappConfigProvider(), MockAttachmentStorage()) - cordappProviderImpl.start() - doReturn(cordappProviderImpl).whenever(it).cordappProvider - doReturn(networkParametersService).whenever(it).networkParametersService - doReturn(networkParameters).whenever(it).networkParameters - val attachmentStorage = rigorousMock() - doReturn(attachmentStorage).whenever(it).attachments - val attachment = rigorousMock() - doReturn(attachment).whenever(attachmentStorage).openAttachment(any()) - doReturn(it.cordappProvider.getContractAttachmentID(AttachmentDummyContract.ATTACHMENT_PROGRAM_ID)).whenever(attachment).id - doReturn(setOf(AttachmentDummyContract.ATTACHMENT_PROGRAM_ID)).whenever(attachment).allContracts - doReturn("app").whenever(attachment).uploader - doReturn(emptyList()).whenever(attachment).signerKeys - val contractAttachmentId = SecureHash.randomSHA256() - doReturn(listOf(contractAttachmentId)).whenever(attachmentStorage) - .getLatestContractAttachments(AttachmentDummyContract.ATTACHMENT_PROGRAM_ID) - doReturn(mock()).whenever(it).identityService - } + private val serviceHub = MockServices() @Test(timeout=300_000) fun `test serialization of WireTransaction with statically loaded contract`() { @@ -112,8 +78,4 @@ class AttachmentsClassLoaderStaticContractTests { val contractClass = Class.forName(ATTACHMENT_PROGRAM_ID) assertThat(contractClass.getDeclaredConstructor().newInstance()).isInstanceOf(Contract::class.java) } - - private fun cordappLoaderForPackages(packages: Collection): CordappLoader { - return JarScanningCordappLoader.fromJarUrls(listOf(cordappWithPackages(*packages.toTypedArray()).jarFile.toUri().toURL())) - } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt index ee210ca365..598b666c58 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt @@ -1,6 +1,5 @@ package net.corda.nodeapi.internal.persistence -import com.github.benmanes.caffeine.cache.Caffeine import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.castIfPossible import net.corda.core.schemas.MappedSchema @@ -54,7 +53,7 @@ class HibernateConfiguration( val sessionFactoryFactory = findSessionFactoryFactory(jdbcUrl, customClassLoader) - private val sessionFactories = cacheFactory.buildNamed, SessionFactory>(Caffeine.newBuilder(), "HibernateConfiguration_sessionFactories") + private val sessionFactories = cacheFactory.buildNamed, SessionFactory>("HibernateConfiguration_sessionFactories") val sessionFactoryForRegisteredSchemas = schemas.let { logger.info("Init HibernateConfiguration for schemas: $it") diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CustomSerializationSchemeAdapterTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CustomSerializationSchemeAdapterTests.kt index 2d4f751ddf..9671c2dfa6 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CustomSerializationSchemeAdapterTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CustomSerializationSchemeAdapterTests.kt @@ -4,6 +4,7 @@ import net.corda.core.serialization.SerializationSchemeContext import net.corda.core.serialization.CustomSerializationScheme import net.corda.core.utilities.ByteSequence import net.corda.nodeapi.internal.serialization.testutils.serializationContext +import net.corda.serialization.internal.verifier.CustomSerializationSchemeAdapter import org.junit.Test import org.junit.jupiter.api.Assertions.assertTrue import java.io.NotSerializableException diff --git a/node/build.gradle b/node/build.gradle index e3f184e590..81c5f23fec 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -73,6 +73,10 @@ jib.container { processResources { from file("$rootDir/config/dev/log4j2.xml") from file("$rootDir/config/dev/jolokia-access.xml") + from(tasks.findByPath(":verifier:shadowJar")) { + into("net/corda/node/verification") + rename { "external-verifier.jar" } + } } processTestResources { diff --git a/node/src/integration-test/kotlin/net/corda/contracts/mutator/MutatorContract.kt b/node/src/integration-test/kotlin/net/corda/contracts/mutator/MutatorContract.kt index 239525c576..d6a44ff4e6 100644 --- a/node/src/integration-test/kotlin/net/corda/contracts/mutator/MutatorContract.kt +++ b/node/src/integration-test/kotlin/net/corda/contracts/mutator/MutatorContract.kt @@ -8,7 +8,7 @@ import net.corda.core.contracts.TransactionState import net.corda.core.contracts.requireSingleCommand import net.corda.core.contracts.requireThat import net.corda.core.identity.AbstractParty -import net.corda.core.internal.Verifier +import net.corda.core.internal.verification.Verifier import net.corda.core.serialization.SerializationContext import net.corda.core.transactions.LedgerTransaction diff --git a/node/src/integration-test/kotlin/net/corda/node/CustomSerializationSchemeDriverTest.kt b/node/src/integration-test/kotlin/net/corda/node/CustomSerializationSchemeDriverTest.kt index 89677dede5..7a30f4840b 100644 --- a/node/src/integration-test/kotlin/net/corda/node/CustomSerializationSchemeDriverTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/CustomSerializationSchemeDriverTest.kt @@ -58,7 +58,7 @@ import org.objenesis.strategy.StdInstantiatorStrategy import java.io.ByteArrayOutputStream import java.lang.reflect.Modifier import java.security.PublicKey -import java.util.* +import java.util.Arrays import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -94,7 +94,10 @@ class CustomSerializationSchemeDriverTest { @Test(timeout = 300_000) fun `flow can write a wire transaction serialized with custom kryo serializer to the ledger`() { - driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = listOf(enclosedCordapp()))) { + driver(DriverParameters( + cordappsForAllNodes = listOf(enclosedCordapp()), + systemProperties = mapOf("experimental.corda.customSerializationScheme" to KryoScheme::class.java.name) + )) { val (alice, bob) = listOf( startNode(NodeParameters(providedName = ALICE_NAME)), startNode(NodeParameters(providedName = BOB_NAME)) @@ -135,7 +138,7 @@ class CustomSerializationSchemeDriverTest { @StartableByRPC @InitiatingFlow - class WriteTxToLedgerFlow(val counterparty: Party, val notary: Party) : FlowLogic() { + class WriteTxToLedgerFlow(private val counterparty: Party, val notary: Party) : FlowLogic() { @Suspendable override fun call(): SecureHash { val wireTx = createWireTx(serviceHub, notary, counterparty.owningKey, KryoScheme.SCHEME_ID) @@ -146,7 +149,7 @@ class CustomSerializationSchemeDriverTest { return fullySignedTx.id } - fun signWireTx(wireTx: WireTransaction) : SignedTransaction { + private fun signWireTx(wireTx: WireTransaction) : SignedTransaction { val signatureMetadata = SignatureMetadata( serviceHub.myInfo.platformVersion, Crypto.findSignatureScheme(serviceHub.myInfo.legalIdentitiesAndCerts.first().owningKey).schemeNumberID @@ -157,18 +160,18 @@ class CustomSerializationSchemeDriverTest { } } + @Suppress("unused") @InitiatedBy(WriteTxToLedgerFlow::class) class SignWireTxFlow(private val session: FlowSession): FlowLogic() { @Suspendable override fun call(): SignedTransaction { - val signTransactionFlow = object : SignTransactionFlow(session) { - override fun checkTransaction(stx: SignedTransaction) { - return - } - } - val txId = subFlow(signTransactionFlow).id + val txId = subFlow(NoCheckSignTransactionFlow(session)).id return subFlow(ReceiveFinalityFlow(session, expectedTxId = txId)) } + + class NoCheckSignTransactionFlow(session: FlowSession) : SignTransactionFlow(session) { + override fun checkTransaction(stx: SignedTransaction) = Unit + } } @StartableByRPC @@ -226,7 +229,7 @@ class CustomSerializationSchemeDriverTest { @StartableByRPC @InitiatingFlow - class SendFlow(val counterparty: Party) : FlowLogic() { + class SendFlow(private val counterparty: Party) : FlowLogic() { @Suspendable override fun call(): Boolean { val wtx = createWireTx(serviceHub, counterparty, counterparty.owningKey, KryoScheme.SCHEME_ID) @@ -237,13 +240,14 @@ class CustomSerializationSchemeDriverTest { } @StartableByRPC - class CreateWireTxFlow(val counterparty: Party) : FlowLogic() { + class CreateWireTxFlow(private val counterparty: Party) : FlowLogic() { @Suspendable override fun call(): WireTransaction { return createWireTx(serviceHub, counterparty, counterparty.owningKey, KryoScheme.SCHEME_ID) } } + @Suppress("unused") @InitiatedBy(SendFlow::class) class ReceiveFlow(private val session: FlowSession): FlowLogic() { @Suspendable @@ -301,6 +305,7 @@ class CustomSerializationSchemeDriverTest { kryo.isRegistrationRequired = false kryo.instantiatorStrategy = CustomInstantiatorStrategy() kryo.classLoader = classLoader + @Suppress("ReplaceJavaStaticMethodWithKotlinAnalog") kryo.register(Arrays.asList("").javaClass, ArraysAsListSerializer()) } diff --git a/node/src/integration-test/kotlin/net/corda/node/verification/ExternalVerificationTest.kt b/node/src/integration-test/kotlin/net/corda/node/verification/ExternalVerificationTest.kt new file mode 100644 index 0000000000..810b1ada08 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/verification/ExternalVerificationTest.kt @@ -0,0 +1,219 @@ +package net.corda.node.verification + +import co.paralleluniverse.fibers.Suspendable +import com.typesafe.config.ConfigFactory +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.Contract +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.TransactionVerificationException +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.NotaryChangeFlow +import net.corda.core.flows.ReceiveFinalityFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party +import net.corda.core.internal.concurrent.map +import net.corda.core.internal.concurrent.transpose +import net.corda.core.messaging.startFlow +import net.corda.core.node.NodeInfo +import net.corda.core.transactions.LedgerTransaction +import net.corda.core.transactions.NotaryChangeWireTransaction +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow +import net.corda.finance.DOLLARS +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.flows.CashIssueFlow +import net.corda.finance.flows.CashPaymentFlow +import net.corda.node.verification.ExternalVerificationTest.FailExternallyContract.State +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.BOC_NAME +import net.corda.testing.core.CHARLIE_NAME +import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.core.singleIdentity +import net.corda.testing.driver.NodeHandle +import net.corda.testing.driver.NodeParameters +import net.corda.testing.node.NotarySpec +import net.corda.testing.node.internal.FINANCE_CORDAPPS +import net.corda.testing.node.internal.cordappWithPackages +import net.corda.testing.node.internal.enclosedCordapp +import net.corda.testing.node.internal.internalDriver +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.junit.Test +import java.io.File +import java.net.InetAddress +import kotlin.io.path.div +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.name +import kotlin.io.path.readText + +class ExternalVerificationTest { + @Test(timeout=300_000) + fun `regular transactions are verified in external verifier`() { + internalDriver( + systemProperties = mapOf("net.corda.node.verification.external" to "true"), + cordappsForAllNodes = FINANCE_CORDAPPS, + notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = true, startInProcess = false)) + ) { + val (notary, alice, bob) = listOf( + defaultNotaryNode, + startNode(NodeParameters(providedName = ALICE_NAME)), + startNode(NodeParameters(providedName = BOB_NAME)) + ).transpose().getOrThrow() + + val (issuanceTx) = alice.rpc.startFlow( + ::CashIssueFlow, + 10.DOLLARS, + OpaqueBytes.of(0x01), + defaultNotaryIdentity + ).returnValue.getOrThrow() + + val (paymentTx) = alice.rpc.startFlow( + ::CashPaymentFlow, + 10.DOLLARS, + bob.nodeInfo.singleIdentity(), + false, + ).returnValue.getOrThrow() + + notary.assertTransactionsWereVerifiedExternally(issuanceTx.id, paymentTx.id) + bob.assertTransactionsWereVerifiedExternally(issuanceTx.id, paymentTx.id) + } + } + + @Test(timeout=300_000) + fun `regular transactions can fail verification in external verifier`() { + internalDriver( + systemProperties = mapOf("net.corda.node.verification.external" to "true"), + cordappsForAllNodes = listOf(cordappWithPackages("net.corda.node.verification", "com.typesafe.config")) + ) { + val (alice, bob, charlie) = listOf( + startNode(NodeParameters(providedName = ALICE_NAME)), + startNode(NodeParameters(providedName = BOB_NAME)), + startNode(NodeParameters(providedName = CHARLIE_NAME)) + ).transpose().getOrThrow() + + // Create a transaction from Alice to Bob, where Charlie is specified as the contract verification trigger + val firstState = alice.rpc.startFlow(::FailExternallyFlow, null, charlie.nodeInfo, bob.nodeInfo).returnValue.getOrThrow() + // When the transaction chain tries to moves onto Charlie, it will trigger the failure + assertThatExceptionOfType(TransactionVerificationException.ContractRejection::class.java) + .isThrownBy { bob.rpc.startFlow(::FailExternallyFlow, firstState, charlie.nodeInfo, charlie.nodeInfo).returnValue.getOrThrow() } + .withMessageContaining("Fail in external verifier: ${firstState.ref.txhash}") + + // Make sure Charlie tried to verify the first transaction externally + assertThat(charlie.externalVerifierLogs()).contains("Fail in external verifier: ${firstState.ref.txhash}") + } + } + + @Test(timeout=300_000) + fun `notary change transactions are verified in external verifier`() { + internalDriver( + systemProperties = mapOf("net.corda.node.verification.external" to "true"), + cordappsForAllNodes = FINANCE_CORDAPPS + enclosedCordapp(), + notarySpecs = listOf(DUMMY_NOTARY_NAME, BOC_NAME).map { NotarySpec(it, validating = true, startInProcess = false) } + ) { + val (notary1, notary2) = notaryHandles.map { handle -> handle.nodeHandles.map { it[0] } }.transpose().getOrThrow() + val alice = startNode(NodeParameters(providedName = ALICE_NAME)).getOrThrow() + + val txId = alice.rpc.startFlow( + ::IssueAndChangeNotaryFlow, + notary1.nodeInfo.singleIdentity(), + notary2.nodeInfo.singleIdentity() + ).returnValue.getOrThrow() + + notary1.assertTransactionsWereVerifiedExternally(txId) + alice.assertTransactionsWereVerifiedExternally(txId) + } + } + + private fun NodeHandle.assertTransactionsWereVerifiedExternally(vararg txIds: SecureHash) { + val verifierLogContent = externalVerifierLogs() + for (txId in txIds) { + assertThat(verifierLogContent).contains("SignedTransaction(id=$txId) verified") + } + } + + private fun NodeHandle.externalVerifierLogs(): String { + val verifierLogs = (baseDirectory / "logs") + .listDirectoryEntries() + .filter { it.name == "verifier-${InetAddress.getLocalHost().hostName}.log" } + assertThat(verifierLogs).describedAs("External verifier was not started").hasSize(1) + return verifierLogs[0].readText() + } + + class FailExternallyContract : Contract { + override fun verify(tx: LedgerTransaction) { + val command = tx.commandsOfType().single() + if (insideExternalVerifier()) { + // The current directory for the external verifier is the node's base directory + val localName = CordaX500Name.parse(ConfigFactory.parseFile(File("node.conf")).getString("myLegalName")) + check(localName != command.value.failForParty.name) { "Fail in external verifier: ${tx.id}" } + } + } + + private fun insideExternalVerifier(): Boolean { + return StackWalker.getInstance().walk { frames -> + frames.anyMatch { it.className.startsWith("net.corda.verifier.") } + } + } + + data class State(val party: Party) : ContractState { + override val participants: List get() = listOf(party) + } + + data class Command(val failForParty: Party) : CommandData + } + + @StartableByRPC + @InitiatingFlow + class FailExternallyFlow(private val inputState: StateAndRef?, + private val failForParty: NodeInfo, + private val recipient: NodeInfo) : FlowLogic>() { + @Suspendable + override fun call(): StateAndRef { + val myParty = serviceHub.myInfo.legalIdentities[0] + val txBuilder = TransactionBuilder(serviceHub.networkMapCache.notaryIdentities[0]) + inputState?.let(txBuilder::addInputState) + txBuilder.addOutputState(State(myParty), FailExternallyContract::class.java.name) + txBuilder.addCommand(FailExternallyContract.Command(failForParty.legalIdentities[0]), myParty.owningKey) + val initialTx = serviceHub.signInitialTransaction(txBuilder) + val sessions = arrayListOf(initiateFlow(recipient.legalIdentities[0])) + inputState?.let { sessions += initiateFlow(it.state.data.party) } + val notarisedTx = subFlow(FinalityFlow(initialTx, sessions)) + return notarisedTx.toLedgerTransaction(serviceHub).outRef(0) + } + } + + @Suppress("unused") + @InitiatedBy(FailExternallyFlow::class) + class ReceiverFlow(private val otherSide: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + subFlow(ReceiveFinalityFlow(otherSide)) + } + } + + + @StartableByRPC + class IssueAndChangeNotaryFlow(private val oldNotary: Party, private val newNotary: Party) : FlowLogic() { + @Suspendable + override fun call(): SecureHash { + subFlow(CashIssueFlow(10.DOLLARS, OpaqueBytes.of(0x01), oldNotary)) + val oldState = serviceHub.vaultService.queryBy(Cash.State::class.java).states.single() + assertThat(oldState.state.notary).isEqualTo(oldNotary) + val newState = subFlow(NotaryChangeFlow(oldState, newNotary)) + assertThat(newState.state.notary).isEqualTo(newNotary) + val notaryChangeTx = serviceHub.validatedTransactions.getTransaction(newState.ref.txhash) + assertThat(notaryChangeTx?.coreTransaction).isInstanceOf(NotaryChangeWireTransaction::class.java) + return notaryChangeTx!!.id + } + } +} diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 72c31fc33c..ece28a229f 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -38,6 +38,7 @@ import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.concurrent.flatMap import net.corda.core.internal.concurrent.map import net.corda.core.internal.concurrent.openFuture +import net.corda.core.internal.cordapp.CordappProviderInternal import net.corda.core.internal.div import net.corda.core.internal.messaging.AttachmentTrustInfoRPCOps import net.corda.core.internal.notary.NotaryService @@ -47,6 +48,7 @@ import net.corda.core.internal.telemetry.SimpleLogTelemetryComponent import net.corda.core.internal.telemetry.TelemetryComponent import net.corda.core.internal.telemetry.TelemetryServiceImpl import net.corda.core.internal.uncheckedCast +import net.corda.core.internal.verification.VerifyingServiceHub import net.corda.core.messaging.ClientRpcSslOptions import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.RPCOps @@ -55,13 +57,11 @@ import net.corda.core.node.AppServiceHub import net.corda.core.node.NetworkParameters import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub -import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.ContractUpgradeService import net.corda.core.node.services.CordaService import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService import net.corda.core.node.services.TelemetryService -import net.corda.core.node.services.TransactionVerifierService import net.corda.core.node.services.diagnostics.DiagnosticsService import net.corda.core.schemas.MappedSchema import net.corda.core.serialization.SerializationWhitelist @@ -70,7 +70,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.internal.AttachmentsClassLoaderCache import net.corda.core.serialization.internal.AttachmentsClassLoaderCacheImpl import net.corda.core.toFuture -import net.corda.core.transactions.LedgerTransaction +import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.days import net.corda.core.utilities.millis @@ -82,7 +82,6 @@ import net.corda.node.internal.checkpoints.FlowManagerRPCOpsImpl import net.corda.node.internal.classloading.requireAnnotation import net.corda.node.internal.cordapp.CordappConfigFileProvider import net.corda.node.internal.cordapp.CordappProviderImpl -import net.corda.node.internal.cordapp.CordappProviderInternal import net.corda.node.internal.cordapp.JarScanningCordappLoader import net.corda.node.internal.cordapp.VirtualCordapp import net.corda.node.internal.rpc.proxies.AuthenticatedRpcOpsProxy @@ -122,10 +121,10 @@ import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.network.PersistentPartyInfoCache import net.corda.node.services.persistence.AbstractPartyDescriptor import net.corda.node.services.persistence.AbstractPartyToX500NameAsStringConverter +import net.corda.node.services.persistence.AesDbEncryptionService import net.corda.node.services.persistence.AttachmentStorageInternal import net.corda.node.services.persistence.DBCheckpointPerformanceRecorder import net.corda.node.services.persistence.DBCheckpointStorage -import net.corda.node.services.persistence.AesDbEncryptionService import net.corda.node.services.persistence.DBTransactionMappingStorage import net.corda.node.services.persistence.DBTransactionStorageLedgerRecovery import net.corda.node.services.persistence.NodeAttachmentService @@ -141,7 +140,6 @@ import net.corda.node.services.statemachine.FlowOperator import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.SingleThreadedStateMachineManager import net.corda.node.services.statemachine.StateMachineManager -import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.services.upgrade.ContractUpgradeServiceImpl import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.AffinityExecutor @@ -157,6 +155,7 @@ import net.corda.nodeapi.internal.cryptoservice.bouncycastle.BCCryptoService import net.corda.nodeapi.internal.lifecycle.NodeLifecycleEvent import net.corda.nodeapi.internal.lifecycle.NodeLifecycleEventsDistributor import net.corda.nodeapi.internal.lifecycle.NodeServicesContext +import net.corda.nodeapi.internal.namedThreadPoolExecutor import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaTransactionSupportImpl import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException @@ -169,7 +168,6 @@ import net.corda.nodeapi.internal.persistence.RestrictedEntityManager import net.corda.nodeapi.internal.persistence.SchemaMigration import net.corda.nodeapi.internal.persistence.contextDatabase import net.corda.nodeapi.internal.persistence.withoutDatabaseAccess -import net.corda.nodeapi.internal.namedThreadPoolExecutor import org.apache.activemq.artemis.utils.ReusableLatch import org.jolokia.jvmagent.JolokiaServer import org.jolokia.jvmagent.JolokiaServerConfig @@ -181,7 +179,6 @@ import java.sql.Savepoint import java.time.Clock import java.time.Duration import java.time.format.DateTimeParseException -import java.util.ArrayList import java.util.Properties import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -299,22 +296,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val pkToIdCache = PublicKeyToOwningIdentityCacheImpl(database, cacheFactory) @Suppress("LeakingThis") val keyManagementService = makeKeyManagementService(identityService).tokenize() - val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersStorage, transactionStorage).also { - attachments.servicesForResolution = it - } - @Suppress("LeakingThis") - val vaultService = makeVaultService(keyManagementService, servicesForResolution, database, cordappLoader).tokenize() val nodeProperties = NodePropertiesPersistentStore(StubbedNodeUniqueIdProvider::value, database, cacheFactory) val flowLogicRefFactory = makeFlowLogicRefFactoryImpl() // TODO Cancelling parameters updates - if we do that, how we ensure that no one uses cancelled parameters in the transactions? val networkMapUpdater = makeNetworkMapUpdater() - @Suppress("LeakingThis") - val transactionVerifierService = InMemoryTransactionVerifierService( - numberOfWorkers = transactionVerifierWorkerCount, - cordappProvider = cordappProvider, - attachments = attachments - ).tokenize() private val attachmentsClassLoaderCache: AttachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(cacheFactory).tokenize() val contractUpgradeService = ContractUpgradeServiceImpl(cacheFactory).tokenize() val auditService = DummyAuditService().tokenize() @@ -326,7 +312,9 @@ abstract class AbstractNode(val configuration: NodeConfiguration, log.warn("MessagingService subscription error", it) }) } - val services = ServiceHubInternalImpl().tokenize() + val services = ServiceHubImpl().tokenize() + @Suppress("LeakingThis") + val vaultService = makeVaultService(keyManagementService, database, cordappLoader).tokenize() val checkpointStorage = DBCheckpointStorage(DBCheckpointPerformanceRecorder(services.monitoringService.metrics), platformClock) @Suppress("LeakingThis") val smm = makeStateMachineManager() @@ -338,7 +326,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, private val cordappTelemetryComponents = MutableClassToInstanceMap.create() private val shutdownExecutor = Executors.newSingleThreadExecutor(DefaultThreadFactory("Shutdown")) - protected abstract val transactionVerifierWorkerCount: Int /** * Should be [rx.schedulers.Schedulers.io] for production, * or [rx.internal.schedulers.CachedThreadScheduler] (with shutdown registered with [runOnStop]) for shared-JVM testing. @@ -469,8 +456,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, Node.printBasicNodeInfo("Running database schema migration scripts ...") val props = configuration.dataSourceProperties if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.") - var pendingAppChanges: Int = 0 - var pendingCoreChanges: Int = 0 + var pendingAppChanges = 0 + var pendingCoreChanges = 0 database.startHikariPool(props, metricRegistry) { dataSource, haveCheckpoints -> val schemaMigration = SchemaMigration(dataSource, cordappLoader, configuration.networkParametersPath, configuration.myLegalName) if(updateCoreSchemas) { @@ -505,13 +492,13 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val updatedSchemas = listOfNotNull( ("core").takeIf { updateCoreSchemas }, ("app").takeIf { updateAppSchemas } - ).joinToString(separator = " and "); + ).joinToString(separator = " and ") val pendingChanges = listOfNotNull( ("no outstanding").takeIf { pendingAppChanges == 0 && pendingCoreChanges == 0 }, ("$pendingCoreChanges outstanding core").takeIf { !updateCoreSchemas && pendingCoreChanges > 0 }, ("$pendingAppChanges outstanding app").takeIf { !updateAppSchemas && pendingAppChanges > 0 } - ).joinToString(prefix = "There are ", postfix = " database changes."); + ).joinToString(prefix = "There are ", postfix = " database changes.") Node.printBasicNodeInfo("Database migration scripts for $updatedSchemas schemas complete. $pendingChanges") } @@ -832,7 +819,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, networkMapCache, NodeInfoWatcher( configuration.baseDirectory, - @Suppress("LeakingThis") rxIoScheduler, Duration.ofMillis(configuration.additionalNodeInfoPollingFrequencyMsec) ), @@ -846,7 +832,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, platformClock, database, flowStarter, - servicesForResolution, + services, flowLogicRefFactory, nodeProperties, configuration.drainingModePollPeriod, @@ -1160,12 +1146,19 @@ abstract class AbstractNode(val configuration: NodeConfiguration, networkParameters: NetworkParameters) protected open fun makeVaultService(keyManagementService: KeyManagementService, - services: NodeServicesForResolution, database: CordaPersistence, cordappLoader: CordappLoader): VaultServiceInternal { return NodeVaultService(platformClock, keyManagementService, services, database, schemaService, cordappLoader.appClassLoader) } + /** + * Dy default only internal verification is done. + * @see VerifyingServiceHub.tryExternalVerification + */ + protected open fun tryExternalVerification(stx: SignedTransaction, checkSufficientSignatures: Boolean): Boolean { + return true + } + // JDK 11: switch to directly instantiating jolokia server (rather than indirectly via dynamically self attaching Java Agents, // which is no longer supported from JDK 9 onwards (https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8180425). // No longer need to use https://github.com/electronicarts/ea-agent-loader either (which is also deprecated) @@ -1178,7 +1171,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } - inner class ServiceHubInternalImpl : SingletonSerializeAsToken(), ServiceHubInternal, ServicesForResolution by servicesForResolution, NetworkParameterUpdateListener { + inner class ServiceHubImpl : SingletonSerializeAsToken(), ServiceHubInternal, NetworkParameterUpdateListener { override val rpcFlows = ArrayList>>() override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage(database) override val identityService: IdentityService get() = this@AbstractNode.identityService @@ -1191,7 +1184,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, override val nodeProperties: NodePropertiesStore get() = this@AbstractNode.nodeProperties override val database: CordaPersistence get() = this@AbstractNode.database override val monitoringService: MonitoringService get() = this@AbstractNode.monitoringService - override val transactionVerifierService: TransactionVerifierService get() = this@AbstractNode.transactionVerifierService override val contractUpgradeService: ContractUpgradeService get() = this@AbstractNode.contractUpgradeService override val auditService: AuditService get() = this@AbstractNode.auditService override val attachments: AttachmentStorageInternal get() = this@AbstractNode.attachments @@ -1216,6 +1208,10 @@ abstract class AbstractNode(val configuration: NodeConfiguration, private lateinit var _networkParameters: NetworkParameters override val networkParameters: NetworkParameters get() = _networkParameters + init { + this@AbstractNode.attachments.servicesForResolution = this + } + fun start(myInfo: NodeInfo, networkParameters: NetworkParameters) { this._myInfo = myInfo this._networkParameters = networkParameters @@ -1300,13 +1296,13 @@ abstract class AbstractNode(val configuration: NodeConfiguration, this@AbstractNode.runOnStop += runOnStop } - override fun specialise(ltx: LedgerTransaction): LedgerTransaction { - return servicesForResolution.specialise(ltx) - } - override fun onNewNetworkParameters(networkParameters: NetworkParameters) { this._networkParameters = networkParameters } + + override fun tryExternalVerification(stx: SignedTransaction, checkSufficientSignatures: Boolean): Boolean { + return this@AbstractNode.tryExternalVerification(stx, checkSufficientSignatures) + } } } diff --git a/node/src/main/kotlin/net/corda/node/internal/AppServiceHubImpl.kt b/node/src/main/kotlin/net/corda/node/internal/AppServiceHubImpl.kt index 5acf706f91..041a1ec756 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AppServiceHubImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AppServiceHubImpl.kt @@ -4,13 +4,13 @@ import net.corda.core.context.InvocationContext import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByService import net.corda.core.internal.FlowStateMachineHandle +import net.corda.core.internal.ServiceHubCoreInternal import net.corda.core.internal.concurrent.doneFuture import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.FlowHandleImpl import net.corda.core.messaging.FlowProgressHandle import net.corda.core.messaging.FlowProgressHandleImpl import net.corda.core.node.AppServiceHub -import net.corda.core.node.ServiceHub import net.corda.core.node.services.ServiceLifecycleEvent import net.corda.core.node.services.ServiceLifecycleObserver import net.corda.core.node.services.vault.CordaTransactionSupport @@ -24,15 +24,16 @@ import net.corda.nodeapi.internal.lifecycle.NodeLifecycleEventsDistributor import net.corda.nodeapi.internal.lifecycle.NodeLifecycleObserver import net.corda.nodeapi.internal.lifecycle.NodeLifecycleObserver.Companion.reportSuccess import rx.Observable -import java.util.* +import java.util.Objects /** * This customizes the ServiceHub for each [net.corda.core.node.services.CordaService] that is initiating flows. */ -internal class AppServiceHubImpl(private val serviceHub: ServiceHub, private val flowStarter: FlowStarter, +internal class AppServiceHubImpl(private val serviceHub: ServiceHubCoreInternal, + private val flowStarter: FlowStarter, override val database: CordaTransactionSupport, private val nodeLifecycleEventsDistributor: NodeLifecycleEventsDistributor) - : AppServiceHub, ServiceHub by serviceHub { + : AppServiceHub, ServiceHubCoreInternal by serviceHub { companion object { diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 975aa9ea41..ffebec67df 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -7,8 +7,8 @@ import com.github.benmanes.caffeine.cache.Caffeine import com.palominolabs.metrics.newrelic.AllEnabledMetricAttributeFilter import com.palominolabs.metrics.newrelic.NewRelicReporter import io.netty.util.NettyRuntime -import net.corda.nodeapi.internal.rpc.client.AMQPClientSerializationScheme import net.corda.cliutils.ShellConstants +import net.corda.common.logging.errorReporting.NodeDatabaseErrors import net.corda.core.concurrent.CordaFuture import net.corda.core.flows.FlowLogic import net.corda.core.identity.CordaX500Name @@ -26,6 +26,7 @@ import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub import net.corda.core.serialization.internal.SerializationEnvironment import net.corda.core.serialization.internal.nodeSerializationEnv +import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.node.CordaClock @@ -36,9 +37,6 @@ import net.corda.node.internal.artemis.BrokerAddresses import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.internal.security.RPCSecurityManagerWithAdditionalUser -import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme -import net.corda.nodeapi.internal.serialization.kryo.KRYO_CHECKPOINT_CONTEXT -import net.corda.nodeapi.internal.serialization.kryo.KryoCheckpointSerializer import net.corda.node.services.Permissions import net.corda.node.services.api.FlowStarter import net.corda.node.services.api.ServiceHubInternal @@ -64,9 +62,8 @@ import net.corda.node.utilities.BindableNamedCacheFactory import net.corda.node.utilities.DefaultNamedCacheFactory import net.corda.node.utilities.DemoClock import net.corda.node.utilities.errorAndTerminate +import net.corda.node.verification.ExternalVerifierHandle import net.corda.nodeapi.internal.ArtemisMessagingClient -import net.corda.common.logging.errorReporting.NodeDatabaseErrors -import net.corda.node.internal.classloading.scanForCustomSerializationScheme import net.corda.nodeapi.internal.ShutdownHook import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.bridging.BridgeControlListener @@ -74,6 +71,10 @@ import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException import net.corda.nodeapi.internal.protonwrapper.netty.toRevocationConfig +import net.corda.nodeapi.internal.rpc.client.AMQPClientSerializationScheme +import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme +import net.corda.nodeapi.internal.serialization.kryo.KRYO_CHECKPOINT_CONTEXT +import net.corda.nodeapi.internal.serialization.kryo.KryoCheckpointSerializer import net.corda.serialization.internal.AMQP_P2P_CONTEXT import net.corda.serialization.internal.AMQP_RPC_CLIENT_CONTEXT import net.corda.serialization.internal.AMQP_RPC_SERVER_CONTEXT @@ -81,6 +82,7 @@ import net.corda.serialization.internal.AMQP_STORAGE_CONTEXT import net.corda.serialization.internal.SerializationFactoryImpl import net.corda.serialization.internal.amqp.SerializationFactoryCacheKey import net.corda.serialization.internal.amqp.SerializerFactory +import net.corda.serialization.internal.verifier.loadCustomSerializationScheme import org.apache.commons.lang3.JavaVersion import org.apache.commons.lang3.SystemUtils import org.h2.jdbc.JdbcSQLNonTransientConnectionException @@ -92,7 +94,6 @@ import java.lang.Long.max import java.lang.Long.min import java.net.BindException import java.net.InetAddress -import java.nio.file.Path import java.nio.file.Paths import java.time.Clock import java.util.concurrent.TimeUnit @@ -194,13 +195,14 @@ open class Node(configuration: NodeConfiguration, } override val log: Logger get() = staticLog - override val transactionVerifierWorkerCount: Int get() = 4 private var internalRpcMessagingClient: InternalRPCMessagingClient? = null private var rpcBroker: ArtemisBroker? = null protected open val journalBufferTimeout : Int? = null + private val externalVerifierHandle = ExternalVerifierHandle(services).also { runOnStop += it::close } + private var shutdownHook: ShutdownHook? = null // DISCUSSION @@ -297,7 +299,7 @@ open class Node(configuration: NodeConfiguration, printBasicNodeInfo("Advertised P2P messaging addresses", nodeInfo.addresses.joinToString()) val rpcServerConfiguration = RPCServerConfiguration.DEFAULT - rpcServerAddresses?.let { + rpcServerAddresses.let { internalRpcMessagingClient = InternalRPCMessagingClient(configuration.p2pSslOptions, it.admin, MAX_RPC_MESSAGE_SIZE, CordaX500Name.build(configuration.p2pSslOptions.keyStore.get()[X509Utilities.CORDA_CLIENT_TLS].subjectX500Principal), rpcServerConfiguration) printBasicNodeInfo("RPC connection address", it.primary.toString()) printBasicNodeInfo("RPC admin connection address", it.admin.toString()) @@ -353,22 +355,18 @@ open class Node(configuration: NodeConfiguration, ) } - private fun startLocalRpcBroker(securityManager: RPCSecurityManager): BrokerAddresses? { - return with(configuration) { - rpcOptions.address.let { - val rpcBrokerDirectory: Path = baseDirectory / "brokers" / "rpc" - with(rpcOptions) { - rpcBroker = if (useSsl) { - ArtemisRpcBroker.withSsl(configuration.p2pSslOptions, this.address, adminAddress, sslConfig!!, securityManager, MAX_RPC_MESSAGE_SIZE, - journalBufferTimeout, jmxMonitoringHttpPort != null, rpcBrokerDirectory, shouldStartLocalShell()) - } else { - ArtemisRpcBroker.withoutSsl(configuration.p2pSslOptions, this.address, adminAddress, securityManager, MAX_RPC_MESSAGE_SIZE, - journalBufferTimeout, jmxMonitoringHttpPort != null, rpcBrokerDirectory, shouldStartLocalShell()) - } - } - rpcBroker!!.addresses + private fun startLocalRpcBroker(securityManager: RPCSecurityManager): BrokerAddresses { + val rpcBrokerDirectory = configuration.baseDirectory / "brokers" / "rpc" + with(configuration.rpcOptions) { + rpcBroker = if (useSsl) { + ArtemisRpcBroker.withSsl(configuration.p2pSslOptions, this.address, adminAddress, sslConfig!!, securityManager, MAX_RPC_MESSAGE_SIZE, + journalBufferTimeout, configuration.jmxMonitoringHttpPort != null, rpcBrokerDirectory, configuration.shouldStartLocalShell()) + } else { + ArtemisRpcBroker.withoutSsl(configuration.p2pSslOptions, this.address, adminAddress, securityManager, MAX_RPC_MESSAGE_SIZE, + journalBufferTimeout, configuration.jmxMonitoringHttpPort != null, rpcBrokerDirectory, configuration.shouldStartLocalShell()) } } + return rpcBroker!!.addresses } override fun myAddresses(): List = listOf(getAdvertisedAddress()) + configuration.additionalP2PAddresses @@ -392,7 +390,7 @@ open class Node(configuration: NodeConfiguration, * machine's public IP address to be used instead by looking through the network interfaces. */ private fun tryDetectIfNotPublicHost(host: String): String? { - return if (host.toLowerCase() == "localhost") { + return if (host.lowercase() == "localhost") { log.warn("p2pAddress specified as localhost. Trying to autodetect a suitable public address to advertise in network map." + "To disable autodetect set detectPublicIp = false in the node.conf, or consider using messagingServerAddress and messagingServerExternal") val foundPublicIP = AddressUtils.tryDetectPublicIP() @@ -572,7 +570,7 @@ open class Node(configuration: NodeConfiguration, if (!initialiseSerialization) return val classloader = cordappLoader.appClassLoader val customScheme = System.getProperty("experimental.corda.customSerializationScheme")?.let { - scanForCustomSerializationScheme(it, classloader) + loadCustomSerializationScheme(it, classloader) } nodeSerializationEnv = SerializationEnvironment.with( SerializationFactoryImpl().apply { @@ -590,6 +588,17 @@ open class Node(configuration: NodeConfiguration, ) } + override fun tryExternalVerification(stx: SignedTransaction, checkSufficientSignatures: Boolean): Boolean { + // TODO Determine from transaction whether it should be verified externally + // TODO If both old and new attachments are present then return true so that internal verification is also done. + return if (java.lang.Boolean.getBoolean("net.corda.node.verification.external")) { + externalVerifierHandle.verifyTransaction(stx, checkSufficientSignatures) + false + } else { + true + } + } + /** Starts a blocking event loop for message dispatch. */ fun run() { internalRpcMessagingClient?.start(rpcBroker!!.serverControl) diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeServicesForResolution.kt b/node/src/main/kotlin/net/corda/node/internal/NodeServicesForResolution.kt deleted file mode 100644 index 5baa528297..0000000000 --- a/node/src/main/kotlin/net/corda/node/internal/NodeServicesForResolution.kt +++ /dev/null @@ -1,15 +0,0 @@ -package net.corda.node.internal - -import net.corda.core.contracts.ContractState -import net.corda.core.contracts.StateAndRef -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TransactionResolutionException -import net.corda.core.node.ServicesForResolution -import java.util.LinkedHashSet - -interface NodeServicesForResolution : ServicesForResolution { - @Throws(TransactionResolutionException::class) - override fun loadStates(stateRefs: Set): Set> = loadStates(stateRefs, LinkedHashSet()) - - fun >> loadStates(input: Iterable, output: C): C -} diff --git a/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt b/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt deleted file mode 100644 index ffb21894c1..0000000000 --- a/node/src/main/kotlin/net/corda/node/internal/ServicesForResolutionImpl.kt +++ /dev/null @@ -1,85 +0,0 @@ -package net.corda.node.internal - -import net.corda.core.contracts.Attachment -import net.corda.core.contracts.AttachmentResolutionException -import net.corda.core.contracts.ContractAttachment -import net.corda.core.contracts.ContractState -import net.corda.core.contracts.StateAndRef -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TransactionResolutionException -import net.corda.core.contracts.TransactionState -import net.corda.core.cordapp.CordappProvider -import net.corda.core.crypto.SecureHash -import net.corda.core.internal.SerializedStateAndRef -import net.corda.core.internal.uncheckedCast -import net.corda.core.node.NetworkParameters -import net.corda.core.node.services.AttachmentStorage -import net.corda.core.node.services.IdentityService -import net.corda.core.node.services.NetworkParametersService -import net.corda.core.node.services.TransactionStorage -import net.corda.core.transactions.BaseTransaction -import net.corda.core.transactions.ContractUpgradeWireTransaction -import net.corda.core.transactions.NotaryChangeWireTransaction -import net.corda.core.transactions.SignedTransaction -import net.corda.core.transactions.WireTransaction -import net.corda.core.transactions.WireTransaction.Companion.resolveStateRefBinaryComponent - -data class ServicesForResolutionImpl( - override val identityService: IdentityService, - override val attachments: AttachmentStorage, - override val cordappProvider: CordappProvider, - override val networkParametersService: NetworkParametersService, - private val validatedTransactions: TransactionStorage -) : NodeServicesForResolution { - override val networkParameters: NetworkParameters get() = networkParametersService.lookup(networkParametersService.currentHash) ?: - throw IllegalArgumentException("No current parameters in network parameters storage") - - @Throws(TransactionResolutionException::class) - override fun loadState(stateRef: StateRef): TransactionState<*> { - return toBaseTransaction(stateRef.txhash).outputs[stateRef.index] - } - - override fun >> loadStates(input: Iterable, output: C): C { - val baseTxs = HashMap() - return input.mapTo(output) { stateRef -> - val baseTx = baseTxs.computeIfAbsent(stateRef.txhash, ::toBaseTransaction) - StateAndRef(uncheckedCast(baseTx.outputs[stateRef.index]), stateRef) - } - } - - @Throws(TransactionResolutionException::class, AttachmentResolutionException::class) - override fun loadContractAttachment(stateRef: StateRef): Attachment { - // We may need to recursively chase transactions if there are notary changes. - fun inner(stateRef: StateRef, forContractClassName: String?): Attachment { - val ctx = getSignedTransaction(stateRef.txhash).coreTransaction - when (ctx) { - is WireTransaction -> { - val transactionState = ctx.outRef(stateRef.index).state - for (attachmentId in ctx.attachments) { - val attachment = attachments.openAttachment(attachmentId) - if (attachment is ContractAttachment && (forContractClassName ?: transactionState.contract) in attachment.allContracts) { - return attachment - } - } - throw AttachmentResolutionException(stateRef.txhash) - } - is ContractUpgradeWireTransaction -> { - return attachments.openAttachment(ctx.upgradedContractAttachmentId) ?: throw AttachmentResolutionException(stateRef.txhash) - } - is NotaryChangeWireTransaction -> { - val transactionState = SerializedStateAndRef(resolveStateRefBinaryComponent(stateRef, this)!!, stateRef).toStateAndRef().state - // TODO: check only one (or until one is resolved successfully), max recursive invocations check? - return ctx.inputs.map { inner(it, transactionState.contract) }.firstOrNull() ?: throw AttachmentResolutionException(stateRef.txhash) - } - else -> throw UnsupportedOperationException("Attempting to resolve attachment for index ${stateRef.index} of a ${ctx.javaClass} transaction. This is not supported.") - } - } - return inner(stateRef, null) - } - - private fun toBaseTransaction(txhash: SecureHash): BaseTransaction = getSignedTransaction(txhash).resolveBaseTransaction(this) - - private fun getSignedTransaction(txhash: SecureHash): SignedTransaction { - return validatedTransactions.getTransaction(txhash) ?: throw TransactionResolutionException(txhash) - } -} diff --git a/node/src/main/kotlin/net/corda/node/internal/classloading/Utils.kt b/node/src/main/kotlin/net/corda/node/internal/classloading/Utils.kt index bb49eeb179..958e879981 100644 --- a/node/src/main/kotlin/net/corda/node/internal/classloading/Utils.kt +++ b/node/src/main/kotlin/net/corda/node/internal/classloading/Utils.kt @@ -2,31 +2,6 @@ package net.corda.node.internal.classloading -import net.corda.core.serialization.CustomSerializationScheme -import net.corda.node.internal.ConfigurationException -import net.corda.nodeapi.internal.serialization.CustomSerializationSchemeAdapter -import net.corda.serialization.internal.SerializationScheme -import java.lang.reflect.Constructor - inline fun Class<*>.requireAnnotation(): A { return requireNotNull(getDeclaredAnnotation(A::class.java)) { "$name needs to be annotated with ${A::class.java.name}" } } - -fun scanForCustomSerializationScheme(className: String, classLoader: ClassLoader) : SerializationScheme { - val schemaClass = try { - Class.forName(className, false, classLoader) - } catch (exception: ClassNotFoundException) { - throw ConfigurationException("$className was declared as a custom serialization scheme but could not be found.") - } - val constructor = validateScheme(schemaClass, className) - return CustomSerializationSchemeAdapter(constructor.newInstance() as CustomSerializationScheme) -} - -private fun validateScheme(clazz: Class<*>, className: String): Constructor<*> { - if (!clazz.interfaces.contains(CustomSerializationScheme::class.java)) { - throw ConfigurationException("$className was declared as a custom serialization scheme but does not implement" + - " ${CustomSerializationScheme::class.java.canonicalName}") - } - return clazz.constructors.singleOrNull { it.parameters.isEmpty() } ?: throw ConfigurationException("$className was declared as a " + - "custom serialization scheme but does not have a no argument constructor.") -} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt index 79a657b0ad..3153db4853 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt @@ -1,7 +1,6 @@ package net.corda.node.internal.cordapp import com.google.common.collect.HashBiMap -import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractClassName import net.corda.core.cordapp.Cordapp import net.corda.core.cordapp.CordappContext @@ -9,19 +8,16 @@ import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER import net.corda.core.internal.cordapp.CordappImpl -import net.corda.core.internal.isUploaderTrusted -import net.corda.core.node.services.AttachmentFixup +import net.corda.core.internal.cordapp.CordappProviderInternal +import net.corda.core.internal.verification.AttachmentFixups import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentStorage -import net.corda.core.serialization.MissingAttachmentsException import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.core.utilities.contextLogger import net.corda.node.services.persistence.AttachmentStorageInternal import net.corda.nodeapi.internal.cordapp.CordappLoader -import java.net.JarURLConnection import java.net.URL +import java.nio.file.FileAlreadyExistsException import java.util.concurrent.ConcurrentHashMap -import java.util.jar.JarFile /** * Cordapp provider and store. For querying CorDapps for their attachment and vice versa. @@ -29,14 +25,11 @@ import java.util.jar.JarFile open class CordappProviderImpl(val cordappLoader: CordappLoader, private val cordappConfigProvider: CordappConfigProvider, private val attachmentStorage: AttachmentStorage) : SingletonSerializeAsToken(), CordappProviderInternal { - companion object { - const val COMMENT_MARKER = '#' - private val log = contextLogger() - } - private val contextCache = ConcurrentHashMap() private val cordappAttachments = HashBiMap.create() - private val attachmentFixups = arrayListOf() + private val attachmentFixups = AttachmentFixups() + + override val appClassLoader: ClassLoader get() = cordappLoader.appClassLoader /** * Current known CorDapps loaded on this node @@ -47,7 +40,7 @@ open class CordappProviderImpl(val cordappLoader: CordappLoader, cordappAttachments.putAll(loadContractsIntoAttachmentStore()) verifyInstalledCordapps() // Load the fix-ups after uploading any new contracts into attachment storage. - attachmentFixups.addAll(loadAttachmentFixups()) + attachmentFixups.load(cordappLoader.appClassLoader) } private fun verifyInstalledCordapps() { @@ -79,116 +72,35 @@ open class CordappProviderImpl(val cordappLoader: CordappLoader, */ fun getCordappAttachmentId(cordapp: Cordapp): SecureHash? = cordappAttachments.inverse()[cordapp.jarPath] - private fun loadContractsIntoAttachmentStore(): Map = - cordapps.filter { it.contractClassNames.isNotEmpty() }.map { cordapp -> - cordapp.jarPath.openStream().use { stream -> - try { - // This code can be reached by [MockNetwork] tests which uses [MockAttachmentStorage] - // [MockAttachmentStorage] cannot implement [AttachmentStorageInternal] because - // doing so results in internal functions being exposed in the public API. - if (attachmentStorage is AttachmentStorageInternal) { - attachmentStorage.privilegedImportAttachment( + private fun loadContractsIntoAttachmentStore(): Map { + return cordapps.filter { it.contractClassNames.isNotEmpty() }.associate { cordapp -> + cordapp.jarPath.openStream().use { stream -> + try { + // This code can be reached by [MockNetwork] tests which uses [MockAttachmentStorage] + // [MockAttachmentStorage] cannot implement [AttachmentStorageInternal] because + // doing so results in internal functions being exposed in the public API. + if (attachmentStorage is AttachmentStorageInternal) { + attachmentStorage.privilegedImportAttachment( stream, DEPLOYED_CORDAPP_UPLOADER, cordapp.info.shortName - ) - } else { - attachmentStorage.importAttachment( + ) + } else { + attachmentStorage.importAttachment( stream, DEPLOYED_CORDAPP_UPLOADER, cordapp.info.shortName - ) - } - } catch (faee: java.nio.file.FileAlreadyExistsException) { - AttachmentId.create(faee.message!!) + ) } - } to cordapp.jarPath - }.toMap() - - /** - * Loads the "fixup" rules from all META-INF/Corda-Fixups files. - * These files have the following format: - * ,...=>,,... - * where each is the SHA256 of a CorDapp JAR that - * [net.corda.core.transactions.TransactionBuilder] will expect to find - * inside [AttachmentStorage]. - * - * These rules are for repairing broken CorDapps. A correctly written - * CorDapp should not require them. - */ - private fun loadAttachmentFixups(): List { - return cordappLoader.appClassLoader.getResources("META-INF/Corda-Fixups").asSequence() - .mapNotNull { fixup -> - fixup.openConnection() as? JarURLConnection - }.filter { fixupConnection -> - isValidFixup(fixupConnection.jarFile) - }.flatMapTo(ArrayList()) { fixupConnection -> - fixupConnection.inputStream.bufferedReader().useLines { lines -> - lines.map { it.substringBefore(COMMENT_MARKER) }.map(String::trim).filterNot(String::isEmpty).map { line -> - val tokens = line.split("=>") - require(tokens.size == 2) { - "Invalid fix-up line '$line' in '${fixupConnection.jarFile.name}'" - } - val source = parseIds(tokens[0]) - require(source.isNotEmpty()) { - "Forbidden empty list of source attachment IDs in '${fixupConnection.jarFile.name}'" - } - val target = parseIds(tokens[1]) - Pair(source, target) - }.toList().asSequence() + } catch (faee: FileAlreadyExistsException) { + AttachmentId.create(faee.message!!) } - } - } - - private fun isValidFixup(jarFile: JarFile): Boolean { - return jarFile.entries().asSequence().all { it.name.startsWith("META-INF/") }.also { isValid -> - if (!isValid) { - log.warn("FixUp '{}' contains files outside META-INF/ - IGNORING!", jarFile.name) - } + } to cordapp.jarPath } } - private fun parseIds(ids: String): Set { - return ids.split(",").map(String::trim) - .filterNot(String::isEmpty) - .mapTo(LinkedHashSet(), SecureHash.Companion::create) - } - - /** - * Apply this node's attachment fix-up rules to the given attachment IDs. - * - * @param attachmentIds A collection of [AttachmentId]s, e.g. as provided by a transaction. - * @return The [attachmentIds] with the fix-up rules applied. - */ override fun fixupAttachmentIds(attachmentIds: Collection): Set { - val replacementIds = LinkedHashSet(attachmentIds) - attachmentFixups.forEach { (source, target) -> - if (replacementIds.containsAll(source)) { - replacementIds.removeAll(source) - replacementIds.addAll(target) - } - } - return replacementIds - } - - /** - * Apply this node's attachment fix-up rules to the given attachments. - * - * @param attachments A collection of [Attachment] objects, e.g. as provided by a transaction. - * @return The [attachments] with the node's fix-up rules applied. - */ - override fun fixupAttachments(attachments: Collection): Collection { - val attachmentsById = attachments.associateByTo(LinkedHashMap(), Attachment::id) - val replacementIds = fixupAttachmentIds(attachmentsById.keys) - attachmentsById.keys.retainAll(replacementIds) - (replacementIds - attachmentsById.keys).forEach { extraId -> - val extraAttachment = attachmentStorage.openAttachment(extraId) - if (extraAttachment == null || !extraAttachment.isUploaderTrusted()) { - throw MissingAttachmentsException(listOf(extraId)) - } - attachmentsById[extraId] = extraAttachment - } - return attachmentsById.values + return attachmentFixups.fixupAttachmentIds(attachmentIds) } /** diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderInternal.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderInternal.kt deleted file mode 100644 index ed8f410bfa..0000000000 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderInternal.kt +++ /dev/null @@ -1,14 +0,0 @@ -package net.corda.node.internal.cordapp - -import net.corda.core.contracts.Attachment -import net.corda.core.cordapp.Cordapp -import net.corda.core.cordapp.CordappProvider -import net.corda.core.flows.FlowLogic -import net.corda.core.internal.CordappFixupInternal -import net.corda.core.internal.cordapp.CordappImpl - -interface CordappProviderInternal : CordappProvider, CordappFixupInternal { - val cordapps: List - fun getCordappForFlow(flowLogic: FlowLogic<*>): Cordapp? - fun fixupAttachments(attachments: Collection): Collection -} diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt index 8fb8e8c671..75f0a759a5 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt @@ -9,15 +9,33 @@ import net.corda.core.CordaRuntimeException import net.corda.core.cordapp.Cordapp import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 -import net.corda.core.flows.* -import net.corda.core.internal.* +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.SchedulableFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.flows.StartableByService +import net.corda.core.internal.JAVA_17_CLASS_FILE_FORMAT_MAJOR_VERSION +import net.corda.core.internal.JAVA_1_2_CLASS_FILE_FORMAT_MAJOR_VERSION +import net.corda.core.internal.JarSignatureCollector +import net.corda.core.internal.PlatformVersionSwitches import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.internal.cordapp.CordappImpl.Companion.UNKNOWN_INFO import net.corda.core.internal.cordapp.get +import net.corda.core.internal.exists +import net.corda.core.internal.hash +import net.corda.core.internal.isAbstractClass +import net.corda.core.internal.list +import net.corda.core.internal.loadClassOfType +import net.corda.core.internal.location import net.corda.core.internal.notary.NotaryService import net.corda.core.internal.notary.SinglePartyNotaryService -import net.corda.core.node.services.CordaService +import net.corda.core.internal.objectOrNewInstance +import net.corda.core.internal.pooledScan +import net.corda.core.internal.readFully import net.corda.core.internal.telemetry.TelemetryComponent +import net.corda.core.internal.toTypedArray +import net.corda.core.internal.warnContractWithoutConstraintPropagation +import net.corda.core.node.services.CordaService import net.corda.core.schemas.MappedSchema import net.corda.core.serialization.CheckpointCustomSerializer import net.corda.core.serialization.SerializationCustomSerializer @@ -33,12 +51,12 @@ import java.math.BigInteger import java.net.URL import java.net.URLClassLoader import java.nio.file.Path -import java.util.* +import java.util.Random +import java.util.ServiceLoader import java.util.concurrent.ConcurrentHashMap import java.util.jar.JarInputStream import java.util.jar.Manifest import java.util.zip.ZipInputStream -import kotlin.collections.LinkedHashSet import kotlin.reflect.KClass /** @@ -363,7 +381,7 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: private fun loadClass(className: String, type: KClass): Class? { return try { - Class.forName(className, false, appClassLoader).asSubclass(type.java) + loadClassOfType(type.java, className, false, appClassLoader) } catch (e: ClassCastException) { logger.warn("As $className must be a sub-type of ${type.java.name}") null diff --git a/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt b/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt index 605bf0bd71..f246c02330 100644 --- a/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt @@ -1,8 +1,6 @@ package net.corda.node.internal.security - import com.github.benmanes.caffeine.cache.Cache -import com.github.benmanes.caffeine.cache.Caffeine import com.google.common.primitives.Ints import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.uncheckedCast @@ -241,7 +239,7 @@ private class CaffeineCacheManager(val maxSize: Long, private fun buildCache(name: String): ShiroCache { logger.info("Constructing cache '$name' with maximumSize=$maxSize, TTL=${timeToLiveSeconds}s") - return cacheFactory.buildNamed(Caffeine.newBuilder(), "RPCSecurityManagerShiroCache_$name").toShiroCache() + return cacheFactory.buildNamed("RPCSecurityManagerShiroCache_$name").toShiroCache() } companion object { diff --git a/node/src/main/kotlin/net/corda/node/migration/CordaMigration.kt b/node/src/main/kotlin/net/corda/node/migration/CordaMigration.kt index 4c28ab7304..717b94a5d1 100644 --- a/node/src/main/kotlin/net/corda/node/migration/CordaMigration.kt +++ b/node/src/main/kotlin/net/corda/node/migration/CordaMigration.kt @@ -7,17 +7,13 @@ import liquibase.database.jvm.JdbcConnection import liquibase.exception.ValidationErrors import liquibase.resource.ResourceAccessor import net.corda.core.schemas.MappedSchema -import net.corda.node.SimpleClock import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.persistence.AbstractPartyToX500NameAsStringConverter -import net.corda.node.services.persistence.DBTransactionStorage -import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.services.persistence.PublicKeyToTextConverter import net.corda.nodeapi.internal.persistence.CordaPersistence import java.io.PrintWriter import java.sql.Connection import java.sql.SQLFeatureNotSupportedException -import java.time.Clock import java.util.logging.Logger import javax.sql.DataSource @@ -39,11 +35,6 @@ abstract class CordaMigration : CustomTaskChange { private lateinit var _cordaDB: CordaPersistence - val servicesForResolution: MigrationServicesForResolution - get() = _servicesForResolution - - private lateinit var _servicesForResolution: MigrationServicesForResolution - /** * Initialise a subset of node services so that data from these can be used to perform migrations. * @@ -60,12 +51,6 @@ abstract class CordaMigration : CustomTaskChange { _cordaDB = createDatabase(url, cacheFactory, identityService, schema) cordaDB.start(dataSource) identityService.database = cordaDB - - cordaDB.transaction { - val dbTransactions = DBTransactionStorage(cordaDB, cacheFactory, SimpleClock(Clock.systemUTC())) - val attachmentsService = NodeAttachmentService(metricRegistry, cacheFactory, cordaDB) - _servicesForResolution = MigrationServicesForResolution(identityService, attachmentsService, dbTransactions, cordaDB, cacheFactory) - } } private fun createDatabase(jdbcUrl: String, diff --git a/node/src/main/kotlin/net/corda/node/migration/MigrationServicesForResolution.kt b/node/src/main/kotlin/net/corda/node/migration/MigrationServicesForResolution.kt deleted file mode 100644 index 0186b9659c..0000000000 --- a/node/src/main/kotlin/net/corda/node/migration/MigrationServicesForResolution.kt +++ /dev/null @@ -1,175 +0,0 @@ -package net.corda.node.migration - -import net.corda.core.contracts.* -import net.corda.core.cordapp.CordappContext -import net.corda.core.cordapp.CordappProvider -import net.corda.core.crypto.SecureHash -import net.corda.core.internal.deserialiseComponentGroup -import net.corda.core.internal.div -import net.corda.core.internal.readObject -import net.corda.core.node.NetworkParameters -import net.corda.core.node.ServicesForResolution -import net.corda.core.node.services.AttachmentId -import net.corda.core.node.services.IdentityService -import net.corda.core.node.services.NetworkParametersService -import net.corda.core.node.services.TransactionStorage -import net.corda.core.serialization.deserialize -import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder -import net.corda.core.serialization.internal.AttachmentsClassLoaderCache -import net.corda.core.serialization.internal.AttachmentsClassLoaderCacheImpl -import net.corda.core.transactions.ContractUpgradeLedgerTransaction -import net.corda.core.transactions.NotaryChangeLedgerTransaction -import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.contextLogger -import net.corda.node.internal.DBNetworkParametersStorage -import net.corda.node.services.attachments.NodeAttachmentTrustCalculator -import net.corda.node.services.persistence.AttachmentStorageInternal -import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME -import net.corda.nodeapi.internal.network.SignedNetworkParameters -import net.corda.nodeapi.internal.persistence.CordaPersistence -import net.corda.nodeapi.internal.persistence.SchemaMigration -import java.nio.file.Paths -import java.time.Clock -import java.time.Duration -import java.util.Comparator.comparingInt - -class MigrationServicesForResolution( - override val identityService: IdentityService, - override val attachments: AttachmentStorageInternal, - private val transactions: TransactionStorage, - private val cordaDB: CordaPersistence, - cacheFactory: MigrationNamedCacheFactory -): ServicesForResolution { - - companion object { - val logger = contextLogger() - } - override val cordappProvider: CordappProvider - get() = object : CordappProvider { - - val cordappLoader = SchemaMigration.loader.get() - - override fun getAppContext(): CordappContext { - TODO("not implemented") - } - - override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? { - TODO("not implemented") - } - } - private val cordappLoader = SchemaMigration.loader.get() - - private val attachmentTrustCalculator = NodeAttachmentTrustCalculator( - attachments, - cacheFactory - ) - - private val attachmentsClassLoaderCache: AttachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(cacheFactory) - - private fun defaultNetworkParameters(): NetworkParameters { - logger.warn("Using a dummy set of network parameters for migration.") - val clock = Clock.systemUTC() - return NetworkParameters( - 1, - listOf(), - 1, - 1, - clock.instant(), - 1, - mapOf(), - Duration.ZERO, - mapOf() - ) - } - - private fun getNetworkParametersFromFile(): SignedNetworkParameters? { - return try { - val dir = System.getProperty(SchemaMigration.NODE_BASE_DIR_KEY) - val path = Paths.get(dir) / NETWORK_PARAMS_FILE_NAME - path.readObject() - } catch (e: Exception) { - logger.info("Couldn't find network parameters file: ${e.message}. This is expected if the node is starting for the first time.") - null - } - } - - override val networkParametersService: NetworkParametersService = object : NetworkParametersService { - - private val storage = DBNetworkParametersStorage.createParametersMap(cacheFactory) - - private val filedParams = getNetworkParametersFromFile() - - override val defaultHash: SecureHash = filedParams?.raw?.hash ?: SecureHash.getZeroHash() - override val currentHash: SecureHash = cordaDB.transaction { - storage.allPersisted.use { - it.max(comparingInt { it.second.verified().epoch }).map { it.first }.orElse(defaultHash) - } - } - - override fun lookup(hash: SecureHash): NetworkParameters? { - // Note that the parameters in any file shouldn't be put into the database - this will be done by the node on startup. - return if (hash == filedParams?.raw?.hash) { - filedParams.raw.deserialize() - } else { - cordaDB.transaction { storage[hash]?.verified() } - } - } - } - - override val networkParameters: NetworkParameters = networkParametersService.lookup(networkParametersService.currentHash) - ?: getNetworkParametersFromFile()?.raw?.deserialize() - ?: defaultNetworkParameters() - - private fun extractStateFromTx(tx: WireTransaction, stateIndices: Collection): List> { - return try { - val txAttachments = tx.attachments.mapNotNull { attachments.openAttachment(it)} - val states = AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext( - txAttachments, - networkParameters, - tx.id, - attachmentTrustCalculator::calculate, - cordappLoader.appClassLoader, - attachmentsClassLoaderCache) { - deserialiseComponentGroup(tx.componentGroups, TransactionState::class, ComponentGroupEnum.OUTPUTS_GROUP, forceDeserialize = true) - } - states.filterIndexed {index, _ -> stateIndices.contains(index)}.toList() - } catch (e: Exception) { - // If there is no attachment that allows the state class to be deserialised correctly, then carpent a state class anyway. It - // might still be possible to access the participants depending on how the state class was serialised. - logger.debug("Could not use attachments to deserialise transaction output states for transaction ${tx.id}") - tx.outputs.filterIndexed { index, _ -> stateIndices.contains(index)} - } - } - - override fun loadState(stateRef: StateRef): TransactionState<*> { - val stx = transactions.getTransaction(stateRef.txhash) - ?: throw MigrationException("Could not get transaction with hash ${stateRef.txhash} out of vault") - val baseTx = stx.resolveBaseTransaction(this) - return when (baseTx) { - is NotaryChangeLedgerTransaction -> baseTx.outputs[stateRef.index] - is ContractUpgradeLedgerTransaction -> baseTx.outputs[stateRef.index] - is WireTransaction -> extractStateFromTx(baseTx, listOf(stateRef.index)).first() - else -> throw MigrationException("Unknown transaction type ${baseTx::class.qualifiedName} found when loading a state") - } - } - - override fun loadStates(stateRefs: Set): Set> { - return stateRefs.groupBy { it.txhash }.flatMap { - val stx = transactions.getTransaction(it.key) - ?: throw MigrationException("Could not get transaction with hash ${it.key} out of vault") - val baseTx = stx.resolveBaseTransaction(this) - val stateList = when (baseTx) { - is NotaryChangeLedgerTransaction -> it.value.map { stateRef -> StateAndRef(baseTx.outputs[stateRef.index], stateRef) } - is ContractUpgradeLedgerTransaction -> it.value.map { stateRef -> StateAndRef(baseTx.outputs[stateRef.index], stateRef) } - is WireTransaction -> extractStateFromTx(baseTx, it.value.map { stateRef -> stateRef.index }) - .mapIndexed {index, state -> StateAndRef(state, StateRef(baseTx.id, index)) } - else -> throw MigrationException("Unknown transaction type ${baseTx::class.qualifiedName} found when loading a state") - } - stateList - }.toSet() - } - - override fun loadContractAttachment(stateRef: StateRef): Attachment { - throw NotImplementedError() - } -} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/migration/VaultStateMigration.kt b/node/src/main/kotlin/net/corda/node/migration/VaultStateMigration.kt index 28d8dc3a89..f47b80c374 100644 --- a/node/src/main/kotlin/net/corda/node/migration/VaultStateMigration.kt +++ b/node/src/main/kotlin/net/corda/node/migration/VaultStateMigration.kt @@ -1,13 +1,9 @@ package net.corda.node.migration import liquibase.database.Database -import net.corda.core.contracts.* -import net.corda.core.identity.CordaX500Name import net.corda.core.node.services.Vault import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentStateRef -import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.internal.* import net.corda.core.utilities.contextLogger import net.corda.node.internal.DBNetworkParametersStorage import net.corda.node.internal.schemas.NodeInfoSchemaV1 @@ -16,103 +12,21 @@ import net.corda.node.services.keys.BasicHSMKeyManagementService import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.persistence.NodeAttachmentService -import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.VaultSchemaV1 -import net.corda.node.services.vault.toStateRef import net.corda.nodeapi.internal.persistence.CordaPersistence -import net.corda.nodeapi.internal.persistence.DatabaseTransaction -import net.corda.nodeapi.internal.persistence.SchemaMigration import net.corda.nodeapi.internal.persistence.currentDBSession -import net.corda.serialization.internal.AMQP_P2P_CONTEXT -import net.corda.serialization.internal.AMQP_STORAGE_CONTEXT -import net.corda.serialization.internal.CordaSerializationMagic -import net.corda.serialization.internal.SerializationFactoryImpl -import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme -import net.corda.serialization.internal.amqp.amqpMagic -import org.hibernate.Session import org.hibernate.query.Query -import java.util.concurrent.ForkJoinPool -import java.util.concurrent.ForkJoinTask -import java.util.concurrent.RecursiveAction import javax.persistence.criteria.Root import javax.persistence.criteria.Selection class VaultStateMigration : CordaMigration() { - companion object { - private val logger = contextLogger() - } - - private fun addStateParties(session: Session, stateAndRef: StateAndRef) { - val state = stateAndRef.state.data - val persistentStateRef = PersistentStateRef(stateAndRef.ref) - try { - state.participants.groupBy { it.owningKey }.forEach { participants -> - val persistentParty = VaultSchemaV1.PersistentParty(persistentStateRef, participants.value.first()) - session.persist(persistentParty) - } - } catch (e: AbstractMethodError) { - // This should only happen if there was no attachment that could be used to deserialise the output states, and the state was - // serialised such that the participants list cannot be accessed (participants is calculated and not marked as a - // SerializableCalculatedProperty. - throw VaultStateMigrationException("Cannot add state parties for state ${stateAndRef.ref} as state class is not on the " + - "classpath and participants cannot be synthesised") - } - } - - private fun getStateAndRef(persistentState: VaultSchemaV1.VaultStates): StateAndRef { - val persistentStateRef = persistentState.stateRef ?: - throw VaultStateMigrationException("Persistent state ref missing from state") - val stateRef = persistentStateRef.toStateRef() - val state = try { - servicesForResolution.loadState(stateRef) - } catch (e: Exception) { - throw VaultStateMigrationException("Could not load state for stateRef $stateRef : ${e.message}", e) - } - return StateAndRef(state, stateRef) - } - - override fun execute(database: Database?) { - logger.info("Migrating vault state data to V4 tables") - if (database == null) { - logger.error("Cannot migrate vault states: Liquibase failed to provide a suitable database connection") - throw VaultStateMigrationException("Cannot migrate vault states as liquibase failed to provide a suitable database connection") - } + override fun execute(database: Database) { initialiseNodeServices(database, setOf(VaultMigrationSchemaV1, VaultSchemaV1, NodeInfoSchemaV1)) - var statesSkipped = 0 val persistentStates = VaultStateIterator(cordaDB) if (persistentStates.numStates > 0) { - logger.warn("Found ${persistentStates.numStates} states to update from a previous version. This may take a while for large " - + "volumes of data.") + throw VaultStateMigrationException("Found ${persistentStates.numStates} states that need to be updated to V4. Please upgrade " + + "to an older version of Corda first to perform this migration.") } - val ourName = CordaX500Name.parse(System.getProperty(SchemaMigration.NODE_X500_NAME)) - VaultStateIterator.withSerializationEnv { - persistentStates.forEach { - val session = currentDBSession() - try { - val stateAndRef = getStateAndRef(it) - - addStateParties(session, stateAndRef) - - // Can get away without checking for AbstractMethodErrors here as these will have already occurred when trying to add - // state parties. - val myKeys = stateAndRef.state.data.participants.map { participant -> participant.owningKey} - .filter { key -> identityService.certificateFromKey(key)?.name == ourName }.toSet() - if (!NodeVaultService.isRelevant(stateAndRef.state.data, myKeys)) { - it.relevancyStatus = Vault.RelevancyStatus.NOT_RELEVANT - } - } catch (e: VaultStateMigrationException) { - logger.warn("An error occurred while migrating a vault state: ${e.message}. Skipping. This will cause the " + - "migration to fail.", e) - statesSkipped++ - } - } - } - if (statesSkipped > 0) { - logger.error("$statesSkipped states could not be migrated as there was no class available for them.") - throw VaultStateMigrationException("Failed to migrate $statesSkipped states in the vault. Check the logs for details of the " + - "error for each state.") - } - logger.info("Finished performing vault state data migration for ${persistentStates.numStates - statesSkipped} states") } } @@ -147,49 +61,9 @@ object VaultMigrationSchemaV1 : MappedSchema(schemaFamily = VaultMigrationSchema * Currently, this class filters out those persistent states that have entries in the state party table. This behaviour is required for the * vault state migration, as entries in this table should not be duplicated. Unconsumed states are also filtered out for performance. */ -class VaultStateIterator(private val database: CordaPersistence) : Iterator { +class VaultStateIterator(private val database: CordaPersistence) { companion object { val logger = contextLogger() - - private object AMQPInspectorSerializationScheme : AbstractAMQPSerializationScheme(emptyList()) { - override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean { - return magic == amqpMagic - } - - override fun rpcClientSerializerFactory(context: SerializationContext) = throw UnsupportedOperationException() - override fun rpcServerSerializerFactory(context: SerializationContext) = throw UnsupportedOperationException() - } - - private fun initialiseSerialization() { - // Deserialise with the lenient carpenter as we only care for the AMQP field getters - _inheritableContextSerializationEnv.set(SerializationEnvironment.with( - SerializationFactoryImpl().apply { - registerScheme(AMQPInspectorSerializationScheme) - }, - p2pContext = AMQP_P2P_CONTEXT.withLenientCarpenter(), - storageContext = AMQP_STORAGE_CONTEXT.withLenientCarpenter() - )) - } - - private fun disableSerialization() { - _inheritableContextSerializationEnv.set(null) - } - - fun withSerializationEnv(block: () -> Unit) { - val newEnv = if (_allEnabledSerializationEnvs.isEmpty()) { - initialiseSerialization() - true - } else { - false - } - effectiveSerializationEnv.serializationFactory.withCurrentContext(effectiveSerializationEnv.storageContext.withLenientCarpenter()) { - block() - } - - if (newEnv) { - disableSerialization() - } - } } private val criteriaBuilder = database.entityManagerFactory.criteriaBuilder val numStates = getTotalStates() @@ -224,111 +98,6 @@ class VaultStateIterator(private val database: CordaPersistence) : Iterator { - endTransaction() - transaction = database.newTransaction() - val query = createVaultStatesQuery(VaultSchemaV1.VaultStates::class.java) { it } - // The above query excludes states that have entries in the state party table. As the iteration proceeds, each state has entries - // added to this table. The result is that when the next page is retrieved, any results that were in the previous page are not in - // the query at all! As such, the next set of states that need processing start at the first result. - query.firstResult = 0 - query.maxResults = pageSize - pageNumber++ - val result = query.resultList - logger.debug("Loaded page $pageNumber of ${(numStates - 1 / pageNumber.toLong()) + 1}. Current page has ${result.size} vault states") - return result - } - - private var currentIndex = 0 - - override fun hasNext(): Boolean { - val nextElementPresent = currentIndex + ((pageNumber - 1) * pageSize) < numStates - if (!nextElementPresent) { - endTransaction() - } - return nextElementPresent - } - - override fun next(): VaultSchemaV1.VaultStates { - if (currentIndex == pageSize) { - currentPage = getNextPage() - currentIndex = 0 - } - val stateToReturn = currentPage[currentIndex] - currentIndex++ - return stateToReturn - } - - // The rest of this class is an attempt at multithreading that was ultimately scuppered by liquibase not providing a connection pool. - // This may be useful as a starting point for improving performance of the migration, so is left here. To start using it, remove the - // serialization environment changes in the execute function in the migration, and change forEach -> parallelForEach. - private val pool = ForkJoinPool.commonPool() - - private class VaultPageTask(val database: CordaPersistence, - val page: List, - val block: (VaultSchemaV1.VaultStates) -> Unit): RecursiveAction() { - - private val pageSize = page.size - private val tolerance = 10 - - override fun compute() { - withSerializationEnv { - if (pageSize > tolerance) { - ForkJoinTask.invokeAll(createSubtasks()) - } else { - applyBlock() - } - } - } - - private fun createSubtasks(): List { - return listOf(VaultPageTask(database, page.subList(0, pageSize / 2), block), VaultPageTask(database, page.subList(pageSize / 2, pageSize), block)) - } - - private fun applyBlock() { - effectiveSerializationEnv.serializationFactory.withCurrentContext(effectiveSerializationEnv.storageContext.withLenientCarpenter()) { - database.transaction { - page.forEach { block(it) } - } - } - } - } - - private fun hasNextPage(): Boolean { - val nextPagePresent = pageNumber * pageSize < numStates - if (!nextPagePresent) { - endTransaction() - } - return nextPagePresent - } - - /** - * Iterate through all states in the vault, parallelizing the work on each page of vault states. - */ - fun parallelForEach(block: (VaultSchemaV1.VaultStates) -> Unit) { - pool.invoke(VaultPageTask(database, currentPage, block)) - while (hasNextPage()) { - currentPage = getNextPage() - pool.invoke(VaultPageTask(database, currentPage, block)) - } - } } class VaultStateMigrationException(msg: String, cause: Exception? = null) : Exception(msg, cause) \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index 0ee732b210..bea46121fc 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -5,8 +5,8 @@ import net.corda.core.context.InvocationContext import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.FlowLogic -import net.corda.core.flows.TransactionMetadata import net.corda.core.flows.StateMachineRunId +import net.corda.core.flows.TransactionMetadata import net.corda.core.identity.CordaX500Name import net.corda.core.internal.FlowStateMachineHandle import net.corda.core.internal.NamedCacheFactory @@ -17,6 +17,7 @@ import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.concurrent.OpenFuture import net.corda.core.internal.dependencies import net.corda.core.internal.requireSupportedHashType +import net.corda.core.internal.verification.Verifier import net.corda.core.internal.warnOnce import net.corda.core.messaging.DataFeed import net.corda.core.messaging.StateMachineTransactionMapping @@ -25,11 +26,13 @@ import net.corda.core.node.StatesToRecord import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NetworkMapCacheBase import net.corda.core.node.services.TransactionStorage +import net.corda.core.serialization.SerializationContext +import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction +import net.corda.core.transactions.defaultVerifier import net.corda.core.utilities.contextLogger import net.corda.node.internal.InitiatedFlowFactory -import net.corda.node.internal.cordapp.CordappProviderInternal import net.corda.node.services.DbTransactionsResolver import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService @@ -37,14 +40,11 @@ import net.corda.node.services.network.NetworkMapUpdater import net.corda.node.services.persistence.AttachmentStorageInternal import net.corda.node.services.statemachine.ExternalEvent import net.corda.node.services.statemachine.FlowStateMachineImpl +import net.corda.node.verification.NoDbAccessVerifier import net.corda.nodeapi.internal.persistence.CordaPersistence -import java.lang.IllegalStateException +import java.security.PublicKey import java.security.SignatureException -import java.util.ArrayList import java.util.Collections -import java.util.HashMap -import java.util.HashSet -import java.util.LinkedHashSet interface NetworkMapCacheInternal : NetworkMapCache, NetworkMapCacheBase { override val nodeReady: OpenFuture @@ -186,11 +186,14 @@ interface ServiceHubInternal : ServiceHubCoreInternal { val configuration: NodeConfiguration val nodeProperties: NodePropertiesStore val networkMapUpdater: NetworkMapUpdater - override val cordappProvider: CordappProviderInternal fun getFlowFactory(initiatingFlowClass: Class>): InitiatedFlowFactory<*>? val cacheFactory: NamedCacheFactory + override fun createVerifier(ltx: LedgerTransaction, serializationContext: SerializationContext): Verifier { + return NoDbAccessVerifier(defaultVerifier(ltx, serializationContext)) + } + override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) = recordTransactions(statesToRecord, txs, SIGNATURE_VERIFICATION_DISABLED) diff --git a/node/src/main/kotlin/net/corda/node/services/attachments/NodeAttachmentTrustCalculator.kt b/node/src/main/kotlin/net/corda/node/services/attachments/NodeAttachmentTrustCalculator.kt index 2305203338..285f7fca5a 100644 --- a/node/src/main/kotlin/net/corda/node/services/attachments/NodeAttachmentTrustCalculator.kt +++ b/node/src/main/kotlin/net/corda/node/services/attachments/NodeAttachmentTrustCalculator.kt @@ -1,10 +1,16 @@ package net.corda.node.services.attachments -import com.github.benmanes.caffeine.cache.Caffeine import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractAttachment import net.corda.core.crypto.SecureHash -import net.corda.core.internal.* +import net.corda.core.internal.AbstractAttachment +import net.corda.core.internal.AttachmentTrustCalculator +import net.corda.core.internal.AttachmentTrustInfo +import net.corda.core.internal.NamedCacheFactory +import net.corda.core.internal.TRUSTED_UPLOADERS +import net.corda.core.internal.VisibleForTesting +import net.corda.core.internal.hash +import net.corda.core.internal.isUploaderTrusted import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.vault.AttachmentQueryCriteria import net.corda.core.node.services.vault.Builder @@ -35,10 +41,7 @@ class NodeAttachmentTrustCalculator( ) : this(attachmentStorage, null, cacheFactory, blacklistedAttachmentSigningKeys) // A cache for caching whether a signing key is trusted - private val trustedKeysCache = cacheFactory.buildNamed( - Caffeine.newBuilder(), - "NodeAttachmentTrustCalculator_trustedKeysCache" - ) + private val trustedKeysCache = cacheFactory.buildNamed("NodeAttachmentTrustCalculator_trustedKeysCache") override fun calculate(attachment: Attachment): Boolean { @@ -92,9 +95,7 @@ class NodeAttachmentTrustCalculator( val trustRoot = if (attachment.isSignedByBlacklistedKey()) { null } else { - attachment.signerKeys - .mapNotNull { publicKeyToTrustRootMap[it] } - .firstOrNull() + attachment.signerKeys.firstNotNullOfOrNull { publicKeyToTrustRootMap[it] } } attachmentTrustInfos += AttachmentTrustInfo( attachmentId = attachment.id, diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToOwningIdentityCacheImpl.kt b/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToOwningIdentityCacheImpl.kt index fd3d01431d..c40b9cad89 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToOwningIdentityCacheImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/PublicKeyToOwningIdentityCacheImpl.kt @@ -1,6 +1,5 @@ package net.corda.node.services.persistence -import com.github.benmanes.caffeine.cache.Caffeine import net.corda.core.crypto.toStringShort import net.corda.core.internal.NamedCacheFactory import net.corda.core.utilities.contextLogger @@ -19,10 +18,7 @@ class PublicKeyToOwningIdentityCacheImpl(private val database: CordaPersistence, val log = contextLogger() } - private val cache = cacheFactory.buildNamed( - Caffeine.newBuilder(), - "PublicKeyToOwningIdentityCache_cache" - ) + private val cache = cacheFactory.buildNamed("PublicKeyToOwningIdentityCache_cache") /** * Return the owning identity associated with a given key. diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt index dfd2b1a8db..178182a9d9 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt @@ -3,10 +3,12 @@ package net.corda.node.services.statemachine import com.google.common.primitives.Primitives import net.corda.core.flows.* import net.corda.core.internal.VisibleForTesting +import net.corda.core.internal.loadClassOfType import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.contextLogger import org.slf4j.Logger +import java.lang.ClassCastException import java.lang.reflect.ParameterizedType import java.lang.reflect.Type import java.lang.reflect.TypeVariable @@ -57,13 +59,13 @@ open class FlowLogicRefFactoryImpl(private val classloader: ClassLoader) : Singl } private fun validatedFlowClassFromName(flowClassName: String): Class> { - val forName = try { - Class.forName(flowClassName, true, classloader) + return try { + loadClassOfType>(flowClassName, true, classloader) } catch (e: ClassNotFoundException) { throw IllegalFlowLogicException(flowClassName, "Flow not found: $flowClassName") - } - return forName.asSubclass(FlowLogic::class.java) ?: + } catch (e: ClassCastException) { throw IllegalFlowLogicException(flowClassName, "The class $flowClassName is not a subclass of FlowLogic.") + } } override fun createForRPC(flowClass: Class>, vararg args: Any?): FlowLogicRef { diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/InMemoryTransactionVerifierService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/InMemoryTransactionVerifierService.kt deleted file mode 100644 index e70712e9e0..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/transactions/InMemoryTransactionVerifierService.kt +++ /dev/null @@ -1,120 +0,0 @@ -package net.corda.node.services.transactions - -import net.corda.core.concurrent.CordaFuture -import net.corda.core.contracts.Attachment -import net.corda.core.contracts.TransactionVerificationException.BrokenTransactionException -import net.corda.core.internal.TransactionVerifierServiceInternal -import net.corda.core.internal.concurrent.openFuture -import net.corda.core.internal.internalFindTrustedAttachmentForClass -import net.corda.core.internal.prepareVerify -import net.corda.core.node.services.AttachmentStorage -import net.corda.core.node.services.TransactionVerifierService -import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.core.transactions.LedgerTransaction -import net.corda.core.utilities.contextLogger -import net.corda.node.internal.cordapp.CordappProviderInternal -import net.corda.nodeapi.internal.persistence.withoutDatabaseAccess - -class InMemoryTransactionVerifierService( - @Suppress("UNUSED_PARAMETER") numberOfWorkers: Int, - private val cordappProvider: CordappProviderInternal, - private val attachments: AttachmentStorage -) : SingletonSerializeAsToken(), TransactionVerifierService, TransactionVerifierServiceInternal, AutoCloseable { - companion object { - private val SEPARATOR = System.lineSeparator() + "-> " - private val log = contextLogger() - - fun Collection<*>.deepEquals(other: Collection<*>): Boolean { - return size == other.size && containsAll(other) && other.containsAll(this) - } - - fun Collection.toPrettyString(): String { - return joinToString(separator = SEPARATOR, prefix = SEPARATOR, postfix = System.lineSeparator()) { attachment -> - attachment.id.toString() - } - } - } - - override fun verify(transaction: LedgerTransaction): CordaFuture<*> { - return openFuture().apply { - capture { - val verifier = transaction.prepareVerify(transaction.attachments) - withoutDatabaseAccess { - verifier.verify() - } - } - } - } - - private fun computeReplacementAttachmentsFor(ltx: LedgerTransaction, missingClass: String?): Collection { - val replacements = cordappProvider.fixupAttachments(ltx.attachments) - return if (replacements.deepEquals(ltx.attachments)) { - /* - * We cannot continue unless we have some idea which - * class is missing from the attachments. - */ - if (missingClass == null) { - throw BrokenTransactionException( - txId = ltx.id, - message = "No fix-up rules provided for broken attachments:${replacements.toPrettyString()}" - ) - } - - /* - * The Node's fix-up rules have not been able to adjust the transaction's attachments, - * so resort to the original mechanism of trying to find an attachment that contains - * the missing class. (Do you feel lucky, Punk?) - */ - val extraAttachment = requireNotNull(attachments.internalFindTrustedAttachmentForClass(missingClass)) { - """Transaction $ltx is incorrectly formed. Most likely it was created during version 3 of Corda - |when the verification logic was more lenient. Attempted to find local dependency for class: $missingClass, - |but could not find one. - |If you wish to verify this transaction, please contact the originator of the transaction and install the - |provided missing JAR. - |You can install it using the RPC command: `uploadAttachment` without restarting the node. - |""".trimMargin() - } - - replacements.toMutableSet().apply { - /* - * Check our transaction doesn't already contain this extra attachment. - * It seems unlikely that we would, but better safe than sorry! - */ - if (!add(extraAttachment)) { - throw BrokenTransactionException( - txId = ltx.id, - message = "Unlinkable class $missingClass inside broken attachments:${replacements.toPrettyString()}" - ) - } - - log.warn("""Detected that transaction $ltx does not contain all cordapp dependencies. - |This may be the result of a bug in a previous version of Corda. - |Attempting to verify using the additional trusted dependency: $extraAttachment for class $missingClass. - |Please check with the originator that this is a valid transaction. - |YOU ARE ONLY SEEING THIS MESSAGE BECAUSE THE CORDAPPS THAT CREATED THIS TRANSACTION ARE BROKEN! - |WE HAVE TRIED TO REPAIR THE TRANSACTION AS BEST WE CAN, BUT CANNOT GUARANTEE WE HAVE SUCCEEDED! - |PLEASE FIX THE CORDAPPS AND MIGRATE THESE BROKEN TRANSACTIONS AS SOON AS POSSIBLE! - |THIS MESSAGE IS **SUPPOSED** TO BE SCARY!! - |""".trimMargin() - ) - } - } else { - replacements - } - } - - override fun reverifyWithFixups(transaction: LedgerTransaction, missingClass: String?): CordaFuture<*> { - return openFuture().apply { - capture { - val replacementAttachments = computeReplacementAttachmentsFor(transaction, missingClass) - log.warn("Reverifying transaction {} with attachments:{}", transaction.id, replacementAttachments.toPrettyString()) - val verifier = transaction.prepareVerify(replacementAttachments.toList()) - withoutDatabaseAccess { - verifier.verify() - } - } - } - } - - override fun close() {} -} diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 098c0d2155..6a20f2c4e5 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -19,8 +19,10 @@ import net.corda.core.internal.ThreadBox import net.corda.core.internal.TransactionDeserialisationException import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.bufferUntilSubscribed +import net.corda.core.internal.mapToSet import net.corda.core.internal.tee import net.corda.core.internal.uncheckedCast +import net.corda.core.internal.verification.VerifyingServiceHub import net.corda.core.internal.warnOnce import net.corda.core.messaging.DataFeed import net.corda.core.node.StatesToRecord @@ -50,7 +52,6 @@ import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.core.utilities.toNonEmptySet import net.corda.core.utilities.trace -import net.corda.node.internal.NodeServicesForResolution import net.corda.node.services.api.SchemaService import net.corda.node.services.api.VaultServiceInternal import net.corda.node.services.schema.PersistentStateService @@ -80,8 +81,6 @@ import javax.persistence.criteria.CriteriaQuery import javax.persistence.criteria.CriteriaUpdate import javax.persistence.criteria.Predicate import javax.persistence.criteria.Root -import kotlin.collections.ArrayList -import kotlin.collections.LinkedHashSet import kotlin.collections.component1 import kotlin.collections.component2 @@ -98,7 +97,7 @@ import kotlin.collections.component2 class NodeVaultService( private val clock: Clock, private val keyManagementService: KeyManagementService, - private val servicesForResolution: NodeServicesForResolution, + private val serviceHub: VerifyingServiceHub, private val database: CordaPersistence, schemaService: SchemaService, private val appClassloader: ClassLoader @@ -231,7 +230,7 @@ class NodeVaultService( // Persist the consumed inputs. consumedStateRefs.forEach { stateRef -> - val state = session.get(VaultSchemaV1.VaultStates::class.java, PersistentStateRef(stateRef)) + val state = session.get(VaultSchemaV1.VaultStates::class.java, PersistentStateRef(stateRef)) state?.run { // Only update the state if it has not previously been consumed (this could have happened if the transaction is being // re-recorded. @@ -312,7 +311,7 @@ class NodeVaultService( fun withValidDeserialization(list: List, txId: SecureHash): Map { var error: TransactionDeserialisationException? = null - val map = (0 until list.size).mapNotNull { idx -> + val map = list.indices.mapNotNull { idx -> try { idx to list[idx] } catch (e: TransactionDeserialisationException) { @@ -354,7 +353,7 @@ class NodeVaultService( val outputRefs = tx.outRefsOfType().map { it.ref } val seenRefs = loadStates(outputRefs).map { it.ref } val unseenRefs = outputRefs - seenRefs - val unseenOutputIdxs = unseenRefs.map { it.index }.toSet() + val unseenOutputIdxs = unseenRefs.mapToSet { it.index } outputs.filter { it.key in unseenOutputIdxs } } else { outputs @@ -383,7 +382,7 @@ class NodeVaultService( StatesToRecord.ALL_VISIBLE, StatesToRecord.ONLY_RELEVANT -> { val notSeenReferences = tx.references - loadStates(tx.references).map { it.ref } // TODO: This is expensive - is there another way? - tx.toLedgerTransaction(servicesForResolution).deserializableRefStates() + tx.toLedgerTransaction(serviceHub).deserializableRefStates() .filter { (_, stateAndRef) -> stateAndRef.ref in notSeenReferences } .values } @@ -398,8 +397,8 @@ class NodeVaultService( // We also can't do filtering beforehand, since for notary change transactions output encumbrance pointers // get recalculated based on input positions. val ltx: FullTransaction = when (tx) { - is NotaryChangeWireTransaction -> tx.resolve(servicesForResolution, emptyList()) - is ContractUpgradeWireTransaction -> tx.resolve(servicesForResolution, emptyList()) + is NotaryChangeWireTransaction -> tx.resolve(serviceHub, emptyList()) + is ContractUpgradeWireTransaction -> tx.resolve(serviceHub, emptyList()) else -> throw IllegalArgumentException("Unsupported transaction type: ${tx.javaClass.name}") } val myKeys by lazy { keyManagementService.filterMyKeys(ltx.outputs.flatMap { it.data.participants.map { it.owningKey } }) } @@ -542,8 +541,8 @@ class NodeVaultService( val stateStatusPredication = criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED) val lockIdPredicate = criteriaBuilder.or(get(VaultSchemaV1.VaultStates::lockId.name).isNull, criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString())) - update.set(get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) - update.set(get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) + update.set(get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) + update.set(get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) update.where(stateStatusPredication, lockIdPredicate, *commonPredicates) } if (updatedRows > 0 && updatedRows == stateRefs.size) { @@ -596,8 +595,8 @@ class NodeVaultService( criteriaBuilder.executeUpdate(session, stateRefs) { update, persistentStateRefs -> val stateStatusPredication = criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED) val lockIdPredicate = criteriaBuilder.equal(get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) - update.set(get(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java)) - update.set(get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) + update.set(get(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java)) + update.set(get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) configure(update, arrayOf(stateStatusPredication, lockIdPredicate), persistentStateRefs) } @@ -748,16 +747,13 @@ class NodeVaultService( if (result0 is VaultSchemaV1.VaultStates) { statesMetadata.add(result0.toStateMetadata()) } else { - log.debug { "OtherResults: ${Arrays.toString(result.toArray())}" } + log.debug { "OtherResults: ${result.toArray().contentToString()}" } otherResults.addAll(result.toArray().asList()) } } } - val states: List> = servicesForResolution.loadStates( - statesMetadata.mapTo(LinkedHashSet()) { it.ref }, - ArrayList() - ) + val states: List> = serviceHub.loadStatesInternal(statesMetadata.mapToSet { it.ref }, ArrayList()) val totalStatesAvailable = when { paging.isDefault -> -1L @@ -844,9 +840,8 @@ class NodeVaultService( log.warn("trackBy is called with an already existing, open DB transaction. As a result, there might be states missing from both the snapshot and observable, included in the returned data feed, because of race conditions.") } val snapshotResults = _queryBy(criteria, paging, sorting, contractStateType) - val snapshotStatesRefs = snapshotResults.statesMetadata.map { it.ref }.toSet() - val snapshotConsumedStatesRefs = snapshotResults.statesMetadata.filter { it.consumedTime != null } - .map { it.ref }.toSet() + val snapshotStatesRefs = snapshotResults.statesMetadata.mapToSet { it.ref } + val snapshotConsumedStatesRefs = snapshotResults.statesMetadata.filter { it.consumedTime != null }.mapToSet { it.ref } val filteredUpdates = updates.filter { it.containsType(contractStateType, snapshotResults.stateTypes) } .map { filterContractStates(it, contractStateType) } .filter { !hasBeenSeen(it, snapshotStatesRefs, snapshotConsumedStatesRefs) } @@ -881,8 +876,8 @@ class NodeVaultService( * the snapshot or in the observable). */ private fun hasBeenSeen(update: Vault.Update, snapshotStatesRefs: Set, snapshotConsumedStatesRefs: Set): Boolean { - val updateProducedStatesRefs = update.produced.map { it.ref }.toSet() - val updateConsumedStatesRefs = update.consumed.map { it.ref }.toSet() + val updateProducedStatesRefs = update.produced.mapToSet { it.ref } + val updateConsumedStatesRefs = update.consumed.mapToSet { it.ref } return snapshotStatesRefs.containsAll(updateProducedStatesRefs) && snapshotConsumedStatesRefs.containsAll(updateConsumedStatesRefs) } diff --git a/node/src/main/kotlin/net/corda/node/utilities/InfrequentlyMutatedCache.kt b/node/src/main/kotlin/net/corda/node/utilities/InfrequentlyMutatedCache.kt index 4121a92cb9..4902ada180 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/InfrequentlyMutatedCache.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/InfrequentlyMutatedCache.kt @@ -1,6 +1,5 @@ package net.corda.node.utilities -import com.github.benmanes.caffeine.cache.Caffeine import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.VisibleForTesting import net.corda.nodeapi.internal.persistence.contextTransactionOrNull @@ -101,7 +100,7 @@ class InfrequentlyMutatedCache(name: String, cacheFactory: Nam } } - private val backingCache = cacheFactory.buildNamed>(Caffeine.newBuilder(), name) + private val backingCache = cacheFactory.buildNamed>(name) // This protects against the cache purging something that is marked as invalid and thus we "forget" it shouldn't be cached. private val currentlyInvalid = ConcurrentHashMap>() diff --git a/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt b/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt index 25688233c2..42b7738a1d 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingCache.kt @@ -15,8 +15,7 @@ class NonInvalidatingCache private constructor( private companion object { private fun buildCache(cacheFactory: NamedCacheFactory, name: String, loadFunction: (K) -> V): LoadingCache { - val builder = Caffeine.newBuilder() - return cacheFactory.buildNamed(builder, name, NonInvalidatingCacheLoader(loadFunction)) + return cacheFactory.buildNamed(name, NonInvalidatingCacheLoader(loadFunction)) } } diff --git a/node/src/main/kotlin/net/corda/node/verification/ExternalVerifierHandle.kt b/node/src/main/kotlin/net/corda/node/verification/ExternalVerifierHandle.kt new file mode 100644 index 0000000000..4b82ab3cf1 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/verification/ExternalVerifierHandle.kt @@ -0,0 +1,229 @@ +package net.corda.node.verification + +import net.corda.core.contracts.Attachment +import net.corda.core.internal.AbstractAttachment +import net.corda.core.internal.copyTo +import net.corda.core.internal.div +import net.corda.core.internal.mapToSet +import net.corda.core.internal.readFully +import net.corda.core.serialization.serialize +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.Try +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.debug +import net.corda.node.services.api.ServiceHubInternal +import net.corda.serialization.internal.GeneratedAttachment +import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme.Companion.customSerializers +import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme.Companion.serializationWhitelists +import net.corda.serialization.internal.verifier.AttachmentWithTrust +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.AttachmentResult +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.AttachmentsResult +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.Initialisation +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.NetworkParametersResult +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.PartiesResult +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.TrustedClassAttachmentResult +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.VerificationRequest +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerificationResult +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetAttachment +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetAttachments +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetNetworkParameters +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetParties +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetTrustedClassAttachment +import net.corda.serialization.internal.verifier.readCordaSerializable +import net.corda.serialization.internal.verifier.writeCordaSerializable +import java.io.DataInputStream +import java.io.DataOutputStream +import java.io.IOException +import java.lang.ProcessBuilder.Redirect +import java.net.ServerSocket +import java.net.Socket +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption.REPLACE_EXISTING +import kotlin.io.path.createDirectories + +/** + * Handle to the node's external verifier. The verifier process is started lazily on the first verification request. + */ +@Suppress("TooGenericExceptionCaught") +class ExternalVerifierHandle(private val serviceHub: ServiceHubInternal) : AutoCloseable { + companion object { + private val log = contextLogger() + + private const val MAX_ATTEMPTS = 5 + + private val verifierJar: Path = Files.createTempFile("corda-external-verifier", ".jar") + init { + // Extract the embedded verifier jar + Companion::class.java.getResourceAsStream("external-verifier.jar")!!.use { + it.copyTo(verifierJar, REPLACE_EXISTING) + } + verifierJar.toFile().deleteOnExit() + } + } + + private lateinit var server: ServerSocket + @Volatile + private var connection: Connection? = null + + fun verifyTransaction(stx: SignedTransaction, checkSufficientSignatures: Boolean) { + log.info("Verify $stx externally, checkSufficientSignatures=$checkSufficientSignatures") + // By definition input states are unique, and so it makes sense to eagerly send them across with the transaction. + // Reference states are not, but for now we'll send them anyway and assume they aren't used often. If this assumption is not + // correct, and there's a benefit, then we can send them lazily. + val stxInputsAndReferences = (stx.inputs + stx.references).associateWith(serviceHub::getSerializedState) + val request = VerificationRequest(stx, stxInputsAndReferences, checkSufficientSignatures) + + // To keep things simple the verifier only supports one verification request at a time. + synchronized(this) { + startServer() + var attempt = 1 + while (true) { + val result = try { + tryVerification(request) + } catch (e: Exception) { + processError(attempt, e) + attempt += 1 + continue + } + when (result) { + is Try.Success -> return + is Try.Failure -> throw result.exception + } + } + } + } + + private fun startServer() { + if (::server.isInitialized) return + server = ServerSocket(0) + // Just in case... + Runtime.getRuntime().addShutdownHook(Thread(::close)) + } + + private fun processError(attempt: Int, e: Exception) { + if (attempt == MAX_ATTEMPTS) { + throw IOException("Unable to verify with external verifier", e) + } else { + log.warn("Unable to verify with external verifier, trying again...", e) + } + try { + connection?.close() + } catch (e: Exception) { + log.debug("Problem closing external verifier connection", e) + } + connection = null + } + + private fun tryVerification(request: VerificationRequest): Try { + val connection = getConnection() + connection.toVerifier.writeCordaSerializable(request) + // Send the verification request and then wait for any requests from verifier for more information. The last message will either + // be a verification success or failure message. + while (true) { + val message = connection.fromVerifier.readCordaSerializable() + log.debug { "Received from external verifier: $message" } + when (message) { + // Process the information the verifier needs and then loop back and wait for more messages + is VerifierRequest -> processVerifierRequest(message, connection) + is VerificationResult -> return message.result + } + } + } + + private fun getConnection(): Connection { + return connection ?: Connection().also { connection = it } + } + + private fun processVerifierRequest(request: VerifierRequest, connection: Connection) { + val result = when (request) { + is GetParties -> PartiesResult(serviceHub.getParties(request.keys)) + is GetAttachment -> AttachmentResult(prepare(serviceHub.attachments.openAttachment(request.id))) + is GetAttachments -> AttachmentsResult(serviceHub.getAttachments(request.ids).map(::prepare)) + is GetNetworkParameters -> NetworkParametersResult(serviceHub.getNetworkParameters(request.id)) + is GetTrustedClassAttachment -> TrustedClassAttachmentResult(serviceHub.getTrustedClassAttachment(request.className)?.id) + } + log.debug { "Sending response to external verifier: $result" } + connection.toVerifier.writeCordaSerializable(result) + } + + private fun prepare(attachment: Attachment?): AttachmentWithTrust? { + if (attachment == null) return null + val isTrusted = serviceHub.isAttachmentTrusted(attachment) + val attachmentForSer = when (attachment) { + // The Attachment retrieved from the database is not serialisable, so we have to convert it into one + is AbstractAttachment -> GeneratedAttachment(attachment.open().readFully(), attachment.uploader) + // For everything else we keep as is, in particular preserving ContractAttachment + else -> attachment + } + return AttachmentWithTrust(attachmentForSer, isTrusted) + } + + override fun close() { + connection?.let { + connection = null + try { + it.close() + } finally { + server.close() + } + } + } + + private inner class Connection : AutoCloseable { + private val verifierProcess: Process + private val socket: Socket + val toVerifier: DataOutputStream + val fromVerifier: DataInputStream + + init { + val logsDirectory = (serviceHub.configuration.baseDirectory / "logs").createDirectories() + val command = listOf( + "${System.getProperty("java.home") / "bin" / "java"}", + "-jar", + "$verifierJar", + "${server.localPort}", + System.getProperty("log4j2.level")?.lowercase() ?: "info" // TODO + ) + log.debug { "Verifier command: $command" } + verifierProcess = ProcessBuilder(command) + .redirectOutput(Redirect.appendTo((logsDirectory / "verifier-stdout.log").toFile())) + .redirectError(Redirect.appendTo((logsDirectory / "verifier-stderr.log").toFile())) + .directory(serviceHub.configuration.baseDirectory.toFile()) + .start() + log.info("External verifier process started; PID ${verifierProcess.pid()}") + + verifierProcess.onExit().whenComplete { _, _ -> + if (connection != null) { + log.error("The external verifier has unexpectedly terminated with error code ${verifierProcess.exitValue()}. " + + "Please check verifier logs for more details.") + } + // Allow a new process to be started on the next verification request + connection = null + } + + socket = server.accept() + toVerifier = DataOutputStream(socket.outputStream) + fromVerifier = DataInputStream(socket.inputStream) + + val cordapps = serviceHub.cordappProvider.cordapps + val initialisation = Initialisation( + customSerializerClassNames = cordapps.customSerializers.mapToSet { it.javaClass.name }, + serializationWhitelistClassNames = cordapps.serializationWhitelists.mapToSet { it.javaClass.name }, + System.getProperty("experimental.corda.customSerializationScheme"), // See Node#initialiseSerialization + serializedCurrentNetworkParameters = serviceHub.networkParameters.serialize() + ) + toVerifier.writeCordaSerializable(initialisation) + } + + override fun close() { + try { + socket.close() + } finally { + verifierProcess.destroyForcibly() + } + } + } +} diff --git a/node/src/main/kotlin/net/corda/node/verification/NoDbAccessVerifier.kt b/node/src/main/kotlin/net/corda/node/verification/NoDbAccessVerifier.kt new file mode 100644 index 0000000000..992031cb80 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/verification/NoDbAccessVerifier.kt @@ -0,0 +1,12 @@ +package net.corda.node.verification + +import net.corda.core.internal.verification.Verifier +import net.corda.nodeapi.internal.persistence.withoutDatabaseAccess + +class NoDbAccessVerifier(private val delegate: Verifier) : Verifier { + override fun verify() { + withoutDatabaseAccess { + delegate.verify() + } + } +} diff --git a/node/src/test/kotlin/net/corda/node/internal/CustomSerializationSchemeScanningTest.kt b/node/src/test/kotlin/net/corda/node/internal/CustomSerializationSchemeScanningTest.kt index 1837a8fb49..e920a197f0 100644 --- a/node/src/test/kotlin/net/corda/node/internal/CustomSerializationSchemeScanningTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/CustomSerializationSchemeScanningTest.kt @@ -1,10 +1,11 @@ package net.corda.node.internal +import net.corda.core.CordaException import net.corda.core.serialization.CustomSerializationScheme import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationSchemeContext import net.corda.core.utilities.ByteSequence -import net.corda.node.internal.classloading.scanForCustomSerializationScheme +import net.corda.serialization.internal.verifier.loadCustomSerializationScheme import org.junit.Test import org.mockito.Mockito import kotlin.test.assertFailsWith @@ -33,7 +34,7 @@ class CustomSerializationSchemeScanningTest { @Test(timeout = 300_000) fun `Can scan for custom serialization scheme and build a serialization scheme`() { - val scheme = scanForCustomSerializationScheme(DummySerializationScheme::class.java.name, this::class.java.classLoader) + val scheme = loadCustomSerializationScheme(DummySerializationScheme::class.java.name, this::class.java.classLoader) val mockContext = Mockito.mock(SerializationContext::class.java) assertFailsWith("Tried to serialize with DummySerializationScheme") { scheme.serialize(Any::class.java, mockContext) @@ -43,27 +44,27 @@ class CustomSerializationSchemeScanningTest { @Test(timeout = 300_000) fun `verification fails with a helpful error if the class is not found in the classloader`() { val missingClassName = "org.testing.DoesNotExist" - assertFailsWith("$missingClassName was declared as a custom serialization scheme but could not " + + assertFailsWith("$missingClassName was declared as a custom serialization scheme but could not " + "be found.") { - scanForCustomSerializationScheme(missingClassName, this::class.java.classLoader) + loadCustomSerializationScheme(missingClassName, this::class.java.classLoader) } } @Test(timeout = 300_000) fun `verification fails with a helpful error if the class is not a custom serialization scheme`() { val schemeName = NonSerializationScheme::class.java.name - assertFailsWith("$schemeName was declared as a custom serialization scheme but does not " + + assertFailsWith("$schemeName was declared as a custom serialization scheme but does not " + "implement CustomSerializationScheme.") { - scanForCustomSerializationScheme(schemeName, this::class.java.classLoader) + loadCustomSerializationScheme(schemeName, this::class.java.classLoader) } } @Test(timeout = 300_000) fun `verification fails with a helpful error if the class does not have a no arg constructor`() { val schemeName = DummySerializationSchemeWithoutNoArgConstructor::class.java.name - assertFailsWith("$schemeName was declared as a custom serialization scheme but does not " + + assertFailsWith("$schemeName was declared as a custom serialization scheme but does not " + "have a no argument constructor.") { - scanForCustomSerializationScheme(schemeName, this::class.java.classLoader) + loadCustomSerializationScheme(schemeName, this::class.java.classLoader) } } } diff --git a/node/src/test/kotlin/net/corda/node/migration/VaultStateMigrationTest.kt b/node/src/test/kotlin/net/corda/node/migration/VaultStateMigrationTest.kt deleted file mode 100644 index b0a5a0e778..0000000000 --- a/node/src/test/kotlin/net/corda/node/migration/VaultStateMigrationTest.kt +++ /dev/null @@ -1,637 +0,0 @@ -package net.corda.node.migration - -import liquibase.database.Database -import liquibase.database.jvm.JdbcConnection -import net.corda.core.contracts.Amount -import net.corda.core.contracts.ContractState -import net.corda.core.contracts.Issued -import net.corda.core.contracts.StateAndRef -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TransactionState -import net.corda.core.contracts.UniqueIdentifier -import net.corda.core.crypto.Crypto -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.SignableData -import net.corda.core.crypto.SignatureMetadata -import net.corda.core.crypto.toStringShort -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.CordaX500Name -import net.corda.core.identity.PartyAndCertificate -import net.corda.core.internal.NotaryChangeTransactionBuilder -import net.corda.core.internal.packageName -import net.corda.core.internal.signWithCert -import net.corda.core.node.NetworkParameters -import net.corda.core.node.NotaryInfo -import net.corda.core.node.services.Vault -import net.corda.core.schemas.PersistentStateRef -import net.corda.core.serialization.SerializationDefaults -import net.corda.core.serialization.serialize -import net.corda.core.transactions.SignedTransaction -import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.contextLogger -import net.corda.finance.DOLLARS -import net.corda.finance.contracts.Commodity -import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.Obligation -import net.corda.finance.contracts.asset.OnLedgerAsset -import net.corda.finance.schemas.CashSchemaV1 -import net.corda.node.internal.DBNetworkParametersStorage -import net.corda.node.internal.schemas.NodeInfoSchemaV1 -import net.corda.node.services.identity.PersistentIdentityService -import net.corda.node.services.keys.BasicHSMKeyManagementService -import net.corda.node.services.persistence.DBTransactionStorage -import net.corda.node.services.vault.VaultSchemaV1 -import net.corda.nodeapi.internal.crypto.X509Utilities -import net.corda.nodeapi.internal.persistence.CordaPersistence -import net.corda.nodeapi.internal.persistence.DatabaseConfig -import net.corda.nodeapi.internal.persistence.contextTransactionOrNull -import net.corda.nodeapi.internal.persistence.currentDBSession -import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.BOB_NAME -import net.corda.testing.core.BOC_NAME -import net.corda.testing.core.CHARLIE_NAME -import net.corda.testing.core.DUMMY_NOTARY_NAME -import net.corda.testing.core.SerializationEnvironmentRule -import net.corda.testing.core.TestIdentity -import net.corda.testing.core.dummyCommand -import net.corda.testing.internal.configureDatabase -import net.corda.testing.internal.vault.CommodityState -import net.corda.testing.internal.vault.DUMMY_LINEAR_CONTRACT_PROGRAM_ID -import net.corda.testing.internal.vault.DummyLinearContract -import net.corda.testing.internal.vault.DummyLinearStateSchemaV1 -import net.corda.testing.node.MockServices -import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties -import net.corda.testing.node.TestClock -import net.corda.testing.node.makeTestIdentityService -import org.assertj.core.api.Assertions.assertThatExceptionOfType -import org.junit.After -import org.junit.Before -import org.junit.ClassRule -import org.junit.Ignore -import org.junit.Test -import org.mockito.Mockito -import java.security.KeyPair -import java.time.Clock -import java.time.Duration -import java.time.Instant -import java.util.Currency -import java.util.Properties -import kotlin.collections.List -import kotlin.collections.component1 -import kotlin.collections.component2 -import kotlin.collections.first -import kotlin.collections.forEach -import kotlin.collections.forEachIndexed -import kotlin.collections.groupBy -import kotlin.collections.listOf -import kotlin.collections.map -import kotlin.collections.mapOf -import kotlin.collections.plus -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertFalse - -/** - * These tests aim to verify that migrating vault states from V3 to later versions works correctly. While these unit tests verify the - * migrating behaviour is correct (tables populated, columns updated for the right states), it comes with a caveat: they do not test that - * deserialising states with the attachment classloader works correctly. - * - * The reason for this is that it is impossible to do so. There is no real way of writing a unit or integration test to upgrade from one - * version to another (at the time of writing). These tests simulate a small part of the upgrade process by directly using hibernate to - * populate a database as a V3 node would, then running the migration class. However, it is impossible to do this for attachments as there - * is no contract state jar to serialise. - */ -class VaultStateMigrationTest { - companion object { - val alice = TestIdentity(ALICE_NAME, 70) - val bankOfCorda = TestIdentity(BOC_NAME) - val bob = TestIdentity(BOB_NAME, 80) - private val charlie = TestIdentity(CHARLIE_NAME, 90) - val dummyCashIssuer = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10) - val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20) - val ALICE get() = alice.party - val ALICE_IDENTITY get() = alice.identity - val BOB get() = bob.party - val BOB_IDENTITY get() = bob.identity - val BOC_IDENTITY get() = bankOfCorda.identity - val BOC_KEY get() = bankOfCorda.keyPair - val CHARLIE get() = charlie.party - val DUMMY_NOTARY get() = dummyNotary.party - val bob2 = TestIdentity(BOB_NAME, 40) - val BOB2 = bob2.party - val BOB2_IDENTITY = bob2.identity - - val clock: TestClock = TestClock(Clock.systemUTC()) - - @ClassRule - @JvmField - val testSerialization = SerializationEnvironmentRule() - - val logger = contextLogger() - } - - val cordappPackages = listOf( - "net.corda.finance.contracts", - CashSchemaV1::class.packageName, - DummyLinearStateSchemaV1::class.packageName) - - lateinit var liquibaseDB: Database - lateinit var cordaDB: CordaPersistence - lateinit var notaryServices: MockServices - - @Before - fun setUp() { - val identityService = makeTestIdentityService(dummyNotary.identity, BOB_IDENTITY, ALICE_IDENTITY) - notaryServices = MockServices(cordappPackages, dummyNotary, identityService, dummyCashIssuer.keyPair, BOC_KEY) - cordaDB = configureDatabase( - makeTestDataSourceProperties(), - DatabaseConfig(), - notaryServices.identityService::wellKnownPartyFromX500Name, - notaryServices.identityService::wellKnownPartyFromAnonymous, - ourName = BOB_IDENTITY.name) - val liquibaseConnection = Mockito.mock(JdbcConnection::class.java) - Mockito.`when`(liquibaseConnection.url).thenReturn(cordaDB.jdbcUrl) - Mockito.`when`(liquibaseConnection.wrappedConnection).thenReturn(cordaDB.dataSource.connection) - liquibaseDB = Mockito.mock(Database::class.java) - Mockito.`when`(liquibaseDB.connection).thenReturn(liquibaseConnection) - - saveOurKeys(listOf(bob.keyPair, bob2.keyPair)) - saveAllIdentities(listOf(BOB_IDENTITY, ALICE_IDENTITY, BOC_IDENTITY, dummyNotary.identity, BOB2_IDENTITY)) - addNetworkParameters() - } - - @After - fun close() { - contextTransactionOrNull?.close() - cordaDB.close() - } - - private fun addNetworkParameters() { - cordaDB.transaction { - val clock = Clock.systemUTC() - val params = NetworkParameters( - 1, - listOf(NotaryInfo(DUMMY_NOTARY, false), NotaryInfo(CHARLIE, false)), - 1, - 1, - clock.instant(), - 1, - mapOf(), - Duration.ZERO, - mapOf() - ) - val signedParams = params.signWithCert(bob.keyPair.private, BOB_IDENTITY.certificate) - val persistentParams = DBNetworkParametersStorage.PersistentNetworkParameters( - SecureHash.allOnesHash.toString(), - params.epoch, - signedParams.raw.bytes, - signedParams.sig.bytes, - signedParams.sig.by.encoded, - X509Utilities.buildCertPath(signedParams.sig.parentCertsChain).encoded - ) - session.save(persistentParams) - } - } - - private fun createCashTransaction(cash: Cash, value: Amount, owner: AbstractParty): SignedTransaction { - val tx = TransactionBuilder(DUMMY_NOTARY) - cash.generateIssue(tx, Amount(value.quantity, Issued(bankOfCorda.ref(1), value.token)), owner, DUMMY_NOTARY) - return notaryServices.signInitialTransaction(tx, bankOfCorda.party.owningKey) - } - - private fun createVaultStatesFromTransaction(tx: SignedTransaction, stateStatus: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) { - cordaDB.transaction { - tx.coreTransaction.outputs.forEachIndexed { index, state -> - val constraintInfo = Vault.ConstraintInfo(state.constraint) - val persistentState = VaultSchemaV1.VaultStates( - notary = state.notary, - contractStateClassName = state.data.javaClass.name, - stateStatus = stateStatus, - recordedTime = clock.instant(), - relevancyStatus = Vault.RelevancyStatus.RELEVANT, //Always persist as relevant to mimic V3 - constraintType = constraintInfo.type(), - constraintData = constraintInfo.data() - ) - persistentState.stateRef = PersistentStateRef(tx.id.toString(), index) - session.save(persistentState) - } - } - } - - private fun saveOurKeys(keys: List) { - cordaDB.transaction { - keys.forEach { - val persistentKey = BasicHSMKeyManagementService.PersistentKey(it.public, it.private) - session.save(persistentKey) - } - } - } - - private fun saveAllIdentities(identities: List) { - cordaDB.transaction { - identities.groupBy { it.name }.forEach { (_, certs) -> - val persistentIDs = certs.map { PersistentIdentityService.PersistentPublicKeyHashToCertificate(it.owningKey.toStringShort(), it.certPath.encoded) } - persistentIDs.forEach { session.save(it) } - val networkIdentity = NodeInfoSchemaV1.DBPartyAndCertificate(certs.first(), true) - val persistentNodeInfo = NodeInfoSchemaV1.PersistentNodeInfo(0, "", listOf(), listOf(networkIdentity), 0, 0) - session.save(persistentNodeInfo) - } - } - } - - private fun storeTransaction(tx: SignedTransaction) { - cordaDB.transaction { - val persistentTx = DBTransactionStorage.DBTransaction( - txId = tx.id.toString(), - stateMachineRunId = null, - transaction = tx.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes, - status = DBTransactionStorage.TransactionStatus.VERIFIED, - timestamp = Instant.now(), - signatures = null - ) - session.save(persistentTx) - } - } - - private fun getVaultStateCount(relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL): Long { - return cordaDB.transaction { - val criteriaBuilder = cordaDB.entityManagerFactory.criteriaBuilder - val criteriaQuery = criteriaBuilder.createQuery(Long::class.java) - val queryRootStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) - criteriaQuery.select(criteriaBuilder.count(queryRootStates)) - if (relevancyStatus != Vault.RelevancyStatus.ALL) { - criteriaQuery.where(criteriaBuilder.equal(queryRootStates.get("relevancyStatus"), relevancyStatus)) - } - val query = session.createQuery(criteriaQuery) - query.singleResult - } - } - - private fun getStatePartyCount(): Long { - return cordaDB.transaction { - val criteriaBuilder = cordaDB.entityManagerFactory.criteriaBuilder - val criteriaQuery = criteriaBuilder.createQuery(Long::class.java) - val queryRootStates = criteriaQuery.from(VaultSchemaV1.PersistentParty::class.java) - criteriaQuery.select(criteriaBuilder.count(queryRootStates)) - val query = session.createQuery(criteriaQuery) - query.singleResult - } - } - - private fun addCashStates(statesToAdd: Int, owner: AbstractParty, stateStatus: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) { - val cash = Cash() - cordaDB.transaction { - (1..statesToAdd).map { createCashTransaction(cash, it.DOLLARS, owner) }.forEach { - storeTransaction(it) - createVaultStatesFromTransaction(it, stateStatus) - } - } - } - - private fun createLinearStateTransaction(idString: String, - parties: List = listOf(), - linearString: String = "foo", - linearNumber: Long = 0L, - linearBoolean: Boolean = false): SignedTransaction { - val tx = TransactionBuilder(notary = dummyNotary.party).apply { - addOutputState(DummyLinearContract.State( - linearId = UniqueIdentifier(idString), - participants = parties, - linearString = linearString, - linearNumber = linearNumber, - linearBoolean = linearBoolean, - linearTimestamp = clock.instant()), DUMMY_LINEAR_CONTRACT_PROGRAM_ID - ) - addCommand(dummyCommand()) - } - return notaryServices.signInitialTransaction(tx) - } - - private fun addLinearStates(statesToAdd: Int, parties: List) { - cordaDB.transaction { - (1..statesToAdd).map { createLinearStateTransaction("A".repeat(it), parties) }.forEach { - storeTransaction(it) - createVaultStatesFromTransaction(it) - } - } - } - - private fun createCommodityTransaction(amount: Amount>, owner: AbstractParty): SignedTransaction { - val txBuilder = TransactionBuilder(notary = dummyNotary.party) - OnLedgerAsset.generateIssue(txBuilder, TransactionState(CommodityState(amount, owner), Obligation.PROGRAM_ID, dummyNotary.party), Obligation.Commands.Issue()) - return notaryServices.signInitialTransaction(txBuilder) - } - - private fun addCommodityStates(statesToAdd: Int, owner: AbstractParty) { - cordaDB.transaction { - (1..statesToAdd).map { - createCommodityTransaction(Amount(it.toLong(), Issued(bankOfCorda.ref(2), Commodity.getInstance("FCOJ")!!)), owner) - }.forEach { - storeTransaction(it) - createVaultStatesFromTransaction(it) - } - } - } - - private fun createNotaryChangeTransaction(inputs: List, paramsHash: SecureHash): SignedTransaction { - val notaryTx = NotaryChangeTransactionBuilder(inputs, DUMMY_NOTARY, CHARLIE, paramsHash).build() - val notaryKey = DUMMY_NOTARY.owningKey - val signableData = SignableData(notaryTx.id, SignatureMetadata(3, Crypto.findSignatureScheme(notaryKey).schemeNumberID)) - val notarySignature = notaryServices.keyManagementService.sign(signableData, notaryKey) - return SignedTransaction(notaryTx, listOf(notarySignature)) - } - - private fun createVaultStatesFromNotaryChangeTransaction(tx: SignedTransaction, inputs: List>) { - cordaDB.transaction { - inputs.forEachIndexed { index, state -> - val constraintInfo = Vault.ConstraintInfo(state.constraint) - val persistentState = VaultSchemaV1.VaultStates( - notary = tx.notary!!, - contractStateClassName = state.data.javaClass.name, - stateStatus = Vault.StateStatus.UNCONSUMED, - recordedTime = clock.instant(), - relevancyStatus = Vault.RelevancyStatus.RELEVANT, //Always persist as relevant to mimic V3 - constraintType = constraintInfo.type(), - constraintData = constraintInfo.data() - ) - persistentState.stateRef = PersistentStateRef(tx.id.toString(), index) - session.save(persistentState) - } - } - } - - private fun getState(clazz: Class): T { - return cordaDB.transaction { - val criteriaBuilder = cordaDB.entityManagerFactory.criteriaBuilder - val criteriaQuery = criteriaBuilder.createQuery(clazz) - val queryRootStates = criteriaQuery.from(clazz) - criteriaQuery.select(queryRootStates) - val query = session.createQuery(criteriaQuery) - query.singleResult - } - } - - private fun checkStatesEqual(expected: VaultSchemaV1.VaultStates, actual: VaultSchemaV1.VaultStates) { - assertEquals(expected.notary, actual.notary) - assertEquals(expected.stateStatus, actual.stateStatus) - assertEquals(expected.relevancyStatus, actual.relevancyStatus) - } - - private fun addToStatePartyTable(stateAndRef: StateAndRef) { - cordaDB.transaction { - val persistentStateRef = PersistentStateRef(stateAndRef.ref.txhash.toString(), stateAndRef.ref.index) - val session = currentDBSession() - stateAndRef.state.data.participants.forEach { - val persistentParty = VaultSchemaV1.PersistentParty( - persistentStateRef, - it - ) - session.save(persistentParty) - } - } - } - - @Test(timeout=300_000) - fun `Check a simple migration works`() { - addCashStates(10, BOB) - addCashStates(10, ALICE) - assertEquals(20, getVaultStateCount()) - assertEquals(0, getStatePartyCount()) - val migration = VaultStateMigration() - migration.execute(liquibaseDB) - assertEquals(20, getVaultStateCount()) - assertEquals(20, getStatePartyCount()) - assertEquals(10, getVaultStateCount(Vault.RelevancyStatus.RELEVANT)) - } - - @Test(timeout=300_000) - fun `Check state paging works`() { - addCashStates(1010, BOB) - - assertEquals(0, getStatePartyCount()) - val migration = VaultStateMigration() - migration.execute(liquibaseDB) - assertEquals(1010, getStatePartyCount()) - assertEquals(1010, getVaultStateCount()) - assertEquals(0, getVaultStateCount(Vault.RelevancyStatus.NOT_RELEVANT)) - } - - @Test(timeout=300_000) - fun `Check state fields are correct`() { - val tx = createCashTransaction(Cash(), 100.DOLLARS, ALICE) - storeTransaction(tx) - createVaultStatesFromTransaction(tx) - val expectedPersistentParty = VaultSchemaV1.PersistentParty( - PersistentStateRef(tx.id.toString(), 0), - ALICE - ) - val state = tx.coreTransaction.outputs.first() - val constraintInfo = Vault.ConstraintInfo(state.constraint) - val expectedPersistentState = VaultSchemaV1.VaultStates( - notary = state.notary, - contractStateClassName = state.data.javaClass.name, - stateStatus = Vault.StateStatus.UNCONSUMED, - recordedTime = clock.instant(), - relevancyStatus = Vault.RelevancyStatus.NOT_RELEVANT, - constraintType = constraintInfo.type(), - constraintData = constraintInfo.data() - ) - - val migration = VaultStateMigration() - migration.execute(liquibaseDB) - val persistentStateParty = getState(VaultSchemaV1.PersistentParty::class.java) - val persistentState = getState(VaultSchemaV1.VaultStates::class.java) - checkStatesEqual(expectedPersistentState, persistentState) - assertEquals(expectedPersistentParty.x500Name, persistentStateParty.x500Name) - assertEquals(expectedPersistentParty.compositeKey, persistentStateParty.compositeKey) - } - - @Test(timeout=300_000) - fun `Check the connection is open post migration`() { - // Liquibase automatically closes the database connection when doing an actual migration. This test ensures the custom migration - // leaves it open. - addCashStates(12, ALICE) - - val migration = VaultStateMigration() - migration.execute(liquibaseDB) - assertFalse(cordaDB.dataSource.connection.isClosed) - } - - @Test(timeout=300_000) - fun `All parties added to state party table`() { - val stx = createLinearStateTransaction("test", parties = listOf(ALICE, BOB, CHARLIE)) - storeTransaction(stx) - createVaultStatesFromTransaction(stx) - - val migration = VaultStateMigration() - migration.execute(liquibaseDB) - assertEquals(3, getStatePartyCount()) - assertEquals(1, getVaultStateCount()) - assertEquals(0, getVaultStateCount(Vault.RelevancyStatus.NOT_RELEVANT)) - } - - @Test(timeout=300_000) - fun `State with corresponding transaction missing fails migration`() { - val cash = Cash() - val unknownTx = createCashTransaction(cash, 100.DOLLARS, BOB) - createVaultStatesFromTransaction(unknownTx) - - addCashStates(10, BOB) - val migration = VaultStateMigration() - assertFailsWith { migration.execute(liquibaseDB) } - assertEquals(10, getStatePartyCount()) - - // Now add the missing transaction and ensure that the migration succeeds - storeTransaction(unknownTx) - migration.execute(liquibaseDB) - assertEquals(11, getStatePartyCount()) - } - - @Test(timeout=300_000) - fun `State with unknown ID is handled correctly`() { - addCashStates(1, CHARLIE) - addCashStates(10, BOB) - val migration = VaultStateMigration() - migration.execute(liquibaseDB) - assertEquals(11, getStatePartyCount()) - assertEquals(1, getVaultStateCount(Vault.RelevancyStatus.NOT_RELEVANT)) - assertEquals(10, getVaultStateCount(Vault.RelevancyStatus.RELEVANT)) - } - - @Test(timeout = 300_000) - fun `Null database causes migration to fail`() { - val migration = VaultStateMigration() - // Just check this does not throw an exception - assertThatExceptionOfType(VaultStateMigrationException::class.java).isThrownBy { - migration.execute(null) - } - } - - @Test(timeout=300_000) - fun `State with non-owning key for our name marked as relevant`() { - val tx = createCashTransaction(Cash(), 100.DOLLARS, BOB2) - storeTransaction(tx) - createVaultStatesFromTransaction(tx) - val state = tx.coreTransaction.outputs.first() - val constraintInfo = Vault.ConstraintInfo(state.constraint) - val expectedPersistentState = VaultSchemaV1.VaultStates( - notary = state.notary, - contractStateClassName = state.data.javaClass.name, - stateStatus = Vault.StateStatus.UNCONSUMED, - recordedTime = clock.instant(), - relevancyStatus = Vault.RelevancyStatus.RELEVANT, - constraintType = constraintInfo.type(), - constraintData = constraintInfo.data() - ) - val migration = VaultStateMigration() - migration.execute(liquibaseDB) - val persistentState = getState(VaultSchemaV1.VaultStates::class.java) - checkStatesEqual(expectedPersistentState, persistentState) - } - - @Test(timeout=300_000) - fun `State already in state party table is excluded`() { - val tx = createCashTransaction(Cash(), 100.DOLLARS, BOB) - storeTransaction(tx) - createVaultStatesFromTransaction(tx) - addToStatePartyTable(tx.coreTransaction.outRef(0)) - addCashStates(5, BOB) - assertEquals(1, getStatePartyCount()) - val migration = VaultStateMigration() - migration.execute(liquibaseDB) - assertEquals(6, getStatePartyCount()) - } - - @Test(timeout=300_000) - fun `Consumed states are not migrated`() { - addCashStates(1010, BOB, Vault.StateStatus.CONSUMED) - assertEquals(0, getStatePartyCount()) - val migration = VaultStateMigration() - migration.execute(liquibaseDB) - assertEquals(0, getStatePartyCount()) - } - - @Test(timeout=300_000) - fun `State created with notary change transaction can be migrated`() { - // This test is a little bit of a hack - it checks that these states are migrated correctly by looking at params in the database, - // but these will not be there for V3 nodes. Handling for this must be tested manually. - val cashTx = createCashTransaction(Cash(), 5.DOLLARS, BOB) - val cashTx2 = createCashTransaction(Cash(), 10.DOLLARS, BOB) - val notaryTx = createNotaryChangeTransaction(listOf(StateRef(cashTx.id, 0), StateRef(cashTx2.id, 0)), SecureHash.allOnesHash) - createVaultStatesFromTransaction(cashTx, stateStatus = Vault.StateStatus.CONSUMED) - createVaultStatesFromTransaction(cashTx2, stateStatus = Vault.StateStatus.CONSUMED) - createVaultStatesFromNotaryChangeTransaction(notaryTx, cashTx.coreTransaction.outputs + cashTx2.coreTransaction.outputs) - storeTransaction(cashTx) - storeTransaction(cashTx2) - storeTransaction(notaryTx) - val migration = VaultStateMigration() - migration.execute(liquibaseDB) - assertEquals(2, getStatePartyCount()) - } - - // Used to test migration performance - @Test(timeout=300_000) -@Ignore - fun `Migrate large database`() { - val statesAtOnce = 500L - val stateMultiplier = 300L - logger.info("Start adding states to vault") - (1..stateMultiplier).forEach { - addCashStates(statesAtOnce.toInt(), BOB) - } - logger.info("Finish adding states to vault") - val migration = VaultStateMigration() - migration.execute(liquibaseDB) - assertEquals((statesAtOnce * stateMultiplier), getStatePartyCount()) - } - - private fun makePersistentDataSourceProperties(): Properties { - val props = Properties() - props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource") - props.setProperty("dataSource.url", "jdbc:h2:~/test/persistence;DB_CLOSE_ON_EXIT=TRUE") - props.setProperty("dataSource.user", "sa") - props.setProperty("dataSource.password", "") - return props - } - - // Used to generate a persistent database for further testing. - @Test(timeout=300_000) -@Ignore - fun `Create persistent DB`() { - val cashStatesToAdd = 1000 - val linearStatesToAdd = 0 - val commodityStatesToAdd = 0 - val stateMultiplier = 10 - - cordaDB = configureDatabase(makePersistentDataSourceProperties(), DatabaseConfig(), notaryServices.identityService::wellKnownPartyFromX500Name, notaryServices.identityService::wellKnownPartyFromAnonymous) - - // Starting the database this way runs the migration under test. This is fine for the unit tests (as the changelog table is ignored), - // but when starting an actual node using these databases the migration will be skipped, as it has an entry in the changelog table. - // This must therefore be removed. - cordaDB.dataSource.connection.createStatement().use { - it.execute("DELETE FROM DATABASECHANGELOG WHERE FILENAME IN ('migration/vault-schema.changelog-v9.xml')") - } - - for (i in 1..stateMultiplier) { - addCashStates(cashStatesToAdd, BOB) - addLinearStates(linearStatesToAdd, listOf(BOB, ALICE)) - addCommodityStates(commodityStatesToAdd, BOB) - } - saveOurKeys(listOf(bob.keyPair)) - saveAllIdentities(listOf(BOB_IDENTITY, ALICE_IDENTITY, BOC_IDENTITY, dummyNotary.identity)) - cordaDB.close() - } - - @Test(timeout=300_000) -@Ignore - fun `Run on persistent DB`() { - cordaDB = configureDatabase(makePersistentDataSourceProperties(), DatabaseConfig(), notaryServices.identityService::wellKnownPartyFromX500Name, notaryServices.identityService::wellKnownPartyFromAnonymous) - val connection = (liquibaseDB.connection as JdbcConnection) - Mockito.`when`(connection.url).thenReturn(cordaDB.jdbcUrl) - Mockito.`when`(connection.wrappedConnection).thenReturn(cordaDB.dataSource.connection) - val migration = VaultStateMigration() - migration.execute(liquibaseDB) - cordaDB.close() - } -} - diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index 44b44bcf40..2da031f5b2 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -9,6 +9,7 @@ import net.corda.core.flows.NotaryFlow import net.corda.core.flows.StateReplacementException import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.internal.getRequiredTransaction import net.corda.core.node.ServiceHub import net.corda.core.node.StatesToRecord import net.corda.core.transactions.TransactionBuilder @@ -116,7 +117,7 @@ class NotaryChangeTests { val newState = future.getOrThrow() assertEquals(newState.state.notary, newNotary) - val recordedTx = clientNodeA.services.validatedTransactions.getTransaction(newState.ref.txhash)!! + val recordedTx = clientNodeA.services.getRequiredTransaction(newState.ref.txhash) val notaryChangeTx = recordedTx.resolveNotaryChangeTransaction(clientNodeA.services) // Check that all encumbrances have been propagated to the outputs @@ -140,7 +141,7 @@ class NotaryChangeTests { // We don't to tx resolution when moving state to another node, so need to add the issue transaction manually // to node B. The resolution process is tested later during notarisation. - clientNodeB.services.recordTransactions(clientNodeA.services.validatedTransactions.getTransaction(issued.ref.txhash)!!) + clientNodeB.services.recordTransactions(clientNodeA.services.getRequiredTransaction(issued.ref.txhash)) val changedNotary = changeNotary(moved, clientNodeB, newNotaryParty) val movedBack = moveState(changedNotary, clientNodeB, clientNodeA) diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt index b67921cf8c..f1c45cf30d 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt @@ -1,6 +1,5 @@ package net.corda.node.services.persistence -import org.mockito.kotlin.* import net.corda.core.contracts.Amount import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef @@ -10,6 +9,7 @@ import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.internal.verification.toVerifyingServiceHub import net.corda.core.node.StatesToRecord import net.corda.core.node.services.IdentityService import net.corda.core.node.services.Vault @@ -28,7 +28,6 @@ import net.corda.finance.schemas.CashSchemaV1 import net.corda.finance.test.SampleCashSchemaV1 import net.corda.finance.test.SampleCashSchemaV2 import net.corda.finance.test.SampleCashSchemaV3 -import net.corda.node.internal.NodeServicesForResolution import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.schema.ContractStateAndRef import net.corda.node.services.schema.NodeSchemaService @@ -41,7 +40,14 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.HibernateConfiguration import net.corda.nodeapi.internal.persistence.HibernateSchemaChangeException -import net.corda.testing.core.* +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.BOC_NAME +import net.corda.testing.core.CHARLIE_NAME +import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.core.TestIdentity +import net.corda.testing.core.singleIdentity import net.corda.testing.internal.configureDatabase import net.corda.testing.internal.vault.DummyDealStateSchemaV1 import net.corda.testing.internal.vault.DummyLinearStateSchemaV1 @@ -49,15 +55,25 @@ import net.corda.testing.internal.vault.DummyLinearStateSchemaV2 import net.corda.testing.internal.vault.VaultFiller import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties -import org.assertj.core.api.Assertions import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.hibernate.SessionFactory -import org.junit.* +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.argThat +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever import java.math.BigDecimal import java.time.Clock import java.time.Instant -import java.util.* +import java.util.Currency +import java.util.Random +import java.util.UUID import javax.persistence.EntityManager import javax.persistence.Tuple import javax.persistence.criteria.CriteriaBuilder @@ -85,7 +101,7 @@ class HibernateConfigurationTest { val vault: VaultService get() = services.vaultService // Hibernate configuration objects - lateinit var hibernateConfig: HibernateConfiguration + private lateinit var hibernateConfig: HibernateConfiguration private lateinit var hibernatePersister: PersistentStateService private lateinit var sessionFactory: SessionFactory private lateinit var entityManager: EntityManager @@ -126,7 +142,7 @@ class HibernateConfigurationTest { override val vaultService = NodeVaultService( Clock.systemUTC(), keyManagementService, - servicesForResolution as NodeServicesForResolution, + toVerifyingServiceHub(), database, schemaService, cordappClassloader @@ -236,7 +252,7 @@ class HibernateConfigurationTest { // execute query val queryResults = entityManager.createQuery(criteriaQuery).resultList - Assertions.assertThat(queryResults.size).isEqualTo(3) + assertThat(queryResults.size).isEqualTo(3) } @Test(timeout=300_000) @@ -327,7 +343,7 @@ class HibernateConfigurationTest { // execute query val queryResults = query.resultList - Assertions.assertThat(queryResults.size).isEqualTo(15) + assertThat(queryResults.size).isEqualTo(15) // try towards end query.firstResult = 100 @@ -335,7 +351,7 @@ class HibernateConfigurationTest { val lastQueryResults = query.resultList - Assertions.assertThat(lastQueryResults.size).isEqualTo(10) + assertThat(lastQueryResults.size).isEqualTo(10) } /** diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt index 7bd3690925..424d6810f2 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt @@ -28,7 +28,6 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.testing.core.singleIdentity import net.corda.testing.flows.registerCoreFlowFactory import net.corda.coretesting.internal.rigorousMock -import net.corda.node.internal.NodeServicesForResolution import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.enclosedCordapp import net.corda.testing.node.internal.startFlow @@ -86,11 +85,10 @@ class VaultSoftLockManagerTest { private val mockNet = InternalMockNetwork(cordappsForAllNodes = listOf(enclosedCordapp()), defaultFactory = { args -> object : InternalMockNetwork.MockNode(args) { override fun makeVaultService(keyManagementService: KeyManagementService, - services: NodeServicesForResolution, database: CordaPersistence, cordappLoader: CordappLoader): VaultServiceInternal { val node = this - val realVault = super.makeVaultService(keyManagementService, services, database, cordappLoader) + val realVault = super.makeVaultService(keyManagementService, database, cordappLoader) return object : SingletonSerializeAsToken(), VaultServiceInternal by realVault { override fun softLockRelease(lockId: UUID, stateRefs: NonEmptySet?) { // Should be called before flow is removed diff --git a/node/src/test/kotlin/net/corda/notary/experimental/raft/RaftNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/notary/experimental/raft/RaftNotaryServiceTests.kt index cde76833d9..9c69d86b17 100644 --- a/node/src/test/kotlin/net/corda/notary/experimental/raft/RaftNotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/notary/experimental/raft/RaftNotaryServiceTests.kt @@ -1,19 +1,17 @@ package net.corda.notary.experimental.raft import net.corda.core.contracts.StateAndRef -import net.corda.core.contracts.StateRef import net.corda.core.flows.NotaryError import net.corda.core.flows.NotaryException import net.corda.core.flows.NotaryFlow import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.concurrent.map -import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds import net.corda.testing.contracts.DummyContract +import net.corda.testing.contracts.DummyContract.SingleOwnerState import net.corda.testing.core.DUMMY_BANK_A_NAME -import net.corda.testing.core.dummyCommand import net.corda.testing.core.singleIdentity import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.InProcess @@ -22,7 +20,7 @@ import net.corda.testing.node.ClusterSpec import net.corda.testing.node.NotarySpec import net.corda.testing.node.internal.DUMMY_CONTRACTS_CORDAPP import org.junit.Test -import java.util.* +import java.util.Random import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -39,20 +37,13 @@ class RaftNotaryServiceTests { val bankA = startNode(providedName = DUMMY_BANK_A_NAME).map { (it as InProcess) }.getOrThrow() val inputState = issueState(bankA, defaultNotaryIdentity) - val firstTxBuilder = TransactionBuilder(defaultNotaryIdentity) - .addInputState(inputState) - .addCommand(dummyCommand(bankA.services.myInfo.singleIdentity().owningKey)) + val firstTxBuilder = DummyContract.move(inputState, bankA.services.myInfo.singleIdentity()) val firstSpendTx = bankA.services.signInitialTransaction(firstTxBuilder) val firstSpend = bankA.startFlow(NotaryFlow.Client(firstSpendTx)) firstSpend.getOrThrow() - val secondSpendBuilder = TransactionBuilder(defaultNotaryIdentity).withItems(inputState).run { - val dummyState = DummyContract.SingleOwnerState(0, bankA.services.myInfo.singleIdentity()) - addOutputState(dummyState, DummyContract.PROGRAM_ID) - addCommand(dummyCommand(bankA.services.myInfo.singleIdentity().owningKey)) - this - } + val secondSpendBuilder = DummyContract.move(inputState, bankA.services.myInfo.singleIdentity()) val secondSpendTx = bankA.services.signInitialTransaction(secondSpendBuilder) val secondSpend = bankA.startFlow(NotaryFlow.Client(secondSpendTx)) @@ -78,10 +69,10 @@ class RaftNotaryServiceTests { } } - private fun issueState(nodeHandle: InProcess, notary: Party): StateAndRef<*> { + private fun issueState(nodeHandle: InProcess, notary: Party): StateAndRef { val builder = DummyContract.generateInitial(Random().nextInt(), notary, nodeHandle.services.myInfo.singleIdentity().ref(0)) val stx = nodeHandle.services.signInitialTransaction(builder) nodeHandle.services.recordTransactions(stx) - return StateAndRef(stx.coreTransaction.outputs.first(), StateRef(stx.id, 0)) + return stx.coreTransaction.outRef(0) } } diff --git a/serialization/build.gradle b/serialization/build.gradle index 6f3cffa35a..7393bae2e7 100644 --- a/serialization/build.gradle +++ b/serialization/build.gradle @@ -28,8 +28,6 @@ dependencies { // For caches rather than guava implementation "com.github.ben-manes.caffeine:caffeine:$caffeine_version" - testImplementation project(":serialization") - testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}" testImplementation "junit:junit:$junit_version" diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/RemoteTypeCarpenter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/RemoteTypeCarpenter.kt index e7b97cf45b..df90dbc534 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/RemoteTypeCarpenter.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/RemoteTypeCarpenter.kt @@ -32,7 +32,7 @@ class SchemaBuildingRemoteTypeCarpenter(private val carpenter: ClassCarpenter): } // Anything else, such as arrays, will be taken care of by the above } } catch (e: ClassCarpenterException) { - throw NotSerializableException("${typeInformation.typeIdentifier.name}: ${e.message}") + throw NotSerializableException("${typeInformation.typeIdentifier.name}: ${e.message}").apply { initCause(e) } } return try { @@ -40,7 +40,7 @@ class SchemaBuildingRemoteTypeCarpenter(private val carpenter: ClassCarpenter): } catch (e: ClassNotFoundException) { // This might happen if we've been asked to carpent up a parameterised type, and it's the rawtype itself // rather than any of its type parameters that were missing. - throw NotSerializableException("Could not carpent ${typeInformation.typeIdentifier.prettyPrint(false)}") + throw NotSerializableException("Could not carpent ${typeInformation.typeIdentifier.prettyPrint(false)}").apply { initCause(e) } } } @@ -87,6 +87,6 @@ class SchemaBuildingRemoteTypeCarpenter(private val carpenter: ClassCarpenter): } private fun RemoteTypeInformation.AnEnum.carpentEnum() { - carpenter.build(EnumSchema(name = typeIdentifier.name, fields = members.associate { it to EnumField() })) + carpenter.build(EnumSchema(name = typeIdentifier.name, fields = members.associateWith { EnumField() })) } -} \ No newline at end of file +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CustomSerializationSchemeAdapter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/verifier/CustomSerializationSchemeAdapter.kt similarity index 66% rename from node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CustomSerializationSchemeAdapter.kt rename to serialization/src/main/kotlin/net/corda/serialization/internal/verifier/CustomSerializationSchemeAdapter.kt index f656f81502..3bbdb170a9 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CustomSerializationSchemeAdapter.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/verifier/CustomSerializationSchemeAdapter.kt @@ -1,8 +1,10 @@ -package net.corda.nodeapi.internal.serialization +package net.corda.serialization.internal.verifier -import net.corda.core.serialization.SerializationSchemeContext +import net.corda.core.CordaException +import net.corda.core.internal.loadClassOfType import net.corda.core.serialization.CustomSerializationScheme import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.SerializationSchemeContext import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.internal.CustomSerializationSchemeUtils.Companion.getCustomSerializationMagicFromSchemeId import net.corda.core.utilities.ByteSequence @@ -12,8 +14,7 @@ import java.io.ByteArrayOutputStream import java.io.NotSerializableException class CustomSerializationSchemeAdapter(private val customScheme: CustomSerializationScheme): SerializationScheme { - - val serializationSchemeMagic = getCustomSerializationMagicFromSchemeId(customScheme.getSchemeId()) + private val serializationSchemeMagic = getCustomSerializationMagicFromSchemeId(customScheme.getSchemeId()) override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean { return magic == serializationSchemeMagic @@ -44,4 +45,21 @@ class CustomSerializationSchemeAdapter(private val customScheme: CustomSerializa override val whitelist = context.whitelist override val properties = context.properties } -} \ No newline at end of file +} + +@Suppress("ThrowsCount") +fun loadCustomSerializationScheme(className: String, classLoader: ClassLoader): SerializationScheme { + val schemeClass = try { + loadClassOfType(className, false, classLoader) + } catch (e: ClassNotFoundException) { + throw CordaException("$className was declared as a custom serialization scheme but could not be found.") + } catch (e: ClassCastException) { + throw CordaException("$className was declared as a custom serialization scheme but does not implement CustomSerializationScheme") + } + val constructor = try { + schemeClass.getDeclaredConstructor() + } catch (e: NoSuchMethodException) { + throw CordaException("$className was declared as a custom serialization scheme but does not have a no argument constructor.") + } + return CustomSerializationSchemeAdapter(constructor.newInstance()) +} diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/verifier/ExternalVerifierTypes.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/verifier/ExternalVerifierTypes.kt new file mode 100644 index 0000000000..61f00b2b98 --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/verifier/ExternalVerifierTypes.kt @@ -0,0 +1,87 @@ +package net.corda.serialization.internal.verifier + +import net.corda.core.contracts.Attachment +import net.corda.core.contracts.StateRef +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.toStringShort +import net.corda.core.identity.Party +import net.corda.core.internal.SerializedTransactionState +import net.corda.core.node.NetworkParameters +import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.Try +import java.io.DataInputStream +import java.io.DataOutputStream +import java.io.EOFException +import java.security.PublicKey + +typealias SerializedNetworkParameters = SerializedBytes + +@CordaSerializable +sealed interface ExternalVerifierInbound { + data class Initialisation( + val customSerializerClassNames: Set, + val serializationWhitelistClassNames: Set, + val customSerializationSchemeClassName: String?, + val serializedCurrentNetworkParameters: SerializedNetworkParameters + ) : ExternalVerifierInbound { + val currentNetworkParameters: NetworkParameters by lazy(serializedCurrentNetworkParameters::deserialize) + + override fun toString(): String { + return "Initialisation(" + + "customSerializerClassNames=$customSerializerClassNames, " + + "serializationWhitelistClassNames=$serializationWhitelistClassNames, " + + "customSerializationSchemeClassName=$customSerializationSchemeClassName, " + + "currentNetworkParameters=$currentNetworkParameters)" + } + } + + data class VerificationRequest( + val stx: SignedTransaction, + val stxInputsAndReferences: Map, + val checkSufficientSignatures: Boolean + ) : ExternalVerifierInbound + + data class PartiesResult(val parties: List) : ExternalVerifierInbound + data class AttachmentResult(val attachment: AttachmentWithTrust?) : ExternalVerifierInbound + data class AttachmentsResult(val attachments: List) : ExternalVerifierInbound + data class NetworkParametersResult(val networkParameters: NetworkParameters?) : ExternalVerifierInbound + data class TrustedClassAttachmentResult(val id: SecureHash?) : ExternalVerifierInbound +} + +@CordaSerializable +data class AttachmentWithTrust(val attachment: Attachment, val isTrusted: Boolean) + +@CordaSerializable +sealed interface ExternalVerifierOutbound { + sealed interface VerifierRequest : ExternalVerifierOutbound { + data class GetParties(val keys: Set) : VerifierRequest { + override fun toString(): String = "GetParty(keys=${keys.map { it.toStringShort() }}})" + } + data class GetAttachment(val id: SecureHash) : VerifierRequest + data class GetAttachments(val ids: Set) : VerifierRequest + data class GetNetworkParameters(val id: SecureHash) : VerifierRequest + data class GetTrustedClassAttachment(val className: String) : VerifierRequest + } + + data class VerificationResult(val result: Try) : ExternalVerifierOutbound +} + +fun DataOutputStream.writeCordaSerializable(payload: Any) { + val serialised = payload.serialize() + writeInt(serialised.size) + serialised.writeTo(this) + flush() +} + +inline fun DataInputStream.readCordaSerializable(): T { + val length = readInt() + val bytes = readNBytes(length) + if (bytes.size != length) { + throw EOFException("Incomplete read of ${T::class.java.name}") + } + return bytes.deserialize() +} diff --git a/settings.gradle b/settings.gradle index 69c3ea22d3..8f2543aebb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -48,6 +48,7 @@ include 'node-api' include 'node-api-tests' include 'node' include 'node:capsule' +include 'verifier' include 'client:jackson' include 'client:jfx' include 'client:mock' diff --git a/testing/core-test-utils/src/main/kotlin/net/corda/coretesting/internal/InternalSerializationTestHelpers.kt b/testing/core-test-utils/src/main/kotlin/net/corda/coretesting/internal/InternalSerializationTestHelpers.kt index 6345dd7549..28cf91fc2b 100644 --- a/testing/core-test-utils/src/main/kotlin/net/corda/coretesting/internal/InternalSerializationTestHelpers.kt +++ b/testing/core-test-utils/src/main/kotlin/net/corda/coretesting/internal/InternalSerializationTestHelpers.kt @@ -7,7 +7,7 @@ import net.corda.core.serialization.CustomSerializationScheme import net.corda.core.serialization.SerializationCustomSerializer import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.internal.SerializationEnvironment -import net.corda.nodeapi.internal.serialization.CustomSerializationSchemeAdapter +import net.corda.serialization.internal.verifier.CustomSerializationSchemeAdapter import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme import net.corda.nodeapi.internal.serialization.kryo.KRYO_CHECKPOINT_CONTEXT import net.corda.nodeapi.internal.serialization.kryo.KryoCheckpointSerializer diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index 7c19eb0265..ecf26a5211 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -1,9 +1,13 @@ package net.corda.testing.node import com.google.common.collect.MutableClassToInstanceMap +import net.corda.core.CordaInternal import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractClassName +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionState import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic @@ -13,9 +17,13 @@ import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.PLATFORM_VERSION import net.corda.core.internal.VisibleForTesting +import net.corda.core.internal.cordapp.CordappProviderInternal +import net.corda.core.internal.getRequiredTransaction +import net.corda.core.internal.mapToSet import net.corda.core.internal.requireSupportedHashType import net.corda.core.internal.telemetry.TelemetryComponent import net.corda.core.internal.telemetry.TelemetryServiceImpl +import net.corda.core.internal.verification.VerifyingServiceHub import net.corda.core.messaging.DataFeed import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.FlowProgressHandle @@ -34,23 +42,22 @@ import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NetworkParametersService import net.corda.core.node.services.ServiceLifecycleObserver import net.corda.core.node.services.TransactionStorage -import net.corda.core.node.services.TransactionVerifierService import net.corda.core.node.services.VaultService import net.corda.core.node.services.diagnostics.DiagnosticsService import net.corda.core.node.services.vault.CordaTransactionSupport import net.corda.core.serialization.SerializeAsToken +import net.corda.core.serialization.internal.AttachmentsClassLoaderCacheImpl import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.coretesting.internal.DEV_ROOT_CA import net.corda.node.VersionInfo -import net.corda.node.internal.ServicesForResolutionImpl -import net.corda.node.internal.NodeServicesForResolution import net.corda.node.internal.cordapp.JarScanningCordappLoader import net.corda.node.services.api.SchemaService import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.api.StateMachineRecordedTransactionMappingStorage import net.corda.node.services.api.VaultServiceInternal import net.corda.node.services.api.WritableTransactionStorage +import net.corda.node.services.attachments.NodeAttachmentTrustCalculator import net.corda.node.services.diagnostics.NodeDiagnosticsService import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.identity.PersistentIdentityService @@ -58,7 +65,6 @@ import net.corda.node.services.keys.BasicHSMKeyManagementService import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.persistence.PublicKeyToOwningIdentityCacheImpl import net.corda.node.services.schema.NodeSchemaService -import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.services.vault.NodeVaultService import net.corda.nodeapi.internal.cordapp.CordappLoader import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -69,6 +75,7 @@ import net.corda.testing.core.TestIdentity import net.corda.testing.internal.MockCordappProvider import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.testing.internal.configureDatabase +import net.corda.testing.internal.services.InternalMockAttachmentStorage import net.corda.testing.node.internal.DriverDSLImpl import net.corda.testing.node.internal.MockCryptoService import net.corda.testing.node.internal.MockKeyManagementService @@ -116,7 +123,6 @@ open class MockServices private constructor( *arrayOf(initialIdentity.keyPair) + moreKeys ) ) : ServiceHub { - companion object { private fun cordappLoaderForPackages(packages: Iterable, versionInfo: VersionInfo = VersionInfo.UNKNOWN): CordappLoader { return JarScanningCordappLoader.fromJarUrls(cordappsForPackages(packages).map { it.jarFile.toUri().toURL() }, versionInfo) @@ -485,24 +491,20 @@ open class MockServices private constructor( private val mockCordappProvider: MockCordappProvider = MockCordappProvider(cordappLoader, attachments).also { it.start() } - override val transactionVerifierService: TransactionVerifierService - get() = InMemoryTransactionVerifierService( - numberOfWorkers = 2, - cordappProvider = mockCordappProvider, - attachments = attachments - ) override val cordappProvider: CordappProvider get() = mockCordappProvider override var networkParametersService: NetworkParametersService = MockNetworkParametersStorage(initialNetworkParameters) override val diagnosticsService: DiagnosticsService = NodeDiagnosticsService() - protected val servicesForResolution: ServicesForResolution - get() = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersService, validatedTransactions) + // This is kept here for backwards compatibility, otherwise this has no extra utility. + protected val servicesForResolution: ServicesForResolution get() = verifyingView + + private val verifyingView: VerifyingServiceHub by lazy { VerifyingView(this) } internal fun makeVaultService(schemaService: SchemaService, database: CordaPersistence, cordappLoader: CordappLoader): VaultServiceInternal { return NodeVaultService( clock, keyManagementService, - servicesForResolution as NodeServicesForResolution, + verifyingView, database, schemaService, cordappLoader.appClassLoader @@ -511,9 +513,9 @@ open class MockServices private constructor( // This needs to be internal as MutableClassToInstanceMap is a guava type and shouldn't be part of our public API /** A map of available [CordaService] implementations */ - internal val cordappServices: MutableClassToInstanceMap = MutableClassToInstanceMap.create() + internal val cordappServices: MutableClassToInstanceMap = MutableClassToInstanceMap.create() - internal val cordappTelemetryComponents: MutableClassToInstanceMap = MutableClassToInstanceMap.create() + private val cordappTelemetryComponents: MutableClassToInstanceMap = MutableClassToInstanceMap.create() override fun cordaService(type: Class): T { require(type.isAnnotationPresent(CordaService::class.java)) { "${type.name} is not a Corda service" } @@ -543,19 +545,43 @@ open class MockServices private constructor( mockCordappProvider.addMockCordapp(contractClassName, attachments) } - override fun loadState(stateRef: StateRef) = servicesForResolution.loadState(stateRef) - override fun loadStates(stateRefs: Set) = servicesForResolution.loadStates(stateRefs) + override fun loadState(stateRef: StateRef): TransactionState { + return getRequiredTransaction(stateRef.txhash).resolveBaseTransaction(this).outputs[stateRef.index] + } + + override fun loadStates(stateRefs: Set): Set> = stateRefs.mapToSet(::toStateAndRef) /** Returns a dummy Attachment, in context of signature constrains non-downgrade rule this default to contract class version `1`. */ override fun loadContractAttachment(stateRef: StateRef) = dummyAttachment -} -/** - * Function which can be used to create a mock [CordaService] for use within testing, such as an Oracle. - */ -fun createMockCordaService(serviceHub: MockServices, serviceConstructor: (AppServiceHub) -> T): T { - class MockAppServiceHubImpl(val serviceHub: MockServices, serviceConstructor: (AppServiceHub) -> T) : AppServiceHub, ServiceHub by serviceHub { - val serviceInstance: T = serviceConstructor(this) + + /** + * All [ServiceHub]s must also implement [VerifyingServiceHub]. However, since [MockServices] is part of the public API, making it + * extend [VerifyingServiceHub] would leak internal APIs. Instead we have this private view class and have the `toVerifyingServiceHub` + * extension method return it. + */ + private class VerifyingView(private val mockServices: MockServices) : VerifyingServiceHub, ServiceHub by mockServices { + override val attachmentTrustCalculator = NodeAttachmentTrustCalculator( + attachmentStorage = InternalMockAttachmentStorage(mockServices.attachments), + cacheFactory = TestingNamedCacheFactory() + ) + + override val attachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(TestingNamedCacheFactory()) + + override val cordappProvider: CordappProviderInternal get() = mockServices.mockCordappProvider + + override fun loadContractAttachment(stateRef: StateRef): Attachment = mockServices.loadContractAttachment(stateRef) + + override fun loadState(stateRef: StateRef): TransactionState<*> = mockServices.loadState(stateRef) + + override fun loadStates(stateRefs: Set): Set> = mockServices.loadStates(stateRefs) + } + + + @CordaInternal + internal class MockAppServiceHubImpl(serviceHub: MockServices, serviceConstructor: (AppServiceHub) -> T) : + AppServiceHub, VerifyingServiceHub by serviceHub.verifyingView { + internal val serviceInstance: T = serviceConstructor(this) init { serviceHub.cordappServices.putInstance(serviceInstance.javaClass, serviceInstance) @@ -576,5 +602,11 @@ fun createMockCordaService(serviceHub: MockServices, serv throw UnsupportedOperationException() } } - return MockAppServiceHubImpl(serviceHub, serviceConstructor).serviceInstance +} + +/** + * Function which can be used to create a mock [CordaService] for use within testing, such as an Oracle. + */ +fun createMockCordaService(serviceHub: MockServices, serviceConstructor: (AppServiceHub) -> T): T { + return MockServices.MockAppServiceHubImpl(serviceHub, serviceConstructor).serviceInstance } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index 50a0a2c017..c370129c15 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -352,7 +352,6 @@ open class InternalMockNetwork(cordappPackages: List = emptyList(), private val entropyCounter = AtomicReference(args.entropyRoot) override val log get() = staticLog - override val transactionVerifierWorkerCount: Int get() = 1 private var _rxIoScheduler: Scheduler? = null override val rxIoScheduler: Scheduler diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt index b460d02f30..eb52d9d614 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt @@ -3,7 +3,6 @@ package net.corda.testing.dsl import com.google.common.util.concurrent.ThreadFactoryBuilder import net.corda.core.DoNotImplement import net.corda.core.contracts.* -import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.NullKeys.NULL_SIGNATURE import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature @@ -12,6 +11,7 @@ import net.corda.core.flows.TransactionMetadata import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.* +import net.corda.core.internal.cordapp.CordappProviderInternal import net.corda.core.internal.notary.NotaryService import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution @@ -95,7 +95,6 @@ data class TestTransactionDSLInterpreter private constructor( // Implementing [ServiceHubCoreInternal] allows better use in internal Corda tests val services: ServicesForResolution = object : ServiceHubCoreInternal, ServiceHub by ledgerInterpreter.services { - // [validatedTransactions.getTransaction] needs overriding as there are no calls to // [ServiceHub.recordTransactions] in the test dsl override val validatedTransactions: TransactionStorage = @@ -129,17 +128,17 @@ data class TestTransactionDSLInterpreter private constructor( override fun loadState(stateRef: StateRef) = ledgerInterpreter.resolveStateRef(stateRef) - override fun loadStates(stateRefs: Set): Set> { - return stateRefs.map { StateAndRef(loadState(it), it) }.toSet() - } - - override val cordappProvider: CordappProvider = - ledgerInterpreter.services.cordappProvider + override val cordappProvider: CordappProviderInternal + get() = ledgerInterpreter.services.cordappProvider as CordappProviderInternal override val notaryService: NotaryService? = null override val attachmentsClassLoaderCache: AttachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(TestingNamedCacheFactory()) + override fun loadContractAttachment(stateRef: StateRef): Attachment { + return ledgerInterpreter.services.loadContractAttachment(stateRef) + } + override fun recordUnnotarisedTransaction(txn: SignedTransaction) {} override fun removeUnnotarisedTransaction(id: SecureHash) {} @@ -169,7 +168,6 @@ data class TestTransactionDSLInterpreter private constructor( override fun reference(stateRef: StateRef) { val state = ledgerInterpreter.resolveStateRef(stateRef) - @Suppress("DEPRECATION") // Will remove when feature finalised. transactionBuilder.addReferenceState(StateAndRef(state, stateRef).referenced()) } diff --git a/verifier/build.gradle b/verifier/build.gradle new file mode 100644 index 0000000000..e794026070 --- /dev/null +++ b/verifier/build.gradle @@ -0,0 +1,35 @@ +plugins { + id "org.jetbrains.kotlin.jvm" + id "application" + id "com.github.johnrengelman.shadow" +} + +application { + mainClass.set("net.corda.verifier.Main") +} + +dependencies { + implementation project(":core") + implementation project(":serialization") + implementation "com.github.ben-manes.caffeine:caffeine:$caffeine_version" + implementation "org.slf4j:jul-to-slf4j:$slf4j_version" + + runtimeOnly "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}" +} + +jar { + manifest { + attributes("Add-Opens": + "java.base/java.lang " + + "java.base/java.lang.reflect " + + "java.base/java.lang.invoke " + + "java.base/java.util " + + "java.base/java.time " + + "java.base/java.io " + + "java.base/java.net " + + "java.base/javax.net.ssl " + + "java.base/java.security.cert " + + "java.base/java.nio" + ) + } +} diff --git a/verifier/src/main/kotlin/net/corda/verifier/ExternalVerificationContext.kt b/verifier/src/main/kotlin/net/corda/verifier/ExternalVerificationContext.kt new file mode 100644 index 0000000000..3db0294a2f --- /dev/null +++ b/verifier/src/main/kotlin/net/corda/verifier/ExternalVerificationContext.kt @@ -0,0 +1,42 @@ +package net.corda.verifier + +import net.corda.core.contracts.Attachment +import net.corda.core.contracts.StateRef +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.Party +import net.corda.core.internal.SerializedTransactionState +import net.corda.core.internal.verification.VerificationSupport +import net.corda.core.node.NetworkParameters +import net.corda.core.serialization.internal.AttachmentsClassLoaderCache +import java.security.PublicKey + +class ExternalVerificationContext( + override val appClassLoader: ClassLoader, + override val attachmentsClassLoaderCache: AttachmentsClassLoaderCache, + private val externalVerifier: ExternalVerifier, + private val transactionInputsAndReferences: Map +) : VerificationSupport { + override val isResolutionLazy: Boolean get() = false + + override fun getParties(keys: Collection): List = externalVerifier.getParties(keys) + + override fun getAttachment(id: SecureHash): Attachment? = externalVerifier.getAttachment(id)?.attachment + + override fun getAttachments(ids: Collection): List { + return externalVerifier.getAttachments(ids).map { it?.attachment } + } + + override fun isAttachmentTrusted(attachment: Attachment): Boolean = externalVerifier.getAttachment(attachment.id)!!.isTrusted + + override fun getTrustedClassAttachment(className: String): Attachment? { + return externalVerifier.getTrustedClassAttachment(className) + } + + override fun getNetworkParameters(id: SecureHash?): NetworkParameters? = externalVerifier.getNetworkParameters(id) + + override fun getSerializedState(stateRef: StateRef): SerializedTransactionState = transactionInputsAndReferences.getValue(stateRef) + + override fun fixupAttachmentIds(attachmentIds: Collection): Set { + return externalVerifier.fixupAttachmentIds(attachmentIds) + } +} diff --git a/verifier/src/main/kotlin/net/corda/verifier/ExternalVerifier.kt b/verifier/src/main/kotlin/net/corda/verifier/ExternalVerifier.kt new file mode 100644 index 0000000000..fc1b5ec121 --- /dev/null +++ b/verifier/src/main/kotlin/net/corda/verifier/ExternalVerifier.kt @@ -0,0 +1,237 @@ +package net.corda.verifier + +import com.github.benmanes.caffeine.cache.Cache +import net.corda.core.contracts.Attachment +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.Party +import net.corda.core.internal.loadClassOfType +import net.corda.core.internal.mapToSet +import net.corda.core.internal.objectOrNewInstance +import net.corda.core.internal.toSynchronised +import net.corda.core.internal.toTypedArray +import net.corda.core.internal.verification.AttachmentFixups +import net.corda.core.node.NetworkParameters +import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.internal.AttachmentsClassLoaderCache +import net.corda.core.serialization.internal.AttachmentsClassLoaderCacheImpl +import net.corda.core.serialization.internal.SerializationEnvironment +import net.corda.core.serialization.internal._contextSerializationEnv +import net.corda.core.utilities.Try +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.debug +import net.corda.serialization.internal.AMQP_P2P_CONTEXT +import net.corda.serialization.internal.CordaSerializationMagic +import net.corda.serialization.internal.SerializationFactoryImpl +import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme +import net.corda.serialization.internal.amqp.AccessOrderLinkedHashMap +import net.corda.serialization.internal.amqp.SerializationFactoryCacheKey +import net.corda.serialization.internal.amqp.SerializerFactory +import net.corda.serialization.internal.amqp.amqpMagic +import net.corda.serialization.internal.verifier.AttachmentWithTrust +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.AttachmentResult +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.AttachmentsResult +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.Initialisation +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.NetworkParametersResult +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.PartiesResult +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.TrustedClassAttachmentResult +import net.corda.serialization.internal.verifier.ExternalVerifierInbound.VerificationRequest +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerificationResult +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetAttachment +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetAttachments +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetNetworkParameters +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetParties +import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetTrustedClassAttachment +import net.corda.serialization.internal.verifier.loadCustomSerializationScheme +import net.corda.serialization.internal.verifier.readCordaSerializable +import net.corda.serialization.internal.verifier.writeCordaSerializable +import java.io.DataInputStream +import java.io.DataOutputStream +import java.net.URLClassLoader +import java.nio.file.Path +import java.security.PublicKey +import java.util.Optional +import kotlin.io.path.div +import kotlin.io.path.listDirectoryEntries + +@Suppress("TooGenericExceptionCaught", "MagicNumber") +class ExternalVerifier( + private val baseDirectory: Path, + private val fromNode: DataInputStream, + private val toNode: DataOutputStream +) { + companion object { + private val log = contextLogger() + } + + private val attachmentsClassLoaderCache: AttachmentsClassLoaderCache + private val attachmentFixups = AttachmentFixups() + private val parties: OptionalCache + private val attachments: OptionalCache + private val networkParametersMap: OptionalCache + private val trustedClassAttachments: OptionalCache + + private lateinit var appClassLoader: ClassLoader + private lateinit var currentNetworkParameters: NetworkParameters + + init { + val cacheFactory = ExternalVerifierNamedCacheFactory() + attachmentsClassLoaderCache = AttachmentsClassLoaderCacheImpl(cacheFactory) + parties = cacheFactory.buildNamed("ExternalVerifier_parties") + attachments = cacheFactory.buildNamed("ExternalVerifier_attachments") + networkParametersMap = cacheFactory.buildNamed("ExternalVerifier_networkParameters") + trustedClassAttachments = cacheFactory.buildNamed("ExternalVerifier_trustedClassAttachments") + } + + fun run() { + initialise() + while (true) { + val request = fromNode.readCordaSerializable() + log.debug { "Received $request" } + verifyTransaction(request) + } + } + + private fun initialise() { + // Use a preliminary serialization context to receive the initialisation message + _contextSerializationEnv.set(SerializationEnvironment.with( + verifierSerializationFactory(), + p2pContext = AMQP_P2P_CONTEXT + )) + + log.info("Waiting for initialisation message from node...") + val initialisation = fromNode.readCordaSerializable() + log.info("Received $initialisation") + + appClassLoader = createAppClassLoader() + + // Then use the initialisation message to create the correct serialization context + _contextSerializationEnv.set(null) + _contextSerializationEnv.set(SerializationEnvironment.with( + verifierSerializationFactory(initialisation, appClassLoader).apply { + initialisation.customSerializationSchemeClassName?.let { + registerScheme(loadCustomSerializationScheme(it, appClassLoader)) + } + }, + p2pContext = AMQP_P2P_CONTEXT.withClassLoader(appClassLoader) + )) + + attachmentFixups.load(appClassLoader) + + currentNetworkParameters = initialisation.currentNetworkParameters + networkParametersMap.put(initialisation.serializedCurrentNetworkParameters.hash, Optional.of(currentNetworkParameters)) + + log.info("External verifier initialised") + } + + private fun createAppClassLoader(): ClassLoader { + val cordappJarUrls = (baseDirectory / "cordapps").listDirectoryEntries() + .stream() + .filter { it.toString().endsWith(".jar") } + .map { it.toUri().toURL() } + .toTypedArray() + log.debug { "CorDapps: ${cordappJarUrls?.joinToString()}" } + return URLClassLoader(cordappJarUrls, javaClass.classLoader) + } + + private fun verifyTransaction(request: VerificationRequest) { + val verificationContext = ExternalVerificationContext(appClassLoader, attachmentsClassLoaderCache, this, request.stxInputsAndReferences) + val result = try { + request.stx.verifyInternal(verificationContext, request.checkSufficientSignatures) + log.info("${request.stx} verified") + Try.Success(Unit) + } catch (t: Throwable) { + log.info("${request.stx} failed to verify", t) + Try.Failure(t) + } + toNode.writeCordaSerializable(VerificationResult(result)) + } + + fun getParties(keys: Collection): List { + return parties.retrieveAll(keys) { + request(GetParties(it)).parties + } + } + + fun getAttachment(id: SecureHash): AttachmentWithTrust? { + return attachments.retrieve(id) { + request(GetAttachment(id)).attachment + } + } + + fun getAttachments(ids: Collection): List { + return attachments.retrieveAll(ids) { + request(GetAttachments(it)).attachments + } + } + + fun getTrustedClassAttachment(className: String): Attachment? { + val attachmentId = trustedClassAttachments.retrieve(className) { + // GetTrustedClassAttachment returns back the attachment ID, not the whole attachment. This lets us avoid downloading the whole + // attachment again if we already have it. + request(GetTrustedClassAttachment(className)).id + } + return attachmentId?.let(::getAttachment)?.attachment + } + + fun getNetworkParameters(id: SecureHash?): NetworkParameters? { + return if (id == null) { + currentNetworkParameters + } else { + networkParametersMap.retrieve(id) { + request(GetNetworkParameters(id)).networkParameters + } + } + } + + fun fixupAttachmentIds(attachmentIds: Collection): Set = attachmentFixups.fixupAttachmentIds(attachmentIds) + + private inline fun request(request: Any): T { + log.debug { "Sending request to node: $request" } + toNode.writeCordaSerializable(request) + val response = fromNode.readCordaSerializable() + log.debug { "Received response from node: $response" } + return response + } + + private fun verifierSerializationFactory(initialisation: Initialisation? = null, classLoader: ClassLoader? = null): SerializationFactoryImpl { + val serializationFactory = SerializationFactoryImpl() + serializationFactory.registerScheme(AMQPVerifierSerializationScheme(initialisation, classLoader)) + return serializationFactory + } + + + private class AMQPVerifierSerializationScheme(initialisation: Initialisation?, classLoader: ClassLoader?) : AbstractAMQPSerializationScheme( + initialisation?.customSerializerClassNames.load(classLoader), + initialisation?.serializationWhitelistClassNames.load(classLoader), + AccessOrderLinkedHashMap(128).toSynchronised() + ) { + override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean { + return magic == amqpMagic && target == SerializationContext.UseCase.P2P + } + + override fun rpcClientSerializerFactory(context: SerializationContext) = throw UnsupportedOperationException() + override fun rpcServerSerializerFactory(context: SerializationContext) = throw UnsupportedOperationException() + + companion object { + inline fun Set?.load(classLoader: ClassLoader?): Set { + return this?.mapToSet { loadClassOfType(it, classLoader = classLoader).kotlin.objectOrNewInstance() } ?: emptySet() + } + } + } +} + +private typealias OptionalCache = Cache> + +private fun OptionalCache.retrieve(key: K, request: () -> V?): V? { + return get(key) { Optional.ofNullable(request()) }!!.orElse(null) +} + +@Suppress("UNCHECKED_CAST") +private fun OptionalCache.retrieveAll(keys: Collection, request: (Set) -> List): List { + val optionalResults = getAll(keys) { + val missingKeys = if (it is Set<*>) it as Set else it.toSet() + val response = request(missingKeys) + missingKeys.zip(response) { key, value -> key to Optional.ofNullable(value) }.toMap() + } + return keys.map { optionalResults.getValue(it).orElse(null) } +} diff --git a/verifier/src/main/kotlin/net/corda/verifier/ExternalVerifierNamedCacheFactory.kt b/verifier/src/main/kotlin/net/corda/verifier/ExternalVerifierNamedCacheFactory.kt new file mode 100644 index 0000000000..cc3887eab8 --- /dev/null +++ b/verifier/src/main/kotlin/net/corda/verifier/ExternalVerifierNamedCacheFactory.kt @@ -0,0 +1,35 @@ +package net.corda.verifier + +import com.github.benmanes.caffeine.cache.Cache +import com.github.benmanes.caffeine.cache.CacheLoader +import com.github.benmanes.caffeine.cache.Caffeine +import com.github.benmanes.caffeine.cache.LoadingCache +import net.corda.core.internal.NamedCacheFactory + +@Suppress("MagicNumber") +class ExternalVerifierNamedCacheFactory : NamedCacheFactory { + companion object { + private const val DEFAULT_CACHE_SIZE = 1024L + } + + override fun buildNamed(caffeine: Caffeine, name: String): Cache { + checkCacheName(name) + return configure(caffeine, name).build() + } + + override fun buildNamed(caffeine: Caffeine, name: String, loader: CacheLoader): LoadingCache { + checkCacheName(name) + return configure(caffeine, name).build(loader) + } + + private fun configure(caffeine: Caffeine, name: String): Caffeine { + return when (name) { + "AttachmentsClassLoader_cache" -> caffeine.maximumSize(32) + "ExternalVerifier_parties" -> caffeine.maximumSize(DEFAULT_CACHE_SIZE) + "ExternalVerifier_attachments" -> caffeine.maximumSize(DEFAULT_CACHE_SIZE) + "ExternalVerifier_networkParameters" -> caffeine.maximumSize(DEFAULT_CACHE_SIZE) + "ExternalVerifier_trustedClassAttachments" -> caffeine.maximumSize(DEFAULT_CACHE_SIZE) + else -> throw IllegalArgumentException("Unexpected cache name $name. Did you add a new cache?") + } + } +} diff --git a/verifier/src/main/kotlin/net/corda/verifier/Main.kt b/verifier/src/main/kotlin/net/corda/verifier/Main.kt new file mode 100644 index 0000000000..ba1269f63d --- /dev/null +++ b/verifier/src/main/kotlin/net/corda/verifier/Main.kt @@ -0,0 +1,46 @@ +package net.corda.verifier + +import net.corda.core.utilities.loggerFor +import org.slf4j.bridge.SLF4JBridgeHandler +import java.io.DataInputStream +import java.io.DataOutputStream +import java.net.Socket +import java.nio.file.Path +import kotlin.io.path.div +import kotlin.system.exitProcess + +@Suppress("TooGenericExceptionCaught") +object Main { + private val log = loggerFor
() + + @JvmStatic + fun main(args: Array) { + val port = args[0].toInt() + val loggingLevel = args[0] + val baseDirectory = Path.of("").toAbsolutePath() + + initLogging(baseDirectory, loggingLevel) + + log.info("External verifier started; PID ${ProcessHandle.current().pid()}") + log.info("Node base directory: $baseDirectory") + + try { + val socket = Socket("localhost", port) + log.info("Connected to node on port $port") + val fromNode = DataInputStream(socket.getInputStream()) + val toNode = DataOutputStream(socket.getOutputStream()) + ExternalVerifier(baseDirectory, fromNode, toNode).run() + } catch (t: Throwable) { + log.error("Unexpected error which has terminated the verifier", t) + exitProcess(1) + } + } + + private fun initLogging(baseDirectory: Path, loggingLevel: String) { + System.setProperty("logPath", (baseDirectory / "logs").toString()) + System.setProperty("defaultLogLevel", loggingLevel) + + SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler. + SLF4JBridgeHandler.install() + } +} diff --git a/verifier/src/main/resources/log4j2.xml b/verifier/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..684820a3df --- /dev/null +++ b/verifier/src/main/resources/log4j2.xml @@ -0,0 +1,44 @@ + + + + + ${sys:logPath:-logs} + verifier-${hostName} + ${log_path}/archive + ${sys:defaultLogLevel:-info} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +