Merge pull request from corda/merge-release/os/4.9-release/os/4.10-2024-11-07-402

ENT-12373: Merging forward updates from release/os/4.9 to release/os/4.10 - 2024-11-07
This commit is contained in:
Adel El-Beik 2024-11-07 14:36:39 +00:00 committed by GitHub
commit 48ec90f9b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 113 additions and 1 deletions
core-deterministic/src/main/kotlin/net/corda/core/contracts
core/src/main/kotlin/net/corda/core
node/src/integration-test/kotlin/net/corda/node

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 {
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 {
return rotateMap[key] ?: key
}

View File

@ -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<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.")
// 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<SignatureAttachmentConstraint>().first { it == makeSignatureAttachmentConstraint(attachmentToUse.signerKeys) }
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.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)
@ -74,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")
@ -131,4 +137,100 @@ 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()
// 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()
}
}