ENT-11445: Support legacy contract CorDapp dependencies

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

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

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

View File

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

View File

@ -57,55 +57,61 @@ processSmokeTestResources {
from(configurations.corda4_11)
}
processIntegrationTestResources {
from(tasks.getByPath(":finance:contracts:jar")) {
rename 'corda-finance-contracts-.*.jar', 'corda-finance-contracts.jar'
}
from(configurations.corda4_11)
}
processTestResources {
from(configurations.corda4_11)
}
dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}"
testImplementation "junit:junit:$junit_version"
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${junit_vintage_version}"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junit_jupiter_version}"
testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}"
testImplementation "commons-fileupload:commons-fileupload:$fileupload_version"
testImplementation project(":core")
testImplementation project(path: ':core', configuration: 'testArtifacts')
testImplementation project(":serialization")
testImplementation project(":finance:contracts")
testImplementation project(":finance:workflows")
testImplementation project(":node")
testImplementation project(":node-api")
testImplementation project(":client:rpc")
testImplementation project(":serialization")
testImplementation project(":common-configuration-parsing")
testImplementation project(":finance:contracts")
testImplementation project(":finance:workflows")
testImplementation project(":core-test-utils")
testImplementation project(":test-utils")
testImplementation project(path: ':core', configuration: 'testArtifacts')
testImplementation project(":node-driver")
// used by FinalityFlowTests
testImplementation project(':testing:cordapps:cashobservers')
testImplementation(project(path: ':core', configuration: 'testArtifacts')) {
transitive = false
}
testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}"
testImplementation "junit:junit:$junit_version"
testImplementation "commons-fileupload:commons-fileupload:$fileupload_version"
// Guava: Google test library (collections test suite)
testImplementation "com.google.guava:guava-testlib:$guava_version"
testImplementation "com.google.jimfs:jimfs:1.1"
testImplementation group: "com.typesafe", name: "config", version: typesafe_config_version
// Bring in the MockNode infrastructure for writing protocol unit tests.
testImplementation project(":node-driver")
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
testImplementation "com.typesafe:config:$typesafe_config_version"
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
// Hamkrest, for fluent, composable matchers
testImplementation "com.natpryce:hamkrest:$hamkrest_version"
// SLF4J: commons-logging bindings for a SLF4J back end
implementation "org.slf4j:jcl-over-slf4j:$slf4j_version"
implementation "org.slf4j:slf4j-api:$slf4j_version"
testImplementation 'org.hamcrest:hamcrest-library:2.1'
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockito_kotlin_version"
testImplementation "org.mockito:mockito-core:$mockito_version"
// AssertJ: for fluent assertions for testing
testImplementation "org.assertj:assertj-core:${assertj_version}"
// Guava: Google utilities library.
testImplementation "com.google.guava:guava:$guava_version"
testImplementation "com.esotericsoftware:kryo:$kryo_version"
testImplementation "co.paralleluniverse:quasar-core:$quasar_version"
testImplementation "org.hibernate:hibernate-core:$hibernate_version"
testImplementation "org.bouncycastle:bcprov-jdk18on:${bouncycastle_version}"
testImplementation "io.netty:netty-common:$netty_version"
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${junit_vintage_version}"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junit_jupiter_version}"
testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}"
// Smoke tests do NOT have any Node code on the classpath!
smokeTestImplementation project(":core")
@ -127,9 +133,6 @@ dependencies {
smokeTestRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}"
smokeTestRuntimeOnly "org.slf4j:slf4j-simple:$slf4j_version"
// used by FinalityFlowTests
testImplementation project(':testing:cordapps:cashobservers')
corda4_11 "net.corda:corda-finance-contracts:4.11"
corda4_11 "net.corda:corda-finance-workflows:4.11"
corda4_11 "net.corda:corda:4.11"

View File

@ -0,0 +1,159 @@
package net.corda.coretests.transactions
import net.corda.core.internal.copyToDirectory
import net.corda.core.internal.hash
import net.corda.core.internal.toPath
import net.corda.core.messaging.startFlow
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.coretesting.internal.delete
import net.corda.coretesting.internal.modifyJarManifest
import net.corda.coretesting.internal.useZipFile
import net.corda.finance.DOLLARS
import net.corda.finance.flows.CashIssueAndPaymentFlow
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
import net.corda.testing.core.internal.JarSignatureTestUtils.signJar
import net.corda.testing.core.internal.JarSignatureTestUtils.unsignJar
import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.NodeParameters
import net.corda.testing.node.internal.DriverDSLImpl
import net.corda.testing.node.internal.FINANCE_WORKFLOWS_CORDAPP
import net.corda.testing.node.internal.internalDriver
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.nio.file.Path
import kotlin.io.path.Path
import kotlin.io.path.absolutePathString
import kotlin.io.path.copyTo
import kotlin.io.path.createDirectories
import kotlin.io.path.deleteExisting
import kotlin.io.path.div
import kotlin.io.path.inputStream
class TransactionBuilderDriverTest {
companion object {
val currentFinanceContractsJar = this::class.java.getResource("/corda-finance-contracts.jar")!!.toPath()
val legacyFinanceContractsJar = this::class.java.getResource("/corda-finance-contracts-4.11.jar")!!.toPath()
}
@Rule
@JvmField
val tempFolder = TemporaryFolder()
@Before
fun initJarSigner() {
tempFolder.root.toPath().generateKey("testAlias", "testPassword", ALICE_NAME.toString())
}
private fun signJar(jar: Path) {
tempFolder.root.toPath().signJar(jar.absolutePathString(), "testAlias", "testPassword")
}
@Test(timeout=300_000)
fun `adds CorDapp dependencies`() {
val (cordapp, dependency) = splitFinanceContractCordapp(currentFinanceContractsJar)
internalDriver(cordappsForAllNodes = listOf(FINANCE_WORKFLOWS_CORDAPP), startNodesInProcess = false) {
cordapp.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
dependency.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
// Start the node with the CorDapp but without the dependency
cordapp.copyToDirectory((baseDirectory(ALICE_NAME) / "cordapps").createDirectories())
val node = startNode(NodeParameters(ALICE_NAME)).getOrThrow()
// First make sure the missing dependency causes an issue
assertThatThrownBy {
createTransaction(node)
}.hasMessageContaining("java.lang.NoClassDefFoundError: net/corda/finance/contracts/asset")
// Upload the missing dependency
dependency.inputStream().use(node.rpc::uploadAttachment)
val stx = createTransaction(node)
assertThat(stx.tx.attachments).contains(cordapp.hash, dependency.hash)
}
}
@Test(timeout=300_000)
fun `adds legacy contracts CorDapp dependencies`() {
val (legacyContracts, legacyDependency) = splitFinanceContractCordapp(legacyFinanceContractsJar)
// Re-sign the current finance contracts CorDapp with the same key as the split legacy CorDapp
val currentContracts = currentFinanceContractsJar.copyTo(Path("${currentFinanceContractsJar.toString().substringBeforeLast(".")}-RESIGNED.jar"), overwrite = true)
currentContracts.unsignJar()
signJar(currentContracts)
internalDriver(
cordappsForAllNodes = listOf(FINANCE_WORKFLOWS_CORDAPP),
startNodesInProcess = false,
networkParameters = testNetworkParameters(minimumPlatformVersion = 4)
) {
currentContracts.inputStream().use(defaultNotaryNode.getOrThrow().rpc::uploadAttachment)
// Start the node with the legacy CorDapp but without the dependency
legacyContracts.copyToDirectory((baseDirectory(ALICE_NAME) / "legacy-contracts").createDirectories())
currentContracts.copyToDirectory((baseDirectory(ALICE_NAME) / "cordapps").createDirectories())
val node = startNode(NodeParameters(ALICE_NAME)).getOrThrow()
// First make sure the missing dependency causes an issue
assertThatThrownBy {
createTransaction(node)
}.hasMessageContaining("java.lang.NoClassDefFoundError: net/corda/finance/contracts/asset")
// Upload the missing dependency
legacyDependency.inputStream().use(node.rpc::uploadAttachment)
val stx = createTransaction(node)
assertThat(stx.tx.legacyAttachments).contains(legacyContracts.hash, legacyDependency.hash)
}
}
/**
* Split the given finance contracts jar into two such that the second jar becomes a dependency to the first.
*/
private fun splitFinanceContractCordapp(contractsJar: Path): Pair<Path, Path> {
val cordapp = tempFolder.newFile("cordapp.jar").toPath()
val dependency = tempFolder.newFile("cordapp-dep.jar").toPath()
// Split the CorDapp into two
contractsJar.copyTo(cordapp, overwrite = true)
cordapp.useZipFile { cordappZipFs ->
dependency.useZipFile { depZipFs ->
val targetDir = depZipFs.getPath("net/corda/finance/contracts/asset").createDirectories()
// CashUtilities happens to be a class that is only invoked in Cash.verify and so it's absence is only detected during
// verification
val clazz = cordappZipFs.getPath("net/corda/finance/contracts/asset/CashUtilities.class")
clazz.copyToDirectory(targetDir)
clazz.deleteExisting()
}
}
cordapp.modifyJarManifest { manifest ->
manifest.mainAttributes.delete("Sealed")
}
cordapp.unsignJar()
// Sign both current and legacy CorDapps with the same key
signJar(cordapp)
// The dependency needs to be signed as it contains a package from the main jar
signJar(dependency)
return Pair(cordapp, dependency)
}
private fun DriverDSLImpl.createTransaction(node: NodeHandle): SignedTransaction {
return node.rpc.startFlow(
::CashIssueAndPaymentFlow,
1.DOLLARS,
OpaqueBytes.of(0x00),
defaultNotaryIdentity,
false,
defaultNotaryIdentity
).returnValue.getOrThrow().stx
}
}

View File

@ -213,7 +213,7 @@ class ExternalVerificationUnsignedCordappsTest {
).returnValue.getOrThrow()
}
assertThat(newNode.externalVerifierLogs()).contains("$issuanceTx failed to verify")
assertThat(newNode.externalVerifierLogs()).contains("WireTransaction(id=${issuanceTx.id}) failed to verify")
}
}
@ -265,10 +265,10 @@ private fun NodeProcess.assertTransactionsWereVerified(verificationType: Verific
val nodeLogs = logs("node")!!
val externalVerifierLogs = externalVerifierLogs()
for (txId in txIds) {
assertThat(nodeLogs).contains("Transaction $txId has verification type $verificationType")
assertThat(nodeLogs).contains("WireTransaction(id=$txId) will be verified ${verificationType.logStatement}")
if (verificationType != VerificationType.IN_PROCESS) {
assertThat(externalVerifierLogs).describedAs("External verifier was not started").isNotNull()
assertThat(externalVerifierLogs).contains("SignedTransaction(id=$txId) verified")
assertThat(externalVerifierLogs).contains("WireTransaction(id=$txId) verified")
}
}
}
@ -283,5 +283,12 @@ private fun NodeProcess.logs(name: String): String? {
}
private enum class VerificationType {
IN_PROCESS, EXTERNAL, BOTH
IN_PROCESS, EXTERNAL, BOTH;
val logStatement: String
get() = when (this) {
IN_PROCESS -> "in-process"
EXTERNAL -> "by the external verifer"
BOTH -> "both in-process and by the external verifer"
}
}

View File

@ -8,17 +8,13 @@ import net.corda.core.internal.copyToDirectory
import net.corda.core.internal.hash
import net.corda.core.internal.toPath
import net.corda.core.transactions.TransactionBuilder
import net.corda.coretesting.internal.useZipFile
import net.corda.finance.DOLLARS
import net.corda.finance.contracts.asset.Cash
import net.corda.finance.issuedBy
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.contracts.DummyContract
import net.corda.testing.contracts.DummyState
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.DummyCommandData
import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey
import net.corda.testing.core.internal.JarSignatureTestUtils.signJar
import net.corda.testing.core.internal.JarSignatureTestUtils.unsignJar
import net.corda.testing.core.singleIdentity
import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP
@ -28,18 +24,14 @@ import net.corda.testing.node.internal.cordappWithPackages
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
import org.junit.After
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.nio.file.Path
import kotlin.io.path.absolutePathString
import kotlin.io.path.copyTo
import kotlin.io.path.createDirectories
import kotlin.io.path.deleteExisting
import kotlin.io.path.div
import kotlin.io.path.inputStream
import kotlin.io.path.listDirectoryEntries
@Suppress("INVISIBLE_MEMBER")
class TransactionBuilderMockNetworkTest {
@ -107,9 +99,9 @@ class TransactionBuilderMockNetworkTest {
@Test(timeout=300_000)
fun `populates legacy attachment group if legacy contract CorDapp is present`() {
val node = mockNetwork.createNode {
it.copyToLegacyContracts(legacyFinanceContractsJar)
InternalMockNetwork.MockNode(it)
val node = mockNetwork.createNode { args ->
args.copyToLegacyContracts(legacyFinanceContractsJar)
InternalMockNetwork.MockNode(args)
}
val builder = TransactionBuilder()
val identity = node.info.singleIdentity()
@ -120,45 +112,6 @@ class TransactionBuilderMockNetworkTest {
stx.verify(node.services)
}
@Test(timeout=300_000)
@Ignore // https://r3-cev.atlassian.net/browse/ENT-11445
fun `adds legacy CorDapp dependencies`() {
val cordapp1 = tempFolder.newFile("cordapp1.jar").toPath()
val cordapp2 = tempFolder.newFile("cordapp2.jar").toPath()
// Split the contracts CorDapp into two
legacyFinanceContractsJar.copyTo(cordapp1, overwrite = true)
cordapp1.useZipFile { zipFs1 ->
cordapp2.useZipFile { zipFs2 ->
val destinationDir = zipFs2.getPath("net/corda/finance/contracts/asset").createDirectories()
zipFs1.getPath("net/corda/finance/contracts/asset")
.listDirectoryEntries("OnLedgerAsset*")
.forEach {
it.copyToDirectory(destinationDir)
it.deleteExisting()
}
}
}
reSignJar(cordapp1)
val node = mockNetwork.createNode {
it.copyToLegacyContracts(cordapp1, cordapp2)
InternalMockNetwork.MockNode(it)
}
val builder = TransactionBuilder()
val identity = node.info.singleIdentity()
Cash().generateIssue(builder, 10.DOLLARS.issuedBy(identity.ref(0x00)), identity, mockNetwork.defaultNotaryIdentity)
val stx = node.services.signInitialTransaction(builder)
assertThat(stx.tx.nonLegacyAttachments).contains(FINANCE_CONTRACTS_CORDAPP.jarFile.hash)
assertThat(stx.tx.legacyAttachments).contains(cordapp1.hash, cordapp2.hash)
stx.verify(node.services)
}
private fun reSignJar(jar: Path) {
jar.unsignJar()
tempFolder.root.toPath().generateKey("testAlias", "testPassword", ALICE_NAME.toString())
tempFolder.root.toPath().signJar(jar.absolutePathString(), "testAlias", "testPassword")
}
private fun MockNodeArgs.copyToLegacyContracts(vararg jars: Path) {
val legacyContractsDir = (config.baseDirectory / "legacy-contracts").createDirectories()
jars.forEach { it.copyToDirectory(legacyContractsDir) }

View File

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

View File

@ -450,7 +450,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction,
// The notary signature(s) are allowed to be missing but no others.
if (notary != null) transaction.verifySignaturesExcept(notary.owningKey) else transaction.verifyRequiredSignatures()
// TODO= [CORDA-3267] Remove duplicate signature verification
val ltx = transaction.verifyInternal(serviceHub.toVerifyingServiceHub(), checkSufficientSignatures = false) as LedgerTransaction?
val ltx = transaction.verifyInternal(serviceHub.toVerifyingServiceHub(), checkSufficientSignatures = false)
// verifyInternal returns null if the transaction was verified externally, which *could* happen on a very odd scenerio of a 4.11
// node creating the transaction but a 4.12 kicking off finality. In that case, we still want a LedgerTransaction object for
// recording to the vault, etc. Note that calling verify() on this will fail as it doesn't have the necessary non-legacy attachments

View File

@ -68,7 +68,7 @@ abstract class AbstractAttachment(dataLoader: () -> ByteArray, val uploader: Str
override fun equals(other: Any?) = other === this || other is Attachment && other.id == this.id
override fun hashCode() = id.hashCode()
override fun toString() = "${javaClass.simpleName}(id=$id)"
override fun toString() = toSimpleString()
}
@Throws(IOException::class)

View File

@ -2,6 +2,7 @@
package net.corda.core.internal
import net.corda.core.contracts.ContractClassName
import net.corda.core.contracts.NamedByHash
import net.corda.core.contracts.TransactionResolutionException
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.DataVendingFlow
@ -93,3 +94,5 @@ fun TransactionStorage.getRequiredTransaction(txhash: SecureHash): SignedTransac
}
fun ServiceHub.getRequiredTransaction(txhash: SecureHash): SignedTransaction = validatedTransactions.getRequiredTransaction(txhash)
fun NamedByHash.toSimpleString(): String = "${javaClass.simpleName}(id=$id)"

View File

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

View File

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

View File

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

View File

@ -128,25 +128,17 @@ interface NodeVerificationSupport : VerificationSupport {
/**
* Scans trusted (installed locally) attachments to find all that contain the [className].
* This is required as a workaround until explicit cordapp dependencies are implemented.
*
* @return the attachments with the highest version.
* @return attachments containing the given class in descending version order. This means any legacy attachments will occur after the
* current version one.
*/
// TODO Should throw when the class is found in multiple contract attachments (not different versions).
override fun getTrustedClassAttachment(className: String): Attachment? {
override fun getTrustedClassAttachments(className: String): List<Attachment> {
val allTrusted = attachments.queryAttachments(
AttachmentsQueryCriteria().withUploader(Builder.`in`(TRUSTED_UPLOADERS)),
// JarScanningCordappLoader makes sure legacy contract CorDapps have a coresponding non-legacy CorDapp, and that the
// legacy CorDapp has a smaller version number. Thus sorting by the version here ensures we never return the legacy attachment.
AttachmentSort(listOf(AttachmentSortColumn(AttachmentSortAttribute.VERSION, Sort.Direction.DESC)))
)
// TODO - add caching if performance is affected.
for (attId in allTrusted) {
val attch = attachments.openAttachment(attId)!!
if (attch.hasFile("$className.class")) return attch
}
return null
val fileName = "$className.class"
return allTrusted.mapNotNull { id -> attachments.openAttachment(id)!!.takeIf { it.hasFile(fileName) } }
}
private fun Attachment.hasFile(className: String): Boolean = openAsJAR().use { it.entries().any { entry -> entry.name == className } }

View File

@ -0,0 +1,64 @@
package net.corda.core.internal.verification
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.utilities.Try
import net.corda.core.utilities.Try.Failure
import net.corda.core.utilities.Try.Success
sealed class VerificationResult {
/**
* The in-process result for the current version of the transcaction.
*/
abstract val inProcessResult: Try<LedgerTransaction?>?
/**
* The external verifier result for the legacy version of the transaction.
*/
abstract val externalResult: Try<Unit>?
abstract fun enforceSuccess(): LedgerTransaction?
data class InProcess(override val inProcessResult: Try<LedgerTransaction?>) : VerificationResult() {
override val externalResult: Try<Unit>?
get() = null
override fun enforceSuccess(): LedgerTransaction? = inProcessResult.getOrThrow()
}
data class External(override val externalResult: Try<Unit>) : VerificationResult() {
override val inProcessResult: Try<LedgerTransaction?>?
get() = null
override fun enforceSuccess(): LedgerTransaction? {
externalResult.getOrThrow()
// We could create a LedgerTransaction here, and except for calling `verify()`, it would be valid to use. However, it's best
// we let the caller deal with that, since we can't prevent them from calling it.
return null
}
}
data class InProcessAndExternal(
override val inProcessResult: Try<LedgerTransaction>,
override val externalResult: Try<Unit>
) : VerificationResult() {
override fun enforceSuccess(): LedgerTransaction {
return when (externalResult) {
is Success -> when (inProcessResult) {
is Success -> inProcessResult.value
is Failure -> throw IllegalStateException(
"Current version of transaction failed to verify, but legacy version did verify (in external verifier)",
inProcessResult.exception
)
}
is Failure -> throw when (inProcessResult) {
is Success -> IllegalStateException(
"Current version of transaction verified, but legacy version failed to verify (in external verifier)",
externalResult.exception
)
is Failure -> inProcessResult.exception.apply { addSuppressed(externalResult.exception) }
}
}
}
}
}

View File

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

View File

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

View File

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

View File

@ -22,8 +22,10 @@ import net.corda.core.crypto.TransactionSignature
import net.corda.core.identity.Party
import net.corda.core.internal.AttachmentWithContext
import net.corda.core.internal.combinedHash
import net.corda.core.internal.getRequiredSigningKeysInternal
import net.corda.core.internal.loadClassOfType
import net.corda.core.internal.mapToSet
import net.corda.core.internal.verification.NodeVerificationSupport
import net.corda.core.internal.verification.VerificationResult
import net.corda.core.internal.verification.VerificationSupport
import net.corda.core.internal.verification.toVerifyingServiceHub
import net.corda.core.node.NetworkParameters
@ -40,6 +42,7 @@ import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.PARA
import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.UPGRADED_ATTACHMENT
import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.UPGRADED_CONTRACT
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.Try
import net.corda.core.utilities.toBase58String
import java.security.PublicKey
@ -159,6 +162,20 @@ data class ContractUpgradeWireTransaction(
return ContractUpgradeFilteredTransaction(visibleComponents, hiddenComponents, digestService)
}
@CordaInternal
@JvmSynthetic
internal fun tryVerify(verificationSupport: NodeVerificationSupport): VerificationResult.External {
// Contract upgrades only work on 4.11 and earlier
return VerificationResult.External(Try.on { verificationSupport.externalVerifierHandle.verifyTransaction(this) })
}
@CordaInternal
@JvmSynthetic
internal fun verifyInProcess(verificationSupport: VerificationSupport) {
// No contract code is run when verifying contract upgrade transactions, it is sufficient to check invariants during initialisation.
ContractUpgradeLedgerTransaction.resolve(verificationSupport, this, emptyList())
}
enum class Component {
INPUTS, NOTARY, LEGACY_ATTACHMENT, UPGRADED_CONTRACT, UPGRADED_ATTACHMENT, PARAMETERS_HASH
}
@ -344,7 +361,7 @@ private constructor(
/** The required signers are the set of all input states' participants. */
override val requiredSigningKeys: Set<PublicKey>
get() = inputs.flatMap { it.state.data.participants }.mapToSet { it.owningKey } + notary.owningKey
get() = getRequiredSigningKeysInternal(inputs.asSequence(), notary)
override fun getKeyDescriptions(keys: Set<PublicKey>): List<String> {
return keys.map { it.toBase58String() }

View File

@ -11,8 +11,10 @@ import net.corda.core.crypto.DigestService
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.TransactionSignature
import net.corda.core.identity.Party
import net.corda.core.internal.getRequiredSigningKeysInternal
import net.corda.core.internal.indexOfOrThrow
import net.corda.core.internal.mapToSet
import net.corda.core.internal.verification.NodeVerificationSupport
import net.corda.core.internal.verification.VerificationResult
import net.corda.core.internal.verification.VerificationSupport
import net.corda.core.internal.verification.toVerifyingServiceHub
import net.corda.core.node.NetworkParameters
@ -27,6 +29,7 @@ import net.corda.core.transactions.NotaryChangeWireTransaction.Component.NEW_NOT
import net.corda.core.transactions.NotaryChangeWireTransaction.Component.NOTARY
import net.corda.core.transactions.NotaryChangeWireTransaction.Component.PARAMETERS_HASH
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.Try
import net.corda.core.utilities.toBase58String
import java.security.PublicKey
@ -107,6 +110,16 @@ data class NotaryChangeWireTransaction(
return resolve(services as ServicesForResolution, sigs)
}
@CordaInternal
@JvmSynthetic
internal fun tryVerify(verificationSupport: NodeVerificationSupport): VerificationResult.InProcess {
return VerificationResult.InProcess(Try.on {
// No contract code is run when verifying notary change transactions, it is sufficient to check invariants during initialisation.
NotaryChangeLedgerTransaction.resolve(verificationSupport, this, emptyList())
null
})
}
enum class Component {
INPUTS, NOTARY, NEW_NOTARY, PARAMETERS_HASH
}
@ -180,7 +193,7 @@ private constructor(
get() = inputs.map { computeOutput(it, newNotary) { inputs.map(StateAndRef<ContractState>::ref) } }
override val requiredSigningKeys: Set<PublicKey>
get() = inputs.flatMap { it.state.data.participants }.mapToSet { it.owningKey } + notary.owningKey
get() = getRequiredSigningKeysInternal(inputs.asSequence(), notary)
override fun getKeyDescriptions(keys: Set<PublicKey>): List<String> {
return keys.map { it.toBase58String() }

View File

@ -3,12 +3,12 @@ package net.corda.core.transactions
import net.corda.core.CordaException
import net.corda.core.CordaInternal
import net.corda.core.CordaThrowable
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.AttachmentResolutionException
import net.corda.core.contracts.NamedByHash
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TransactionResolutionException
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.contracts.TransactionVerificationException.TransactionNetworkParameterOrderingException
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignableData
import net.corda.core.crypto.SignatureMetadata
@ -16,34 +16,26 @@ import net.corda.core.crypto.TransactionSignature
import net.corda.core.crypto.sign
import net.corda.core.crypto.toStringShort
import net.corda.core.identity.Party
import net.corda.core.internal.TransactionDeserialisationException
import net.corda.core.internal.VisibleForTesting
import net.corda.core.internal.equivalent
import net.corda.core.internal.isUploaderTrusted
import net.corda.core.internal.getRequiredSigningKeysInternal
import net.corda.core.internal.toSimpleString
import net.corda.core.internal.verification.NodeVerificationSupport
import net.corda.core.internal.verification.VerificationSupport
import net.corda.core.internal.verification.toVerifyingServiceHub
import net.corda.core.node.ServiceHub
import net.corda.core.node.ServicesForResolution
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.MissingAttachmentsException
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.internal.MissingSerializerException
import net.corda.core.serialization.serialize
import net.corda.core.utilities.Try
import net.corda.core.utilities.Try.Failure
import net.corda.core.utilities.Try.Success
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import java.io.NotSerializableException
import net.corda.core.utilities.toBase58String
import java.security.KeyPair
import java.security.PublicKey
import java.security.SignatureException
import java.util.function.Predicate
/**
* SignedTransaction wraps a serialized WireTransaction. It contains one or more signatures, each one for
* SignedTransaction wraps a serialized [CoreTransaction], though it will almost exclusively be a [WireTransaction].
* It contains one or more signatures, each one for
* a public key (including composite keys) that is mentioned inside a transaction command. SignedTransaction is the top level transaction type
* and the type most frequently passed around the network and stored. The identity of a transaction is the hash of Merkle root
* of a WireTransaction, therefore if you are storing data keyed by WT hash be aware that multiple different STs may
@ -163,12 +155,6 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
val verifyingServiceHub = services.toVerifyingServiceHub()
// We need parameters check here, because finality flow calls stx.toLedgerTransaction() and then verify.
resolveAndCheckNetworkParameters(verifyingServiceHub)
return toLedgerTransactionInternal(verifyingServiceHub, checkSufficientSignatures)
}
@JvmSynthetic
@CordaInternal
fun toLedgerTransactionInternal(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean): LedgerTransaction {
// TODO: We could probably optimise the below by
// a) not throwing if threshold is eventually satisfied, but some of the rest of the signatures are failing.
// b) omit verifying signatures when threshold requirement is met.
@ -176,12 +162,8 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
// For the above to work, [checkSignaturesAreValid] should take the [requiredSigningKeys] as input
// and probably combine logic from signature validation and key-fulfilment
// in [TransactionWithSignatures.verifySignaturesExcept].
if (checkSufficientSignatures) {
verifyRequiredSignatures() // It internally invokes checkSignaturesAreValid().
} else {
checkSignaturesAreValid()
}
return tx.toLedgerTransactionInternal(verificationSupport)
verifySignatures(verifyingServiceHub, checkSufficientSignatures)
return tx.toLedgerTransactionInternal(verifyingServiceHub)
}
/**
@ -210,252 +192,50 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
*/
@CordaInternal
@JvmSynthetic
internal fun verifyInternal(verificationSupport: NodeVerificationSupport, checkSufficientSignatures: Boolean = true): FullTransaction? {
internal fun verifyInternal(verificationSupport: NodeVerificationSupport, checkSufficientSignatures: Boolean = true): LedgerTransaction? {
resolveAndCheckNetworkParameters(verificationSupport)
val verificationType = determineVerificationType()
log.debug { "Transaction $id has verification type $verificationType" }
return when (verificationType) {
VerificationType.IN_PROCESS -> verifyInProcess(verificationSupport, checkSufficientSignatures)
VerificationType.BOTH -> {
val inProcessResult = Try.on { verifyInProcess(verificationSupport, checkSufficientSignatures) }
val externalResult = Try.on { verificationSupport.externalVerifierHandle.verifyTransaction(this, checkSufficientSignatures) }
ensureSameResult(inProcessResult, externalResult)
}
VerificationType.EXTERNAL -> {
verificationSupport.externalVerifierHandle.verifyTransaction(this, checkSufficientSignatures)
// We could create a LedgerTransaction here, and except for calling `verify()`, it would be valid to use. However, it's best
// we let the caller deal with that, since we can't control what they will do with it.
null
}
}
}
private fun determineVerificationType(): VerificationType {
verifySignatures(verificationSupport, checkSufficientSignatures)
val ctx = coreTransaction
return when (ctx) {
is WireTransaction -> {
when {
ctx.legacyAttachments.isEmpty() -> VerificationType.IN_PROCESS
ctx.nonLegacyAttachments.isEmpty() -> VerificationType.EXTERNAL
else -> VerificationType.BOTH
}
}
// Contract upgrades only work on 4.11 and earlier
is ContractUpgradeWireTransaction -> VerificationType.EXTERNAL
else -> VerificationType.IN_PROCESS // The default is always in-process
}
}
private fun ensureSameResult(inProcessResult: Try<FullTransaction>, externalResult: Try<*>): FullTransaction {
return when (externalResult) {
is Success -> when (inProcessResult) {
is Success -> inProcessResult.value
is Failure -> throw IllegalStateException("In-process verification of $id failed, but it succeeded in external verifier")
.apply { addSuppressed(inProcessResult.exception) }
}
is Failure -> throw when (inProcessResult) {
is Success -> IllegalStateException("In-process verification of $id succeeded, but it failed in external verifier")
is Failure -> inProcessResult.exception // Throw the in-process exception, with the external exception suppressed
}.apply { addSuppressed(externalResult.exception) }
}
}
private enum class VerificationType {
IN_PROCESS, EXTERNAL, BOTH
}
/**
* Verifies this transaction in-process. This assumes the current process has the correct classpath for all the contracts.
*
* @return The [FullTransaction] that was successfully verified
*/
@CordaInternal
@JvmSynthetic
internal fun verifyInProcess(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean): FullTransaction {
return when (coreTransaction) {
is NotaryChangeWireTransaction -> verifyNotaryChangeTransaction(verificationSupport, checkSufficientSignatures)
is ContractUpgradeWireTransaction -> verifyContractUpgradeTransaction(verificationSupport, checkSufficientSignatures)
else -> verifyRegularTransaction(verificationSupport, checkSufficientSignatures)
val verificationResult = when (ctx) {
// TODO: Verify contract constraints here as well as in LedgerTransaction to ensure that anything being deserialised
// from the attachment is trusted. This will require some partial serialisation work to not load the ContractState
// objects from the TransactionState.
is WireTransaction -> ctx.tryVerify(verificationSupport)
is ContractUpgradeWireTransaction -> ctx.tryVerify(verificationSupport)
is NotaryChangeWireTransaction -> ctx.tryVerify(verificationSupport)
else -> throw IllegalStateException("${ctx.toSimpleString()} cannot be verified")
}
return verificationResult.enforceSuccess()
}
@Suppress("ThrowsCount")
private fun resolveAndCheckNetworkParameters(services: NodeVerificationSupport) {
val hashOrDefault = networkParametersHash ?: services.networkParametersService.defaultHash
val txNetworkParameters = services.networkParametersService.lookup(hashOrDefault)
?: throw TransactionResolutionException(id)
val txNetworkParameters = services.networkParametersService.lookup(hashOrDefault) ?: throw TransactionResolutionException(id)
val groupedInputsAndRefs = (inputs + references).groupBy { it.txhash }
groupedInputsAndRefs.map { entry ->
val tx = services.validatedTransactions.getTransaction(entry.key)?.coreTransaction
?: throw TransactionResolutionException(id)
for ((txId, stateRefs) in groupedInputsAndRefs) {
val tx = services.validatedTransactions.getTransaction(txId)?.coreTransaction ?: throw TransactionResolutionException(id)
val paramHash = tx.networkParametersHash ?: services.networkParametersService.defaultHash
val params = services.networkParametersService.lookup(paramHash) ?: throw TransactionResolutionException(id)
if (txNetworkParameters.epoch < params.epoch)
throw TransactionVerificationException.TransactionNetworkParameterOrderingException(id, entry.value.first(), txNetworkParameters, params)
}
}
/** No contract code is run when verifying notary change transactions, it is sufficient to check invariants during initialisation. */
private fun verifyNotaryChangeTransaction(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean): NotaryChangeLedgerTransaction {
val ntx = NotaryChangeLedgerTransaction.resolve(verificationSupport, coreTransaction as NotaryChangeWireTransaction, sigs)
if (checkSufficientSignatures) ntx.verifyRequiredSignatures()
else checkSignaturesAreValid()
return ntx
}
/** No contract code is run when verifying contract upgrade transactions, it is sufficient to check invariants during initialisation. */
private fun verifyContractUpgradeTransaction(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean): ContractUpgradeLedgerTransaction {
val ctx = ContractUpgradeLedgerTransaction.resolve(verificationSupport, coreTransaction as ContractUpgradeWireTransaction, sigs)
if (checkSufficientSignatures) ctx.verifyRequiredSignatures()
else checkSignaturesAreValid()
return ctx
}
// TODO: Verify contract constraints here as well as in LedgerTransaction to ensure that anything being deserialised
// from the attachment is trusted. This will require some partial serialisation work to not load the ContractState
// objects from the TransactionState.
private fun verifyRegularTransaction(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean): LedgerTransaction {
val ltx = toLedgerTransactionInternal(verificationSupport, checkSufficientSignatures)
try {
ltx.verify()
} catch (e: NoClassDefFoundError) {
checkReverifyAllowed(e)
val missingClass = e.message ?: throw e
log.warn("Transaction {} has missing class: {}", ltx.id, missingClass)
reverifyWithFixups(ltx, verificationSupport, missingClass)
} catch (e: NotSerializableException) {
checkReverifyAllowed(e)
retryVerification(e, e, ltx, verificationSupport)
} catch (e: TransactionDeserialisationException) {
checkReverifyAllowed(e)
retryVerification(e.cause, e, ltx, verificationSupport)
}
return ltx
}
private fun checkReverifyAllowed(ex: Throwable) {
// If that transaction was created with and after Corda 4 then just fail.
// The lenient dependency verification is only supported for Corda 3 transactions.
// To detect if the transaction was created before Corda 4 we check if the transaction has the NetworkParameters component group.
if (networkParametersHash != null) {
log.warn("TRANSACTION VERIFY FAILED - No attempt to auto-repair as TX is Corda 4+")
throw ex
}
}
@Suppress("ThrowsCount")
private fun retryVerification(cause: Throwable?, ex: Throwable, ltx: LedgerTransaction, verificationSupport: VerificationSupport) {
when (cause) {
is MissingSerializerException -> {
log.warn("Missing serializers: typeDescriptor={}, typeNames={}", cause.typeDescriptor ?: "<unknown>", cause.typeNames)
reverifyWithFixups(ltx, verificationSupport, null)
if (txNetworkParameters.epoch < params.epoch) {
throw TransactionNetworkParameterOrderingException(id, stateRefs.first(), txNetworkParameters, params)
}
is NotSerializableException -> {
val underlying = cause.cause
if (underlying is ClassNotFoundException) {
val missingClass = underlying.message?.replace('.', '/') ?: throw ex
log.warn("Transaction {} has missing class: {}", ltx.id, missingClass)
reverifyWithFixups(ltx, verificationSupport, missingClass)
} else {
throw ex
}
}
else -> throw ex
}
}
// Transactions created before Corda 4 can be missing dependencies on other CorDapps.
// This code has detected a missing custom serializer - probably located inside a workflow CorDapp.
// We need to extract this CorDapp from AttachmentStorage and try verifying this transaction again.
private fun reverifyWithFixups(ltx: LedgerTransaction, verificationSupport: VerificationSupport, missingClass: String?) {
log.warn("""Detected that transaction $id does not contain all cordapp dependencies.
|This may be the result of a bug in a previous version of Corda.
|Attempting to re-verify having applied this node's fix-up rules.
|Please check with the originator that this is a valid transaction.""".trimMargin())
val replacementAttachments = computeReplacementAttachments(ltx, verificationSupport, missingClass)
log.warn("Reverifying transaction {} with attachments:{}", ltx.id, replacementAttachments)
ltx.verifyInternal(replacementAttachments.toList())
}
private fun computeReplacementAttachments(ltx: LedgerTransaction,
verificationSupport: VerificationSupport,
missingClass: String?): Collection<Attachment> {
val replacements = fixupAttachments(verificationSupport, ltx.attachments)
if (!replacements.equivalent(ltx.attachments)) {
return replacements
}
// We cannot continue unless we have some idea which class is missing from the attachments.
if (missingClass == null) {
throw TransactionVerificationException.BrokenTransactionException(
txId = ltx.id,
message = "No fix-up rules provided for broken attachments: $replacements"
)
}
/*
* The Node's fix-up rules have not been able to adjust the transaction's attachments,
* so resort to the original mechanism of trying to find an attachment that contains
* the missing class.
*/
val extraAttachment = requireNotNull(verificationSupport.getTrustedClassAttachment(missingClass)) {
"""Transaction $ltx is incorrectly formed. Most likely it was created during version 3 of Corda
|when the verification logic was more lenient. Attempted to find local dependency for class: $missingClass,
|but could not find one.
|If you wish to verify this transaction, please contact the originator of the transaction and install the
|provided missing JAR.
|You can install it using the RPC command: `uploadAttachment` without restarting the node.
|""".trimMargin()
}
return replacements.toMutableSet().apply {
/*
* Check our transaction doesn't already contain this extra attachment.
* It seems unlikely that we would, but better safe than sorry!
*/
if (!add(extraAttachment)) {
throw TransactionVerificationException.BrokenTransactionException(
txId = ltx.id,
message = "Unlinkable class $missingClass inside broken attachments: $replacements"
)
private fun verifySignatures(verificationSupport: NodeVerificationSupport, checkSufficientSignatures: Boolean) {
if (checkSufficientSignatures) {
val ctx = coreTransaction
val tws: TransactionWithSignatures = when (ctx) {
is WireTransaction -> this // SignedTransaction implements TransactionWithSignatures in terms of WireTransaction
else -> CoreTransactionWithSignatures(ctx, sigs, verificationSupport)
}
log.warn("""Detected that transaction $ltx does not contain all cordapp dependencies.
|This may be the result of a bug in a previous version of Corda.
|Attempting to verify using the additional trusted dependency: $extraAttachment for class $missingClass.
|Please check with the originator that this is a valid transaction.
|YOU ARE ONLY SEEING THIS MESSAGE BECAUSE THE CORDAPPS THAT CREATED THIS TRANSACTION ARE BROKEN!
|WE HAVE TRIED TO REPAIR THE TRANSACTION AS BEST WE CAN, BUT CANNOT GUARANTEE WE HAVE SUCCEEDED!
|PLEASE FIX THE CORDAPPS AND MIGRATE THESE BROKEN TRANSACTIONS AS SOON AS POSSIBLE!
|THIS MESSAGE IS **SUPPOSED** TO BE SCARY!!
|""".trimMargin()
)
tws.verifyRequiredSignatures() // Internally checkSignaturesAreValid is invoked
} else {
checkSignaturesAreValid()
}
}
/**
* Apply this node's attachment fix-up rules to the given attachments.
*
* @param attachments A collection of [Attachment] objects, e.g. as provided by a transaction.
* @return The [attachments] with the node's fix-up rules applied.
*/
private fun fixupAttachments(verificationSupport: VerificationSupport, attachments: Collection<Attachment>): Collection<Attachment> {
val attachmentsById = attachments.associateByTo(LinkedHashMap(), Attachment::id)
val replacementIds = verificationSupport.fixupAttachmentIds(attachmentsById.keys)
attachmentsById.keys.retainAll(replacementIds)
val extraIds = replacementIds - attachmentsById.keys
val extraAttachments = verificationSupport.getAttachments(extraIds)
for ((index, extraId) in extraIds.withIndex()) {
val extraAttachment = extraAttachments[index]
if (extraAttachment == null || !extraAttachment.isUploaderTrusted()) {
throw MissingAttachmentsException(listOf(extraId))
}
attachmentsById[extraId] = extraAttachment
}
return attachmentsById.values
}
/**
* Resolves the underlying base transaction and then returns it, handling any special case transactions such as
* [NotaryChangeWireTransaction].
@ -512,7 +292,7 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
return ctx.resolve(services, sigs)
}
override fun toString(): String = "${javaClass.simpleName}(id=$id)"
override fun toString(): String = toSimpleString()
private companion object {
private fun missingSignatureMsg(missing: Set<PublicKey>, descriptions: List<String>, id: SecureHash): String {
@ -520,13 +300,28 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
"keys: ${missing.joinToString { it.toStringShort() }}, " +
"by signers: ${descriptions.joinToString()} "
}
private val log = contextLogger()
}
class SignaturesMissingException(val missing: Set<PublicKey>, val descriptions: List<String>, override val id: SecureHash)
: NamedByHash, SignatureException(missingSignatureMsg(missing, descriptions, id)), CordaThrowable by CordaException(missingSignatureMsg(missing, descriptions, id))
/**
* A [TransactionWithSignatures] wrapper for [CoreTransaction]s which need to resolve their input states in order to get at the signers
* list.
*/
private data class CoreTransactionWithSignatures(
private val ctx: CoreTransaction,
override val sigs: List<TransactionSignature>,
private val verificationSupport: NodeVerificationSupport
) : TransactionWithSignatures, NamedByHash by ctx {
override val requiredSigningKeys: Set<PublicKey>
get() = getRequiredSigningKeysInternal(ctx.inputs.asSequence().map(verificationSupport::getStateAndRef), ctx.notary)
override fun getKeyDescriptions(keys: Set<PublicKey>): List<String> = keys.map { it.toBase58String() }
override fun toString(): String = toSimpleString()
}
//region Deprecated
/** Returns the contained [NotaryChangeWireTransaction], or throws if this is a normal transaction. */
@Deprecated("No replacement, this should not be used outside of Corda core")

View File

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

View File

@ -4,6 +4,7 @@ import net.corda.core.DoNotImplement
import net.corda.core.contracts.NamedByHash
import net.corda.core.crypto.TransactionSignature
import net.corda.core.crypto.isFulfilledBy
import net.corda.core.internal.mapToSet
import net.corda.core.transactions.SignedTransaction.SignaturesMissingException
import net.corda.core.utilities.toNonEmptySet
import java.security.InvalidKeyException
@ -99,9 +100,9 @@ interface TransactionWithSignatures : NamedByHash {
* Return the [PublicKey]s for which we still need signatures.
*/
fun getMissingSigners(): Set<PublicKey> {
val sigKeys = sigs.map { it.by }.toSet()
val sigKeys = sigs.mapToSet { it.by }
// TODO Problem is that we can get single PublicKey wrapped as CompositeKey in allowedToBeMissing/mustSign
// equals on CompositeKey won't catch this case (do we want to single PublicKey be equal to the same key wrapped in CompositeKey with threshold 1?)
return requiredSigningKeys.filter { !it.isFulfilledBy(sigKeys) }.toSet()
return requiredSigningKeys.asSequence().filter { !it.isFulfilledBy(sigKeys) }.toSet()
}
}

View File

@ -15,6 +15,7 @@ import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TimeWindow
import net.corda.core.contracts.TransactionResolutionException
import net.corda.core.contracts.TransactionState
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.crypto.DigestService
import net.corda.core.crypto.MerkleTree
import net.corda.core.crypto.SecureHash
@ -24,14 +25,22 @@ import net.corda.core.identity.Party
import net.corda.core.internal.Emoji
import net.corda.core.internal.SerializedStateAndRef
import net.corda.core.internal.SerializedTransactionState
import net.corda.core.internal.TransactionDeserialisationException
import net.corda.core.internal.createComponentGroups
import net.corda.core.internal.deserialiseComponentGroup
import net.corda.core.internal.equivalent
import net.corda.core.internal.flatMapToSet
import net.corda.core.internal.getGroup
import net.corda.core.internal.isUploaderTrusted
import net.corda.core.internal.lazyMapped
import net.corda.core.internal.mapToSet
import net.corda.core.internal.toSimpleString
import net.corda.core.internal.uncheckedCast
import net.corda.core.internal.verification.NodeVerificationSupport
import net.corda.core.internal.verification.VerificationResult
import net.corda.core.internal.verification.VerificationResult.External
import net.corda.core.internal.verification.VerificationResult.InProcess
import net.corda.core.internal.verification.VerificationResult.InProcessAndExternal
import net.corda.core.internal.verification.VerificationSupport
import net.corda.core.internal.verification.toVerifyingServiceHub
import net.corda.core.node.NetworkParameters
@ -39,9 +48,15 @@ import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.AttachmentId
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
import net.corda.core.serialization.MissingAttachmentsException
import net.corda.core.serialization.SerializationFactory
import net.corda.core.serialization.internal.MissingSerializerException
import net.corda.core.serialization.serialize
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.Try
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import java.io.NotSerializableException
import java.security.PublicKey
import java.security.SignatureException
import java.util.function.Predicate
@ -71,7 +86,7 @@ import java.util.function.Predicate
* </ul></p>
*/
@CordaSerializable
@Suppress("ThrowsCount")
@Suppress("ThrowsCount", "TooManyFunctions", "MagicNumber")
class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: PrivacySalt, digestService: DigestService) : TraversableTransaction(componentGroups, digestService) {
constructor(componentGroups: List<ComponentGroup>) : this(componentGroups, PrivacySalt())
@ -164,14 +179,14 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
}
// These are not used
override val appClassLoader: ClassLoader get() = throw AbstractMethodError()
override fun getTrustedClassAttachment(className: String) = throw AbstractMethodError()
override fun getTrustedClassAttachments(className: String) = throw AbstractMethodError()
override fun fixupAttachmentIds(attachmentIds: Collection<SecureHash>) = throw AbstractMethodError()
})
}
@CordaInternal
@JvmSynthetic
fun toLedgerTransactionInternal(verificationSupport: VerificationSupport): LedgerTransaction {
internal fun toLedgerTransactionInternal(verificationSupport: VerificationSupport): LedgerTransaction {
// Look up public keys to authenticated identities.
val authenticatedCommands = if (verificationSupport.isInProcess) {
commands.lazyMapped { cmd, _ ->
@ -360,43 +375,211 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
sig.verify(id)
}
@CordaInternal
@JvmSynthetic
internal fun tryVerify(verificationSupport: NodeVerificationSupport): VerificationResult {
return when {
legacyAttachments.isEmpty() -> {
log.debug { "${toSimpleString()} will be verified in-process" }
InProcess(Try.on { verifyInProcess(verificationSupport) })
}
nonLegacyAttachments.isEmpty() -> {
log.debug { "${toSimpleString()} will be verified by the external verifer" }
External(Try.on { verificationSupport.externalVerifierHandle.verifyTransaction(this) })
}
else -> {
log.debug { "${toSimpleString()} will be verified both in-process and by the external verifer" }
val inProcessResult = Try.on { verifyInProcess(verificationSupport) }
val externalResult = Try.on { verificationSupport.externalVerifierHandle.verifyTransaction(this) }
InProcessAndExternal(inProcessResult, externalResult)
}
}
}
@CordaInternal
@JvmSynthetic
internal fun verifyInProcess(verificationSupport: VerificationSupport): LedgerTransaction {
val ltx = toLedgerTransactionInternal(verificationSupport)
try {
ltx.verify()
} catch (e: NoClassDefFoundError) {
checkReverifyAllowed(e)
val missingClass = e.message ?: throw e
log.warn("Transaction {} has missing class: {}", ltx.id, missingClass)
reverifyWithFixups(ltx, verificationSupport, missingClass)
} catch (e: NotSerializableException) {
checkReverifyAllowed(e)
retryVerification(e, e, ltx, verificationSupport)
} catch (e: TransactionDeserialisationException) {
checkReverifyAllowed(e)
retryVerification(e.cause, e, ltx, verificationSupport)
}
return ltx
}
private fun checkReverifyAllowed(ex: Throwable) {
// If that transaction was created with and after Corda 4 then just fail.
// The lenient dependency verification is only supported for Corda 3 transactions.
// To detect if the transaction was created before Corda 4 we check if the transaction has the NetworkParameters component group.
if (networkParametersHash != null) {
log.warn("TRANSACTION VERIFY FAILED - No attempt to auto-repair as TX is Corda 4+")
throw ex
}
}
private fun retryVerification(cause: Throwable?, ex: Throwable, ltx: LedgerTransaction, verificationSupport: VerificationSupport) {
when (cause) {
is MissingSerializerException -> {
log.warn("Missing serializers: typeDescriptor={}, typeNames={}", cause.typeDescriptor ?: "<unknown>", cause.typeNames)
reverifyWithFixups(ltx, verificationSupport, null)
}
is NotSerializableException -> {
val underlying = cause.cause
if (underlying is ClassNotFoundException) {
val missingClass = underlying.message?.replace('.', '/') ?: throw ex
log.warn("Transaction {} has missing class: {}", ltx.id, missingClass)
reverifyWithFixups(ltx, verificationSupport, missingClass)
} else {
throw ex
}
}
else -> throw ex
}
}
// Transactions created before Corda 4 can be missing dependencies on other CorDapps.
// This code has detected a missing custom serializer - probably located inside a workflow CorDapp.
// We need to extract this CorDapp from AttachmentStorage and try verifying this transaction again.
private fun reverifyWithFixups(ltx: LedgerTransaction, verificationSupport: VerificationSupport, missingClass: String?) {
log.warn("""Detected that transaction $id does not contain all cordapp dependencies.
|This may be the result of a bug in a previous version of Corda.
|Attempting to re-verify having applied this node's fix-up rules.
|Please check with the originator that this is a valid transaction.""".trimMargin())
val replacementAttachments = computeReplacementAttachments(ltx, verificationSupport, missingClass)
log.warn("Reverifying transaction {} with attachments:{}", ltx.id, replacementAttachments)
ltx.verifyInternal(replacementAttachments.toList())
}
private fun computeReplacementAttachments(ltx: LedgerTransaction,
verificationSupport: VerificationSupport,
missingClass: String?): Collection<Attachment> {
val replacements = fixupAttachments(verificationSupport, ltx.attachments)
if (!replacements.equivalent(ltx.attachments)) {
return replacements
}
// We cannot continue unless we have some idea which class is missing from the attachments.
if (missingClass == null) {
throw TransactionVerificationException.BrokenTransactionException(
txId = ltx.id,
message = "No fix-up rules provided for broken attachments: $replacements"
)
}
/*
* The Node's fix-up rules have not been able to adjust the transaction's attachments,
* so resort to the original mechanism of trying to find an attachment that contains
* the missing class.
*/
val extraAttachment = requireNotNull(verificationSupport.getTrustedClassAttachments(missingClass).firstOrNull()) {
"""Transaction $ltx is incorrectly formed. Most likely it was created during version 3 of Corda
|when the verification logic was more lenient. Attempted to find local dependency for class: $missingClass,
|but could not find one.
|If you wish to verify this transaction, please contact the originator of the transaction and install the
|provided missing JAR.
|You can install it using the RPC command: `uploadAttachment` without restarting the node.
|""".trimMargin()
}
return replacements.toMutableSet().apply {
/*
* Check our transaction doesn't already contain this extra attachment.
* It seems unlikely that we would, but better safe than sorry!
*/
if (!add(extraAttachment)) {
throw TransactionVerificationException.BrokenTransactionException(
txId = ltx.id,
message = "Unlinkable class $missingClass inside broken attachments: $replacements"
)
}
log.warn("""Detected that transaction $ltx does not contain all cordapp dependencies.
|This may be the result of a bug in a previous version of Corda.
|Attempting to verify using the additional trusted dependency: $extraAttachment for class $missingClass.
|Please check with the originator that this is a valid transaction.
|YOU ARE ONLY SEEING THIS MESSAGE BECAUSE THE CORDAPPS THAT CREATED THIS TRANSACTION ARE BROKEN!
|WE HAVE TRIED TO REPAIR THE TRANSACTION AS BEST WE CAN, BUT CANNOT GUARANTEE WE HAVE SUCCEEDED!
|PLEASE FIX THE CORDAPPS AND MIGRATE THESE BROKEN TRANSACTIONS AS SOON AS POSSIBLE!
|THIS MESSAGE IS **SUPPOSED** TO BE SCARY!!
|""".trimMargin()
)
}
}
/**
* Apply this node's attachment fix-up rules to the given attachments.
*
* @param attachments A collection of [Attachment] objects, e.g. as provided by a transaction.
* @return The [attachments] with the node's fix-up rules applied.
*/
private fun fixupAttachments(verificationSupport: VerificationSupport, attachments: Collection<Attachment>): Collection<Attachment> {
val attachmentsById = attachments.associateByTo(LinkedHashMap(), Attachment::id)
val replacementIds = verificationSupport.fixupAttachmentIds(attachmentsById.keys)
attachmentsById.keys.retainAll(replacementIds)
val extraIds = replacementIds - attachmentsById.keys
val extraAttachments = verificationSupport.getAttachments(extraIds)
for ((index, extraId) in extraIds.withIndex()) {
val extraAttachment = extraAttachments[index]
if (extraAttachment == null || !extraAttachment.isUploaderTrusted()) {
throw MissingAttachmentsException(listOf(extraId))
}
attachmentsById[extraId] = extraAttachment
}
return attachmentsById.values
}
override fun toString(): String {
val buf = StringBuilder()
buf.appendLine("Transaction:")
val buf = StringBuilder(1024)
buf.appendLine("Transaction $id:")
for (reference in references) {
val emoji = Emoji.rightArrow
buf.appendLine("${emoji}REFS: $reference")
buf.appendLine("${emoji}REFS: $reference")
}
for (input in inputs) {
val emoji = Emoji.rightArrow
buf.appendLine("${emoji}INPUT: $input")
buf.appendLine("${emoji}INPUT: $input")
}
for ((data) in outputs) {
val emoji = Emoji.leftArrow
buf.appendLine("${emoji}OUTPUT: $data")
buf.appendLine("${emoji}OUTPUT: $data")
}
for (command in commands) {
val emoji = Emoji.diamond
buf.appendLine("${emoji}COMMAND: $command")
buf.appendLine("${emoji}COMMAND: $command")
}
for (attachment in attachments) {
for (attachment in nonLegacyAttachments) {
val emoji = Emoji.paperclip
buf.appendLine("${emoji}ATTACHMENT: $attachment")
buf.appendLine("${emoji}ATTACHMENT: $attachment")
}
for (attachment in legacyAttachments) {
val emoji = Emoji.paperclip
buf.appendLine("${emoji}ATTACHMENT: $attachment (legacy)")
}
if (networkParametersHash != null) {
buf.appendLine("PARAMETERS HASH: $networkParametersHash")
val emoji = Emoji.newspaper
buf.appendLine("${emoji}NETWORK PARAMS: $networkParametersHash")
}
return buf.toString()
}
override fun equals(other: Any?): Boolean {
if (other is WireTransaction) {
return (this.id == other.id)
}
return false
}
override fun equals(other: Any?): Boolean = other is WireTransaction && this.id == other.id
override fun hashCode(): Int = id.hashCode()
private companion object {
private val log = contextLogger()
}
}
/**

View File

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

View File

@ -6,10 +6,11 @@ import net.corda.core.internal.copyTo
import net.corda.core.internal.level
import net.corda.core.internal.mapToSet
import net.corda.core.internal.readFully
import net.corda.core.internal.toSimpleString
import net.corda.core.internal.verification.ExternalVerifierHandle
import net.corda.core.internal.verification.NodeVerificationSupport
import net.corda.core.serialization.serialize
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.CoreTransaction
import net.corda.core.utilities.Try
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
@ -22,7 +23,7 @@ import net.corda.serialization.internal.verifier.ExternalVerifierInbound.Attachm
import net.corda.serialization.internal.verifier.ExternalVerifierInbound.Initialisation
import net.corda.serialization.internal.verifier.ExternalVerifierInbound.NetworkParametersResult
import net.corda.serialization.internal.verifier.ExternalVerifierInbound.PartiesResult
import net.corda.serialization.internal.verifier.ExternalVerifierInbound.TrustedClassAttachmentResult
import net.corda.serialization.internal.verifier.ExternalVerifierInbound.TrustedClassAttachmentsResult
import net.corda.serialization.internal.verifier.ExternalVerifierInbound.VerificationRequest
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerificationResult
@ -31,7 +32,7 @@ import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.Verifi
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetAttachments
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetNetworkParameters
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetParties
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetTrustedClassAttachment
import net.corda.serialization.internal.verifier.ExternalVerifierOutbound.VerifierRequest.GetTrustedClassAttachments
import net.corda.serialization.internal.verifier.readCordaSerializable
import net.corda.serialization.internal.verifier.writeCordaSerializable
import java.io.DataInputStream
@ -74,13 +75,13 @@ class ExternalVerifierHandleImpl(
@Volatile
private var connection: Connection? = null
override fun verifyTransaction(stx: SignedTransaction, checkSufficientSignatures: Boolean) {
log.info("Verify $stx externally, checkSufficientSignatures=$checkSufficientSignatures")
override fun verifyTransaction(ctx: CoreTransaction) {
log.info("Verify ${ctx.toSimpleString()} externally")
// By definition input states are unique, and so it makes sense to eagerly send them across with the transaction.
// Reference states are not, but for now we'll send them anyway and assume they aren't used often. If this assumption is not
// correct, and there's a benefit, then we can send them lazily.
val stxInputsAndReferences = (stx.inputs + stx.references).associateWith(verificationSupport::getSerializedState)
val request = VerificationRequest(stx, stxInputsAndReferences, checkSufficientSignatures)
val ctxInputsAndReferences = (ctx.inputs + ctx.references).associateWith(verificationSupport::getSerializedState)
val request = VerificationRequest(ctx, ctxInputsAndReferences)
// To keep things simple the verifier only supports one verification request at a time.
synchronized(this) {
@ -146,23 +147,22 @@ class ExternalVerifierHandleImpl(
private fun processVerifierRequest(request: VerifierRequest, connection: Connection) {
val result = when (request) {
is GetParties -> PartiesResult(verificationSupport.getParties(request.keys))
is GetAttachment -> AttachmentResult(prepare(verificationSupport.getAttachment(request.id)))
is GetAttachments -> AttachmentsResult(verificationSupport.getAttachments(request.ids).map(::prepare))
is GetAttachment -> AttachmentResult(verificationSupport.getAttachment(request.id)?.withTrust())
is GetAttachments -> AttachmentsResult(verificationSupport.getAttachments(request.ids).map { it?.withTrust() })
is GetNetworkParameters -> NetworkParametersResult(verificationSupport.getNetworkParameters(request.id))
is GetTrustedClassAttachment -> TrustedClassAttachmentResult(verificationSupport.getTrustedClassAttachment(request.className)?.id)
is GetTrustedClassAttachments -> TrustedClassAttachmentsResult(verificationSupport.getTrustedClassAttachments(request.className).map { it.id })
}
log.debug { "Sending response to external verifier: $result" }
connection.toVerifier.writeCordaSerializable(result)
}
private fun prepare(attachment: Attachment?): AttachmentWithTrust? {
if (attachment == null) return null
val isTrusted = verificationSupport.isAttachmentTrusted(attachment)
val attachmentForSer = when (attachment) {
private fun Attachment.withTrust(): AttachmentWithTrust {
val isTrusted = verificationSupport.isAttachmentTrusted(this)
val attachmentForSer = when (this) {
// The Attachment retrieved from the database is not serialisable, so we have to convert it into one
is AbstractAttachment -> GeneratedAttachment(attachment.open().readFully(), attachment.uploader)
is AbstractAttachment -> GeneratedAttachment(open().readFully(), uploader)
// For everything else we keep as is, in particular preserving ContractAttachment
else -> attachment
else -> this
}
return AttachmentWithTrust(attachmentForSer, isTrusted)
}

View File

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

View File

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

View File

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