mirror of
https://github.com/corda/corda.git
synced 2024-12-20 21:43:14 +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)
|
}, DummyContract.PROGRAM_ID)
|
||||||
|
|
||||||
private fun signedAttachment(vararg parties: Party) = ContractAttachment.create(object : AbstractAttachment({ byteArrayOf() }, "test") {
|
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 }
|
override val signerKeys: List<PublicKey> get() = parties.map { it.owningKey }
|
||||||
}, DummyContract.PROGRAM_ID, signerKeys = 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.SerializationContext
|
||||||
import net.corda.core.serialization.SerializationFactory
|
import net.corda.core.serialization.SerializationFactory
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import java.io.NotSerializableException
|
|
||||||
import java.lang.Exception
|
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.ArrayDeque
|
import java.util.*
|
||||||
import java.util.UUID
|
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
import kotlin.collections.component1
|
import kotlin.collections.component1
|
||||||
@ -273,16 +270,17 @@ open class TransactionBuilder(
|
|||||||
val refStateContractAttachments: List<AttachmentId> = referenceStateGroups
|
val refStateContractAttachments: List<AttachmentId> = referenceStateGroups
|
||||||
.filterNot { it.key in allContracts }
|
.filterNot { it.key in allContracts }
|
||||||
.map { refStateEntry ->
|
.map { refStateEntry ->
|
||||||
selectAttachmentThatSatisfiesConstraints(true, refStateEntry.key, refStateEntry.value, emptySet(), services)
|
getInstalledContractAttachmentId(
|
||||||
|
refStateEntry.key,
|
||||||
|
refStateEntry.value,
|
||||||
|
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() }
|
|
||||||
|
|
||||||
// For each contract, resolve the AutomaticPlaceholderConstraint, and select the attachment.
|
// For each contract, resolve the AutomaticPlaceholderConstraint, and select the attachment.
|
||||||
val contractAttachmentsAndResolvedOutputStates: List<Pair<AttachmentId, List<TransactionState<ContractState>>?>> = allContracts.toSet()
|
val contractAttachmentsAndResolvedOutputStates: List<Pair<AttachmentId, List<TransactionState<ContractState>>?>> = allContracts.toSet()
|
||||||
.map { ctr ->
|
.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 }
|
val resolvedStates: List<TransactionState<ContractState>> = contractAttachmentsAndResolvedOutputStates.mapNotNull { it.second }
|
||||||
@ -316,13 +314,45 @@ open class TransactionBuilder(
|
|||||||
private fun handleContract(
|
private fun handleContract(
|
||||||
contractClassName: ContractClassName,
|
contractClassName: ContractClassName,
|
||||||
inputStates: List<TransactionState<ContractState>>?,
|
inputStates: List<TransactionState<ContractState>>?,
|
||||||
inputStateRefs: Set<StateRef>?,
|
|
||||||
outputStates: List<TransactionState<ContractState>>?,
|
outputStates: List<TransactionState<ContractState>>?,
|
||||||
explicitContractAttachment: AttachmentId?,
|
explicitContractAttachment: AttachmentId?,
|
||||||
services: ServicesForResolution
|
services: ServicesForResolution
|
||||||
): Pair<AttachmentId, List<TransactionState<ContractState>>?> {
|
): Pair<AttachmentId, List<TransactionState<ContractState>>?> {
|
||||||
val inputsAndOutputs = (inputStates ?: emptyList()) + (outputStates ?: emptyList())
|
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.
|
// Determine if there are any HashConstraints that pin the version of a contract. If there are, check if we trust them.
|
||||||
val hashAttachments = inputsAndOutputs
|
val hashAttachments = inputsAndOutputs
|
||||||
.filter { it.constraint is HashAttachmentConstraint }
|
.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.
|
// 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
|
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.
|
// This will contain the hash of the JAR that will be used by this Transaction.
|
||||||
val selectedAttachmentId = forcedAttachmentId ?: selectAttachment()
|
val selectedAttachmentId = forcedAttachmentId ?: selectAttachment()
|
||||||
|
|
||||||
@ -402,6 +425,25 @@ open class TransactionBuilder(
|
|||||||
return Pair(selectedAttachmentId, resolvedOutputStates)
|
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.
|
* 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.
|
* 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>>?,
|
inputStates: List<TransactionState<ContractState>>?,
|
||||||
attachmentToUse: ContractAttachment,
|
attachmentToUse: ContractAttachment,
|
||||||
services: ServicesForResolution): AttachmentConstraint = when {
|
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 -> {
|
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}.")
|
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)) {
|
if (useWhitelistedByZoneAttachmentConstraint(contractClassName, services.networkParameters)) {
|
||||||
@ -437,7 +479,8 @@ open class TransactionBuilder(
|
|||||||
*/
|
*/
|
||||||
private fun attachmentConstraintsTransition(
|
private fun attachmentConstraintsTransition(
|
||||||
constraints: Set<AttachmentConstraint>,
|
constraints: Set<AttachmentConstraint>,
|
||||||
attachmentToUse: ContractAttachment
|
attachmentToUse: ContractAttachment,
|
||||||
|
services: ServicesForResolution
|
||||||
): AttachmentConstraint = when {
|
): AttachmentConstraint = when {
|
||||||
|
|
||||||
// Sanity check.
|
// Sanity check.
|
||||||
@ -452,7 +495,8 @@ open class TransactionBuilder(
|
|||||||
throw IllegalArgumentException("Cannot mix HashConstraints with different hashes in the same transaction.")
|
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.
|
// 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 }!!
|
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.
|
||||||
@ -460,14 +504,7 @@ open class TransactionBuilder(
|
|||||||
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
|
||||||
constraints.any { it is WhitelistedByZoneAttachmentConstraint } && attachmentToUse.isSigned -> {
|
constraints.any { it is WhitelistedByZoneAttachmentConstraint } && attachmentToUse.isSigned && services.networkParameters.minimumPlatformVersion >= 4 -> transitionToSignatureConstraint(constraints, attachmentToUse)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This condition is hit when the current node has not installed the latest signed version but has already received states that have been migrated
|
// 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 ->
|
constraints.any { it is SignatureAttachmentConstraint } && !attachmentToUse.isSigned ->
|
||||||
@ -479,17 +516,23 @@ open class TransactionBuilder(
|
|||||||
else -> throw IllegalArgumentException("Unexpected constraints $constraints.")
|
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>) =
|
private fun makeSignatureAttachmentConstraint(attachmentSigners: List<PublicKey>) =
|
||||||
SignatureAttachmentConstraint(CompositeKey.Builder().addKeys(attachmentSigners).build())
|
SignatureAttachmentConstraint(CompositeKey.Builder().addKeys(attachmentSigners).build())
|
||||||
|
|
||||||
/**
|
private fun getInstalledContractAttachmentId(
|
||||||
* This method should only be called for upgradeable contracts.
|
contractClassName: String,
|
||||||
*/
|
states: List<TransactionState<ContractState>>,
|
||||||
private fun selectAttachmentThatSatisfiesConstraints(isReference: Boolean, contractClassName: String, states: List<TransactionState<ContractState>>, stateRefs: Set<StateRef>?, services: ServicesForResolution): AttachmentId {
|
services: ServicesForResolution
|
||||||
val constraints = states.map { it.constraint }
|
): AttachmentId {
|
||||||
require(constraints.none { it in automaticConstraints })
|
|
||||||
require(isReference || constraints.none { it is HashAttachmentConstraint })
|
|
||||||
|
|
||||||
return services.cordappProvider.getContractAttachmentID(contractClassName)
|
return services.cordappProvider.getContractAttachmentID(contractClassName)
|
||||||
?: throw MissingContractAttachments(states, contractClassName)
|
?: throw MissingContractAttachments(states, contractClassName)
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,12 @@ import net.corda.core.contracts.*
|
|||||||
import net.corda.core.crypto.sha256
|
import net.corda.core.crypto.sha256
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.flows.StartableByRPC
|
import net.corda.core.flows.StartableByRPC
|
||||||
|
import net.corda.core.identity.AbstractParty
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.*
|
import net.corda.core.internal.*
|
||||||
import net.corda.core.messaging.startFlow
|
import net.corda.core.messaging.startFlow
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.transactions.CoreTransaction
|
import net.corda.core.transactions.CoreTransaction
|
||||||
import net.corda.core.transactions.LedgerTransaction
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
import net.corda.core.transactions.SignedTransaction
|
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.flows.isQuasarAgentSpecified
|
||||||
import net.corda.node.services.Permissions.Companion.invokeRpc
|
import net.corda.node.services.Permissions.Companion.invokeRpc
|
||||||
import net.corda.node.services.Permissions.Companion.startFlow
|
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.common.internal.testNetworkParameters
|
||||||
import net.corda.testing.core.singleIdentity
|
import net.corda.testing.core.singleIdentity
|
||||||
import net.corda.testing.driver.DriverDSL
|
import net.corda.testing.driver.DriverDSL
|
||||||
@ -46,6 +46,7 @@ class SignatureConstraintVersioningTests {
|
|||||||
private val oldUnsignedCordapp = baseUnsigned.copy(versionId = 2)
|
private val oldUnsignedCordapp = baseUnsigned.copy(versionId = 2)
|
||||||
private val oldCordapp = base.copy(versionId = 2)
|
private val oldCordapp = base.copy(versionId = 2)
|
||||||
private val newCordapp = base.copy(versionId = 3)
|
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 user = User("mark", "dadada", setOf(startFlow<CreateMessage>(), startFlow<ConsumeMessage>(), invokeRpc("vaultQuery")))
|
||||||
private val message = Message("Hello world!")
|
private val message = Message("Hello world!")
|
||||||
private val transformedMessage = Message(message.value + "A")
|
private val transformedMessage = Message(message.value + "A")
|
||||||
@ -82,7 +83,7 @@ class SignatureConstraintVersioningTests {
|
|||||||
page.states.singleOrNull()
|
page.states.singleOrNull()
|
||||||
}
|
}
|
||||||
CordaRPCClient(nodeHandle.rpcAddress).start(user.username, user.password).use {
|
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 {
|
result = CordaRPCClient(nodeHandle.rpcAddress).start(user.username, user.password).use {
|
||||||
val page = it.proxy.vaultQuery(MessageState::class.java)
|
val page = it.proxy.vaultQuery(MessageState::class.java)
|
||||||
@ -100,18 +101,182 @@ class SignatureConstraintVersioningTests {
|
|||||||
@Test
|
@Test
|
||||||
fun `auto migration from WhitelistConstraint to SignatureConstraint`() {
|
fun `auto migration from WhitelistConstraint to SignatureConstraint`() {
|
||||||
assumeFalse(System.getProperty("os.name").toLowerCase().startsWith("win")) // See NodeStatePersistenceTests.kt.
|
assumeFalse(System.getProperty("os.name").toLowerCase().startsWith("win")) // See NodeStatePersistenceTests.kt.
|
||||||
val transaction =
|
val (issuanceTransaction, consumingTransaction) = upgradeCorDappBetweenTransactions(
|
||||||
upgradeCorDappBetweenTransactions(oldUnsignedCordapp, newCordapp, listOf(oldUnsignedCordapp, newCordapp))
|
cordapp = oldUnsignedCordapp,
|
||||||
assertEquals(1, transaction.outputs.size)
|
newCordapp = newCordapp,
|
||||||
assertTrue(transaction.outputs.single().constraint is SignatureAttachmentConstraint)
|
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
|
@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.
|
assumeFalse(System.getProperty("os.name").toLowerCase().startsWith("win")) // See NodeStatePersistenceTests.kt.
|
||||||
assertThatExceptionOfType(CordaRuntimeException::class.java).isThrownBy {
|
assertThatExceptionOfType(CordaRuntimeException::class.java).isThrownBy {
|
||||||
upgradeCorDappBetweenTransactions(oldUnsignedCordapp, newCordapp, emptyList())
|
upgradeCorDappBetweenTransactions(
|
||||||
}.withMessageContaining("Selected output constraint: $WhitelistedByZoneAttachmentConstraint not satisfying")
|
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(
|
private fun upgradeCorDappBetweenTransactions(
|
||||||
cordapp: CustomCordapp,
|
cordapp: CustomCordapp,
|
||||||
newCordapp: CustomCordapp,
|
newCordapp: CustomCordapp,
|
||||||
whiteListedCordapps: List<CustomCordapp>
|
whiteListedCordapps: Map<ContractClassName, List<CustomCordapp>>,
|
||||||
): CoreTransaction {
|
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(
|
return internalDriver(
|
||||||
inMemoryDB = false,
|
inMemoryDB = false,
|
||||||
startNodesInProcess = isQuasarAgentSpecified(),
|
startNodesInProcess = startNodesInProcess,
|
||||||
networkParameters = testNetworkParameters(
|
networkParameters = testNetworkParameters(
|
||||||
notaries = emptyList(),
|
notaries = emptyList(),
|
||||||
minimumPlatformVersion = 4, whitelistedContractImplementations = mapOf(TEST_MESSAGE_CONTRACT_PROGRAM_ID to attachmentHashes)
|
minimumPlatformVersion = minimumPlatformVersion,
|
||||||
)
|
whitelistedContractImplementations = whitelistedAttachmentHashes
|
||||||
|
),
|
||||||
|
systemProperties = systemProperties
|
||||||
) {
|
) {
|
||||||
// create transaction using first Cordapp
|
// create transaction using first Cordapp
|
||||||
val (nodeName, baseDirectory) = createIssuanceTransaction(cordapp)
|
val (nodeName, baseDirectory, issuanceTransaction) = createIssuanceTransaction(cordapp)
|
||||||
// delete the first cordapp
|
// delete the first cordapp
|
||||||
deleteCorDapp(baseDirectory, cordapp)
|
deleteCorDapp(baseDirectory, cordapp)
|
||||||
// create transaction using the upgraded cordapp resuing input for transaction
|
// 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> {
|
private fun DriverDSL.createIssuanceTransaction(cordapp: CustomCordapp): Triple<CordaX500Name, Path, CoreTransaction> {
|
||||||
val nodeHandle = startNode(NodeParameters(rpcUsers = listOf(user), additionalCordapps = listOf(cordapp))).getOrThrow()
|
val nodeHandle = startNode(
|
||||||
|
NodeParameters(
|
||||||
|
rpcUsers = listOf(user),
|
||||||
|
additionalCordapps = listOf(cordapp)
|
||||||
|
)
|
||||||
|
).getOrThrow()
|
||||||
val nodeName = nodeHandle.nodeInfo.singleIdentity().name
|
val nodeName = nodeHandle.nodeInfo.singleIdentity().name
|
||||||
CordaRPCClient(nodeHandle.rpcAddress).start(user.username, user.password).use {
|
val tx = CordaRPCClient(nodeHandle.rpcAddress).start(user.username, user.password).use {
|
||||||
it.proxy.startFlow(::CreateMessage, message, defaultNotaryIdentity).returnValue.getOrThrow()
|
it.proxy.startFlow(::CreateMessage, message, defaultNotaryIdentity)
|
||||||
|
.returnValue.getOrThrow().coreTransaction
|
||||||
}
|
}
|
||||||
nodeHandle.stop()
|
nodeHandle.stop()
|
||||||
return Pair(nodeName, nodeHandle.baseDirectory)
|
return Triple(nodeName, nodeHandle.baseDirectory, tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteCorDapp(baseDirectory: Path, cordapp: CustomCordapp) {
|
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()
|
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(
|
val nodeHandle = startNode(
|
||||||
NodeParameters(
|
NodeParameters(
|
||||||
providedName = nodeName,
|
providedName = nodeName,
|
||||||
@ -166,12 +360,21 @@ class SignatureConstraintVersioningTests {
|
|||||||
additionalCordapps = listOf(cordapp)
|
additionalCordapps = listOf(cordapp)
|
||||||
)
|
)
|
||||||
).getOrThrow()
|
).getOrThrow()
|
||||||
val result: StateAndRef<MessageState>? = CordaRPCClient(nodeHandle.rpcAddress).start(user.username, user.password).use {
|
val result: StateAndRef<MessageState>? =
|
||||||
|
CordaRPCClient(nodeHandle.rpcAddress).start(user.username, user.password).use {
|
||||||
val page = it.proxy.vaultQuery(MessageState::class.java)
|
val page = it.proxy.vaultQuery(MessageState::class.java)
|
||||||
page.states.singleOrNull()
|
page.states.singleOrNull()
|
||||||
}
|
}
|
||||||
val transaction = CordaRPCClient(nodeHandle.rpcAddress).start(user.username, user.password).use {
|
val transaction =
|
||||||
it.proxy.startFlow(::ConsumeMessage, result!!, defaultNotaryIdentity).returnValue.getOrThrow()
|
CordaRPCClient(nodeHandle.rpcAddress).start(user.username, user.password).use {
|
||||||
|
it.proxy.startFlow(
|
||||||
|
::ConsumeMessage,
|
||||||
|
result!!,
|
||||||
|
defaultNotaryIdentity,
|
||||||
|
specifyExistingConstraint,
|
||||||
|
addAnotherAutomaticConstraintState
|
||||||
|
)
|
||||||
|
.returnValue.getOrThrow()
|
||||||
}
|
}
|
||||||
nodeHandle.stop()
|
nodeHandle.stop()
|
||||||
return transaction
|
return transaction
|
||||||
@ -179,13 +382,21 @@ class SignatureConstraintVersioningTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@StartableByRPC
|
@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
|
@Suspendable
|
||||||
override fun call(): SignedTransaction {
|
override fun call(): SignedTransaction {
|
||||||
val messageState = MessageState(message = message, by = ourIdentity)
|
val messageState = MessageState(message = message, by = ourIdentity)
|
||||||
val txCommand = Command(DummyMessageContract.Commands.Send(), messageState.participants.map { it.owningKey })
|
val txCommand = Command(
|
||||||
val txBuilder = TransactionBuilder(notary).withItems(StateAndContract(messageState, TEST_MESSAGE_CONTRACT_PROGRAM_ID), txCommand)
|
DummyMessageContract.Commands.Send(),
|
||||||
txBuilder.toWireTransaction(serviceHub).toLedgerTransaction(serviceHub).verify()
|
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)
|
val signedTx = serviceHub.signInitialTransaction(txBuilder)
|
||||||
serviceHub.recordTransactions(signedTx)
|
serviceHub.recordTransactions(signedTx)
|
||||||
return signedTx
|
return signedTx
|
||||||
@ -194,14 +405,41 @@ class CreateMessage(private val message: Message, private val notary: Party) : F
|
|||||||
|
|
||||||
//TODO merge both flows?
|
//TODO merge both flows?
|
||||||
@StartableByRPC
|
@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
|
@Suspendable
|
||||||
override fun call(): SignedTransaction {
|
override fun call(): SignedTransaction {
|
||||||
val oldMessageState = stateRef.state.data
|
val oldMessageState = stateRef.state.data
|
||||||
val messageState = MessageState(Message(oldMessageState.message.value + "A"), ourIdentity, stateRef.state.data.linearId)
|
val messageState = MessageState(
|
||||||
val txCommand = Command(DummyMessageContract.Commands.Send(), messageState.participants.map { it.owningKey })
|
Message(oldMessageState.message.value + "A"),
|
||||||
val txBuilder =
|
ourIdentity,
|
||||||
TransactionBuilder(notary).withItems(StateAndContract(messageState, TEST_MESSAGE_CONTRACT_PROGRAM_ID), txCommand, stateRef)
|
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()
|
txBuilder.toWireTransaction(serviceHub).toLedgerTransaction(serviceHub).verify()
|
||||||
val signedTx = serviceHub.signInitialTransaction(txBuilder)
|
val signedTx = serviceHub.signInitialTransaction(txBuilder)
|
||||||
serviceHub.recordTransactions(signedTx)
|
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
|
//TODO enrich original MessageContract for new command
|
||||||
const val TEST_MESSAGE_CONTRACT_PROGRAM_ID = "net.corda.contracts.DummyMessageContract"
|
const val TEST_MESSAGE_CONTRACT_PROGRAM_ID = "net.corda.contracts.DummyMessageContract"
|
||||||
|
|
||||||
@ -216,9 +466,7 @@ open class DummyMessageContract : Contract {
|
|||||||
override fun verify(tx: LedgerTransaction) {
|
override fun verify(tx: LedgerTransaction) {
|
||||||
val command = tx.commands.requireSingleCommand<Commands.Send>()
|
val command = tx.commands.requireSingleCommand<Commands.Send>()
|
||||||
requireThat {
|
requireThat {
|
||||||
// Generic constraints around the IOU transaction.
|
val out = tx.outputsOfType<MessageState>().first()
|
||||||
"Only one output state should be created." using (tx.outputs.size == 1)
|
|
||||||
val out = tx.outputsOfType<MessageState>().single()
|
|
||||||
"Message sender must sign." using (command.signers.containsAll(out.participants.map { it.owningKey }))
|
"Message sender must sign." using (command.signers.containsAll(out.participants.map { it.owningKey }))
|
||||||
"Message value must not be empty." using (out.message.value.isNotBlank())
|
"Message value must not be empty." using (out.message.value.isNotBlank())
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user