mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
CORDA-2920 Hash to Signature Constraint automatic propagation
Allow Hash Constraints to propagate to Signature Constraints. When propagating, the new jar is added to the transaction instead of the original contract attachment (jar). The following requirements must be met to do so: - System property "net.corda.node.disableHashConstraints" must be set to `true` - The min platform version must be 4 or greater - There must be an input state with a hash constraint - There must be no output states with a hash constraint - The new jar must be signed If these requirements are not met, the original Hash Constraint will be kept and the contract attachment related to it is also used. This transition is done at the start of `handleContract` as it is not the normal path a transition would follow. It is considered a backdoor and should be treated separately from the rest of the attachment and constraint logic. Furthermore, it will only work in private network since all nodes must set the special `disableHashConstraints` flag.
This commit is contained in:
parent
41634d1fda
commit
9bf26c20e0
@ -146,7 +146,7 @@ class TransactionBuilderTest {
|
||||
}, DummyContract.PROGRAM_ID)
|
||||
|
||||
private fun signedAttachment(vararg parties: Party) = ContractAttachment.create(object : AbstractAttachment({ byteArrayOf() }, "test") {
|
||||
override val id: SecureHash get() = throw UnsupportedOperationException()
|
||||
override val id: SecureHash get() = contractAttachmentId
|
||||
|
||||
override val signerKeys: List<PublicKey> get() = parties.map { it.owningKey }
|
||||
}, DummyContract.PROGRAM_ID, signerKeys = parties.map { it.owningKey })
|
||||
|
@ -16,13 +16,10 @@ import net.corda.core.node.services.KeyManagementService
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationFactory
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.Exception
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.ArrayDeque
|
||||
import java.util.UUID
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.component1
|
||||
@ -271,18 +268,19 @@ open class TransactionBuilder(
|
||||
// Filter out all contracts that might have been already used by 'normal' input or output states.
|
||||
val referenceStateGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = referencesWithTransactionState.groupBy { it.contract }
|
||||
val refStateContractAttachments: List<AttachmentId> = referenceStateGroups
|
||||
.filterNot { it.key in allContracts }
|
||||
.map { refStateEntry ->
|
||||
selectAttachmentThatSatisfiesConstraints(true, refStateEntry.key, refStateEntry.value, emptySet(), services)
|
||||
}
|
||||
|
||||
val contractClassNameToInputStateRef: Map<ContractClassName, Set<StateRef>> = inputsWithTransactionState.map { Pair(it.state.contract, it.ref) }
|
||||
.groupBy { it.first }.mapValues { it.value.map { e -> e.second }.toSet() }
|
||||
.filterNot { it.key in allContracts }
|
||||
.map { refStateEntry ->
|
||||
getInstalledContractAttachmentId(
|
||||
refStateEntry.key,
|
||||
refStateEntry.value,
|
||||
services
|
||||
)
|
||||
}
|
||||
|
||||
// For each contract, resolve the AutomaticPlaceholderConstraint, and select the attachment.
|
||||
val contractAttachmentsAndResolvedOutputStates: List<Pair<AttachmentId, List<TransactionState<ContractState>>?>> = allContracts.toSet()
|
||||
.map { ctr ->
|
||||
handleContract(ctr, inputContractGroups[ctr], contractClassNameToInputStateRef[ctr], outputContractGroups[ctr], explicitAttachmentContractsMap[ctr], services)
|
||||
handleContract(ctr, inputContractGroups[ctr], outputContractGroups[ctr], explicitAttachmentContractsMap[ctr], services)
|
||||
}
|
||||
|
||||
val resolvedStates: List<TransactionState<ContractState>> = contractAttachmentsAndResolvedOutputStates.mapNotNull { it.second }
|
||||
@ -316,13 +314,45 @@ open class TransactionBuilder(
|
||||
private fun handleContract(
|
||||
contractClassName: ContractClassName,
|
||||
inputStates: List<TransactionState<ContractState>>?,
|
||||
inputStateRefs: Set<StateRef>?,
|
||||
outputStates: List<TransactionState<ContractState>>?,
|
||||
explicitContractAttachment: AttachmentId?,
|
||||
services: ServicesForResolution
|
||||
): Pair<AttachmentId, List<TransactionState<ContractState>>?> {
|
||||
val inputsAndOutputs = (inputStates ?: emptyList()) + (outputStates ?: emptyList())
|
||||
|
||||
fun selectAttachment() = getInstalledContractAttachmentId(
|
||||
contractClassName,
|
||||
inputsAndOutputs.filterNot { it.constraint in automaticConstraints },
|
||||
services
|
||||
)
|
||||
|
||||
/*
|
||||
This block handles the very specific code path where a [HashAttachmentConstraint] can
|
||||
migrate to a [SignatureAttachmentConstraint]. If all the criteria is met, this function
|
||||
will return early as the rest of the logic is no longer required.
|
||||
|
||||
This can only happen in a private network where all nodes have started with
|
||||
a system parameter that disables the hash constraint check.
|
||||
*/
|
||||
if (canMigrateFromHashToSignatureConstraint(inputStates, outputStates, services)) {
|
||||
val attachmentId = selectAttachment()
|
||||
val attachment = services.attachments.openAttachment(attachmentId)
|
||||
require(attachment != null) { "Contract attachment $attachmentId for $contractClassName is missing." }
|
||||
if ((attachment as ContractAttachment).isSigned && (explicitContractAttachment == null || explicitContractAttachment == attachment.id)) {
|
||||
val signatureConstraint =
|
||||
makeSignatureAttachmentConstraint(attachment.signerKeys)
|
||||
require(signatureConstraint.isSatisfiedBy(attachment)) { "Selected output constraint: $signatureConstraint not satisfying ${attachment.id}" }
|
||||
val resolvedOutputStates = outputStates?.map {
|
||||
if (it.constraint in automaticConstraints) {
|
||||
it.copy(constraint = signatureConstraint)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
return attachment.id to resolvedOutputStates
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if there are any HashConstraints that pin the version of a contract. If there are, check if we trust them.
|
||||
val hashAttachments = inputsAndOutputs
|
||||
.filter { it.constraint is HashAttachmentConstraint }
|
||||
@ -349,13 +379,6 @@ open class TransactionBuilder(
|
||||
// This will contain the hash of the JAR that *has* to be used by this Transaction, because it is explicit. Or null if none.
|
||||
val forcedAttachmentId = explicitContractAttachment ?: hashAttachments.singleOrNull()?.id
|
||||
|
||||
fun selectAttachment() = selectAttachmentThatSatisfiesConstraints(
|
||||
false,
|
||||
contractClassName,
|
||||
inputsAndOutputs.filterNot { it.constraint in automaticConstraints },
|
||||
inputStateRefs,
|
||||
services)
|
||||
|
||||
// This will contain the hash of the JAR that will be used by this Transaction.
|
||||
val selectedAttachmentId = forcedAttachmentId ?: selectAttachment()
|
||||
|
||||
@ -402,6 +425,25 @@ open class TransactionBuilder(
|
||||
return Pair(selectedAttachmentId, resolvedOutputStates)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the current transaction can migrate from a [HashAttachmentConstraint] to a
|
||||
* [SignatureAttachmentConstraint]. This is only possible in very specific scenarios. Most
|
||||
* importantly, [HashAttachmentConstraint.disableHashConstraints] must be set to `true` for
|
||||
* any possibility of transition off of existing [HashAttachmentConstraint]s.
|
||||
*/
|
||||
private fun canMigrateFromHashToSignatureConstraint(
|
||||
inputStates: List<TransactionState<ContractState>>?,
|
||||
outputStates: List<TransactionState<ContractState>>?,
|
||||
services: ServicesForResolution
|
||||
): Boolean {
|
||||
return HashAttachmentConstraint.disableHashConstraints
|
||||
&& services.networkParameters.minimumPlatformVersion >= 4
|
||||
// `disableHashConstraints == true` therefore it does not matter if there are
|
||||
// multiple input states with different hash constraints
|
||||
&& inputStates?.any { it.constraint is HashAttachmentConstraint } == true
|
||||
&& outputStates?.none { it.constraint is HashAttachmentConstraint } == true
|
||||
}
|
||||
|
||||
/**
|
||||
* If there are multiple input states with different constraints then run the constraint intersection logic to determine the resulting output constraint.
|
||||
* For issuing transactions where the attachmentToUse is JarSigned, then default to the SignatureConstraint with all the signatures.
|
||||
@ -412,7 +454,7 @@ open class TransactionBuilder(
|
||||
inputStates: List<TransactionState<ContractState>>?,
|
||||
attachmentToUse: ContractAttachment,
|
||||
services: ServicesForResolution): AttachmentConstraint = when {
|
||||
inputStates != null -> attachmentConstraintsTransition(inputStates.groupBy { it.constraint }.keys, attachmentToUse)
|
||||
inputStates != null -> attachmentConstraintsTransition(inputStates.groupBy { it.constraint }.keys, attachmentToUse, services)
|
||||
attachmentToUse.signerKeys.isNotEmpty() && services.networkParameters.minimumPlatformVersion < 4 -> {
|
||||
log.warnOnce("Signature constraints not available on network requiring a minimum platform version of 4. Current is: ${services.networkParameters.minimumPlatformVersion}.")
|
||||
if (useWhitelistedByZoneAttachmentConstraint(contractClassName, services.networkParameters)) {
|
||||
@ -437,7 +479,8 @@ open class TransactionBuilder(
|
||||
*/
|
||||
private fun attachmentConstraintsTransition(
|
||||
constraints: Set<AttachmentConstraint>,
|
||||
attachmentToUse: ContractAttachment
|
||||
attachmentToUse: ContractAttachment,
|
||||
services: ServicesForResolution
|
||||
): AttachmentConstraint = when {
|
||||
|
||||
// Sanity check.
|
||||
@ -452,7 +495,8 @@ open class TransactionBuilder(
|
||||
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.
|
||||
// TODO - this could change if we decide to introduce a way to gracefully migrate from the Hash Constraint to the Signature Constraint.
|
||||
// Migration from HashAttachmentConstraint to SignatureAttachmentConstraint is handled in [TransactionBuilder.handleContract]
|
||||
// If we have reached this point, then no migration is possible and the existing HashAttachmentConstraint must be used
|
||||
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.
|
||||
@ -460,14 +504,7 @@ open class TransactionBuilder(
|
||||
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
|
||||
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 {
|
||||
signatureConstraint != null -> signatureConstraint
|
||||
else -> makeSignatureAttachmentConstraint(attachmentToUse.signerKeys)
|
||||
}
|
||||
}
|
||||
constraints.any { it is WhitelistedByZoneAttachmentConstraint } && attachmentToUse.isSigned && services.networkParameters.minimumPlatformVersion >= 4 -> transitionToSignatureConstraint(constraints, attachmentToUse)
|
||||
|
||||
// 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 ->
|
||||
@ -479,19 +516,25 @@ open class TransactionBuilder(
|
||||
else -> throw IllegalArgumentException("Unexpected constraints $constraints.")
|
||||
}
|
||||
|
||||
private fun transitionToSignatureConstraint(constraints: Set<AttachmentConstraint>, attachmentToUse: ContractAttachment): SignatureAttachmentConstraint {
|
||||
val signatureConstraint = constraints.singleOrNull { it is SignatureAttachmentConstraint } as? SignatureAttachmentConstraint
|
||||
// If there were states transitioned already used in the current transaction use that signature constraint, otherwise create a new one.
|
||||
return when {
|
||||
signatureConstraint != null -> signatureConstraint
|
||||
else -> makeSignatureAttachmentConstraint(attachmentToUse.signerKeys)
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeSignatureAttachmentConstraint(attachmentSigners: List<PublicKey>) =
|
||||
SignatureAttachmentConstraint(CompositeKey.Builder().addKeys(attachmentSigners).build())
|
||||
|
||||
/**
|
||||
* This method should only be called for upgradeable contracts.
|
||||
*/
|
||||
private fun selectAttachmentThatSatisfiesConstraints(isReference: Boolean, contractClassName: String, states: List<TransactionState<ContractState>>, stateRefs: Set<StateRef>?, services: ServicesForResolution): AttachmentId {
|
||||
val constraints = states.map { it.constraint }
|
||||
require(constraints.none { it in automaticConstraints })
|
||||
require(isReference || constraints.none { it is HashAttachmentConstraint })
|
||||
|
||||
private fun getInstalledContractAttachmentId(
|
||||
contractClassName: String,
|
||||
states: List<TransactionState<ContractState>>,
|
||||
services: ServicesForResolution
|
||||
): AttachmentId {
|
||||
return services.cordappProvider.getContractAttachmentID(contractClassName)
|
||||
?: throw MissingContractAttachments(states, contractClassName)
|
||||
?: throw MissingContractAttachments(states, contractClassName)
|
||||
}
|
||||
|
||||
private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters) = contractClassName in networkParameters.whitelistedContractImplementations.keys
|
||||
|
@ -7,10 +7,12 @@ 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.AbstractParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.CoreTransaction
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
@ -19,8 +21,6 @@ import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.flows.isQuasarAgentSpecified
|
||||
import net.corda.node.services.Permissions.Companion.invokeRpc
|
||||
import net.corda.node.services.Permissions.Companion.startFlow
|
||||
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
|
||||
@ -46,6 +46,7 @@ class SignatureConstraintVersioningTests {
|
||||
private val oldUnsignedCordapp = baseUnsigned.copy(versionId = 2)
|
||||
private val oldCordapp = base.copy(versionId = 2)
|
||||
private val newCordapp = base.copy(versionId = 3)
|
||||
private val newUnsignedCordapp = baseUnsigned.copy(versionId = 3)
|
||||
private val user = User("mark", "dadada", setOf(startFlow<CreateMessage>(), startFlow<ConsumeMessage>(), invokeRpc("vaultQuery")))
|
||||
private val message = Message("Hello world!")
|
||||
private val transformedMessage = Message(message.value + "A")
|
||||
@ -55,9 +56,9 @@ class SignatureConstraintVersioningTests {
|
||||
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()
|
||||
@ -71,18 +72,18 @@ class SignatureConstraintVersioningTests {
|
||||
val result = {
|
||||
(baseDirectory(nodeName) / "cordapps").deleteRecursively()
|
||||
val nodeHandle = startNode(
|
||||
NodeParameters(
|
||||
providedName = nodeName,
|
||||
rpcUsers = listOf(user),
|
||||
additionalCordapps = listOf(newCordapp)
|
||||
)
|
||||
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()
|
||||
}
|
||||
CordaRPCClient(nodeHandle.rpcAddress).start(user.username, user.password).use {
|
||||
it.proxy.startFlow(::ConsumeMessage, result!!, defaultNotaryIdentity).returnValue.getOrThrow()
|
||||
it.proxy.startFlow(::ConsumeMessage, result!!, defaultNotaryIdentity, false, false).returnValue.getOrThrow()
|
||||
}
|
||||
result = CordaRPCClient(nodeHandle.rpcAddress).start(user.username, user.password).use {
|
||||
val page = it.proxy.vaultQuery(MessageState::class.java)
|
||||
@ -100,18 +101,182 @@ class SignatureConstraintVersioningTests {
|
||||
@Test
|
||||
fun `auto migration from WhitelistConstraint to SignatureConstraint`() {
|
||||
assumeFalse(System.getProperty("os.name").toLowerCase().startsWith("win")) // See NodeStatePersistenceTests.kt.
|
||||
val transaction =
|
||||
upgradeCorDappBetweenTransactions(oldUnsignedCordapp, newCordapp, listOf(oldUnsignedCordapp, newCordapp))
|
||||
assertEquals(1, transaction.outputs.size)
|
||||
assertTrue(transaction.outputs.single().constraint is SignatureAttachmentConstraint)
|
||||
val (issuanceTransaction, consumingTransaction) = upgradeCorDappBetweenTransactions(
|
||||
cordapp = oldUnsignedCordapp,
|
||||
newCordapp = newCordapp,
|
||||
whiteListedCordapps = mapOf(
|
||||
TEST_MESSAGE_CONTRACT_PROGRAM_ID to listOf(
|
||||
oldUnsignedCordapp,
|
||||
newCordapp
|
||||
)
|
||||
),
|
||||
systemProperties = emptyMap(),
|
||||
startNodesInProcess = false
|
||||
)
|
||||
assertEquals(1, issuanceTransaction.outputs.size)
|
||||
assertTrue(issuanceTransaction.outputs.single().constraint is WhitelistedByZoneAttachmentConstraint)
|
||||
assertEquals(1, consumingTransaction.outputs.size)
|
||||
assertTrue(consumingTransaction.outputs.single().constraint is SignatureAttachmentConstraint)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `auto migration from WhitelistConstraint to SignatureConstraint fail for not whitelisted signed JAR`() {
|
||||
fun `WhitelistConstraint cannot be migrated to SignatureConstraint if platform version is not 4 or greater`() {
|
||||
assumeFalse(System.getProperty("os.name").toLowerCase().startsWith("win")) // See NodeStatePersistenceTests.kt.
|
||||
val (issuanceTransaction, consumingTransaction) = upgradeCorDappBetweenTransactions(
|
||||
cordapp = oldUnsignedCordapp,
|
||||
newCordapp = newCordapp,
|
||||
whiteListedCordapps = mapOf(
|
||||
TEST_MESSAGE_CONTRACT_PROGRAM_ID to listOf(
|
||||
oldUnsignedCordapp,
|
||||
newCordapp
|
||||
)
|
||||
),
|
||||
systemProperties = emptyMap(),
|
||||
startNodesInProcess = false,
|
||||
minimumPlatformVersion = 3
|
||||
)
|
||||
assertEquals(1, issuanceTransaction.outputs.size)
|
||||
assertTrue(issuanceTransaction.outputs.single().constraint is WhitelistedByZoneAttachmentConstraint)
|
||||
assertEquals(1, consumingTransaction.outputs.size)
|
||||
assertTrue(consumingTransaction.outputs.single().constraint is WhitelistedByZoneAttachmentConstraint)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WhitelistConstraint cannot be migrated to SignatureConstraint if signed JAR is not whitelisted`() {
|
||||
assumeFalse(System.getProperty("os.name").toLowerCase().startsWith("win")) // See NodeStatePersistenceTests.kt.
|
||||
assertThatExceptionOfType(CordaRuntimeException::class.java).isThrownBy {
|
||||
upgradeCorDappBetweenTransactions(oldUnsignedCordapp, newCordapp, emptyList())
|
||||
}.withMessageContaining("Selected output constraint: $WhitelistedByZoneAttachmentConstraint not satisfying")
|
||||
upgradeCorDappBetweenTransactions(
|
||||
cordapp = oldUnsignedCordapp,
|
||||
newCordapp = newCordapp,
|
||||
whiteListedCordapps = mapOf(TEST_MESSAGE_CONTRACT_PROGRAM_ID to emptyList()),
|
||||
systemProperties = emptyMap(),
|
||||
startNodesInProcess = true
|
||||
)
|
||||
}
|
||||
.withMessageContaining("Selected output constraint: $WhitelistedByZoneAttachmentConstraint not satisfying")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `auto migration from WhitelistConstraint to SignatureConstraint will only transition states that do not have a constraint specified`() {
|
||||
assumeFalse(System.getProperty("os.name").toLowerCase().startsWith("win")) // See NodeStatePersistenceTests.kt.
|
||||
val (issuanceTransaction, consumingTransaction) = upgradeCorDappBetweenTransactions(
|
||||
cordapp = oldUnsignedCordapp,
|
||||
newCordapp = newCordapp,
|
||||
whiteListedCordapps = mapOf(
|
||||
TEST_MESSAGE_CONTRACT_PROGRAM_ID to listOf(
|
||||
oldUnsignedCordapp,
|
||||
newCordapp
|
||||
)
|
||||
),
|
||||
systemProperties = emptyMap(),
|
||||
startNodesInProcess = true,
|
||||
specifyExistingConstraint = true,
|
||||
addAnotherAutomaticConstraintState = true
|
||||
)
|
||||
assertEquals(1, issuanceTransaction.outputs.size)
|
||||
assertTrue(issuanceTransaction.outputs.single().constraint is WhitelistedByZoneAttachmentConstraint)
|
||||
assertEquals(2, consumingTransaction.outputs.size)
|
||||
assertTrue(consumingTransaction.outputs[0].constraint is WhitelistedByZoneAttachmentConstraint)
|
||||
assertTrue(consumingTransaction.outputs[1].constraint is SignatureAttachmentConstraint)
|
||||
assertEquals(
|
||||
issuanceTransaction.outputs.single().constraint,
|
||||
consumingTransaction.outputs.first().constraint,
|
||||
"The constraint from the issuance transaction should be the same constraint used in the consuming transaction for the first state"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `auto migration from HashConstraint to SignatureConstraint`() {
|
||||
assumeFalse(System.getProperty("os.name").toLowerCase().startsWith("win")) // See NodeStatePersistenceTests.kt.
|
||||
val (issuanceTransaction, consumingTransaction) = upgradeCorDappBetweenTransactions(
|
||||
cordapp = oldUnsignedCordapp,
|
||||
newCordapp = newCordapp,
|
||||
whiteListedCordapps = emptyMap(),
|
||||
systemProperties = mapOf("net.corda.node.disableHashConstraints" to true.toString()),
|
||||
startNodesInProcess = false
|
||||
)
|
||||
assertEquals(1, issuanceTransaction.outputs.size)
|
||||
assertTrue(issuanceTransaction.outputs.single().constraint is HashAttachmentConstraint)
|
||||
assertEquals(1, consumingTransaction.outputs.size)
|
||||
assertTrue(consumingTransaction.outputs.single().constraint is SignatureAttachmentConstraint)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `HashConstraint cannot be migrated if 'disableHashConstraints' system property is not set to true`() {
|
||||
assumeFalse(System.getProperty("os.name").toLowerCase().startsWith("win")) // See NodeStatePersistenceTests.kt.
|
||||
val (issuanceTransaction, consumingTransaction) = upgradeCorDappBetweenTransactions(
|
||||
cordapp = oldUnsignedCordapp,
|
||||
newCordapp = newCordapp,
|
||||
whiteListedCordapps = emptyMap(),
|
||||
systemProperties = emptyMap(),
|
||||
startNodesInProcess = false
|
||||
)
|
||||
assertEquals(1, issuanceTransaction.outputs.size)
|
||||
assertTrue(issuanceTransaction.outputs.single().constraint is HashAttachmentConstraint)
|
||||
assertEquals(1, consumingTransaction.outputs.size)
|
||||
assertTrue(consumingTransaction.outputs.single().constraint is HashAttachmentConstraint)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `HashConstraint cannot be migrated to SignatureConstraint if new jar is not signed`() {
|
||||
assumeFalse(System.getProperty("os.name").toLowerCase().startsWith("win")) // See NodeStatePersistenceTests.kt.
|
||||
val (issuanceTransaction, consumingTransaction) = upgradeCorDappBetweenTransactions(
|
||||
cordapp = oldUnsignedCordapp,
|
||||
newCordapp = newUnsignedCordapp,
|
||||
whiteListedCordapps = emptyMap(),
|
||||
systemProperties = mapOf("net.corda.node.disableHashConstraints" to true.toString()),
|
||||
startNodesInProcess = false
|
||||
)
|
||||
assertEquals(1, issuanceTransaction.outputs.size)
|
||||
assertTrue(issuanceTransaction.outputs.single().constraint is HashAttachmentConstraint)
|
||||
assertEquals(1, consumingTransaction.outputs.size)
|
||||
assertTrue(consumingTransaction.outputs.single().constraint is HashAttachmentConstraint)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `HashConstraint cannot be migrated to SignatureConstraint if platform version is not 4 or greater`() {
|
||||
assumeFalse(System.getProperty("os.name").toLowerCase().startsWith("win")) // See NodeStatePersistenceTests.kt.
|
||||
val (issuanceTransaction, consumingTransaction) = upgradeCorDappBetweenTransactions(
|
||||
cordapp = oldUnsignedCordapp,
|
||||
newCordapp = newCordapp,
|
||||
whiteListedCordapps = emptyMap(),
|
||||
systemProperties = mapOf("net.corda.node.disableHashConstraints" to true.toString()),
|
||||
startNodesInProcess = false,
|
||||
minimumPlatformVersion = 3
|
||||
)
|
||||
assertEquals(1, issuanceTransaction.outputs.size)
|
||||
assertTrue(issuanceTransaction.outputs.single().constraint is HashAttachmentConstraint)
|
||||
assertEquals(1, consumingTransaction.outputs.size)
|
||||
assertTrue(consumingTransaction.outputs.single().constraint is HashAttachmentConstraint)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `HashConstraint cannot be migrated to SignatureConstraint if a HashConstraint is specified for one state and another uses an AutomaticPlaceholderConstraint`() {
|
||||
assumeFalse(System.getProperty("os.name").toLowerCase().startsWith("win")) // See NodeStatePersistenceTests.kt.
|
||||
val (issuanceTransaction, consumingTransaction) = upgradeCorDappBetweenTransactions(
|
||||
cordapp = oldUnsignedCordapp,
|
||||
newCordapp = newCordapp,
|
||||
whiteListedCordapps = emptyMap(),
|
||||
systemProperties = mapOf("net.corda.node.disableHashConstraints" to true.toString()),
|
||||
startNodesInProcess = false,
|
||||
specifyExistingConstraint = true,
|
||||
addAnotherAutomaticConstraintState = true
|
||||
)
|
||||
assertEquals(1, issuanceTransaction.outputs.size)
|
||||
assertTrue(issuanceTransaction.outputs.single().constraint is HashAttachmentConstraint)
|
||||
assertEquals(2, consumingTransaction.outputs.size)
|
||||
assertTrue(consumingTransaction.outputs[0].constraint is HashAttachmentConstraint)
|
||||
assertTrue(consumingTransaction.outputs[1].constraint is HashAttachmentConstraint)
|
||||
assertEquals(
|
||||
issuanceTransaction.outputs.single().constraint,
|
||||
consumingTransaction.outputs.first().constraint,
|
||||
"The constraint from the issuance transaction should be the same constraint used in the consuming transaction"
|
||||
)
|
||||
assertEquals(
|
||||
consumingTransaction.outputs[0].constraint,
|
||||
consumingTransaction.outputs[1].constraint,
|
||||
"The AutomaticPlaceholderConstraint of the second state should become the same HashConstraint used in other state"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -121,44 +286,73 @@ class SignatureConstraintVersioningTests {
|
||||
private fun upgradeCorDappBetweenTransactions(
|
||||
cordapp: CustomCordapp,
|
||||
newCordapp: CustomCordapp,
|
||||
whiteListedCordapps: List<CustomCordapp>
|
||||
): CoreTransaction {
|
||||
whiteListedCordapps: Map<ContractClassName, List<CustomCordapp>>,
|
||||
systemProperties: Map<String, String>,
|
||||
startNodesInProcess: Boolean,
|
||||
minimumPlatformVersion: Int = 4,
|
||||
specifyExistingConstraint: Boolean = false,
|
||||
addAnotherAutomaticConstraintState: Boolean = false
|
||||
): Pair<CoreTransaction, CoreTransaction> {
|
||||
|
||||
val attachmentHashes = whiteListedCordapps.map { Files.newInputStream(it.jarFile).readFully().sha256() }
|
||||
val whitelistedAttachmentHashes = whiteListedCordapps.mapValues { (_, cordapps) ->
|
||||
cordapps.map {
|
||||
Files.newInputStream(it.jarFile).readFully().sha256()
|
||||
}
|
||||
}
|
||||
|
||||
return internalDriver(
|
||||
inMemoryDB = false,
|
||||
startNodesInProcess = isQuasarAgentSpecified(),
|
||||
startNodesInProcess = startNodesInProcess,
|
||||
networkParameters = testNetworkParameters(
|
||||
notaries = emptyList(),
|
||||
minimumPlatformVersion = 4, whitelistedContractImplementations = mapOf(TEST_MESSAGE_CONTRACT_PROGRAM_ID to attachmentHashes)
|
||||
)
|
||||
minimumPlatformVersion = minimumPlatformVersion,
|
||||
whitelistedContractImplementations = whitelistedAttachmentHashes
|
||||
),
|
||||
systemProperties = systemProperties
|
||||
) {
|
||||
// create transaction using first Cordapp
|
||||
val (nodeName, baseDirectory) = createIssuanceTransaction(cordapp)
|
||||
val (nodeName, baseDirectory, issuanceTransaction) = createIssuanceTransaction(cordapp)
|
||||
// delete the first cordapp
|
||||
deleteCorDapp(baseDirectory, cordapp)
|
||||
// create transaction using the upgraded cordapp resuing input for transaction
|
||||
createConsumingTransaction(nodeName, newCordapp).coreTransaction
|
||||
val consumingTransaction = createConsumingTransaction(
|
||||
nodeName,
|
||||
newCordapp,
|
||||
specifyExistingConstraint,
|
||||
addAnotherAutomaticConstraintState
|
||||
).coreTransaction
|
||||
issuanceTransaction to consumingTransaction
|
||||
}
|
||||
}
|
||||
|
||||
private fun DriverDSL.createIssuanceTransaction(cordapp: CustomCordapp): Pair<CordaX500Name, Path> {
|
||||
val nodeHandle = startNode(NodeParameters(rpcUsers = listOf(user), additionalCordapps = listOf(cordapp))).getOrThrow()
|
||||
private fun DriverDSL.createIssuanceTransaction(cordapp: CustomCordapp): Triple<CordaX500Name, Path, CoreTransaction> {
|
||||
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()
|
||||
val tx = CordaRPCClient(nodeHandle.rpcAddress).start(user.username, user.password).use {
|
||||
it.proxy.startFlow(::CreateMessage, message, defaultNotaryIdentity)
|
||||
.returnValue.getOrThrow().coreTransaction
|
||||
}
|
||||
nodeHandle.stop()
|
||||
return Pair(nodeName, nodeHandle.baseDirectory)
|
||||
return Triple(nodeName, nodeHandle.baseDirectory, tx)
|
||||
}
|
||||
|
||||
private fun deleteCorDapp(baseDirectory: Path, cordapp: CustomCordapp) {
|
||||
val cordappPath = baseDirectory.resolve(Paths.get("cordapps")).resolve(cordapp.jarFile.fileName)
|
||||
val cordappPath =
|
||||
baseDirectory.resolve(Paths.get("cordapps")).resolve(cordapp.jarFile.fileName)
|
||||
cordappPath.delete()
|
||||
}
|
||||
|
||||
private fun DriverDSL.createConsumingTransaction(nodeName: CordaX500Name, cordapp: CustomCordapp): SignedTransaction {
|
||||
private fun DriverDSL.createConsumingTransaction(
|
||||
nodeName: CordaX500Name,
|
||||
cordapp: CustomCordapp,
|
||||
specifyExistingConstraint: Boolean,
|
||||
addAnotherAutomaticConstraintState: Boolean
|
||||
): SignedTransaction {
|
||||
val nodeHandle = startNode(
|
||||
NodeParameters(
|
||||
providedName = nodeName,
|
||||
@ -166,26 +360,43 @@ class SignatureConstraintVersioningTests {
|
||||
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()
|
||||
}
|
||||
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,
|
||||
specifyExistingConstraint,
|
||||
addAnotherAutomaticConstraintState
|
||||
)
|
||||
.returnValue.getOrThrow()
|
||||
}
|
||||
nodeHandle.stop()
|
||||
return transaction
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
class CreateMessage(private val message: Message, private val notary: Party) : FlowLogic<SignedTransaction>() {
|
||||
class CreateMessage(private val message: Message, private val notary: Party) :
|
||||
FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val messageState = MessageState(message = message, by = ourIdentity)
|
||||
val txCommand = Command(DummyMessageContract.Commands.Send(), messageState.participants.map { it.owningKey })
|
||||
val txBuilder = TransactionBuilder(notary).withItems(StateAndContract(messageState, TEST_MESSAGE_CONTRACT_PROGRAM_ID), txCommand)
|
||||
txBuilder.toWireTransaction(serviceHub).toLedgerTransaction(serviceHub).verify()
|
||||
val txCommand = Command(
|
||||
DummyMessageContract.Commands.Send(),
|
||||
messageState.participants.map { it.owningKey })
|
||||
val txBuilder = TransactionBuilder(notary).withItems(
|
||||
StateAndContract(
|
||||
messageState,
|
||||
TEST_MESSAGE_CONTRACT_PROGRAM_ID
|
||||
), txCommand
|
||||
)
|
||||
txBuilder.toLedgerTransaction(serviceHub).verify()
|
||||
val signedTx = serviceHub.signInitialTransaction(txBuilder)
|
||||
serviceHub.recordTransactions(signedTx)
|
||||
return signedTx
|
||||
@ -194,14 +405,41 @@ class CreateMessage(private val message: Message, private val notary: Party) : F
|
||||
|
||||
//TODO merge both flows?
|
||||
@StartableByRPC
|
||||
class ConsumeMessage(private val stateRef: StateAndRef<MessageState>, private val notary: Party) : FlowLogic<SignedTransaction>() {
|
||||
class ConsumeMessage(
|
||||
private val stateRef: StateAndRef<MessageState>,
|
||||
private val notary: Party,
|
||||
private val specifyExistingConstraint: Boolean,
|
||||
private val addAnotherAutomaticConstraintState: Boolean
|
||||
) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
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 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).apply {
|
||||
if (specifyExistingConstraint) {
|
||||
addOutputState(messageState, stateRef.state.constraint)
|
||||
} else {
|
||||
addOutputState(messageState)
|
||||
}
|
||||
if (addAnotherAutomaticConstraintState) {
|
||||
addOutputState(
|
||||
MessageState(
|
||||
Message("Another message"),
|
||||
ourIdentity,
|
||||
UniqueIdentifier()
|
||||
)
|
||||
)
|
||||
}
|
||||
addInputState(stateRef)
|
||||
addCommand(txCommand)
|
||||
}
|
||||
txBuilder.toWireTransaction(serviceHub).toLedgerTransaction(serviceHub).verify()
|
||||
val signedTx = serviceHub.signInitialTransaction(txBuilder)
|
||||
serviceHub.recordTransactions(signedTx)
|
||||
@ -209,6 +447,18 @@ class ConsumeMessage(private val stateRef: StateAndRef<MessageState>, private va
|
||||
}
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
data class Message(val value: String)
|
||||
|
||||
@BelongsToContract(DummyMessageContract::class)
|
||||
data class MessageState(
|
||||
val message: Message,
|
||||
val by: Party,
|
||||
override val linearId: UniqueIdentifier = UniqueIdentifier()
|
||||
) : LinearState {
|
||||
override val participants: List<AbstractParty> = listOf(by)
|
||||
}
|
||||
|
||||
//TODO enrich original MessageContract for new command
|
||||
const val TEST_MESSAGE_CONTRACT_PROGRAM_ID = "net.corda.contracts.DummyMessageContract"
|
||||
|
||||
@ -216,9 +466,7 @@ open class DummyMessageContract : Contract {
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
val command = tx.commands.requireSingleCommand<Commands.Send>()
|
||||
requireThat {
|
||||
// Generic constraints around the IOU transaction.
|
||||
"Only one output state should be created." using (tx.outputs.size == 1)
|
||||
val out = tx.outputsOfType<MessageState>().single()
|
||||
val out = tx.outputsOfType<MessageState>().first()
|
||||
"Message sender must sign." using (command.signers.containsAll(out.participants.map { it.owningKey }))
|
||||
"Message value must not be empty." using (out.message.value.isNotBlank())
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user