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:
Dan Newton 2019-04-29 17:37:57 +01:00 committed by Shams Asari
parent 0a38ff084e
commit 6662d205f8
3 changed files with 139 additions and 51 deletions

View File

@ -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.

View File

@ -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).

View File

@ -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)