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 3a81987091..d6ed4e254a 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? {