ENT-11445: Support legacy contract CorDapp dependencies

The `TransactionBuilder` has been updated to look for any missing dependencies to legacy contract attachments, in the same way it does for missing dependencies for CorDapps in the "cordapps" directory,

Since `TransactionBuilder` does verification on the `WireTransaction` and not a `SignedTransaction`, much of the verification logic in `SignedTransaction` had to moved to `WireTransaction` to allow the external verifier to be involved. The external verifier receives a `CoreTransaction` to verify instead of a `SignedTransaction`. `SignedTransaction.verify` does the signature checks first in-process, before then delegating the reset of the verification to the `CoreTransaction`.

A legacy contract dependency is defined as an attachment containing the missing class which isn't also a non-legacy Cordapp (i.e. a CorDapp which isn't in the "cordapp" directory).
This commit is contained in:
Shams Asari 2024-03-13 10:36:12 +00:00
parent 5b8fc6f503
commit b3265314ce
28 changed files with 740 additions and 487 deletions

View File

@ -552,8 +552,6 @@ public interface net.corda.core.contracts.Attachment extends net.corda.core.cont
public interface net.corda.core.contracts.AttachmentConstraint public interface net.corda.core.contracts.AttachmentConstraint
public abstract boolean isSatisfiedBy(net.corda.core.contracts.Attachment) public abstract boolean isSatisfiedBy(net.corda.core.contracts.Attachment)
## ##
public final class net.corda.core.contracts.AttachmentConstraintKt extends java.lang.Object
##
@CordaSerializable @CordaSerializable
public final class net.corda.core.contracts.AttachmentResolutionException extends net.corda.core.flows.FlowException public final class net.corda.core.contracts.AttachmentResolutionException extends net.corda.core.flows.FlowException
public <init>(net.corda.core.crypto.SecureHash) public <init>(net.corda.core.crypto.SecureHash)

View File

@ -57,55 +57,61 @@ processSmokeTestResources {
from(configurations.corda4_11) 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 { processTestResources {
from(configurations.corda4_11) from(configurations.corda4_11)
} }
dependencies { 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(":core")
testImplementation project(path: ':core', configuration: 'testArtifacts') testImplementation project(":serialization")
testImplementation project(":finance:contracts")
testImplementation project(":finance:workflows")
testImplementation project(":node") testImplementation project(":node")
testImplementation project(":node-api") testImplementation project(":node-api")
testImplementation project(":client:rpc") testImplementation project(":client:rpc")
testImplementation project(":serialization")
testImplementation project(":common-configuration-parsing") testImplementation project(":common-configuration-parsing")
testImplementation project(":finance:contracts")
testImplementation project(":finance:workflows")
testImplementation project(":core-test-utils") testImplementation project(":core-test-utils")
testImplementation project(":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) // Guava: Google test library (collections test suite)
testImplementation "com.google.guava:guava-testlib:$guava_version" testImplementation "com.google.guava:guava-testlib:$guava_version"
testImplementation "com.google.jimfs:jimfs:1.1" testImplementation "com.google.jimfs:jimfs:1.1"
testImplementation group: "com.typesafe", name: "config", version: typesafe_config_version testImplementation "com.typesafe:config:$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 "org.jetbrains.kotlin:kotlin-test:$kotlin_version" testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
// Hamkrest, for fluent, composable matchers // Hamkrest, for fluent, composable matchers
testImplementation "com.natpryce:hamkrest:$hamkrest_version" testImplementation "com.natpryce:hamkrest:$hamkrest_version"
testImplementation 'org.hamcrest:hamcrest-library:2.1'
// SLF4J: commons-logging bindings for a SLF4J back end testImplementation "org.mockito.kotlin:mockito-kotlin:$mockito_kotlin_version"
implementation "org.slf4j:jcl-over-slf4j:$slf4j_version" testImplementation "org.mockito:mockito-core:$mockito_version"
implementation "org.slf4j:slf4j-api:$slf4j_version"
// AssertJ: for fluent assertions for testing // AssertJ: for fluent assertions for testing
testImplementation "org.assertj:assertj-core:${assertj_version}" testImplementation "org.assertj:assertj-core:${assertj_version}"
// Guava: Google utilities library. // Guava: Google utilities library.
testImplementation "com.google.guava:guava:$guava_version" 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! // Smoke tests do NOT have any Node code on the classpath!
smokeTestImplementation project(":core") smokeTestImplementation project(":core")
@ -127,9 +133,6 @@ dependencies {
smokeTestRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}" smokeTestRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}"
smokeTestRuntimeOnly "org.slf4j:slf4j-simple:$slf4j_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-contracts:4.11"
corda4_11 "net.corda:corda-finance-workflows:4.11" corda4_11 "net.corda:corda-finance-workflows:4.11"
corda4_11 "net.corda:corda:4.11" corda4_11 "net.corda:corda:4.11"

View File

@ -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<Path, Path> {
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
}
}

View File

@ -213,7 +213,7 @@ class ExternalVerificationUnsignedCordappsTest {
).returnValue.getOrThrow() ).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 nodeLogs = logs("node")!!
val externalVerifierLogs = externalVerifierLogs() val externalVerifierLogs = externalVerifierLogs()
for (txId in txIds) { 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) { if (verificationType != VerificationType.IN_PROCESS) {
assertThat(externalVerifierLogs).describedAs("External verifier was not started").isNotNull() 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 { private enum class VerificationType {
IN_PROCESS, EXTERNAL, BOTH 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"
}
}

View File

@ -8,17 +8,13 @@ import net.corda.core.internal.copyToDirectory
import net.corda.core.internal.hash import net.corda.core.internal.hash
import net.corda.core.internal.toPath import net.corda.core.internal.toPath
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.coretesting.internal.useZipFile
import net.corda.finance.DOLLARS import net.corda.finance.DOLLARS
import net.corda.finance.contracts.asset.Cash import net.corda.finance.contracts.asset.Cash
import net.corda.finance.issuedBy import net.corda.finance.issuedBy
import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContract
import net.corda.testing.contracts.DummyState import net.corda.testing.contracts.DummyState
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.DummyCommandData 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.internal.JarSignatureTestUtils.unsignJar
import net.corda.testing.core.singleIdentity import net.corda.testing.core.singleIdentity
import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP 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.assertThat
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
import org.junit.After import org.junit.After
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder import org.junit.rules.TemporaryFolder
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.absolutePathString
import kotlin.io.path.copyTo import kotlin.io.path.copyTo
import kotlin.io.path.createDirectories import kotlin.io.path.createDirectories
import kotlin.io.path.deleteExisting
import kotlin.io.path.div import kotlin.io.path.div
import kotlin.io.path.inputStream import kotlin.io.path.inputStream
import kotlin.io.path.listDirectoryEntries
@Suppress("INVISIBLE_MEMBER") @Suppress("INVISIBLE_MEMBER")
class TransactionBuilderMockNetworkTest { class TransactionBuilderMockNetworkTest {
@ -107,9 +99,9 @@ class TransactionBuilderMockNetworkTest {
@Test(timeout=300_000) @Test(timeout=300_000)
fun `populates legacy attachment group if legacy contract CorDapp is present`() { fun `populates legacy attachment group if legacy contract CorDapp is present`() {
val node = mockNetwork.createNode { val node = mockNetwork.createNode { args ->
it.copyToLegacyContracts(legacyFinanceContractsJar) args.copyToLegacyContracts(legacyFinanceContractsJar)
InternalMockNetwork.MockNode(it) InternalMockNetwork.MockNode(args)
} }
val builder = TransactionBuilder() val builder = TransactionBuilder()
val identity = node.info.singleIdentity() val identity = node.info.singleIdentity()
@ -120,45 +112,6 @@ class TransactionBuilderMockNetworkTest {
stx.verify(node.services) 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) { private fun MockNodeArgs.copyToLegacyContracts(vararg jars: Path) {
val legacyContractsDir = (config.baseDirectory / "legacy-contracts").createDirectories() val legacyContractsDir = (config.baseDirectory / "legacy-contracts").createDirectories()
jars.forEach { it.copyToDirectory(legacyContractsDir) } jars.forEach { it.copyToDirectory(legacyContractsDir) }

View File

@ -11,13 +11,12 @@ import net.corda.core.internal.utilities.Internable
import net.corda.core.internal.utilities.PrivateInterner import net.corda.core.internal.utilities.PrivateInterner
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug import net.corda.core.utilities.debug
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import java.lang.annotation.Inherited import java.lang.annotation.Inherited
import java.security.PublicKey import java.security.PublicKey
private val log = loggerFor<AttachmentConstraint>()
/** /**
* This annotation should only be added to [Contract] classes. * 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. * 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 { data class HashAttachmentConstraint(val attachmentId: SecureHash) : AttachmentConstraint {
companion object { companion object {
private val log = contextLogger()
val disableHashConstraints = System.getProperty("net.corda.node.disableHashConstraints")?.toBoolean() ?: false val disableHashConstraints = System.getProperty("net.corda.node.disableHashConstraints")?.toBoolean() ?: false
} }
override fun isSatisfiedBy(attachment: Attachment): Boolean { override fun isSatisfiedBy(attachment: Attachment): Boolean {
return if (attachment is AttachmentWithContext) { return if (attachment is AttachmentWithContext) {
log.debug("Checking attachment uploader ${attachment.contractAttachment.uploader} is trusted") 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. * It allows for centralized control over the cordapps that can be used.
*/ */
object WhitelistedByZoneAttachmentConstraint : AttachmentConstraint { object WhitelistedByZoneAttachmentConstraint : AttachmentConstraint {
private val log = loggerFor<WhitelistedByZoneAttachmentConstraint>()
override fun isSatisfiedBy(attachment: Attachment): Boolean { override fun isSatisfiedBy(attachment: Attachment): Boolean {
return if (attachment is AttachmentWithContext) { return if (attachment is AttachmentWithContext) {
val whitelist = attachment.whitelistedContractImplementations val whitelist = attachment.whitelistedContractImplementations
@ -120,6 +124,8 @@ data class SignatureAttachmentConstraint(val key: PublicKey) : AttachmentConstra
} }
companion object : Internable<SignatureAttachmentConstraint> { companion object : Internable<SignatureAttachmentConstraint> {
private val log = contextLogger()
@CordaInternal @CordaInternal
override val interner = PrivateInterner<SignatureAttachmentConstraint>() override val interner = PrivateInterner<SignatureAttachmentConstraint>()

View File

@ -450,7 +450,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction,
// The notary signature(s) are allowed to be missing but no others. // The notary signature(s) are allowed to be missing but no others.
if (notary != null) transaction.verifySignaturesExcept(notary.owningKey) else transaction.verifyRequiredSignatures() if (notary != null) transaction.verifySignaturesExcept(notary.owningKey) else transaction.verifyRequiredSignatures()
// TODO= [CORDA-3267] Remove duplicate signature verification // 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 // 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 // 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 // recording to the vault, etc. Note that calling verify() on this will fail as it doesn't have the necessary non-legacy attachments

View File

@ -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 equals(other: Any?) = other === this || other is Attachment && other.id == this.id
override fun hashCode() = id.hashCode() override fun hashCode() = id.hashCode()
override fun toString() = "${javaClass.simpleName}(id=$id)" override fun toString() = toSimpleString()
} }
@Throws(IOException::class) @Throws(IOException::class)

View File

@ -2,6 +2,7 @@
package net.corda.core.internal package net.corda.core.internal
import net.corda.core.contracts.ContractClassName import net.corda.core.contracts.ContractClassName
import net.corda.core.contracts.NamedByHash
import net.corda.core.contracts.TransactionResolutionException import net.corda.core.contracts.TransactionResolutionException
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.flows.DataVendingFlow 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 ServiceHub.getRequiredTransaction(txhash: SecureHash): SignedTransaction = validatedTransactions.getRequiredTransaction(txhash)
fun NamedByHash.toSimpleString(): String = "${javaClass.simpleName}(id=$id)"

View File

@ -148,8 +148,8 @@ fun <T> List<T>.indexOfOrThrow(item: T): Int {
@Suppress("INVISIBLE_MEMBER", "RemoveExplicitTypeArguments") // Because the external verifier uses Kotlin 1.2 @Suppress("INVISIBLE_MEMBER", "RemoveExplicitTypeArguments") // Because the external verifier uses Kotlin 1.2
inline fun <T, R> Collection<T>.mapToSet(transform: (T) -> R): Set<R> { inline fun <T, R> Collection<T>.mapToSet(transform: (T) -> R): Set<R> {
return when (size) { return when (size) {
0 -> return emptySet() 0 -> emptySet()
1 -> return setOf(transform(first())) 1 -> setOf(transform(first()))
else -> mapTo(LinkedHashSet<R>(mapCapacity(size)), transform) else -> mapTo(LinkedHashSet<R>(mapCapacity(size)), transform)
} }
} }

View File

@ -166,8 +166,10 @@ fun deserialiseCommands(
} }
val componentHashes = group.components.mapIndexed { index, component -> digestService.componentHash(group.nonces[index], component) } val componentHashes = group.components.mapIndexed { index, component -> digestService.componentHash(group.nonces[index], component) }
val leafIndices = componentHashes.map { group.partialMerkleTree.leafIndex(it) } 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" } check(leafIndices.max()!! < signersList.size) { "Invalid Transaction. A command with no corresponding signer detected" }
}
commandDataList.lazyMapped { commandData, index -> Command(commandData, signersList[leafIndices[index]]) } commandDataList.lazyMapped { commandData, index -> Command(commandData, signersList[leafIndices[index]]) }
} else { } else {
// It is a WireTransaction // It is a WireTransaction
@ -313,3 +315,14 @@ internal fun checkNotaryWhitelisted(ftx: FullTransaction) {
} }
} }
} }
fun getRequiredSigningKeysInternal(inputs: Sequence<StateAndRef<*>>, notary: Party?): Set<PublicKey> {
val keys = LinkedHashSet<PublicKey>()
for (input in inputs) {
input.state.data.participants.mapTo(keys) { it.owningKey }
}
if (notary != null) {
keys += notary.owningKey
}
return keys
}

View File

@ -1,7 +1,7 @@
package net.corda.core.internal.verification package net.corda.core.internal.verification
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.CoreTransaction
interface ExternalVerifierHandle : AutoCloseable { interface ExternalVerifierHandle : AutoCloseable {
fun verifyTransaction(stx: SignedTransaction, checkSufficientSignatures: Boolean) fun verifyTransaction(ctx: CoreTransaction)
} }

View File

@ -128,25 +128,17 @@ interface NodeVerificationSupport : VerificationSupport {
/** /**
* Scans trusted (installed locally) attachments to find all that contain the [className]. * 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 getTrustedClassAttachments(className: String): List<Attachment> {
override fun getTrustedClassAttachment(className: String): Attachment? {
val allTrusted = attachments.queryAttachments( val allTrusted = attachments.queryAttachments(
AttachmentsQueryCriteria().withUploader(Builder.`in`(TRUSTED_UPLOADERS)), 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))) AttachmentSort(listOf(AttachmentSortColumn(AttachmentSortAttribute.VERSION, Sort.Direction.DESC)))
) )
val fileName = "$className.class"
// TODO - add caching if performance is affected. return allTrusted.mapNotNull { id -> attachments.openAttachment(id)!!.takeIf { it.hasFile(fileName) } }
for (attId in allTrusted) {
val attch = attachments.openAttachment(attId)!!
if (attch.hasFile("$className.class")) return attch
}
return null
} }
private fun Attachment.hasFile(className: String): Boolean = openAsJAR().use { it.entries().any { entry -> entry.name == className } } private fun Attachment.hasFile(className: String): Boolean = openAsJAR().use { it.entries().any { entry -> entry.name == className } }

View File

@ -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<LedgerTransaction?>?
/**
* The external verifier result for the legacy version of the transaction.
*/
abstract val externalResult: Try<Unit>?
abstract fun enforceSuccess(): LedgerTransaction?
data class InProcess(override val inProcessResult: Try<LedgerTransaction?>) : VerificationResult() {
override val externalResult: Try<Unit>?
get() = null
override fun enforceSuccess(): LedgerTransaction? = inProcessResult.getOrThrow()
}
data class External(override val externalResult: Try<Unit>) : VerificationResult() {
override val inProcessResult: Try<LedgerTransaction?>?
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<LedgerTransaction>,
override val externalResult: Try<Unit>
) : 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) }
}
}
}
}
}

View File

@ -34,7 +34,7 @@ interface VerificationSupport {
fun isAttachmentTrusted(attachment: Attachment): Boolean fun isAttachmentTrusted(attachment: Attachment): Boolean
fun getTrustedClassAttachment(className: String): Attachment? fun getTrustedClassAttachments(className: String): List<Attachment>
fun getNetworkParameters(id: SecureHash?): NetworkParameters? fun getNetworkParameters(id: SecureHash?): NetworkParameters?

View File

@ -1,4 +1,3 @@
package net.corda.core.node.services package net.corda.core.node.services
import net.corda.core.DoNotImplement import net.corda.core.DoNotImplement

View File

@ -1,16 +1,22 @@
package net.corda.core.transactions package net.corda.core.transactions
import net.corda.core.DoNotImplement 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.identity.Party
import net.corda.core.internal.castIfPossible import net.corda.core.internal.castIfPossible
import net.corda.core.internal.indexOfOrThrow import net.corda.core.internal.indexOfOrThrow
import net.corda.core.internal.toSimpleString
import net.corda.core.internal.uncheckedCast import net.corda.core.internal.uncheckedCast
import java.util.function.Predicate import java.util.function.Predicate
/** /**
* An abstract class defining fields shared by all transaction types in the system. * An abstract class defining fields shared by all transaction types in the system.
*/ */
@Suppress("RedundantSamConstructor") // Because the external verifier uses Kotlin 1.2
@DoNotImplement @DoNotImplement
abstract class BaseTransaction : NamedByHash { abstract class BaseTransaction : NamedByHash {
/** A list of reusable reference data states which can be referred to by other contracts in this transaction. */ /** 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) }) return findOutRef(T::class.java, Predicate { predicate(it) })
} }
override fun toString(): String = "${javaClass.simpleName}(id=$id)" override fun toString(): String = toSimpleString()
} }

View File

@ -22,8 +22,10 @@ import net.corda.core.crypto.TransactionSignature
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.AttachmentWithContext import net.corda.core.internal.AttachmentWithContext
import net.corda.core.internal.combinedHash import net.corda.core.internal.combinedHash
import net.corda.core.internal.getRequiredSigningKeysInternal
import net.corda.core.internal.loadClassOfType 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.VerificationSupport
import net.corda.core.internal.verification.toVerifyingServiceHub import net.corda.core.internal.verification.toVerifyingServiceHub
import net.corda.core.node.NetworkParameters 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_ATTACHMENT
import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.UPGRADED_CONTRACT import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.UPGRADED_CONTRACT
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.Try
import net.corda.core.utilities.toBase58String import net.corda.core.utilities.toBase58String
import java.security.PublicKey import java.security.PublicKey
@ -159,6 +162,20 @@ data class ContractUpgradeWireTransaction(
return ContractUpgradeFilteredTransaction(visibleComponents, hiddenComponents, digestService) 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 { enum class Component {
INPUTS, NOTARY, LEGACY_ATTACHMENT, UPGRADED_CONTRACT, UPGRADED_ATTACHMENT, PARAMETERS_HASH 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. */ /** The required signers are the set of all input states' participants. */
override val requiredSigningKeys: Set<PublicKey> override val requiredSigningKeys: Set<PublicKey>
get() = inputs.flatMap { it.state.data.participants }.mapToSet { it.owningKey } + notary.owningKey get() = getRequiredSigningKeysInternal(inputs.asSequence(), notary)
override fun getKeyDescriptions(keys: Set<PublicKey>): List<String> { override fun getKeyDescriptions(keys: Set<PublicKey>): List<String> {
return keys.map { it.toBase58String() } return keys.map { it.toBase58String() }

View File

@ -11,8 +11,10 @@ import net.corda.core.crypto.DigestService
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.TransactionSignature
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.getRequiredSigningKeysInternal
import net.corda.core.internal.indexOfOrThrow 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.VerificationSupport
import net.corda.core.internal.verification.toVerifyingServiceHub import net.corda.core.internal.verification.toVerifyingServiceHub
import net.corda.core.node.NetworkParameters 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.NOTARY
import net.corda.core.transactions.NotaryChangeWireTransaction.Component.PARAMETERS_HASH import net.corda.core.transactions.NotaryChangeWireTransaction.Component.PARAMETERS_HASH
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.Try
import net.corda.core.utilities.toBase58String import net.corda.core.utilities.toBase58String
import java.security.PublicKey import java.security.PublicKey
@ -107,6 +110,16 @@ data class NotaryChangeWireTransaction(
return resolve(services as ServicesForResolution, sigs) 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 { enum class Component {
INPUTS, NOTARY, NEW_NOTARY, PARAMETERS_HASH INPUTS, NOTARY, NEW_NOTARY, PARAMETERS_HASH
} }
@ -180,7 +193,7 @@ private constructor(
get() = inputs.map { computeOutput(it, newNotary) { inputs.map(StateAndRef<ContractState>::ref) } } get() = inputs.map { computeOutput(it, newNotary) { inputs.map(StateAndRef<ContractState>::ref) } }
override val requiredSigningKeys: Set<PublicKey> override val requiredSigningKeys: Set<PublicKey>
get() = inputs.flatMap { it.state.data.participants }.mapToSet { it.owningKey } + notary.owningKey get() = getRequiredSigningKeysInternal(inputs.asSequence(), notary)
override fun getKeyDescriptions(keys: Set<PublicKey>): List<String> { override fun getKeyDescriptions(keys: Set<PublicKey>): List<String> {
return keys.map { it.toBase58String() } return keys.map { it.toBase58String() }

View File

@ -3,12 +3,12 @@ package net.corda.core.transactions
import net.corda.core.CordaException import net.corda.core.CordaException
import net.corda.core.CordaInternal import net.corda.core.CordaInternal
import net.corda.core.CordaThrowable import net.corda.core.CordaThrowable
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.AttachmentResolutionException import net.corda.core.contracts.AttachmentResolutionException
import net.corda.core.contracts.NamedByHash import net.corda.core.contracts.NamedByHash
import net.corda.core.contracts.StateRef import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TransactionResolutionException import net.corda.core.contracts.TransactionResolutionException
import net.corda.core.contracts.TransactionVerificationException import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.contracts.TransactionVerificationException.TransactionNetworkParameterOrderingException
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignableData
import net.corda.core.crypto.SignatureMetadata 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.sign
import net.corda.core.crypto.toStringShort import net.corda.core.crypto.toStringShort
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.TransactionDeserialisationException
import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.VisibleForTesting
import net.corda.core.internal.equivalent import net.corda.core.internal.getRequiredSigningKeysInternal
import net.corda.core.internal.isUploaderTrusted import net.corda.core.internal.toSimpleString
import net.corda.core.internal.verification.NodeVerificationSupport 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.internal.verification.toVerifyingServiceHub
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
import net.corda.core.node.ServicesForResolution import net.corda.core.node.ServicesForResolution
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.MissingAttachmentsException
import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import net.corda.core.serialization.internal.MissingSerializerException
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.utilities.Try import net.corda.core.utilities.toBase58String
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 java.security.KeyPair import java.security.KeyPair
import java.security.PublicKey import java.security.PublicKey
import java.security.SignatureException import java.security.SignatureException
import java.util.function.Predicate 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 * 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 * 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 * 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<CoreTransaction>,
val verifyingServiceHub = services.toVerifyingServiceHub() val verifyingServiceHub = services.toVerifyingServiceHub()
// We need parameters check here, because finality flow calls stx.toLedgerTransaction() and then verify. // We need parameters check here, because finality flow calls stx.toLedgerTransaction() and then verify.
resolveAndCheckNetworkParameters(verifyingServiceHub) resolveAndCheckNetworkParameters(verifyingServiceHub)
return toLedgerTransactionInternal(verifyingServiceHub, checkSufficientSignatures)
}
@JvmSynthetic
@CordaInternal
fun toLedgerTransactionInternal(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean): LedgerTransaction {
// TODO: We could probably optimise the below by // 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. // 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. // b) omit verifying signatures when threshold requirement is met.
@ -176,12 +162,8 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
// For the above to work, [checkSignaturesAreValid] should take the [requiredSigningKeys] as input // For the above to work, [checkSignaturesAreValid] should take the [requiredSigningKeys] as input
// and probably combine logic from signature validation and key-fulfilment // and probably combine logic from signature validation and key-fulfilment
// in [TransactionWithSignatures.verifySignaturesExcept]. // in [TransactionWithSignatures.verifySignaturesExcept].
if (checkSufficientSignatures) { verifySignatures(verifyingServiceHub, checkSufficientSignatures)
verifyRequiredSignatures() // It internally invokes checkSignaturesAreValid(). return tx.toLedgerTransactionInternal(verifyingServiceHub)
} else {
checkSignaturesAreValid()
}
return tx.toLedgerTransactionInternal(verificationSupport)
} }
/** /**
@ -210,252 +192,50 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
*/ */
@CordaInternal @CordaInternal
@JvmSynthetic @JvmSynthetic
internal fun verifyInternal(verificationSupport: NodeVerificationSupport, checkSufficientSignatures: Boolean = true): FullTransaction? { internal fun verifyInternal(verificationSupport: NodeVerificationSupport, checkSufficientSignatures: Boolean = true): LedgerTransaction? {
resolveAndCheckNetworkParameters(verificationSupport) resolveAndCheckNetworkParameters(verificationSupport)
val verificationType = determineVerificationType() verifySignatures(verificationSupport, checkSufficientSignatures)
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 {
val ctx = coreTransaction val ctx = coreTransaction
return when (ctx) { val verificationResult = when (ctx) {
is WireTransaction -> { // TODO: Verify contract constraints here as well as in LedgerTransaction to ensure that anything being deserialised
when { // from the attachment is trusted. This will require some partial serialisation work to not load the ContractState
ctx.legacyAttachments.isEmpty() -> VerificationType.IN_PROCESS // objects from the TransactionState.
ctx.nonLegacyAttachments.isEmpty() -> VerificationType.EXTERNAL is WireTransaction -> ctx.tryVerify(verificationSupport)
else -> VerificationType.BOTH is ContractUpgradeWireTransaction -> ctx.tryVerify(verificationSupport)
} is NotaryChangeWireTransaction -> ctx.tryVerify(verificationSupport)
} else -> throw IllegalStateException("${ctx.toSimpleString()} cannot be verified")
// 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<FullTransaction>, 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)
} }
return verificationResult.enforceSuccess()
} }
@Suppress("ThrowsCount") @Suppress("ThrowsCount")
private fun resolveAndCheckNetworkParameters(services: NodeVerificationSupport) { private fun resolveAndCheckNetworkParameters(services: NodeVerificationSupport) {
val hashOrDefault = networkParametersHash ?: services.networkParametersService.defaultHash val hashOrDefault = networkParametersHash ?: services.networkParametersService.defaultHash
val txNetworkParameters = services.networkParametersService.lookup(hashOrDefault) val txNetworkParameters = services.networkParametersService.lookup(hashOrDefault) ?: throw TransactionResolutionException(id)
?: throw TransactionResolutionException(id)
val groupedInputsAndRefs = (inputs + references).groupBy { it.txhash } val groupedInputsAndRefs = (inputs + references).groupBy { it.txhash }
groupedInputsAndRefs.map { entry -> for ((txId, stateRefs) in groupedInputsAndRefs) {
val tx = services.validatedTransactions.getTransaction(entry.key)?.coreTransaction val tx = services.validatedTransactions.getTransaction(txId)?.coreTransaction ?: throw TransactionResolutionException(id)
?: throw TransactionResolutionException(id)
val paramHash = tx.networkParametersHash ?: services.networkParametersService.defaultHash val paramHash = tx.networkParametersHash ?: services.networkParametersService.defaultHash
val params = services.networkParametersService.lookup(paramHash) ?: throw TransactionResolutionException(id) val params = services.networkParametersService.lookup(paramHash) ?: throw TransactionResolutionException(id)
if (txNetworkParameters.epoch < params.epoch) if (txNetworkParameters.epoch < params.epoch) {
throw TransactionVerificationException.TransactionNetworkParameterOrderingException(id, entry.value.first(), txNetworkParameters, params) throw TransactionNetworkParameterOrderingException(id, stateRefs.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 ?: "<unknown>", 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. private fun verifySignatures(verificationSupport: NodeVerificationSupport, checkSufficientSignatures: Boolean) {
// This code has detected a missing custom serializer - probably located inside a workflow CorDapp. if (checkSufficientSignatures) {
// We need to extract this CorDapp from AttachmentStorage and try verifying this transaction again. val ctx = coreTransaction
private fun reverifyWithFixups(ltx: LedgerTransaction, verificationSupport: VerificationSupport, missingClass: String?) { val tws: TransactionWithSignatures = when (ctx) {
log.warn("""Detected that transaction $id does not contain all cordapp dependencies. is WireTransaction -> this // SignedTransaction implements TransactionWithSignatures in terms of WireTransaction
|This may be the result of a bug in a previous version of Corda. else -> CoreTransactionWithSignatures(ctx, sigs, verificationSupport)
|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<Attachment> {
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"
)
} }
tws.verifyRequiredSignatures() // Internally checkSignaturesAreValid is invoked
log.warn("""Detected that transaction $ltx does not contain all cordapp dependencies. } else {
|This may be the result of a bug in a previous version of Corda. checkSignaturesAreValid()
|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<Attachment>): Collection<Attachment> {
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 * Resolves the underlying base transaction and then returns it, handling any special case transactions such as
* [NotaryChangeWireTransaction]. * [NotaryChangeWireTransaction].
@ -512,7 +292,7 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
return ctx.resolve(services, sigs) return ctx.resolve(services, sigs)
} }
override fun toString(): String = "${javaClass.simpleName}(id=$id)" override fun toString(): String = toSimpleString()
private companion object { private companion object {
private fun missingSignatureMsg(missing: Set<PublicKey>, descriptions: List<String>, id: SecureHash): String { private fun missingSignatureMsg(missing: Set<PublicKey>, descriptions: List<String>, id: SecureHash): String {
@ -520,13 +300,28 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
"keys: ${missing.joinToString { it.toStringShort() }}, " + "keys: ${missing.joinToString { it.toStringShort() }}, " +
"by signers: ${descriptions.joinToString()} " "by signers: ${descriptions.joinToString()} "
} }
private val log = contextLogger()
} }
class SignaturesMissingException(val missing: Set<PublicKey>, val descriptions: List<String>, override val id: SecureHash) class SignaturesMissingException(val missing: Set<PublicKey>, val descriptions: List<String>, override val id: SecureHash)
: NamedByHash, SignatureException(missingSignatureMsg(missing, descriptions, id)), CordaThrowable by CordaException(missingSignatureMsg(missing, descriptions, id)) : 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<TransactionSignature>,
private val verificationSupport: NodeVerificationSupport
) : TransactionWithSignatures, NamedByHash by ctx {
override val requiredSigningKeys: Set<PublicKey>
get() = getRequiredSigningKeysInternal(ctx.inputs.asSequence().map(verificationSupport::getStateAndRef), ctx.notary)
override fun getKeyDescriptions(keys: Set<PublicKey>): List<String> = keys.map { it.toBase58String() }
override fun toString(): String = toSimpleString()
}
//region Deprecated //region Deprecated
/** Returns the contained [NotaryChangeWireTransaction], or throws if this is a normal transaction. */ /** Returns the contained [NotaryChangeWireTransaction], or throws if this is a normal transaction. */
@Deprecated("No replacement, this should not be used outside of Corda core") @Deprecated("No replacement, this should not be used outside of Corda core")

View File

@ -25,6 +25,7 @@ import net.corda.core.serialization.SerializationFactory
import net.corda.core.serialization.SerializationMagic import net.corda.core.serialization.SerializationMagic
import net.corda.core.serialization.SerializationSchemeContext import net.corda.core.serialization.SerializationSchemeContext
import net.corda.core.serialization.internal.CustomSerializationSchemeUtils.Companion.getCustomSerializationMagicFromSchemeId import net.corda.core.serialization.internal.CustomSerializationSchemeUtils.Companion.getCustomSerializationMagicFromSchemeId
import net.corda.core.utilities.Try.Failure
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import java.security.PublicKey import java.security.PublicKey
import java.time.Duration import java.time.Duration
@ -89,6 +90,7 @@ open class TransactionBuilder(
private val inputsWithTransactionState = arrayListOf<StateAndRef<ContractState>>() private val inputsWithTransactionState = arrayListOf<StateAndRef<ContractState>>()
private val referencesWithTransactionState = arrayListOf<TransactionState<ContractState>>() private val referencesWithTransactionState = arrayListOf<TransactionState<ContractState>>()
private var excludedAttachments: Set<AttachmentId> = emptySet() private var excludedAttachments: Set<AttachmentId> = emptySet()
private var extraLegacyAttachments: MutableSet<AttachmentId>? = null
/** /**
* Creates a copy of the builder. * Creates a copy of the builder.
@ -196,20 +198,26 @@ open class TransactionBuilder(
val wireTx = SerializationFactory.defaultFactory.withCurrentContext(serializationContext) { val wireTx = SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
// Sort the attachments to ensure transaction builds are stable. // Sort the attachments to ensure transaction builds are stable.
val attachmentsBuilder = allContractAttachments.mapTo(TreeSet()) { it.currentAttachment.id } val nonLegacyAttachments = allContractAttachments.mapTo(TreeSet()) { it.currentAttachment.id }.apply {
attachmentsBuilder.addAll(attachments) addAll(attachments)
attachmentsBuilder.removeAll(excludedAttachments) removeAll(excludedAttachments)
}.toList()
val legacyAttachments = allContractAttachments.mapNotNullTo(TreeSet()) { it.legacyAttachment?.id }.apply {
if (extraLegacyAttachments != null) {
addAll(extraLegacyAttachments!!)
}
}.toList()
WireTransaction( WireTransaction(
createComponentGroups( createComponentGroups(
inputStates(), inputStates(),
resolvedOutputs, resolvedOutputs,
commands(), commands(),
attachmentsBuilder.toList(), nonLegacyAttachments,
notary, notary,
window, window,
referenceStates, referenceStates,
serviceHub.networkParametersService.currentHash, serviceHub.networkParametersService.currentHash,
allContractAttachments.mapNotNullTo(TreeSet()) { it.legacyAttachment?.id }.toList() legacyAttachments
), ),
privacySalt, privacySalt,
serviceHub.digestService 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. * @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 { private fun addMissingDependency(serviceHub: VerifyingServiceHub, wireTx: WireTransaction, tryCount: Int): Boolean {
return try { val verificationResult = wireTx.tryVerify(serviceHub)
wireTx.toLedgerTransactionInternal(serviceHub).verify() // Check both legacy and non-legacy components are working, and try to add any missing dependencies if either are not.
// The transaction verified successfully without adding any extra dependency. (verificationResult.inProcessResult as? Failure)?.let { (inProcessException) ->
false return addMissingDependency(inProcessException, wireTx, false, serviceHub, tryCount)
} catch (e: Throwable) { }
val rootError = e.rootClassNotFoundCause(ClassNotFoundException::class, NoClassDefFoundError::class) (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 { private fun addMissingDependency(e: Throwable, wireTx: WireTransaction, isLegacy: Boolean, serviceHub: VerifyingServiceHub, tryCount: Int): Boolean {
// Handle various exceptions that can be thrown during verification and drill down the wrappings. val missingClass = extractMissingClass(e)
// Note: this is a best effort to preserve backwards compatibility. if (log.isDebugEnabled) {
rootError is ClassNotFoundException -> { log.debug("Checking if transaction has missing attachment (missingClass=$missingClass) (legacy=$isLegacy) $wireTx", e)
// Using nonLegacyAttachments here as the verification above was done in-process and thus only the nonLegacyAttachments }
// are used. return when {
// TODO This might change with ENT-11445 where we add support for legacy contract dependencies. missingClass != null -> {
((tryCount == 0) && fixupAttachments(wireTx.nonLegacyAttachments, serviceHub, e)) val attachments = if (isLegacy) wireTx.legacyAttachments else wireTx.nonLegacyAttachments
|| addMissingAttachment((rootError.message ?: throw e).replace('.', '/'), serviceHub, e) (tryCount == 0 && fixupAttachments(attachments, serviceHub, e)) || addMissingAttachment(missingClass, isLegacy, serviceHub, e)
} }
rootError is NoClassDefFoundError -> { // Ignore these exceptions as they will break unit tests.
((tryCount == 0) && fixupAttachments(wireTx.nonLegacyAttachments, serviceHub, e)) // The point here is only to detect missing dependencies. The other exceptions are irrelevant.
|| addMissingAttachment(rootError.message ?: throw e, serviceHub, e) e is TransactionVerificationException -> false
} e is TransactionResolutionException -> false
e is IllegalStateException -> false
// Ignore these exceptions as they will break unit tests. e is IllegalArgumentException -> false
// The point here is only to detect missing dependencies. The other exceptions are irrelevant. // Fail early if none of the expected scenarios were hit.
e is TransactionVerificationException -> false else -> {
e is TransactionResolutionException -> false log.error("""The transaction currently built will not validate because of an unknown error most likely caused by a
e is IllegalStateException -> false missing dependency in the transaction attachments.
e is IllegalArgumentException -> false Please contact the developer of the CorDapp for further instructions.
""".trimIndent(), e)
// Fail early if none of the expected scenarios were hit. throw e
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<out Throwable>): String? {
return substringAfterLast("${exceptionClass.java.name}: ", "").takeIf { it.isNotEmpty() }
}
private fun fixupAttachments( private fun fixupAttachments(
txAttachments: List<AttachmentId>, txAttachments: List<AttachmentId>,
serviceHub: VerifyingServiceHub, serviceHub: VerifyingServiceHub,
@ -314,7 +334,7 @@ open class TransactionBuilder(
return true 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)) { if (!isValidJavaClass(missingClass)) {
log.warn("Could not autodetect a valid attachment for the transaction being built.") log.warn("Could not autodetect a valid attachment for the transaction being built.")
throw originalException throw originalException
@ -323,7 +343,14 @@ open class TransactionBuilder(
throw originalException 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) { if (attachment == null) {
log.error("""The transaction currently built is missing an attachment for class: $missingClass. 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. Please contact the developer of the CorDapp and install the latest version, as this approach might be insecure.
""".trimIndent()) """.trimIndent())
addAttachment(attachment.id) if (isLegacy) {
(extraLegacyAttachments ?: LinkedHashSet<AttachmentId>().also { extraLegacyAttachments = it }) += attachment.id
} else {
addAttachment(attachment.id)
}
return true return true
} }

View File

@ -4,6 +4,7 @@ import net.corda.core.DoNotImplement
import net.corda.core.contracts.NamedByHash import net.corda.core.contracts.NamedByHash
import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.TransactionSignature
import net.corda.core.crypto.isFulfilledBy import net.corda.core.crypto.isFulfilledBy
import net.corda.core.internal.mapToSet
import net.corda.core.transactions.SignedTransaction.SignaturesMissingException import net.corda.core.transactions.SignedTransaction.SignaturesMissingException
import net.corda.core.utilities.toNonEmptySet import net.corda.core.utilities.toNonEmptySet
import java.security.InvalidKeyException import java.security.InvalidKeyException
@ -99,9 +100,9 @@ interface TransactionWithSignatures : NamedByHash {
* Return the [PublicKey]s for which we still need signatures. * Return the [PublicKey]s for which we still need signatures.
*/ */
fun getMissingSigners(): Set<PublicKey> { fun getMissingSigners(): Set<PublicKey> {
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 // 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?) // 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()
} }
} }

View File

@ -15,6 +15,7 @@ import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TimeWindow import net.corda.core.contracts.TimeWindow
import net.corda.core.contracts.TransactionResolutionException import net.corda.core.contracts.TransactionResolutionException
import net.corda.core.contracts.TransactionState import net.corda.core.contracts.TransactionState
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.crypto.DigestService import net.corda.core.crypto.DigestService
import net.corda.core.crypto.MerkleTree import net.corda.core.crypto.MerkleTree
import net.corda.core.crypto.SecureHash 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.Emoji
import net.corda.core.internal.SerializedStateAndRef import net.corda.core.internal.SerializedStateAndRef
import net.corda.core.internal.SerializedTransactionState import net.corda.core.internal.SerializedTransactionState
import net.corda.core.internal.TransactionDeserialisationException
import net.corda.core.internal.createComponentGroups import net.corda.core.internal.createComponentGroups
import net.corda.core.internal.deserialiseComponentGroup import net.corda.core.internal.deserialiseComponentGroup
import net.corda.core.internal.equivalent
import net.corda.core.internal.flatMapToSet import net.corda.core.internal.flatMapToSet
import net.corda.core.internal.getGroup import net.corda.core.internal.getGroup
import net.corda.core.internal.isUploaderTrusted import net.corda.core.internal.isUploaderTrusted
import net.corda.core.internal.lazyMapped import net.corda.core.internal.lazyMapped
import net.corda.core.internal.mapToSet import net.corda.core.internal.mapToSet
import net.corda.core.internal.toSimpleString
import net.corda.core.internal.uncheckedCast 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.VerificationSupport
import net.corda.core.internal.verification.toVerifyingServiceHub import net.corda.core.internal.verification.toVerifyingServiceHub
import net.corda.core.node.NetworkParameters 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.node.services.AttachmentId
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.DeprecatedConstructorForDeserialization import net.corda.core.serialization.DeprecatedConstructorForDeserialization
import net.corda.core.serialization.MissingAttachmentsException
import net.corda.core.serialization.SerializationFactory import net.corda.core.serialization.SerializationFactory
import net.corda.core.serialization.internal.MissingSerializerException
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.utilities.OpaqueBytes 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.PublicKey
import java.security.SignatureException import java.security.SignatureException
import java.util.function.Predicate import java.util.function.Predicate
@ -71,7 +86,7 @@ import java.util.function.Predicate
* </ul></p> * </ul></p>
*/ */
@CordaSerializable @CordaSerializable
@Suppress("ThrowsCount") @Suppress("ThrowsCount", "TooManyFunctions", "MagicNumber")
class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: PrivacySalt, digestService: DigestService) : TraversableTransaction(componentGroups, digestService) { class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: PrivacySalt, digestService: DigestService) : TraversableTransaction(componentGroups, digestService) {
constructor(componentGroups: List<ComponentGroup>) : this(componentGroups, PrivacySalt()) constructor(componentGroups: List<ComponentGroup>) : this(componentGroups, PrivacySalt())
@ -164,14 +179,14 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
} }
// These are not used // These are not used
override val appClassLoader: ClassLoader get() = throw AbstractMethodError() 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<SecureHash>) = throw AbstractMethodError() override fun fixupAttachmentIds(attachmentIds: Collection<SecureHash>) = throw AbstractMethodError()
}) })
} }
@CordaInternal @CordaInternal
@JvmSynthetic @JvmSynthetic
fun toLedgerTransactionInternal(verificationSupport: VerificationSupport): LedgerTransaction { internal fun toLedgerTransactionInternal(verificationSupport: VerificationSupport): LedgerTransaction {
// Look up public keys to authenticated identities. // Look up public keys to authenticated identities.
val authenticatedCommands = if (verificationSupport.isInProcess) { val authenticatedCommands = if (verificationSupport.isInProcess) {
commands.lazyMapped { cmd, _ -> commands.lazyMapped { cmd, _ ->
@ -360,43 +375,211 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
sig.verify(id) 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 ?: "<unknown>", 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<Attachment> {
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<Attachment>): Collection<Attachment> {
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 { override fun toString(): String {
val buf = StringBuilder() val buf = StringBuilder(1024)
buf.appendLine("Transaction:") buf.appendLine("Transaction $id:")
for (reference in references) { for (reference in references) {
val emoji = Emoji.rightArrow val emoji = Emoji.rightArrow
buf.appendLine("${emoji}REFS: $reference") buf.appendLine("${emoji}REFS: $reference")
} }
for (input in inputs) { for (input in inputs) {
val emoji = Emoji.rightArrow val emoji = Emoji.rightArrow
buf.appendLine("${emoji}INPUT: $input") buf.appendLine("${emoji}INPUT: $input")
} }
for ((data) in outputs) { for ((data) in outputs) {
val emoji = Emoji.leftArrow val emoji = Emoji.leftArrow
buf.appendLine("${emoji}OUTPUT: $data") buf.appendLine("${emoji}OUTPUT: $data")
} }
for (command in commands) { for (command in commands) {
val emoji = Emoji.diamond 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 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) { if (networkParametersHash != null) {
buf.appendLine("PARAMETERS HASH: $networkParametersHash") val emoji = Emoji.newspaper
buf.appendLine("${emoji}NETWORK PARAMS: $networkParametersHash")
} }
return buf.toString() return buf.toString()
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean = other is WireTransaction && this.id == other.id
if (other is WireTransaction) {
return (this.id == other.id)
}
return false
}
override fun hashCode(): Int = id.hashCode() override fun hashCode(): Int = id.hashCode()
private companion object {
private val log = contextLogger()
}
} }
/** /**

View File

@ -205,8 +205,8 @@ class JarScanningCordappLoader(private val cordappJars: Set<Path>,
"corresponding newer version (4.12 or later). Please add this corresponding CorDapp or remove the legacy one." "corresponding newer version (4.12 or later). Please add this corresponding CorDapp or remove the legacy one."
} }
check(newerCordapp.contractVersionId > legacyCordapp.contractVersionId) { check(newerCordapp.contractVersionId > legacyCordapp.contractVersionId) {
"Newer contract CorDapp '${newerCordapp.jarFile}' does not have a higher version number " + "Newer contract CorDapp '${newerCordapp.jarFile}' does not have a higher versionId " +
"(${newerCordapp.contractVersionId}) compared to corresponding legacy contract CorDapp " + "(${newerCordapp.contractVersionId}) than corresponding legacy contract CorDapp " +
"'${legacyCordapp.jarFile}' (${legacyCordapp.contractVersionId})" "'${legacyCordapp.jarFile}' (${legacyCordapp.contractVersionId})"
} }
} }

View File

@ -6,10 +6,11 @@ import net.corda.core.internal.copyTo
import net.corda.core.internal.level import net.corda.core.internal.level
import net.corda.core.internal.mapToSet import net.corda.core.internal.mapToSet
import net.corda.core.internal.readFully 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.ExternalVerifierHandle
import net.corda.core.internal.verification.NodeVerificationSupport import net.corda.core.internal.verification.NodeVerificationSupport
import net.corda.core.serialization.serialize 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.Try
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug 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.Initialisation
import net.corda.serialization.internal.verifier.ExternalVerifierInbound.NetworkParametersResult import net.corda.serialization.internal.verifier.ExternalVerifierInbound.NetworkParametersResult
import net.corda.serialization.internal.verifier.ExternalVerifierInbound.PartiesResult 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.ExternalVerifierInbound.VerificationRequest
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound import net.corda.serialization.internal.verifier.ExternalVerifierOutbound
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerificationResult 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.GetAttachments
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetNetworkParameters 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.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.readCordaSerializable
import net.corda.serialization.internal.verifier.writeCordaSerializable import net.corda.serialization.internal.verifier.writeCordaSerializable
import java.io.DataInputStream import java.io.DataInputStream
@ -74,13 +75,13 @@ class ExternalVerifierHandleImpl(
@Volatile @Volatile
private var connection: Connection? = null private var connection: Connection? = null
override fun verifyTransaction(stx: SignedTransaction, checkSufficientSignatures: Boolean) { override fun verifyTransaction(ctx: CoreTransaction) {
log.info("Verify $stx externally, checkSufficientSignatures=$checkSufficientSignatures") 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. // 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 // 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. // correct, and there's a benefit, then we can send them lazily.
val stxInputsAndReferences = (stx.inputs + stx.references).associateWith(verificationSupport::getSerializedState) val ctxInputsAndReferences = (ctx.inputs + ctx.references).associateWith(verificationSupport::getSerializedState)
val request = VerificationRequest(stx, stxInputsAndReferences, checkSufficientSignatures) val request = VerificationRequest(ctx, ctxInputsAndReferences)
// To keep things simple the verifier only supports one verification request at a time. // To keep things simple the verifier only supports one verification request at a time.
synchronized(this) { synchronized(this) {
@ -146,23 +147,22 @@ class ExternalVerifierHandleImpl(
private fun processVerifierRequest(request: VerifierRequest, connection: Connection) { private fun processVerifierRequest(request: VerifierRequest, connection: Connection) {
val result = when (request) { val result = when (request) {
is GetParties -> PartiesResult(verificationSupport.getParties(request.keys)) is GetParties -> PartiesResult(verificationSupport.getParties(request.keys))
is GetAttachment -> AttachmentResult(prepare(verificationSupport.getAttachment(request.id))) is GetAttachment -> AttachmentResult(verificationSupport.getAttachment(request.id)?.withTrust())
is GetAttachments -> AttachmentsResult(verificationSupport.getAttachments(request.ids).map(::prepare)) is GetAttachments -> AttachmentsResult(verificationSupport.getAttachments(request.ids).map { it?.withTrust() })
is GetNetworkParameters -> NetworkParametersResult(verificationSupport.getNetworkParameters(request.id)) 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" } log.debug { "Sending response to external verifier: $result" }
connection.toVerifier.writeCordaSerializable(result) connection.toVerifier.writeCordaSerializable(result)
} }
private fun prepare(attachment: Attachment?): AttachmentWithTrust? { private fun Attachment.withTrust(): AttachmentWithTrust {
if (attachment == null) return null val isTrusted = verificationSupport.isAttachmentTrusted(this)
val isTrusted = verificationSupport.isAttachmentTrusted(attachment) val attachmentForSer = when (this) {
val attachmentForSer = when (attachment) {
// The Attachment retrieved from the database is not serialisable, so we have to convert it into one // 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 // For everything else we keep as is, in particular preserving ContractAttachment
else -> attachment else -> this
} }
return AttachmentWithTrust(attachmentForSer, isTrusted) return AttachmentWithTrust(attachmentForSer, isTrusted)
} }

View File

@ -11,7 +11,7 @@ import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize 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.Try
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
@ -40,16 +40,17 @@ sealed class ExternalVerifierInbound {
} }
data class VerificationRequest( data class VerificationRequest(
val stx: SignedTransaction, val ctx: CoreTransaction,
val stxInputsAndReferences: Map<StateRef, SerializedTransactionState>, val ctxInputsAndReferences: Map<StateRef, SerializedTransactionState>
val checkSufficientSignatures: Boolean ) : ExternalVerifierInbound() {
) : ExternalVerifierInbound() override fun toString(): String = "VerificationRequest(ctx=$ctx)"
}
data class PartiesResult(val parties: List<Party?>) : ExternalVerifierInbound() data class PartiesResult(val parties: List<Party?>) : ExternalVerifierInbound()
data class AttachmentResult(val attachment: AttachmentWithTrust?) : ExternalVerifierInbound() data class AttachmentResult(val attachment: AttachmentWithTrust?) : ExternalVerifierInbound()
data class AttachmentsResult(val attachments: List<AttachmentWithTrust?>) : ExternalVerifierInbound() data class AttachmentsResult(val attachments: List<AttachmentWithTrust?>) : ExternalVerifierInbound()
data class NetworkParametersResult(val networkParameters: NetworkParameters?) : ExternalVerifierInbound() data class NetworkParametersResult(val networkParameters: NetworkParameters?) : ExternalVerifierInbound()
data class TrustedClassAttachmentResult(val id: SecureHash?) : ExternalVerifierInbound() data class TrustedClassAttachmentsResult(val ids: List<SecureHash>) : ExternalVerifierInbound()
} }
@CordaSerializable @CordaSerializable
@ -59,12 +60,12 @@ data class AttachmentWithTrust(val attachment: Attachment, val isTrusted: Boolea
sealed class ExternalVerifierOutbound { sealed class ExternalVerifierOutbound {
sealed class VerifierRequest : ExternalVerifierOutbound() { sealed class VerifierRequest : ExternalVerifierOutbound() {
data class GetParties(val keys: Set<PublicKey>) : VerifierRequest() { data class GetParties(val keys: Set<PublicKey>) : 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 GetAttachment(val id: SecureHash) : VerifierRequest()
data class GetAttachments(val ids: Set<SecureHash>) : VerifierRequest() data class GetAttachments(val ids: Set<SecureHash>) : VerifierRequest()
data class GetNetworkParameters(val id: SecureHash) : 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<Unit>) : ExternalVerifierOutbound() data class VerificationResult(val result: Try<Unit>) : ExternalVerifierOutbound()

View File

@ -28,8 +28,8 @@ class ExternalVerificationContext(
override fun isAttachmentTrusted(attachment: Attachment): Boolean = externalVerifier.getAttachment(attachment.id)!!.isTrusted override fun isAttachmentTrusted(attachment: Attachment): Boolean = externalVerifier.getAttachment(attachment.id)!!.isTrusted
override fun getTrustedClassAttachment(className: String): Attachment? { override fun getTrustedClassAttachments(className: String): List<Attachment> {
return externalVerifier.getTrustedClassAttachment(className) return externalVerifier.getTrustedClassAttachments(className)
} }
override fun getNetworkParameters(id: SecureHash?): NetworkParameters? = externalVerifier.getNetworkParameters(id) override fun getNetworkParameters(id: SecureHash?): NetworkParameters? = externalVerifier.getNetworkParameters(id)

View File

@ -7,6 +7,7 @@ import net.corda.core.identity.Party
import net.corda.core.internal.loadClassOfType import net.corda.core.internal.loadClassOfType
import net.corda.core.internal.mapToSet import net.corda.core.internal.mapToSet
import net.corda.core.internal.objectOrNewInstance import net.corda.core.internal.objectOrNewInstance
import net.corda.core.internal.toSimpleString
import net.corda.core.internal.toSynchronised import net.corda.core.internal.toSynchronised
import net.corda.core.internal.toTypedArray import net.corda.core.internal.toTypedArray
import net.corda.core.internal.verification.AttachmentFixups 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.AttachmentsClassLoaderCacheImpl
import net.corda.core.serialization.internal.SerializationEnvironment import net.corda.core.serialization.internal.SerializationEnvironment
import net.corda.core.serialization.internal._contextSerializationEnv 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.Try
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug 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.Initialisation
import net.corda.serialization.internal.verifier.ExternalVerifierInbound.NetworkParametersResult import net.corda.serialization.internal.verifier.ExternalVerifierInbound.NetworkParametersResult
import net.corda.serialization.internal.verifier.ExternalVerifierInbound.PartiesResult 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.ExternalVerifierInbound.VerificationRequest
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerificationResult 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.GetAttachment
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetAttachments 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.GetNetworkParameters
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetParties 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.loadCustomSerializationScheme
import net.corda.serialization.internal.verifier.readCordaSerializable import net.corda.serialization.internal.verifier.readCordaSerializable
import net.corda.serialization.internal.verifier.writeCordaSerializable import net.corda.serialization.internal.verifier.writeCordaSerializable
@ -68,7 +71,7 @@ class ExternalVerifier(
private val parties: OptionalCache<PublicKey, Party> private val parties: OptionalCache<PublicKey, Party>
private val attachments: OptionalCache<SecureHash, AttachmentWithTrust> private val attachments: OptionalCache<SecureHash, AttachmentWithTrust>
private val networkParametersMap: OptionalCache<SecureHash, NetworkParameters> private val networkParametersMap: OptionalCache<SecureHash, NetworkParameters>
private val trustedClassAttachments: OptionalCache<String, SecureHash> private val trustedClassAttachments: Cache<String, List<SecureHash>>
private lateinit var appClassLoader: ClassLoader private lateinit var appClassLoader: ClassLoader
private lateinit var currentNetworkParameters: NetworkParameters private lateinit var currentNetworkParameters: NetworkParameters
@ -134,13 +137,18 @@ class ExternalVerifier(
@Suppress("INVISIBLE_MEMBER") @Suppress("INVISIBLE_MEMBER")
private fun verifyTransaction(request: VerificationRequest) { private fun verifyTransaction(request: VerificationRequest) {
val verificationContext = ExternalVerificationContext(appClassLoader, attachmentsClassLoaderCache, this, request.stxInputsAndReferences) val verificationContext = ExternalVerificationContext(appClassLoader, attachmentsClassLoaderCache, this, request.ctxInputsAndReferences)
val result: Try<Unit> = try { val result: Try<Unit> = try {
request.stx.verifyInProcess(verificationContext, request.checkSufficientSignatures) val ctx = request.ctx
log.info("${request.stx} verified") 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) Try.Success(Unit)
} catch (t: Throwable) { } catch (t: Throwable) {
log.info("${request.stx} failed to verify", t) log.info("${request.ctx.toSimpleString()} failed to verify", t)
Try.Failure(t) Try.Failure(t)
} }
toNode.writeCordaSerializable(VerificationResult(result)) toNode.writeCordaSerializable(VerificationResult(result))
@ -164,13 +172,13 @@ class ExternalVerifier(
} }
} }
fun getTrustedClassAttachment(className: String): Attachment? { fun getTrustedClassAttachments(className: String): List<Attachment> {
val attachmentId = trustedClassAttachments.retrieve(className) { val attachmentIds = trustedClassAttachments.get(className) {
// GetTrustedClassAttachment returns back the attachment ID, not the whole attachment. This lets us avoid downloading the whole // GetTrustedClassAttachments returns back the attachment IDs, not the whole attachments. This lets us avoid downloading the
// attachment again if we already have it. // entire attachments again if we already have them.
request<TrustedClassAttachmentResult>(GetTrustedClassAttachment(className)).id request<TrustedClassAttachmentsResult>(GetTrustedClassAttachments(className)).ids
} }!!
return attachmentId?.let(::getAttachment)?.attachment return attachmentIds.map { getAttachment(it)!!.attachment }
} }
fun getNetworkParameters(id: SecureHash?): NetworkParameters? { fun getNetworkParameters(id: SecureHash?): NetworkParameters? {