mirror of
https://github.com/corda/corda.git
synced 2024-12-22 22:32:26 +00:00
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:
parent
5b8fc6f503
commit
b3265314ce
.ci
core-tests
build.gradle
src
integration-test/kotlin/net/corda/coretests/transactions
smoke-test/kotlin/net/corda/coretests/verification
test/kotlin/net/corda/coretests/transactions
core/src/main/kotlin/net/corda/core
contracts
flows
internal
node/services
transactions
node/src/main/kotlin/net/corda/node
serialization/src/main/kotlin/net/corda/serialization/internal/verifier
verifier/src/main/kotlin/net/corda/verifier
@ -552,8 +552,6 @@ public interface net.corda.core.contracts.Attachment extends net.corda.core.cont
|
||||
public interface net.corda.core.contracts.AttachmentConstraint
|
||||
public abstract boolean isSatisfiedBy(net.corda.core.contracts.Attachment)
|
||||
##
|
||||
public final class net.corda.core.contracts.AttachmentConstraintKt extends java.lang.Object
|
||||
##
|
||||
@CordaSerializable
|
||||
public final class net.corda.core.contracts.AttachmentResolutionException extends net.corda.core.flows.FlowException
|
||||
public <init>(net.corda.core.crypto.SecureHash)
|
||||
|
@ -57,55 +57,61 @@ processSmokeTestResources {
|
||||
from(configurations.corda4_11)
|
||||
}
|
||||
|
||||
processIntegrationTestResources {
|
||||
from(tasks.getByPath(":finance:contracts:jar")) {
|
||||
rename 'corda-finance-contracts-.*.jar', 'corda-finance-contracts.jar'
|
||||
}
|
||||
from(configurations.corda4_11)
|
||||
}
|
||||
|
||||
processTestResources {
|
||||
from(configurations.corda4_11)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}"
|
||||
testImplementation "junit:junit:$junit_version"
|
||||
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${junit_vintage_version}"
|
||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junit_jupiter_version}"
|
||||
testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}"
|
||||
|
||||
testImplementation "commons-fileupload:commons-fileupload:$fileupload_version"
|
||||
testImplementation project(":core")
|
||||
testImplementation project(path: ':core', configuration: 'testArtifacts')
|
||||
|
||||
testImplementation project(":serialization")
|
||||
testImplementation project(":finance:contracts")
|
||||
testImplementation project(":finance:workflows")
|
||||
testImplementation project(":node")
|
||||
testImplementation project(":node-api")
|
||||
testImplementation project(":client:rpc")
|
||||
testImplementation project(":serialization")
|
||||
testImplementation project(":common-configuration-parsing")
|
||||
testImplementation project(":finance:contracts")
|
||||
testImplementation project(":finance:workflows")
|
||||
testImplementation project(":core-test-utils")
|
||||
testImplementation project(":test-utils")
|
||||
testImplementation project(path: ':core', configuration: 'testArtifacts')
|
||||
|
||||
testImplementation project(":node-driver")
|
||||
// used by FinalityFlowTests
|
||||
testImplementation project(':testing:cordapps:cashobservers')
|
||||
testImplementation(project(path: ':core', configuration: 'testArtifacts')) {
|
||||
transitive = false
|
||||
}
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}"
|
||||
testImplementation "junit:junit:$junit_version"
|
||||
testImplementation "commons-fileupload:commons-fileupload:$fileupload_version"
|
||||
// Guava: Google test library (collections test suite)
|
||||
testImplementation "com.google.guava:guava-testlib:$guava_version"
|
||||
testImplementation "com.google.jimfs:jimfs:1.1"
|
||||
testImplementation group: "com.typesafe", name: "config", version: typesafe_config_version
|
||||
|
||||
// Bring in the MockNode infrastructure for writing protocol unit tests.
|
||||
testImplementation project(":node-driver")
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
testImplementation "com.typesafe:config:$typesafe_config_version"
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
|
||||
// Hamkrest, for fluent, composable matchers
|
||||
testImplementation "com.natpryce:hamkrest:$hamkrest_version"
|
||||
|
||||
// SLF4J: commons-logging bindings for a SLF4J back end
|
||||
implementation "org.slf4j:jcl-over-slf4j:$slf4j_version"
|
||||
implementation "org.slf4j:slf4j-api:$slf4j_version"
|
||||
|
||||
testImplementation 'org.hamcrest:hamcrest-library:2.1'
|
||||
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockito_kotlin_version"
|
||||
testImplementation "org.mockito:mockito-core:$mockito_version"
|
||||
// AssertJ: for fluent assertions for testing
|
||||
testImplementation "org.assertj:assertj-core:${assertj_version}"
|
||||
|
||||
// Guava: Google utilities library.
|
||||
testImplementation "com.google.guava:guava:$guava_version"
|
||||
testImplementation "com.esotericsoftware:kryo:$kryo_version"
|
||||
testImplementation "co.paralleluniverse:quasar-core:$quasar_version"
|
||||
testImplementation "org.hibernate:hibernate-core:$hibernate_version"
|
||||
testImplementation "org.bouncycastle:bcprov-jdk18on:${bouncycastle_version}"
|
||||
testImplementation "io.netty:netty-common:$netty_version"
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
|
||||
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${junit_vintage_version}"
|
||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junit_jupiter_version}"
|
||||
testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}"
|
||||
|
||||
// Smoke tests do NOT have any Node code on the classpath!
|
||||
smokeTestImplementation project(":core")
|
||||
@ -127,9 +133,6 @@ dependencies {
|
||||
smokeTestRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}"
|
||||
smokeTestRuntimeOnly "org.slf4j:slf4j-simple:$slf4j_version"
|
||||
|
||||
// used by FinalityFlowTests
|
||||
testImplementation project(':testing:cordapps:cashobservers')
|
||||
|
||||
corda4_11 "net.corda:corda-finance-contracts:4.11"
|
||||
corda4_11 "net.corda:corda-finance-workflows:4.11"
|
||||
corda4_11 "net.corda:corda:4.11"
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -213,7 +213,7 @@ class ExternalVerificationUnsignedCordappsTest {
|
||||
).returnValue.getOrThrow()
|
||||
}
|
||||
|
||||
assertThat(newNode.externalVerifierLogs()).contains("$issuanceTx failed to verify")
|
||||
assertThat(newNode.externalVerifierLogs()).contains("WireTransaction(id=${issuanceTx.id}) failed to verify")
|
||||
}
|
||||
}
|
||||
|
||||
@ -265,10 +265,10 @@ private fun NodeProcess.assertTransactionsWereVerified(verificationType: Verific
|
||||
val nodeLogs = logs("node")!!
|
||||
val externalVerifierLogs = externalVerifierLogs()
|
||||
for (txId in txIds) {
|
||||
assertThat(nodeLogs).contains("Transaction $txId has verification type $verificationType")
|
||||
assertThat(nodeLogs).contains("WireTransaction(id=$txId) will be verified ${verificationType.logStatement}")
|
||||
if (verificationType != VerificationType.IN_PROCESS) {
|
||||
assertThat(externalVerifierLogs).describedAs("External verifier was not started").isNotNull()
|
||||
assertThat(externalVerifierLogs).contains("SignedTransaction(id=$txId) verified")
|
||||
assertThat(externalVerifierLogs).contains("WireTransaction(id=$txId) verified")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -283,5 +283,12 @@ private fun NodeProcess.logs(name: String): String? {
|
||||
}
|
||||
|
||||
private enum class VerificationType {
|
||||
IN_PROCESS, EXTERNAL, BOTH
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
@ -8,17 +8,13 @@ import net.corda.core.internal.copyToDirectory
|
||||
import net.corda.core.internal.hash
|
||||
import net.corda.core.internal.toPath
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.coretesting.internal.useZipFile
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.issuedBy
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.contracts.DummyState
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.DummyCommandData
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.signJar
|
||||
import net.corda.testing.core.internal.JarSignatureTestUtils.unsignJar
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP
|
||||
@ -28,18 +24,14 @@ import net.corda.testing.node.internal.cordappWithPackages
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||
import org.junit.After
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.copyTo
|
||||
import kotlin.io.path.createDirectories
|
||||
import kotlin.io.path.deleteExisting
|
||||
import kotlin.io.path.div
|
||||
import kotlin.io.path.inputStream
|
||||
import kotlin.io.path.listDirectoryEntries
|
||||
|
||||
@Suppress("INVISIBLE_MEMBER")
|
||||
class TransactionBuilderMockNetworkTest {
|
||||
@ -107,9 +99,9 @@ class TransactionBuilderMockNetworkTest {
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `populates legacy attachment group if legacy contract CorDapp is present`() {
|
||||
val node = mockNetwork.createNode {
|
||||
it.copyToLegacyContracts(legacyFinanceContractsJar)
|
||||
InternalMockNetwork.MockNode(it)
|
||||
val node = mockNetwork.createNode { args ->
|
||||
args.copyToLegacyContracts(legacyFinanceContractsJar)
|
||||
InternalMockNetwork.MockNode(args)
|
||||
}
|
||||
val builder = TransactionBuilder()
|
||||
val identity = node.info.singleIdentity()
|
||||
@ -120,45 +112,6 @@ class TransactionBuilderMockNetworkTest {
|
||||
stx.verify(node.services)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Ignore // https://r3-cev.atlassian.net/browse/ENT-11445
|
||||
fun `adds legacy CorDapp dependencies`() {
|
||||
val cordapp1 = tempFolder.newFile("cordapp1.jar").toPath()
|
||||
val cordapp2 = tempFolder.newFile("cordapp2.jar").toPath()
|
||||
// Split the contracts CorDapp into two
|
||||
legacyFinanceContractsJar.copyTo(cordapp1, overwrite = true)
|
||||
cordapp1.useZipFile { zipFs1 ->
|
||||
cordapp2.useZipFile { zipFs2 ->
|
||||
val destinationDir = zipFs2.getPath("net/corda/finance/contracts/asset").createDirectories()
|
||||
zipFs1.getPath("net/corda/finance/contracts/asset")
|
||||
.listDirectoryEntries("OnLedgerAsset*")
|
||||
.forEach {
|
||||
it.copyToDirectory(destinationDir)
|
||||
it.deleteExisting()
|
||||
}
|
||||
}
|
||||
}
|
||||
reSignJar(cordapp1)
|
||||
|
||||
val node = mockNetwork.createNode {
|
||||
it.copyToLegacyContracts(cordapp1, cordapp2)
|
||||
InternalMockNetwork.MockNode(it)
|
||||
}
|
||||
val builder = TransactionBuilder()
|
||||
val identity = node.info.singleIdentity()
|
||||
Cash().generateIssue(builder, 10.DOLLARS.issuedBy(identity.ref(0x00)), identity, mockNetwork.defaultNotaryIdentity)
|
||||
val stx = node.services.signInitialTransaction(builder)
|
||||
assertThat(stx.tx.nonLegacyAttachments).contains(FINANCE_CONTRACTS_CORDAPP.jarFile.hash)
|
||||
assertThat(stx.tx.legacyAttachments).contains(cordapp1.hash, cordapp2.hash)
|
||||
stx.verify(node.services)
|
||||
}
|
||||
|
||||
private fun reSignJar(jar: Path) {
|
||||
jar.unsignJar()
|
||||
tempFolder.root.toPath().generateKey("testAlias", "testPassword", ALICE_NAME.toString())
|
||||
tempFolder.root.toPath().signJar(jar.absolutePathString(), "testAlias", "testPassword")
|
||||
}
|
||||
|
||||
private fun MockNodeArgs.copyToLegacyContracts(vararg jars: Path) {
|
||||
val legacyContractsDir = (config.baseDirectory / "legacy-contracts").createDirectories()
|
||||
jars.forEach { it.copyToDirectory(legacyContractsDir) }
|
||||
|
@ -11,13 +11,12 @@ import net.corda.core.internal.utilities.Internable
|
||||
import net.corda.core.internal.utilities.PrivateInterner
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import java.lang.annotation.Inherited
|
||||
import java.security.PublicKey
|
||||
|
||||
private val log = loggerFor<AttachmentConstraint>()
|
||||
|
||||
/**
|
||||
* This annotation should only be added to [Contract] classes.
|
||||
* If the annotation is present, then we assume that [Contract.verify] will ensure that the output states have an acceptable constraint.
|
||||
@ -49,8 +48,11 @@ object AlwaysAcceptAttachmentConstraint : AttachmentConstraint {
|
||||
*/
|
||||
data class HashAttachmentConstraint(val attachmentId: SecureHash) : AttachmentConstraint {
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
|
||||
val disableHashConstraints = System.getProperty("net.corda.node.disableHashConstraints")?.toBoolean() ?: false
|
||||
}
|
||||
|
||||
override fun isSatisfiedBy(attachment: Attachment): Boolean {
|
||||
return if (attachment is AttachmentWithContext) {
|
||||
log.debug("Checking attachment uploader ${attachment.contractAttachment.uploader} is trusted")
|
||||
@ -68,6 +70,8 @@ data class HashAttachmentConstraint(val attachmentId: SecureHash) : AttachmentCo
|
||||
* It allows for centralized control over the cordapps that can be used.
|
||||
*/
|
||||
object WhitelistedByZoneAttachmentConstraint : AttachmentConstraint {
|
||||
private val log = loggerFor<WhitelistedByZoneAttachmentConstraint>()
|
||||
|
||||
override fun isSatisfiedBy(attachment: Attachment): Boolean {
|
||||
return if (attachment is AttachmentWithContext) {
|
||||
val whitelist = attachment.whitelistedContractImplementations
|
||||
@ -120,6 +124,8 @@ data class SignatureAttachmentConstraint(val key: PublicKey) : AttachmentConstra
|
||||
}
|
||||
|
||||
companion object : Internable<SignatureAttachmentConstraint> {
|
||||
private val log = contextLogger()
|
||||
|
||||
@CordaInternal
|
||||
override val interner = PrivateInterner<SignatureAttachmentConstraint>()
|
||||
|
||||
|
@ -450,7 +450,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction,
|
||||
// The notary signature(s) are allowed to be missing but no others.
|
||||
if (notary != null) transaction.verifySignaturesExcept(notary.owningKey) else transaction.verifyRequiredSignatures()
|
||||
// TODO= [CORDA-3267] Remove duplicate signature verification
|
||||
val ltx = transaction.verifyInternal(serviceHub.toVerifyingServiceHub(), checkSufficientSignatures = false) as LedgerTransaction?
|
||||
val ltx = transaction.verifyInternal(serviceHub.toVerifyingServiceHub(), checkSufficientSignatures = false)
|
||||
// verifyInternal returns null if the transaction was verified externally, which *could* happen on a very odd scenerio of a 4.11
|
||||
// node creating the transaction but a 4.12 kicking off finality. In that case, we still want a LedgerTransaction object for
|
||||
// recording to the vault, etc. Note that calling verify() on this will fail as it doesn't have the necessary non-legacy attachments
|
||||
|
@ -68,7 +68,7 @@ abstract class AbstractAttachment(dataLoader: () -> ByteArray, val uploader: Str
|
||||
|
||||
override fun equals(other: Any?) = other === this || other is Attachment && other.id == this.id
|
||||
override fun hashCode() = id.hashCode()
|
||||
override fun toString() = "${javaClass.simpleName}(id=$id)"
|
||||
override fun toString() = toSimpleString()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
|
@ -2,6 +2,7 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.contracts.NamedByHash
|
||||
import net.corda.core.contracts.TransactionResolutionException
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.DataVendingFlow
|
||||
@ -93,3 +94,5 @@ fun TransactionStorage.getRequiredTransaction(txhash: SecureHash): SignedTransac
|
||||
}
|
||||
|
||||
fun ServiceHub.getRequiredTransaction(txhash: SecureHash): SignedTransaction = validatedTransactions.getRequiredTransaction(txhash)
|
||||
|
||||
fun NamedByHash.toSimpleString(): String = "${javaClass.simpleName}(id=$id)"
|
||||
|
@ -148,8 +148,8 @@ fun <T> List<T>.indexOfOrThrow(item: T): Int {
|
||||
@Suppress("INVISIBLE_MEMBER", "RemoveExplicitTypeArguments") // Because the external verifier uses Kotlin 1.2
|
||||
inline fun <T, R> Collection<T>.mapToSet(transform: (T) -> R): Set<R> {
|
||||
return when (size) {
|
||||
0 -> return emptySet()
|
||||
1 -> return setOf(transform(first()))
|
||||
0 -> emptySet()
|
||||
1 -> setOf(transform(first()))
|
||||
else -> mapTo(LinkedHashSet<R>(mapCapacity(size)), transform)
|
||||
}
|
||||
}
|
||||
|
@ -166,8 +166,10 @@ fun deserialiseCommands(
|
||||
}
|
||||
val componentHashes = group.components.mapIndexed { index, component -> digestService.componentHash(group.nonces[index], component) }
|
||||
val leafIndices = componentHashes.map { group.partialMerkleTree.leafIndex(it) }
|
||||
if (leafIndices.isNotEmpty())
|
||||
if (leafIndices.isNotEmpty()) {
|
||||
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION") // Because the external verifier uses Kotlin 1.2
|
||||
check(leafIndices.max()!! < signersList.size) { "Invalid Transaction. A command with no corresponding signer detected" }
|
||||
}
|
||||
commandDataList.lazyMapped { commandData, index -> Command(commandData, signersList[leafIndices[index]]) }
|
||||
} else {
|
||||
// It is a WireTransaction
|
||||
@ -313,3 +315,14 @@ internal fun checkNotaryWhitelisted(ftx: FullTransaction) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getRequiredSigningKeysInternal(inputs: Sequence<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
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package net.corda.core.internal.verification
|
||||
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.CoreTransaction
|
||||
|
||||
interface ExternalVerifierHandle : AutoCloseable {
|
||||
fun verifyTransaction(stx: SignedTransaction, checkSufficientSignatures: Boolean)
|
||||
fun verifyTransaction(ctx: CoreTransaction)
|
||||
}
|
||||
|
@ -128,25 +128,17 @@ interface NodeVerificationSupport : VerificationSupport {
|
||||
|
||||
/**
|
||||
* Scans trusted (installed locally) attachments to find all that contain the [className].
|
||||
* This is required as a workaround until explicit cordapp dependencies are implemented.
|
||||
*
|
||||
* @return the attachments with the highest version.
|
||||
* @return attachments containing the given class in descending version order. This means any legacy attachments will occur after the
|
||||
* current version one.
|
||||
*/
|
||||
// TODO Should throw when the class is found in multiple contract attachments (not different versions).
|
||||
override fun getTrustedClassAttachment(className: String): Attachment? {
|
||||
override fun getTrustedClassAttachments(className: String): List<Attachment> {
|
||||
val allTrusted = attachments.queryAttachments(
|
||||
AttachmentsQueryCriteria().withUploader(Builder.`in`(TRUSTED_UPLOADERS)),
|
||||
// JarScanningCordappLoader makes sure legacy contract CorDapps have a coresponding non-legacy CorDapp, and that the
|
||||
// legacy CorDapp has a smaller version number. Thus sorting by the version here ensures we never return the legacy attachment.
|
||||
AttachmentSort(listOf(AttachmentSortColumn(AttachmentSortAttribute.VERSION, Sort.Direction.DESC)))
|
||||
)
|
||||
|
||||
// TODO - add caching if performance is affected.
|
||||
for (attId in allTrusted) {
|
||||
val attch = attachments.openAttachment(attId)!!
|
||||
if (attch.hasFile("$className.class")) return attch
|
||||
}
|
||||
return null
|
||||
val fileName = "$className.class"
|
||||
return allTrusted.mapNotNull { id -> attachments.openAttachment(id)!!.takeIf { it.hasFile(fileName) } }
|
||||
}
|
||||
|
||||
private fun Attachment.hasFile(className: String): Boolean = openAsJAR().use { it.entries().any { entry -> entry.name == className } }
|
||||
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -34,7 +34,7 @@ interface VerificationSupport {
|
||||
|
||||
fun isAttachmentTrusted(attachment: Attachment): Boolean
|
||||
|
||||
fun getTrustedClassAttachment(className: String): Attachment?
|
||||
fun getTrustedClassAttachments(className: String): List<Attachment>
|
||||
|
||||
fun getNetworkParameters(id: SecureHash?): NetworkParameters?
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
package net.corda.core.node.services
|
||||
|
||||
import net.corda.core.DoNotImplement
|
||||
|
@ -1,16 +1,22 @@
|
||||
package net.corda.core.transactions
|
||||
|
||||
import net.corda.core.DoNotImplement
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.NamedByHash
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.castIfPossible
|
||||
import net.corda.core.internal.indexOfOrThrow
|
||||
import net.corda.core.internal.toSimpleString
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import java.util.function.Predicate
|
||||
|
||||
/**
|
||||
* An abstract class defining fields shared by all transaction types in the system.
|
||||
*/
|
||||
@Suppress("RedundantSamConstructor") // Because the external verifier uses Kotlin 1.2
|
||||
@DoNotImplement
|
||||
abstract class BaseTransaction : NamedByHash {
|
||||
/** A list of reusable reference data states which can be referred to by other contracts in this transaction. */
|
||||
@ -163,5 +169,5 @@ abstract class BaseTransaction : NamedByHash {
|
||||
return findOutRef(T::class.java, Predicate { predicate(it) })
|
||||
}
|
||||
|
||||
override fun toString(): String = "${javaClass.simpleName}(id=$id)"
|
||||
override fun toString(): String = toSimpleString()
|
||||
}
|
@ -22,8 +22,10 @@ import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.AttachmentWithContext
|
||||
import net.corda.core.internal.combinedHash
|
||||
import net.corda.core.internal.getRequiredSigningKeysInternal
|
||||
import net.corda.core.internal.loadClassOfType
|
||||
import net.corda.core.internal.mapToSet
|
||||
import net.corda.core.internal.verification.NodeVerificationSupport
|
||||
import net.corda.core.internal.verification.VerificationResult
|
||||
import net.corda.core.internal.verification.VerificationSupport
|
||||
import net.corda.core.internal.verification.toVerifyingServiceHub
|
||||
import net.corda.core.node.NetworkParameters
|
||||
@ -40,6 +42,7 @@ import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.PARA
|
||||
import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.UPGRADED_ATTACHMENT
|
||||
import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.UPGRADED_CONTRACT
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.core.utilities.toBase58String
|
||||
import java.security.PublicKey
|
||||
|
||||
@ -159,6 +162,20 @@ data class ContractUpgradeWireTransaction(
|
||||
return ContractUpgradeFilteredTransaction(visibleComponents, hiddenComponents, digestService)
|
||||
}
|
||||
|
||||
@CordaInternal
|
||||
@JvmSynthetic
|
||||
internal fun tryVerify(verificationSupport: NodeVerificationSupport): VerificationResult.External {
|
||||
// Contract upgrades only work on 4.11 and earlier
|
||||
return VerificationResult.External(Try.on { verificationSupport.externalVerifierHandle.verifyTransaction(this) })
|
||||
}
|
||||
|
||||
@CordaInternal
|
||||
@JvmSynthetic
|
||||
internal fun verifyInProcess(verificationSupport: VerificationSupport) {
|
||||
// No contract code is run when verifying contract upgrade transactions, it is sufficient to check invariants during initialisation.
|
||||
ContractUpgradeLedgerTransaction.resolve(verificationSupport, this, emptyList())
|
||||
}
|
||||
|
||||
enum class Component {
|
||||
INPUTS, NOTARY, LEGACY_ATTACHMENT, UPGRADED_CONTRACT, UPGRADED_ATTACHMENT, PARAMETERS_HASH
|
||||
}
|
||||
@ -344,7 +361,7 @@ private constructor(
|
||||
|
||||
/** The required signers are the set of all input states' participants. */
|
||||
override val requiredSigningKeys: Set<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> {
|
||||
return keys.map { it.toBase58String() }
|
||||
|
@ -11,8 +11,10 @@ import net.corda.core.crypto.DigestService
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.getRequiredSigningKeysInternal
|
||||
import net.corda.core.internal.indexOfOrThrow
|
||||
import net.corda.core.internal.mapToSet
|
||||
import net.corda.core.internal.verification.NodeVerificationSupport
|
||||
import net.corda.core.internal.verification.VerificationResult
|
||||
import net.corda.core.internal.verification.VerificationSupport
|
||||
import net.corda.core.internal.verification.toVerifyingServiceHub
|
||||
import net.corda.core.node.NetworkParameters
|
||||
@ -27,6 +29,7 @@ import net.corda.core.transactions.NotaryChangeWireTransaction.Component.NEW_NOT
|
||||
import net.corda.core.transactions.NotaryChangeWireTransaction.Component.NOTARY
|
||||
import net.corda.core.transactions.NotaryChangeWireTransaction.Component.PARAMETERS_HASH
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.core.utilities.toBase58String
|
||||
import java.security.PublicKey
|
||||
|
||||
@ -107,6 +110,16 @@ data class NotaryChangeWireTransaction(
|
||||
return resolve(services as ServicesForResolution, sigs)
|
||||
}
|
||||
|
||||
@CordaInternal
|
||||
@JvmSynthetic
|
||||
internal fun tryVerify(verificationSupport: NodeVerificationSupport): VerificationResult.InProcess {
|
||||
return VerificationResult.InProcess(Try.on {
|
||||
// No contract code is run when verifying notary change transactions, it is sufficient to check invariants during initialisation.
|
||||
NotaryChangeLedgerTransaction.resolve(verificationSupport, this, emptyList())
|
||||
null
|
||||
})
|
||||
}
|
||||
|
||||
enum class Component {
|
||||
INPUTS, NOTARY, NEW_NOTARY, PARAMETERS_HASH
|
||||
}
|
||||
@ -180,7 +193,7 @@ private constructor(
|
||||
get() = inputs.map { computeOutput(it, newNotary) { inputs.map(StateAndRef<ContractState>::ref) } }
|
||||
|
||||
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> {
|
||||
return keys.map { it.toBase58String() }
|
||||
|
@ -3,12 +3,12 @@ package net.corda.core.transactions
|
||||
import net.corda.core.CordaException
|
||||
import net.corda.core.CordaInternal
|
||||
import net.corda.core.CordaThrowable
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.AttachmentResolutionException
|
||||
import net.corda.core.contracts.NamedByHash
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TransactionResolutionException
|
||||
import net.corda.core.contracts.TransactionVerificationException
|
||||
import net.corda.core.contracts.TransactionVerificationException.TransactionNetworkParameterOrderingException
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.SignableData
|
||||
import net.corda.core.crypto.SignatureMetadata
|
||||
@ -16,34 +16,26 @@ import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.sign
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.TransactionDeserialisationException
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.internal.equivalent
|
||||
import net.corda.core.internal.isUploaderTrusted
|
||||
import net.corda.core.internal.getRequiredSigningKeysInternal
|
||||
import net.corda.core.internal.toSimpleString
|
||||
import net.corda.core.internal.verification.NodeVerificationSupport
|
||||
import net.corda.core.internal.verification.VerificationSupport
|
||||
import net.corda.core.internal.verification.toVerifyingServiceHub
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.MissingAttachmentsException
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.internal.MissingSerializerException
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.core.utilities.Try.Failure
|
||||
import net.corda.core.utilities.Try.Success
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import java.io.NotSerializableException
|
||||
import net.corda.core.utilities.toBase58String
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
import java.util.function.Predicate
|
||||
|
||||
/**
|
||||
* SignedTransaction wraps a serialized WireTransaction. It contains one or more signatures, each one for
|
||||
* SignedTransaction wraps a serialized [CoreTransaction], though it will almost exclusively be a [WireTransaction].
|
||||
* It contains one or more signatures, each one for
|
||||
* a public key (including composite keys) that is mentioned inside a transaction command. SignedTransaction is the top level transaction type
|
||||
* and the type most frequently passed around the network and stored. The identity of a transaction is the hash of Merkle root
|
||||
* of a WireTransaction, therefore if you are storing data keyed by WT hash be aware that multiple different STs may
|
||||
@ -163,12 +155,6 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
|
||||
val verifyingServiceHub = services.toVerifyingServiceHub()
|
||||
// We need parameters check here, because finality flow calls stx.toLedgerTransaction() and then verify.
|
||||
resolveAndCheckNetworkParameters(verifyingServiceHub)
|
||||
return toLedgerTransactionInternal(verifyingServiceHub, checkSufficientSignatures)
|
||||
}
|
||||
|
||||
@JvmSynthetic
|
||||
@CordaInternal
|
||||
fun toLedgerTransactionInternal(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean): LedgerTransaction {
|
||||
// TODO: We could probably optimise the below by
|
||||
// a) not throwing if threshold is eventually satisfied, but some of the rest of the signatures are failing.
|
||||
// b) omit verifying signatures when threshold requirement is met.
|
||||
@ -176,12 +162,8 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
|
||||
// For the above to work, [checkSignaturesAreValid] should take the [requiredSigningKeys] as input
|
||||
// and probably combine logic from signature validation and key-fulfilment
|
||||
// in [TransactionWithSignatures.verifySignaturesExcept].
|
||||
if (checkSufficientSignatures) {
|
||||
verifyRequiredSignatures() // It internally invokes checkSignaturesAreValid().
|
||||
} else {
|
||||
checkSignaturesAreValid()
|
||||
}
|
||||
return tx.toLedgerTransactionInternal(verificationSupport)
|
||||
verifySignatures(verifyingServiceHub, checkSufficientSignatures)
|
||||
return tx.toLedgerTransactionInternal(verifyingServiceHub)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -210,252 +192,50 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
|
||||
*/
|
||||
@CordaInternal
|
||||
@JvmSynthetic
|
||||
internal fun verifyInternal(verificationSupport: NodeVerificationSupport, checkSufficientSignatures: Boolean = true): FullTransaction? {
|
||||
internal fun verifyInternal(verificationSupport: NodeVerificationSupport, checkSufficientSignatures: Boolean = true): LedgerTransaction? {
|
||||
resolveAndCheckNetworkParameters(verificationSupport)
|
||||
val verificationType = determineVerificationType()
|
||||
log.debug { "Transaction $id has verification type $verificationType" }
|
||||
return when (verificationType) {
|
||||
VerificationType.IN_PROCESS -> verifyInProcess(verificationSupport, checkSufficientSignatures)
|
||||
VerificationType.BOTH -> {
|
||||
val inProcessResult = Try.on { verifyInProcess(verificationSupport, checkSufficientSignatures) }
|
||||
val externalResult = Try.on { verificationSupport.externalVerifierHandle.verifyTransaction(this, checkSufficientSignatures) }
|
||||
ensureSameResult(inProcessResult, externalResult)
|
||||
}
|
||||
VerificationType.EXTERNAL -> {
|
||||
verificationSupport.externalVerifierHandle.verifyTransaction(this, checkSufficientSignatures)
|
||||
// We could create a LedgerTransaction here, and except for calling `verify()`, it would be valid to use. However, it's best
|
||||
// we let the caller deal with that, since we can't control what they will do with it.
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun determineVerificationType(): VerificationType {
|
||||
verifySignatures(verificationSupport, checkSufficientSignatures)
|
||||
val ctx = coreTransaction
|
||||
return when (ctx) {
|
||||
is WireTransaction -> {
|
||||
when {
|
||||
ctx.legacyAttachments.isEmpty() -> VerificationType.IN_PROCESS
|
||||
ctx.nonLegacyAttachments.isEmpty() -> VerificationType.EXTERNAL
|
||||
else -> VerificationType.BOTH
|
||||
}
|
||||
}
|
||||
// Contract upgrades only work on 4.11 and earlier
|
||||
is ContractUpgradeWireTransaction -> VerificationType.EXTERNAL
|
||||
else -> VerificationType.IN_PROCESS // The default is always in-process
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureSameResult(inProcessResult: Try<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)
|
||||
val verificationResult = when (ctx) {
|
||||
// TODO: Verify contract constraints here as well as in LedgerTransaction to ensure that anything being deserialised
|
||||
// from the attachment is trusted. This will require some partial serialisation work to not load the ContractState
|
||||
// objects from the TransactionState.
|
||||
is WireTransaction -> ctx.tryVerify(verificationSupport)
|
||||
is ContractUpgradeWireTransaction -> ctx.tryVerify(verificationSupport)
|
||||
is NotaryChangeWireTransaction -> ctx.tryVerify(verificationSupport)
|
||||
else -> throw IllegalStateException("${ctx.toSimpleString()} cannot be verified")
|
||||
}
|
||||
return verificationResult.enforceSuccess()
|
||||
}
|
||||
|
||||
@Suppress("ThrowsCount")
|
||||
private fun resolveAndCheckNetworkParameters(services: NodeVerificationSupport) {
|
||||
val hashOrDefault = networkParametersHash ?: services.networkParametersService.defaultHash
|
||||
val txNetworkParameters = services.networkParametersService.lookup(hashOrDefault)
|
||||
?: throw TransactionResolutionException(id)
|
||||
val txNetworkParameters = services.networkParametersService.lookup(hashOrDefault) ?: throw TransactionResolutionException(id)
|
||||
val groupedInputsAndRefs = (inputs + references).groupBy { it.txhash }
|
||||
groupedInputsAndRefs.map { entry ->
|
||||
val tx = services.validatedTransactions.getTransaction(entry.key)?.coreTransaction
|
||||
?: throw TransactionResolutionException(id)
|
||||
for ((txId, stateRefs) in groupedInputsAndRefs) {
|
||||
val tx = services.validatedTransactions.getTransaction(txId)?.coreTransaction ?: throw TransactionResolutionException(id)
|
||||
val paramHash = tx.networkParametersHash ?: services.networkParametersService.defaultHash
|
||||
val params = services.networkParametersService.lookup(paramHash) ?: throw TransactionResolutionException(id)
|
||||
if (txNetworkParameters.epoch < params.epoch)
|
||||
throw TransactionVerificationException.TransactionNetworkParameterOrderingException(id, entry.value.first(), txNetworkParameters, params)
|
||||
}
|
||||
}
|
||||
|
||||
/** No contract code is run when verifying notary change transactions, it is sufficient to check invariants during initialisation. */
|
||||
private fun verifyNotaryChangeTransaction(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean): NotaryChangeLedgerTransaction {
|
||||
val ntx = NotaryChangeLedgerTransaction.resolve(verificationSupport, coreTransaction as NotaryChangeWireTransaction, sigs)
|
||||
if (checkSufficientSignatures) ntx.verifyRequiredSignatures()
|
||||
else checkSignaturesAreValid()
|
||||
return ntx
|
||||
}
|
||||
|
||||
/** No contract code is run when verifying contract upgrade transactions, it is sufficient to check invariants during initialisation. */
|
||||
private fun verifyContractUpgradeTransaction(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean): ContractUpgradeLedgerTransaction {
|
||||
val ctx = ContractUpgradeLedgerTransaction.resolve(verificationSupport, coreTransaction as ContractUpgradeWireTransaction, sigs)
|
||||
if (checkSufficientSignatures) ctx.verifyRequiredSignatures()
|
||||
else checkSignaturesAreValid()
|
||||
return ctx
|
||||
}
|
||||
|
||||
// TODO: Verify contract constraints here as well as in LedgerTransaction to ensure that anything being deserialised
|
||||
// from the attachment is trusted. This will require some partial serialisation work to not load the ContractState
|
||||
// objects from the TransactionState.
|
||||
private fun verifyRegularTransaction(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean): LedgerTransaction {
|
||||
val ltx = toLedgerTransactionInternal(verificationSupport, checkSufficientSignatures)
|
||||
try {
|
||||
ltx.verify()
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
checkReverifyAllowed(e)
|
||||
val missingClass = e.message ?: throw e
|
||||
log.warn("Transaction {} has missing class: {}", ltx.id, missingClass)
|
||||
reverifyWithFixups(ltx, verificationSupport, missingClass)
|
||||
} catch (e: NotSerializableException) {
|
||||
checkReverifyAllowed(e)
|
||||
retryVerification(e, e, ltx, verificationSupport)
|
||||
} catch (e: TransactionDeserialisationException) {
|
||||
checkReverifyAllowed(e)
|
||||
retryVerification(e.cause, e, ltx, verificationSupport)
|
||||
}
|
||||
return ltx
|
||||
}
|
||||
|
||||
private fun checkReverifyAllowed(ex: Throwable) {
|
||||
// If that transaction was created with and after Corda 4 then just fail.
|
||||
// The lenient dependency verification is only supported for Corda 3 transactions.
|
||||
// To detect if the transaction was created before Corda 4 we check if the transaction has the NetworkParameters component group.
|
||||
if (networkParametersHash != null) {
|
||||
log.warn("TRANSACTION VERIFY FAILED - No attempt to auto-repair as TX is Corda 4+")
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ThrowsCount")
|
||||
private fun retryVerification(cause: Throwable?, ex: Throwable, ltx: LedgerTransaction, verificationSupport: VerificationSupport) {
|
||||
when (cause) {
|
||||
is MissingSerializerException -> {
|
||||
log.warn("Missing serializers: typeDescriptor={}, typeNames={}", cause.typeDescriptor ?: "<unknown>", cause.typeNames)
|
||||
reverifyWithFixups(ltx, verificationSupport, null)
|
||||
if (txNetworkParameters.epoch < params.epoch) {
|
||||
throw TransactionNetworkParameterOrderingException(id, stateRefs.first(), txNetworkParameters, params)
|
||||
}
|
||||
is NotSerializableException -> {
|
||||
val underlying = cause.cause
|
||||
if (underlying is ClassNotFoundException) {
|
||||
val missingClass = underlying.message?.replace('.', '/') ?: throw ex
|
||||
log.warn("Transaction {} has missing class: {}", ltx.id, missingClass)
|
||||
reverifyWithFixups(ltx, verificationSupport, missingClass)
|
||||
} else {
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
else -> throw ex
|
||||
}
|
||||
}
|
||||
|
||||
// Transactions created before Corda 4 can be missing dependencies on other CorDapps.
|
||||
// This code has detected a missing custom serializer - probably located inside a workflow CorDapp.
|
||||
// We need to extract this CorDapp from AttachmentStorage and try verifying this transaction again.
|
||||
private fun reverifyWithFixups(ltx: LedgerTransaction, verificationSupport: VerificationSupport, missingClass: String?) {
|
||||
log.warn("""Detected that transaction $id does not contain all cordapp dependencies.
|
||||
|This may be the result of a bug in a previous version of Corda.
|
||||
|Attempting to re-verify having applied this node's fix-up rules.
|
||||
|Please check with the originator that this is a valid transaction.""".trimMargin())
|
||||
|
||||
val replacementAttachments = computeReplacementAttachments(ltx, verificationSupport, missingClass)
|
||||
log.warn("Reverifying transaction {} with attachments:{}", ltx.id, replacementAttachments)
|
||||
ltx.verifyInternal(replacementAttachments.toList())
|
||||
}
|
||||
|
||||
private fun computeReplacementAttachments(ltx: LedgerTransaction,
|
||||
verificationSupport: VerificationSupport,
|
||||
missingClass: String?): Collection<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"
|
||||
)
|
||||
private fun verifySignatures(verificationSupport: NodeVerificationSupport, checkSufficientSignatures: Boolean) {
|
||||
if (checkSufficientSignatures) {
|
||||
val ctx = coreTransaction
|
||||
val tws: TransactionWithSignatures = when (ctx) {
|
||||
is WireTransaction -> this // SignedTransaction implements TransactionWithSignatures in terms of WireTransaction
|
||||
else -> CoreTransactionWithSignatures(ctx, sigs, verificationSupport)
|
||||
}
|
||||
|
||||
log.warn("""Detected that transaction $ltx does not contain all cordapp dependencies.
|
||||
|This may be the result of a bug in a previous version of Corda.
|
||||
|Attempting to verify using the additional trusted dependency: $extraAttachment for class $missingClass.
|
||||
|Please check with the originator that this is a valid transaction.
|
||||
|YOU ARE ONLY SEEING THIS MESSAGE BECAUSE THE CORDAPPS THAT CREATED THIS TRANSACTION ARE BROKEN!
|
||||
|WE HAVE TRIED TO REPAIR THE TRANSACTION AS BEST WE CAN, BUT CANNOT GUARANTEE WE HAVE SUCCEEDED!
|
||||
|PLEASE FIX THE CORDAPPS AND MIGRATE THESE BROKEN TRANSACTIONS AS SOON AS POSSIBLE!
|
||||
|THIS MESSAGE IS **SUPPOSED** TO BE SCARY!!
|
||||
|""".trimMargin()
|
||||
)
|
||||
tws.verifyRequiredSignatures() // Internally checkSignaturesAreValid is invoked
|
||||
} else {
|
||||
checkSignaturesAreValid()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply this node's attachment fix-up rules to the given attachments.
|
||||
*
|
||||
* @param attachments A collection of [Attachment] objects, e.g. as provided by a transaction.
|
||||
* @return The [attachments] with the node's fix-up rules applied.
|
||||
*/
|
||||
private fun fixupAttachments(verificationSupport: VerificationSupport, attachments: Collection<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
|
||||
* [NotaryChangeWireTransaction].
|
||||
@ -512,7 +292,7 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
|
||||
return ctx.resolve(services, sigs)
|
||||
}
|
||||
|
||||
override fun toString(): String = "${javaClass.simpleName}(id=$id)"
|
||||
override fun toString(): String = toSimpleString()
|
||||
|
||||
private companion object {
|
||||
private fun missingSignatureMsg(missing: Set<PublicKey>, descriptions: List<String>, id: SecureHash): String {
|
||||
@ -520,13 +300,28 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
|
||||
"keys: ${missing.joinToString { it.toStringShort() }}, " +
|
||||
"by signers: ${descriptions.joinToString()} "
|
||||
}
|
||||
|
||||
private val log = contextLogger()
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
/**
|
||||
* 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
|
||||
/** Returns the contained [NotaryChangeWireTransaction], or throws if this is a normal transaction. */
|
||||
@Deprecated("No replacement, this should not be used outside of Corda core")
|
||||
|
@ -25,6 +25,7 @@ import net.corda.core.serialization.SerializationFactory
|
||||
import net.corda.core.serialization.SerializationMagic
|
||||
import net.corda.core.serialization.SerializationSchemeContext
|
||||
import net.corda.core.serialization.internal.CustomSerializationSchemeUtils.Companion.getCustomSerializationMagicFromSchemeId
|
||||
import net.corda.core.utilities.Try.Failure
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
@ -89,6 +90,7 @@ open class TransactionBuilder(
|
||||
private val inputsWithTransactionState = arrayListOf<StateAndRef<ContractState>>()
|
||||
private val referencesWithTransactionState = arrayListOf<TransactionState<ContractState>>()
|
||||
private var excludedAttachments: Set<AttachmentId> = emptySet()
|
||||
private var extraLegacyAttachments: MutableSet<AttachmentId>? = null
|
||||
|
||||
/**
|
||||
* Creates a copy of the builder.
|
||||
@ -196,20 +198,26 @@ open class TransactionBuilder(
|
||||
|
||||
val wireTx = SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
|
||||
// Sort the attachments to ensure transaction builds are stable.
|
||||
val attachmentsBuilder = allContractAttachments.mapTo(TreeSet()) { it.currentAttachment.id }
|
||||
attachmentsBuilder.addAll(attachments)
|
||||
attachmentsBuilder.removeAll(excludedAttachments)
|
||||
val nonLegacyAttachments = allContractAttachments.mapTo(TreeSet()) { it.currentAttachment.id }.apply {
|
||||
addAll(attachments)
|
||||
removeAll(excludedAttachments)
|
||||
}.toList()
|
||||
val legacyAttachments = allContractAttachments.mapNotNullTo(TreeSet()) { it.legacyAttachment?.id }.apply {
|
||||
if (extraLegacyAttachments != null) {
|
||||
addAll(extraLegacyAttachments!!)
|
||||
}
|
||||
}.toList()
|
||||
WireTransaction(
|
||||
createComponentGroups(
|
||||
inputStates(),
|
||||
resolvedOutputs,
|
||||
commands(),
|
||||
attachmentsBuilder.toList(),
|
||||
nonLegacyAttachments,
|
||||
notary,
|
||||
window,
|
||||
referenceStates,
|
||||
serviceHub.networkParametersService.currentHash,
|
||||
allContractAttachments.mapNotNullTo(TreeSet()) { it.legacyAttachment?.id }.toList()
|
||||
legacyAttachments
|
||||
),
|
||||
privacySalt,
|
||||
serviceHub.digestService
|
||||
@ -229,59 +237,71 @@ open class TransactionBuilder(
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the first exception in the hierarchy that matches one of the [types].
|
||||
private tailrec fun Throwable.rootClassNotFoundCause(vararg types: KClass<*>): Throwable = when {
|
||||
this::class in types -> this
|
||||
this.cause == null -> this
|
||||
else -> this.cause!!.rootClassNotFoundCause(*types)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if a new dependency was successfully added.
|
||||
*/
|
||||
// TODO This entire code path needs to be updated to work with legacy attachments and automically adding their dependencies. ENT-11445
|
||||
private fun addMissingDependency(serviceHub: VerifyingServiceHub, wireTx: WireTransaction, tryCount: Int): Boolean {
|
||||
return try {
|
||||
wireTx.toLedgerTransactionInternal(serviceHub).verify()
|
||||
// The transaction verified successfully without adding any extra dependency.
|
||||
false
|
||||
} catch (e: Throwable) {
|
||||
val rootError = e.rootClassNotFoundCause(ClassNotFoundException::class, NoClassDefFoundError::class)
|
||||
val verificationResult = wireTx.tryVerify(serviceHub)
|
||||
// Check both legacy and non-legacy components are working, and try to add any missing dependencies if either are not.
|
||||
(verificationResult.inProcessResult as? Failure)?.let { (inProcessException) ->
|
||||
return addMissingDependency(inProcessException, wireTx, false, serviceHub, tryCount)
|
||||
}
|
||||
(verificationResult.externalResult as? Failure)?.let { (externalException) ->
|
||||
return addMissingDependency(externalException, wireTx, true, serviceHub, tryCount)
|
||||
}
|
||||
// The transaction verified successfully without needing any extra dependency.
|
||||
return false
|
||||
}
|
||||
|
||||
when {
|
||||
// Handle various exceptions that can be thrown during verification and drill down the wrappings.
|
||||
// Note: this is a best effort to preserve backwards compatibility.
|
||||
rootError is ClassNotFoundException -> {
|
||||
// Using nonLegacyAttachments here as the verification above was done in-process and thus only the nonLegacyAttachments
|
||||
// are used.
|
||||
// TODO This might change with ENT-11445 where we add support for legacy contract dependencies.
|
||||
((tryCount == 0) && fixupAttachments(wireTx.nonLegacyAttachments, serviceHub, e))
|
||||
|| addMissingAttachment((rootError.message ?: throw e).replace('.', '/'), serviceHub, e)
|
||||
}
|
||||
rootError is NoClassDefFoundError -> {
|
||||
((tryCount == 0) && fixupAttachments(wireTx.nonLegacyAttachments, serviceHub, e))
|
||||
|| addMissingAttachment(rootError.message ?: throw e, serviceHub, e)
|
||||
}
|
||||
|
||||
// Ignore these exceptions as they will break unit tests.
|
||||
// The point here is only to detect missing dependencies. The other exceptions are irrelevant.
|
||||
e is TransactionVerificationException -> false
|
||||
e is TransactionResolutionException -> false
|
||||
e is IllegalStateException -> false
|
||||
e is IllegalArgumentException -> false
|
||||
|
||||
// Fail early if none of the expected scenarios were hit.
|
||||
else -> {
|
||||
log.error("""The transaction currently built will not validate because of an unknown error most likely caused by a
|
||||
missing dependency in the transaction attachments.
|
||||
Please contact the developer of the CorDapp for further instructions.
|
||||
""".trimIndent(), e)
|
||||
throw e
|
||||
}
|
||||
private fun addMissingDependency(e: Throwable, wireTx: WireTransaction, isLegacy: Boolean, serviceHub: VerifyingServiceHub, tryCount: Int): Boolean {
|
||||
val missingClass = extractMissingClass(e)
|
||||
if (log.isDebugEnabled) {
|
||||
log.debug("Checking if transaction has missing attachment (missingClass=$missingClass) (legacy=$isLegacy) $wireTx", e)
|
||||
}
|
||||
return when {
|
||||
missingClass != null -> {
|
||||
val attachments = if (isLegacy) wireTx.legacyAttachments else wireTx.nonLegacyAttachments
|
||||
(tryCount == 0 && fixupAttachments(attachments, serviceHub, e)) || addMissingAttachment(missingClass, isLegacy, serviceHub, e)
|
||||
}
|
||||
// Ignore these exceptions as they will break unit tests.
|
||||
// The point here is only to detect missing dependencies. The other exceptions are irrelevant.
|
||||
e is TransactionVerificationException -> false
|
||||
e is TransactionResolutionException -> false
|
||||
e is IllegalStateException -> false
|
||||
e is IllegalArgumentException -> false
|
||||
// Fail early if none of the expected scenarios were hit.
|
||||
else -> {
|
||||
log.error("""The transaction currently built will not validate because of an unknown error most likely caused by a
|
||||
missing dependency in the transaction attachments.
|
||||
Please contact the developer of the CorDapp for further instructions.
|
||||
""".trimIndent(), e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractMissingClass(throwable: Throwable): String? {
|
||||
var current = throwable
|
||||
while (true) {
|
||||
if (current is ClassNotFoundException) {
|
||||
return current.message?.replace('.', '/')
|
||||
}
|
||||
if (current is NoClassDefFoundError) {
|
||||
return current.message
|
||||
}
|
||||
val message = current.message
|
||||
if (message != null) {
|
||||
message.extractClassAfter(NoClassDefFoundError::class)?.let { return it }
|
||||
message.extractClassAfter(ClassNotFoundException::class)?.let { return it.replace('.', '/') }
|
||||
}
|
||||
current = current.cause ?: return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.extractClassAfter(exceptionClass: KClass<out Throwable>): String? {
|
||||
return substringAfterLast("${exceptionClass.java.name}: ", "").takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
private fun fixupAttachments(
|
||||
txAttachments: List<AttachmentId>,
|
||||
serviceHub: VerifyingServiceHub,
|
||||
@ -314,7 +334,7 @@ open class TransactionBuilder(
|
||||
return true
|
||||
}
|
||||
|
||||
private fun addMissingAttachment(missingClass: String, serviceHub: VerifyingServiceHub, originalException: Throwable): Boolean {
|
||||
private fun addMissingAttachment(missingClass: String, isLegacy: Boolean, serviceHub: VerifyingServiceHub, originalException: Throwable): Boolean {
|
||||
if (!isValidJavaClass(missingClass)) {
|
||||
log.warn("Could not autodetect a valid attachment for the transaction being built.")
|
||||
throw originalException
|
||||
@ -323,7 +343,14 @@ open class TransactionBuilder(
|
||||
throw originalException
|
||||
}
|
||||
|
||||
val attachment = serviceHub.getTrustedClassAttachment(missingClass)
|
||||
val attachments = serviceHub.getTrustedClassAttachments(missingClass)
|
||||
val attachment = if (isLegacy) {
|
||||
// Any attachment which contains the class but isn't a non-legacy CorDapp is *probably* the legacy attachment we're looking for
|
||||
val nonLegacyCordapps = serviceHub.cordappProvider.cordapps.mapToSet { it.jarHash }
|
||||
attachments.firstOrNull { it.id !in nonLegacyCordapps }
|
||||
} else {
|
||||
attachments.firstOrNull()
|
||||
}
|
||||
|
||||
if (attachment == null) {
|
||||
log.error("""The transaction currently built is missing an attachment for class: $missingClass.
|
||||
@ -338,7 +365,12 @@ open class TransactionBuilder(
|
||||
Please contact the developer of the CorDapp and install the latest version, as this approach might be insecure.
|
||||
""".trimIndent())
|
||||
|
||||
addAttachment(attachment.id)
|
||||
if (isLegacy) {
|
||||
(extraLegacyAttachments ?: LinkedHashSet<AttachmentId>().also { extraLegacyAttachments = it }) += attachment.id
|
||||
} else {
|
||||
addAttachment(attachment.id)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import net.corda.core.DoNotImplement
|
||||
import net.corda.core.contracts.NamedByHash
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.internal.mapToSet
|
||||
import net.corda.core.transactions.SignedTransaction.SignaturesMissingException
|
||||
import net.corda.core.utilities.toNonEmptySet
|
||||
import java.security.InvalidKeyException
|
||||
@ -99,9 +100,9 @@ interface TransactionWithSignatures : NamedByHash {
|
||||
* Return the [PublicKey]s for which we still need signatures.
|
||||
*/
|
||||
fun getMissingSigners(): Set<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
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.contracts.TransactionResolutionException
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.contracts.TransactionVerificationException
|
||||
import net.corda.core.crypto.DigestService
|
||||
import net.corda.core.crypto.MerkleTree
|
||||
import net.corda.core.crypto.SecureHash
|
||||
@ -24,14 +25,22 @@ import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.Emoji
|
||||
import net.corda.core.internal.SerializedStateAndRef
|
||||
import net.corda.core.internal.SerializedTransactionState
|
||||
import net.corda.core.internal.TransactionDeserialisationException
|
||||
import net.corda.core.internal.createComponentGroups
|
||||
import net.corda.core.internal.deserialiseComponentGroup
|
||||
import net.corda.core.internal.equivalent
|
||||
import net.corda.core.internal.flatMapToSet
|
||||
import net.corda.core.internal.getGroup
|
||||
import net.corda.core.internal.isUploaderTrusted
|
||||
import net.corda.core.internal.lazyMapped
|
||||
import net.corda.core.internal.mapToSet
|
||||
import net.corda.core.internal.toSimpleString
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.internal.verification.NodeVerificationSupport
|
||||
import net.corda.core.internal.verification.VerificationResult
|
||||
import net.corda.core.internal.verification.VerificationResult.External
|
||||
import net.corda.core.internal.verification.VerificationResult.InProcess
|
||||
import net.corda.core.internal.verification.VerificationResult.InProcessAndExternal
|
||||
import net.corda.core.internal.verification.VerificationSupport
|
||||
import net.corda.core.internal.verification.toVerifyingServiceHub
|
||||
import net.corda.core.node.NetworkParameters
|
||||
@ -39,9 +48,15 @@ import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||
import net.corda.core.serialization.MissingAttachmentsException
|
||||
import net.corda.core.serialization.SerializationFactory
|
||||
import net.corda.core.serialization.internal.MissingSerializerException
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import java.io.NotSerializableException
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
import java.util.function.Predicate
|
||||
@ -71,7 +86,7 @@ import java.util.function.Predicate
|
||||
* </ul></p>
|
||||
*/
|
||||
@CordaSerializable
|
||||
@Suppress("ThrowsCount")
|
||||
@Suppress("ThrowsCount", "TooManyFunctions", "MagicNumber")
|
||||
class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: PrivacySalt, digestService: DigestService) : TraversableTransaction(componentGroups, digestService) {
|
||||
constructor(componentGroups: List<ComponentGroup>) : this(componentGroups, PrivacySalt())
|
||||
|
||||
@ -164,14 +179,14 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
}
|
||||
// These are not used
|
||||
override val appClassLoader: ClassLoader get() = throw AbstractMethodError()
|
||||
override fun getTrustedClassAttachment(className: String) = throw AbstractMethodError()
|
||||
override fun getTrustedClassAttachments(className: String) = throw AbstractMethodError()
|
||||
override fun fixupAttachmentIds(attachmentIds: Collection<SecureHash>) = throw AbstractMethodError()
|
||||
})
|
||||
}
|
||||
|
||||
@CordaInternal
|
||||
@JvmSynthetic
|
||||
fun toLedgerTransactionInternal(verificationSupport: VerificationSupport): LedgerTransaction {
|
||||
internal fun toLedgerTransactionInternal(verificationSupport: VerificationSupport): LedgerTransaction {
|
||||
// Look up public keys to authenticated identities.
|
||||
val authenticatedCommands = if (verificationSupport.isInProcess) {
|
||||
commands.lazyMapped { cmd, _ ->
|
||||
@ -360,43 +375,211 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
sig.verify(id)
|
||||
}
|
||||
|
||||
@CordaInternal
|
||||
@JvmSynthetic
|
||||
internal fun tryVerify(verificationSupport: NodeVerificationSupport): VerificationResult {
|
||||
return when {
|
||||
legacyAttachments.isEmpty() -> {
|
||||
log.debug { "${toSimpleString()} will be verified in-process" }
|
||||
InProcess(Try.on { verifyInProcess(verificationSupport) })
|
||||
}
|
||||
nonLegacyAttachments.isEmpty() -> {
|
||||
log.debug { "${toSimpleString()} will be verified by the external verifer" }
|
||||
External(Try.on { verificationSupport.externalVerifierHandle.verifyTransaction(this) })
|
||||
}
|
||||
else -> {
|
||||
log.debug { "${toSimpleString()} will be verified both in-process and by the external verifer" }
|
||||
val inProcessResult = Try.on { verifyInProcess(verificationSupport) }
|
||||
val externalResult = Try.on { verificationSupport.externalVerifierHandle.verifyTransaction(this) }
|
||||
InProcessAndExternal(inProcessResult, externalResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@CordaInternal
|
||||
@JvmSynthetic
|
||||
internal fun verifyInProcess(verificationSupport: VerificationSupport): LedgerTransaction {
|
||||
val ltx = toLedgerTransactionInternal(verificationSupport)
|
||||
try {
|
||||
ltx.verify()
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
checkReverifyAllowed(e)
|
||||
val missingClass = e.message ?: throw e
|
||||
log.warn("Transaction {} has missing class: {}", ltx.id, missingClass)
|
||||
reverifyWithFixups(ltx, verificationSupport, missingClass)
|
||||
} catch (e: NotSerializableException) {
|
||||
checkReverifyAllowed(e)
|
||||
retryVerification(e, e, ltx, verificationSupport)
|
||||
} catch (e: TransactionDeserialisationException) {
|
||||
checkReverifyAllowed(e)
|
||||
retryVerification(e.cause, e, ltx, verificationSupport)
|
||||
}
|
||||
return ltx
|
||||
}
|
||||
|
||||
private fun checkReverifyAllowed(ex: Throwable) {
|
||||
// If that transaction was created with and after Corda 4 then just fail.
|
||||
// The lenient dependency verification is only supported for Corda 3 transactions.
|
||||
// To detect if the transaction was created before Corda 4 we check if the transaction has the NetworkParameters component group.
|
||||
if (networkParametersHash != null) {
|
||||
log.warn("TRANSACTION VERIFY FAILED - No attempt to auto-repair as TX is Corda 4+")
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
|
||||
private fun retryVerification(cause: Throwable?, ex: Throwable, ltx: LedgerTransaction, verificationSupport: VerificationSupport) {
|
||||
when (cause) {
|
||||
is MissingSerializerException -> {
|
||||
log.warn("Missing serializers: typeDescriptor={}, typeNames={}", cause.typeDescriptor ?: "<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 {
|
||||
val buf = StringBuilder()
|
||||
buf.appendLine("Transaction:")
|
||||
val buf = StringBuilder(1024)
|
||||
buf.appendLine("Transaction $id:")
|
||||
for (reference in references) {
|
||||
val emoji = Emoji.rightArrow
|
||||
buf.appendLine("${emoji}REFS: $reference")
|
||||
buf.appendLine("${emoji}REFS: $reference")
|
||||
}
|
||||
for (input in inputs) {
|
||||
val emoji = Emoji.rightArrow
|
||||
buf.appendLine("${emoji}INPUT: $input")
|
||||
buf.appendLine("${emoji}INPUT: $input")
|
||||
}
|
||||
for ((data) in outputs) {
|
||||
val emoji = Emoji.leftArrow
|
||||
buf.appendLine("${emoji}OUTPUT: $data")
|
||||
buf.appendLine("${emoji}OUTPUT: $data")
|
||||
}
|
||||
for (command in commands) {
|
||||
val emoji = Emoji.diamond
|
||||
buf.appendLine("${emoji}COMMAND: $command")
|
||||
buf.appendLine("${emoji}COMMAND: $command")
|
||||
}
|
||||
for (attachment in attachments) {
|
||||
for (attachment in nonLegacyAttachments) {
|
||||
val emoji = Emoji.paperclip
|
||||
buf.appendLine("${emoji}ATTACHMENT: $attachment")
|
||||
buf.appendLine("${emoji}ATTACHMENT: $attachment")
|
||||
}
|
||||
for (attachment in legacyAttachments) {
|
||||
val emoji = Emoji.paperclip
|
||||
buf.appendLine("${emoji}ATTACHMENT: $attachment (legacy)")
|
||||
}
|
||||
if (networkParametersHash != null) {
|
||||
buf.appendLine("PARAMETERS HASH: $networkParametersHash")
|
||||
val emoji = Emoji.newspaper
|
||||
buf.appendLine("${emoji}NETWORK PARAMS: $networkParametersHash")
|
||||
}
|
||||
return buf.toString()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is WireTransaction) {
|
||||
return (this.id == other.id)
|
||||
}
|
||||
return false
|
||||
}
|
||||
override fun equals(other: Any?): Boolean = other is WireTransaction && this.id == other.id
|
||||
|
||||
override fun hashCode(): Int = id.hashCode()
|
||||
|
||||
private companion object {
|
||||
private val log = contextLogger()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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."
|
||||
}
|
||||
check(newerCordapp.contractVersionId > legacyCordapp.contractVersionId) {
|
||||
"Newer contract CorDapp '${newerCordapp.jarFile}' does not have a higher version number " +
|
||||
"(${newerCordapp.contractVersionId}) compared to corresponding legacy contract CorDapp " +
|
||||
"Newer contract CorDapp '${newerCordapp.jarFile}' does not have a higher versionId " +
|
||||
"(${newerCordapp.contractVersionId}) than corresponding legacy contract CorDapp " +
|
||||
"'${legacyCordapp.jarFile}' (${legacyCordapp.contractVersionId})"
|
||||
}
|
||||
}
|
||||
|
@ -6,10 +6,11 @@ import net.corda.core.internal.copyTo
|
||||
import net.corda.core.internal.level
|
||||
import net.corda.core.internal.mapToSet
|
||||
import net.corda.core.internal.readFully
|
||||
import net.corda.core.internal.toSimpleString
|
||||
import net.corda.core.internal.verification.ExternalVerifierHandle
|
||||
import net.corda.core.internal.verification.NodeVerificationSupport
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.CoreTransaction
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
@ -22,7 +23,7 @@ import net.corda.serialization.internal.verifier.ExternalVerifierInbound.Attachm
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierInbound.Initialisation
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierInbound.NetworkParametersResult
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierInbound.PartiesResult
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierInbound.TrustedClassAttachmentResult
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierInbound.TrustedClassAttachmentsResult
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierInbound.VerificationRequest
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerificationResult
|
||||
@ -31,7 +32,7 @@ import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.Verifi
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetAttachments
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetNetworkParameters
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetParties
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetTrustedClassAttachment
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetTrustedClassAttachments
|
||||
import net.corda.serialization.internal.verifier.readCordaSerializable
|
||||
import net.corda.serialization.internal.verifier.writeCordaSerializable
|
||||
import java.io.DataInputStream
|
||||
@ -74,13 +75,13 @@ class ExternalVerifierHandleImpl(
|
||||
@Volatile
|
||||
private var connection: Connection? = null
|
||||
|
||||
override fun verifyTransaction(stx: SignedTransaction, checkSufficientSignatures: Boolean) {
|
||||
log.info("Verify $stx externally, checkSufficientSignatures=$checkSufficientSignatures")
|
||||
override fun verifyTransaction(ctx: CoreTransaction) {
|
||||
log.info("Verify ${ctx.toSimpleString()} externally")
|
||||
// By definition input states are unique, and so it makes sense to eagerly send them across with the transaction.
|
||||
// Reference states are not, but for now we'll send them anyway and assume they aren't used often. If this assumption is not
|
||||
// correct, and there's a benefit, then we can send them lazily.
|
||||
val stxInputsAndReferences = (stx.inputs + stx.references).associateWith(verificationSupport::getSerializedState)
|
||||
val request = VerificationRequest(stx, stxInputsAndReferences, checkSufficientSignatures)
|
||||
val ctxInputsAndReferences = (ctx.inputs + ctx.references).associateWith(verificationSupport::getSerializedState)
|
||||
val request = VerificationRequest(ctx, ctxInputsAndReferences)
|
||||
|
||||
// To keep things simple the verifier only supports one verification request at a time.
|
||||
synchronized(this) {
|
||||
@ -146,23 +147,22 @@ class ExternalVerifierHandleImpl(
|
||||
private fun processVerifierRequest(request: VerifierRequest, connection: Connection) {
|
||||
val result = when (request) {
|
||||
is GetParties -> PartiesResult(verificationSupport.getParties(request.keys))
|
||||
is GetAttachment -> AttachmentResult(prepare(verificationSupport.getAttachment(request.id)))
|
||||
is GetAttachments -> AttachmentsResult(verificationSupport.getAttachments(request.ids).map(::prepare))
|
||||
is GetAttachment -> AttachmentResult(verificationSupport.getAttachment(request.id)?.withTrust())
|
||||
is GetAttachments -> AttachmentsResult(verificationSupport.getAttachments(request.ids).map { it?.withTrust() })
|
||||
is GetNetworkParameters -> NetworkParametersResult(verificationSupport.getNetworkParameters(request.id))
|
||||
is GetTrustedClassAttachment -> TrustedClassAttachmentResult(verificationSupport.getTrustedClassAttachment(request.className)?.id)
|
||||
is GetTrustedClassAttachments -> TrustedClassAttachmentsResult(verificationSupport.getTrustedClassAttachments(request.className).map { it.id })
|
||||
}
|
||||
log.debug { "Sending response to external verifier: $result" }
|
||||
connection.toVerifier.writeCordaSerializable(result)
|
||||
}
|
||||
|
||||
private fun prepare(attachment: Attachment?): AttachmentWithTrust? {
|
||||
if (attachment == null) return null
|
||||
val isTrusted = verificationSupport.isAttachmentTrusted(attachment)
|
||||
val attachmentForSer = when (attachment) {
|
||||
private fun Attachment.withTrust(): AttachmentWithTrust {
|
||||
val isTrusted = verificationSupport.isAttachmentTrusted(this)
|
||||
val attachmentForSer = when (this) {
|
||||
// The Attachment retrieved from the database is not serialisable, so we have to convert it into one
|
||||
is AbstractAttachment -> GeneratedAttachment(attachment.open().readFully(), attachment.uploader)
|
||||
is AbstractAttachment -> GeneratedAttachment(open().readFully(), uploader)
|
||||
// For everything else we keep as is, in particular preserving ContractAttachment
|
||||
else -> attachment
|
||||
else -> this
|
||||
}
|
||||
return AttachmentWithTrust(attachmentForSer, isTrusted)
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.CoreTransaction
|
||||
import net.corda.core.utilities.Try
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
@ -40,16 +40,17 @@ sealed class ExternalVerifierInbound {
|
||||
}
|
||||
|
||||
data class VerificationRequest(
|
||||
val stx: SignedTransaction,
|
||||
val stxInputsAndReferences: Map<StateRef, SerializedTransactionState>,
|
||||
val checkSufficientSignatures: Boolean
|
||||
) : ExternalVerifierInbound()
|
||||
val ctx: CoreTransaction,
|
||||
val ctxInputsAndReferences: Map<StateRef, SerializedTransactionState>
|
||||
) : ExternalVerifierInbound() {
|
||||
override fun toString(): String = "VerificationRequest(ctx=$ctx)"
|
||||
}
|
||||
|
||||
data class PartiesResult(val parties: List<Party?>) : ExternalVerifierInbound()
|
||||
data class AttachmentResult(val attachment: AttachmentWithTrust?) : ExternalVerifierInbound()
|
||||
data class AttachmentsResult(val attachments: List<AttachmentWithTrust?>) : ExternalVerifierInbound()
|
||||
data class NetworkParametersResult(val networkParameters: NetworkParameters?) : ExternalVerifierInbound()
|
||||
data class TrustedClassAttachmentResult(val id: SecureHash?) : ExternalVerifierInbound()
|
||||
data class TrustedClassAttachmentsResult(val ids: List<SecureHash>) : ExternalVerifierInbound()
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
@ -59,12 +60,12 @@ data class AttachmentWithTrust(val attachment: Attachment, val isTrusted: Boolea
|
||||
sealed class ExternalVerifierOutbound {
|
||||
sealed class VerifierRequest : ExternalVerifierOutbound() {
|
||||
data class GetParties(val keys: Set<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 GetAttachments(val ids: Set<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()
|
||||
|
@ -28,8 +28,8 @@ class ExternalVerificationContext(
|
||||
|
||||
override fun isAttachmentTrusted(attachment: Attachment): Boolean = externalVerifier.getAttachment(attachment.id)!!.isTrusted
|
||||
|
||||
override fun getTrustedClassAttachment(className: String): Attachment? {
|
||||
return externalVerifier.getTrustedClassAttachment(className)
|
||||
override fun getTrustedClassAttachments(className: String): List<Attachment> {
|
||||
return externalVerifier.getTrustedClassAttachments(className)
|
||||
}
|
||||
|
||||
override fun getNetworkParameters(id: SecureHash?): NetworkParameters? = externalVerifier.getNetworkParameters(id)
|
||||
|
@ -7,6 +7,7 @@ import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.loadClassOfType
|
||||
import net.corda.core.internal.mapToSet
|
||||
import net.corda.core.internal.objectOrNewInstance
|
||||
import net.corda.core.internal.toSimpleString
|
||||
import net.corda.core.internal.toSynchronised
|
||||
import net.corda.core.internal.toTypedArray
|
||||
import net.corda.core.internal.verification.AttachmentFixups
|
||||
@ -16,6 +17,8 @@ import net.corda.core.serialization.internal.AttachmentsClassLoaderCache
|
||||
import net.corda.core.serialization.internal.AttachmentsClassLoaderCacheImpl
|
||||
import net.corda.core.serialization.internal.SerializationEnvironment
|
||||
import net.corda.core.serialization.internal._contextSerializationEnv
|
||||
import net.corda.core.transactions.ContractUpgradeWireTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
@ -33,14 +36,14 @@ import net.corda.serialization.internal.verifier.ExternalVerifierInbound.Attachm
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierInbound.Initialisation
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierInbound.NetworkParametersResult
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierInbound.PartiesResult
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierInbound.TrustedClassAttachmentResult
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierInbound.TrustedClassAttachmentsResult
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierInbound.VerificationRequest
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerificationResult
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetAttachment
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetAttachments
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetNetworkParameters
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetParties
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetTrustedClassAttachment
|
||||
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetTrustedClassAttachments
|
||||
import net.corda.serialization.internal.verifier.loadCustomSerializationScheme
|
||||
import net.corda.serialization.internal.verifier.readCordaSerializable
|
||||
import net.corda.serialization.internal.verifier.writeCordaSerializable
|
||||
@ -68,7 +71,7 @@ class ExternalVerifier(
|
||||
private val parties: OptionalCache<PublicKey, Party>
|
||||
private val attachments: OptionalCache<SecureHash, AttachmentWithTrust>
|
||||
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 currentNetworkParameters: NetworkParameters
|
||||
@ -134,13 +137,18 @@ class ExternalVerifier(
|
||||
|
||||
@Suppress("INVISIBLE_MEMBER")
|
||||
private fun verifyTransaction(request: VerificationRequest) {
|
||||
val verificationContext = ExternalVerificationContext(appClassLoader, attachmentsClassLoaderCache, this, request.stxInputsAndReferences)
|
||||
val verificationContext = ExternalVerificationContext(appClassLoader, attachmentsClassLoaderCache, this, request.ctxInputsAndReferences)
|
||||
val result: Try<Unit> = try {
|
||||
request.stx.verifyInProcess(verificationContext, request.checkSufficientSignatures)
|
||||
log.info("${request.stx} verified")
|
||||
val ctx = request.ctx
|
||||
when (ctx) {
|
||||
is WireTransaction -> ctx.verifyInProcess(verificationContext)
|
||||
is ContractUpgradeWireTransaction -> ctx.verifyInProcess(verificationContext)
|
||||
else -> throw IllegalArgumentException("${ctx.toSimpleString()} not supported")
|
||||
}
|
||||
log.info("${ctx.toSimpleString()} verified")
|
||||
Try.Success(Unit)
|
||||
} catch (t: Throwable) {
|
||||
log.info("${request.stx} failed to verify", t)
|
||||
log.info("${request.ctx.toSimpleString()} failed to verify", t)
|
||||
Try.Failure(t)
|
||||
}
|
||||
toNode.writeCordaSerializable(VerificationResult(result))
|
||||
@ -164,13 +172,13 @@ class ExternalVerifier(
|
||||
}
|
||||
}
|
||||
|
||||
fun getTrustedClassAttachment(className: String): Attachment? {
|
||||
val attachmentId = trustedClassAttachments.retrieve(className) {
|
||||
// GetTrustedClassAttachment returns back the attachment ID, not the whole attachment. This lets us avoid downloading the whole
|
||||
// attachment again if we already have it.
|
||||
request<TrustedClassAttachmentResult>(GetTrustedClassAttachment(className)).id
|
||||
}
|
||||
return attachmentId?.let(::getAttachment)?.attachment
|
||||
fun getTrustedClassAttachments(className: String): List<Attachment> {
|
||||
val attachmentIds = trustedClassAttachments.get(className) {
|
||||
// GetTrustedClassAttachments returns back the attachment IDs, not the whole attachments. This lets us avoid downloading the
|
||||
// entire attachments again if we already have them.
|
||||
request<TrustedClassAttachmentsResult>(GetTrustedClassAttachments(className)).ids
|
||||
}!!
|
||||
return attachmentIds.map { getAttachment(it)!!.attachment }
|
||||
}
|
||||
|
||||
fun getNetworkParameters(id: SecureHash?): NetworkParameters? {
|
||||
|
Loading…
Reference in New Issue
Block a user