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