Merge pull request #7871 from corda/adel/ENT-12401

ENT-12373: Can now cope with diff input states from diff rotated CorDapps
This commit is contained in:
Adel El-Beik 2024-11-07 11:07:11 +00:00 committed by GitHub
commit 9acce23bce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 113 additions and 1 deletions

View File

@ -74,6 +74,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

@ -75,6 +75,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

@ -657,7 +657,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?.retrieveRotatedKeys()?.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
@ -673,6 +675,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
@ -33,6 +37,7 @@ import org.junit.Test
import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever import com.nhaarman.mockito_kotlin.whenever
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)
@ -74,6 +79,7 @@ class ContractWithRotatedKeyTest {
val keyStoreDir1 = SelfCleaningDir() val keyStoreDir1 = SelfCleaningDir()
val keyStoreDir2 = 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 packageOwnerKey1 = keyStoreDir1.path.generateKey(alias="alias1")
val packageOwnerKey2 = keyStoreDir2.path.generateKey(alias="alias1") val packageOwnerKey2 = keyStoreDir2.path.generateKey(alias="alias1")
@ -131,4 +137,100 @@ 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()
// 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)
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()
}
} }