diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 56c1bd80e4..72eea8da0a 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -7924,11 +7924,6 @@ public final class net.corda.core.transactions.WireTransaction extends net.corda public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServicesForResolution) @NotNull public String toString() - @NotNull - public static final net.corda.core.transactions.WireTransaction$Companion Companion -## -public static final class net.corda.core.transactions.WireTransaction$Companion extends java.lang.Object - public (kotlin.jvm.internal.DefaultConstructorMarker) ## public final class net.corda.core.utilities.ByteArrays extends java.lang.Object @NotNull diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt index 24e3efd707..3dddecec1a 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt @@ -95,7 +95,8 @@ import java.math.BigDecimal import java.security.PublicKey import java.security.cert.CertPath import java.time.Instant -import java.util.* +import java.util.Currency +import java.util.UUID class CordaModule : SimpleModule("corda-core") { override fun setupModule(context: SetupContext) { @@ -256,6 +257,7 @@ private data class StxJson( private interface WireTransactionMixin private class WireTransactionSerializer : JsonSerializer() { + @Suppress("INVISIBLE_MEMBER") override fun serialize(value: WireTransaction, gen: JsonGenerator, serializers: SerializerProvider) { gen.writeObject(WireTransactionJson( value.digestService, @@ -265,7 +267,7 @@ private class WireTransactionSerializer : JsonSerializer() { value.outputs, value.commands, value.timeWindow, - value.attachments, + value.legacyAttachments.map { "$it-legacy" } + value.nonLegacyAttachments.map { it.toString() }, value.references, value.privacySalt, value.networkParametersHash @@ -276,15 +278,18 @@ private class WireTransactionSerializer : JsonSerializer() { private class WireTransactionDeserializer : JsonDeserializer() { override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): WireTransaction { val wrapper = parser.readValueAs() + // We're not concerned with backwards compatibility for any JSON string that was created with 4.11 and being materialised in 4.12. + val (legacyAttachments, newerAttachments) = wrapper.attachments.partition { it.endsWith("-legacy") } val componentGroups = createComponentGroups( wrapper.inputs, wrapper.outputs, wrapper.commands, - wrapper.attachments, + newerAttachments.map(SecureHash::parse), wrapper.notary, wrapper.timeWindow, wrapper.references, - wrapper.networkParametersHash + wrapper.networkParametersHash, + legacyAttachments.map { SecureHash.parse(it.removeSuffix("-legacy")) } ) return WireTransaction(componentGroups, wrapper.privacySalt, wrapper.digestService ?: DigestService.sha2_256) } @@ -297,10 +302,11 @@ private class WireTransactionJson(@get:JsonInclude(Include.NON_NULL) val digestS val outputs: List>, val commands: List>, val timeWindow: TimeWindow?, - val attachments: List, + val attachments: List, val references: List, val privacySalt: PrivacySalt, - val networkParametersHash: SecureHash?) + val networkParametersHash: SecureHash? +) private interface TransactionStateMixin { @get:JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) diff --git a/core-tests/build.gradle b/core-tests/build.gradle index 39da988bf4..80c9b5c6ab 100644 --- a/core-tests/build.gradle +++ b/core-tests/build.gradle @@ -57,6 +57,10 @@ processSmokeTestResources { 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" diff --git a/core-tests/src/smoke-test/kotlin/net/corda/coretests/verification/ExternalVerificationTests.kt b/core-tests/src/smoke-test/kotlin/net/corda/coretests/verification/ExternalVerificationTests.kt index c62690e14e..415161f797 100644 --- a/core-tests/src/smoke-test/kotlin/net/corda/coretests/verification/ExternalVerificationTests.kt +++ b/core-tests/src/smoke-test/kotlin/net/corda/coretests/verification/ExternalVerificationTests.kt @@ -1,6 +1,5 @@ package net.corda.coretests.verification -import co.paralleluniverse.strands.concurrent.CountDownLatch import net.corda.client.rpc.CordaRPCClientConfiguration import net.corda.client.rpc.notUsed import net.corda.core.contracts.Amount @@ -13,12 +12,18 @@ import net.corda.core.internal.toPath import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow import net.corda.core.node.NodeInfo +import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow +import net.corda.coretests.verification.VerificationType.BOTH +import net.corda.coretests.verification.VerificationType.EXTERNAL import net.corda.finance.DOLLARS +import net.corda.finance.USD +import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.AbstractCashFlow import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow +import net.corda.finance.workflows.getCashBalance import net.corda.nodeapi.internal.config.User import net.corda.smoketesting.NodeParams import net.corda.smoketesting.NodeProcess @@ -31,15 +36,16 @@ import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.AfterClass import org.junit.BeforeClass import org.junit.Test +import rx.Observable import java.net.InetAddress import java.nio.file.Path import java.util.Currency +import java.util.concurrent.CompletableFuture import java.util.concurrent.atomic.AtomicInteger import kotlin.io.path.Path import kotlin.io.path.copyTo import kotlin.io.path.div import kotlin.io.path.listDirectoryEntries -import kotlin.io.path.name import kotlin.io.path.readText class ExternalVerificationSignedCordappsTest { @@ -48,27 +54,30 @@ class ExternalVerificationSignedCordappsTest { private lateinit var notaries: List private lateinit var oldNode: NodeProcess - private lateinit var newNode: NodeProcess + private lateinit var currentNode: NodeProcess @BeforeClass @JvmStatic fun startNodes() { - // The 4.11 finance CorDapp jars - val oldCordapps = listOf("contracts", "workflows").map { smokeTestResource("corda-finance-$it-4.11.jar") } + val (legacyContractsCordapp, legacyWorkflowsCordapp) = listOf("contracts", "workflows").map { smokeTestResource("corda-finance-$it-4.11.jar") } // The current version finance CorDapp jars - val newCordapps = listOf("contracts", "workflows").map { smokeTestResource("corda-finance-$it.jar") } + val currentCordapps = listOf("contracts", "workflows").map { smokeTestResource("corda-finance-$it.jar") } notaries = factory.createNotaries( - nodeParams(DUMMY_NOTARY_NAME, oldCordapps), - nodeParams(CordaX500Name("Notary Service 2", "Zurich", "CH"), newCordapps) + nodeParams(DUMMY_NOTARY_NAME, cordappJars = currentCordapps, legacyContractJars = listOf(legacyContractsCordapp)), + nodeParams(CordaX500Name("Notary Service 2", "Zurich", "CH"), currentCordapps) ) oldNode = factory.createNode(nodeParams( CordaX500Name("Old", "Delhi", "IN"), - oldCordapps + listOf(smokeTestResource("4.11-workflows-cordapp.jar")), - CordaRPCClientConfiguration(minimumServerProtocolVersion = 13), + listOf(legacyContractsCordapp, legacyWorkflowsCordapp, smokeTestResource("4.11-workflows-cordapp.jar")), + clientRpcConfig = CordaRPCClientConfiguration(minimumServerProtocolVersion = 13), version = "4.11" )) - newNode = factory.createNode(nodeParams(CordaX500Name("New", "York", "US"), newCordapps)) + currentNode = factory.createNode(nodeParams( + CordaX500Name("New", "York", "US"), + currentCordapps, + listOf(legacyContractsCordapp) + )) } @AfterClass @@ -79,8 +88,17 @@ class ExternalVerificationSignedCordappsTest { } @Test(timeout=300_000) - fun `transaction containing 4_11 contract sent to new node`() { - assertCashIssuanceAndPayment(issuer = oldNode, recipient = newNode) + fun `transaction containing 4_11 contract attachment only sent to current node`() { + val (issuanceTx, paymentTx) = cashIssuanceAndPayment(issuer = oldNode, recipient = currentNode) + notaries[0].assertTransactionsWereVerified(EXTERNAL, paymentTx.id) + currentNode.assertTransactionsWereVerified(EXTERNAL, issuanceTx.id, paymentTx.id) + } + + @Test(timeout=300_000) + fun `transaction containing 4_11 and 4_12 contract attachments sent to old node`() { + val (issuanceTx, paymentTx) = cashIssuanceAndPayment(issuer = currentNode, recipient = oldNode) + notaries[0].assertTransactionsWereVerified(BOTH, paymentTx.id) + currentNode.assertTransactionsWereVerified(BOTH, issuanceTx.id, paymentTx.id) } @Test(timeout=300_000) @@ -94,12 +112,14 @@ class ExternalVerificationSignedCordappsTest { oldRpc.startFlow(::IssueAndChangeNotaryFlow, notaryIdentities[0], notaryIdentities[1]).returnValue.getOrThrow() } - private fun assertCashIssuanceAndPayment(issuer: NodeProcess, recipient: NodeProcess) { + private fun cashIssuanceAndPayment(issuer: NodeProcess, recipient: NodeProcess): Pair { val issuerRpc = issuer.connect(superUser).proxy val recipientRpc = recipient.connect(superUser).proxy val recipientNodeInfo = recipientRpc.nodeInfo() val notaryIdentity = issuerRpc.notaryIdentities()[0] + val beforeAmount = recipientRpc.getCashBalance(USD) + val (issuanceTx) = issuerRpc.startFlow( ::CashIssueFlow, 10.DOLLARS, @@ -110,6 +130,9 @@ class ExternalVerificationSignedCordappsTest { issuerRpc.waitForVisibility(recipientNodeInfo) recipientRpc.waitForVisibility(issuerRpc.nodeInfo()) + val (_, update) = recipientRpc.vaultTrack(Cash.State::class.java) + val cashArrived = update.waitForFirst { true } + val (paymentTx) = issuerRpc.startFlow( ::CashPaymentFlow, 10.DOLLARS, @@ -117,8 +140,10 @@ class ExternalVerificationSignedCordappsTest { false, ).returnValue.getOrThrow() - notaries[0].assertTransactionsWereVerifiedExternally(issuanceTx.id, paymentTx.id) - recipient.assertTransactionsWereVerifiedExternally(issuanceTx.id, paymentTx.id) + cashArrived.getOrThrow() + assertThat(recipientRpc.getCashBalance(USD) - beforeAmount).isEqualTo(10.DOLLARS) + + return Pair(issuanceTx, paymentTx) } } @@ -134,18 +159,18 @@ class ExternalVerificationUnsignedCordappsTest { @JvmStatic fun startNodes() { // The 4.11 finance CorDapp jars - val oldCordapps = listOf(unsignedResourceJar("corda-finance-contracts-4.11.jar"), smokeTestResource("corda-finance-workflows-4.11.jar")) + val legacyCordapps = listOf(unsignedResourceJar("corda-finance-contracts-4.11.jar"), smokeTestResource("corda-finance-workflows-4.11.jar")) // The current version finance CorDapp jars - val newCordapps = listOf(unsignedResourceJar("corda-finance-contracts.jar"), smokeTestResource("corda-finance-workflows.jar")) + val currentCordapps = listOf(unsignedResourceJar("corda-finance-contracts.jar"), smokeTestResource("corda-finance-workflows.jar")) - notary = factory.createNotaries(nodeParams(DUMMY_NOTARY_NAME, oldCordapps))[0] + notary = factory.createNotaries(nodeParams(DUMMY_NOTARY_NAME, currentCordapps))[0] oldNode = factory.createNode(nodeParams( CordaX500Name("Old", "Delhi", "IN"), - oldCordapps, - CordaRPCClientConfiguration(minimumServerProtocolVersion = 13), + legacyCordapps, + clientRpcConfig = CordaRPCClientConfiguration(minimumServerProtocolVersion = 13), version = "4.11" )) - newNode = factory.createNode(nodeParams(CordaX500Name("New", "York", "US"), newCordapps)) + newNode = factory.createNode(nodeParams(CordaX500Name("New", "York", "US"), currentCordapps)) } @AfterClass @@ -200,6 +225,7 @@ private fun smokeTestResource(name: String): Path = ExternalVerificationSignedCo private fun nodeParams( legalName: CordaX500Name, cordappJars: List = emptyList(), + legacyContractJars: List = emptyList(), clientRpcConfig: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT, version: String? = null ): NodeParams { @@ -210,6 +236,7 @@ private fun nodeParams( rpcAdminPort = portCounter.andIncrement, users = listOf(superUser), cordappJars = cordappJars, + legacyContractJars = legacyContractJars, clientRpcConfig = clientRpcConfig, version = version ) @@ -220,28 +247,41 @@ private fun CordaRPCOps.waitForVisibility(other: NodeInfo) { if (other in snapshot) { updates.notUsed() } else { - val found = CountDownLatch(1) - val subscription = updates.subscribe { - if (it.node == other) { - found.countDown() - } + updates.waitForFirst { it.node == other }.getOrThrow() + } +} + +private fun Observable.waitForFirst(predicate: (T) -> Boolean): CompletableFuture { + val found = CompletableFuture() + val subscription = subscribe { + if (predicate(it)) { + found.complete(Unit) } - found.await() - subscription.unsubscribe() } + return found.whenComplete { _, _ -> subscription.unsubscribe() } } -private fun NodeProcess.assertTransactionsWereVerifiedExternally(vararg txIds: SecureHash) { - val verifierLogContent = externalVerifierLogs() +private fun NodeProcess.assertTransactionsWereVerified(verificationType: VerificationType, vararg txIds: SecureHash) { + val nodeLogs = logs("node")!! + val externalVerifierLogs = externalVerifierLogs() for (txId in txIds) { - assertThat(verifierLogContent).contains("SignedTransaction(id=$txId) verified") + assertThat(nodeLogs).contains("Transaction $txId has verification type $verificationType") + if (verificationType != VerificationType.IN_PROCESS) { + assertThat(externalVerifierLogs).describedAs("External verifier was not started").isNotNull() + assertThat(externalVerifierLogs).contains("SignedTransaction(id=$txId) verified") + } } } -private fun NodeProcess.externalVerifierLogs(): String { - val verifierLogs = (nodeDir / "logs") - .listDirectoryEntries() - .filter { it.name == "verifier-${InetAddress.getLocalHost().hostName}.log" } - assertThat(verifierLogs).describedAs("External verifier was not started").hasSize(1) - return verifierLogs[0].readText() +private fun NodeProcess.externalVerifierLogs(): String? = logs("verifier") + +private fun NodeProcess.logs(name: String): String? { + return (nodeDir / "logs") + .listDirectoryEntries("$name-${InetAddress.getLocalHost().hostName}.log") + .singleOrNull() + ?.readText() } + +private enum class VerificationType { + IN_PROCESS, EXTERNAL, BOTH +} \ No newline at end of file diff --git a/core-tests/src/test/kotlin/net/corda/coretests/transactions/CompatibleTransactionTests.kt b/core-tests/src/test/kotlin/net/corda/coretests/transactions/CompatibleTransactionTests.kt index 651aa2d8f7..7fc2ad4f1c 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/transactions/CompatibleTransactionTests.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/transactions/CompatibleTransactionTests.kt @@ -1,23 +1,56 @@ package net.corda.coretests.transactions -import net.corda.core.contracts.* -import net.corda.core.contracts.ComponentGroupEnum.* -import net.corda.core.crypto.* +import net.corda.core.contracts.Command +import net.corda.core.contracts.ComponentGroupEnum +import net.corda.core.contracts.ComponentGroupEnum.ATTACHMENTS_V2_GROUP +import net.corda.core.contracts.ComponentGroupEnum.COMMANDS_GROUP +import net.corda.core.contracts.ComponentGroupEnum.INPUTS_GROUP +import net.corda.core.contracts.ComponentGroupEnum.NOTARY_GROUP +import net.corda.core.contracts.ComponentGroupEnum.OUTPUTS_GROUP +import net.corda.core.contracts.ComponentGroupEnum.PARAMETERS_GROUP +import net.corda.core.contracts.ComponentGroupEnum.SIGNERS_GROUP +import net.corda.core.contracts.ComponentGroupEnum.TIMEWINDOW_GROUP +import net.corda.core.contracts.PrivacySalt +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TimeWindow +import net.corda.core.contracts.TransactionState +import net.corda.core.crypto.DigestService +import net.corda.core.crypto.MerkleTree +import net.corda.core.crypto.PartialMerkleTree +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.generateKeyPair +import net.corda.core.crypto.secureRandomBytes import net.corda.core.internal.accessAvailableComponentHashes import net.corda.core.internal.accessGroupHashes import net.corda.core.internal.accessGroupMerkleRoots import net.corda.core.internal.createComponentGroups +import net.corda.core.internal.getRequiredGroup import net.corda.core.serialization.serialize -import net.corda.core.transactions.* +import net.corda.core.transactions.ComponentGroup +import net.corda.core.transactions.ComponentVisibilityException +import net.corda.core.transactions.FilteredComponentGroup +import net.corda.core.transactions.FilteredTransaction +import net.corda.core.transactions.FilteredTransactionVerificationException +import net.corda.core.transactions.NetworkParametersHash +import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.OpaqueBytes import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState -import net.corda.testing.core.* +import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.core.TestIdentity +import net.corda.testing.core.dummyCommand import org.junit.Rule import org.junit.Test import java.time.Instant import java.util.function.Predicate -import kotlin.test.* +import kotlin.test.assertEquals +import kotlin.test.assertFails +import kotlin.test.assertFailsWith +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull class CompatibleTransactionTests { private companion object { @@ -47,7 +80,7 @@ class CompatibleTransactionTests { private val inputGroup by lazy { ComponentGroup(INPUTS_GROUP.ordinal, inputs.map { it.serialize() }) } private val outputGroup by lazy { ComponentGroup(OUTPUTS_GROUP.ordinal, outputs.map { it.serialize() }) } private val commandGroup by lazy { ComponentGroup(COMMANDS_GROUP.ordinal, commands.map { it.value.serialize() }) } - private val attachmentGroup by lazy { ComponentGroup(ATTACHMENTS_GROUP.ordinal, attachments.map { it.serialize() }) } // The list is empty. + private val attachmentGroup by lazy { ComponentGroup(ATTACHMENTS_V2_GROUP.ordinal, attachments.map { it.serialize() }) } // The list is empty. private val notaryGroup by lazy { ComponentGroup(NOTARY_GROUP.ordinal, listOf(notary.serialize())) } private val timeWindowGroup by lazy { ComponentGroup(TIMEWINDOW_GROUP.ordinal, listOf(timeWindow.serialize())) } private val signersGroup by lazy { ComponentGroup(SIGNERS_GROUP.ordinal, commands.map { it.signers.serialize() }) } @@ -96,7 +129,7 @@ class CompatibleTransactionTests { // Ordering inside a component group matters. val inputsShuffled = listOf(stateRef2, stateRef1, stateRef3) - val inputShuffledGroup = ComponentGroup(INPUTS_GROUP.ordinal, inputsShuffled.map { it -> it.serialize() }) + val inputShuffledGroup = ComponentGroup(INPUTS_GROUP.ordinal, inputsShuffled.map { it.serialize() }) val componentGroupsB = listOf( inputShuffledGroup, outputGroup, @@ -114,8 +147,8 @@ class CompatibleTransactionTests { // But outputs group Merkle leaf (and the rest) remained the same. assertEquals(wireTransactionA.accessGroupMerkleRoots()[OUTPUTS_GROUP.ordinal], wireTransaction1ShuffledInputs.accessGroupMerkleRoots()[OUTPUTS_GROUP.ordinal]) assertEquals(wireTransactionA.accessGroupMerkleRoots()[NOTARY_GROUP.ordinal], wireTransaction1ShuffledInputs.accessGroupMerkleRoots()[NOTARY_GROUP.ordinal]) - assertNull(wireTransactionA.accessGroupMerkleRoots()[ATTACHMENTS_GROUP.ordinal]) - assertNull(wireTransaction1ShuffledInputs.accessGroupMerkleRoots()[ATTACHMENTS_GROUP.ordinal]) + assertNull(wireTransactionA.accessGroupMerkleRoots()[ATTACHMENTS_V2_GROUP.ordinal]) + assertNull(wireTransaction1ShuffledInputs.accessGroupMerkleRoots()[ATTACHMENTS_V2_GROUP.ordinal]) // Group leaves (components) ordering does not affect the id. In this case, we added outputs group before inputs. val shuffledComponentGroupsA = listOf( @@ -140,7 +173,7 @@ class CompatibleTransactionTests { inputGroup, outputGroup, commandGroup, - ComponentGroup(ATTACHMENTS_GROUP.ordinal, inputGroup.components), + ComponentGroup(ATTACHMENTS_V2_GROUP.ordinal, inputGroup.components), notaryGroup, timeWindowGroup, signersGroup @@ -201,23 +234,16 @@ class CompatibleTransactionTests { @Test(timeout=300_000) fun `FilteredTransaction constructors and compatibility`() { // Filter out all of the components. - val ftxNothing = wireTransactionA.buildFilteredTransaction(Predicate { false }) // Nothing filtered. + val ftxNothing = wireTransactionA.buildFilteredTransaction { false } // Nothing filtered. // Although nothing filtered, we still receive the group hashes for the top level Merkle tree. // Note that attachments are not sent, but group hashes include the allOnesHash flag for the attachment group hash; that's why we expect +1 group hashes. assertEquals(wireTransactionA.componentGroups.size + 1, ftxNothing.groupHashes.size) ftxNothing.verify() // Include all of the components. - val ftxAll = wireTransactionA.buildFilteredTransaction(Predicate { true }) // All filtered. + val ftxAll = wireTransactionA.buildFilteredTransaction { true } // All filtered. ftxAll.verify() - ftxAll.checkAllComponentsVisible(INPUTS_GROUP) - ftxAll.checkAllComponentsVisible(OUTPUTS_GROUP) - ftxAll.checkAllComponentsVisible(COMMANDS_GROUP) - ftxAll.checkAllComponentsVisible(ATTACHMENTS_GROUP) - ftxAll.checkAllComponentsVisible(NOTARY_GROUP) - ftxAll.checkAllComponentsVisible(TIMEWINDOW_GROUP) - ftxAll.checkAllComponentsVisible(SIGNERS_GROUP) - ftxAll.checkAllComponentsVisible(PARAMETERS_GROUP) + ComponentGroupEnum.entries.forEach(ftxAll::checkAllComponentsVisible) // Filter inputs only. fun filtering(elem: Any): Boolean { @@ -232,9 +258,9 @@ class CompatibleTransactionTests { ftxInputs.checkAllComponentsVisible(INPUTS_GROUP) assertEquals(1, ftxInputs.filteredComponentGroups.size) // We only add component groups that are not empty, thus in this case: the inputs only. - assertEquals(3, ftxInputs.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.components.size) // All 3 inputs are present. - assertEquals(3, ftxInputs.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.nonces.size) // And their corresponding nonces. - assertNotNull(ftxInputs.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.partialMerkleTree) // And the Merkle tree. + assertEquals(3, ftxInputs.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).components.size) // All 3 inputs are present. + assertEquals(3, ftxInputs.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).nonces.size) // And their corresponding nonces. + assertNotNull(ftxInputs.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).partialMerkleTree) // And the Merkle tree. // Filter one input only. fun filteringOneInput(elem: Any) = elem == inputs[0] @@ -244,9 +270,9 @@ class CompatibleTransactionTests { assertFailsWith { ftxOneInput.checkAllComponentsVisible(INPUTS_GROUP) } assertEquals(1, ftxOneInput.filteredComponentGroups.size) // We only add component groups that are not empty, thus in this case: the inputs only. - assertEquals(1, ftxOneInput.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.components.size) // 1 input is present. - assertEquals(1, ftxOneInput.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.nonces.size) // And its corresponding nonce. - assertNotNull(ftxOneInput.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.partialMerkleTree) // And the Merkle tree. + assertEquals(1, ftxOneInput.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).components.size) // 1 input is present. + assertEquals(1, ftxOneInput.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).nonces.size) // And its corresponding nonce. + assertNotNull(ftxOneInput.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).partialMerkleTree) // And the Merkle tree. // The old client (receiving more component types than expected) is still compatible. val componentGroupsCompatibleA = listOf( @@ -265,14 +291,14 @@ class CompatibleTransactionTests { assertEquals(wireTransactionCompatibleA.id, ftxCompatible.id) assertEquals(1, ftxCompatible.filteredComponentGroups.size) - assertEquals(3, ftxCompatible.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.components.size) - assertEquals(3, ftxCompatible.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.nonces.size) - assertNotNull(ftxCompatible.filteredComponentGroups.firstOrNull { it.groupIndex == INPUTS_GROUP.ordinal }!!.partialMerkleTree) + assertEquals(3, ftxCompatible.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).components.size) + assertEquals(3, ftxCompatible.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).nonces.size) + assertNotNull(ftxCompatible.filteredComponentGroups.getRequiredGroup(INPUTS_GROUP).partialMerkleTree) assertNull(wireTransactionCompatibleA.networkParametersHash) assertNull(ftxCompatible.networkParametersHash) // Now, let's allow everything, including the new component type that we cannot process. - val ftxCompatibleAll = wireTransactionCompatibleA.buildFilteredTransaction(Predicate { true }) // All filtered, including the unknown component. + val ftxCompatibleAll = wireTransactionCompatibleA.buildFilteredTransaction { true } // All filtered, including the unknown component. ftxCompatibleAll.verify() assertEquals(wireTransactionCompatibleA.id, ftxCompatibleAll.id) @@ -292,7 +318,7 @@ class CompatibleTransactionTests { ftxCompatibleNoInputs.verify() assertFailsWith { ftxCompatibleNoInputs.checkAllComponentsVisible(INPUTS_GROUP) } assertEquals(wireTransactionCompatibleA.componentGroups.size - 1, ftxCompatibleNoInputs.filteredComponentGroups.size) - assertEquals(wireTransactionCompatibleA.componentGroups.map { it.groupIndex }.max(), ftxCompatibleNoInputs.groupHashes.size - 1) + assertEquals(wireTransactionCompatibleA.componentGroups.maxOfOrNull { it.groupIndex }, ftxCompatibleNoInputs.groupHashes.size - 1) } @Test(timeout=300_000) @@ -451,7 +477,7 @@ class CompatibleTransactionTests { val key2CommandsFtx = wtx.buildFilteredTransaction(Predicate(::filterKEY2Commands)) // val commandDataComponents = key1CommandsFtx.filteredComponentGroups[0].components - val commandDataHashes = wtx.accessAvailableComponentHashes()[ComponentGroupEnum.COMMANDS_GROUP.ordinal]!! + val commandDataHashes = wtx.accessAvailableComponentHashes()[COMMANDS_GROUP.ordinal]!! val noLastCommandDataPMT = PartialMerkleTree.build( MerkleTree.getMerkleTree(commandDataHashes, wtx.digestService), commandDataHashes.subList(0, 1) @@ -466,7 +492,7 @@ class CompatibleTransactionTests { ) val signerComponents = key1CommandsFtx.filteredComponentGroups[1].components - val signerHashes = wtx.accessAvailableComponentHashes()[ComponentGroupEnum.SIGNERS_GROUP.ordinal]!! + val signerHashes = wtx.accessAvailableComponentHashes()[SIGNERS_GROUP.ordinal]!! val noLastSignerPMT = PartialMerkleTree.build( MerkleTree.getMerkleTree(signerHashes, wtx.digestService), signerHashes.subList(0, 2) @@ -527,7 +553,7 @@ class CompatibleTransactionTests { // Modify last signer (we have a pointer from commandData). // Update partial Merkle tree for signers. val alterSignerComponents = signerComponents.subList(0, 2) + signerComponents[1] // Third one is removed and the 2nd command is added twice. - val alterSignersHashes = wtx.accessAvailableComponentHashes()[ComponentGroupEnum.SIGNERS_GROUP.ordinal]!!.subList(0, 2) + wtx.digestService.componentHash(key1CommandsFtx.filteredComponentGroups[1].nonces[2], alterSignerComponents[2]) + val alterSignersHashes = wtx.accessAvailableComponentHashes()[SIGNERS_GROUP.ordinal]!!.subList(0, 2) + wtx.digestService.componentHash(key1CommandsFtx.filteredComponentGroups[1].nonces[2], alterSignerComponents[2]) val alterMTree = MerkleTree.getMerkleTree(alterSignersHashes, wtx.digestService) val alterSignerPMTK = PartialMerkleTree.build( alterMTree, @@ -561,7 +587,7 @@ class CompatibleTransactionTests { fun `parameters hash visibility`() { fun paramsFilter(elem: Any): Boolean = elem is NetworkParametersHash && elem.hash == paramsHash fun attachmentFilter(elem: Any): Boolean = elem is SecureHash && elem == paramsHash - val attachments = ComponentGroup(ATTACHMENTS_GROUP.ordinal, listOf(paramsHash.serialize())) // Same hash as network parameters + val attachments = ComponentGroup(ATTACHMENTS_V2_GROUP.ordinal, listOf(paramsHash.serialize())) // Same hash as network parameters val componentGroups = listOf( inputGroup, outputGroup, @@ -577,12 +603,12 @@ class CompatibleTransactionTests { ftx1.verify() assertEquals(wtx.id, ftx1.id) ftx1.checkAllComponentsVisible(PARAMETERS_GROUP) - assertFailsWith { ftx1.checkAllComponentsVisible(ATTACHMENTS_GROUP) } + assertFailsWith { ftx1.checkAllComponentsVisible(ATTACHMENTS_V2_GROUP) } // Filter only attachment. val ftx2 = wtx.buildFilteredTransaction(Predicate(::attachmentFilter)) ftx2.verify() assertEquals(wtx.id, ftx2.id) - ftx2.checkAllComponentsVisible(ATTACHMENTS_GROUP) + ftx2.checkAllComponentsVisible(ATTACHMENTS_V2_GROUP) assertFailsWith { ftx2.checkAllComponentsVisible(PARAMETERS_GROUP) } } } diff --git a/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderMockNetworkTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderMockNetworkTest.kt new file mode 100644 index 0000000000..a8286298c1 --- /dev/null +++ b/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderMockNetworkTest.kt @@ -0,0 +1,166 @@ +package net.corda.coretests.transactions + +import net.corda.core.contracts.SignatureAttachmentConstraint +import net.corda.core.contracts.TransactionState +import net.corda.core.internal.PlatformVersionSwitches.MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS +import net.corda.core.internal.RPC_UPLOADER +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 +import net.corda.testing.node.internal.InternalMockNetwork +import net.corda.testing.node.internal.MockNodeArgs +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 { + companion object { + val legacyFinanceContractsJar = this::class.java.getResource("/corda-finance-contracts-4.11.jar")!!.toPath() + } + + @Rule + @JvmField + val tempFolder = TemporaryFolder() + + private val mockNetwork = InternalMockNetwork( + cordappsForAllNodes = setOf( + FINANCE_CONTRACTS_CORDAPP, + cordappWithPackages("net.corda.testing.contracts").signed() + ), + initialNetworkParameters = testNetworkParameters(minimumPlatformVersion = MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS) + ) + + @After + fun close() { + mockNetwork.close() + } + + @Test(timeout=300_000) + fun `automatic signature constraint`() { + val services = mockNetwork.notaryNodes[0].services + + val attachment = services.attachments.openAttachment(services.attachments.getLatestContractAttachments(DummyContract.PROGRAM_ID)[0]) + val attachmentSigner = attachment!!.signerKeys.single() + + val expectedConstraint = SignatureAttachmentConstraint(attachmentSigner) + assertThat(expectedConstraint.isSatisfiedBy(attachment)).isTrue() + + val outputState = TransactionState(data = DummyState(), contract = DummyContract.PROGRAM_ID, notary = mockNetwork.defaultNotaryIdentity) + val builder = TransactionBuilder() + .addOutputState(outputState) + .addCommand(DummyCommandData, mockNetwork.defaultNotaryIdentity.owningKey) + val wtx = builder.toWireTransaction(services) + + assertThat(wtx.outputs).containsOnly(outputState.copy(constraint = expectedConstraint)) + } + + @Test(timeout=300_000) + fun `contract overlap in explicit attachments`() { + val duplicateJar = tempFolder.newFile("duplicate.jar").toPath() + FINANCE_CONTRACTS_CORDAPP.jarFile.copyTo(duplicateJar, overwrite = true) + duplicateJar.unsignJar() // Change its hash + + val node = mockNetwork.createNode() + val duplicateId = duplicateJar.inputStream().use { + node.services.attachments.privilegedImportAttachment(it, RPC_UPLOADER, null) + } + assertThat(FINANCE_CONTRACTS_CORDAPP.jarFile.hash).isNotEqualTo(duplicateId) + + val builder = TransactionBuilder() + builder.addAttachment(FINANCE_CONTRACTS_CORDAPP.jarFile.hash) + builder.addAttachment(duplicateId) + val identity = node.info.singleIdentity() + Cash().generateIssue(builder, 10.DOLLARS.issuedBy(identity.ref(0x00)), identity, mockNetwork.defaultNotaryIdentity) + assertThatIllegalArgumentException() + .isThrownBy { builder.toWireTransaction(node.services) } + .withMessageContaining("Multiple attachments specified for the same contract") + } + + @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 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(legacyFinanceContractsJar.hash) + stx.verify(node.services) + } + + @Test(timeout=300_000) + @Ignore // https://r3-cev.atlassian.net/browse/ENT-11445 + fun `adds legacy CorDapp dependencies`() { + val cordapp1 = tempFolder.newFile("cordapp1.jar").toPath() + val cordapp2 = tempFolder.newFile("cordapp2.jar").toPath() + // Split the contracts CorDapp into two + legacyFinanceContractsJar.copyTo(cordapp1, overwrite = true) + cordapp1.useZipFile { zipFs1 -> + cordapp2.useZipFile { zipFs2 -> + val destinationDir = zipFs2.getPath("net/corda/finance/contracts/asset").createDirectories() + zipFs1.getPath("net/corda/finance/contracts/asset") + .listDirectoryEntries("OnLedgerAsset*") + .forEach { + it.copyToDirectory(destinationDir) + it.deleteExisting() + } + } + } + reSignJar(cordapp1) + + val node = mockNetwork.createNode { + it.copyToLegacyContracts(cordapp1, cordapp2) + InternalMockNetwork.MockNode(it) + } + val builder = TransactionBuilder() + val identity = node.info.singleIdentity() + Cash().generateIssue(builder, 10.DOLLARS.issuedBy(identity.ref(0x00)), identity, mockNetwork.defaultNotaryIdentity) + val stx = node.services.signInitialTransaction(builder) + assertThat(stx.tx.nonLegacyAttachments).contains(FINANCE_CONTRACTS_CORDAPP.jarFile.hash) + assertThat(stx.tx.legacyAttachments).contains(cordapp1.hash, cordapp2.hash) + stx.verify(node.services) + } + + private fun reSignJar(jar: Path) { + jar.unsignJar() + tempFolder.root.toPath().generateKey("testAlias", "testPassword", ALICE_NAME.toString()) + tempFolder.root.toPath().signJar(jar.absolutePathString(), "testAlias", "testPassword") + } + + private fun MockNodeArgs.copyToLegacyContracts(vararg jars: Path) { + val legacyContractsDir = (config.baseDirectory / "legacy-contracts").createDirectories() + jars.forEach { it.copyToDirectory(legacyContractsDir) } + } +} diff --git a/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderTest.kt b/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderTest.kt index 53788d5b70..9155b42611 100644 --- a/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderTest.kt +++ b/core-tests/src/test/kotlin/net/corda/coretests/transactions/TransactionBuilderTest.kt @@ -3,7 +3,6 @@ package net.corda.coretests.transactions import net.corda.core.contracts.Command import net.corda.core.contracts.HashAttachmentConstraint import net.corda.core.contracts.PrivacySalt -import net.corda.core.contracts.SignatureAttachmentConstraint import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef import net.corda.core.contracts.TimeWindow @@ -16,7 +15,6 @@ import net.corda.core.internal.PLATFORM_VERSION import net.corda.core.internal.RPC_UPLOADER import net.corda.core.internal.digestService import net.corda.core.node.ZoneVersionTooLowException -import net.corda.core.serialization.internal._driverSerializationEnv import net.corda.core.transactions.TransactionBuilder import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract @@ -26,15 +24,12 @@ import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.DummyCommandData import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity -import net.corda.testing.node.MockNetwork -import net.corda.testing.node.MockNetworkParameters import net.corda.testing.node.MockServices import net.corda.testing.node.internal.cordappWithPackages import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.assertj.core.api.Assertions.assertThatIllegalArgumentException import org.assertj.core.api.Assertions.assertThatThrownBy -import org.junit.Assert.assertTrue import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -56,6 +51,7 @@ class TransactionBuilderTest { private val contractAttachmentId = services.attachments.getLatestContractAttachments(DummyContract.PROGRAM_ID)[0] @Test(timeout=300_000) + @Suppress("INVISIBLE_MEMBER") fun `bare minimum issuance tx`() { val outputState = TransactionState( data = DummyState(), @@ -70,6 +66,9 @@ class TransactionBuilderTest { assertThat(wtx.outputs).containsOnly(outputState) assertThat(wtx.commands).containsOnly(Command(DummyCommandData, notary.owningKey)) assertThat(wtx.networkParametersHash).isEqualTo(services.networkParametersService.currentHash) + // From 4.12 attachments are added to the new component group by default + assertThat(wtx.nonLegacyAttachments).isNotEmpty + assertThat(wtx.legacyAttachments).isEmpty() } @Test(timeout=300_000) @@ -105,41 +104,6 @@ class TransactionBuilderTest { } } - @Test(timeout=300_000) - fun `automatic signature constraint`() { - // We need to use a MockNetwork so that we can create a signed attachment. However, SerializationEnvironmentRule and MockNetwork - // don't work well together, so we temporarily clear out the driverSerializationEnv for this test. - val driverSerializationEnv = _driverSerializationEnv.get() - _driverSerializationEnv.set(null) - val mockNetwork = MockNetwork( - MockNetworkParameters( - networkParameters = testNetworkParameters(minimumPlatformVersion = PLATFORM_VERSION), - cordappsForAllNodes = listOf(cordappWithPackages("net.corda.testing.contracts").signed()) - ) - ) - - try { - val services = mockNetwork.notaryNodes[0].services - - val attachment = services.attachments.openAttachment(services.attachments.getLatestContractAttachments(DummyContract.PROGRAM_ID)[0]) - val attachmentSigner = attachment!!.signerKeys.single() - - val expectedConstraint = SignatureAttachmentConstraint(attachmentSigner) - assertTrue(expectedConstraint.isSatisfiedBy(attachment)) - - val outputState = TransactionState(data = DummyState(), contract = DummyContract.PROGRAM_ID, notary = notary) - val builder = TransactionBuilder() - .addOutputState(outputState) - .addCommand(DummyCommandData, notary.owningKey) - val wtx = builder.toWireTransaction(services) - - assertThat(wtx.outputs).containsOnly(outputState.copy(constraint = expectedConstraint)) - } finally { - mockNetwork.stopNodes() - _driverSerializationEnv.set(driverSerializationEnv) - } - } - @Test(timeout=300_000) fun `list accessors are mutable copies`() { val inputState1 = TransactionState(DummyState(), DummyContract.PROGRAM_ID, notary) diff --git a/core/src/main/kotlin/net/corda/core/contracts/ComponentGroupEnum.kt b/core/src/main/kotlin/net/corda/core/contracts/ComponentGroupEnum.kt index 6492d8154f..93bdeff2dc 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ComponentGroupEnum.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ComponentGroupEnum.kt @@ -8,10 +8,11 @@ enum class ComponentGroupEnum { INPUTS_GROUP, // ordinal = 0. OUTPUTS_GROUP, // ordinal = 1. COMMANDS_GROUP, // ordinal = 2. - ATTACHMENTS_GROUP, // ordinal = 3. + ATTACHMENTS_GROUP, // ordinal = 3. This is for legacy attachments. It's not been renamed for backwards compatibility. NOTARY_GROUP, // ordinal = 4. TIMEWINDOW_GROUP, // ordinal = 5. SIGNERS_GROUP, // ordinal = 6. REFERENCES_GROUP, // ordinal = 7. - PARAMETERS_GROUP // ordinal = 8. + PARAMETERS_GROUP, // ordinal = 8. + ATTACHMENTS_V2_GROUP // ordinal = 9. From 4.12+ this group is used for attachments. } diff --git a/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt b/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt index ab38724c4e..b6c2b2e83d 100644 --- a/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt @@ -67,12 +67,12 @@ import java.security.PublicKey class CollectSignaturesFlow @JvmOverloads constructor(val partiallySignedTx: SignedTransaction, val sessionsToCollectFrom: Collection, val myOptionalKeys: Iterable?, - override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic() { + override val progressTracker: ProgressTracker = tracker()) : FlowLogic() { @JvmOverloads constructor( partiallySignedTx: SignedTransaction, sessionsToCollectFrom: Collection, - progressTracker: ProgressTracker = CollectSignaturesFlow.tracker() + progressTracker: ProgressTracker = tracker() ) : this(partiallySignedTx, sessionsToCollectFrom, null, progressTracker) companion object { @@ -100,6 +100,7 @@ class CollectSignaturesFlow @JvmOverloads constructor(val partiallySignedTx: Sig // The signatures must be valid and the transaction must be valid. partiallySignedTx.verifySignaturesExcept(notSigned) + // TODO Should this be calling SignedTransaction.verify directly? https://r3-cev.atlassian.net/browse/ENT-11458 partiallySignedTx.tx.toLedgerTransaction(serviceHub).verify() // Determine who still needs to sign. @@ -235,7 +236,7 @@ class CollectSignatureFlow(val partiallySignedTx: SignedTransaction, val session * - Call the flow via [FlowLogic.subFlow] * - The flow returns the transaction signed with the additional signature. * - * Example - checking and signing a transaction involving a [net.corda.core.contracts.DummyContract], see + * Example - checking and signing a transaction involving a `DummyContract`, see * CollectSignaturesFlowTests.kt for further examples: * * class Responder(val otherPartySession: FlowSession): FlowLogic() { @@ -259,7 +260,7 @@ class CollectSignatureFlow(val partiallySignedTx: SignedTransaction, val session * @param otherSideSession The session which is providing you a transaction to sign. */ abstract class SignTransactionFlow @JvmOverloads constructor(val otherSideSession: FlowSession, - override val progressTracker: ProgressTracker = SignTransactionFlow.tracker()) : FlowLogic() { + override val progressTracker: ProgressTracker = tracker()) : FlowLogic() { companion object { object RECEIVING : ProgressTracker.Step("Receiving transaction proposal for signing.") @@ -287,6 +288,7 @@ abstract class SignTransactionFlow @JvmOverloads constructor(val otherSideSessio checkMySignaturesRequired(stx, signingKeys) // Check the signatures which have already been provided. Usually the Initiators and possibly an Oracle's. checkSignatures(stx) + // TODO Should this be calling SignedTransaction.verify directly? https://r3-cev.atlassian.net/browse/ENT-11458 stx.tx.toLedgerTransaction(serviceHub).verify() // Perform some custom verification over the transaction. try { diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt index a2f3e23609..aa7837f651 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt @@ -11,12 +11,14 @@ import net.corda.core.internal.PlatformVersionSwitches import net.corda.core.internal.ServiceHubCoreInternal import net.corda.core.internal.pushToLoggingContext import net.corda.core.internal.telemetry.telemetryServiceInternal +import net.corda.core.internal.verification.toVerifyingServiceHub import net.corda.core.internal.warnOnce import net.corda.core.node.StatesToRecord import net.corda.core.node.StatesToRecord.ONLY_RELEVANT import net.corda.core.serialization.DeprecatedConstructorForDeserialization import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.Try import net.corda.core.utilities.debug @@ -170,6 +172,7 @@ class FinalityFlow private constructor(val transaction: SignedTransaction, @Suppress("ComplexMethod", "NestedBlockDepth") @Throws(NotaryException::class) override fun call(): SignedTransaction { + require(transaction.coreTransaction is WireTransaction) // Sanity check if (!newApi) { logger.warnOnce("The current usage of FinalityFlow is unsafe. Please consider upgrading your CorDapp to use " + "FinalityFlow with FlowSessions. (${serviceHub.getAppContext().cordapp.info})") @@ -447,9 +450,12 @@ 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.toLedgerTransaction(serviceHub, false) - ltx.verify() - return ltx + val ltx = transaction.verifyInternal(serviceHub.toVerifyingServiceHub(), checkSufficientSignatures = false) as LedgerTransaction? + // 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 + // for verification by the node. + return ltx ?: transaction.toLedgerTransaction(serviceHub, checkSufficientSignatures = false) } } diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalAttachment.kt b/core/src/main/kotlin/net/corda/core/internal/InternalAttachment.kt deleted file mode 100644 index 8214064bb2..0000000000 --- a/core/src/main/kotlin/net/corda/core/internal/InternalAttachment.kt +++ /dev/null @@ -1,28 +0,0 @@ -package net.corda.core.internal - -import net.corda.core.contracts.Attachment -import net.corda.core.contracts.ContractAttachment - -interface InternalAttachment : Attachment { - /** - * The version of the Kotlin metadata, if this attachment has one. See `kotlinx.metadata.jvm.JvmMetadataVersion` for more information on - * how this maps to the Kotlin language version. - */ - val kotlinMetadataVersion: String? -} - -/** - * Because [ContractAttachment] is public API, we can't make it implement [InternalAttachment] without also leaking it out. - * - * @see InternalAttachment.kotlinMetadataVersion - */ -val Attachment.kotlinMetadataVersion: String? get() { - var attachment = this - while (true) { - when (attachment) { - is InternalAttachment -> return attachment.kotlinMetadataVersion - is ContractAttachment -> attachment = attachment.attachment - else -> return null - } - } -} diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index 95929aca32..105b55616e 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -79,7 +79,14 @@ import kotlin.math.roundToLong import kotlin.reflect.KClass import kotlin.reflect.full.createInstance -val Throwable.rootCause: Throwable get() = cause?.rootCause ?: this +val Throwable.rootCause: Throwable + get() { + var root = this + while (true) { + root = root.cause ?: return root + } + } + val Throwable.rootMessage: String? get() { var message = this.message var throwable = cause @@ -231,8 +238,6 @@ inline fun elapsedTime(block: () -> Unit): Duration { fun Logger.logElapsedTime(label: String, body: () -> T): T = logElapsedTime(label, this, body) -// TODO: Add inline back when a new Kotlin version is released and check if the java.lang.VerifyError -// returns in the IRSSimulationTest. If not, commit the inline back. fun logElapsedTime(label: String, logger: Logger? = null, body: () -> T): T { // Use nanoTime as it's monotonic. val now = System.nanoTime() @@ -639,16 +644,10 @@ val Logger.level: Level else -> throw IllegalStateException("Unknown logging level") } -const val JAVA_1_2_CLASS_FILE_FORMAT_MAJOR_VERSION = 46 -const val JAVA_17_CLASS_FILE_FORMAT_MAJOR_VERSION = 61 +const val JAVA_1_2_CLASS_FILE_MAJOR_VERSION = 46 +const val JAVA_8_CLASS_FILE_MAJOR_VERSION = 52 +const val JAVA_17_CLASS_FILE_MAJOR_VERSION = 61 -/** - * String extension functions - to keep calling code readable following upgrade to Kotlin 1.9 - */ -fun String.capitalize() : String { - return this.replaceFirstChar { it.titlecase(Locale.getDefault()) } -} -fun String.decapitalize() : String { - return this.replaceFirstChar { it.lowercase(Locale.getDefault()) } -} +fun String.capitalize(): String = replaceFirstChar { it.titlecase(Locale.getDefault()) } +fun String.decapitalize(): String = replaceFirstChar { it.lowercase(Locale.getDefault()) } diff --git a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt index 3896d5648c..456f6b6c6c 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt @@ -94,7 +94,7 @@ class ResolveTransactionsFlow private constructor( fun fetchMissingAttachments(transaction: SignedTransaction): Boolean { val tx = transaction.coreTransaction val attachmentIds = when (tx) { - is WireTransaction -> tx.attachments.toSet() + is WireTransaction -> tx.allAttachments is ContractUpgradeWireTransaction -> setOf(tx.legacyContractAttachmentId, tx.upgradedContractAttachmentId) else -> return false } diff --git a/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt b/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt index e07b50d020..3d3056da5f 100644 --- a/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt @@ -1,6 +1,27 @@ package net.corda.core.internal -import net.corda.core.contracts.* +import net.corda.core.contracts.Command +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.ComponentGroupEnum +import net.corda.core.contracts.ComponentGroupEnum.ATTACHMENTS_GROUP +import net.corda.core.contracts.ComponentGroupEnum.ATTACHMENTS_V2_GROUP +import net.corda.core.contracts.ComponentGroupEnum.COMMANDS_GROUP +import net.corda.core.contracts.ComponentGroupEnum.INPUTS_GROUP +import net.corda.core.contracts.ComponentGroupEnum.NOTARY_GROUP +import net.corda.core.contracts.ComponentGroupEnum.OUTPUTS_GROUP +import net.corda.core.contracts.ComponentGroupEnum.PARAMETERS_GROUP +import net.corda.core.contracts.ComponentGroupEnum.REFERENCES_GROUP +import net.corda.core.contracts.ComponentGroupEnum.SIGNERS_GROUP +import net.corda.core.contracts.ComponentGroupEnum.TIMEWINDOW_GROUP +import net.corda.core.contracts.ContractClassName +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.NamedByHash +import net.corda.core.contracts.PrivacySalt +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TimeWindow +import net.corda.core.contracts.TransactionState +import net.corda.core.contracts.TransactionVerificationException import net.corda.core.crypto.DigestService import net.corda.core.crypto.SecureHash import net.corda.core.crypto.algorithm @@ -8,8 +29,20 @@ import net.corda.core.crypto.internal.DigestAlgorithmFactory import net.corda.core.flows.FlowLogic import net.corda.core.identity.Party import net.corda.core.node.ServicesForResolution -import net.corda.core.serialization.* -import net.corda.core.transactions.* +import net.corda.core.serialization.MissingAttachmentsException +import net.corda.core.serialization.MissingAttachmentsRuntimeException +import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.SerializationFactory +import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize +import net.corda.core.transactions.BaseTransaction +import net.corda.core.transactions.ComponentGroup +import net.corda.core.transactions.ContractUpgradeWireTransaction +import net.corda.core.transactions.FilteredComponentGroup +import net.corda.core.transactions.FullTransaction +import net.corda.core.transactions.NotaryChangeWireTransaction +import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.OpaqueBytes import java.io.ByteArrayOutputStream import java.security.PublicKey @@ -68,8 +101,7 @@ fun deserialiseComponentGroup(componentGroups: List, forceDeserialize: Boolean = false, factory: SerializationFactory = SerializationFactory.defaultFactory, context: SerializationContext = factory.defaultContext): List { - val group = componentGroups.firstOrNull { it.groupIndex == groupEnum.ordinal } - + val group = componentGroups.getGroup(groupEnum) if (group == null || group.components.isEmpty()) { return emptyList() } @@ -85,7 +117,7 @@ fun deserialiseComponentGroup(componentGroups: List, factory.deserialize(component, clazz.java, context) } catch (e: MissingAttachmentsException) { /** - * [ServiceHub.signInitialTransaction] forgets to declare that + * `ServiceHub.signInitialTransaction` forgets to declare that * it may throw any checked exceptions. Wrap this one inside * an unchecked version to avoid breaking Java CorDapps. */ @@ -96,7 +128,13 @@ fun deserialiseComponentGroup(componentGroups: List, } } -/** +fun List.getGroup(type: ComponentGroupEnum): T? = firstOrNull { it.groupIndex == type.ordinal } + +fun List.getRequiredGroup(type: ComponentGroupEnum): T { + return requireNotNull(getGroup(type)) { "Missing component group $type" } +} + +/**x * Exception raised if an error was encountered while attempting to deserialise a component group in a transaction. */ class TransactionDeserialisationException(groupEnum: ComponentGroupEnum, index: Int, cause: Exception): @@ -119,9 +157,9 @@ fun deserialiseCommands( // TODO: we could avoid deserialising unrelated signers. // However, current approach ensures the transaction is not malformed // and it will throw if any of the signers objects is not List of public keys). - val signersList: List> = uncheckedCast(deserialiseComponentGroup(componentGroups, List::class, ComponentGroupEnum.SIGNERS_GROUP, forceDeserialize, factory, context)) - val commandDataList: List = deserialiseComponentGroup(componentGroups, CommandData::class, ComponentGroupEnum.COMMANDS_GROUP, forceDeserialize, factory, context) - val group = componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.COMMANDS_GROUP.ordinal } + val signersList: List> = uncheckedCast(deserialiseComponentGroup(componentGroups, List::class, SIGNERS_GROUP, forceDeserialize, factory, context)) + val commandDataList: List = deserialiseComponentGroup(componentGroups, CommandData::class, COMMANDS_GROUP, forceDeserialize, factory, context) + val group = componentGroups.getGroup(COMMANDS_GROUP) return if (group is FilteredComponentGroup) { check(commandDataList.size <= signersList.size) { "Invalid Transaction. Less Signers (${signersList.size}) than CommandData (${commandDataList.size}) objects" @@ -141,10 +179,7 @@ fun deserialiseCommands( } } -/** - * Creating list of [ComponentGroup] used in one of the constructors of [WireTransaction] required - * for backwards compatibility purposes. - */ +@Suppress("LongParameterList") fun createComponentGroups(inputs: List, outputs: List>, commands: List>, @@ -152,26 +187,37 @@ fun createComponentGroups(inputs: List, notary: Party?, timeWindow: TimeWindow?, references: List, - networkParametersHash: SecureHash?): List { + networkParametersHash: SecureHash?, + // The old attachments group is now only used to create transaction compatible with 4.11 (or earlier) nodes + legacyAttachments: List = emptyList()): List { val serializationFactory = SerializationFactory.defaultFactory val serializationContext = serializationFactory.defaultContext val serialize = { value: Any, _: Int -> value.serialize(serializationFactory, serializationContext) } val componentGroupMap: MutableList = mutableListOf() - if (inputs.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.INPUTS_GROUP.ordinal, inputs.lazyMapped(serialize))) - if (references.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.REFERENCES_GROUP.ordinal, references.lazyMapped(serialize))) - if (outputs.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.OUTPUTS_GROUP.ordinal, outputs.lazyMapped(serialize))) + componentGroupMap.addListGroup(INPUTS_GROUP, inputs, serialize) + componentGroupMap.addListGroup(REFERENCES_GROUP, references, serialize) + componentGroupMap.addListGroup(OUTPUTS_GROUP, outputs, serialize) // Adding commandData only to the commands group. Signers are added in their own group. - if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.COMMANDS_GROUP.ordinal, commands.map { it.value }.lazyMapped(serialize))) - if (attachments.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.ATTACHMENTS_GROUP.ordinal, attachments.lazyMapped(serialize))) - if (notary != null) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.NOTARY_GROUP.ordinal, listOf(notary).lazyMapped(serialize))) - if (timeWindow != null) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.TIMEWINDOW_GROUP.ordinal, listOf(timeWindow).lazyMapped(serialize))) + componentGroupMap.addListGroup(COMMANDS_GROUP, commands.map { it.value }, serialize) + // Attachments which can only be processed by 4.12 and later. + componentGroupMap.addListGroup(ATTACHMENTS_V2_GROUP, attachments, serialize) + // The original attachments group now only contains attachments which can be processed by 4.11 and ealier (and the external verifier). + componentGroupMap.addListGroup(ATTACHMENTS_GROUP, legacyAttachments, serialize) + if (notary != null) componentGroupMap.add(ComponentGroup(NOTARY_GROUP.ordinal, listOf(notary).lazyMapped(serialize))) + if (timeWindow != null) componentGroupMap.add(ComponentGroup(TIMEWINDOW_GROUP.ordinal, listOf(timeWindow).lazyMapped(serialize))) // Adding signers to their own group. This is required for command visibility purposes: a party receiving // a FilteredTransaction can now verify it sees all the commands it should sign. - if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.SIGNERS_GROUP.ordinal, commands.map { it.signers }.lazyMapped(serialize))) - if (networkParametersHash != null) componentGroupMap.add(ComponentGroup(ComponentGroupEnum.PARAMETERS_GROUP.ordinal, listOf(networkParametersHash.serialize()))) + componentGroupMap.addListGroup(SIGNERS_GROUP, commands.map { it.signers }, serialize) + if (networkParametersHash != null) componentGroupMap.add(ComponentGroup(PARAMETERS_GROUP.ordinal, listOf(networkParametersHash.serialize()))) return componentGroupMap } +private fun MutableList.addListGroup(type: ComponentGroupEnum, list: List, serialize: (Any, Int) -> SerializedBytes) { + if (list.isNotEmpty()) { + add(ComponentGroup(type.ordinal, list.lazyMapped(serialize))) + } +} + typealias SerializedTransactionState = SerializedBytes> /** @@ -267,10 +313,3 @@ internal fun checkNotaryWhitelisted(ftx: FullTransaction) { } } } - -val CoreTransaction.attachmentIds: List - get() = when (this) { - is WireTransaction -> attachments - is ContractUpgradeWireTransaction -> listOf(legacyContractAttachmentId, upgradedContractAttachmentId) - else -> emptyList() - } diff --git a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt index 961607f086..9e745aec44 100644 --- a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt +++ b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt @@ -36,6 +36,7 @@ data class CordappImpl( override val minimumPlatformVersion: Int, override val targetPlatformVersion: Int, override val jarHash: SecureHash.SHA256 = jarFile.hash, + val languageVersion: LanguageVersion = LanguageVersion.Data, val notaryService: Class? = null, /** Indicates whether the CorDapp is loaded from external sources, or generated on node startup (virtual). */ val isLoaded: Boolean = true, diff --git a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappProviderInternal.kt b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappProviderInternal.kt index ea4a3d42e0..fd83ba26f1 100644 --- a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappProviderInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappProviderInternal.kt @@ -14,7 +14,10 @@ interface CordappProviderInternal : CordappProvider { fun getCordappForFlow(flowLogic: FlowLogic<*>): Cordapp? /** - * Similar to [getContractAttachmentID] except it returns the [ContractAttachment] object. + * Similar to [getContractAttachmentID] except it returns the [ContractAttachment] object and also returns an optional second attachment + * representing the legacy version (4.11 or earlier) of the contract, if one exists. */ - fun getContractAttachment(contractClassName: ContractClassName): ContractAttachment? + fun getContractAttachments(contractClassName: ContractClassName): ContractAttachmentWithLegacy? } + +data class ContractAttachmentWithLegacy(val currentAttachment: ContractAttachment, val legacyAttachment: ContractAttachment? = null) diff --git a/core/src/main/kotlin/net/corda/core/internal/cordapp/KotlinMetadataVersion.kt b/core/src/main/kotlin/net/corda/core/internal/cordapp/KotlinMetadataVersion.kt new file mode 100644 index 0000000000..3fc946dd03 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/cordapp/KotlinMetadataVersion.kt @@ -0,0 +1,32 @@ +package net.corda.core.internal.cordapp + +data class KotlinMetadataVersion(val major: Int, val minor: Int, val patch: Int = 0) : Comparable { + companion object { + fun from(versionArray: IntArray): KotlinMetadataVersion { + val (major, minor, patch) = versionArray + return KotlinMetadataVersion(major, minor, patch) + } + } + + init { + require(major >= 0) { "Major version should be not less than 0" } + require(minor >= 0) { "Minor version should be not less than 0" } + require(patch >= 0) { "Patch version should be not less than 0" } + } + + /** + * Returns the equivalent [KotlinVersion] without the patch. + */ + val languageMinorVersion: KotlinVersion + // See `kotlinx.metadata.jvm.JvmMetadataVersion` + get() = if (major == 1 && minor == 1) KotlinVersion(1, 2) else KotlinVersion(major, minor) + + override fun compareTo(other: KotlinMetadataVersion): Int { + val majors = this.major.compareTo(other.major) + if (majors != 0) return majors + val minors = this.minor.compareTo(other.minor) + return if (minors != 0) minors else this.patch.compareTo(other.patch) + } + + override fun toString(): String = "$major.$minor.$patch" +} diff --git a/core/src/main/kotlin/net/corda/core/internal/cordapp/LanguageVersion.kt b/core/src/main/kotlin/net/corda/core/internal/cordapp/LanguageVersion.kt new file mode 100644 index 0000000000..d85a714844 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/cordapp/LanguageVersion.kt @@ -0,0 +1,56 @@ +package net.corda.core.internal.cordapp + +import net.corda.core.internal.JAVA_17_CLASS_FILE_MAJOR_VERSION +import net.corda.core.internal.JAVA_1_2_CLASS_FILE_MAJOR_VERSION +import net.corda.core.internal.JAVA_8_CLASS_FILE_MAJOR_VERSION + +sealed class LanguageVersion { + /** + * Returns true if this version is compatible with Corda 4.11 or earlier. + */ + abstract val isLegacyCompatible: Boolean + + /** + * Returns true if this version is compatible with Corda 4.12 or later. + */ + abstract val isNonLegacyCompatible: Boolean + + @Suppress("ConvertObjectToDataObject") // External verifier uses Kotlin 1.2 + object Data : LanguageVersion() { + override val isLegacyCompatible: Boolean + get() = true + + override val isNonLegacyCompatible: Boolean + get() = true + + override fun toString(): String = "Data" + } + + data class Bytecode(val classFileMajorVersion: Int, val kotlinMetadataVersion: KotlinMetadataVersion?): LanguageVersion() { + companion object { + private val KOTLIN_1_2_VERSION = KotlinVersion(1, 2) + private val KOTLIN_1_9_VERSION = KotlinVersion(1, 9) + } + + init { + require(classFileMajorVersion in JAVA_1_2_CLASS_FILE_MAJOR_VERSION..JAVA_17_CLASS_FILE_MAJOR_VERSION) { + "Unsupported class file major version $classFileMajorVersion" + } + val kotlinVersion = kotlinMetadataVersion?.languageMinorVersion + require(kotlinVersion == null || kotlinVersion == KOTLIN_1_2_VERSION || kotlinVersion == KOTLIN_1_9_VERSION) { + "Unsupported Kotlin metadata version $kotlinMetadataVersion" + } + } + + override val isLegacyCompatible: Boolean + get() = when { + classFileMajorVersion > JAVA_8_CLASS_FILE_MAJOR_VERSION -> false + kotlinMetadataVersion == null -> true // Java 8 CorDapp is fine + else -> kotlinMetadataVersion.languageMinorVersion == KOTLIN_1_2_VERSION + } + + override val isNonLegacyCompatible: Boolean + // Java-only CorDapp will always be compatible on 4.12 + get() = if (kotlinMetadataVersion == null) true else kotlinMetadataVersion.languageMinorVersion == KOTLIN_1_9_VERSION + } +} diff --git a/core/src/main/kotlin/net/corda/core/internal/verification/NodeVerificationSupport.kt b/core/src/main/kotlin/net/corda/core/internal/verification/NodeVerificationSupport.kt index f4b40dc1a0..a7b400ccc5 100644 --- a/core/src/main/kotlin/net/corda/core/internal/verification/NodeVerificationSupport.kt +++ b/core/src/main/kotlin/net/corda/core/internal/verification/NodeVerificationSupport.kt @@ -1,7 +1,7 @@ package net.corda.core.internal.verification import net.corda.core.contracts.Attachment -import net.corda.core.contracts.ComponentGroupEnum +import net.corda.core.contracts.ComponentGroupEnum.OUTPUTS_GROUP import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionResolutionException import net.corda.core.crypto.SecureHash @@ -11,6 +11,7 @@ import net.corda.core.internal.SerializedTransactionState import net.corda.core.internal.TRUSTED_UPLOADERS import net.corda.core.internal.cordapp.CordappProviderInternal import net.corda.core.internal.entries +import net.corda.core.internal.getRequiredGroup import net.corda.core.internal.getRequiredTransaction import net.corda.core.node.NetworkParameters import net.corda.core.node.services.AttachmentStorage @@ -85,9 +86,7 @@ interface NodeVerificationSupport : VerificationSupport { private fun getRegularOutput(coreTransaction: WireTransaction, outputIndex: Int): SerializedTransactionState { @Suppress("UNCHECKED_CAST") - return coreTransaction.componentGroups - .first { it.groupIndex == ComponentGroupEnum.OUTPUTS_GROUP.ordinal } - .components[outputIndex] as SerializedTransactionState + return coreTransaction.componentGroups.getRequiredGroup(OUTPUTS_GROUP).components[outputIndex] as SerializedTransactionState } /** @@ -137,6 +136,8 @@ interface NodeVerificationSupport : VerificationSupport { override fun getTrustedClassAttachment(className: String): 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))) ) diff --git a/core/src/main/kotlin/net/corda/core/internal/verification/VerificationSupport.kt b/core/src/main/kotlin/net/corda/core/internal/verification/VerificationSupport.kt index 5d1ea265bb..98835a3350 100644 --- a/core/src/main/kotlin/net/corda/core/internal/verification/VerificationSupport.kt +++ b/core/src/main/kotlin/net/corda/core/internal/verification/VerificationSupport.kt @@ -18,7 +18,7 @@ import java.security.PublicKey * Represents the operations required to resolve and verify a transaction. */ interface VerificationSupport { - val isResolutionLazy: Boolean get() = true + val isInProcess: Boolean get() = true val appClassLoader: ClassLoader diff --git a/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt b/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt index dab65e4374..ca38124ca9 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/internal/AttachmentsClassLoader.kt @@ -8,8 +8,8 @@ import net.corda.core.contracts.TransactionVerificationException import net.corda.core.contracts.TransactionVerificationException.OverlappingAttachmentsException import net.corda.core.contracts.TransactionVerificationException.PackageOwnershipException import net.corda.core.crypto.SecureHash -import net.corda.core.internal.JAVA_17_CLASS_FILE_FORMAT_MAJOR_VERSION -import net.corda.core.internal.JAVA_1_2_CLASS_FILE_FORMAT_MAJOR_VERSION +import net.corda.core.internal.JAVA_17_CLASS_FILE_MAJOR_VERSION +import net.corda.core.internal.JAVA_1_2_CLASS_FILE_MAJOR_VERSION import net.corda.core.internal.JarSignatureCollector import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.PlatformVersionSwitches @@ -340,7 +340,7 @@ object AttachmentsClassLoaderBuilder { val transactionClassLoader = AttachmentsClassLoader(attachments, key.params, txId, isAttachmentTrusted, parent) val serializers = try { createInstancesOfClassesImplementing(transactionClassLoader, SerializationCustomSerializer::class.java, - JAVA_1_2_CLASS_FILE_FORMAT_MAJOR_VERSION..JAVA_17_CLASS_FILE_FORMAT_MAJOR_VERSION) + JAVA_1_2_CLASS_FILE_MAJOR_VERSION..JAVA_17_CLASS_FILE_MAJOR_VERSION) } catch (ex: UnsupportedClassVersionError) { throw TransactionVerificationException.UnsupportedClassVersionError(txId, ex.message!!, ex) } diff --git a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt index 897598335b..4895f18226 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt @@ -1,12 +1,15 @@ package net.corda.core.transactions import net.corda.core.CordaException +import net.corda.core.CordaInternal import net.corda.core.contracts.* import net.corda.core.contracts.ComponentGroupEnum.* import net.corda.core.crypto.* import net.corda.core.identity.Party import net.corda.core.internal.deserialiseCommands import net.corda.core.internal.deserialiseComponentGroup +import net.corda.core.internal.getGroup +import net.corda.core.internal.getRequiredGroup import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.DeprecatedConstructorForDeserialization import net.corda.core.serialization.SerializedBytes @@ -29,8 +32,34 @@ abstract class TraversableTransaction(open val componentGroups: List) : this(componentGroups, DigestService.sha2_256) + /** + * Returns the attachments compatible with 4.11 and earlier. This may be empty, which means this transaction cannot be verified by a + * 4.11 node. On 4.12 and later these attachments are ignored. + */ + val legacyAttachments: List = deserialiseComponentGroup(componentGroups, SecureHash::class, ATTACHMENTS_GROUP) + + /** + * Returns the attachments compatible with 4.12 and later. This will be empty for transactions created on 4.11 or earlier. + * + * [legacyAttachments] and [nonLegacyAttachments] are independent of each other and may contain the same attachments. This is to provide backwards + * compatiblity and enable both 4.11 and 4.12 nodes to verify the same transaction. + */ + @CordaInternal + @JvmSynthetic + internal val nonLegacyAttachments: List = deserialiseComponentGroup(componentGroups, SecureHash::class, ATTACHMENTS_V2_GROUP) + /** Hashes of the ZIP/JAR files that are needed to interpret the contents of this wire transaction. */ - val attachments: List = deserialiseComponentGroup(componentGroups, SecureHash::class, ATTACHMENTS_GROUP) + val attachments: List + get() = when { + legacyAttachments.isEmpty() -> nonLegacyAttachments // 4.12+ only transaction + nonLegacyAttachments.isEmpty() -> legacyAttachments // 4.11 or earlier transaction + else -> nonLegacyAttachments // This is a backwards compatible transaction, but from an API PoV we're not concerned with the legacy attachments + } + + @CordaInternal + internal val allAttachments: Set + @JvmSynthetic + get() = legacyAttachments.toMutableSet().apply { addAll(nonLegacyAttachments) } /** Pointers to the input states on the ledger, identified by (tx identity hash, output index). */ override val inputs: List = deserialiseComponentGroup(componentGroups, StateRef::class, INPUTS_GROUP) @@ -67,18 +96,20 @@ abstract class TraversableTransaction(open val componentGroups: List> get() { - val result = mutableListOf(inputs, outputs, commands, attachments, references) + val result = mutableListOf(inputs, outputs, commands, legacyAttachments, references) notary?.let { result += listOf(it) } timeWindow?.let { result += listOf(it) } networkParametersHash?.let { result += listOf(it) } + result += nonLegacyAttachments return result } } @@ -153,12 +184,10 @@ class FilteredTransaction internal constructor( // This is required for visibility purposes, see FilteredTransaction.checkAllCommandsVisible() for more details. if (componentGroupIndex == COMMANDS_GROUP.ordinal && !signersIncluded) { signersIncluded = true - val signersGroupIndex = SIGNERS_GROUP.ordinal // There exist commands, thus the signers group is not empty. - val signersGroupComponents = wtx.componentGroups.first { it.groupIndex == signersGroupIndex } - filteredSerialisedComponents[signersGroupIndex] = signersGroupComponents.components.toMutableList() - filteredComponentNonces[signersGroupIndex] = wtx.availableComponentNonces[signersGroupIndex]!!.toMutableList() - filteredComponentHashes[signersGroupIndex] = wtx.availableComponentHashes[signersGroupIndex]!!.toMutableList() + filteredSerialisedComponents[SIGNERS_GROUP.ordinal] = wtx.componentGroups.getRequiredGroup(SIGNERS_GROUP).components.toMutableList() + filteredComponentNonces[SIGNERS_GROUP.ordinal] = wtx.availableComponentNonces[SIGNERS_GROUP.ordinal]!!.toMutableList() + filteredComponentHashes[SIGNERS_GROUP.ordinal] = wtx.availableComponentHashes[SIGNERS_GROUP.ordinal]!!.toMutableList() } } @@ -166,7 +195,8 @@ class FilteredTransaction internal constructor( wtx.inputs.forEachIndexed { internalIndex, it -> filter(it, INPUTS_GROUP.ordinal, internalIndex) } wtx.outputs.forEachIndexed { internalIndex, it -> filter(it, OUTPUTS_GROUP.ordinal, internalIndex) } wtx.commands.forEachIndexed { internalIndex, it -> filter(it, COMMANDS_GROUP.ordinal, internalIndex) } - wtx.attachments.forEachIndexed { internalIndex, it -> filter(it, ATTACHMENTS_GROUP.ordinal, internalIndex) } + wtx.legacyAttachments.forEachIndexed { internalIndex, it -> filter(it, ATTACHMENTS_GROUP.ordinal, internalIndex) } + wtx.nonLegacyAttachments.forEachIndexed { internalIndex, it -> filter(it, ATTACHMENTS_V2_GROUP.ordinal, internalIndex) } if (wtx.notary != null) filter(wtx.notary, NOTARY_GROUP.ordinal, 0) if (wtx.timeWindow != null) filter(wtx.timeWindow, TIMEWINDOW_GROUP.ordinal, 0) // Note that because [inputs] and [references] share the same type [StateRef], we use a wrapper for references [ReferenceStateRef], @@ -269,7 +299,7 @@ class FilteredTransaction internal constructor( */ @Throws(ComponentVisibilityException::class) fun checkAllComponentsVisible(componentGroupEnum: ComponentGroupEnum) { - val group = filteredComponentGroups.firstOrNull { it.groupIndex == componentGroupEnum.ordinal } + val group = filteredComponentGroups.getGroup(componentGroupEnum) if (group == null) { // If we don't receive elements of a particular component, check if its ordinal is bigger that the // groupHashes.size or if the group hash is allOnesHash, @@ -300,7 +330,7 @@ class FilteredTransaction internal constructor( */ @Throws(ComponentVisibilityException::class) fun checkCommandVisibility(publicKey: PublicKey) { - val commandSigners = componentGroups.firstOrNull { it.groupIndex == SIGNERS_GROUP.ordinal } + val commandSigners = componentGroups.getGroup(SIGNERS_GROUP) val expectedNumOfCommands = expectedNumOfCommands(publicKey, commandSigners) val receivedForThisKeyNumOfCommands = commands.filter { publicKey in it.signers }.size visibilityCheck(expectedNumOfCommands == receivedForThisKeyNumOfCommands) { diff --git a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt index 726788bb1b..91651ac006 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -18,10 +18,8 @@ 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.attachmentIds import net.corda.core.internal.equivalent import net.corda.core.internal.isUploaderTrusted -import net.corda.core.internal.kotlinMetadataVersion import net.corda.core.internal.verification.NodeVerificationSupport import net.corda.core.internal.verification.VerificationSupport import net.corda.core.internal.verification.toVerifyingServiceHub @@ -33,6 +31,9 @@ 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 @@ -204,37 +205,58 @@ data class SignedTransaction(val txBits: SerializedBytes, * * Depending on the contract attachments, this method will either verify this transaction in-process or send it to the external verifier * for out-of-process verification. + * + * @return The [FullTransaction] that was successfully verified in-process. Returns null if the verification was successfully done externally. */ @CordaInternal @JvmSynthetic - fun verifyInternal(verificationSupport: NodeVerificationSupport, checkSufficientSignatures: Boolean = true) { + internal fun verifyInternal(verificationSupport: NodeVerificationSupport, checkSufficientSignatures: Boolean = true): FullTransaction? { resolveAndCheckNetworkParameters(verificationSupport) - val verificationType = determineVerificationType(verificationSupport) + val verificationType = determineVerificationType() log.debug { "Transaction $id has verification type $verificationType" } - if (verificationType == VerificationType.IN_PROCESS || verificationType == VerificationType.BOTH) { - verifyInProcess(verificationSupport, checkSufficientSignatures) - } - if (verificationType == VerificationType.EXTERNAL || verificationType == VerificationType.BOTH) { - verificationSupport.externalVerifierHandle.verifyTransaction(this, checkSufficientSignatures) + 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(verificationSupport: VerificationSupport): VerificationType { - var old = false - var new = false - for (attachmentId in coreTransaction.attachmentIds) { - val (major, minor) = verificationSupport.getAttachment(attachmentId)?.kotlinMetadataVersion?.split(".") ?: continue - // Metadata version 1.1 maps to language versions 1.0 to 1.3 - if (major == "1" && minor == "1") { - old = true - } else { - new = true + private fun determineVerificationType(): VerificationType { + 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 } - return when { - old && new -> VerificationType.BOTH - old -> VerificationType.EXTERNAL - else -> VerificationType.IN_PROCESS + } + + private fun ensureSameResult(inProcessResult: Try, externalResult: Try<*>): FullTransaction { + return when (externalResult) { + is Success -> when (inProcessResult) { + is Success -> inProcessResult.value + is Failure -> throw IllegalStateException("In-process verification of $id failed, but it succeeded in external verifier") + .apply { addSuppressed(inProcessResult.exception) } + } + is Failure -> throw when (inProcessResult) { + is Success -> IllegalStateException("In-process verification of $id succeeded, but it failed in external verifier") + is Failure -> inProcessResult.exception // Throw the in-process exception, with the external exception suppressed + }.apply { addSuppressed(externalResult.exception) } } } @@ -244,11 +266,13 @@ data class SignedTransaction(val txBits: SerializedBytes, /** * 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 - fun verifyInProcess(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean) { - when (coreTransaction) { + 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) @@ -272,23 +296,25 @@ data class SignedTransaction(val txBits: SerializedBytes, } /** 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) { + 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) { + 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) { + private fun verifyRegularTransaction(verificationSupport: VerificationSupport, checkSufficientSignatures: Boolean): LedgerTransaction { val ltx = toLedgerTransactionInternal(verificationSupport, checkSufficientSignatures) try { ltx.verify() @@ -304,6 +330,7 @@ data class SignedTransaction(val txBits: SerializedBytes, checkReverifyAllowed(e) retryVerification(e.cause, e, ltx, verificationSupport) } + return ltx } private fun checkReverifyAllowed(ex: Throwable) { diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 5c37d7efe3..9db53fc49d 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -9,6 +9,7 @@ import net.corda.core.crypto.SignatureMetadata import net.corda.core.identity.Party import net.corda.core.internal.* import net.corda.core.internal.PlatformVersionSwitches.MIGRATE_ATTACHMENT_TO_SIGNATURE_CONSTRAINTS +import net.corda.core.internal.cordapp.ContractAttachmentWithLegacy import net.corda.core.internal.verification.VerifyingServiceHub import net.corda.core.internal.verification.toVerifyingServiceHub import net.corda.core.node.NetworkParameters @@ -152,7 +153,7 @@ open class TransactionBuilder( */ @Throws(MissingContractAttachments::class) fun toWireTransaction(services: ServicesForResolution, schemeId: Int): WireTransaction { - return toWireTransaction(services, schemeId, emptyMap()).apply { checkSupportedHashType() } + return toWireTransaction(services, schemeId, emptyMap()) } /** @@ -195,7 +196,7 @@ open class TransactionBuilder( val wireTx = SerializationFactory.defaultFactory.withCurrentContext(serializationContext) { // Sort the attachments to ensure transaction builds are stable. - val attachmentsBuilder = allContractAttachments.mapTo(TreeSet()) { it.id } + val attachmentsBuilder = allContractAttachments.mapTo(TreeSet()) { it.currentAttachment.id } attachmentsBuilder.addAll(attachments) attachmentsBuilder.removeAll(excludedAttachments) WireTransaction( @@ -207,7 +208,8 @@ open class TransactionBuilder( notary, window, referenceStates, - serviceHub.networkParametersService.currentHash + serviceHub.networkParametersService.currentHash, + allContractAttachments.mapNotNullTo(TreeSet()) { it.legacyAttachment?.id }.toList() ), privacySalt, serviceHub.digestService @@ -237,6 +239,7 @@ open class TransactionBuilder( /** * @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() @@ -249,11 +252,14 @@ open class TransactionBuilder( // 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 -> { - ((tryCount == 0) && fixupAttachments(wireTx.attachments, serviceHub, e)) + // 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.attachments, serviceHub, e)) + ((tryCount == 0) && fixupAttachments(wireTx.nonLegacyAttachments, serviceHub, e)) || addMissingAttachment(rootError.message ?: throw e, serviceHub, e) } @@ -347,7 +353,7 @@ open class TransactionBuilder( */ private fun selectContractAttachmentsAndOutputStateConstraints( serviceHub: VerifyingServiceHub - ): Pair, List>> { + ): Pair, List>> { // Determine the explicitly set contract attachments. val explicitContractToAttachments = attachments .mapNotNull { serviceHub.attachments.openAttachment(it) as? ContractAttachment } @@ -367,7 +373,7 @@ open class TransactionBuilder( = referencesWithTransactionState.groupBy { it.contract } val refStateContractAttachments = referenceStateGroups .filterNot { it.key in allContracts } - .map { refStateEntry -> serviceHub.getInstalledContractAttachment(refStateEntry.key, refStateEntry::value) } + .map { refStateEntry -> serviceHub.getInstalledContractAttachments(refStateEntry.key, refStateEntry::value) } // For each contract, resolve the AutomaticPlaceholderConstraint, and select the attachment. val contractAttachmentsAndResolvedOutputStates = allContracts.map { contract -> @@ -413,10 +419,10 @@ open class TransactionBuilder( outputStates: List>?, explicitContractAttachment: ContractAttachment?, serviceHub: VerifyingServiceHub - ): Pair>> { + ): Pair>> { val inputsAndOutputs = (inputStates ?: emptyList()) + (outputStates ?: emptyList()) - fun selectAttachmentForContract() = serviceHub.getInstalledContractAttachment(contractClassName) { + fun selectAttachmentForContract() = serviceHub.getInstalledContractAttachments(contractClassName) { inputsAndOutputs.filterNot { it.constraint in automaticConstraints } } @@ -429,14 +435,15 @@ open class TransactionBuilder( a system parameter that disables the hash constraint check. */ if (canMigrateFromHashToSignatureConstraint(inputStates, outputStates, serviceHub)) { - val attachment = selectAttachmentForContract() + val attachmentWithLegacy = selectAttachmentForContract() + val (attachment) = attachmentWithLegacy if (attachment.isSigned && (explicitContractAttachment == null || explicitContractAttachment.id == attachment.id)) { val signatureConstraint = makeSignatureAttachmentConstraint(attachment.signerKeys) require(signatureConstraint.isSatisfiedBy(attachment)) { "Selected output constraint: $signatureConstraint not satisfying ${attachment.id}" } val resolvedOutputStates = outputStates?.map { if (it.constraint in automaticConstraints) it.copy(constraint = signatureConstraint) else it } ?: emptyList() - return attachment to resolvedOutputStates + return attachmentWithLegacy to resolvedOutputStates } } @@ -467,9 +474,9 @@ open class TransactionBuilder( } } // This *has* to be used by this transaction as it is explicit - explicitContractAttachment + ContractAttachmentWithLegacy(explicitContractAttachment, null) // By definition there can be no legacy version } else { - hashAttachment ?: selectAttachmentForContract() + hashAttachment?.let { ContractAttachmentWithLegacy(it, null) } ?: selectAttachmentForContract() } // For Exit transactions (no output states) there is no need to resolve the output constraints. @@ -491,10 +498,10 @@ open class TransactionBuilder( } // This is the logic to determine the constraint which will replace the AutomaticPlaceholderConstraint. - val defaultOutputConstraint = selectAttachmentConstraint(contractClassName, inputStates, selectedAttachment, serviceHub) + val defaultOutputConstraint = selectAttachmentConstraint(contractClassName, inputStates, selectedAttachment.currentAttachment, serviceHub) // Sanity check that the selected attachment actually passes. - val constraintAttachment = AttachmentWithContext(selectedAttachment, contractClassName, serviceHub.networkParameters.whitelistedContractImplementations) + val constraintAttachment = AttachmentWithContext(selectedAttachment.currentAttachment, contractClassName, serviceHub.networkParameters.whitelistedContractImplementations) require(defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) { "Selected output constraint: $defaultOutputConstraint not satisfying $selectedAttachment" } @@ -506,7 +513,7 @@ open class TransactionBuilder( } else { // If the constraint on the output state is already set, and is not a valid transition or can't be transitioned, then fail early. inputStates?.forEach { input -> - require(outputConstraint.canBeTransitionedFrom(input.constraint, selectedAttachment)) { + require(outputConstraint.canBeTransitionedFrom(input.constraint, selectedAttachment.currentAttachment)) { "Output state constraint $outputConstraint cannot be transitioned from ${input.constraint}" } } @@ -629,12 +636,18 @@ open class TransactionBuilder( SignatureAttachmentConstraint.create(CompositeKey.Builder().addKeys(attachmentSigners) .build()) - private inline fun VerifyingServiceHub.getInstalledContractAttachment( + private inline fun VerifyingServiceHub.getInstalledContractAttachments( contractClassName: String, statesForException: () -> List> - ): ContractAttachment { - return cordappProvider.getContractAttachment(contractClassName) + ): ContractAttachmentWithLegacy { + // TODO Stop using legacy attachments when the 4.12 min platform version is reached https://r3-cev.atlassian.net/browse/ENT-11479 + val attachmentWithLegacy = cordappProvider.getContractAttachments(contractClassName) ?: throw MissingContractAttachments(statesForException(), contractClassName) + if (attachmentWithLegacy.legacyAttachment == null) { + log.warnOnce("Contract $contractClassName does not have a legacy (4.11 or earlier) version installed. This means the " + + "transaction will not be compatible with older nodes.") + } + return attachmentWithLegacy } private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters): Boolean { @@ -646,6 +659,7 @@ open class TransactionBuilder( @Throws(AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class) fun verify(services: ServiceHub) { + // TODO ENT-11445: Need to verify via SignedTransaction to ensure legacy components also work toLedgerTransaction(services).verify() } diff --git a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt index 856847a5b8..83ecf55704 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -25,6 +25,7 @@ import net.corda.core.internal.SerializedStateAndRef import net.corda.core.internal.SerializedTransactionState import net.corda.core.internal.createComponentGroups 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 @@ -162,7 +163,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr @JvmSynthetic fun toLedgerTransactionInternal(verificationSupport: VerificationSupport): LedgerTransaction { // Look up public keys to authenticated identities. - val authenticatedCommands = if (verificationSupport.isResolutionLazy) { + val authenticatedCommands = if (verificationSupport.isInProcess) { commands.lazyMapped { cmd, _ -> val parties = verificationSupport.getParties(cmd.signers).filterNotNull() CommandWithParties(cmd.signers, parties, cmd.value) @@ -193,13 +194,15 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr } val resolvedReferences = serializedResolvedReferences.lazyMapped(toStateAndRef) - val resolvedAttachments = if (verificationSupport.isResolutionLazy) { - attachments.lazyMapped { id, _ -> + val resolvedAttachments = if (verificationSupport.isInProcess) { + // The 4.12+ node only looks at the new attachments group + nonLegacyAttachments.lazyMapped { id, _ -> verificationSupport.getAttachment(id) ?: throw AttachmentResolutionException(id) } } else { - verificationSupport.getAttachments(attachments).mapIndexed { index, attachment -> - attachment ?: throw AttachmentResolutionException(attachments[index]) + // The 4.11 external verifier only looks at the legacy attachments group since it will only contain attachments compatible with 4.11 + verificationSupport.getAttachments(legacyAttachments).mapIndexed { index, attachment -> + attachment ?: throw AttachmentResolutionException(legacyAttachments[index]) } } @@ -248,7 +251,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr // This calculates a value that is slightly lower than the actual re-serialized version. But it is stable and does not depend on the classloader. fun componentGroupSize(componentGroup: ComponentGroupEnum): Int { - return this.componentGroups.firstOrNull { it.groupIndex == componentGroup.ordinal }?.let { cg -> cg.components.sumOf { it.size } + 4 } ?: 0 + return componentGroups.getGroup(componentGroup)?.let { cg -> cg.components.sumOf { it.size } + 4 } ?: 0 } // Check attachments size first as they are most likely to go over the limit. With ContractAttachment instances @@ -320,10 +323,10 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr * nothing about the rest. */ internal val availableComponentNonces: Map> by lazy { - if(digestService.hashAlgorithm == SecureHash.SHA2_256) { + if (digestService.hashAlgorithm == SecureHash.SHA2_256) { componentGroups.associate { it.groupIndex to it.components.mapIndexed { internalIndex, internalIt -> digestService.componentHash(internalIt, privacySalt, it.groupIndex, internalIndex) } } } else { - componentGroups.associate { it.groupIndex to it.components.mapIndexed { internalIndex, _ -> digestService.computeNonce(privacySalt, it.groupIndex, internalIndex) } } + componentGroups.associate { it.groupIndex to List(it.components.size) { internalIndex -> digestService.computeNonce(privacySalt, it.groupIndex, internalIndex) } } } } @@ -343,23 +346,10 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr * @throws IllegalArgumentException if the signature key doesn't appear in any command. */ fun checkSignature(sig: TransactionSignature) { - require(commands.any { it.signers.any { sig.by in it.keys } }) { "Signature key doesn't match any command" } + require(commands.any { it.signers.any { signer -> sig.by in signer.keys } }) { "Signature key doesn't match any command" } sig.verify(id) } - companion object { - @CordaInternal - @Deprecated("Do not use, this is internal API") - fun createComponentGroups(inputs: List, - outputs: List>, - commands: List>, - attachments: List, - notary: Party?, - timeWindow: TimeWindow?): List { - return createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow, emptyList(), null) - } - } - override fun toString(): String { val buf = StringBuilder() buf.appendLine("Transaction:") diff --git a/finance/contracts/build.gradle b/finance/contracts/build.gradle index de48c6454f..345d641d84 100644 --- a/finance/contracts/build.gradle +++ b/finance/contracts/build.gradle @@ -51,7 +51,7 @@ cordapp { minimumPlatformVersion 1 contract { name "Corda Finance Demo" - versionId 1 + versionId 2 vendor "R3" licence "Open Source (Apache 2)" } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/cordapp/CordappLoader.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/cordapp/CordappLoader.kt index 87eca433c4..dcfc3a6a14 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/cordapp/CordappLoader.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/cordapp/CordappLoader.kt @@ -14,6 +14,11 @@ interface CordappLoader : AutoCloseable { */ val cordapps: List + /** + * Returns all legacy (4.11 or older) contract CorDapps. These are used to form backward compatible transactions. + */ + val legacyContractCordapps: List + /** * Returns a [ClassLoader] containing all types from all [Cordapp]s. */ diff --git a/node/build.gradle b/node/build.gradle index da28a8798e..44c08ef17a 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -23,8 +23,10 @@ configurations { integrationTestImplementation.extendsFrom testImplementation integrationTestRuntimeOnly.extendsFrom testRuntimeOnly - slowIntegrationTestCompile.extendsFrom testImplementation + slowIntegrationTestImplementation.extendsFrom testImplementation slowIntegrationTestRuntimeOnly.extendsFrom testRuntimeOnly + + corda4_11 } sourceSets { @@ -89,6 +91,7 @@ processTestResources { from(tasks.getByPath(":testing:cordapps:cashobservers:jar")) { rename 'testing-cashobservers-cordapp-.*.jar', 'testing-cashobservers-cordapp.jar' } + from(configurations.corda4_11) } // To find potential version conflicts, run "gradle htmlDependencyReport" and then look in @@ -104,30 +107,22 @@ dependencies { implementation project(':common-configuration-parsing') implementation project(':common-logging') implementation project(':serialization') - - implementation "io.opentelemetry:opentelemetry-api:${open_telemetry_version}" // Backwards compatibility goo: Apps expect confidential-identities to be loaded by default. // We could eventually gate this on a target-version check. implementation project(':confidential-identities') - + implementation "io.opentelemetry:opentelemetry-api:${open_telemetry_version}" // Log4J: logging framework (with SLF4J bindings) implementation "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}" implementation "org.apache.logging.log4j:log4j-web:${log4j_version}" implementation "org.slf4j:jul-to-slf4j:$slf4j_version" - implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" - implementation "org.fusesource.jansi:jansi:$jansi_version" implementation "com.google.guava:guava:$guava_version" implementation "commons-io:commons-io:$commons_io_version" - // For caches rather than guava implementation "com.github.ben-manes.caffeine:caffeine:$caffeine_version" - // For async logging implementation "com.lmax:disruptor:$disruptor_version" - // Artemis: for reliable p2p message queues. // TODO: remove the forced update of commons-collections and beanutils when artemis updates them implementation "org.apache.commons:commons-collections4:${commons_collections_version}" @@ -142,92 +137,66 @@ dependencies { // Bouncy castle support needed for X509 certificate manipulation implementation "org.bouncycastle:bcprov-jdk18on:${bouncycastle_version}" implementation "org.bouncycastle:bcpkix-jdk18on:${bouncycastle_version}" - implementation "com.esotericsoftware:kryo:$kryo_version" - implementation "com.fasterxml.jackson.core:jackson-annotations:${jackson_version}" implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_version" - - runtimeOnly("org.apache.activemq:artemis-amqp-protocol:${artemis_version}") { - // Gains our proton-j version from core module. - exclude group: 'org.apache.qpid', module: 'proton-j' - exclude group: 'org.jgroups', module: 'jgroups' - } - // Manifests: for reading stuff from the manifest file implementation "com.jcabi:jcabi-manifests:$jcabi_manifests_version" - // Coda Hale's Metrics: for monitoring of key statistics implementation "io.dropwizard.metrics:metrics-jmx:$metrics_version" implementation "io.github.classgraph:classgraph:$class_graph_version" implementation "org.liquibase:liquibase-core:$liquibase_version" - // TypeSafe Config: for simple and human friendly config files. implementation "com.typesafe:config:$typesafe_config_version" - implementation "io.reactivex:rxjava:$rxjava_version" - implementation("org.apache.activemq:artemis-amqp-protocol:${artemis_version}") { // Gains our proton-j version from core module. exclude group: 'org.apache.qpid', module: 'proton-j' exclude group: 'org.jgroups', module: 'jgroups' } + // For H2 database support in persistence + implementation "com.h2database:h2:$h2_version" + // SQL connection pooling library + implementation "com.zaxxer:HikariCP:${hikari_version}" + // Hibernate: an object relational mapper for writing state objects to the database automatically. + implementation "org.hibernate:hibernate-core:$hibernate_version" + implementation "org.hibernate:hibernate-java8:$hibernate_version" + // OkHTTP: Simple HTTP library. + implementation "com.squareup.okhttp3:okhttp:$okhttp_version" + // Apache Shiro: authentication, authorization and session management. + implementation "org.apache.shiro:shiro-core:${shiro_version}" + //Picocli for command line interface + implementation "info.picocli:picocli:$picocli_version" + // BFT-Smart dependencies + implementation 'com.github.bft-smart:library:master-v1.1-beta-g6215ec8-87' + // Java Atomix: RAFT library + implementation 'io.atomix.copycat:copycat-client:1.2.3' + implementation 'io.atomix.copycat:copycat-server:1.2.3' + implementation 'io.atomix.catalyst:catalyst-netty:1.1.2' + // Jolokia JVM monitoring agent, required to push logs through slf4j + implementation "org.jolokia:jolokia-jvm:${jolokia_version}:agent" + // Optional New Relic JVM reporter, used to push metrics to the configured account associated with a newrelic.yml configuration. See https://mvnrepository.com/artifact/com.palominolabs.metrics/metrics-new-relic + implementation "com.palominolabs.metrics:metrics-new-relic:${metrics_new_relic_version}" + // Adding native SSL library to allow using native SSL with Artemis and AMQP + implementation "io.netty:netty-tcnative-boringssl-static:$tcnative_version" + implementation 'org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.8.0' - 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(project(':test-cli')) + testImplementation(project(':test-utils')) // Unit testing helpers. - testImplementation "org.assertj:assertj-core:${assertj_version}" testImplementation project(':node-driver') testImplementation project(':core-test-utils') testImplementation project(':test-utils') testImplementation project(':client:jfx') testImplementation project(':finance:contracts') testImplementation project(':finance:workflows') - // sample test schemas testImplementation project(path: ':finance:contracts', configuration: 'testArtifacts') - - // For H2 database support in persistence - implementation "com.h2database:h2:$h2_version" - - // SQL connection pooling library - implementation "com.zaxxer:HikariCP:${hikari_version}" - - // Hibernate: an object relational mapper for writing state objects to the database automatically. - implementation "org.hibernate:hibernate-core:$hibernate_version" - implementation "org.hibernate:hibernate-java8:$hibernate_version" - - // OkHTTP: Simple HTTP library. - implementation "com.squareup.okhttp3:okhttp:$okhttp_version" - - // Apache Shiro: authentication, authorization and session management. - implementation "org.apache.shiro:shiro-core:${shiro_version}" - - //Picocli for command line interface - implementation "info.picocli:picocli:$picocli_version" - - integrationTestImplementation project(":testing:cordapps:dbfailure:dbfcontracts") - - // Integration test helpers - integrationTestImplementation "de.javakaffee:kryo-serializers:$kryo_serializer_version" - integrationTestImplementation "junit:junit:$junit_version" - integrationTestImplementation "org.assertj:assertj-core:${assertj_version}" - integrationTestImplementation "org.apache.qpid:qpid-jms-client:${protonj_version}" - integrationTestImplementation "net.i2p.crypto:eddsa:$eddsa_version" - - // BFT-Smart dependencies - implementation 'com.github.bft-smart:library:master-v1.1-beta-g6215ec8-87' - - // Java Atomix: RAFT library - implementation 'io.atomix.copycat:copycat-client:1.2.3' - implementation 'io.atomix.copycat:copycat-server:1.2.3' - implementation 'io.atomix.catalyst:catalyst-netty:1.1.2' - + testImplementation project(':testing:cordapps:dbfailure:dbfworkflows') + testImplementation "org.assertj:assertj-core:${assertj_version}" + testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" + testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}" + testImplementation "junit:junit:$junit_version" // Jetty dependencies for NetworkMapClient test. // Web stuff: for HTTP[S] servlets testImplementation "org.hamcrest:hamcrest-library:2.1" @@ -238,43 +207,33 @@ dependencies { testImplementation "com.google.jimfs:jimfs:1.1" testImplementation "co.paralleluniverse:quasar-core:$quasar_version" testImplementation "com.natpryce:hamkrest:$hamkrest_version" - // Jersey for JAX-RS implementation for use in Jetty testImplementation "org.glassfish.jersey.core:jersey-server:${jersey_version}" testImplementation "org.glassfish.jersey.containers:jersey-container-servlet-core:${jersey_version}" testImplementation "org.glassfish.jersey.containers:jersey-container-jetty-http:${jersey_version}" - // Jolokia JVM monitoring agent, required to push logs through slf4j - implementation "org.jolokia:jolokia-jvm:${jolokia_version}:agent" - // Optional New Relic JVM reporter, used to push metrics to the configured account associated with a newrelic.yml configuration. See https://mvnrepository.com/artifact/com.palominolabs.metrics/metrics-new-relic - implementation "com.palominolabs.metrics:metrics-new-relic:${metrics_new_relic_version}" - - // Adding native SSL library to allow using native SSL with Artemis and AMQP - implementation "io.netty:netty-tcnative-boringssl-static:$tcnative_version" - implementation 'org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.8.0' - - // Byteman for runtime (termination) rules injection on the running node - // Submission tool allowing to install rules on running nodes - slowIntegrationTestCompile "org.jboss.byteman:byteman-submit:4.0.22" - // The actual Byteman agent which should only be in the classpath of the out of process nodes - slowIntegrationTestCompile "org.jboss.byteman:byteman:4.0.22" - - testImplementation(project(':test-cli')) - testImplementation(project(':test-utils')) - - slowIntegrationTestCompile sourceSets.main.output - slowIntegrationTestCompile sourceSets.test.output - slowIntegrationTestCompile configurations.implementation - slowIntegrationTestCompile configurations.testImplementation - slowIntegrationTestRuntimeOnly configurations.runtimeOnly - slowIntegrationTestRuntimeOnly configurations.testRuntimeOnly + 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}" + integrationTestImplementation project(":testing:cordapps:dbfailure:dbfcontracts") integrationTestImplementation project(":testing:cordapps:missingmigration") - - testImplementation project(':testing:cordapps:dbfailure:dbfworkflows') + // Integration test helpers + integrationTestImplementation "de.javakaffee:kryo-serializers:$kryo_serializer_version" + integrationTestImplementation "junit:junit:$junit_version" + integrationTestImplementation "org.assertj:assertj-core:${assertj_version}" + integrationTestImplementation "org.apache.qpid:qpid-jms-client:${protonj_version}" + integrationTestImplementation "net.i2p.crypto:eddsa:$eddsa_version" // used by FinalityFlowErrorHandlingTest slowIntegrationTestImplementation project(':testing:cordapps:cashobservers') + // Byteman for runtime (termination) rules injection on the running node + // Submission tool allowing to install rules on running nodes + slowIntegrationTestImplementation "org.jboss.byteman:byteman-submit:4.0.22" + // The actual Byteman agent which should only be in the classpath of the out of process nodes + slowIntegrationTestImplementation "org.jboss.byteman:byteman:4.0.22" + + corda4_11 "net.corda:corda-finance-contracts:4.11" } tasks.withType(JavaCompile).configureEach { diff --git a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt index c22e129251..5a87c87c41 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt @@ -1,51 +1,42 @@ package net.corda.node.services import co.paralleluniverse.fibers.Suspendable -import net.corda.core.contracts.* -import net.corda.core.flows.* -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.CordaX500Name +import net.corda.core.contracts.Amount +import net.corda.core.contracts.TransactionVerificationException +import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.FlowException +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.ReceiveFinalityFlow +import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party -import net.corda.core.internal.* import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.startFlow -import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap -import net.corda.testing.common.internal.checkNotOnClasspath +import net.corda.finance.DOLLARS +import net.corda.finance.flows.CashIssueFlow +import net.corda.finance.workflows.asset.CashUtils import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.singleIdentity -import net.corda.testing.driver.DriverDSL import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.NodeParameters import net.corda.testing.driver.driver import net.corda.testing.node.NotarySpec +import net.corda.testing.node.internal.FINANCE_CORDAPPS +import net.corda.testing.node.internal.FINANCE_WORKFLOWS_CORDAPP import net.corda.testing.node.internal.enclosedCordapp import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Test -import java.net.URL -import java.net.URLClassLoader -import kotlin.io.path.createDirectories -import kotlin.io.path.div +import java.util.Currency class AttachmentLoadingTests { - private companion object { - val isolatedJar: URL = AttachmentLoadingTests::class.java.getResource("/isolated.jar")!! - val isolatedClassLoader = URLClassLoader(arrayOf(isolatedJar)) - val issuanceFlowClass: Class> = uncheckedCast(loadFromIsolated("net.corda.isolated.workflows.IsolatedIssuanceFlow")) - - init { - checkNotOnClasspath("net.corda.isolated.contracts.AnotherDummyContract") { - "isolated module cannot be on the classpath as otherwise it will be pulled into the nodes the driver creates and " + - "contaminate the tests. This is a known issue with the driver and we must work around it until it's fixed." - } - } - - fun loadFromIsolated(className: String): Class<*> = Class.forName(className, false, isolatedClassLoader) - } - @Test(timeout=300_000) fun `contracts downloaded from the network are not executed`() { driver(DriverParameters( @@ -53,61 +44,36 @@ class AttachmentLoadingTests { notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = false)), cordappsForAllNodes = listOf(enclosedCordapp()) )) { - installIsolatedCordapp(ALICE_NAME) - val (alice, bob) = listOf( - startNode(providedName = ALICE_NAME), - startNode(providedName = BOB_NAME) + startNode(NodeParameters(ALICE_NAME, additionalCordapps = FINANCE_CORDAPPS)), + startNode(NodeParameters(BOB_NAME, additionalCordapps = listOf(FINANCE_WORKFLOWS_CORDAPP))) ).transpose().getOrThrow() - val stateRef = alice.rpc.startFlowDynamic(issuanceFlowClass, 1234).returnValue.getOrThrow() + alice.rpc.startFlow(::CashIssueFlow, 10.DOLLARS, OpaqueBytes.of(0x00), defaultNotaryIdentity).returnValue.getOrThrow() - assertThatThrownBy { alice.rpc.startFlow(::ConsumeAndBroadcastFlow, stateRef, bob.nodeInfo.singleIdentity()).returnValue.getOrThrow() } + assertThatThrownBy { alice.rpc.startFlow(::ConsumeAndBroadcastFlow, 10.DOLLARS, bob.nodeInfo.singleIdentity()).returnValue.getOrThrow() } // ConsumeAndBroadcastResponderFlow re-throws any non-FlowExceptions with just their class name in the message so that // we can verify here Bob threw the correct exception .hasMessage(TransactionVerificationException.UntrustedAttachmentsException::class.java.name) } } - @Test(timeout=300_000) - fun `contract is executed if installed locally`() { - driver(DriverParameters( - startNodesInProcess = false, - notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = false)), - cordappsForAllNodes = listOf(enclosedCordapp()) - )) { - installIsolatedCordapp(ALICE_NAME) - installIsolatedCordapp(BOB_NAME) - - val (alice, bob) = listOf( - startNode(providedName = ALICE_NAME), - startNode(providedName = BOB_NAME) - ).transpose().getOrThrow() - - val stateRef = alice.rpc.startFlowDynamic(issuanceFlowClass, 1234).returnValue.getOrThrow() - alice.rpc.startFlow(::ConsumeAndBroadcastFlow, stateRef, bob.nodeInfo.singleIdentity()).returnValue.getOrThrow() - } - } - - private fun DriverDSL.installIsolatedCordapp(name: CordaX500Name) { - val cordappsDir = (baseDirectory(name) / "cordapps").createDirectories() - isolatedJar.toPath().copyToDirectory(cordappsDir) - } - @InitiatingFlow @StartableByRPC - class ConsumeAndBroadcastFlow(private val stateRef: StateRef, private val otherSide: Party) : FlowLogic() { + class ConsumeAndBroadcastFlow(private val amount: Amount, private val otherSide: Party) : FlowLogic() { @Suspendable override fun call() { val notary = serviceHub.networkMapCache.notaryIdentities[0] - val stateAndRef = serviceHub.toStateAndRef(stateRef) - val stx = serviceHub.signInitialTransaction( - TransactionBuilder(notary) - .addInputState(stateAndRef) - .addOutputState(ConsumeContract.State()) - .addCommand(Command(ConsumeContract.Cmd, ourIdentity.owningKey)) + val builder = TransactionBuilder(notary) + val (_, keysForSigning) = CashUtils.generateSpend( + serviceHub, + builder, + amount, + ourIdentityAndCert, + otherSide, + anonymous = false ) - stx.verify(serviceHub, checkSufficientSignatures = false) + val stx = serviceHub.signInitialTransaction(builder, keysForSigning) val session = initiateFlow(otherSide) subFlow(FinalityFlow(stx, session)) // It's important we wait on this dummy receive, as otherwise it's possible we miss any errors the other side throws @@ -129,16 +95,4 @@ class AttachmentLoadingTests { otherSide.send("OK") } } - - class ConsumeContract : Contract { - override fun verify(tx: LedgerTransaction) { - // Accept everything - } - - class State : ContractState { - override val participants: List get() = emptyList() - } - - object Cmd : TypeOnlyCommandData() - } } diff --git a/node/src/integration-test/resources/isolated.jar b/node/src/integration-test/resources/isolated.jar deleted file mode 100644 index 3df99a710f..0000000000 Binary files a/node/src/integration-test/resources/isolated.jar and /dev/null differ diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 7d9d670b0d..57cbea71f5 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -79,6 +79,7 @@ import net.corda.node.internal.classloading.requireAnnotation import net.corda.node.internal.cordapp.CordappConfigFileProvider import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.node.internal.cordapp.JarScanningCordappLoader +import net.corda.node.internal.cordapp.JarScanningCordappLoader.Companion.LEGACY_CONTRACTS_DIR_NAME import net.corda.node.internal.cordapp.VirtualCordapp import net.corda.node.internal.rpc.proxies.AuthenticatedRpcOpsProxy import net.corda.node.internal.rpc.proxies.ThreadContextAdjustingRpcOpsProxy @@ -187,6 +188,7 @@ import java.util.function.Consumer import javax.persistence.EntityManager import javax.sql.DataSource import kotlin.io.path.div +import kotlin.io.path.exists /** * A base node implementation that can be customised either for production (with real implementations that do real @@ -853,6 +855,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } return JarScanningCordappLoader.fromDirectories( configuration.cordappDirectories, + (configuration.baseDirectory / LEGACY_CONTRACTS_DIR_NAME).takeIf { it.exists() }, versionInfo, extraCordapps = generatedCordapps, signerKeyFingerprintBlacklist = blacklistedKeys diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt index e870be4047..60e6483074 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt @@ -6,6 +6,7 @@ import net.corda.core.cordapp.Cordapp import net.corda.core.cordapp.CordappContext import net.corda.core.flows.FlowLogic import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER +import net.corda.core.internal.cordapp.ContractAttachmentWithLegacy import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.internal.cordapp.CordappProviderInternal import net.corda.core.internal.groupByMultipleKeys @@ -38,6 +39,7 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader, fun start() { loadContractsIntoAttachmentStore(cordappLoader.cordapps) + loadContractsIntoAttachmentStore(cordappLoader.legacyContractCordapps) flowToCordapp = makeFlowToCordapp() // Load the fix-ups after uploading any new contracts into attachment storage. attachmentFixups.load(cordappLoader.appClassLoader) @@ -56,12 +58,18 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader, } override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? { - // loadContractsIntoAttachmentStore makes sure the jarHash is the attachment ID - return cordappLoader.cordapps.find { contractClassName in it.contractClassNames }?.jarHash + return cordappLoader.cordapps.findCordapp(contractClassName) } - override fun getContractAttachment(contractClassName: ContractClassName): ContractAttachment? { - return getContractAttachmentID(contractClassName)?.let(::getContractAttachment) + override fun getContractAttachments(contractClassName: ContractClassName): ContractAttachmentWithLegacy? { + val currentAttachmentId = getContractAttachmentID(contractClassName) ?: return null + val legacyAttachmentId = cordappLoader.legacyContractCordapps.findCordapp(contractClassName) + return ContractAttachmentWithLegacy(getContractAttachment(currentAttachmentId), legacyAttachmentId?.let(::getContractAttachment)) + } + + private fun List.findCordapp(contractClassName: ContractClassName): AttachmentId? { + // loadContractsIntoAttachmentStore makes sure the jarHash is the attachment ID + return find { contractClassName in it.contractClassNames }?.jarHash } private fun loadContractsIntoAttachmentStore(cordapps: List) { diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt index 0d80548cd3..3fdf5bd9d2 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt @@ -1,6 +1,7 @@ package net.corda.node.internal.cordapp import io.github.classgraph.ClassGraph +import io.github.classgraph.ClassInfo import io.github.classgraph.ClassInfoList import io.github.classgraph.ScanResult import net.corda.common.logging.errorReporting.CordappErrors @@ -18,13 +19,15 @@ import net.corda.core.internal.JarSignatureCollector import net.corda.core.internal.PlatformVersionSwitches import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.internal.cordapp.CordappImpl.Companion.UNKNOWN_INFO +import net.corda.core.internal.cordapp.KotlinMetadataVersion +import net.corda.core.internal.cordapp.LanguageVersion import net.corda.core.internal.cordapp.get import net.corda.core.internal.flatMapToSet +import net.corda.core.internal.groupByMultipleKeys import net.corda.core.internal.hash import net.corda.core.internal.isAbstractClass import net.corda.core.internal.loadClassOfType import net.corda.core.internal.location -import net.corda.core.internal.groupByMultipleKeys import net.corda.core.internal.mapToSet import net.corda.core.internal.notary.NotaryService import net.corda.core.internal.notary.SinglePartyNotaryService @@ -42,6 +45,7 @@ import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug +import net.corda.core.utilities.trace import net.corda.node.VersionInfo import net.corda.nodeapi.internal.cordapp.CordappLoader import net.corda.nodeapi.internal.coreContractClasses @@ -50,6 +54,7 @@ import java.lang.reflect.Modifier import java.net.URLClassLoader import java.nio.file.Path import java.util.ServiceLoader +import java.util.TreeSet import java.util.jar.JarInputStream import java.util.jar.Manifest import kotlin.io.path.absolutePathString @@ -57,27 +62,35 @@ import kotlin.io.path.exists import kotlin.io.path.inputStream import kotlin.io.path.isSameFileAs import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.useDirectoryEntries import kotlin.reflect.KClass +import kotlin.reflect.KProperty1 /** * Handles CorDapp loading and classpath scanning of CorDapp JARs * * @property cordappJars The classpath of cordapp JARs + * @property legacyContractJars Legacy contract CorDapps (4.11 or earlier) needed for backwards compatibility with 4.11 nodes. */ @Suppress("TooManyFunctions") class JarScanningCordappLoader(private val cordappJars: Set, + private val legacyContractJars: Set = emptySet(), private val versionInfo: VersionInfo = VersionInfo.UNKNOWN, private val extraCordapps: List = emptyList(), private val signerKeyFingerprintBlacklist: List = emptyList()) : CordappLoader { companion object { private val logger = contextLogger() + const val LEGACY_CONTRACTS_DIR_NAME = "legacy-contracts" + /** * Creates a CordappLoader from multiple directories. * * @param cordappDirs Directories used to scan for CorDapp JARs. + * @param legacyContractsDir Directory containing legacy contract CorDapps (4.11 or earlier). */ fun fromDirectories(cordappDirs: Collection, + legacyContractsDir: Path? = null, versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List = emptyList(), signerKeyFingerprintBlacklist: List = emptyList()): JarScanningCordappLoader { @@ -86,12 +99,14 @@ class JarScanningCordappLoader(private val cordappJars: Set, .asSequence() .flatMap { if (it.exists()) it.listDirectoryEntries("*.jar") else emptyList() } .toSet() - return JarScanningCordappLoader(cordappJars, versionInfo, extraCordapps, signerKeyFingerprintBlacklist) + val legacyContractJars = legacyContractsDir?.useDirectoryEntries("*.jar") { it.toSet() } ?: emptySet() + return JarScanningCordappLoader(cordappJars, legacyContractJars, versionInfo, extraCordapps, signerKeyFingerprintBlacklist) } } init { logger.debug { "cordappJars: $cordappJars" } + logger.debug { "legacyContractJars: $legacyContractJars" } } override val appClassLoader = URLClassLoader(cordappJars.stream().map { it.toUri().toURL() }.toTypedArray(), javaClass.classLoader) @@ -99,21 +114,46 @@ class JarScanningCordappLoader(private val cordappJars: Set, private val internal by lazy(::InternalHolder) override val cordapps: List - get() = internal.cordapps + get() = internal.nonLegacyCordapps + + override val legacyContractCordapps: List + get() = internal.legacyContractCordapps override fun close() = appClassLoader.close() private inner class InternalHolder { - val cordapps = cordappJars.mapTo(ArrayList(), ::scanCordapp) + val nonLegacyCordapps = cordappJars.mapTo(ArrayList(), ::scanCordapp) + val legacyContractCordapps = legacyContractJars.map(::scanCordapp) init { - checkInvalidCordapps() - checkDuplicateCordapps() - checkContractOverlap() - cordapps += extraCordapps + commonChecks(nonLegacyCordapps, LanguageVersion::isNonLegacyCompatible) + nonLegacyCordapps += extraCordapps + if (legacyContractCordapps.isNotEmpty()) { + commonChecks(legacyContractCordapps, LanguageVersion::isLegacyCompatible) + checkLegacyContracts() + } } - private fun checkInvalidCordapps() { + private fun commonChecks(cordapps: List, compatibilityProperty: KProperty1) { + for (cordapp in cordapps) { + check(compatibilityProperty(cordapp.languageVersion)) { + val isLegacyCompatibleCheck = compatibilityProperty == LanguageVersion::isLegacyCompatible + val msg = when { + isLegacyCompatibleCheck -> "not legacy; please remove or place it in the node's CorDapps directory." + cordapp.contractClassNames.isEmpty() -> "legacy (should be 4.12 or later)" + else -> "legacy contracts; please place it in the node's '$LEGACY_CONTRACTS_DIR_NAME' directory." + } + "CorDapp ${cordapp.jarFile} is $msg" + } + } + checkInvalidCordapps(cordapps) + checkDuplicateCordapps(cordapps) + // The same contract may occur in both 4.11 and 4.12 CorDapps for ledger compatibility, so we only check for overlap within each + // compatibility group + checkContractOverlap(cordapps) + } + + private fun checkInvalidCordapps(cordapps: List) { val invalidCordapps = LinkedHashMap() for (cordapp in cordapps) { @@ -139,7 +179,7 @@ class JarScanningCordappLoader(private val cordappJars: Set, } } - private fun checkDuplicateCordapps() { + private fun checkDuplicateCordapps(cordapps: List) { for (group in cordapps.groupBy { it.jarHash }.values) { if (group.size > 1) { throw DuplicateCordappsInstalledException(group[0], group.drop(1)) @@ -147,12 +187,38 @@ class JarScanningCordappLoader(private val cordappJars: Set, } } - private fun checkContractOverlap() { + private fun checkContractOverlap(cordapps: List) { cordapps.groupByMultipleKeys(CordappImpl::contractClassNames) { contract, cordapp1, cordapp2 -> throw IllegalStateException("Contract $contract occuring in multiple CorDapps (${cordapp1.name}, ${cordapp2.name}). " + "Please remove the previous version when upgrading to a new version.") } } + + private fun checkLegacyContracts() { + for (legacyCordapp in legacyContractCordapps) { + if (legacyCordapp.contractClassNames.isEmpty()) continue + logger.debug { "Contracts CorDapp ${legacyCordapp.name} is legacy (4.11 or older), searching for corresponding 4.12+ contracts" } + for (legacyContract in legacyCordapp.contractClassNames) { + val newerCordapp = nonLegacyCordapps.find { legacyContract in it.contractClassNames } + checkNotNull(newerCordapp) { + "Contract $legacyContract in legacy CorDapp (4.11 or older) '${legacyCordapp.jarFile}' does not have a " + + "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 " + + "'${legacyCordapp.jarFile}' (${legacyCordapp.contractVersionId})" + } + } + } + } + + private val CordappImpl.contractVersionId: Int + get() = when (val info = info) { + is Cordapp.Info.Contract -> info.versionId + is Cordapp.Info.ContractAndWorkflow -> info.contract.versionId + else -> 1 + } } private fun ScanResult.toCordapp(path: Path): CordappImpl { @@ -160,6 +226,8 @@ class JarScanningCordappLoader(private val cordappJars: Set, val info = parseCordappInfo(manifest, CordappImpl.jarName(path)) val minPlatformVersion = manifest?.get(CordappImpl.MIN_PLATFORM_VERSION)?.toIntOrNull() ?: 1 val targetPlatformVersion = manifest?.get(CordappImpl.TARGET_PLATFORM_VERSION)?.toIntOrNull() ?: minPlatformVersion + val languageVersion = determineLanguageVersion(path) + logger.debug { "$path: $languageVersion" } return CordappImpl( path, findContractClassNames(this), @@ -177,6 +245,7 @@ class JarScanningCordappLoader(private val cordappJars: Set, info, minPlatformVersion, targetPlatformVersion, + languageVersion = languageVersion, notaryService = findNotaryService(this), explicitCordappClasses = findAllCordappClasses(this) ) @@ -360,6 +429,36 @@ class JarScanningCordappLoader(private val cordappJars: Set, private fun ClassInfoList.getAllConcreteClasses(type: KClass): List> { return mapNotNull { loadClass(it.name, type)?.takeUnless(Class<*>::isAbstractClass) } } + + private fun ScanResult.determineLanguageVersion(cordappJar: Path): LanguageVersion { + val allClasses = allClassesAsMap.values + if (allClasses.isEmpty()) { + return LanguageVersion.Data + } + val classFileMajorVersion = allClasses.maxOf { it.classfileMajorVersion } + val kotlinMetadataVersion = allClasses + .mapNotNullTo(TreeSet()) { it.kotlinMetadataVersion() } + .let { kotlinMetadataVersions -> + // If there's more than one minor version of Kotlin + if (kotlinMetadataVersions.size > 1 && kotlinMetadataVersions.mapToSet { it.copy(patch = 0) }.size > 1) { + logger.warn("CorDapp $cordappJar comprised of multiple Kotlin versions (kotlinMetadataVersions=$kotlinMetadataVersions). " + + "This may cause compatibility issues.") + } + kotlinMetadataVersions.takeIf { it.isNotEmpty() }?.last() + } + try { + return LanguageVersion.Bytecode(classFileMajorVersion, kotlinMetadataVersion) + } catch (e: IllegalArgumentException) { + throw IllegalStateException("Unable to load CorDapp $cordappJar: ${e.message}") + } + } + + private fun ClassInfo.kotlinMetadataVersion(): KotlinMetadataVersion? { + val kotlinMetadata = getAnnotationInfo(Metadata::class.java) ?: return null + val kotlinMetadataVersion = KotlinMetadataVersion.from(kotlinMetadata.parameterValues.get("mv").value as IntArray) + logger.trace { "$name: $kotlinMetadataVersion" } + return kotlinMetadataVersion + } } /** diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index fc17b10d77..e89578af87 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt @@ -6,8 +6,6 @@ import com.google.common.hash.HashCode import com.google.common.hash.Hashing import com.google.common.hash.HashingInputStream import com.google.common.io.CountingInputStream -import kotlinx.metadata.jvm.KotlinModuleMetadata -import kotlinx.metadata.jvm.UnstableMetadataApi import net.corda.core.CordaRuntimeException import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractAttachment @@ -18,7 +16,6 @@ import net.corda.core.internal.AbstractAttachment import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER import net.corda.core.internal.FetchAttachmentsFlow import net.corda.core.internal.JarSignatureCollector -import net.corda.core.internal.InternalAttachment import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.P2P_UPLOADER import net.corda.core.internal.RPC_UPLOADER @@ -28,7 +25,6 @@ import net.corda.core.internal.Version import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.cordapp.CordappImpl.Companion.CORDAPP_CONTRACT_VERSION import net.corda.core.internal.cordapp.CordappImpl.Companion.DEFAULT_CORDAPP_VERSION -import net.corda.core.internal.entries import net.corda.core.internal.isUploaderTrusted import net.corda.core.internal.readFully import net.corda.core.internal.utilities.ZipBombDetector @@ -266,8 +262,7 @@ class NodeAttachmentService @JvmOverloads constructor( private val checkOnLoad: Boolean, uploader: String?, override val signerKeys: List, - override val kotlinMetadataVersion: String? - ) : AbstractAttachment(dataLoader, uploader), InternalAttachment, SerializeAsToken { + ) : AbstractAttachment(dataLoader, uploader), SerializeAsToken { override fun open(): InputStream { val stream = super.open() @@ -280,7 +275,6 @@ class NodeAttachmentService @JvmOverloads constructor( private val checkOnLoad: Boolean, private val uploader: String?, private val signerKeys: List, - private val kotlinMetadataVersion: String? ) : SerializationToken { override fun fromToken(context: SerializeAsTokenContext) = AttachmentImpl( id, @@ -288,12 +282,10 @@ class NodeAttachmentService @JvmOverloads constructor( checkOnLoad, uploader, signerKeys, - kotlinMetadataVersion ) } - override fun toToken(context: SerializeAsTokenContext) = - Token(id, checkOnLoad, uploader, signerKeys, kotlinMetadataVersion) + override fun toToken(context: SerializeAsTokenContext) = Token(id, checkOnLoad, uploader, signerKeys) } private val attachmentContentCache = NonInvalidatingWeightBasedCache( @@ -311,24 +303,13 @@ class NodeAttachmentService @JvmOverloads constructor( } } - @OptIn(UnstableMetadataApi::class) private fun createAttachmentFromDatabase(attachment: DBAttachment): Attachment { - // TODO Cache this as a column in the database - val jis = JarInputStream(attachment.content.inputStream()) - val kotlinMetadataVersions = jis.entries() - .filter { it.name.endsWith(".kotlin_module") } - .map { KotlinModuleMetadata.read(jis.readAllBytes()).version } - .toSortedSet() - if (kotlinMetadataVersions.size > 1) { - log.warn("Attachment ${attachment.attId} seems to be comprised of multiple Kotlin versions: $kotlinMetadataVersions") - } val attachmentImpl = AttachmentImpl( id = SecureHash.create(attachment.attId), dataLoader = { attachment.content }, checkOnLoad = checkAttachmentsOnLoad, uploader = attachment.uploader, - signerKeys = attachment.signers?.toList() ?: emptyList(), - kotlinMetadataVersion = kotlinMetadataVersions.takeIf { it.isNotEmpty() }?.last()?.toString() + signerKeys = attachment.signers?.toList() ?: emptyList() ) val contracts = attachment.contractClassNames return if (!contracts.isNullOrEmpty()) { @@ -376,14 +357,6 @@ class NodeAttachmentService @JvmOverloads constructor( return import(jar, uploader, filename) } - override fun privilegedImportOrGetAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId { - return try { - import(jar, uploader, filename) - } catch (faee: FileAlreadyExistsException) { - AttachmentId.create(faee.message!!) - } - } - override fun hasAttachment(attachmentId: AttachmentId): Boolean = database.transaction { currentDBSession().find(DBAttachment::class.java, attachmentId.toString()) != null } diff --git a/node/src/main/kotlin/net/corda/node/verification/ExternalVerifierHandleImpl.kt b/node/src/main/kotlin/net/corda/node/verification/ExternalVerifierHandleImpl.kt index 0108e78dde..c386f287b5 100644 --- a/node/src/main/kotlin/net/corda/node/verification/ExternalVerifierHandleImpl.kt +++ b/node/src/main/kotlin/net/corda/node/verification/ExternalVerifierHandleImpl.kt @@ -195,7 +195,7 @@ class ExternalVerifierHandleImpl( "${server.localPort}", log.level.name.lowercase() ) - log.debug { "Verifier command: $command" } + log.debug { "External verifier command: $command" } val logsDirectory = (baseDirectory / "logs").createDirectories() verifierProcess = ProcessBuilder(command) .redirectOutput(Redirect.appendTo((logsDirectory / "verifier-stdout.log").toFile())) diff --git a/node/src/test/kotlin/net/corda/node/internal/NodeH2SecurityTests.kt b/node/src/test/kotlin/net/corda/node/internal/NodeH2SecurityTests.kt index e9c677b7f1..1c46fa8c54 100644 --- a/node/src/test/kotlin/net/corda/node/internal/NodeH2SecurityTests.kt +++ b/node/src/test/kotlin/net/corda/node/internal/NodeH2SecurityTests.kt @@ -1,9 +1,5 @@ package net.corda.node.internal -import org.mockito.kotlin.atLeast -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever import net.corda.core.identity.CordaX500Name import net.corda.core.serialization.SerializeAsToken import net.corda.core.utilities.NetworkHostAndPort @@ -20,12 +16,17 @@ import net.corda.nodeapi.internal.persistence.DatabaseConfig import org.assertj.core.api.Assertions.assertThat import org.h2.tools.Server import org.junit.Test +import org.mockito.kotlin.atLeast +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import java.net.InetAddress import java.sql.Connection import java.sql.DatabaseMetaData -import java.util.* +import java.util.Properties import java.util.concurrent.ExecutorService import javax.sql.DataSource +import kotlin.io.path.Path import kotlin.test.assertFailsWith class NodeH2SecurityTests { @@ -133,13 +134,13 @@ class NodeH2SecurityTests { init { whenever(config.database).thenReturn(database) whenever(config.dataSourceProperties).thenReturn(hikaryProperties) - whenever(config.baseDirectory).thenReturn(mock()) + whenever(config.baseDirectory).thenReturn(Path(".")) whenever(config.effectiveH2Settings).thenAnswer { NodeH2Settings(address) } whenever(config.telemetry).thenReturn(mock()) whenever(config.myLegalName).thenReturn(CordaX500Name(null, "client-${address.toString()}", "Corda", "London", null, "GB")) } - private inner class MockNode: Node(config, VersionInfo.UNKNOWN, false) { + private inner class MockNode : Node(config, VersionInfo.UNKNOWN, false) { fun startDb() = startDatabase() override fun makeMessagingService(): MessagingService { diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt index c61994d6d7..88942ca7cc 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt @@ -36,8 +36,9 @@ import kotlin.test.assertFailsWith class CordappProviderImplTests { private companion object { - val financeContractsJar = this::class.java.getResource("/corda-finance-contracts.jar")!!.toPath() - val financeWorkflowsJar = this::class.java.getResource("/corda-finance-workflows.jar")!!.toPath() + val currentFinanceContractsJar = this::class.java.getResource("/corda-finance-contracts.jar")!!.toPath() + val currentFinanceWorkflowsJar = this::class.java.getResource("/corda-finance-workflows.jar")!!.toPath() + val legacyFinanceContractsJar = this::class.java.getResource("/corda-finance-contracts-4.11.jar")!!.toPath() @JvmField val ID1 = AttachmentId.randomSHA256() @@ -83,7 +84,7 @@ class CordappProviderImplTests { @Test(timeout=300_000) fun `test that we find a cordapp class that is loaded into the store`() { - val provider = newCordappProvider(setOf(financeContractsJar)) + val provider = newCordappProvider(setOf(currentFinanceContractsJar)) val expected = provider.cordapps.first() val actual = provider.getCordappForClass(Cash::class.java.name) @@ -94,7 +95,7 @@ class CordappProviderImplTests { @Test(timeout=300_000) fun `test that we find an attachment for a cordapp contract class`() { - val provider = newCordappProvider(setOf(financeContractsJar)) + val provider = newCordappProvider(setOf(currentFinanceContractsJar)) val expected = provider.getAppContext(provider.cordapps.first()).attachmentId val actual = provider.getContractAttachmentID(Cash::class.java.name) @@ -106,7 +107,7 @@ class CordappProviderImplTests { fun `test cordapp configuration`() { val configProvider = MockCordappConfigProvider() configProvider.cordappConfigs["corda-finance-contracts"] = ConfigFactory.parseString("key=value") - val provider = newCordappProvider(setOf(financeContractsJar), cordappConfigProvider = configProvider) + val provider = newCordappProvider(setOf(currentFinanceContractsJar), cordappConfigProvider = configProvider) val expected = provider.getAppContext(provider.cordapps.first()).config @@ -115,23 +116,33 @@ class CordappProviderImplTests { @Test(timeout=300_000) fun getCordappForFlow() { - val provider = newCordappProvider(setOf(financeWorkflowsJar)) + val provider = newCordappProvider(setOf(currentFinanceWorkflowsJar)) val cashIssueFlow = CashIssueFlow(10.DOLLARS, OpaqueBytes.of(0x00), TestIdentity(ALICE_NAME).party) - assertThat(provider.getCordappForFlow(cashIssueFlow)?.jarPath?.toPath()).isEqualTo(financeWorkflowsJar) + assertThat(provider.getCordappForFlow(cashIssueFlow)?.jarPath?.toPath()).isEqualTo(currentFinanceWorkflowsJar) } @Test(timeout=300_000) fun `does not load the same flow across different CorDapps`() { val unsignedJar = tempFolder.newFile("duplicate.jar").toPath() - financeWorkflowsJar.copyTo(unsignedJar, overwrite = true) + currentFinanceWorkflowsJar.copyTo(unsignedJar, overwrite = true) // We just need to change the file's hash and thus avoid the duplicate CorDapp check unsignedJar.unsignJar() - assertThat(unsignedJar.hash).isNotEqualTo(financeWorkflowsJar.hash) + assertThat(unsignedJar.hash).isNotEqualTo(currentFinanceWorkflowsJar.hash) assertFailsWith { - newCordappProvider(setOf(financeWorkflowsJar, unsignedJar)) + newCordappProvider(setOf(currentFinanceWorkflowsJar, unsignedJar)) } } + @Test(timeout=300_000) + fun `retrieving legacy attachment for contract`() { + val provider = newCordappProvider(setOf(currentFinanceContractsJar), setOf(legacyFinanceContractsJar)) + val (current, legacy) = provider.getContractAttachments(Cash::class.java.name)!! + assertThat(current.id).isEqualTo(currentFinanceContractsJar.hash) + assertThat(legacy?.id).isEqualTo(legacyFinanceContractsJar.hash) + // getContractAttachmentID should always return the non-legacy attachment ID + assertThat(provider.getContractAttachmentID(Cash::class.java.name)).isEqualTo(currentFinanceContractsJar.hash) + } + @Test(timeout=300_000) fun `test fixup rule that adds attachment`() { val fixupJar = File.createTempFile("fixup", ".jar") @@ -220,8 +231,10 @@ class CordappProviderImplTests { return this } - private fun newCordappProvider(cordappJars: Set, cordappConfigProvider: CordappConfigProvider = stubConfigProvider): CordappProviderImpl { - val loader = JarScanningCordappLoader(cordappJars) + private fun newCordappProvider(cordappJars: Set, + legacyContractJars: Set = emptySet(), + cordappConfigProvider: CordappConfigProvider = stubConfigProvider): CordappProviderImpl { + val loader = JarScanningCordappLoader(cordappJars, legacyContractJars) return CordappProviderImpl(loader, cordappConfigProvider, attachmentStore).apply { start() } } } diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt index 23ef514454..4de05a3910 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt @@ -27,6 +27,7 @@ import net.corda.testing.core.internal.ContractJarTestUtils.makeTestContractJar import net.corda.testing.core.internal.JarSignatureTestUtils.generateKey import net.corda.testing.core.internal.JarSignatureTestUtils.getJarSigners import net.corda.testing.core.internal.JarSignatureTestUtils.signJar +import net.corda.testing.core.internal.JarSignatureTestUtils.unsignJar import net.corda.testing.internal.LogHelper import net.corda.testing.node.internal.cordappWithPackages import org.assertj.core.api.Assertions.assertThat @@ -69,8 +70,9 @@ class DummyRPCFlow : FlowLogic() { class JarScanningCordappLoaderTest { private companion object { - val financeContractsJar = this::class.java.getResource("/corda-finance-contracts.jar")!!.toPath() - val financeWorkflowsJar = this::class.java.getResource("/corda-finance-workflows.jar")!!.toPath() + val legacyFinanceContractsJar = this::class.java.getResource("/corda-finance-contracts-4.11.jar")!!.toPath() + val currentFinanceContractsJar = this::class.java.getResource("/corda-finance-contracts.jar")!!.toPath() + val currentFinanceWorkflowsJar = this::class.java.getResource("/corda-finance-workflows.jar")!!.toPath() init { LogHelper.setLevel(JarScanningCordappLoaderTest::class) @@ -90,20 +92,20 @@ class JarScanningCordappLoaderTest { @Test(timeout=300_000) fun `constructed CordappImpls contains the right classes`() { - val loader = JarScanningCordappLoader(setOf(financeContractsJar, financeWorkflowsJar)) + val loader = JarScanningCordappLoader(setOf(currentFinanceContractsJar, currentFinanceWorkflowsJar)) val (contractsCordapp, workflowsCordapp) = loader.cordapps assertThat(contractsCordapp.contractClassNames).contains(Cash::class.java.name, CommercialPaper::class.java.name) assertThat(contractsCordapp.customSchemas).contains(CashSchemaV1, CommercialPaperSchemaV1) assertThat(contractsCordapp.info).isInstanceOf(Cordapp.Info.Contract::class.java) assertThat(contractsCordapp.allFlows).isEmpty() - assertThat(contractsCordapp.jarFile).isEqualTo(financeContractsJar) + assertThat(contractsCordapp.jarFile).isEqualTo(currentFinanceContractsJar) assertThat(workflowsCordapp.allFlows).contains(CashIssueFlow::class.java, CashPaymentFlow::class.java) assertThat(workflowsCordapp.services).contains(ConfigHolder::class.java) assertThat(workflowsCordapp.info).isInstanceOf(Cordapp.Info.Workflow::class.java) assertThat(workflowsCordapp.contractClassNames).isEmpty() - assertThat(workflowsCordapp.jarFile).isEqualTo(financeWorkflowsJar) + assertThat(workflowsCordapp.jarFile).isEqualTo(currentFinanceWorkflowsJar) for (actualCordapp in loader.cordapps) { assertThat(actualCordapp.cordappClasses) @@ -196,22 +198,32 @@ class JarScanningCordappLoaderTest { @Test(timeout=300_000) fun `loads app signed by allowed certificate`() { - val loader = JarScanningCordappLoader(setOf(financeContractsJar), signerKeyFingerprintBlacklist = emptyList()) + val loader = JarScanningCordappLoader(setOf(currentFinanceContractsJar), signerKeyFingerprintBlacklist = emptyList()) assertThat(loader.cordapps).hasSize(1) } @Test(timeout = 300_000) fun `does not load app signed by blacklisted certificate`() { - val cordappLoader = JarScanningCordappLoader(setOf(financeContractsJar), signerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES) + val cordappLoader = JarScanningCordappLoader(setOf(currentFinanceContractsJar), signerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES) assertThatExceptionOfType(InvalidCordappException::class.java).isThrownBy { cordappLoader.cordapps } } + @Test(timeout=300_000) + fun `does not load legacy contract CorDapp signed by blacklisted certificate`() { + val unsignedJar = currentFinanceContractsJar.duplicate { unsignJar() } + val loader = JarScanningCordappLoader(setOf(unsignedJar), setOf(legacyFinanceContractsJar), signerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES) + assertThatExceptionOfType(InvalidCordappException::class.java) + .isThrownBy { loader.cordapps } + .withMessageContaining("Corresponding contracts are signed by blacklisted key(s)") + .withMessageContaining(legacyFinanceContractsJar.name) + } + @Test(timeout=300_000) fun `does not load duplicate CorDapps`() { - val duplicateJar = financeWorkflowsJar.duplicate() - val loader = JarScanningCordappLoader(setOf(financeWorkflowsJar, duplicateJar)) + val duplicateJar = currentFinanceWorkflowsJar.duplicate() + val loader = JarScanningCordappLoader(setOf(currentFinanceWorkflowsJar, duplicateJar)) assertFailsWith { loader.cordapps } @@ -235,7 +247,7 @@ class JarScanningCordappLoaderTest { @Test(timeout=300_000) fun `loads app signed by both allowed and non-blacklisted certificate`() { - val jar = financeWorkflowsJar.duplicate { + val jar = currentFinanceWorkflowsJar.duplicate { tempFolder.root.toPath().generateKey("testAlias", "testPassword", ALICE_NAME.toString()) tempFolder.root.toPath().signJar(absolutePathString(), "testAlias", "testPassword") } @@ -244,6 +256,38 @@ class JarScanningCordappLoaderTest { assertThat(loader.cordapps).hasSize(1) } + @Test(timeout=300_000) + fun `loads both legacy and current versions of the same contracts CorDapp`() { + val loader = JarScanningCordappLoader(setOf(currentFinanceContractsJar), setOf(legacyFinanceContractsJar)) + assertThat(loader.cordapps).hasSize(1) // Legacy contract CorDapps are not part of the main list + assertThat(loader.legacyContractCordapps).hasSize(1) + assertThat(loader.legacyContractCordapps.single().jarFile).isEqualTo(legacyFinanceContractsJar) + } + + @Test(timeout=300_000) + fun `does not load legacy contracts CorDapp without the corresponding current version`() { + val loader = JarScanningCordappLoader(setOf(currentFinanceWorkflowsJar), setOf(legacyFinanceContractsJar)) + assertThatIllegalStateException() + .isThrownBy { loader.legacyContractCordapps } + .withMessageContaining("does not have a corresponding newer version (4.12 or later). Please add this corresponding CorDapp or remove the legacy one.") + } + + @Test(timeout=300_000) + fun `checks if legacy contract CorDapp is actually legacy`() { + val loader = JarScanningCordappLoader(setOf(currentFinanceContractsJar), setOf(currentFinanceContractsJar)) + assertThatIllegalStateException() + .isThrownBy { loader.legacyContractCordapps } + .withMessageContaining("${currentFinanceContractsJar.name} is not legacy; please remove or place it in the node's CorDapps directory.") + } + + @Test(timeout=300_000) + fun `does not load if legacy CorDapp present in general list`() { + val loader = JarScanningCordappLoader(setOf(legacyFinanceContractsJar)) + assertThatIllegalStateException() + .isThrownBy { loader.cordapps } + .withMessageContaining("${legacyFinanceContractsJar.name} is legacy contracts; please place it in the node's 'legacy-contracts' directory.") + } + private inline fun Path.duplicate(name: String = "duplicate.jar", modify: Path.() -> Unit = { }): Path { val copy = tempFolder.newFile(name).toPath() copyTo(copy, overwrite = true) @@ -252,7 +296,7 @@ class JarScanningCordappLoaderTest { } private fun minAndTargetCordapp(minVersion: Int?, targetVersion: Int?): Path { - return financeWorkflowsJar.duplicate { + return currentFinanceWorkflowsJar.duplicate { modifyJarManifest { manifest -> manifest.setOrDeleteAttribute("Min-Platform-Version", minVersion?.toString()) manifest.setOrDeleteAttribute("Target-Platform-Version", targetVersion?.toString()) diff --git a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeParams.kt b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeParams.kt index a4a5f2ea72..4eb065d787 100644 --- a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeParams.kt +++ b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeParams.kt @@ -18,6 +18,7 @@ class NodeParams @JvmOverloads constructor( val rpcAdminPort: Int, val users: List, val cordappJars: List = emptyList(), + val legacyContractJars: List = emptyList(), val jarDirs: List = emptyList(), val clientRpcConfig: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT, val devMode: Boolean = true, diff --git a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt index 41f06b28ca..4b0a42dde2 100644 --- a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt +++ b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt @@ -138,6 +138,10 @@ class NodeProcess( log.info("Node directory: {}", nodeDir) val cordappsDir = (nodeDir / CORDAPPS_DIR_NAME).createDirectory() params.cordappJars.forEach { it.copyToDirectory(cordappsDir) } + if (params.legacyContractJars.isNotEmpty()) { + val legacyContractsDir = (nodeDir / "legacy-contracts").createDirectories() + params.legacyContractJars.forEach { it.copyToDirectory(legacyContractsDir) } + } (nodeDir / "node.conf").writeText(params.createNodeConfig(isNotary)) networkParametersCopier.install(nodeDir) nodeInfoFilesCopier.addConfig(nodeDir) diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt index 04f9f81b62..a8bd092de6 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt @@ -7,7 +7,6 @@ import net.corda.core.contracts.StateRef import net.corda.core.contracts.TimeWindow import net.corda.core.contracts.TransactionState import net.corda.core.crypto.Crypto -import net.corda.core.crypto.Crypto.generateKeyPair import net.corda.core.crypto.DigestService import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty @@ -49,13 +48,11 @@ import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity import java.io.ByteArrayOutputStream -import java.io.IOException -import java.net.ServerSocket import java.nio.file.Path import java.security.KeyPair import java.security.cert.X509CRL import java.security.cert.X509Certificate -import java.util.* +import java.util.Properties import java.util.jar.JarOutputStream import java.util.jar.Manifest import java.util.zip.ZipEntry @@ -111,7 +108,7 @@ fun createDevIntermediateCaCertPath( */ fun createDevNodeCaCertPath( legalName: CordaX500Name, - nodeKeyPair: KeyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME), + nodeKeyPair: KeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME), rootCaName: X500Principal = defaultRootCaName, intermediateCaName: X500Principal = defaultIntermediateCaName ): Triple { @@ -156,7 +153,6 @@ fun fixedCrlSource(crls: Set): CrlSource { } } -/** This is the same as the deprecated [WireTransaction] c'tor but avoids the deprecation warning. */ @SuppressWarnings("LongParameterList") fun createWireTransaction(inputs: List, attachments: List, @@ -164,9 +160,10 @@ fun createWireTransaction(inputs: List, commands: List>, notary: Party?, timeWindow: TimeWindow?, + legacyAttachments: List = emptyList(), privacySalt: PrivacySalt = PrivacySalt(), digestService: DigestService = DigestService.default): WireTransaction { - val componentGroups = createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow, emptyList(), null) + val componentGroups = createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow, emptyList(), null, legacyAttachments) return WireTransaction(componentGroups, privacySalt, digestService) } @@ -251,20 +248,5 @@ fun withTestSerializationEnvIfNotSet(block: () -> R): R { } } -/** - * Used to check if particular port is already bound i.e. not vacant - */ -fun isLocalPortBound(port: Int): Boolean { - return try { - ServerSocket(port).use { - // Successful means that the port was vacant - false - } - } catch (e: IOException) { - // Failed to open server socket means that it is already bound by someone - true - } -} - @JvmField val IS_S390X = System.getProperty("os.arch") == "s390x" diff --git a/verifier/src/main/kotlin/net/corda/verifier/ExternalVerificationContext.kt b/verifier/src/main/kotlin/net/corda/verifier/ExternalVerificationContext.kt index 3db0294a2f..60e49dcd2c 100644 --- a/verifier/src/main/kotlin/net/corda/verifier/ExternalVerificationContext.kt +++ b/verifier/src/main/kotlin/net/corda/verifier/ExternalVerificationContext.kt @@ -16,7 +16,7 @@ class ExternalVerificationContext( private val externalVerifier: ExternalVerifier, private val transactionInputsAndReferences: Map ) : VerificationSupport { - override val isResolutionLazy: Boolean get() = false + override val isInProcess: Boolean get() = false override fun getParties(keys: Collection): List = externalVerifier.getParties(keys) diff --git a/verifier/src/main/kotlin/net/corda/verifier/ExternalVerifier.kt b/verifier/src/main/kotlin/net/corda/verifier/ExternalVerifier.kt index 140788746c..91f60d0060 100644 --- a/verifier/src/main/kotlin/net/corda/verifier/ExternalVerifier.kt +++ b/verifier/src/main/kotlin/net/corda/verifier/ExternalVerifier.kt @@ -132,6 +132,7 @@ class ExternalVerifier( return URLClassLoader(cordappJarUrls, javaClass.classLoader) } + @Suppress("INVISIBLE_MEMBER") private fun verifyTransaction(request: VerificationRequest) { val verificationContext = ExternalVerificationContext(appClassLoader, attachmentsClassLoaderCache, this, request.stxInputsAndReferences) val result: Try = try {