From b3265314ce9f968f593fc6041ceca5b8bece5086 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Wed, 13 Mar 2024 10:36:12 +0000 Subject: [PATCH] ENT-11445: Support legacy contract CorDapp dependencies The `TransactionBuilder` has been updated to look for any missing dependencies to legacy contract attachments, in the same way it does for missing dependencies for CorDapps in the "cordapps" directory, Since `TransactionBuilder` does verification on the `WireTransaction` and not a `SignedTransaction`, much of the verification logic in `SignedTransaction` had to moved to `WireTransaction` to allow the external verifier to be involved. The external verifier receives a `CoreTransaction` to verify instead of a `SignedTransaction`. `SignedTransaction.verify` does the signature checks first in-process, before then delegating the reset of the verification to the `CoreTransaction`. A legacy contract dependency is defined as an attachment containing the missing class which isn't also a non-legacy Cordapp (i.e. a CorDapp which isn't in the "cordapp" directory). --- .ci/api-current.txt | 2 - core-tests/build.gradle | 63 ++-- .../TransactionBuilderDriverTest.kt | 159 +++++++++ .../verification/ExternalVerificationTests.kt | 17 +- .../TransactionBuilderMockNetworkTest.kt | 53 +-- .../core/contracts/AttachmentConstraint.kt | 10 +- .../net/corda/core/flows/FinalityFlow.kt | 2 +- .../corda/core/internal/AbstractAttachment.kt | 2 +- .../net/corda/core/internal/CordaUtils.kt | 3 + .../net/corda/core/internal/InternalUtils.kt | 4 +- .../corda/core/internal/TransactionUtils.kt | 15 +- .../verification/ExternalVerifierHandle.kt | 4 +- .../verification/NodeVerificationSupport.kt | 18 +- .../verification/VerificationResult.kt | 64 ++++ .../verification/VerificationSupport.kt | 2 +- .../core/node/services/AttachmentStorage.kt | 1 - .../core/transactions/BaseTransaction.kt | 10 +- .../ContractUpgradeTransactions.kt | 21 +- .../transactions/NotaryChangeTransactions.kt | 17 +- .../core/transactions/SignedTransaction.kt | 307 +++--------------- .../core/transactions/TransactionBuilder.kt | 136 +++++--- .../transactions/TransactionWithSignatures.kt | 5 +- .../core/transactions/WireTransaction.kt | 219 ++++++++++++- .../cordapp/JarScanningCordappLoader.kt | 4 +- .../ExternalVerifierHandleImpl.kt | 32 +- .../verifier/ExternalVerifierTypes.kt | 17 +- .../verifier/ExternalVerificationContext.kt | 4 +- .../net/corda/verifier/ExternalVerifier.kt | 36 +- 28 files changed, 740 insertions(+), 487 deletions(-) create mode 100644 core-tests/src/integration-test/kotlin/net/corda/coretests/transactions/TransactionBuilderDriverTest.kt create mode 100644 core/src/main/kotlin/net/corda/core/internal/verification/VerificationResult.kt diff --git a/.ci/api-current.txt b/.ci/api-current.txt index d4f9301255..705718ba32 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -552,8 +552,6 @@ public interface net.corda.core.contracts.Attachment extends net.corda.core.cont public interface net.corda.core.contracts.AttachmentConstraint public abstract boolean isSatisfiedBy(net.corda.core.contracts.Attachment) ## -public final class net.corda.core.contracts.AttachmentConstraintKt extends java.lang.Object -## @CordaSerializable public final class net.corda.core.contracts.AttachmentResolutionException extends net.corda.core.flows.FlowException public (net.corda.core.crypto.SecureHash) diff --git a/core-tests/build.gradle b/core-tests/build.gradle index 16347f88e2..4d0f767387 100644 --- a/core-tests/build.gradle +++ b/core-tests/build.gradle @@ -57,55 +57,61 @@ processSmokeTestResources { from(configurations.corda4_11) } +processIntegrationTestResources { + from(tasks.getByPath(":finance:contracts:jar")) { + rename 'corda-finance-contracts-.*.jar', 'corda-finance-contracts.jar' + } + from(configurations.corda4_11) +} + processTestResources { from(configurations.corda4_11) } dependencies { - testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}" - testImplementation "junit:junit:$junit_version" - testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${junit_vintage_version}" - testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junit_jupiter_version}" - testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}" - - testImplementation "commons-fileupload:commons-fileupload:$fileupload_version" testImplementation project(":core") - testImplementation project(path: ':core', configuration: 'testArtifacts') - + testImplementation project(":serialization") + testImplementation project(":finance:contracts") + testImplementation project(":finance:workflows") testImplementation project(":node") testImplementation project(":node-api") testImplementation project(":client:rpc") - testImplementation project(":serialization") testImplementation project(":common-configuration-parsing") - testImplementation project(":finance:contracts") - testImplementation project(":finance:workflows") testImplementation project(":core-test-utils") testImplementation project(":test-utils") - testImplementation project(path: ':core', configuration: 'testArtifacts') - + testImplementation project(":node-driver") + // used by FinalityFlowTests + testImplementation project(':testing:cordapps:cashobservers') + testImplementation(project(path: ':core', configuration: 'testArtifacts')) { + transitive = false + } + testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}" + testImplementation "junit:junit:$junit_version" + testImplementation "commons-fileupload:commons-fileupload:$fileupload_version" // Guava: Google test library (collections test suite) testImplementation "com.google.guava:guava-testlib:$guava_version" testImplementation "com.google.jimfs:jimfs:1.1" - testImplementation group: "com.typesafe", name: "config", version: typesafe_config_version - - // Bring in the MockNode infrastructure for writing protocol unit tests. - testImplementation project(":node-driver") - - implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + testImplementation "com.typesafe:config:$typesafe_config_version" testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" - // Hamkrest, for fluent, composable matchers testImplementation "com.natpryce:hamkrest:$hamkrest_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" - + testImplementation 'org.hamcrest:hamcrest-library:2.1' + testImplementation "org.mockito.kotlin:mockito-kotlin:$mockito_kotlin_version" + testImplementation "org.mockito:mockito-core:$mockito_version" // AssertJ: for fluent assertions for testing testImplementation "org.assertj:assertj-core:${assertj_version}" - // Guava: Google utilities library. testImplementation "com.google.guava:guava:$guava_version" + testImplementation "com.esotericsoftware:kryo:$kryo_version" + testImplementation "co.paralleluniverse:quasar-core:$quasar_version" + testImplementation "org.hibernate:hibernate-core:$hibernate_version" + testImplementation "org.bouncycastle:bcprov-jdk18on:${bouncycastle_version}" + testImplementation "io.netty:netty-common:$netty_version" + testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + + testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${junit_vintage_version}" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junit_jupiter_version}" + testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}" // Smoke tests do NOT have any Node code on the classpath! smokeTestImplementation project(":core") @@ -127,9 +133,6 @@ dependencies { smokeTestRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}" smokeTestRuntimeOnly "org.slf4j:slf4j-simple:$slf4j_version" - // used by FinalityFlowTests - testImplementation project(':testing:cordapps:cashobservers') - corda4_11 "net.corda:corda-finance-contracts:4.11" corda4_11 "net.corda:corda-finance-workflows:4.11" corda4_11 "net.corda:corda:4.11" diff --git a/core-tests/src/integration-test/kotlin/net/corda/coretests/transactions/TransactionBuilderDriverTest.kt b/core-tests/src/integration-test/kotlin/net/corda/coretests/transactions/TransactionBuilderDriverTest.kt new file mode 100644 index 0000000000..818b511919 --- /dev/null +++ b/core-tests/src/integration-test/kotlin/net/corda/coretests/transactions/TransactionBuilderDriverTest.kt @@ -0,0 +1,159 @@ +package net.corda.coretests.transactions + +import net.corda.core.internal.copyToDirectory +import net.corda.core.internal.hash +import net.corda.core.internal.toPath +import net.corda.core.messaging.startFlow +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow +import net.corda.coretesting.internal.delete +import net.corda.coretesting.internal.modifyJarManifest +import net.corda.coretesting.internal.useZipFile +import net.corda.finance.DOLLARS +import net.corda.finance.flows.CashIssueAndPaymentFlow +import net.corda.testing.common.internal.testNetworkParameters +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey +import net.corda.testing.core.internal.JarSignatureTestUtils.signJar +import net.corda.testing.core.internal.JarSignatureTestUtils.unsignJar +import net.corda.testing.driver.NodeHandle +import net.corda.testing.driver.NodeParameters +import net.corda.testing.node.internal.DriverDSLImpl +import net.corda.testing.node.internal.FINANCE_WORKFLOWS_CORDAPP +import net.corda.testing.node.internal.internalDriver +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.nio.file.Path +import kotlin.io.path.Path +import kotlin.io.path.absolutePathString +import kotlin.io.path.copyTo +import kotlin.io.path.createDirectories +import kotlin.io.path.deleteExisting +import kotlin.io.path.div +import kotlin.io.path.inputStream + +class TransactionBuilderDriverTest { + companion object { + val currentFinanceContractsJar = this::class.java.getResource("/corda-finance-contracts.jar")!!.toPath() + val legacyFinanceContractsJar = this::class.java.getResource("/corda-finance-contracts-4.11.jar")!!.toPath() + } + + @Rule + @JvmField + val tempFolder = TemporaryFolder() + + @Before + fun initJarSigner() { + tempFolder.root.toPath().generateKey("testAlias", "testPassword", ALICE_NAME.toString()) + } + + private fun signJar(jar: Path) { + tempFolder.root.toPath().signJar(jar.absolutePathString(), "testAlias", "testPassword") + } + + @Test(timeout=300_000) + fun `adds CorDapp dependencies`() { + val (cordapp, dependency) = splitFinanceContractCordapp(currentFinanceContractsJar) + internalDriver(cordappsForAllNodes = listOf(FINANCE_WORKFLOWS_CORDAPP), startNodesInProcess = false) { + cordapp.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment) + dependency.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment) + + // Start the node with the CorDapp but without the dependency + cordapp.copyToDirectory((baseDirectory(ALICE_NAME) / "cordapps").createDirectories()) + val node = startNode(NodeParameters(ALICE_NAME)).getOrThrow() + + // First make sure the missing dependency causes an issue + assertThatThrownBy { + createTransaction(node) + }.hasMessageContaining("java.lang.NoClassDefFoundError: net/corda/finance/contracts/asset") + + // Upload the missing dependency + dependency.inputStream().use(node.rpc::uploadAttachment) + + val stx = createTransaction(node) + assertThat(stx.tx.attachments).contains(cordapp.hash, dependency.hash) + } + } + + @Test(timeout=300_000) + fun `adds legacy contracts CorDapp dependencies`() { + val (legacyContracts, legacyDependency) = splitFinanceContractCordapp(legacyFinanceContractsJar) + + // Re-sign the current finance contracts CorDapp with the same key as the split legacy CorDapp + val currentContracts = currentFinanceContractsJar.copyTo(Path("${currentFinanceContractsJar.toString().substringBeforeLast(".")}-RESIGNED.jar"), overwrite = true) + currentContracts.unsignJar() + signJar(currentContracts) + + internalDriver( + cordappsForAllNodes = listOf(FINANCE_WORKFLOWS_CORDAPP), + startNodesInProcess = false, + networkParameters = testNetworkParameters(minimumPlatformVersion = 4) + ) { + currentContracts.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment) + + // Start the node with the legacy CorDapp but without the dependency + legacyContracts.copyToDirectory((baseDirectory(ALICE_NAME) / "legacy-contracts").createDirectories()) + currentContracts.copyToDirectory((baseDirectory(ALICE_NAME) / "cordapps").createDirectories()) + val node = startNode(NodeParameters(ALICE_NAME)).getOrThrow() + + // First make sure the missing dependency causes an issue + assertThatThrownBy { + createTransaction(node) + }.hasMessageContaining("java.lang.NoClassDefFoundError: net/corda/finance/contracts/asset") + + // Upload the missing dependency + legacyDependency.inputStream().use(node.rpc::uploadAttachment) + + val stx = createTransaction(node) + assertThat(stx.tx.legacyAttachments).contains(legacyContracts.hash, legacyDependency.hash) + } + } + + /** + * Split the given finance contracts jar into two such that the second jar becomes a dependency to the first. + */ + private fun splitFinanceContractCordapp(contractsJar: Path): Pair { + val cordapp = tempFolder.newFile("cordapp.jar").toPath() + val dependency = tempFolder.newFile("cordapp-dep.jar").toPath() + + // Split the CorDapp into two + contractsJar.copyTo(cordapp, overwrite = true) + cordapp.useZipFile { cordappZipFs -> + dependency.useZipFile { depZipFs -> + val targetDir = depZipFs.getPath("net/corda/finance/contracts/asset").createDirectories() + // CashUtilities happens to be a class that is only invoked in Cash.verify and so it's absence is only detected during + // verification + val clazz = cordappZipFs.getPath("net/corda/finance/contracts/asset/CashUtilities.class") + clazz.copyToDirectory(targetDir) + clazz.deleteExisting() + } + } + cordapp.modifyJarManifest { manifest -> + manifest.mainAttributes.delete("Sealed") + } + cordapp.unsignJar() + + // Sign both current and legacy CorDapps with the same key + signJar(cordapp) + // The dependency needs to be signed as it contains a package from the main jar + signJar(dependency) + + return Pair(cordapp, dependency) + } + + private fun DriverDSLImpl.createTransaction(node: NodeHandle): SignedTransaction { + return node.rpc.startFlow( + ::CashIssueAndPaymentFlow, + 1.DOLLARS, + OpaqueBytes.of(0x00), + defaultNotaryIdentity, + false, + defaultNotaryIdentity + ).returnValue.getOrThrow().stx + } +} diff --git a/core-tests/src/smoke-test/kotlin/net/corda/coretests/verification/ExternalVerificationTests.kt b/core-tests/src/smoke-test/kotlin/net/corda/coretests/verification/ExternalVerificationTests.kt index 415161f797..58b4f501e0 100644 --- a/core-tests/src/smoke-test/kotlin/net/corda/coretests/verification/ExternalVerificationTests.kt +++ b/core-tests/src/smoke-test/kotlin/net/corda/coretests/verification/ExternalVerificationTests.kt @@ -213,7 +213,7 @@ class ExternalVerificationUnsignedCordappsTest { ).returnValue.getOrThrow() } - assertThat(newNode.externalVerifierLogs()).contains("$issuanceTx failed to verify") + assertThat(newNode.externalVerifierLogs()).contains("WireTransaction(id=${issuanceTx.id}) failed to verify") } } @@ -265,10 +265,10 @@ private fun NodeProcess.assertTransactionsWereVerified(verificationType: Verific val nodeLogs = logs("node")!! val externalVerifierLogs = externalVerifierLogs() for (txId in txIds) { - assertThat(nodeLogs).contains("Transaction $txId has verification type $verificationType") + assertThat(nodeLogs).contains("WireTransaction(id=$txId) will be verified ${verificationType.logStatement}") if (verificationType != VerificationType.IN_PROCESS) { assertThat(externalVerifierLogs).describedAs("External verifier was not started").isNotNull() - assertThat(externalVerifierLogs).contains("SignedTransaction(id=$txId) verified") + assertThat(externalVerifierLogs).contains("WireTransaction(id=$txId) verified") } } } @@ -283,5 +283,12 @@ private fun NodeProcess.logs(name: String): String? { } private enum class VerificationType { - IN_PROCESS, EXTERNAL, BOTH -} \ No newline at end of file + IN_PROCESS, EXTERNAL, BOTH; + + val logStatement: String + get() = when (this) { + IN_PROCESS -> "in-process" + EXTERNAL -> "by the external verifer" + BOTH -> "both in-process and by the external verifer" + } +} diff --git a/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderMockNetworkTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderMockNetworkTest.kt index a8286298c1..b54fbc334d 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderMockNetworkTest.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderMockNetworkTest.kt @@ -8,17 +8,13 @@ import net.corda.core.internal.copyToDirectory import net.corda.core.internal.hash import net.corda.core.internal.toPath import net.corda.core.transactions.TransactionBuilder -import net.corda.coretesting.internal.useZipFile import net.corda.finance.DOLLARS import net.corda.finance.contracts.asset.Cash import net.corda.finance.issuedBy 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.DummyCommandData -import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey -import net.corda.testing.core.internal.JarSignatureTestUtils.signJar import net.corda.testing.core.internal.JarSignatureTestUtils.unsignJar import net.corda.testing.core.singleIdentity import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP @@ -28,18 +24,14 @@ import net.corda.testing.node.internal.cordappWithPackages import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatIllegalArgumentException import org.junit.After -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder import java.nio.file.Path -import kotlin.io.path.absolutePathString import kotlin.io.path.copyTo import kotlin.io.path.createDirectories -import kotlin.io.path.deleteExisting import kotlin.io.path.div import kotlin.io.path.inputStream -import kotlin.io.path.listDirectoryEntries @Suppress("INVISIBLE_MEMBER") class TransactionBuilderMockNetworkTest { @@ -107,9 +99,9 @@ class TransactionBuilderMockNetworkTest { @Test(timeout=300_000) fun `populates legacy attachment group if legacy contract CorDapp is present`() { - val node = mockNetwork.createNode { - it.copyToLegacyContracts(legacyFinanceContractsJar) - InternalMockNetwork.MockNode(it) + val node = mockNetwork.createNode { args -> + args.copyToLegacyContracts(legacyFinanceContractsJar) + InternalMockNetwork.MockNode(args) } val builder = TransactionBuilder() val identity = node.info.singleIdentity() @@ -120,45 +112,6 @@ class TransactionBuilderMockNetworkTest { stx.verify(node.services) } - @Test(timeout=300_000) - @Ignore // https://r3-cev.atlassian.net/browse/ENT-11445 - fun `adds legacy CorDapp dependencies`() { - val cordapp1 = tempFolder.newFile("cordapp1.jar").toPath() - val cordapp2 = tempFolder.newFile("cordapp2.jar").toPath() - // Split the contracts CorDapp into two - legacyFinanceContractsJar.copyTo(cordapp1, overwrite = true) - cordapp1.useZipFile { zipFs1 -> - cordapp2.useZipFile { zipFs2 -> - val destinationDir = zipFs2.getPath("net/corda/finance/contracts/asset").createDirectories() - zipFs1.getPath("net/corda/finance/contracts/asset") - .listDirectoryEntries("OnLedgerAsset*") - .forEach { - it.copyToDirectory(destinationDir) - it.deleteExisting() - } - } - } - reSignJar(cordapp1) - - val node = mockNetwork.createNode { - it.copyToLegacyContracts(cordapp1, cordapp2) - InternalMockNetwork.MockNode(it) - } - val builder = TransactionBuilder() - val identity = node.info.singleIdentity() - Cash().generateIssue(builder, 10.DOLLARS.issuedBy(identity.ref(0x00)), identity, mockNetwork.defaultNotaryIdentity) - val stx = node.services.signInitialTransaction(builder) - assertThat(stx.tx.nonLegacyAttachments).contains(FINANCE_CONTRACTS_CORDAPP.jarFile.hash) - assertThat(stx.tx.legacyAttachments).contains(cordapp1.hash, cordapp2.hash) - stx.verify(node.services) - } - - private fun reSignJar(jar: Path) { - jar.unsignJar() - tempFolder.root.toPath().generateKey("testAlias", "testPassword", ALICE_NAME.toString()) - tempFolder.root.toPath().signJar(jar.absolutePathString(), "testAlias", "testPassword") - } - private fun MockNodeArgs.copyToLegacyContracts(vararg jars: Path) { val legacyContractsDir = (config.baseDirectory / "legacy-contracts").createDirectories() jars.forEach { it.copyToDirectory(legacyContractsDir) } diff --git a/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt b/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt index f989f1f2fa..1e3593d28d 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt @@ -11,13 +11,12 @@ import net.corda.core.internal.utilities.Internable import net.corda.core.internal.utilities.PrivateInterner import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor import java.lang.annotation.Inherited import java.security.PublicKey -private val log = loggerFor() - /** * This annotation should only be added to [Contract] classes. * If the annotation is present, then we assume that [Contract.verify] will ensure that the output states have an acceptable constraint. @@ -49,8 +48,11 @@ object AlwaysAcceptAttachmentConstraint : AttachmentConstraint { */ data class HashAttachmentConstraint(val attachmentId: SecureHash) : AttachmentConstraint { companion object { + private val log = contextLogger() + val disableHashConstraints = System.getProperty("net.corda.node.disableHashConstraints")?.toBoolean() ?: false } + override fun isSatisfiedBy(attachment: Attachment): Boolean { return if (attachment is AttachmentWithContext) { log.debug("Checking attachment uploader ${attachment.contractAttachment.uploader} is trusted") @@ -68,6 +70,8 @@ data class HashAttachmentConstraint(val attachmentId: SecureHash) : AttachmentCo * It allows for centralized control over the cordapps that can be used. */ object WhitelistedByZoneAttachmentConstraint : AttachmentConstraint { + private val log = loggerFor() + override fun isSatisfiedBy(attachment: Attachment): Boolean { return if (attachment is AttachmentWithContext) { val whitelist = attachment.whitelistedContractImplementations @@ -120,6 +124,8 @@ data class SignatureAttachmentConstraint(val key: PublicKey) : AttachmentConstra } companion object : Internable { + private val log = contextLogger() + @CordaInternal override val interner = PrivateInterner() diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt index aa7837f651..2b0afbb95a 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt @@ -450,7 +450,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, // The notary signature(s) are allowed to be missing but no others. if (notary != null) transaction.verifySignaturesExcept(notary.owningKey) else transaction.verifyRequiredSignatures() // TODO= [CORDA-3267] Remove duplicate signature verification - val ltx = transaction.verifyInternal(serviceHub.toVerifyingServiceHub(), checkSufficientSignatures = false) as LedgerTransaction? + val ltx = transaction.verifyInternal(serviceHub.toVerifyingServiceHub(), checkSufficientSignatures = false) // verifyInternal returns null if the transaction was verified externally, which *could* happen on a very odd scenerio of a 4.11 // node creating the transaction but a 4.12 kicking off finality. In that case, we still want a LedgerTransaction object for // recording to the vault, etc. Note that calling verify() on this will fail as it doesn't have the necessary non-legacy attachments diff --git a/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt b/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt index 2c2b332fcd..a07d9b1f7a 100644 --- a/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt +++ b/core/src/main/kotlin/net/corda/core/internal/AbstractAttachment.kt @@ -68,7 +68,7 @@ abstract class AbstractAttachment(dataLoader: () -> ByteArray, val uploader: Str override fun equals(other: Any?) = other === this || other is Attachment && other.id == this.id override fun hashCode() = id.hashCode() - override fun toString() = "${javaClass.simpleName}(id=$id)" + override fun toString() = toSimpleString() } @Throws(IOException::class) 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 070ad4b6ac..e5cef3fca2 100644 --- a/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt @@ -2,6 +2,7 @@ package net.corda.core.internal import net.corda.core.contracts.ContractClassName +import net.corda.core.contracts.NamedByHash import net.corda.core.contracts.TransactionResolutionException import net.corda.core.crypto.SecureHash import net.corda.core.flows.DataVendingFlow @@ -93,3 +94,5 @@ fun TransactionStorage.getRequiredTransaction(txhash: SecureHash): SignedTransac } fun ServiceHub.getRequiredTransaction(txhash: SecureHash): SignedTransaction = validatedTransactions.getRequiredTransaction(txhash) + +fun NamedByHash.toSimpleString(): String = "${javaClass.simpleName}(id=$id)" 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 105b55616e..7a04c02a16 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -148,8 +148,8 @@ fun List.indexOfOrThrow(item: T): Int { @Suppress("INVISIBLE_MEMBER", "RemoveExplicitTypeArguments") // Because the external verifier uses Kotlin 1.2 inline fun Collection.mapToSet(transform: (T) -> R): Set { return when (size) { - 0 -> return emptySet() - 1 -> return setOf(transform(first())) + 0 -> emptySet() + 1 -> setOf(transform(first())) else -> mapTo(LinkedHashSet(mapCapacity(size)), transform) } } 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 3d3056da5f..900b541ba9 100644 --- a/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt @@ -166,8 +166,10 @@ fun deserialiseCommands( } val componentHashes = group.components.mapIndexed { index, component -> digestService.componentHash(group.nonces[index], component) } val leafIndices = componentHashes.map { group.partialMerkleTree.leafIndex(it) } - if (leafIndices.isNotEmpty()) + if (leafIndices.isNotEmpty()) { + @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") // Because the external verifier uses Kotlin 1.2 check(leafIndices.max()!! < signersList.size) { "Invalid Transaction. A command with no corresponding signer detected" } + } commandDataList.lazyMapped { commandData, index -> Command(commandData, signersList[leafIndices[index]]) } } else { // It is a WireTransaction @@ -313,3 +315,14 @@ internal fun checkNotaryWhitelisted(ftx: FullTransaction) { } } } + +fun getRequiredSigningKeysInternal(inputs: Sequence>, notary: Party?): Set { + val keys = LinkedHashSet() + for (input in inputs) { + input.state.data.participants.mapTo(keys) { it.owningKey } + } + if (notary != null) { + keys += notary.owningKey + } + return keys +} diff --git a/core/src/main/kotlin/net/corda/core/internal/verification/ExternalVerifierHandle.kt b/core/src/main/kotlin/net/corda/core/internal/verification/ExternalVerifierHandle.kt index e9f1e92e88..5563193024 100644 --- a/core/src/main/kotlin/net/corda/core/internal/verification/ExternalVerifierHandle.kt +++ b/core/src/main/kotlin/net/corda/core/internal/verification/ExternalVerifierHandle.kt @@ -1,7 +1,7 @@ package net.corda.core.internal.verification -import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.CoreTransaction interface ExternalVerifierHandle : AutoCloseable { - fun verifyTransaction(stx: SignedTransaction, checkSufficientSignatures: Boolean) + fun verifyTransaction(ctx: CoreTransaction) } diff --git a/core/src/main/kotlin/net/corda/core/internal/verification/NodeVerificationSupport.kt b/core/src/main/kotlin/net/corda/core/internal/verification/NodeVerificationSupport.kt index a7b400ccc5..6a8a5ffbc5 100644 --- a/core/src/main/kotlin/net/corda/core/internal/verification/NodeVerificationSupport.kt +++ b/core/src/main/kotlin/net/corda/core/internal/verification/NodeVerificationSupport.kt @@ -128,25 +128,17 @@ interface NodeVerificationSupport : VerificationSupport { /** * 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. + * @return attachments containing the given class in descending version order. This means any legacy attachments will occur after the + * current version one. */ - // TODO Should throw when the class is found in multiple contract attachments (not different versions). - override fun getTrustedClassAttachment(className: String): Attachment? { + override fun getTrustedClassAttachments(className: String): List { val allTrusted = attachments.queryAttachments( AttachmentsQueryCriteria().withUploader(Builder.`in`(TRUSTED_UPLOADERS)), - // JarScanningCordappLoader makes sure legacy contract CorDapps have a coresponding non-legacy CorDapp, and that the - // legacy CorDapp has a smaller version number. Thus sorting by the version here ensures we never return the legacy attachment. AttachmentSort(listOf(AttachmentSortColumn(AttachmentSortAttribute.VERSION, Sort.Direction.DESC))) ) - - // TODO - add caching if performance is affected. - for (attId in allTrusted) { - val attch = attachments.openAttachment(attId)!! - if (attch.hasFile("$className.class")) return attch - } - return null + val fileName = "$className.class" + return allTrusted.mapNotNull { id -> attachments.openAttachment(id)!!.takeIf { it.hasFile(fileName) } } } private fun Attachment.hasFile(className: String): Boolean = openAsJAR().use { it.entries().any { entry -> entry.name == className } } diff --git a/core/src/main/kotlin/net/corda/core/internal/verification/VerificationResult.kt b/core/src/main/kotlin/net/corda/core/internal/verification/VerificationResult.kt new file mode 100644 index 0000000000..a316e12375 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/verification/VerificationResult.kt @@ -0,0 +1,64 @@ +package net.corda.core.internal.verification + +import net.corda.core.transactions.LedgerTransaction +import net.corda.core.utilities.Try +import net.corda.core.utilities.Try.Failure +import net.corda.core.utilities.Try.Success + +sealed class VerificationResult { + /** + * The in-process result for the current version of the transcaction. + */ + abstract val inProcessResult: Try? + + /** + * The external verifier result for the legacy version of the transaction. + */ + abstract val externalResult: Try? + + abstract fun enforceSuccess(): LedgerTransaction? + + + data class InProcess(override val inProcessResult: Try) : VerificationResult() { + override val externalResult: Try? + get() = null + + override fun enforceSuccess(): LedgerTransaction? = inProcessResult.getOrThrow() + } + + data class External(override val externalResult: Try) : VerificationResult() { + override val inProcessResult: Try? + get() = null + + override fun enforceSuccess(): LedgerTransaction? { + externalResult.getOrThrow() + // We could create a LedgerTransaction here, and except for calling `verify()`, it would be valid to use. However, it's best + // we let the caller deal with that, since we can't prevent them from calling it. + return null + } + } + + data class InProcessAndExternal( + override val inProcessResult: Try, + override val externalResult: Try + ) : VerificationResult() { + override fun enforceSuccess(): LedgerTransaction { + return when (externalResult) { + is Success -> when (inProcessResult) { + is Success -> inProcessResult.value + is Failure -> throw IllegalStateException( + "Current version of transaction failed to verify, but legacy version did verify (in external verifier)", + inProcessResult.exception + ) + } + is Failure -> throw when (inProcessResult) { + is Success -> IllegalStateException( + "Current version of transaction verified, but legacy version failed to verify (in external verifier)", + externalResult.exception + ) + is Failure -> inProcessResult.exception.apply { addSuppressed(externalResult.exception) } + } + } + } + } +} 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 index 98835a3350..401b8135f4 100644 --- a/core/src/main/kotlin/net/corda/core/internal/verification/VerificationSupport.kt +++ b/core/src/main/kotlin/net/corda/core/internal/verification/VerificationSupport.kt @@ -34,7 +34,7 @@ interface VerificationSupport { fun isAttachmentTrusted(attachment: Attachment): Boolean - fun getTrustedClassAttachment(className: String): Attachment? + fun getTrustedClassAttachments(className: String): List fun getNetworkParameters(id: SecureHash?): NetworkParameters? diff --git a/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt b/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt index e39de6f7b7..20b29e3b1e 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt @@ -1,4 +1,3 @@ - package net.corda.core.node.services import net.corda.core.DoNotImplement diff --git a/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt index 23b986b721..9c17aa7696 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt @@ -1,16 +1,22 @@ package net.corda.core.transactions import net.corda.core.DoNotImplement -import net.corda.core.contracts.* +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.NamedByHash +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionState import net.corda.core.identity.Party import net.corda.core.internal.castIfPossible import net.corda.core.internal.indexOfOrThrow +import net.corda.core.internal.toSimpleString import net.corda.core.internal.uncheckedCast import java.util.function.Predicate /** * An abstract class defining fields shared by all transaction types in the system. */ +@Suppress("RedundantSamConstructor") // Because the external verifier uses Kotlin 1.2 @DoNotImplement abstract class BaseTransaction : NamedByHash { /** A list of reusable reference data states which can be referred to by other contracts in this transaction. */ @@ -163,5 +169,5 @@ abstract class BaseTransaction : NamedByHash { return findOutRef(T::class.java, Predicate { predicate(it) }) } - override fun toString(): String = "${javaClass.simpleName}(id=$id)" + override fun toString(): String = toSimpleString() } \ 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 74fe0bcbb7..e2809a51fe 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt @@ -22,8 +22,10 @@ import net.corda.core.crypto.TransactionSignature import net.corda.core.identity.Party import net.corda.core.internal.AttachmentWithContext import net.corda.core.internal.combinedHash +import net.corda.core.internal.getRequiredSigningKeysInternal import net.corda.core.internal.loadClassOfType -import net.corda.core.internal.mapToSet +import net.corda.core.internal.verification.NodeVerificationSupport +import net.corda.core.internal.verification.VerificationResult import net.corda.core.internal.verification.VerificationSupport import net.corda.core.internal.verification.toVerifyingServiceHub import net.corda.core.node.NetworkParameters @@ -40,6 +42,7 @@ import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.PARA 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.Try import net.corda.core.utilities.toBase58String import java.security.PublicKey @@ -159,6 +162,20 @@ data class ContractUpgradeWireTransaction( return ContractUpgradeFilteredTransaction(visibleComponents, hiddenComponents, digestService) } + @CordaInternal + @JvmSynthetic + internal fun tryVerify(verificationSupport: NodeVerificationSupport): VerificationResult.External { + // Contract upgrades only work on 4.11 and earlier + return VerificationResult.External(Try.on { verificationSupport.externalVerifierHandle.verifyTransaction(this) }) + } + + @CordaInternal + @JvmSynthetic + internal fun verifyInProcess(verificationSupport: VerificationSupport) { + // No contract code is run when verifying contract upgrade transactions, it is sufficient to check invariants during initialisation. + ContractUpgradeLedgerTransaction.resolve(verificationSupport, this, emptyList()) + } + enum class Component { INPUTS, NOTARY, LEGACY_ATTACHMENT, UPGRADED_CONTRACT, UPGRADED_ATTACHMENT, PARAMETERS_HASH } @@ -344,7 +361,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 }.mapToSet { it.owningKey } + notary.owningKey + get() = getRequiredSigningKeysInternal(inputs.asSequence(), notary) override fun getKeyDescriptions(keys: Set): List { return keys.map { it.toBase58String() } 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 1594d03a62..7e18999049 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt @@ -11,8 +11,10 @@ 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.getRequiredSigningKeysInternal import net.corda.core.internal.indexOfOrThrow -import net.corda.core.internal.mapToSet +import net.corda.core.internal.verification.NodeVerificationSupport +import net.corda.core.internal.verification.VerificationResult import net.corda.core.internal.verification.VerificationSupport import net.corda.core.internal.verification.toVerifyingServiceHub import net.corda.core.node.NetworkParameters @@ -27,6 +29,7 @@ import net.corda.core.transactions.NotaryChangeWireTransaction.Component.NEW_NOT 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.Try import net.corda.core.utilities.toBase58String import java.security.PublicKey @@ -107,6 +110,16 @@ data class NotaryChangeWireTransaction( return resolve(services as ServicesForResolution, sigs) } + @CordaInternal + @JvmSynthetic + internal fun tryVerify(verificationSupport: NodeVerificationSupport): VerificationResult.InProcess { + return VerificationResult.InProcess(Try.on { + // No contract code is run when verifying notary change transactions, it is sufficient to check invariants during initialisation. + NotaryChangeLedgerTransaction.resolve(verificationSupport, this, emptyList()) + null + }) + } + enum class Component { INPUTS, NOTARY, NEW_NOTARY, PARAMETERS_HASH } @@ -180,7 +193,7 @@ private constructor( get() = inputs.map { computeOutput(it, newNotary) { inputs.map(StateAndRef::ref) } } override val requiredSigningKeys: Set - get() = inputs.flatMap { it.state.data.participants }.mapToSet { it.owningKey } + notary.owningKey + get() = getRequiredSigningKeysInternal(inputs.asSequence(), notary) 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 91651ac006..b15875d4e2 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -3,12 +3,12 @@ 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.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.contracts.TransactionVerificationException.TransactionNetworkParameterOrderingException import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignatureMetadata @@ -16,34 +16,26 @@ 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.VisibleForTesting -import net.corda.core.internal.equivalent -import net.corda.core.internal.isUploaderTrusted +import net.corda.core.internal.getRequiredSigningKeysInternal +import net.corda.core.internal.toSimpleString import net.corda.core.internal.verification.NodeVerificationSupport -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.Try -import net.corda.core.utilities.Try.Failure -import net.corda.core.utilities.Try.Success -import net.corda.core.utilities.contextLogger -import net.corda.core.utilities.debug -import java.io.NotSerializableException +import net.corda.core.utilities.toBase58String import java.security.KeyPair import java.security.PublicKey import java.security.SignatureException import java.util.function.Predicate /** - * SignedTransaction wraps a serialized WireTransaction. It contains one or more signatures, each one for + * SignedTransaction wraps a serialized [CoreTransaction], though it will almost exclusively be a [WireTransaction]. + * It contains one or more signatures, each one for * a public key (including composite keys) that is mentioned inside a transaction command. SignedTransaction is the top level transaction type * and the type most frequently passed around the network and stored. The identity of a transaction is the hash of Merkle root * of a WireTransaction, therefore if you are storing data keyed by WT hash be aware that multiple different STs may @@ -163,12 +155,6 @@ data class SignedTransaction(val txBits: SerializedBytes, val verifyingServiceHub = services.toVerifyingServiceHub() // We need parameters check here, because finality flow calls stx.toLedgerTransaction() and then verify. resolveAndCheckNetworkParameters(verifyingServiceHub) - return toLedgerTransactionInternal(verifyingServiceHub, checkSufficientSignatures) - } - - @JvmSynthetic - @CordaInternal - 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. @@ -176,12 +162,8 @@ data class SignedTransaction(val txBits: SerializedBytes, // For the above to work, [checkSignaturesAreValid] should take the [requiredSigningKeys] as input // and probably combine logic from signature validation and key-fulfilment // in [TransactionWithSignatures.verifySignaturesExcept]. - if (checkSufficientSignatures) { - verifyRequiredSignatures() // It internally invokes checkSignaturesAreValid(). - } else { - checkSignaturesAreValid() - } - return tx.toLedgerTransactionInternal(verificationSupport) + verifySignatures(verifyingServiceHub, checkSufficientSignatures) + return tx.toLedgerTransactionInternal(verifyingServiceHub) } /** @@ -210,252 +192,50 @@ data class SignedTransaction(val txBits: SerializedBytes, */ @CordaInternal @JvmSynthetic - internal fun verifyInternal(verificationSupport: NodeVerificationSupport, checkSufficientSignatures: Boolean = true): FullTransaction? { + internal fun verifyInternal(verificationSupport: NodeVerificationSupport, checkSufficientSignatures: Boolean = true): LedgerTransaction? { resolveAndCheckNetworkParameters(verificationSupport) - val verificationType = determineVerificationType() - log.debug { "Transaction $id has verification type $verificationType" } - return when (verificationType) { - VerificationType.IN_PROCESS -> verifyInProcess(verificationSupport, checkSufficientSignatures) - VerificationType.BOTH -> { - val inProcessResult = Try.on { verifyInProcess(verificationSupport, checkSufficientSignatures) } - val externalResult = Try.on { verificationSupport.externalVerifierHandle.verifyTransaction(this, checkSufficientSignatures) } - ensureSameResult(inProcessResult, externalResult) - } - VerificationType.EXTERNAL -> { - verificationSupport.externalVerifierHandle.verifyTransaction(this, checkSufficientSignatures) - // We could create a LedgerTransaction here, and except for calling `verify()`, it would be valid to use. However, it's best - // we let the caller deal with that, since we can't control what they will do with it. - null - } - } - } - - private fun determineVerificationType(): VerificationType { + verifySignatures(verificationSupport, checkSufficientSignatures) val ctx = coreTransaction - return when (ctx) { - is WireTransaction -> { - when { - ctx.legacyAttachments.isEmpty() -> VerificationType.IN_PROCESS - ctx.nonLegacyAttachments.isEmpty() -> VerificationType.EXTERNAL - else -> VerificationType.BOTH - } - } - // Contract upgrades only work on 4.11 and earlier - is ContractUpgradeWireTransaction -> VerificationType.EXTERNAL - else -> VerificationType.IN_PROCESS // The default is always in-process - } - } - - private fun ensureSameResult(inProcessResult: Try, externalResult: Try<*>): FullTransaction { - return when (externalResult) { - is Success -> when (inProcessResult) { - is Success -> inProcessResult.value - is Failure -> throw IllegalStateException("In-process verification of $id failed, but it succeeded in external verifier") - .apply { addSuppressed(inProcessResult.exception) } - } - is Failure -> throw when (inProcessResult) { - is Success -> IllegalStateException("In-process verification of $id succeeded, but it failed in external verifier") - is Failure -> inProcessResult.exception // Throw the in-process exception, with the external exception suppressed - }.apply { addSuppressed(externalResult.exception) } - } - } - - private enum class VerificationType { - IN_PROCESS, EXTERNAL, BOTH - } - - /** - * Verifies this transaction in-process. This assumes the current process has the correct classpath for all the contracts. - * - * @return The [FullTransaction] that was successfully verified - */ - @CordaInternal - @JvmSynthetic - internal fun verifyInProcess(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean): FullTransaction { - return when (coreTransaction) { - is NotaryChangeWireTransaction -> verifyNotaryChangeTransaction(verificationSupport, checkSufficientSignatures) - is ContractUpgradeWireTransaction -> verifyContractUpgradeTransaction(verificationSupport, checkSufficientSignatures) - else -> verifyRegularTransaction(verificationSupport, checkSufficientSignatures) + val verificationResult = when (ctx) { + // 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. + is WireTransaction -> ctx.tryVerify(verificationSupport) + is ContractUpgradeWireTransaction -> ctx.tryVerify(verificationSupport) + is NotaryChangeWireTransaction -> ctx.tryVerify(verificationSupport) + else -> throw IllegalStateException("${ctx.toSimpleString()} cannot be verified") } + return verificationResult.enforceSuccess() } @Suppress("ThrowsCount") private fun resolveAndCheckNetworkParameters(services: NodeVerificationSupport) { val hashOrDefault = networkParametersHash ?: services.networkParametersService.defaultHash - val txNetworkParameters = services.networkParametersService.lookup(hashOrDefault) - ?: throw TransactionResolutionException(id) + val txNetworkParameters = services.networkParametersService.lookup(hashOrDefault) ?: throw TransactionResolutionException(id) val groupedInputsAndRefs = (inputs + references).groupBy { it.txhash } - groupedInputsAndRefs.map { entry -> - val tx = services.validatedTransactions.getTransaction(entry.key)?.coreTransaction - ?: throw TransactionResolutionException(id) + for ((txId, stateRefs) in groupedInputsAndRefs) { + val tx = services.validatedTransactions.getTransaction(txId)?.coreTransaction ?: throw TransactionResolutionException(id) val paramHash = tx.networkParametersHash ?: services.networkParametersService.defaultHash val params = services.networkParametersService.lookup(paramHash) ?: throw TransactionResolutionException(id) - if (txNetworkParameters.epoch < params.epoch) - throw TransactionVerificationException.TransactionNetworkParameterOrderingException(id, entry.value.first(), txNetworkParameters, params) - } - } - - /** No contract code is run when verifying notary change transactions, it is sufficient to check invariants during initialisation. */ - private fun verifyNotaryChangeTransaction(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean): NotaryChangeLedgerTransaction { - val ntx = NotaryChangeLedgerTransaction.resolve(verificationSupport, coreTransaction as NotaryChangeWireTransaction, sigs) - if (checkSufficientSignatures) ntx.verifyRequiredSignatures() - else checkSignaturesAreValid() - return ntx - } - - /** No contract code is run when verifying contract upgrade transactions, it is sufficient to check invariants during initialisation. */ - private fun verifyContractUpgradeTransaction(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean): ContractUpgradeLedgerTransaction { - val ctx = ContractUpgradeLedgerTransaction.resolve(verificationSupport, coreTransaction as ContractUpgradeWireTransaction, sigs) - if (checkSufficientSignatures) ctx.verifyRequiredSignatures() - else checkSignaturesAreValid() - return ctx - } - - // 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(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean): LedgerTransaction { - val ltx = toLedgerTransactionInternal(verificationSupport, checkSufficientSignatures) - try { - ltx.verify() - } catch (e: NoClassDefFoundError) { - checkReverifyAllowed(e) - val missingClass = e.message ?: throw e - log.warn("Transaction {} has missing class: {}", ltx.id, missingClass) - reverifyWithFixups(ltx, verificationSupport, missingClass) - } catch (e: NotSerializableException) { - checkReverifyAllowed(e) - retryVerification(e, e, ltx, verificationSupport) - } catch (e: TransactionDeserialisationException) { - checkReverifyAllowed(e) - retryVerification(e.cause, e, ltx, verificationSupport) - } - return ltx - } - - private fun checkReverifyAllowed(ex: Throwable) { - // If that transaction was created with and after Corda 4 then just fail. - // The lenient dependency verification is only supported for Corda 3 transactions. - // To detect if the transaction was created before Corda 4 we check if the transaction has the NetworkParameters component group. - if (networkParametersHash != null) { - log.warn("TRANSACTION VERIFY FAILED - No attempt to auto-repair as TX is Corda 4+") - throw ex - } - } - - @Suppress("ThrowsCount") - 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, verificationSupport, null) + if (txNetworkParameters.epoch < params.epoch) { + throw TransactionNetworkParameterOrderingException(id, stateRefs.first(), txNetworkParameters, params) } - 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, verificationSupport, missingClass) - } else { - throw ex - } - } - else -> throw ex } } - // 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, 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()) - - 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" - ) + private fun verifySignatures(verificationSupport: NodeVerificationSupport, checkSufficientSignatures: Boolean) { + if (checkSufficientSignatures) { + val ctx = coreTransaction + val tws: TransactionWithSignatures = when (ctx) { + is WireTransaction -> this // SignedTransaction implements TransactionWithSignatures in terms of WireTransaction + else -> CoreTransactionWithSignatures(ctx, sigs, verificationSupport) } - - 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() - ) + tws.verifyRequiredSignatures() // Internally checkSignaturesAreValid is invoked + } else { + checkSignaturesAreValid() } } - /** - * 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 - } - /** * Resolves the underlying base transaction and then returns it, handling any special case transactions such as * [NotaryChangeWireTransaction]. @@ -512,7 +292,7 @@ data class SignedTransaction(val txBits: SerializedBytes, return ctx.resolve(services, sigs) } - override fun toString(): String = "${javaClass.simpleName}(id=$id)" + override fun toString(): String = toSimpleString() private companion object { private fun missingSignatureMsg(missing: Set, descriptions: List, id: SecureHash): String { @@ -520,13 +300,28 @@ data class SignedTransaction(val txBits: SerializedBytes, "keys: ${missing.joinToString { it.toStringShort() }}, " + "by signers: ${descriptions.joinToString()} " } - - private val log = contextLogger() } class SignaturesMissingException(val missing: Set, val descriptions: List, override val id: SecureHash) : NamedByHash, SignatureException(missingSignatureMsg(missing, descriptions, id)), CordaThrowable by CordaException(missingSignatureMsg(missing, descriptions, id)) + /** + * A [TransactionWithSignatures] wrapper for [CoreTransaction]s which need to resolve their input states in order to get at the signers + * list. + */ + private data class CoreTransactionWithSignatures( + private val ctx: CoreTransaction, + override val sigs: List, + private val verificationSupport: NodeVerificationSupport + ) : TransactionWithSignatures, NamedByHash by ctx { + override val requiredSigningKeys: Set + get() = getRequiredSigningKeysInternal(ctx.inputs.asSequence().map(verificationSupport::getStateAndRef), ctx.notary) + + override fun getKeyDescriptions(keys: Set): List = keys.map { it.toBase58String() } + + override fun toString(): String = toSimpleString() + } + //region Deprecated /** Returns the contained [NotaryChangeWireTransaction], or throws if this is a normal transaction. */ @Deprecated("No replacement, this should not be used outside of Corda core") 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 9db53fc49d..b86fd6b0ee 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -25,6 +25,7 @@ import net.corda.core.serialization.SerializationFactory import net.corda.core.serialization.SerializationMagic import net.corda.core.serialization.SerializationSchemeContext import net.corda.core.serialization.internal.CustomSerializationSchemeUtils.Companion.getCustomSerializationMagicFromSchemeId +import net.corda.core.utilities.Try.Failure import net.corda.core.utilities.contextLogger import java.security.PublicKey import java.time.Duration @@ -89,6 +90,7 @@ open class TransactionBuilder( private val inputsWithTransactionState = arrayListOf>() private val referencesWithTransactionState = arrayListOf>() private var excludedAttachments: Set = emptySet() + private var extraLegacyAttachments: MutableSet? = null /** * Creates a copy of the builder. @@ -196,20 +198,26 @@ open class TransactionBuilder( val wireTx = SerializationFactory.defaultFactory.withCurrentContext(serializationContext) { // Sort the attachments to ensure transaction builds are stable. - val attachmentsBuilder = allContractAttachments.mapTo(TreeSet()) { it.currentAttachment.id } - attachmentsBuilder.addAll(attachments) - attachmentsBuilder.removeAll(excludedAttachments) + val nonLegacyAttachments = allContractAttachments.mapTo(TreeSet()) { it.currentAttachment.id }.apply { + addAll(attachments) + removeAll(excludedAttachments) + }.toList() + val legacyAttachments = allContractAttachments.mapNotNullTo(TreeSet()) { it.legacyAttachment?.id }.apply { + if (extraLegacyAttachments != null) { + addAll(extraLegacyAttachments!!) + } + }.toList() WireTransaction( createComponentGroups( inputStates(), resolvedOutputs, commands(), - attachmentsBuilder.toList(), + nonLegacyAttachments, notary, window, referenceStates, serviceHub.networkParametersService.currentHash, - allContractAttachments.mapNotNullTo(TreeSet()) { it.legacyAttachment?.id }.toList() + legacyAttachments ), privacySalt, serviceHub.digestService @@ -229,59 +237,71 @@ open class TransactionBuilder( } } - // Returns the first exception in the hierarchy that matches one of the [types]. - private tailrec fun Throwable.rootClassNotFoundCause(vararg types: KClass<*>): Throwable = when { - this::class in types -> this - this.cause == null -> this - else -> this.cause!!.rootClassNotFoundCause(*types) - } - /** * @return true if a new dependency was successfully added. */ - // TODO This entire code path needs to be updated to work with legacy attachments and automically adding their dependencies. ENT-11445 private fun addMissingDependency(serviceHub: VerifyingServiceHub, wireTx: WireTransaction, tryCount: Int): Boolean { - return try { - wireTx.toLedgerTransactionInternal(serviceHub).verify() - // The transaction verified successfully without adding any extra dependency. - false - } catch (e: Throwable) { - val rootError = e.rootClassNotFoundCause(ClassNotFoundException::class, NoClassDefFoundError::class) + val verificationResult = wireTx.tryVerify(serviceHub) + // Check both legacy and non-legacy components are working, and try to add any missing dependencies if either are not. + (verificationResult.inProcessResult as? Failure)?.let { (inProcessException) -> + return addMissingDependency(inProcessException, wireTx, false, serviceHub, tryCount) + } + (verificationResult.externalResult as? Failure)?.let { (externalException) -> + return addMissingDependency(externalException, wireTx, true, serviceHub, tryCount) + } + // The transaction verified successfully without needing any extra dependency. + return false + } - when { - // 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 -> { - // Using nonLegacyAttachments here as the verification above was done in-process and thus only the nonLegacyAttachments - // are used. - // TODO This might change with ENT-11445 where we add support for legacy contract dependencies. - ((tryCount == 0) && fixupAttachments(wireTx.nonLegacyAttachments, serviceHub, e)) - || addMissingAttachment((rootError.message ?: throw e).replace('.', '/'), serviceHub, e) - } - rootError is NoClassDefFoundError -> { - ((tryCount == 0) && fixupAttachments(wireTx.nonLegacyAttachments, serviceHub, e)) - || addMissingAttachment(rootError.message ?: throw e, serviceHub, e) - } - - // Ignore these exceptions as they will break unit tests. - // The point here is only to detect missing dependencies. The other exceptions are irrelevant. - e is TransactionVerificationException -> false - e is TransactionResolutionException -> false - e is IllegalStateException -> false - e is IllegalArgumentException -> false - - // Fail early if none of the expected scenarios were hit. - else -> { - log.error("""The transaction currently built will not validate because of an unknown error most likely caused by a - missing dependency in the transaction attachments. - Please contact the developer of the CorDapp for further instructions. - """.trimIndent(), e) - throw e - } + private fun addMissingDependency(e: Throwable, wireTx: WireTransaction, isLegacy: Boolean, serviceHub: VerifyingServiceHub, tryCount: Int): Boolean { + val missingClass = extractMissingClass(e) + if (log.isDebugEnabled) { + log.debug("Checking if transaction has missing attachment (missingClass=$missingClass) (legacy=$isLegacy) $wireTx", e) + } + return when { + missingClass != null -> { + val attachments = if (isLegacy) wireTx.legacyAttachments else wireTx.nonLegacyAttachments + (tryCount == 0 && fixupAttachments(attachments, serviceHub, e)) || addMissingAttachment(missingClass, isLegacy, serviceHub, e) + } + // Ignore these exceptions as they will break unit tests. + // The point here is only to detect missing dependencies. The other exceptions are irrelevant. + e is TransactionVerificationException -> false + e is TransactionResolutionException -> false + e is IllegalStateException -> false + e is IllegalArgumentException -> false + // Fail early if none of the expected scenarios were hit. + else -> { + log.error("""The transaction currently built will not validate because of an unknown error most likely caused by a + missing dependency in the transaction attachments. + Please contact the developer of the CorDapp for further instructions. + """.trimIndent(), e) + throw e } } } + private fun extractMissingClass(throwable: Throwable): String? { + var current = throwable + while (true) { + if (current is ClassNotFoundException) { + return current.message?.replace('.', '/') + } + if (current is NoClassDefFoundError) { + return current.message + } + val message = current.message + if (message != null) { + message.extractClassAfter(NoClassDefFoundError::class)?.let { return it } + message.extractClassAfter(ClassNotFoundException::class)?.let { return it.replace('.', '/') } + } + current = current.cause ?: return null + } + } + + private fun String.extractClassAfter(exceptionClass: KClass): String? { + return substringAfterLast("${exceptionClass.java.name}: ", "").takeIf { it.isNotEmpty() } + } + private fun fixupAttachments( txAttachments: List, serviceHub: VerifyingServiceHub, @@ -314,7 +334,7 @@ open class TransactionBuilder( return true } - private fun addMissingAttachment(missingClass: String, serviceHub: VerifyingServiceHub, originalException: Throwable): Boolean { + private fun addMissingAttachment(missingClass: String, isLegacy: Boolean, serviceHub: VerifyingServiceHub, originalException: Throwable): Boolean { if (!isValidJavaClass(missingClass)) { log.warn("Could not autodetect a valid attachment for the transaction being built.") throw originalException @@ -323,7 +343,14 @@ open class TransactionBuilder( throw originalException } - val attachment = serviceHub.getTrustedClassAttachment(missingClass) + val attachments = serviceHub.getTrustedClassAttachments(missingClass) + val attachment = if (isLegacy) { + // Any attachment which contains the class but isn't a non-legacy CorDapp is *probably* the legacy attachment we're looking for + val nonLegacyCordapps = serviceHub.cordappProvider.cordapps.mapToSet { it.jarHash } + attachments.firstOrNull { it.id !in nonLegacyCordapps } + } else { + attachments.firstOrNull() + } if (attachment == null) { log.error("""The transaction currently built is missing an attachment for class: $missingClass. @@ -338,7 +365,12 @@ open class TransactionBuilder( Please contact the developer of the CorDapp and install the latest version, as this approach might be insecure. """.trimIndent()) - addAttachment(attachment.id) + if (isLegacy) { + (extraLegacyAttachments ?: LinkedHashSet().also { extraLegacyAttachments = it }) += attachment.id + } else { + addAttachment(attachment.id) + } + return true } diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt index def2ad6634..13f019188d 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt @@ -4,6 +4,7 @@ import net.corda.core.DoNotImplement import net.corda.core.contracts.NamedByHash import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.isFulfilledBy +import net.corda.core.internal.mapToSet import net.corda.core.transactions.SignedTransaction.SignaturesMissingException import net.corda.core.utilities.toNonEmptySet import java.security.InvalidKeyException @@ -99,9 +100,9 @@ interface TransactionWithSignatures : NamedByHash { * Return the [PublicKey]s for which we still need signatures. */ fun getMissingSigners(): Set { - val sigKeys = sigs.map { it.by }.toSet() + val sigKeys = sigs.mapToSet { it.by } // TODO Problem is that we can get single PublicKey wrapped as CompositeKey in allowedToBeMissing/mustSign // equals on CompositeKey won't catch this case (do we want to single PublicKey be equal to the same key wrapped in CompositeKey with threshold 1?) - return requiredSigningKeys.filter { !it.isFulfilledBy(sigKeys) }.toSet() + return requiredSigningKeys.asSequence().filter { !it.isFulfilledBy(sigKeys) }.toSet() } } 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 021b09e97b..c561ea6ecb 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -15,6 +15,7 @@ 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.contracts.TransactionVerificationException import net.corda.core.crypto.DigestService import net.corda.core.crypto.MerkleTree import net.corda.core.crypto.SecureHash @@ -24,14 +25,22 @@ import net.corda.core.identity.Party import net.corda.core.internal.Emoji import net.corda.core.internal.SerializedStateAndRef import net.corda.core.internal.SerializedTransactionState +import net.corda.core.internal.TransactionDeserialisationException import net.corda.core.internal.createComponentGroups import net.corda.core.internal.deserialiseComponentGroup +import net.corda.core.internal.equivalent import net.corda.core.internal.flatMapToSet import net.corda.core.internal.getGroup import net.corda.core.internal.isUploaderTrusted import net.corda.core.internal.lazyMapped import net.corda.core.internal.mapToSet +import net.corda.core.internal.toSimpleString import net.corda.core.internal.uncheckedCast +import net.corda.core.internal.verification.NodeVerificationSupport +import net.corda.core.internal.verification.VerificationResult +import net.corda.core.internal.verification.VerificationResult.External +import net.corda.core.internal.verification.VerificationResult.InProcess +import net.corda.core.internal.verification.VerificationResult.InProcessAndExternal import net.corda.core.internal.verification.VerificationSupport import net.corda.core.internal.verification.toVerifyingServiceHub import net.corda.core.node.NetworkParameters @@ -39,9 +48,15 @@ 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.MissingAttachmentsException import net.corda.core.serialization.SerializationFactory +import net.corda.core.serialization.internal.MissingSerializerException import net.corda.core.serialization.serialize import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.Try +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.debug +import java.io.NotSerializableException import java.security.PublicKey import java.security.SignatureException import java.util.function.Predicate @@ -71,7 +86,7 @@ import java.util.function.Predicate *

*/ @CordaSerializable -@Suppress("ThrowsCount") +@Suppress("ThrowsCount", "TooManyFunctions", "MagicNumber") class WireTransaction(componentGroups: List, val privacySalt: PrivacySalt, digestService: DigestService) : TraversableTransaction(componentGroups, digestService) { constructor(componentGroups: List) : this(componentGroups, PrivacySalt()) @@ -164,14 +179,14 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr } // These are not used override val appClassLoader: ClassLoader get() = throw AbstractMethodError() - override fun getTrustedClassAttachment(className: String) = throw AbstractMethodError() + override fun getTrustedClassAttachments(className: String) = throw AbstractMethodError() override fun fixupAttachmentIds(attachmentIds: Collection) = throw AbstractMethodError() }) } @CordaInternal @JvmSynthetic - fun toLedgerTransactionInternal(verificationSupport: VerificationSupport): LedgerTransaction { + internal fun toLedgerTransactionInternal(verificationSupport: VerificationSupport): LedgerTransaction { // Look up public keys to authenticated identities. val authenticatedCommands = if (verificationSupport.isInProcess) { commands.lazyMapped { cmd, _ -> @@ -360,43 +375,211 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr sig.verify(id) } + @CordaInternal + @JvmSynthetic + internal fun tryVerify(verificationSupport: NodeVerificationSupport): VerificationResult { + return when { + legacyAttachments.isEmpty() -> { + log.debug { "${toSimpleString()} will be verified in-process" } + InProcess(Try.on { verifyInProcess(verificationSupport) }) + } + nonLegacyAttachments.isEmpty() -> { + log.debug { "${toSimpleString()} will be verified by the external verifer" } + External(Try.on { verificationSupport.externalVerifierHandle.verifyTransaction(this) }) + } + else -> { + log.debug { "${toSimpleString()} will be verified both in-process and by the external verifer" } + val inProcessResult = Try.on { verifyInProcess(verificationSupport) } + val externalResult = Try.on { verificationSupport.externalVerifierHandle.verifyTransaction(this) } + InProcessAndExternal(inProcessResult, externalResult) + } + } + } + + @CordaInternal + @JvmSynthetic + internal fun verifyInProcess(verificationSupport: VerificationSupport): LedgerTransaction { + val ltx = toLedgerTransactionInternal(verificationSupport) + try { + ltx.verify() + } catch (e: NoClassDefFoundError) { + checkReverifyAllowed(e) + val missingClass = e.message ?: throw e + log.warn("Transaction {} has missing class: {}", ltx.id, missingClass) + reverifyWithFixups(ltx, verificationSupport, missingClass) + } catch (e: NotSerializableException) { + checkReverifyAllowed(e) + retryVerification(e, e, ltx, verificationSupport) + } catch (e: TransactionDeserialisationException) { + checkReverifyAllowed(e) + retryVerification(e.cause, e, ltx, verificationSupport) + } + return ltx + } + + private fun checkReverifyAllowed(ex: Throwable) { + // If that transaction was created with and after Corda 4 then just fail. + // The lenient dependency verification is only supported for Corda 3 transactions. + // To detect if the transaction was created before Corda 4 we check if the transaction has the NetworkParameters component group. + if (networkParametersHash != null) { + log.warn("TRANSACTION VERIFY FAILED - No attempt to auto-repair as TX is Corda 4+") + throw ex + } + } + + 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, 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, verificationSupport, missingClass) + } else { + throw ex + } + } + else -> throw ex + } + } + + // 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, 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()) + + 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.getTrustedClassAttachments(missingClass).firstOrNull()) { + """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 + } + override fun toString(): String { - val buf = StringBuilder() - buf.appendLine("Transaction:") + val buf = StringBuilder(1024) + buf.appendLine("Transaction $id:") for (reference in references) { val emoji = Emoji.rightArrow - buf.appendLine("${emoji}REFS: $reference") + buf.appendLine("${emoji}REFS: $reference") } for (input in inputs) { val emoji = Emoji.rightArrow - buf.appendLine("${emoji}INPUT: $input") + buf.appendLine("${emoji}INPUT: $input") } for ((data) in outputs) { val emoji = Emoji.leftArrow - buf.appendLine("${emoji}OUTPUT: $data") + buf.appendLine("${emoji}OUTPUT: $data") } for (command in commands) { val emoji = Emoji.diamond - buf.appendLine("${emoji}COMMAND: $command") + buf.appendLine("${emoji}COMMAND: $command") } - for (attachment in attachments) { + for (attachment in nonLegacyAttachments) { val emoji = Emoji.paperclip - buf.appendLine("${emoji}ATTACHMENT: $attachment") + buf.appendLine("${emoji}ATTACHMENT: $attachment") + } + for (attachment in legacyAttachments) { + val emoji = Emoji.paperclip + buf.appendLine("${emoji}ATTACHMENT: $attachment (legacy)") } if (networkParametersHash != null) { - buf.appendLine("PARAMETERS HASH: $networkParametersHash") + val emoji = Emoji.newspaper + buf.appendLine("${emoji}NETWORK PARAMS: $networkParametersHash") } return buf.toString() } - override fun equals(other: Any?): Boolean { - if (other is WireTransaction) { - return (this.id == other.id) - } - return false - } + override fun equals(other: Any?): Boolean = other is WireTransaction && this.id == other.id override fun hashCode(): Int = id.hashCode() + + private companion object { + private val log = contextLogger() + } } /** 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 3fdf5bd9d2..958f18baf1 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 @@ -205,8 +205,8 @@ class JarScanningCordappLoader(private val cordappJars: Set, "corresponding newer version (4.12 or later). Please add this corresponding CorDapp or remove the legacy one." } check(newerCordapp.contractVersionId > legacyCordapp.contractVersionId) { - "Newer contract CorDapp '${newerCordapp.jarFile}' does not have a higher version number " + - "(${newerCordapp.contractVersionId}) compared to corresponding legacy contract CorDapp " + + "Newer contract CorDapp '${newerCordapp.jarFile}' does not have a higher versionId " + + "(${newerCordapp.contractVersionId}) than corresponding legacy contract CorDapp " + "'${legacyCordapp.jarFile}' (${legacyCordapp.contractVersionId})" } } diff --git a/node/src/main/kotlin/net/corda/node/verification/ExternalVerifierHandleImpl.kt b/node/src/main/kotlin/net/corda/node/verification/ExternalVerifierHandleImpl.kt index c386f287b5..72a35b9ad7 100644 --- a/node/src/main/kotlin/net/corda/node/verification/ExternalVerifierHandleImpl.kt +++ b/node/src/main/kotlin/net/corda/node/verification/ExternalVerifierHandleImpl.kt @@ -6,10 +6,11 @@ import net.corda.core.internal.copyTo import net.corda.core.internal.level import net.corda.core.internal.mapToSet import net.corda.core.internal.readFully +import net.corda.core.internal.toSimpleString import net.corda.core.internal.verification.ExternalVerifierHandle import net.corda.core.internal.verification.NodeVerificationSupport import net.corda.core.serialization.serialize -import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.CoreTransaction import net.corda.core.utilities.Try import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug @@ -22,7 +23,7 @@ import net.corda.serialization.internal.verifier.ExternalVerifierInbound.Attachm 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.TrustedClassAttachmentsResult import net.corda.serialization.internal.verifier.ExternalVerifierInbound.VerificationRequest import net.corda.serialization.internal.verifier.ExternalVerifierOutbound import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerificationResult @@ -31,7 +32,7 @@ import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.Verifi 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.ExternalVerifierOutbound.VerifierRequest.GetTrustedClassAttachments import net.corda.serialization.internal.verifier.readCordaSerializable import net.corda.serialization.internal.verifier.writeCordaSerializable import java.io.DataInputStream @@ -74,13 +75,13 @@ class ExternalVerifierHandleImpl( @Volatile private var connection: Connection? = null - override fun verifyTransaction(stx: SignedTransaction, checkSufficientSignatures: Boolean) { - log.info("Verify $stx externally, checkSufficientSignatures=$checkSufficientSignatures") + override fun verifyTransaction(ctx: CoreTransaction) { + log.info("Verify ${ctx.toSimpleString()} externally") // 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(verificationSupport::getSerializedState) - val request = VerificationRequest(stx, stxInputsAndReferences, checkSufficientSignatures) + val ctxInputsAndReferences = (ctx.inputs + ctx.references).associateWith(verificationSupport::getSerializedState) + val request = VerificationRequest(ctx, ctxInputsAndReferences) // To keep things simple the verifier only supports one verification request at a time. synchronized(this) { @@ -146,23 +147,22 @@ class ExternalVerifierHandleImpl( private fun processVerifierRequest(request: VerifierRequest, connection: Connection) { val result = when (request) { is GetParties -> PartiesResult(verificationSupport.getParties(request.keys)) - is GetAttachment -> AttachmentResult(prepare(verificationSupport.getAttachment(request.id))) - is GetAttachments -> AttachmentsResult(verificationSupport.getAttachments(request.ids).map(::prepare)) + is GetAttachment -> AttachmentResult(verificationSupport.getAttachment(request.id)?.withTrust()) + is GetAttachments -> AttachmentsResult(verificationSupport.getAttachments(request.ids).map { it?.withTrust() }) is GetNetworkParameters -> NetworkParametersResult(verificationSupport.getNetworkParameters(request.id)) - is GetTrustedClassAttachment -> TrustedClassAttachmentResult(verificationSupport.getTrustedClassAttachment(request.className)?.id) + is GetTrustedClassAttachments -> TrustedClassAttachmentsResult(verificationSupport.getTrustedClassAttachments(request.className).map { it.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 = verificationSupport.isAttachmentTrusted(attachment) - val attachmentForSer = when (attachment) { + private fun Attachment.withTrust(): AttachmentWithTrust { + val isTrusted = verificationSupport.isAttachmentTrusted(this) + val attachmentForSer = when (this) { // 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) + is AbstractAttachment -> GeneratedAttachment(open().readFully(), uploader) // For everything else we keep as is, in particular preserving ContractAttachment - else -> attachment + else -> this } return AttachmentWithTrust(attachmentForSer, isTrusted) } 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 index 3dd893dbb5..617f2f1124 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/verifier/ExternalVerifierTypes.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/verifier/ExternalVerifierTypes.kt @@ -11,7 +11,7 @@ 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.transactions.CoreTransaction import net.corda.core.utilities.Try import java.io.DataInputStream import java.io.DataOutputStream @@ -40,16 +40,17 @@ sealed class ExternalVerifierInbound { } data class VerificationRequest( - val stx: SignedTransaction, - val stxInputsAndReferences: Map, - val checkSufficientSignatures: Boolean - ) : ExternalVerifierInbound() + val ctx: CoreTransaction, + val ctxInputsAndReferences: Map + ) : ExternalVerifierInbound() { + override fun toString(): String = "VerificationRequest(ctx=$ctx)" + } 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() + data class TrustedClassAttachmentsResult(val ids: List) : ExternalVerifierInbound() } @CordaSerializable @@ -59,12 +60,12 @@ data class AttachmentWithTrust(val attachment: Attachment, val isTrusted: Boolea sealed class ExternalVerifierOutbound { sealed class VerifierRequest : ExternalVerifierOutbound() { data class GetParties(val keys: Set) : VerifierRequest() { - override fun toString(): String = "GetParty(keys=${keys.map { it.toStringShort() }}})" + override fun toString(): String = "GetParties(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 GetTrustedClassAttachments(val className: String) : VerifierRequest() } data class VerificationResult(val result: Try) : ExternalVerifierOutbound() diff --git a/verifier/src/main/kotlin/net/corda/verifier/ExternalVerificationContext.kt b/verifier/src/main/kotlin/net/corda/verifier/ExternalVerificationContext.kt index 60e49dcd2c..c410564c0a 100644 --- a/verifier/src/main/kotlin/net/corda/verifier/ExternalVerificationContext.kt +++ b/verifier/src/main/kotlin/net/corda/verifier/ExternalVerificationContext.kt @@ -28,8 +28,8 @@ class ExternalVerificationContext( override fun isAttachmentTrusted(attachment: Attachment): Boolean = externalVerifier.getAttachment(attachment.id)!!.isTrusted - override fun getTrustedClassAttachment(className: String): Attachment? { - return externalVerifier.getTrustedClassAttachment(className) + override fun getTrustedClassAttachments(className: String): List { + return externalVerifier.getTrustedClassAttachments(className) } override fun getNetworkParameters(id: SecureHash?): NetworkParameters? = externalVerifier.getNetworkParameters(id) diff --git a/verifier/src/main/kotlin/net/corda/verifier/ExternalVerifier.kt b/verifier/src/main/kotlin/net/corda/verifier/ExternalVerifier.kt index 91f60d0060..904397699e 100644 --- a/verifier/src/main/kotlin/net/corda/verifier/ExternalVerifier.kt +++ b/verifier/src/main/kotlin/net/corda/verifier/ExternalVerifier.kt @@ -7,6 +7,7 @@ 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.toSimpleString import net.corda.core.internal.toSynchronised import net.corda.core.internal.toTypedArray import net.corda.core.internal.verification.AttachmentFixups @@ -16,6 +17,8 @@ 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.transactions.ContractUpgradeWireTransaction +import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.Try import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug @@ -33,14 +36,14 @@ import net.corda.serialization.internal.verifier.ExternalVerifierInbound.Attachm 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.TrustedClassAttachmentsResult 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.ExternalVerifierOutbound.VerifierRequest.GetTrustedClassAttachments import net.corda.serialization.internal.verifier.loadCustomSerializationScheme import net.corda.serialization.internal.verifier.readCordaSerializable import net.corda.serialization.internal.verifier.writeCordaSerializable @@ -68,7 +71,7 @@ class ExternalVerifier( private val parties: OptionalCache private val attachments: OptionalCache private val networkParametersMap: OptionalCache - private val trustedClassAttachments: OptionalCache + private val trustedClassAttachments: Cache> private lateinit var appClassLoader: ClassLoader private lateinit var currentNetworkParameters: NetworkParameters @@ -134,13 +137,18 @@ class ExternalVerifier( @Suppress("INVISIBLE_MEMBER") private fun verifyTransaction(request: VerificationRequest) { - val verificationContext = ExternalVerificationContext(appClassLoader, attachmentsClassLoaderCache, this, request.stxInputsAndReferences) + val verificationContext = ExternalVerificationContext(appClassLoader, attachmentsClassLoaderCache, this, request.ctxInputsAndReferences) val result: Try = try { - request.stx.verifyInProcess(verificationContext, request.checkSufficientSignatures) - log.info("${request.stx} verified") + val ctx = request.ctx + when (ctx) { + is WireTransaction -> ctx.verifyInProcess(verificationContext) + is ContractUpgradeWireTransaction -> ctx.verifyInProcess(verificationContext) + else -> throw IllegalArgumentException("${ctx.toSimpleString()} not supported") + } + log.info("${ctx.toSimpleString()} verified") Try.Success(Unit) } catch (t: Throwable) { - log.info("${request.stx} failed to verify", t) + log.info("${request.ctx.toSimpleString()} failed to verify", t) Try.Failure(t) } toNode.writeCordaSerializable(VerificationResult(result)) @@ -164,13 +172,13 @@ class ExternalVerifier( } } - 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 getTrustedClassAttachments(className: String): List { + val attachmentIds = trustedClassAttachments.get(className) { + // GetTrustedClassAttachments returns back the attachment IDs, not the whole attachments. This lets us avoid downloading the + // entire attachments again if we already have them. + request(GetTrustedClassAttachments(className)).ids + }!! + return attachmentIds.map { getAttachment(it)!!.attachment } } fun getNetworkParameters(id: SecureHash?): NetworkParameters? {