mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
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:
commit
9acce23bce
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user