Merge pull request #7868 from corda/adel/ENT-12373

ENT-12373: Can now cope with diff input states from diff rotated CorDapps.
This commit is contained in:
Adel El-Beik 2024-11-05 17:34:50 +00:00 committed by GitHub
commit 7c591de607
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 109 additions and 1 deletions

View File

@ -78,6 +78,8 @@ data class RotatedKeys(val rotatedSigningKeys: List<List<SecureHash>> = emptyLis
} }
} }
fun rotateToHash(key: PublicKey) = rotate(key.hash.sha256())
private fun rotate(key: SecureHash): SecureHash { private fun rotate(key: SecureHash): SecureHash {
return rotateMap[key] ?: key return rotateMap[key] ?: key
} }

View File

@ -659,7 +659,9 @@ open class TransactionBuilder(
constraints.any { it is HashAttachmentConstraint } -> constraints.find { it is HashAttachmentConstraint }!! 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. // 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<SignatureAttachmentConstraint>().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.") 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 // This ensures a smooth migration from a Whitelist Constraint to a Signature Constraint
@ -675,6 +677,10 @@ open class TransactionBuilder(
// When all input states have the same constraint. // When all input states have the same constraint.
constraints.size == 1 -> constraints.single() 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<SignatureAttachmentConstraint>().first { it == makeSignatureAttachmentConstraint(attachmentToUse.signerKeys) }
else -> throw IllegalArgumentException("Unexpected constraints $constraints.") else -> throw IllegalArgumentException("Unexpected constraints $constraints.")
} }

View File

@ -2,14 +2,18 @@ package net.corda.node
import net.corda.core.crypto.sha256 import net.corda.core.crypto.sha256
import net.corda.core.internal.hash import net.corda.core.internal.hash
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.finance.DOLLARS import net.corda.finance.DOLLARS
import net.corda.finance.GBP import net.corda.finance.GBP
import net.corda.finance.POUNDS import net.corda.finance.POUNDS
import net.corda.finance.USD import net.corda.finance.USD
import net.corda.finance.contracts.asset.Cash
import net.corda.finance.flows.CashIssueAndPaymentFlow import net.corda.finance.flows.CashIssueAndPaymentFlow
import net.corda.finance.flows.CashIssueFlow
import net.corda.finance.flows.CashPaymentFlow import net.corda.finance.flows.CashPaymentFlow
import net.corda.finance.`issued by`
import net.corda.finance.workflows.getCashBalance import net.corda.finance.workflows.getCashBalance
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.RotatedCorDappSignerKeyConfiguration import net.corda.node.services.config.RotatedCorDappSignerKeyConfiguration
@ -34,6 +38,7 @@ import org.mockito.kotlin.doReturn
import org.mockito.kotlin.whenever import org.mockito.kotlin.whenever
import kotlin.io.path.div import kotlin.io.path.div
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class ContractWithRotatedKeyTest { class ContractWithRotatedKeyTest {
private val ref = OpaqueBytes.of(0x01) private val ref = OpaqueBytes.of(0x01)
@ -132,4 +137,99 @@ class ContractWithRotatedKeyTest {
keyStoreDir1.close() keyStoreDir1.close()
keyStoreDir2.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()
}
} }