From d54f1e2eeb7847aff72595a901452755335e452f Mon Sep 17 00:00:00 2001 From: Adel El-Beik Date: Mon, 4 Nov 2024 19:44:25 +0000 Subject: [PATCH 1/2] ENT-12373: Can now cope with diff input states from diff rotated CorDapps. --- .../net/corda/core/contracts/RotatedKeys.kt | 2 + .../core/transactions/TransactionBuilder.kt | 8 +- .../corda/node/ContractWithRotatedKeyTest.kt | 100 ++++++++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) diff --git a/core/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt b/core/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt index 71d3aca7f6..88a1a374fa 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt @@ -75,6 +75,8 @@ data class RotatedKeys(val rotatedSigningKeys: List> = emptyLis } } + fun rotateToHash(key: PublicKey) = rotate(key.hash.sha256()) + private fun rotate(key: SecureHash): SecureHash { return rotateMap[key] ?: key } 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 163c4446ee..14cbf5db34 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -657,7 +657,9 @@ open class TransactionBuilder( constraints.any { it is HashAttachmentConstraint } -> constraints.find { it is HashAttachmentConstraint }!! // TODO, we don't currently support mixing signature constraints with different signers. This will change once we introduce third party signers. - constraints.count { it is SignatureAttachmentConstraint } > 1 -> + (constraints.count { it is SignatureAttachmentConstraint } > 1) && + (constraints.filterIsInstance().map { serviceHub?.toVerifyingServiceHub()?.rotatedKeys?.rotateToHash(it.key) ?: it.key}.toSet().size > 1) + -> throw IllegalArgumentException("Cannot mix SignatureAttachmentConstraints signed by different parties in the same transaction.") // This ensures a smooth migration from a Whitelist Constraint to a Signature Constraint @@ -673,6 +675,10 @@ open class TransactionBuilder( // When all input states have the same constraint. constraints.size == 1 -> constraints.single() + // if we are here then the multiple SignatureAttachmentConstraint keys must be rotations of each other due to above check + (constraints.count { it is SignatureAttachmentConstraint } > 1) + -> constraints.filterIsInstance().first { it == makeSignatureAttachmentConstraint(attachmentToUse.signerKeys) } + else -> throw IllegalArgumentException("Unexpected constraints $constraints.") } diff --git a/node/src/integration-test/kotlin/net/corda/node/ContractWithRotatedKeyTest.kt b/node/src/integration-test/kotlin/net/corda/node/ContractWithRotatedKeyTest.kt index e58f63ea9f..512d05ac9d 100644 --- a/node/src/integration-test/kotlin/net/corda/node/ContractWithRotatedKeyTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/ContractWithRotatedKeyTest.kt @@ -2,14 +2,18 @@ package net.corda.node import net.corda.core.crypto.sha256 import net.corda.core.internal.hash +import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.finance.DOLLARS import net.corda.finance.GBP import net.corda.finance.POUNDS import net.corda.finance.USD +import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.CashIssueAndPaymentFlow +import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow +import net.corda.finance.`issued by` import net.corda.finance.workflows.getCashBalance import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.RotatedCorDappSignerKeyConfiguration @@ -33,6 +37,7 @@ import org.junit.Test import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever import kotlin.test.assertEquals +import kotlin.test.assertFailsWith class ContractWithRotatedKeyTest { private val ref = OpaqueBytes.of(0x01) @@ -131,4 +136,99 @@ class ContractWithRotatedKeyTest { keyStoreDir1.close() keyStoreDir2.close() } + + @Test(timeout = 300_000) + fun `transaction can be created with multiple contract input states from rotated CorDapps`() { + val keyStoreDir1 = SelfCleaningDir() + val keyStoreDir2 = SelfCleaningDir() + + val packageOwnerKey1 = keyStoreDir1.path.generateKey(alias="1-testcordapp-rsa") + val packageOwnerKey2 = keyStoreDir2.path.generateKey(alias="1-testcordapp-rsa") + + val unsignedFinanceCorDapp1 = cordappWithPackages("net.corda.finance", "migration", "META-INF.services") + val unsignedFinanceCorDapp2 = cordappWithPackages("net.corda.finance", "migration", "META-INF.services").copy(versionId = 2) + + val signedFinanceCorDapp1 = unsignedFinanceCorDapp1.signed( keyStoreDir1.path ) + val signedFinanceCorDapp2 = unsignedFinanceCorDapp2.signed( keyStoreDir2.path ) + + val configOverrides = { conf: NodeConfiguration -> + val rotatedKeys = listOf(RotatedCorDappSignerKeyConfiguration(listOf(packageOwnerKey1.hash.sha256().toString(), packageOwnerKey2.hash.sha256().toString()))) + doReturn(rotatedKeys).whenever(conf).rotatedCordappSignerKeys + } + + val alice = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME, additionalCordapps = listOf(signedFinanceCorDapp1), configOverrides = configOverrides)) + + val flow1 = alice.services.startFlow(CashIssueAndPaymentFlow(300.DOLLARS, ref, alice.party, false, mockNet.defaultNotaryIdentity)) + val flow2 = alice.services.startFlow(CashIssueAndPaymentFlow(1000.POUNDS, ref, alice.party, false, mockNet.defaultNotaryIdentity)) + mockNet.runNetwork() + flow1.resultFuture.getOrThrow() + flow2.resultFuture.getOrThrow() + + val alice2 = restartNodeAndDeleteOldCorDapps(mockNet, alice, parameters = InternalMockNodeParameters(additionalCordapps = listOf(signedFinanceCorDapp2), configOverrides = configOverrides)) + + val flow3 = alice2.services.startFlow(CashIssueFlow(700.DOLLARS, ref, mockNet.defaultNotaryIdentity)) + mockNet.runNetwork() + flow3.resultFuture.getOrThrow() + + val vaultStates = alice2.services.vaultService.queryBy(Cash.State::class.java).states + assertEquals(3, vaultStates.size) + + val outputState = Cash.State(1000.POUNDS `issued by` alice2.party.ref(1), alice2.party) + val tx = TransactionBuilder(notary = mockNet.defaultNotaryIdentity, serviceHub = alice2.services).apply { + addInputState(vaultStates[0]) + addInputState(vaultStates[1]) + addInputState(vaultStates[2]) + addOutputState(outputState) + addCommand(Cash.Commands.Move(), listOf(alice2.party.owningKey)) + } + tx.toWireTransaction(alice2.services) + + keyStoreDir1.close() + keyStoreDir2.close() + } + + @Test(timeout = 300_000) + fun `transaction creation fails with multiple contract input states from different CorDapps`() { + val keyStoreDir1 = SelfCleaningDir() + val keyStoreDir2 = SelfCleaningDir() + + val unsignedFinanceCorDapp1 = cordappWithPackages("net.corda.finance", "migration", "META-INF.services") + val unsignedFinanceCorDapp2 = cordappWithPackages("net.corda.finance", "migration", "META-INF.services").copy(versionId = 2) + + val signedFinanceCorDapp1 = unsignedFinanceCorDapp1.signed( keyStoreDir1.path ) + val signedFinanceCorDapp2 = unsignedFinanceCorDapp2.signed( keyStoreDir2.path ) + + val alice = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME, additionalCordapps = listOf(signedFinanceCorDapp1))) + + val flow1 = alice.services.startFlow(CashIssueAndPaymentFlow(300.DOLLARS, ref, alice.party, false, mockNet.defaultNotaryIdentity)) + val flow2 = alice.services.startFlow(CashIssueAndPaymentFlow(1000.POUNDS, ref, alice.party, false, mockNet.defaultNotaryIdentity)) + mockNet.runNetwork() + flow1.resultFuture.getOrThrow() + flow2.resultFuture.getOrThrow() + + val alice2 = restartNodeAndDeleteOldCorDapps(mockNet, alice, parameters = InternalMockNodeParameters(additionalCordapps = listOf(signedFinanceCorDapp2))) + + val flow3 = alice2.services.startFlow(CashIssueFlow(700.DOLLARS, ref, mockNet.defaultNotaryIdentity)) + mockNet.runNetwork() + flow3.resultFuture.getOrThrow() + + val vaultStates = alice2.services.vaultService.queryBy(Cash.State::class.java).states + assertEquals(3, vaultStates.size) + + val outputState = Cash.State(1000.POUNDS `issued by` alice2.party.ref(1), alice2.party) + val tx = TransactionBuilder(notary = mockNet.defaultNotaryIdentity, serviceHub = alice2.services).apply { + addInputState(vaultStates[0]) + addInputState(vaultStates[1]) + addInputState(vaultStates[2]) + addOutputState(outputState) + addCommand(Cash.Commands.Move(), listOf(alice2.party.owningKey)) + } + assertFailsWith(IllegalArgumentException::class) { + tx.toWireTransaction(alice2.services) + }.also { + assertEquals("Cannot mix SignatureAttachmentConstraints signed by different parties in the same transaction.", it.message) + } + keyStoreDir1.close() + keyStoreDir2.close() + } } \ No newline at end of file From 4524c6522f1412698f73b2c07feebc7b02be15e4 Mon Sep 17 00:00:00 2001 From: Adel El-Beik Date: Wed, 6 Nov 2024 15:17:23 +0000 Subject: [PATCH 2/2] ENT-12401: Fixed compilation errors and changed alias from 4.12 variant. --- .../src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt | 2 ++ .../net/corda/core/transactions/TransactionBuilder.kt | 2 +- .../kotlin/net/corda/node/ContractWithRotatedKeyTest.kt | 6 ++++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/core-deterministic/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt b/core-deterministic/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt index 8078308f12..288423eb60 100644 --- a/core-deterministic/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt +++ b/core-deterministic/src/main/kotlin/net/corda/core/contracts/RotatedKeys.kt @@ -74,6 +74,8 @@ data class RotatedKeys(val rotatedSigningKeys: List> = emptyLis } } + fun rotateToHash(key: PublicKey) = rotate(key.hash.sha256()) + private fun rotate(key: SecureHash): SecureHash { return rotateMap[key] ?: key } 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 14cbf5db34..f76f43d3e4 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -658,7 +658,7 @@ open class TransactionBuilder( // TODO, we don't currently support mixing signature constraints with different signers. This will change once we introduce third party signers. (constraints.count { it is SignatureAttachmentConstraint } > 1) && - (constraints.filterIsInstance().map { serviceHub?.toVerifyingServiceHub()?.rotatedKeys?.rotateToHash(it.key) ?: it.key}.toSet().size > 1) + (constraints.filterIsInstance().map { serviceHub?.retrieveRotatedKeys()?.rotateToHash(it.key) ?: it.key}.toSet().size > 1) -> throw IllegalArgumentException("Cannot mix SignatureAttachmentConstraints signed by different parties in the same transaction.") diff --git a/node/src/integration-test/kotlin/net/corda/node/ContractWithRotatedKeyTest.kt b/node/src/integration-test/kotlin/net/corda/node/ContractWithRotatedKeyTest.kt index 512d05ac9d..efa592db71 100644 --- a/node/src/integration-test/kotlin/net/corda/node/ContractWithRotatedKeyTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/ContractWithRotatedKeyTest.kt @@ -79,6 +79,7 @@ class ContractWithRotatedKeyTest { val keyStoreDir1 = SelfCleaningDir() val keyStoreDir2 = SelfCleaningDir() + // Note the alias below is different in 4.12 and above and it needs to match the alias used internally val packageOwnerKey1 = keyStoreDir1.path.generateKey(alias="alias1") val packageOwnerKey2 = keyStoreDir2.path.generateKey(alias="alias1") @@ -142,8 +143,9 @@ class ContractWithRotatedKeyTest { val keyStoreDir1 = SelfCleaningDir() val keyStoreDir2 = SelfCleaningDir() - val packageOwnerKey1 = keyStoreDir1.path.generateKey(alias="1-testcordapp-rsa") - val packageOwnerKey2 = keyStoreDir2.path.generateKey(alias="1-testcordapp-rsa") + // Note the alias below is different in 4.12 and above and it needs to match the alias used internally + val packageOwnerKey1 = keyStoreDir1.path.generateKey(alias="alias1") + val packageOwnerKey2 = keyStoreDir2.path.generateKey(alias="alias1") val unsignedFinanceCorDapp1 = cordappWithPackages("net.corda.finance", "migration", "META-INF.services") val unsignedFinanceCorDapp2 = cordappWithPackages("net.corda.finance", "migration", "META-INF.services").copy(versionId = 2)