mirror of
https://github.com/corda/corda.git
synced 2025-02-20 09:26:41 +00:00
CORDA-2280 Automatic propagation of whitelisted to Signature Constraints (#5053)
If a single whitelisted constraint is being used by input states and the version of the cordapp changes + is signed, then the constraint will transition to a signature constraint.
This commit is contained in:
parent
0a38ff084e
commit
6662d205f8
@ -426,20 +426,20 @@ open class TransactionBuilder(
|
||||
*
|
||||
* TODO - once support for third party signing is added, it should be implemented here. ( a constraint with 2 signatures is less restrictive than a constraint with 1 more signature)
|
||||
*/
|
||||
private fun attachmentConstraintsTransition(constraints: Set<AttachmentConstraint>, attachmentToUse: ContractAttachment): AttachmentConstraint = when {
|
||||
private fun attachmentConstraintsTransition(
|
||||
constraints: Set<AttachmentConstraint>,
|
||||
attachmentToUse: ContractAttachment
|
||||
): AttachmentConstraint = when {
|
||||
|
||||
// Sanity check.
|
||||
constraints.isEmpty() -> throw IllegalArgumentException("Cannot transition from no constraints.")
|
||||
|
||||
// When all input states have the same constraint.
|
||||
constraints.size == 1 -> constraints.single()
|
||||
|
||||
// Fail when combining the insecure AlwaysAcceptAttachmentConstraint with something else. The size must be at least 2 at this point.
|
||||
constraints.any { it is AlwaysAcceptAttachmentConstraint } ->
|
||||
// Fail when combining the insecure AlwaysAcceptAttachmentConstraint with something else.
|
||||
constraints.size > 1 && constraints.any { it is AlwaysAcceptAttachmentConstraint } ->
|
||||
throw IllegalArgumentException("Can't mix the AlwaysAcceptAttachmentConstraint with a secure constraint in the same transaction. This can be used to hide insecure transitions.")
|
||||
|
||||
// Multiple states with Hash constraints with different hashes. This should not happen as we checked already.
|
||||
constraints.all { it is HashAttachmentConstraint } ->
|
||||
constraints.size > 1 && constraints.all { it is HashAttachmentConstraint } ->
|
||||
throw IllegalArgumentException("Cannot mix HashConstraints with different hashes in the same transaction.")
|
||||
|
||||
// The HashAttachmentConstraint is the strongest constraint, so it wins when mixed with anything. As long as the actual constraints pass.
|
||||
@ -447,25 +447,31 @@ 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.all { it is SignatureAttachmentConstraint } ->
|
||||
constraints.count { it is SignatureAttachmentConstraint } > 1 ->
|
||||
throw IllegalArgumentException("Cannot mix SignatureAttachmentConstraints signed by different parties in the same transaction.")
|
||||
|
||||
// This ensures a smooth migration from the Whitelist Constraint, given that for the transaction to be valid it still has to pass both constraints.
|
||||
// The transition is possible only when the SignatureConstraint contains ALL signers from the attachment.
|
||||
constraints.any { it is SignatureAttachmentConstraint } && constraints.any { it is WhitelistedByZoneAttachmentConstraint } -> {
|
||||
val signatureConstraint = constraints.mapNotNull { it as? SignatureAttachmentConstraint }.single()
|
||||
// This ensures a smooth migration from a Whitelist Constraint to a Signature Constraint
|
||||
constraints.any { it is WhitelistedByZoneAttachmentConstraint } && attachmentToUse.isSigned -> {
|
||||
val signatureConstraint = constraints.singleOrNull { it is SignatureAttachmentConstraint }
|
||||
// If there were states transitioned already used in the current transaction use that signature constraint, otherwise create a new one.
|
||||
when {
|
||||
attachmentToUse.signerKeys.isEmpty() -> throw IllegalArgumentException("Cannot mix a state with the WhitelistedByZoneAttachmentConstraint and a state with the SignatureAttachmentConstraint, when the latest attachment is not signed. Please contact your Zone operator.")
|
||||
signatureConstraint.key.keys.containsAll(attachmentToUse.signerKeys) -> signatureConstraint
|
||||
else -> throw IllegalArgumentException("Attempting to transition a WhitelistedByZoneAttachmentConstraint state backed by an attachment signed by multiple parties to a weaker SignatureConstraint that does not require all those signatures. Please contact your Zone operator.")
|
||||
signatureConstraint != null -> signatureConstraint
|
||||
else -> makeSignatureAttachmentConstraint(attachmentToUse.signerKeys)
|
||||
}
|
||||
}
|
||||
|
||||
// This condition is hit when the current node has not installed the latest signed version but has already received states that have been migrated
|
||||
constraints.any { it is SignatureAttachmentConstraint } && !attachmentToUse.isSigned ->
|
||||
throw IllegalArgumentException("Attempting to create an illegal transaction. Please install the latest signed version for the $attachmentToUse Cordapp.")
|
||||
|
||||
// When all input states have the same constraint.
|
||||
constraints.size == 1 -> constraints.single()
|
||||
|
||||
else -> throw IllegalArgumentException("Unexpected constraints $constraints.")
|
||||
}
|
||||
|
||||
private fun makeSignatureAttachmentConstraint(attachmentSigners: List<PublicKey>) =
|
||||
SignatureAttachmentConstraint(CompositeKey.Builder().addKeys(attachmentSigners.map { it }).build())
|
||||
SignatureAttachmentConstraint(CompositeKey.Builder().addKeys(attachmentSigners).build())
|
||||
|
||||
/**
|
||||
* This method should only be called for upgradeable contracts.
|
||||
|
@ -13,9 +13,9 @@ CorDapp constraints migration
|
||||
.. note:: Before reading this page, you should be familiar with the key concepts of :doc:`Contract Constraints <api-contract-constraints>`.
|
||||
|
||||
Corda 4 introduces and recommends building signed CorDapps that issue states with signature constraints.
|
||||
Existing on ledger states issued before Corda 4 are not automatically transitioned to new signature constraints when building transactions in Corda 4.
|
||||
This document explains how to modify existing CorDapp flows to explicitly consume and evolve pre Corda 4 states, and outlines a future mechanism
|
||||
where such states will transition automatically (without explicit migration code).
|
||||
When building transactions in Corda 4, existing on ledger states issued before Corda 4 are only automatically transitioned to the new
|
||||
Signature Constraint if they were originally using the CZ Whitelisted Constraint. This document explains how to modify existing CorDapp flows to
|
||||
explicitly consume and evolve pre Corda 4 states.
|
||||
|
||||
Faced with the exercise of upgrading an existing Corda 3.x CorDapp to Corda 4, you need to consider the following:
|
||||
|
||||
@ -129,22 +129,13 @@ Corda 4.0 requires some additional steps to consume and evolve pre-existing on-l
|
||||
The key used for signing will be used to sign all subsequent releases, so it should be stored appropriately. The JAR can be signed by multiple keys owned
|
||||
by different parties and it will be expressed as a ``CompositeKey`` in the ``SignatureAttachmentConstraint`` (See :doc:`api-core-types`).
|
||||
|
||||
2. The new Corda 4 signed CorDapp JAR must be registered with the CZ network operator (as whitelisted in the network parameters which are distributed
|
||||
to all nodes in that CZ). The CZ network operator should check that the JAR is signed and not allow any more versions of it to be whitelisted in the future.
|
||||
From now on the development organisation that signed the JAR is responsible for signing new versions.
|
||||
2. Any flow that builds transactions using this CorDapp will automatically transition states to use the ``SignatureAttachmentConstraint`` if
|
||||
no other constraint is specified. Therefore, there are two ways to alter the existing code.
|
||||
|
||||
The process of CZ network CorDapp whitelisting depends on how the Corda network is configured:
|
||||
* Do not specify a constraint
|
||||
* Explicitly add a Signature Constraint
|
||||
|
||||
- if using a hosted CZ network (such as `The Corda Network <https://docs.corda.net/head/corda-network/index.html>`_ or
|
||||
`UAT Environment <https://docs.corda.net/head/corda-network/UAT.html>`_ ) running an Identity Operator (formerly known as Doorman) and
|
||||
Network Map Service, you should manually send the hashes of the two JARs to the CZ network operator and request these be added using
|
||||
their network parameter update process.
|
||||
|
||||
- if using a local network created using the Network Bootstrapper tool, please follow the instructions in
|
||||
:ref:`Updating the contract whitelist for bootstrapped networks <bootstrapper_updating_whitelisted_contracts>` to can add both CorDapp Contract JAR hashes.
|
||||
|
||||
3. Any flows that build transactions using this CorDapp will have the responsibility of transitioning states to the ``SignatureAttachmentConstraint``.
|
||||
This is done explicitly in the code by setting the constraint of the output states to signers of the latest version of the whitelisted jar:
|
||||
The code below details how to explicitly add a Signature Constraint:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
@ -174,12 +165,6 @@ Corda 4.0 requires some additional steps to consume and evolve pre-existing on-l
|
||||
// Set the Signature constraint on the new state to migrate away from the WhitelistConstraint.
|
||||
.addOutputState(outputState, myContract, new SignatureAttachmentConstraint(ownersKey))
|
||||
|
||||
4. As a node operator you need to add the new signed version of the contracts CorDapp to the ``/cordapps`` folder together with the latest version of the flows jar.
|
||||
3. As a node operator you need to add the new signed version of the contracts CorDapp to the ``/cordapps`` folder together with the latest version of the flows jar.
|
||||
Please also ensure that the original unsigned contracts CorDapp is removed from the ``/cordapps`` folder (this will already be present in the
|
||||
nodes attachments store) to ensure the lookup code in step 3 retrieves the correct signed contract CorDapp JAR.
|
||||
|
||||
Later releases
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
The next version of Corda will provide automatic transition of *CZ whitelisted* constrained states. This means that signed CorDapps running on a Corda 4.x node will
|
||||
automatically propagate any pre-existing on-ledger *CZ whitelisted* constrained states (and generate *signature* constrained outputs).
|
||||
|
@ -2,14 +2,16 @@ package net.corda.contracts
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.core.CordaRuntimeException
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.deleteRecursively
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.packageName
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.transactions.CoreTransaction
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
@ -21,32 +23,41 @@ import net.corda.testMessage.Message
|
||||
import net.corda.testMessage.MessageState
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.driver.DriverDSL
|
||||
import net.corda.testing.driver.NodeParameters
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.node.internal.CustomCordapp
|
||||
import net.corda.testing.node.internal.cordappWithPackages
|
||||
import net.corda.testing.node.internal.internalDriver
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.junit.Assume.assumeFalse
|
||||
import org.junit.Test
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class SignatureConstraintVersioningTests {
|
||||
|
||||
private val base = cordappWithPackages(MessageState::class.packageName, DummyMessageContract::class.packageName).signed()
|
||||
private val baseUnsigned = cordappWithPackages(MessageState::class.packageName, DummyMessageContract::class.packageName)
|
||||
private val base = baseUnsigned.signed()
|
||||
private val oldUnsignedCordapp = baseUnsigned.copy(versionId = 2)
|
||||
private val oldCordapp = base.copy(versionId = 2)
|
||||
private val newCordapp = base.copy(versionId = 3)
|
||||
private val user = User("mark", "dadada", setOf(startFlow<CreateMessage>(), startFlow<ConsumeMessage>(), invokeRpc("vaultQuery")))
|
||||
private val message = Message("Hello world!")
|
||||
private val transformetMessage = Message(message.value + "A")
|
||||
private val transformedMessage = Message(message.value + "A")
|
||||
|
||||
@Test
|
||||
fun `can evolve from lower contract class version to higher one`() {
|
||||
assumeFalse(System.getProperty("os.name").toLowerCase().startsWith("win")) // See NodeStatePersistenceTests.kt.
|
||||
|
||||
val stateAndRef: StateAndRef<MessageState>? = internalDriver(
|
||||
inMemoryDB = false,
|
||||
startNodesInProcess = isQuasarAgentSpecified(),
|
||||
networkParameters = testNetworkParameters(notaries = emptyList(), minimumPlatformVersion = 4)
|
||||
inMemoryDB = false,
|
||||
startNodesInProcess = isQuasarAgentSpecified(),
|
||||
networkParameters = testNetworkParameters(notaries = emptyList(), minimumPlatformVersion = 4)
|
||||
) {
|
||||
val nodeName = {
|
||||
val nodeHandle = startNode(NodeParameters(rpcUsers = listOf(user), additionalCordapps = listOf(oldCordapp))).getOrThrow()
|
||||
@ -59,7 +70,13 @@ class SignatureConstraintVersioningTests {
|
||||
}()
|
||||
val result = {
|
||||
(baseDirectory(nodeName) / "cordapps").deleteRecursively()
|
||||
val nodeHandle = startNode(NodeParameters(providedName = nodeName, rpcUsers = listOf(user), additionalCordapps = listOf(newCordapp))).getOrThrow()
|
||||
val nodeHandle = startNode(
|
||||
NodeParameters(
|
||||
providedName = nodeName,
|
||||
rpcUsers = listOf(user),
|
||||
additionalCordapps = listOf(newCordapp)
|
||||
)
|
||||
).getOrThrow()
|
||||
var result: StateAndRef<MessageState>? = CordaRPCClient(nodeHandle.rpcAddress).start(user.username, user.password).use {
|
||||
val page = it.proxy.vaultQuery(MessageState::class.java)
|
||||
page.states.singleOrNull()
|
||||
@ -77,7 +94,86 @@ class SignatureConstraintVersioningTests {
|
||||
result
|
||||
}
|
||||
assertNotNull(stateAndRef)
|
||||
assertEquals(transformetMessage, stateAndRef!!.state.data.message)
|
||||
assertEquals(transformedMessage, stateAndRef!!.state.data.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `auto migration from WhitelistConstraint to SignatureConstraint`() {
|
||||
val transaction =
|
||||
upgradeCorDappBetweenTransactions(oldUnsignedCordapp, newCordapp, listOf(oldUnsignedCordapp, newCordapp))
|
||||
assertEquals(1, transaction.outputs.size)
|
||||
assertTrue(transaction.outputs.single().constraint is SignatureAttachmentConstraint)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `auto migration from WhitelistConstraint to SignatureConstraint fail for not whitelisted signed JAR`() {
|
||||
assertThatExceptionOfType(CordaRuntimeException::class.java).isThrownBy {
|
||||
upgradeCorDappBetweenTransactions(oldUnsignedCordapp, newCordapp, emptyList())
|
||||
}.withMessageContaining("Selected output constraint: $WhitelistedByZoneAttachmentConstraint not satisfying")
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an issuance transaction on one version of a cordapp
|
||||
* Upgrade the cordapp and create a consuming transaction using it
|
||||
*/
|
||||
private fun upgradeCorDappBetweenTransactions(
|
||||
cordapp: CustomCordapp,
|
||||
newCordapp: CustomCordapp,
|
||||
whiteListedCordapps: List<CustomCordapp>
|
||||
): CoreTransaction {
|
||||
assumeFalse(System.getProperty("os.name").toLowerCase().startsWith("win")) // See NodeStatePersistenceTests.kt.
|
||||
|
||||
val attachmentHashes = whiteListedCordapps.map { Files.newInputStream(it.jarFile).readFully().sha256() }
|
||||
|
||||
return internalDriver(
|
||||
inMemoryDB = false,
|
||||
startNodesInProcess = isQuasarAgentSpecified(),
|
||||
networkParameters = testNetworkParameters(
|
||||
notaries = emptyList(),
|
||||
minimumPlatformVersion = 4, whitelistedContractImplementations = mapOf(TEST_MESSAGE_CONTRACT_PROGRAM_ID to attachmentHashes)
|
||||
)
|
||||
) {
|
||||
// create transaction using first Cordapp
|
||||
val (nodeName, baseDirectory) = createIssuanceTransaction(cordapp)
|
||||
// delete the first cordapp
|
||||
deleteCorDapp(baseDirectory, cordapp)
|
||||
// create transaction using the upgraded cordapp resuing input for transaction
|
||||
createConsumingTransaction(nodeName, newCordapp).coreTransaction
|
||||
}
|
||||
}
|
||||
|
||||
private fun DriverDSL.createIssuanceTransaction(cordapp: CustomCordapp): Pair<CordaX500Name, Path> {
|
||||
val nodeHandle = startNode(NodeParameters(rpcUsers = listOf(user), additionalCordapps = listOf(cordapp))).getOrThrow()
|
||||
val nodeName = nodeHandle.nodeInfo.singleIdentity().name
|
||||
CordaRPCClient(nodeHandle.rpcAddress).start(user.username, user.password).use {
|
||||
it.proxy.startFlow(::CreateMessage, message, defaultNotaryIdentity).returnValue.getOrThrow()
|
||||
}
|
||||
nodeHandle.stop()
|
||||
return Pair(nodeName, nodeHandle.baseDirectory)
|
||||
}
|
||||
|
||||
private fun deleteCorDapp(baseDirectory: Path, cordapp: CustomCordapp) {
|
||||
val cordappPath = baseDirectory.resolve(Paths.get("cordapps")).resolve(cordapp.jarFile.fileName)
|
||||
cordappPath.delete()
|
||||
}
|
||||
|
||||
private fun DriverDSL.createConsumingTransaction(nodeName: CordaX500Name, cordapp: CustomCordapp): SignedTransaction {
|
||||
val nodeHandle = startNode(
|
||||
NodeParameters(
|
||||
providedName = nodeName,
|
||||
rpcUsers = listOf(user),
|
||||
additionalCordapps = listOf(cordapp)
|
||||
)
|
||||
).getOrThrow()
|
||||
val result: StateAndRef<MessageState>? = CordaRPCClient(nodeHandle.rpcAddress).start(user.username, user.password).use {
|
||||
val page = it.proxy.vaultQuery(MessageState::class.java)
|
||||
page.states.singleOrNull()
|
||||
}
|
||||
val transaction = CordaRPCClient(nodeHandle.rpcAddress).start(user.username, user.password).use {
|
||||
it.proxy.startFlow(::ConsumeMessage, result!!, defaultNotaryIdentity).returnValue.getOrThrow()
|
||||
}
|
||||
nodeHandle.stop()
|
||||
return transaction
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,7 +199,8 @@ class ConsumeMessage(private val stateRef: StateAndRef<MessageState>, private va
|
||||
val oldMessageState = stateRef.state.data
|
||||
val messageState = MessageState(Message(oldMessageState.message.value + "A"), ourIdentity, stateRef.state.data.linearId)
|
||||
val txCommand = Command(DummyMessageContract.Commands.Send(), messageState.participants.map { it.owningKey })
|
||||
val txBuilder = TransactionBuilder(notary).withItems(StateAndContract(messageState, TEST_MESSAGE_CONTRACT_PROGRAM_ID), txCommand, stateRef)
|
||||
val txBuilder =
|
||||
TransactionBuilder(notary).withItems(StateAndContract(messageState, TEST_MESSAGE_CONTRACT_PROGRAM_ID), txCommand, stateRef)
|
||||
txBuilder.toWireTransaction(serviceHub).toLedgerTransaction(serviceHub).verify()
|
||||
val signedTx = serviceHub.signInitialTransaction(txBuilder)
|
||||
serviceHub.recordTransactions(signedTx)
|
||||
|
Loading…
x
Reference in New Issue
Block a user