mirror of
https://github.com/corda/corda.git
synced 2025-02-01 08:48:09 +00:00
ENT-2222 Constraints propagation
ENT-2222 Fix tests ENT-2222 Fix tests ENT-2222 Add ledger transaction verification logic ENT-2222 Fixed IRS test ENT-2222 Fixed IRS test ENT-2222 Fixed unit test ENT-2222 Better kdocs ENT-2222 Support for reference states ENT-2222 Fix support for reference states ENT-2222 Revert wrong change ENT-2222 Fix Kdoc ENT-2222 Fix Kdoc ENT-2222 Better docs ENT-2222 Address code review comments ENT-2222 Fix test ENT-2222 Fix rebase ENT-2222 Add documentation around constraint propagation ENT-2222 Add tests for contract propagation ENT-2222 Add Signature Constraints propagation - first draft ENT-2222 fix tests ENT-2222 more tests ENT-2222 unseal the TransactionVerificationException ENT-2222 unseal the TransactionVerificationException ENT-2222 more docs ENT-2222 address code review comments ENT-2222 address code review comments ENT-2222 re-implement transition logic ENT-2222 better comments and checks ENT-2222 Fix tests ENT-2222 merge fixes
This commit is contained in:
parent
7902d758bd
commit
f96a59932c
@ -229,6 +229,15 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
|
||||
fun `SignedTransaction (WireTransaction)`() {
|
||||
val attachmentId = SecureHash.randomSHA256()
|
||||
doReturn(attachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID)
|
||||
val attachmentStorage = rigorousMock<AttachmentStorage>()
|
||||
doReturn(attachmentStorage).whenever(services).attachments
|
||||
val attachment = rigorousMock<ContractAttachment>()
|
||||
doReturn(attachment).whenever(attachmentStorage).openAttachment(attachmentId)
|
||||
doReturn(attachmentId).whenever(attachment).id
|
||||
doReturn(emptyList<Party>()).whenever(attachment).signers
|
||||
doReturn(setOf(DummyContract.PROGRAM_ID)).whenever(attachment).allContracts
|
||||
doReturn("app").whenever(attachment).uploader
|
||||
|
||||
val wtx = TransactionBuilder(
|
||||
notary = DUMMY_NOTARY,
|
||||
inputs = mutableListOf(StateRef(SecureHash.randomSHA256(), 1)),
|
||||
|
@ -3,20 +3,80 @@ package net.corda.core.contracts
|
||||
import net.corda.core.DoNotImplement
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint.isSatisfiedBy
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.crypto.keys
|
||||
import net.corda.core.internal.AttachmentWithContext
|
||||
import net.corda.core.internal.isUploaderTrusted
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.warnOnce
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.lang.annotation.Inherited
|
||||
import java.security.PublicKey
|
||||
|
||||
/** Constrain which contract-code-containing attachment can be used with a [ContractState]. */
|
||||
/**
|
||||
* This annotation should only be added to [Contract] classes.
|
||||
* If the annotation is present, then we assume that [Contract.verify] will ensure that the output states have an acceptable constraint.
|
||||
* If the annotation is missing, then the default - secure - constraint propagation logic is enforced by the platform.
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Inherited
|
||||
annotation class NoConstraintPropagation
|
||||
|
||||
/**
|
||||
* Constrain which contract-code-containing attachment can be used with a [Contract].
|
||||
* */
|
||||
@CordaSerializable
|
||||
@DoNotImplement
|
||||
interface AttachmentConstraint {
|
||||
/** Returns whether the given contract attachment can be used with the [ContractState] associated with this constraint object. */
|
||||
fun isSatisfiedBy(attachment: Attachment): Boolean
|
||||
|
||||
/**
|
||||
* This method will be used in conjunction with [NoConstraintPropagation]. It is run during transaction verification when the contract is not annotated with [NoConstraintPropagation].
|
||||
* When constraints propagation is enabled, constraints set on output states need to follow certain rules with regards to constraints of input states.
|
||||
*
|
||||
* Rules:
|
||||
* * It is allowed for output states to inherit the exact same constraint as the input states.
|
||||
* * The [AlwaysAcceptAttachmentConstraint] is not allowed to transition to a different constraint, as that could be used to hide malicious behaviour.
|
||||
* * Nothing can be migrated from the [HashAttachmentConstraint] except a [HashAttachmentConstraint] with the same hash.
|
||||
* * Anything (except the [AlwaysAcceptAttachmentConstraint]) can be transitioned to a [HashAttachmentConstraint].
|
||||
* * You can transition from the [WhitelistedByZoneAttachmentConstraint] to the [SignatureAttachmentConstraint] only if all signers of the JAR are required to sign in the future.
|
||||
*
|
||||
* TODO - SignatureConstraint third party signers.
|
||||
*/
|
||||
fun canBeTransitionedFrom(input: AttachmentConstraint, attachment: ContractAttachment): Boolean {
|
||||
val output = this
|
||||
return when {
|
||||
// These branches should not happen, as this has been already checked.
|
||||
input is AutomaticPlaceholderConstraint || output is AutomaticPlaceholderConstraint -> throw IllegalArgumentException("Illegal constraint: AutomaticPlaceholderConstraint.")
|
||||
input is AutomaticHashConstraint || output is AutomaticHashConstraint -> throw IllegalArgumentException("Illegal constraint: AutomaticHashConstraint.")
|
||||
|
||||
// Transition to the same constraint.
|
||||
input == output -> true
|
||||
|
||||
// You can't transition from the AlwaysAcceptAttachmentConstraint to anything else, as it could hide something illegal.
|
||||
input is AlwaysAcceptAttachmentConstraint && output !is AlwaysAcceptAttachmentConstraint -> false
|
||||
|
||||
// Nothing can be migrated from the HashConstraint except a HashConstraint with the same Hash. (This check is redundant, but added for clarity)
|
||||
// TODO - this might change if we decide to allow migration to the SignatureConstraint.
|
||||
input is HashAttachmentConstraint && output is HashAttachmentConstraint -> input == output
|
||||
input is HashAttachmentConstraint && output !is HashAttachmentConstraint -> false
|
||||
|
||||
// Anything (except the AlwaysAcceptAttachmentConstraint) can be transformed to a HashAttachmentConstraint.
|
||||
input !is HashAttachmentConstraint && output is HashAttachmentConstraint -> true
|
||||
|
||||
// The SignatureAttachmentConstraint allows migration from a Signature constraint with the same key.
|
||||
// TODO - we don't support currently third party signers. When we do, the output key will have to be stronger then the input key.
|
||||
input is SignatureAttachmentConstraint && output is SignatureAttachmentConstraint -> input.key == output.key
|
||||
|
||||
// You can transition from the WhitelistConstraint to the SignatureConstraint only if all signers of the JAR are required to sign in the future.
|
||||
input is WhitelistedByZoneAttachmentConstraint && output is SignatureAttachmentConstraint ->
|
||||
attachment.signers.isNotEmpty() && output.key.keys.containsAll(attachment.signers)
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** An [AttachmentConstraint] where [isSatisfiedBy] always returns true. */
|
||||
@ -48,26 +108,64 @@ data class HashAttachmentConstraint(val attachmentId: SecureHash) : AttachmentCo
|
||||
object WhitelistedByZoneAttachmentConstraint : AttachmentConstraint {
|
||||
override fun isSatisfiedBy(attachment: Attachment): Boolean {
|
||||
return if (attachment is AttachmentWithContext) {
|
||||
val whitelist = attachment.whitelistedContractImplementations ?: throw IllegalStateException("Unable to verify WhitelistedByZoneAttachmentConstraint - whitelist not specified")
|
||||
val whitelist = attachment.whitelistedContractImplementations
|
||||
?: throw IllegalStateException("Unable to verify WhitelistedByZoneAttachmentConstraint - whitelist not specified")
|
||||
attachment.id in (whitelist[attachment.stateContract] ?: emptyList())
|
||||
} else false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This [AttachmentConstraint] is a convenience class that will be automatically resolved to a [HashAttachmentConstraint].
|
||||
* The resolution occurs in [TransactionBuilder.toWireTransaction] and uses the [TransactionState.contract] value
|
||||
* to find a corresponding loaded [Cordapp] that contains such a contract, and then uses that [Cordapp] as the
|
||||
* [Attachment].
|
||||
*
|
||||
* If, for any reason, this class is not automatically resolved the default implementation is to fail, because the
|
||||
* intent of this class is that it should be replaced by a correct [HashAttachmentConstraint] and verify against an
|
||||
* actual [Attachment].
|
||||
*/
|
||||
@KeepForDJVM
|
||||
@Deprecated("The name is no longer valid as multiple constraints were added.", replaceWith = ReplaceWith("AutomaticPlaceholderConstraint"), level = DeprecationLevel.WARNING)
|
||||
object AutomaticHashConstraint : AttachmentConstraint {
|
||||
override fun isSatisfiedBy(attachment: Attachment): Boolean {
|
||||
throw UnsupportedOperationException("Contracts cannot be satisfied by an AutomaticHashConstraint placeholder")
|
||||
throw UnsupportedOperationException("Contracts cannot be satisfied by an AutomaticHashConstraint placeholder.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This [AttachmentConstraint] is a convenience class that acts as a placeholder and will be automatically resolved by the platform when set on an output state.
|
||||
* It is the default constraint of all output states.
|
||||
*
|
||||
* The resolution occurs in [TransactionBuilder.toWireTransaction] and is based on the input states and the attachments.
|
||||
* If the [Contract] was not annotated with [NoConstraintPropagation], then the platform will ensure the correct constraint propagation.
|
||||
*/
|
||||
@KeepForDJVM
|
||||
object AutomaticPlaceholderConstraint : AttachmentConstraint {
|
||||
override fun isSatisfiedBy(attachment: Attachment): Boolean {
|
||||
throw UnsupportedOperationException("Contracts cannot be satisfied by an AutomaticPlaceholderConstraint placeholder.")
|
||||
}
|
||||
}
|
||||
|
||||
private val logger = LoggerFactory.getLogger(AttachmentConstraint::class.java)
|
||||
private val validConstraints = setOf(
|
||||
AlwaysAcceptAttachmentConstraint::class,
|
||||
HashAttachmentConstraint::class,
|
||||
WhitelistedByZoneAttachmentConstraint::class,
|
||||
SignatureAttachmentConstraint::class)
|
||||
|
||||
/**
|
||||
* Fails if the constraint is not of a known type.
|
||||
* Only the Corda core is allowed to implement the [AttachmentConstraint] interface.
|
||||
*/
|
||||
internal fun checkConstraintValidity(state: TransactionState<*>) {
|
||||
require(state.constraint::class in validConstraints) { "Found state ${state.contract} with an illegal constraint: ${state.constraint}" }
|
||||
if (state.constraint is AlwaysAcceptAttachmentConstraint) {
|
||||
logger.warnOnce("Found state ${state.contract} that is constrained by the insecure: AlwaysAcceptAttachmentConstraint.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for the [NoConstraintPropagation] annotation on the contractClassName.
|
||||
* If it's present it means that the automatic secure core behaviour is not applied, and it's up to the contract developer to enforce a secure propagation logic.
|
||||
*/
|
||||
internal fun ContractClassName.contractHasAutomaticConstraintPropagation(classLoader: ClassLoader? = null) =
|
||||
(classLoader ?: NoConstraintPropagation::class.java.classLoader)
|
||||
.loadClass(this).getAnnotation(NoConstraintPropagation::class.java) == null
|
||||
|
||||
fun ContractClassName.warnContractWithoutConstraintPropagation(classLoader: ClassLoader? = null) {
|
||||
if (!this.contractHasAutomaticConstraintPropagation(classLoader)) {
|
||||
logger.warnOnce("Found contract $this with automatic constraint propagation disabled.")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,7 +58,8 @@ data class TransactionState<out T : ContractState> @JvmOverloads constructor(
|
||||
/**
|
||||
* A validator for the contract attachments on the transaction.
|
||||
*/
|
||||
val constraint: AttachmentConstraint = AutomaticHashConstraint) {
|
||||
val constraint: AttachmentConstraint = AutomaticPlaceholderConstraint) {
|
||||
|
||||
private companion object {
|
||||
val logger = loggerFor<TransactionState<*>>()
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ class AttachmentResolutionException(val hash: SecureHash) : FlowException("Attac
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
@CordaSerializable
|
||||
sealed class TransactionVerificationException(val txId: SecureHash, message: String, cause: Throwable?)
|
||||
abstract class TransactionVerificationException(val txId: SecureHash, message: String, cause: Throwable?)
|
||||
: FlowException("$message, transaction: $txId", cause) {
|
||||
|
||||
/**
|
||||
@ -50,6 +50,19 @@ sealed class TransactionVerificationException(val txId: SecureHash, message: Str
|
||||
constructor(txId: SecureHash, contract: Contract, cause: Throwable) : this(txId, contract.javaClass.name, cause)
|
||||
}
|
||||
|
||||
/**
|
||||
* This exception happens when a transaction was not built correctly.
|
||||
* When a contract is not annotated with [NoConstraintPropagation], then the platform ensures that the constraints of output states transition correctly from input states.
|
||||
*
|
||||
* @property txId The transaction.
|
||||
* @property contractClass The fully qualified class name of the failing contract.
|
||||
* @property inputConstraint The constraint of the input state.
|
||||
* @property outputConstraint The constraint of the outputs state.
|
||||
*/
|
||||
@KeepForDJVM
|
||||
class ConstraintPropagationRejection(txId: SecureHash, val contractClass: String, inputConstraint: AttachmentConstraint, outputConstraint: AttachmentConstraint)
|
||||
: TransactionVerificationException(txId, "Contract constraints for $contractClass are not propagated correctly. The outputConstraint: $outputConstraint is not a valid transition from the input constraint: $inputConstraint.", null)
|
||||
|
||||
/**
|
||||
* The transaction attachment that contains the [contractClass] class didn't meet the constraints specified by
|
||||
* the [TransactionState.constraint] object. This usually implies a version mismatch of some kind.
|
||||
|
@ -119,7 +119,7 @@ open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any)
|
||||
/**
|
||||
* This is a wildcard payload to be used by the invoker of the [DataVendingFlow] to allow unlimited access to its vault.
|
||||
*
|
||||
* Todo Fails with a serialization exception if it is not a list. Why?
|
||||
* TODO Fails with a serialization exception if it is not a list. Why?
|
||||
*/
|
||||
@CordaSerializable
|
||||
object RetrieveAnyTransactionPayload : ArrayList<Any>()
|
@ -61,6 +61,9 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
val inputStates: List<ContractState> get() = inputs.map { it.state.data }
|
||||
val referenceStates: List<ContractState> get() = references.map { it.state.data }
|
||||
|
||||
private val inputAndOutputStates = inputs.asSequence().map { it.state } + outputs.asSequence()
|
||||
private val allStates = inputAndOutputStates + references.asSequence().map { it.state }
|
||||
|
||||
/**
|
||||
* Returns the typed input StateAndRef at the specified index
|
||||
* @param index The index into the inputs.
|
||||
@ -75,20 +78,22 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
*/
|
||||
@Throws(TransactionVerificationException::class)
|
||||
fun verify() {
|
||||
val contractAttachmentsByContract: Map<ContractClassName, ContractAttachment> = getUniqueContractAttachmentsByContract()
|
||||
|
||||
// TODO - verify for version downgrade
|
||||
validateStatesAgainstContract()
|
||||
verifyConstraints()
|
||||
verifyConstraintsValidity(contractAttachmentsByContract)
|
||||
verifyConstraints(contractAttachmentsByContract)
|
||||
verifyContracts()
|
||||
}
|
||||
|
||||
private fun allStates() = inputs.asSequence().map { it.state } + outputs.asSequence()
|
||||
|
||||
/**
|
||||
* For all input and output [TransactionState]s, validates that the wrapped [ContractState] matches up with the
|
||||
* wrapped [Contract], as declared by the [BelongsToContract] annotation on the [ContractState]'s class.
|
||||
*
|
||||
* A warning will be written to the log if any mismatch is detected.
|
||||
*/
|
||||
private fun validateStatesAgainstContract() = allStates().forEach(::validateStateAgainstContract)
|
||||
private fun validateStatesAgainstContract() = allStates.forEach(::validateStateAgainstContract)
|
||||
|
||||
private fun validateStateAgainstContract(state: TransactionState<ContractState>) {
|
||||
state.data.requiredContractClassName?.let { requiredContractClassName ->
|
||||
@ -101,20 +106,50 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that all contract constraints are valid for each state before running any contract code
|
||||
* Enforces the validity of the actual constraints.
|
||||
* * Constraints should be one of the valid supported ones.
|
||||
* * Constraints should propagate correctly if not marked otherwise.
|
||||
*/
|
||||
private fun verifyConstraintsValidity(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) {
|
||||
// First check that the constraints are valid.
|
||||
for (state in allStates) {
|
||||
checkConstraintValidity(state)
|
||||
}
|
||||
|
||||
// Group the inputs and outputs by contract, and for each contract verify the constraints propagation logic.
|
||||
// This is not required for reference states as there is nothing to propagate.
|
||||
val inputContractGroups = inputs.groupBy { it.state.contract }
|
||||
val outputContractGroups = outputs.groupBy { it.contract }
|
||||
|
||||
for (contractClassName in (inputContractGroups.keys + outputContractGroups.keys)) {
|
||||
if (contractClassName.contractHasAutomaticConstraintPropagation()) {
|
||||
// Verify that the constraints of output states have at least the same level of restriction as the constraints of the corresponding input states.
|
||||
val inputConstraints = inputContractGroups[contractClassName]?.map { it.state.constraint }?.toSet()
|
||||
val outputConstraints = outputContractGroups[contractClassName]?.map { it.constraint }?.toSet()
|
||||
outputConstraints?.forEach { outputConstraint ->
|
||||
inputConstraints?.forEach { inputConstraint ->
|
||||
if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, contractAttachmentsByContract[contractClassName]!! ))) {
|
||||
throw TransactionVerificationException.ConstraintPropagationRejection(id, contractClassName, inputConstraint, outputConstraint)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
contractClassName.warnContractWithoutConstraintPropagation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that all contract constraints are passing before running any contract code.
|
||||
*
|
||||
* In case the transaction was created on this node then the attachments will contain the hash of the current cordapp jars.
|
||||
* In case this verifies an older transaction or one originated on a different node, then this verifies that the attachments
|
||||
* are valid.
|
||||
* This check is running the [AttachmentConstraint.isSatisfiedBy] method for each corresponding [ContractAttachment].
|
||||
*
|
||||
* @throws TransactionVerificationException if the constraints fail to verify
|
||||
*/
|
||||
private fun verifyConstraints() {
|
||||
val contractAttachmentsByContract = getUniqueContractAttachmentsByContract()
|
||||
|
||||
for (state in allStates()) {
|
||||
val contractAttachment = contractAttachmentsByContract[state.contract] ?:
|
||||
throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
|
||||
private fun verifyConstraints(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) {
|
||||
for (state in allStates) {
|
||||
val contractAttachment = contractAttachmentsByContract[state.contract]
|
||||
?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
|
||||
|
||||
val constraintAttachment = AttachmentWithContext(contractAttachment, state.contract,
|
||||
networkParameters?.whitelistedContractImplementations)
|
||||
@ -152,7 +187,7 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
* Check the transaction is contract-valid by running the verify() for each input and output state contract.
|
||||
* If any contract fails to verify, the whole transaction is considered to be invalid.
|
||||
*/
|
||||
private fun verifyContracts() = allStates().forEach { ts ->
|
||||
private fun verifyContracts() = inputAndOutputStates.forEach { ts ->
|
||||
val contractClass = getContractClass(ts)
|
||||
val contract = createContractInstance(contractClass)
|
||||
|
||||
@ -181,7 +216,6 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
throw TransactionVerificationException.ContractCreationError(id, contractClass.name, e)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Make sure the notary has stayed the same. As we can't tell how inputs and outputs connect, if there
|
||||
* are any inputs or reference inputs, all outputs must have the same notary.
|
||||
|
@ -4,11 +4,12 @@ import co.paralleluniverse.strands.Strand
|
||||
import net.corda.core.CordaInternal
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.cordapp.CordappProvider
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.AttachmentWithContext
|
||||
import net.corda.core.internal.FlowStateMachine
|
||||
import net.corda.core.internal.ensureMinimumPlatformVersion
|
||||
import net.corda.core.internal.isUploaderTrusted
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
@ -97,8 +98,7 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
// DOCEND 1
|
||||
|
||||
/**
|
||||
* Generates a [WireTransaction] from this builder and resolves any [AutomaticHashConstraint] on contracts to
|
||||
* [HashAttachmentConstraint].
|
||||
* Generates a [WireTransaction] from this builder, resolves any [AutomaticPlaceholderConstraint], and selects the attachments to use for this transaction.
|
||||
*
|
||||
* @returns A new [WireTransaction] that will be unaffected by further changes to this [TransactionBuilder].
|
||||
*
|
||||
@ -114,21 +114,11 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
services.ensureMinimumPlatformVersion(4, "Reference states")
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the [AutomaticHashConstraint]s to [HashAttachmentConstraint]s,
|
||||
* [WhitelistedByZoneAttachmentConstraint]s or [SignatureAttachmentConstraint]s based on a global parameter.
|
||||
*
|
||||
* The [AutomaticHashConstraint] allows for less boiler plate when constructing transactions since for the
|
||||
* typical case the named contract will be available when building the transaction. In exceptional cases the
|
||||
* [TransactionStates] must be created with an explicit [AttachmentConstraint]
|
||||
*/
|
||||
val resolvedOutputs = outputs.map { state ->
|
||||
state.withConstraint(when {
|
||||
state.constraint !== AutomaticHashConstraint -> state.constraint
|
||||
useWhitelistedByZoneAttachmentConstraint(state.contract, services.networkParameters) ->
|
||||
WhitelistedByZoneAttachmentConstraint
|
||||
else -> makeAttachmentConstraint(services, state)
|
||||
})
|
||||
val (allContractAttachments: Collection<SecureHash>, resolvedOutputs: List<TransactionState<ContractState>>) = selectContractAttachmentsAndOutputStateConstraints(services, serializationContext)
|
||||
|
||||
// Final sanity check that all states have the correct constraints.
|
||||
for (state in (inputsWithTransactionState + resolvedOutputs)) {
|
||||
checkConstraintValidity(state)
|
||||
}
|
||||
|
||||
return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
|
||||
@ -137,51 +127,250 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
inputStates(),
|
||||
resolvedOutputs,
|
||||
commands,
|
||||
attachments + makeContractAttachments(services.cordappProvider),
|
||||
(allContractAttachments + attachments).toSortedSet().toList(), // Sort the attachments to ensure transaction builds are stable.
|
||||
notary,
|
||||
window,
|
||||
referenceStates
|
||||
),
|
||||
referenceStates),
|
||||
privacySalt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TransactionState<ContractState>.withConstraint(newConstraint: AttachmentConstraint) =
|
||||
if (newConstraint == constraint) this else copy(constraint = newConstraint)
|
||||
/**
|
||||
* This method is responsible for selecting the contract versions to be used for the current transaction and resolve the output state [AutomaticPlaceholderConstraint]s.
|
||||
* The contract attachments are used to create a deterministic Classloader to deserialise the transaction and to run the contract verification.
|
||||
*
|
||||
* The selection logic depends on the Attachment Constraints of the input, output and reference states, also on the explicitly set attachments.
|
||||
* TODO also on the versions of the attachments of the transactions generating the input states. ( after we add versioning)
|
||||
*/
|
||||
private fun selectContractAttachmentsAndOutputStateConstraints(
|
||||
services: ServicesForResolution, serializationContext: SerializationContext?): Pair<Collection<SecureHash>, List<TransactionState<ContractState>>> {
|
||||
|
||||
private fun makeAttachmentConstraint(services: ServicesForResolution, state: TransactionState<ContractState>): AttachmentConstraint {
|
||||
val attachmentId = services.cordappProvider.getContractAttachmentID(state.contract)
|
||||
?: throw MissingContractAttachments(listOf(state))
|
||||
// Determine the explicitly set contract attachments.
|
||||
val explicitAttachmentContracts: List<Pair<ContractClassName, SecureHash>> = this.attachments
|
||||
.map(services.attachments::openAttachment)
|
||||
.mapNotNull { it as? ContractAttachment }
|
||||
.flatMap { attch ->
|
||||
attch.allContracts.map { it to attch.id }
|
||||
}
|
||||
|
||||
val attachmentSigners = services.attachments.openAttachment(attachmentId)?.signers
|
||||
?: throw MissingContractAttachments(listOf(state))
|
||||
// And fail early if there's more than 1 for a contract.
|
||||
require(explicitAttachmentContracts.isEmpty() || explicitAttachmentContracts.groupBy { (ctr, _) -> ctr }.all { (_, groups) -> groups.size == 1 }) { "Multiple attachments set for the same contract." }
|
||||
|
||||
return when {
|
||||
attachmentSigners.isEmpty() -> HashAttachmentConstraint(attachmentId)
|
||||
else -> makeSignatureAttachmentConstraint(attachmentSigners)
|
||||
val explicitAttachmentContractsMap: Map<ContractClassName, SecureHash> = explicitAttachmentContracts.toMap()
|
||||
|
||||
val inputContractGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = inputsWithTransactionState.groupBy { it.contract }
|
||||
val outputContractGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = outputs.groupBy { it.contract }
|
||||
|
||||
val allContracts: Set<ContractClassName> = inputContractGroups.keys + outputContractGroups.keys
|
||||
|
||||
// Handle reference states.
|
||||
// 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.map { it.constraint }, 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], outputContractGroups[ctr], explicitAttachmentContractsMap[ctr], serializationContext, services)
|
||||
}
|
||||
|
||||
val resolvedStates: List<TransactionState<ContractState>> = contractAttachmentsAndResolvedOutputStates.mapNotNull { it.second }.flatten()
|
||||
|
||||
// The output states need to preserve the order in which they were added.
|
||||
val resolvedOutputStatesInTheOriginalOrder: List<TransactionState<ContractState>> = outputStates().map { os -> resolvedStates.find { rs -> rs.data == os.data }!! }
|
||||
|
||||
val attachments: Collection<AttachmentId> = contractAttachmentsAndResolvedOutputStates.map { it.first } + refStateContractAttachments
|
||||
|
||||
return Pair(attachments, resolvedOutputStatesInTheOriginalOrder)
|
||||
}
|
||||
|
||||
private val automaticConstraints = setOf(AutomaticPlaceholderConstraint, AutomaticHashConstraint)
|
||||
|
||||
/**
|
||||
* Selects an attachment and resolves the constraints for the output states with [AutomaticPlaceholderConstraint].
|
||||
*
|
||||
* This is the place where the complex logic of the upgradability of contracts and constraint propagation is handled.
|
||||
*
|
||||
* * For contracts that *are not* annotated with @[NoConstraintPropagation], this will attempt to determine a constraint for the output states
|
||||
* that is a valid transition from all the constraints of the input states.
|
||||
*
|
||||
* * For contracts that *are* annotated with @[NoConstraintPropagation], this enforces setting an explicit output constraint.
|
||||
*
|
||||
* * For states with the [HashAttachmentConstraint], if an attachment with that hash is installed on the current node, then it will be inherited by the output states and selected for the transaction.
|
||||
* Otherwise a [MissingContractAttachments] is thrown.
|
||||
*
|
||||
* * For input states with [WhitelistedByZoneAttachmentConstraint] or a [AlwaysAcceptAttachmentConstraint] implementations, then the currently installed cordapp version is used.
|
||||
*/
|
||||
private fun handleContract(
|
||||
contractClassName: ContractClassName,
|
||||
inputStates: List<TransactionState<ContractState>>?,
|
||||
outputStates: List<TransactionState<ContractState>>?,
|
||||
explicitContractAttachment: AttachmentId?,
|
||||
serializationContext: SerializationContext?,
|
||||
services: ServicesForResolution
|
||||
): Pair<AttachmentId, List<TransactionState<ContractState>>?> {
|
||||
val inputsAndOutputs = (inputStates ?: emptyList()) + (outputStates ?: emptyList())
|
||||
|
||||
// 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 }
|
||||
.map { state ->
|
||||
val attachment = services.attachments.openAttachment((state.constraint as HashAttachmentConstraint).attachmentId)
|
||||
if (attachment == null || attachment !is ContractAttachment || !isUploaderTrusted(attachment.uploader)) {
|
||||
// This should never happen because these are input states that should have been validated already.
|
||||
throw MissingContractAttachments(listOf(state))
|
||||
}
|
||||
attachment
|
||||
}.toSet()
|
||||
|
||||
// Check that states with the HashConstraint don't conflict between themselves or with an explicitly set attachment.
|
||||
require(hashAttachments.size <= 1) {
|
||||
"Transaction was built with $contractClassName states with multiple HashConstraints. This is illegal, because it makes it impossible to validate with a single version of the contract code."
|
||||
}
|
||||
if (explicitContractAttachment != null && hashAttachments.singleOrNull() != null) {
|
||||
require(explicitContractAttachment == (hashAttachments.single() as ContractAttachment).attachment.id) {
|
||||
"An attachment has been explicitly set for contract $contractClassName in the transaction builder which conflicts with the HashConstraint of a state."
|
||||
}
|
||||
}
|
||||
|
||||
// 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.map { it.constraint }.toSet().filterNot { it in automaticConstraints },
|
||||
services)
|
||||
|
||||
// This will contain the hash of the JAR that will be used by this Transaction.
|
||||
val selectedAttachmentId = forcedAttachmentId ?: selectAttachment()
|
||||
|
||||
val attachmentToUse = services.attachments.openAttachment(selectedAttachmentId)?.let { it as ContractAttachment }
|
||||
?: throw IllegalArgumentException("Contract attachment $selectedAttachmentId for $contractClassName is missing.")
|
||||
|
||||
// For Exit transactions (no output states) there is no need to resolve the output constraints.
|
||||
if (outputStates == null) {
|
||||
return Pair(selectedAttachmentId, null)
|
||||
}
|
||||
|
||||
// If there are no automatic constraints, there is nothing to resolve.
|
||||
if (outputStates.none { it.constraint in automaticConstraints }) {
|
||||
return Pair(selectedAttachmentId, outputStates)
|
||||
}
|
||||
|
||||
// The final step is to resolve AutomaticPlaceholderConstraint.
|
||||
val automaticConstraintPropagation = contractClassName.contractHasAutomaticConstraintPropagation(serializationContext?.deserializationClassLoader)
|
||||
|
||||
// When automaticConstraintPropagation is disabled for a contract, output states must an explicit Constraint.
|
||||
require(automaticConstraintPropagation) { "Contract $contractClassName was marked with @NoConstraintPropagation, which means the constraint of the output states has to be set explicitly." }
|
||||
|
||||
// This is the logic to determine the constraint which will replace the AutomaticPlaceholderConstraint.
|
||||
val defaultOutputConstraint = selectAttachmentConstraint(contractClassName, inputStates, attachmentToUse, services)
|
||||
|
||||
// Sanity check that the selected attachment actually passes.
|
||||
val constraintAttachment = AttachmentWithContext(attachmentToUse, contractClassName, services.networkParameters.whitelistedContractImplementations)
|
||||
require(defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) { "Selected output constraint: $defaultOutputConstraint not satisfying $selectedAttachmentId" }
|
||||
|
||||
val resolvedOutputStates = outputStates.map {
|
||||
val outputConstraint = it.constraint
|
||||
if (outputConstraint in automaticConstraints) {
|
||||
it.copy(constraint = defaultOutputConstraint)
|
||||
} else {
|
||||
// If the constraint on the output state is already set, and is not a valid transition or can't be transitioned, then fail early.
|
||||
inputStates?.forEach { input ->
|
||||
require(outputConstraint.canBeTransitionedFrom(input.constraint, attachmentToUse)) { "Output state constraint $outputConstraint cannot be transitions from ${input.constraint}" }
|
||||
}
|
||||
require(outputConstraint.isSatisfiedBy(constraintAttachment)) { "Output state constraint check fails. $outputConstraint" }
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
return Pair(selectedAttachmentId, resolvedOutputStates)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* TODO - in the future this step can actually create a new ContractAttachment by merging 2 signed jars of the same version.
|
||||
*/
|
||||
private fun selectAttachmentConstraint(
|
||||
contractClassName: ContractClassName,
|
||||
inputStates: List<TransactionState<ContractState>>?,
|
||||
attachmentToUse: ContractAttachment,
|
||||
services: ServicesForResolution): AttachmentConstraint = when {
|
||||
inputStates != null -> attachmentConstraintsTransition(inputStates.groupBy { it.constraint }.keys, attachmentToUse)
|
||||
useWhitelistedByZoneAttachmentConstraint(contractClassName, services.networkParameters) -> WhitelistedByZoneAttachmentConstraint
|
||||
attachmentToUse.signers.isNotEmpty() -> makeSignatureAttachmentConstraint(attachmentToUse.signers)
|
||||
else -> HashAttachmentConstraint(attachmentToUse.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a set of [AttachmentConstraint]s, this function implements the rules on how constraints can evolve.
|
||||
*
|
||||
* This should be an exhaustive check, and should mirror [AttachmentConstraint.canBeTransitionedFrom].
|
||||
*
|
||||
* 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 {
|
||||
|
||||
// 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 } ->
|
||||
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 } ->
|
||||
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.
|
||||
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 } ->
|
||||
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()
|
||||
when {
|
||||
attachmentToUse.signers.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.signers) -> 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.")
|
||||
}
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("Unexpected constraints $constraints.")
|
||||
}
|
||||
|
||||
private fun makeSignatureAttachmentConstraint(attachmentSigners: List<PublicKey>) =
|
||||
SignatureAttachmentConstraint(CompositeKey.Builder().addKeys(attachmentSigners.map { it }).build())
|
||||
|
||||
private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters) =
|
||||
contractClassName in networkParameters.whitelistedContractImplementations.keys
|
||||
|
||||
/**
|
||||
* The attachments added to the current transaction contain only the hashes of the current cordapps.
|
||||
* NOT the hashes of the cordapps that were used when the input states were created ( in case they changed in the meantime)
|
||||
* TODO - review this logic
|
||||
* This method should only be called for upgradeable contracts.
|
||||
*
|
||||
* For now we use the currently installed CorDapp version.
|
||||
* TODO - When the SignatureConstraint and contract version logic is in, this will need to query the attachments table and find the latest one that satisfies all constraints.
|
||||
* TODO - select a version of the contract that is no older than the one from the previous transactions.
|
||||
*/
|
||||
private fun makeContractAttachments(cordappProvider: CordappProvider): List<AttachmentId> {
|
||||
// Reference inputs not included as it is not necessary to verify them.
|
||||
return (inputsWithTransactionState + outputs).map { state ->
|
||||
cordappProvider.getContractAttachmentID(state.contract)
|
||||
?: throw MissingContractAttachments(listOf(state))
|
||||
}.distinct()
|
||||
private fun selectAttachmentThatSatisfiesConstraints(isReference: Boolean, contractClassName: String, constraints: List<AttachmentConstraint>, services: ServicesForResolution): AttachmentId {
|
||||
require(constraints.none { it in automaticConstraints })
|
||||
require(isReference || constraints.none { it is HashAttachmentConstraint })
|
||||
return services.cordappProvider.getContractAttachmentID(contractClassName)!!
|
||||
}
|
||||
|
||||
private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters) = contractClassName in networkParameters.whitelistedContractImplementations.keys
|
||||
|
||||
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class)
|
||||
fun toLedgerTransaction(services: ServiceHub) = toWireTransaction(services).toLedgerTransaction(services)
|
||||
|
||||
@ -272,10 +461,8 @@ with @BelongsToContract, or supply an explicit contract parameter to addOutputSt
|
||||
""".trimIndent().replace('\n', ' ')
|
||||
},
|
||||
notary: Party, encumbrance: Int? = null,
|
||||
constraint: AttachmentConstraint = AutomaticHashConstraint
|
||||
): TransactionBuilder {
|
||||
return addOutputState(TransactionState(state, contract, notary, encumbrance, constraint))
|
||||
}
|
||||
constraint: AttachmentConstraint = AutomaticPlaceholderConstraint
|
||||
) = addOutputState(TransactionState(state, contract, notary, encumbrance, constraint))
|
||||
|
||||
/** A default notary must be specified during builder construction to use this method */
|
||||
@JvmOverloads
|
||||
@ -289,7 +476,7 @@ Unable to infer Contract class name because state class ${state::class.java.name
|
||||
with @BelongsToContract, or supply an explicit contract parameter to addOutputState().
|
||||
""".trimIndent().replace('\n', ' ')
|
||||
},
|
||||
constraint: AttachmentConstraint = AutomaticHashConstraint
|
||||
constraint: AttachmentConstraint = AutomaticPlaceholderConstraint
|
||||
) = apply {
|
||||
checkNotNull(notary) {
|
||||
"Need to specify a notary for the state, or set a default one on TransactionBuilder initialisation"
|
||||
|
@ -9,6 +9,7 @@ import net.corda.core.serialization.CordaSerializable
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.Future
|
||||
import kotlin.reflect.KProperty
|
||||
@ -135,12 +136,16 @@ fun <V> Future<V>.getOrThrow(timeout: Duration? = null): V = try {
|
||||
throw e.cause!!
|
||||
}
|
||||
|
||||
private val warnings = mutableSetOf<String>()
|
||||
private const val MAX_SIZE = 100
|
||||
private val warnings = Collections.newSetFromMap(object : LinkedHashMap<String, Boolean>() {
|
||||
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, Boolean>?) = size > MAX_SIZE
|
||||
})
|
||||
|
||||
/**
|
||||
* Utility to help log a warning message only once.
|
||||
* It's not thread safe, as in the worst case the warning will be logged twice.
|
||||
* It implements an ad hoc Fifo cache because there's none available in the standard libraries.
|
||||
*/
|
||||
@Synchronized
|
||||
fun Logger.warnOnce(warning: String) {
|
||||
if (warning !in warnings) {
|
||||
warnings.add(warning)
|
||||
|
@ -1,8 +1,13 @@
|
||||
package net.corda.core
|
||||
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.times
|
||||
import com.nhaarman.mockito_kotlin.verify
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.warnOnce
|
||||
import org.assertj.core.api.Assertions.*
|
||||
import org.junit.Test
|
||||
import org.slf4j.Logger
|
||||
import rx.subjects.PublishSubject
|
||||
import java.util.*
|
||||
import java.util.concurrent.CancellationException
|
||||
@ -58,4 +63,24 @@ class UtilsTest {
|
||||
future.get()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `warnOnce works, but the backing cache grows only to a maximum size`() {
|
||||
val MAX_SIZE = 100
|
||||
|
||||
val logger = mock<Logger>()
|
||||
logger.warnOnce("a")
|
||||
logger.warnOnce("b")
|
||||
logger.warnOnce("b")
|
||||
|
||||
// This should cause the eviction of "a".
|
||||
(1..MAX_SIZE).forEach { logger.warnOnce("$it") }
|
||||
logger.warnOnce("a")
|
||||
|
||||
// "a" should be logged twice because it was evicted.
|
||||
verify(logger, times(2)).warn("a")
|
||||
|
||||
// "b" should be logged only once because there was no eviction.
|
||||
verify(logger, times(1)).warn("b")
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,277 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.MissingContractAttachments
|
||||
import net.corda.finance.POUNDS
|
||||
import net.corda.finance.`issued by`
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.node.services.api.IdentityServiceInternal
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.core.TestIdentity
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.ledger
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ConstraintsPropagationTests {
|
||||
|
||||
private companion object {
|
||||
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
||||
val ALICE = TestIdentity(CordaX500Name("ALICE", "London", "GB"))
|
||||
val ALICE_PARTY get() = ALICE.party
|
||||
val ALICE_PUBKEY get() = ALICE.publicKey
|
||||
val BOB = TestIdentity(CordaX500Name("BOB", "London", "GB"))
|
||||
val BOB_PARTY get() = BOB.party
|
||||
val BOB_PUBKEY get() = BOB.publicKey
|
||||
val noPropagationContractClassName = "net.corda.core.contracts.NoPropagationContract"
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
|
||||
private val ledgerServices = MockServices(
|
||||
cordappPackages = listOf("net.corda.finance.contracts.asset"),
|
||||
initialIdentity = ALICE,
|
||||
identityService = rigorousMock<IdentityServiceInternal>().also {
|
||||
doReturn(ALICE_PARTY).whenever(it).partyFromKey(ALICE_PUBKEY)
|
||||
doReturn(BOB_PARTY).whenever(it).partyFromKey(BOB_PUBKEY)
|
||||
},
|
||||
networkParameters = testNetworkParameters(minimumPlatformVersion = 4)
|
||||
.copy(whitelistedContractImplementations = mapOf(
|
||||
Cash.PROGRAM_ID to listOf(SecureHash.zeroHash, SecureHash.allOnesHash),
|
||||
noPropagationContractClassName to listOf(SecureHash.zeroHash)))
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `Happy path with the HashConstraint`() {
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.allOnesHash)
|
||||
output(Cash.PROGRAM_ID, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.allOnesHash), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Issue())
|
||||
verifies()
|
||||
}
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.allOnesHash)
|
||||
input("c1")
|
||||
output(Cash.PROGRAM_ID, "c2", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.allOnesHash), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Fail early in the TransactionBuilder when attempting to change the hash of the HashConstraint on the spending transaction`() {
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
output(Cash.PROGRAM_ID, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.zeroHash), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Issue())
|
||||
verifies()
|
||||
}
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.allOnesHash)
|
||||
input("c1")
|
||||
output(Cash.PROGRAM_ID, "c2", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.allOnesHash), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Transaction validation fails, when constraints do not propagate correctly`() {
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
output(Cash.PROGRAM_ID, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.zeroHash), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Issue())
|
||||
verifies()
|
||||
}
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
input("c1")
|
||||
output(Cash.PROGRAM_ID, "c2", DUMMY_NOTARY, null, WhitelistedByZoneAttachmentConstraint, Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
failsWith("are not propagated correctly")
|
||||
}
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
input("c1")
|
||||
output(Cash.PROGRAM_ID, "c3", DUMMY_NOTARY, null, SignatureAttachmentConstraint(ALICE_PUBKEY), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
failsWith("are not propagated correctly")
|
||||
}
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
input("c1")
|
||||
output(Cash.PROGRAM_ID, "c4", DUMMY_NOTARY, null, AlwaysAcceptAttachmentConstraint, Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
failsWith("are not propagated correctly")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `When the constraint of the output state is a valid transition from the input state, transaction validation works`() {
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
output(Cash.PROGRAM_ID, "c1", DUMMY_NOTARY, null, WhitelistedByZoneAttachmentConstraint, Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Issue())
|
||||
verifies()
|
||||
}
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
input("c1")
|
||||
output(Cash.PROGRAM_ID, "c2", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.zeroHash), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Switching from the WhitelistConstraint to the Signature Constraint is possible if the attachment satisfies both constraints, and the signature constraint inherits all jar signatures`() {
|
||||
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
output(Cash.PROGRAM_ID, "w1", DUMMY_NOTARY, null, WhitelistedByZoneAttachmentConstraint, Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Issue())
|
||||
verifies()
|
||||
}
|
||||
|
||||
// the attachment is signed
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.allOnesHash, listOf(ALICE_PARTY.owningKey))
|
||||
input("w1")
|
||||
output(Cash.PROGRAM_ID, "w2", DUMMY_NOTARY, null, SignatureAttachmentConstraint(ALICE_PUBKEY), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Switching from the WhitelistConstraint to the Signature Constraint fails if the signature constraint does not inherit all jar signatures`() {
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
output(Cash.PROGRAM_ID, "w1", DUMMY_NOTARY, null, WhitelistedByZoneAttachmentConstraint, Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Issue())
|
||||
verifies()
|
||||
}
|
||||
|
||||
// the attachment is not signed
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
input("w1")
|
||||
output(Cash.PROGRAM_ID, "w2", DUMMY_NOTARY, null, SignatureAttachmentConstraint(ALICE_PUBKEY), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
// Note that it fails after the constraints propagation check, because the attachment is not signed.
|
||||
failsWith("are not propagated correctly")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `On contract annotated with NoConstraintPropagation there is no platform check for propagation, but the transaction builder can't use the AutomaticPlaceholderConstraint`() {
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
transaction {
|
||||
attachment(noPropagationContractClassName, SecureHash.zeroHash)
|
||||
output(noPropagationContractClassName, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.zeroHash), NoPropagationContractState())
|
||||
command(ALICE_PUBKEY, NoPropagationContract.Create())
|
||||
verifies()
|
||||
}
|
||||
transaction {
|
||||
attachment(noPropagationContractClassName, SecureHash.zeroHash)
|
||||
input("c1")
|
||||
output(noPropagationContractClassName, "c2", DUMMY_NOTARY, null, WhitelistedByZoneAttachmentConstraint, NoPropagationContractState())
|
||||
command(ALICE_PUBKEY, NoPropagationContract.Create())
|
||||
verifies()
|
||||
}
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
transaction {
|
||||
attachment(noPropagationContractClassName, SecureHash.zeroHash)
|
||||
input("c1")
|
||||
output(noPropagationContractClassName, "c3", DUMMY_NOTARY, null, AutomaticPlaceholderConstraint, NoPropagationContractState())
|
||||
command(ALICE_PUBKEY, NoPropagationContract.Create())
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Attachment canBeTransitionedFrom behaves as expected`() {
|
||||
|
||||
val attachment = mock<ContractAttachment>()
|
||||
whenever(attachment.signers).thenReturn(listOf(ALICE_PARTY.owningKey))
|
||||
|
||||
// Exhaustive positive check
|
||||
assertTrue(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment))
|
||||
assertTrue(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment))
|
||||
|
||||
assertTrue(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment))
|
||||
assertTrue(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment))
|
||||
|
||||
assertTrue(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment))
|
||||
|
||||
assertTrue(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment))
|
||||
|
||||
// Exhaustive negative check
|
||||
assertFalse(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment))
|
||||
assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment))
|
||||
assertFalse(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment))
|
||||
|
||||
assertFalse(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment))
|
||||
|
||||
assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment))
|
||||
assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment))
|
||||
|
||||
assertFalse(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment))
|
||||
assertFalse(SignatureAttachmentConstraint(BOB_PUBKEY).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment))
|
||||
assertFalse(SignatureAttachmentConstraint(BOB_PUBKEY).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment))
|
||||
|
||||
assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment))
|
||||
assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment))
|
||||
assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment))
|
||||
|
||||
// Fail when encounter a AutomaticPlaceholderConstraint
|
||||
assertFailsWith<IllegalArgumentException> { HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(AutomaticPlaceholderConstraint, attachment) }
|
||||
assertFailsWith<IllegalArgumentException> { AutomaticPlaceholderConstraint.canBeTransitionedFrom(AutomaticPlaceholderConstraint, attachment) }
|
||||
}
|
||||
}
|
||||
|
||||
@BelongsToContract(NoPropagationContract::class)
|
||||
class NoPropagationContractState : ContractState {
|
||||
override val participants: List<AbstractParty>
|
||||
get() = emptyList()
|
||||
}
|
||||
|
||||
@NoConstraintPropagation
|
||||
class NoPropagationContract : Contract {
|
||||
interface Commands : CommandData
|
||||
class Create : Commands
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
//do nothing
|
||||
}
|
||||
}
|
@ -35,11 +35,11 @@ class VaultUpdateTests {
|
||||
private val stateRef3 = StateRef(SecureHash.randomSHA256(), 3)
|
||||
private val stateRef4 = StateRef(SecureHash.randomSHA256(), 4)
|
||||
|
||||
private val stateAndRef0 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY), stateRef0)
|
||||
private val stateAndRef1 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY), stateRef1)
|
||||
private val stateAndRef2 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY), stateRef2)
|
||||
private val stateAndRef3 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY), stateRef3)
|
||||
private val stateAndRef4 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY), stateRef4)
|
||||
private val stateAndRef0 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), stateRef0)
|
||||
private val stateAndRef1 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), stateRef1)
|
||||
private val stateAndRef2 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), stateRef2)
|
||||
private val stateAndRef3 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), stateRef3)
|
||||
private val stateAndRef4 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), stateRef4)
|
||||
|
||||
@Test
|
||||
fun `nothing plus nothing is nothing`() {
|
||||
|
@ -63,7 +63,7 @@ class TransactionSerializationTests {
|
||||
// It refers to a fake TX/state that we don't bother creating here.
|
||||
val depositRef = MINI_CORP.ref(1)
|
||||
val fakeStateRef = generateStateRef()
|
||||
val inputState = StateAndRef(TransactionState(TestCash.State(depositRef, 100.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY), fakeStateRef)
|
||||
val inputState = StateAndRef(TransactionState(TestCash.State(depositRef, 100.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), fakeStateRef )
|
||||
val outputState = TransactionState(TestCash.State(depositRef, 600.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY)
|
||||
val changeState = TransactionState(TestCash.State(depositRef, 400.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY)
|
||||
val megaCorpServices = MockServices(listOf("net.corda.core.serialization"), MEGA_CORP.name, rigorousMock(), MEGA_CORP_KEY)
|
||||
|
@ -73,7 +73,7 @@ class LedgerTransactionQueryTests {
|
||||
)
|
||||
services.recordTransactions(fakeIssueTx)
|
||||
val dummyStateRef = StateRef(fakeIssueTx.id, 0)
|
||||
return StateAndRef(TransactionState(dummyState, DummyContract.PROGRAM_ID, DUMMY_NOTARY, null), dummyStateRef)
|
||||
return StateAndRef(TransactionState(dummyState, DummyContract.PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), dummyStateRef)
|
||||
}
|
||||
|
||||
private fun makeDummyTransaction(): LedgerTransaction {
|
||||
|
@ -187,7 +187,7 @@ class ReferenceStateTests {
|
||||
@Test
|
||||
fun `state ref cannot be a reference input and regular input in the same transaction`() {
|
||||
val state = ExampleState(ALICE_PARTY, "HELLO CORDA")
|
||||
val stateAndRef = StateAndRef(TransactionState(state, CONTRACT_ID, DUMMY_NOTARY), StateRef(SecureHash.zeroHash, 0))
|
||||
val stateAndRef = StateAndRef(TransactionState(state, CONTRACT_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), StateRef(SecureHash.zeroHash, 0))
|
||||
assertFailsWith(IllegalArgumentException::class, "A StateRef cannot be both an input and a reference input in the same transaction.") {
|
||||
@Suppress("DEPRECATION") // To be removed when feature is finalised.
|
||||
TransactionBuilder(notary = DUMMY_NOTARY).addInputState(stateAndRef).addReferenceState(stateAndRef.referenced())
|
||||
|
@ -41,7 +41,15 @@ class TransactionBuilderTest {
|
||||
doReturn(cordappProvider).whenever(services).cordappProvider
|
||||
doReturn(contractAttachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID)
|
||||
doReturn(testNetworkParameters()).whenever(services).networkParameters
|
||||
doReturn(attachments).whenever(services).attachments
|
||||
|
||||
val attachmentStorage = rigorousMock<AttachmentStorage>()
|
||||
doReturn(attachmentStorage).whenever(services).attachments
|
||||
val attachment = rigorousMock<ContractAttachment>()
|
||||
doReturn(attachment).whenever(attachmentStorage).openAttachment(contractAttachmentId)
|
||||
doReturn(contractAttachmentId).whenever(attachment).id
|
||||
doReturn(setOf(DummyContract.PROGRAM_ID)).whenever(attachment).allContracts
|
||||
doReturn("app").whenever(attachment).uploader
|
||||
doReturn(emptyList<Party>()).whenever(attachment).signers
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -104,6 +112,7 @@ class TransactionBuilderTest {
|
||||
assertTrue(expectedConstraint.isSatisfiedBy(signedAttachment))
|
||||
assertFalse(expectedConstraint.isSatisfiedBy(unsignedAttachment))
|
||||
|
||||
doReturn(attachments).whenever(services).attachments
|
||||
doReturn(signedAttachment).whenever(attachments).openAttachment(contractAttachmentId)
|
||||
|
||||
val outputState = TransactionState(data = DummyState(), contract = DummyContract.PROGRAM_ID, notary = notary)
|
||||
@ -113,19 +122,17 @@ class TransactionBuilderTest {
|
||||
val wtx = builder.toWireTransaction(services)
|
||||
|
||||
assertThat(wtx.outputs).containsOnly(outputState.copy(constraint = expectedConstraint))
|
||||
|
||||
}
|
||||
|
||||
|
||||
private val unsignedAttachment = object : AbstractAttachment({ byteArrayOf() }) {
|
||||
private val unsignedAttachment = ContractAttachment(object : AbstractAttachment({ byteArrayOf() }) {
|
||||
override val id: SecureHash get() = throw UnsupportedOperationException()
|
||||
|
||||
override val signers: List<PublicKey> get() = emptyList()
|
||||
}
|
||||
}, DummyContract.PROGRAM_ID)
|
||||
|
||||
private fun signedAttachment(vararg parties: Party) = object : AbstractAttachment({ byteArrayOf() }) {
|
||||
private fun signedAttachment(vararg parties: Party) = ContractAttachment(object : AbstractAttachment({ byteArrayOf() }) {
|
||||
override val id: SecureHash get() = throw UnsupportedOperationException()
|
||||
|
||||
override val signers: List<PublicKey> get() = parties.map { it.owningKey }
|
||||
}
|
||||
}, DummyContract.PROGRAM_ID)
|
||||
}
|
||||
|
@ -19,22 +19,31 @@ isn't allowed unless you're a cash issuer - otherwise you could print money for
|
||||
For a transaction to be valid, the ``verify`` function associated with each state must run successfully. However,
|
||||
for this to be secure, it is not sufficient to specify the ``verify`` function by name as there may exist multiple
|
||||
different implementations with the same method signature and enclosing class. This normally will happen as applications
|
||||
evolve, but could also happen maliciously.
|
||||
evolve, but could also happen maliciously as anyone can create a JAR with a class of that name.
|
||||
|
||||
Contract constraints solve this problem by allowing a contract developer to constrain which ``verify`` functions out of
|
||||
the universe of implementations can be used (i.e. the universe is everything that matches the signature and contract
|
||||
Contract constraints solve this problem by allowing a state creator to constrain which ``verify`` functions out of
|
||||
the universe of implementations can be used (i.e. the universe is everything that matches the class name and contract
|
||||
constraints restrict this universe to a subset). Constraints are satisfied by attachments (JARs). You are not allowed to
|
||||
attach two JARs that both define the same application due to the *no overlap rule*. This rule specifies that two
|
||||
attachment JARs may not provide the same file path. If they do, the transaction is considered invalid. Because each
|
||||
state specifies both a constraint over attachments *and* a Contract class name to use, the specified class must appear
|
||||
in only one attachment.
|
||||
|
||||
So who picks the attachment to use? It is chosen by the creator of the transaction that has to satisfy input constraints.
|
||||
The transaction creator also gets to pick the constraints used by any output states, but the contract logic itself may
|
||||
have opinions about what those constraints are - a typical contract would require that the constraints are propagated,
|
||||
that is, the contract will not just enforce the validity of the next transaction that uses a state, but *all successive
|
||||
transactions as well*. The constraints mechanism creates a balance of power between the creator of data on
|
||||
the ledger and the user who is trying to edit it, which can be very useful when managing upgrades to Corda applications.
|
||||
Recap: A corda transaction transitions input states to output states. Each state is composed of data, the name of the class that verifies the transition(contract), and
|
||||
the contract constraint. The transaction also contains a list of attachments (normal JARs) from where these classes will be loaded. There must be only one JAR containing each contract.
|
||||
The contract constraints are responsible to ensure the attachment JARs are following the rules set by the creators of the input states (in a continuous chain to the issue).
|
||||
This way, we have both valid data and valid code that checks the transition packed into the transaction.
|
||||
|
||||
So who picks the attachment to use? It is chosen by the creator of the transaction but has to satisfy the constraints of the input states.
|
||||
This is because any node doing transaction resolution will actually verify the selected attachment against all constraints,
|
||||
so the transaction will only be valid if it passes those checks.
|
||||
For example, when the input state is constrained by the ``HashAttachmentConstraint``, can only attach the JAR with that hash to the transaction.
|
||||
|
||||
The transaction creator also gets to pick the constraints used by any output states.
|
||||
When building a transaction, the default constraint on output states is ``AutomaticPlaceholderConstraint``, which means that corda will select the appropriate constraint.
|
||||
Unless specified otherwise, attachment constraints will propagate from input to output states. (The rules are described below)
|
||||
Constraint propagation is also enforced during transaction verification, where for normal transactions (not explicit upgrades, or notary changes),
|
||||
the constraints of the output states are required to "inherit" the constraint of the input states. ( See below for details)
|
||||
|
||||
There are two ways of handling upgrades to a smart contract in Corda:
|
||||
|
||||
@ -84,18 +93,19 @@ time effectively stop being a part of the network.
|
||||
signed by a specified identity, via the regular Java ``jarsigner`` tool. This will be the most flexible type
|
||||
and the smoothest to deploy: no restarts or contract upgrade transactions are needed.
|
||||
|
||||
**Defaults.** The default constraint type is either a zone constraint, if the network parameters in effect when the
|
||||
transaction is built contain an entry for that contract class, or a hash constraint if not.
|
||||
**Defaults.** Currently, the default constraint type is either a zone constraint, if the network parameters in effect when the
|
||||
transaction is built contain an entry for that contract class, or a hash constraint if not. Once the Signature Constraints are introduced,
|
||||
the default constraint will be the Signature Constraint if the jar is signed.
|
||||
|
||||
A ``TransactionState`` has a ``constraint`` field that represents that state's attachment constraint. When a party
|
||||
constructs a ``TransactionState``, or adds a state using ``TransactionBuilder.addOutput(ContractState)`` without
|
||||
specifying the constraint parameter, a default value (``AutomaticHashConstraint``) is used. This default will be
|
||||
specifying the constraint parameter, a default value (``AutomaticPlaceholderConstraint``) is used. This default will be
|
||||
automatically resolved to a specific ``HashAttachmentConstraint`` or a ``WhitelistedByZoneAttachmentConstraint``.
|
||||
This automatic resolution occurs when a ``TransactionBuilder`` is converted to a ``WireTransaction``. This reduces
|
||||
the boilerplate that would otherwise be involved.
|
||||
|
||||
Finally, an ``AlwaysAcceptAttachmentConstraint`` can be used which accepts anything, though this is intended for
|
||||
testing only.
|
||||
testing only, and a warning will be shown if used by a contract.
|
||||
|
||||
Please note that the ``AttachmentConstraint`` interface is marked as ``@DoNotImplement``. You are not allowed to write
|
||||
new constraint types. Only the platform may implement this interface. If you tried, other nodes would not understand
|
||||
@ -116,48 +126,38 @@ a flow:
|
||||
TransactionBuilder tx = new TransactionBuilder();
|
||||
|
||||
Party notaryParty = ... // a notary party
|
||||
|
||||
tx.addInputState(...)
|
||||
tx.addInputState(...)
|
||||
|
||||
DummyState contractState = new DummyState();
|
||||
|
||||
SecureHash myAttachmentHash = SecureHash.parse("2b4042aed7e0e39d312c4c477dca1d96ec5a878ddcfd5583251a8367edbd4a5f");
|
||||
TransactionState transactionState = new TransactionState(contractState, DummyContract.Companion.getPROGRAMID(), notaryParty, new AttachmentHashConstraint(myAttachmentHash));
|
||||
|
||||
TransactionState transactionState = new TransactionState(contractState, DummyContract.Companion.getPROGRAMID(), notaryParty, null, HashAttachmentConstraint(myhash));
|
||||
tx.addOutputState(transactionState);
|
||||
WireTransaction wtx = tx.toWireTransaction(serviceHub); // This is where an automatic constraint would be resolved.
|
||||
LedgerTransaction ltx = wtx.toLedgerTransaction(serviceHub);
|
||||
ltx.verify(); // Verifies both the attachment constraints and contracts
|
||||
|
||||
Hard-coding the hash of your app in the code itself can be pretty awkward, so the API also offers the ``AutomaticHashConstraint``.
|
||||
This isn't a real constraint that will appear in a transaction: it acts as a marker to the ``TransactionBuilder`` that
|
||||
you require the hash of the node's installed app which supplies the specified contract to be used. In practice, when using
|
||||
hash constraints, you almost always want "whatever the current code is" and not a hard-coded hash. So this automatic
|
||||
constraint placeholder is useful.
|
||||
|
||||
FinalityFlow
|
||||
------------
|
||||
Issues when using the HashAttachmentConstraint
|
||||
----------------------------------------------
|
||||
|
||||
It's possible to encounter contract constraint issues when notarising transactions with the ``FinalityFlow`` on a network
|
||||
containing multiple versions of the same CorDapp. This will happen when using hash constraints or with zone constraints
|
||||
if the zone whitelist has missing CorDapp versions. If a participating party fails to validate the **notarised** transaction
|
||||
then we have a scenario where the members of the network do not have a consistent view of the ledger.
|
||||
When setting up a new network, it is possible to encounter errors when states are issued with the ``HashAttachmentConstraint``,
|
||||
but not all nodes have that same version of the CorDapp installed locally.
|
||||
|
||||
Therefore, if the finality handler flow (which is run on the counter-party) errors for any reason it will always be sent to
|
||||
the flow hospital. From there it's suspended waiting to be retried on node restart. This gives the node operator the opportunity
|
||||
to recover from those errors, which in the case of contract constraint violations means either updating the CorDapp or
|
||||
adding its hash to the zone whitelist.
|
||||
In this case, flows will fail with a ``ContractConstraintRejection``, and the failed flow will be sent to the flow hospital.
|
||||
From there it's suspended waiting to be retried on node restart.
|
||||
This gives the node operator the opportunity to recover from those errors, which in the case of constraint violations means
|
||||
adding the right cordapp jar to the ``cordapps`` folder.
|
||||
|
||||
.. note:: This is a temporary issue in the current version of Corda, until we implement some missing features which will
|
||||
enable a seamless handling of differences in CorDapp versions.
|
||||
|
||||
CorDapps as attachments
|
||||
-----------------------
|
||||
|
||||
CorDapp JARs (see :doc:`cordapp-overview`) that are installed to the node and contain classes implementing the ``Contract``
|
||||
interface are automatically loaded into the ``AttachmentStorage`` of a node at startup.
|
||||
|
||||
After CorDapps are loaded into the attachment store the node creates a link between contract classes and the attachment
|
||||
that they were loaded from. This makes it possible to find the attachment for any given contract. This is how the
|
||||
automatic resolution of attachments is done by the ``TransactionBuilder`` and how, when verifying the constraints and
|
||||
contracts, attachments are associated with their respective contracts.
|
||||
CorDapp JARs (see :doc:`cordapp-overview`) that contain classes implementing the ``Contract`` interface are automatically
|
||||
loaded into the ``AttachmentStorage`` of a node, and made available as ``ContractAttachments``.
|
||||
They are retrievable by hash using ``AttachmentStorage.openAttachment``.
|
||||
These JARs can either be installed on the node or fetched from the network using the ``FetchAttachmentsFlow``.
|
||||
|
||||
.. note:: The obvious way to write a CorDapp is to put all you states, contracts, flows and support code into a single
|
||||
Java module. This will work but it will effectively publish your entire app onto the ledger. That has two problems:
|
||||
@ -166,6 +166,51 @@ contracts, attachments are associated with their respective contracts.
|
||||
app into multiple modules: one which contains just states, contracts and core data types. And another which contains
|
||||
the rest of the app. See :ref:`cordapp-structure`.
|
||||
|
||||
|
||||
Constraints propagation
|
||||
-----------------------
|
||||
|
||||
As was mentioned above, the TransactionBuilder API gives the CorDapp developer or even malicious node owner the possibility
|
||||
to construct output states with a constraint of his choosing.
|
||||
Also, as listed above, some constraints are more restrictive then others.
|
||||
For example, the ``HashAttachmentConstraint`` is the most restrictive, basically reducing the universe of possible attachments
|
||||
to 1, while the ``AlwaysAcceptAttachmentConstraint`` allows any attachment to be selected.
|
||||
|
||||
For the ledger to remain in a consistent state, the expected behavior is for output state to inherit the constraints of input states.
|
||||
This guarantees that for example, a transaction can't output a state with the ``AlwaysAcceptAttachmentConstraint`` when the
|
||||
corresponding input state was the ``HashAttachmentConstraint``. Translated, this means that if this rule is enforced, it ensures
|
||||
that the output state will be spent under similar conditions as it was created.
|
||||
|
||||
Before version 4, the constraint propagation logic was expected to be enforced in the contract verify code, as it has access to the entire Transaction.
|
||||
|
||||
Starting with version 4 of Corda, the constraint propagation logic has been implemented and enforced directly by the platform,
|
||||
unless disabled using ``@NoConstraintPropagation`` - which reverts to the previous behavior.
|
||||
|
||||
For Contracts that are not annotated with ``@NoConstraintPropagation``, the platform implements a fairly simple constraint transition policy
|
||||
to ensure security and also allow the possibility to transition to the new SignatureAttachmentConstraint.
|
||||
|
||||
During transaction building the ``AutomaticPlaceholderConstraint`` for output states will be resolved and the best contract attachment versions
|
||||
will be selected based on a variety of factors so that the above holds true.
|
||||
If it can't find attachments in storage or there are no possible constraints, the Transaction Builder will fail early.
|
||||
|
||||
For example:
|
||||
- In the simple case, if a ``MyContract`` input state is constrained by the ``HashAttachmentConstraint``, then the constraints of all output states of that type will be resolved
|
||||
to the ``HashAttachmentConstraint`` with the same hash, and the attachment with that hash will be selected.
|
||||
- For upgradeable constraints like the ``WhitelistedByZoneAttachmentConstraint``, the output states will inherit the same,
|
||||
and the selected attachment will be the latest version installed on the node.
|
||||
- A more complex case is when for ``MyContract``, one input state is constrained by the ``HashAttachmentConstraint``, while another
|
||||
state by the ``WhitelistedByZoneAttachmentConstraint``. To respect the rule from above, if the hash of the ``HashAttachmentConstraint``
|
||||
is whitelisted by the network, then the output states will inherit the ``HashAttachmentConstraint``, as it is more restrictive.
|
||||
If the hash was not whitelisted, then the builder will fail as it is unable to select a correct constraint.
|
||||
- The ``SignatureAttachmentConstraint`` is an upgradeable constraint, same as the ``WhitelistedByZoneAttachmentConstraint``.
|
||||
By convention we allow states to transition to the ``SignatureAttachmentConstraint`` from the ``WhitelistedByZoneAttachmentConstraint`` as long as the Signatures
|
||||
from new constraints are all the jarsigners from the whitelisted attachment.
|
||||
|
||||
|
||||
For Contracts that are annotated with ``@NoConstraintPropagation``, the platform requires that the Transaction Builder specifies
|
||||
an actual constraint for the output states (the ``AutomaticPlaceholderConstraint`` can't be used) .
|
||||
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
||||
|
@ -501,7 +501,7 @@ class CashTests {
|
||||
|
||||
private fun makeCash(amount: Amount<Currency>, issuer: AbstractParty, depositRef: Byte = 1) =
|
||||
StateAndRef(
|
||||
TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), ourIdentity), Cash.PROGRAM_ID, dummyNotary.party),
|
||||
TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), ourIdentity), Cash.PROGRAM_ID, dummyNotary.party, constraint = AlwaysAcceptAttachmentConstraint),
|
||||
StateRef(SecureHash.randomSHA256(), Random().nextInt(32))
|
||||
)
|
||||
|
||||
|
@ -314,7 +314,7 @@ class ObligationTests {
|
||||
}
|
||||
|
||||
private inline fun <reified T : ContractState> getStateAndRef(state: T, contractClassName: ContractClassName): StateAndRef<T> {
|
||||
val txState = TransactionState(state, contractClassName, DUMMY_NOTARY)
|
||||
val txState = TransactionState(state, contractClassName, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint)
|
||||
return StateAndRef(txState, StateRef(SecureHash.randomSHA256(), 0))
|
||||
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ class AttachmentsClassLoaderStaticContractTests {
|
||||
|
||||
class AttachmentDummyContract : Contract {
|
||||
companion object {
|
||||
private const val ATTACHMENT_PROGRAM_ID = "net.corda.nodeapi.internal.AttachmentsClassLoaderStaticContractTests\$AttachmentDummyContract"
|
||||
const val ATTACHMENT_PROGRAM_ID = "net.corda.nodeapi.internal.AttachmentsClassLoaderStaticContractTests\$AttachmentDummyContract"
|
||||
}
|
||||
|
||||
data class State(val magicNumber: Int = 0) : ContractState {
|
||||
@ -83,7 +83,14 @@ class AttachmentsClassLoaderStaticContractTests {
|
||||
cordappProviderImpl.start(testNetworkParameters().whitelistedContractImplementations)
|
||||
doReturn(cordappProviderImpl).whenever(it).cordappProvider
|
||||
doReturn(testNetworkParameters()).whenever(it).networkParameters
|
||||
doReturn(attachments).whenever(it).attachments
|
||||
val attachmentStorage = rigorousMock<AttachmentStorage>()
|
||||
doReturn(attachmentStorage).whenever(it).attachments
|
||||
val attachment = rigorousMock<ContractAttachment>()
|
||||
doReturn(attachment).whenever(attachmentStorage).openAttachment(any())
|
||||
doReturn(it.cordappProvider.getContractAttachmentID(AttachmentDummyContract.ATTACHMENT_PROGRAM_ID)).whenever(attachment).id
|
||||
doReturn(setOf(AttachmentDummyContract.ATTACHMENT_PROGRAM_ID)).whenever(attachment).allContracts
|
||||
doReturn("app").whenever(attachment).uploader
|
||||
doReturn(emptyList<Party>()).whenever(attachment).signers
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -81,6 +81,6 @@ class RaftNotaryServiceTests {
|
||||
val builder = DummyContract.generateInitial(Random().nextInt(), notary, nodeHandle.services.myInfo.singleIdentity().ref(0))
|
||||
val stx = nodeHandle.services.signInitialTransaction(builder)
|
||||
nodeHandle.services.recordTransactions(stx)
|
||||
return StateAndRef(builder.outputStates().first(), StateRef(stx.id, 0))
|
||||
return StateAndRef(stx.coreTransaction.outputs.first(), StateRef(stx.id, 0))
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package net.corda.node.internal.cordapp
|
||||
|
||||
import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner
|
||||
import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult
|
||||
import net.corda.core.contracts.warnContractWithoutConstraintPropagation
|
||||
import net.corda.core.cordapp.Cordapp
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
@ -67,7 +68,7 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
*/
|
||||
fun fromJarUrls(scanJars: List<URL>) = JarScanningCordappLoader(scanJars.map { it.restricted() })
|
||||
|
||||
private fun URL.restricted(rootPackageName: String? = null) = RestrictedURL(this, rootPackageName)
|
||||
private fun URL.restricted(rootPackageName: String? = null) = RestrictedURL(this, rootPackageName)
|
||||
|
||||
private fun jarUrlsInDirectory(directory: Path): List<URL> {
|
||||
|
||||
@ -169,7 +170,11 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
}
|
||||
|
||||
private fun findContractClassNames(scanResult: RestrictedScanResult): List<String> {
|
||||
return coreContractClasses.flatMap { scanResult.getNamesOfClassesImplementing(it) }.distinct()
|
||||
val contractClasses = coreContractClasses.flatMap { scanResult.getNamesOfClassesImplementing(it) }.distinct()
|
||||
for (contractClass in contractClasses) {
|
||||
contractClass.warnContractWithoutConstraintPropagation(appClassLoader)
|
||||
}
|
||||
return contractClasses
|
||||
}
|
||||
|
||||
private fun findPlugins(cordappJarPath: RestrictedURL): List<SerializationWhitelist> {
|
||||
@ -273,7 +278,9 @@ abstract class CordappLoaderTemplate : CordappLoader {
|
||||
cordapps.flatMap { corDapp -> corDapp.allFlows.map { flow -> flow to corDapp } }
|
||||
.groupBy { it.first }
|
||||
.mapValues {
|
||||
if(it.value.size > 1) { throw MultipleCordappsForFlowException("There are multiple CorDapp JARs on the classpath for flow ${it.value.first().first.name}: [ ${it.value.joinToString { it.second.name }} ].") }
|
||||
if (it.value.size > 1) {
|
||||
throw MultipleCordappsForFlowException("There are multiple CorDapp JARs on the classpath for flow ${it.value.first().first.name}: [ ${it.value.joinToString { it.second.name }} ].")
|
||||
}
|
||||
it.value.single().second
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,6 @@
|
||||
package net.corda.node.services.schema
|
||||
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
@ -70,7 +67,7 @@ class PersistentStateServiceTests {
|
||||
val persistentStateService = PersistentStateService(schemaService)
|
||||
database.transaction {
|
||||
val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party
|
||||
persistentStateService.persist(setOf(StateAndRef(TransactionState(TestState(), DummyContract.PROGRAM_ID, MEGA_CORP), StateRef(SecureHash.sha256("dummy"), 0))))
|
||||
persistentStateService.persist(setOf(StateAndRef(TransactionState(TestState(), DummyContract.PROGRAM_ID, MEGA_CORP, constraint = AlwaysAcceptAttachmentConstraint), StateRef(SecureHash.sha256("dummy"), 0))))
|
||||
currentDBSession().flush()
|
||||
val parentRowCountResult = connection.prepareStatement("select count(*) from Parents").executeQuery()
|
||||
parentRowCountResult.next()
|
||||
|
@ -332,6 +332,6 @@ class ValidatingNotaryServiceTests {
|
||||
val signedByNode = serviceHub.signInitialTransaction(tx)
|
||||
val stx = notaryNode.services.addSignature(signedByNode, notary.owningKey)
|
||||
serviceHub.recordTransactions(stx)
|
||||
return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0))
|
||||
return StateAndRef(stx.coreTransaction.outputs.first(), StateRef(stx.id, 0))
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,7 @@
|
||||
package net.corda.irs.contract
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.CommandWithParties
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.contracts.SchedulableState
|
||||
import net.corda.core.contracts.ScheduledActivity
|
||||
import net.corda.core.contracts.StateAndContract
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.contracts.TypeOnlyCommandData
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.contracts.requireThat
|
||||
import net.corda.core.contracts.select
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.flows.FlowLogicRefFactory
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
@ -254,8 +240,7 @@ class InterestRateSwap : Contract {
|
||||
* @return LocalDate or null if no more fixings.
|
||||
*/
|
||||
fun nextFixingDate(): LocalDate? {
|
||||
return floatingLegPaymentSchedule.
|
||||
filter { it.value.rate is ReferenceRate }.// TODO - a better way to determine what fixings remain to be fixed
|
||||
return floatingLegPaymentSchedule.filter { it.value.rate is ReferenceRate }.// TODO - a better way to determine what fixings remain to be fixed
|
||||
minBy { it.value.fixingDate.toEpochDay() }?.value?.fixingDate
|
||||
}
|
||||
|
||||
@ -650,7 +635,7 @@ class InterestRateSwap : Contract {
|
||||
}
|
||||
|
||||
override fun generateFix(ptx: TransactionBuilder, oldState: StateAndRef<*>, fix: Fix) {
|
||||
InterestRateSwap().generateFix(ptx, StateAndRef(TransactionState(this, IRS_PROGRAM_ID, oldState.state.notary), oldState.ref), fix)
|
||||
InterestRateSwap().generateFix(ptx, StateAndRef(TransactionState(this, IRS_PROGRAM_ID, oldState.state.notary, constraint = AlwaysAcceptAttachmentConstraint), oldState.ref), fix)
|
||||
}
|
||||
|
||||
override fun nextFixingOf(): FixOf? {
|
||||
@ -748,10 +733,9 @@ class InterestRateSwap : Contract {
|
||||
|
||||
// Put all the above into a new State object.
|
||||
val state = State(fixedLeg, floatingLeg, newCalculation, common, oracle)
|
||||
return TransactionBuilder(notary).withItems(
|
||||
StateAndContract(state, IRS_PROGRAM_ID),
|
||||
Command(Commands.Agree(), listOf(state.floatingLeg.floatingRatePayer.owningKey, state.fixedLeg.fixedRatePayer.owningKey))
|
||||
)
|
||||
return TransactionBuilder(notary)
|
||||
.addCommand(Command(Commands.Agree(), listOf(state.floatingLeg.floatingRatePayer.owningKey, state.fixedLeg.fixedRatePayer.owningKey)))
|
||||
.addOutputState(TransactionState(state, IRS_PROGRAM_ID, notary, null, AlwaysAcceptAttachmentConstraint))
|
||||
}
|
||||
|
||||
private fun calcFixingDate(date: LocalDate, fixingPeriodOffset: Int, calendar: BusinessCalendar): LocalDate {
|
||||
@ -767,7 +751,8 @@ class InterestRateSwap : Contract {
|
||||
tx.addOutputState(
|
||||
irs.state.data.copy(calculation = irs.state.data.calculation.applyFixing(fixing.of.forDay, fixedRate)),
|
||||
irs.state.contract,
|
||||
irs.state.notary
|
||||
irs.state.notary,
|
||||
constraint = AlwaysAcceptAttachmentConstraint
|
||||
)
|
||||
tx.addCommand(Commands.Refix(fixing), listOf(irs.state.data.floatingLeg.floatingRatePayer.owningKey, irs.state.data.fixedLeg.fixedRatePayer.owningKey))
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import net.corda.core.internal.UNKNOWN_UPLOADER
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
@ -147,8 +148,13 @@ data class TestTransactionDSLInterpreter private constructor(
|
||||
override fun _tweak(dsl: TransactionDSLInterpreter.() -> EnforceVerifyOrFail) = copy().dsl()
|
||||
|
||||
override fun _attachment(contractClassName: ContractClassName) {
|
||||
(services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage)
|
||||
attachment((services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage))
|
||||
}
|
||||
|
||||
override fun _attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List<PublicKey>){
|
||||
attachment((services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage, attachmentId, signers))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class TestLedgerDSLInterpreter private constructor(
|
||||
|
@ -1,9 +1,19 @@
|
||||
package net.corda.testing.dsl
|
||||
|
||||
import net.corda.core.DoNotImplement
|
||||
import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.AttachmentConstraint
|
||||
import net.corda.core.contracts.AutomaticPlaceholderConstraint
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.seconds
|
||||
import java.security.PublicKey
|
||||
@ -80,6 +90,13 @@ interface TransactionDSLInterpreter : Verifies, OutputStateLookup {
|
||||
* @param contractClassName The contract class to attach
|
||||
*/
|
||||
fun _attachment(contractClassName: ContractClassName)
|
||||
|
||||
/**
|
||||
* Attaches an attachment containing the named contract to the transaction
|
||||
* @param contractClassName The contract class to attach
|
||||
* @param attachmentId The attachment
|
||||
*/
|
||||
fun _attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List<PublicKey>)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -131,37 +148,37 @@ class TransactionDSL<out T : TransactionDSLInterpreter>(interpreter: T, private
|
||||
* Adds a labelled output to the transaction.
|
||||
*/
|
||||
fun output(contractClassName: ContractClassName, label: String, notary: Party, contractState: ContractState) =
|
||||
output(contractClassName, label, notary, null, AutomaticHashConstraint, contractState)
|
||||
output(contractClassName, label, notary, null, AutomaticPlaceholderConstraint, contractState)
|
||||
|
||||
/**
|
||||
* Adds a labelled output to the transaction.
|
||||
*/
|
||||
fun output(contractClassName: ContractClassName, label: String, encumbrance: Int, contractState: ContractState) =
|
||||
output(contractClassName, label, notary, encumbrance, AutomaticHashConstraint, contractState)
|
||||
output(contractClassName, label, notary, encumbrance, AutomaticPlaceholderConstraint, contractState)
|
||||
|
||||
/**
|
||||
* Adds a labelled output to the transaction.
|
||||
*/
|
||||
fun output(contractClassName: ContractClassName, label: String, contractState: ContractState) =
|
||||
output(contractClassName, label, notary, null, AutomaticHashConstraint, contractState)
|
||||
output(contractClassName, label, notary, null, AutomaticPlaceholderConstraint, contractState)
|
||||
|
||||
/**
|
||||
* Adds an output to the transaction.
|
||||
*/
|
||||
fun output(contractClassName: ContractClassName, notary: Party, contractState: ContractState) =
|
||||
output(contractClassName, null, notary, null, AutomaticHashConstraint, contractState)
|
||||
output(contractClassName, null, notary, null, AutomaticPlaceholderConstraint, contractState)
|
||||
|
||||
/**
|
||||
* Adds an output to the transaction.
|
||||
*/
|
||||
fun output(contractClassName: ContractClassName, encumbrance: Int, contractState: ContractState) =
|
||||
output(contractClassName, null, notary, encumbrance, AutomaticHashConstraint, contractState)
|
||||
output(contractClassName, null, notary, encumbrance, AutomaticPlaceholderConstraint, contractState)
|
||||
|
||||
/**
|
||||
* Adds an output to the transaction.
|
||||
*/
|
||||
fun output(contractClassName: ContractClassName, contractState: ContractState) =
|
||||
output(contractClassName, null, notary, null, AutomaticHashConstraint, contractState)
|
||||
output(contractClassName, null, notary, null, AutomaticPlaceholderConstraint, contractState)
|
||||
|
||||
/**
|
||||
* Adds a command to the transaction.
|
||||
@ -186,5 +203,8 @@ class TransactionDSL<out T : TransactionDSLInterpreter>(interpreter: T, private
|
||||
*/
|
||||
fun attachment(contractClassName: ContractClassName) = _attachment(contractClassName)
|
||||
|
||||
fun attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List<PublicKey>) = _attachment(contractClassName, attachmentId, signers)
|
||||
fun attachment(contractClassName: ContractClassName, attachmentId: AttachmentId) = _attachment(contractClassName, attachmentId, emptyList())
|
||||
|
||||
fun attachments(vararg contractClassNames: ContractClassName) = contractClassNames.forEach { attachment(it) }
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package net.corda.testing.internal
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.cordapp.Cordapp
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
|
||||
import net.corda.core.internal.cordapp.CordappImpl
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
@ -11,6 +12,7 @@ import net.corda.node.cordapp.CordappLoader
|
||||
import net.corda.node.internal.cordapp.CordappProviderImpl
|
||||
import net.corda.testing.services.MockAttachmentStorage
|
||||
import java.nio.file.Paths
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
class MockCordappProvider(
|
||||
@ -21,7 +23,7 @@ class MockCordappProvider(
|
||||
|
||||
private val cordappRegistry = mutableListOf<Pair<Cordapp, AttachmentId>>()
|
||||
|
||||
fun addMockCordapp(contractClassName: ContractClassName, attachments: MockAttachmentStorage) {
|
||||
fun addMockCordapp(contractClassName: ContractClassName, attachments: MockAttachmentStorage, contractHash: AttachmentId? = null, signers: List<PublicKey> = emptyList()): AttachmentId {
|
||||
val cordapp = CordappImpl(
|
||||
contractClassNames = listOf(contractClassName),
|
||||
initiatedFlows = emptyList(),
|
||||
@ -35,21 +37,23 @@ class MockCordappProvider(
|
||||
jarPath = Paths.get("").toUri().toURL(),
|
||||
allFlows = emptyList(),
|
||||
jarHash = SecureHash.allOnesHash)
|
||||
if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) }) {
|
||||
cordappRegistry.add(Pair(cordapp, findOrImportAttachment(listOf(contractClassName), contractClassName.toByteArray(), attachments)))
|
||||
if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) && it.second == contractHash }) {
|
||||
cordappRegistry.add(Pair(cordapp, findOrImportAttachment(listOf(contractClassName), contractClassName.toByteArray(), attachments, contractHash, signers)))
|
||||
}
|
||||
return cordappRegistry.findLast { contractClassName in it.first.contractClassNames }?.second!!
|
||||
}
|
||||
|
||||
override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? = cordappRegistry.find { it.first.contractClassNames.contains(contractClassName) }?.second ?: super.getContractAttachmentID(contractClassName)
|
||||
override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? = cordappRegistry.find { it.first.contractClassNames.contains(contractClassName) }?.second
|
||||
?: super.getContractAttachmentID(contractClassName)
|
||||
|
||||
private fun findOrImportAttachment(contractClassNames: List<ContractClassName>, data: ByteArray, attachments: MockAttachmentStorage): AttachmentId {
|
||||
val existingAttachment = attachments.files.filter {
|
||||
Arrays.equals(it.value.second, data)
|
||||
private fun findOrImportAttachment(contractClassNames: List<ContractClassName>, data: ByteArray, attachments: MockAttachmentStorage, contractHash: AttachmentId?, signers: List<PublicKey>): AttachmentId {
|
||||
val existingAttachment = attachments.files.filter { (attachmentId, content) ->
|
||||
contractHash == attachmentId
|
||||
}
|
||||
return if (!existingAttachment.isEmpty()) {
|
||||
existingAttachment.keys.first()
|
||||
} else {
|
||||
attachments.importContractAttachment(contractClassNames, DEPLOYED_CORDAPP_UPLOADER, data.inputStream())
|
||||
attachments.importContractAttachment(contractClassNames, DEPLOYED_CORDAPP_UPLOADER, data.inputStream(), contractHash, signers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import net.corda.core.contracts.ContractAttachment
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.AbstractAttachment
|
||||
import net.corda.core.internal.UNKNOWN_UPLOADER
|
||||
import net.corda.core.internal.readFully
|
||||
@ -15,6 +16,7 @@ import net.corda.core.node.services.vault.AttachmentSort
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.nodeapi.internal.withContractsInJar
|
||||
import java.io.InputStream
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
import java.util.jar.JarInputStream
|
||||
|
||||
@ -53,21 +55,22 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
|
||||
}
|
||||
}
|
||||
|
||||
fun importContractAttachment(contractClassNames: List<ContractClassName>, uploader: String, jar: InputStream): AttachmentId = importAttachmentInternal(jar, uploader, contractClassNames)
|
||||
@JvmOverloads
|
||||
fun importContractAttachment(contractClassNames: List<ContractClassName>, uploader: String, jar: InputStream, attachmentId: AttachmentId? = null, signers: List<PublicKey> = emptyList()): AttachmentId = importAttachmentInternal(jar, uploader, contractClassNames, attachmentId, signers)
|
||||
|
||||
fun getAttachmentIdAndBytes(jar: InputStream): Pair<AttachmentId, ByteArray> = jar.readFully().let { bytes -> Pair(bytes.sha256(), bytes) }
|
||||
|
||||
private class MockAttachment(dataLoader: () -> ByteArray, override val id: SecureHash) : AbstractAttachment(dataLoader)
|
||||
private class MockAttachment(dataLoader: () -> ByteArray, override val id: SecureHash, override val signers: List<PublicKey>) : AbstractAttachment(dataLoader)
|
||||
|
||||
private fun importAttachmentInternal(jar: InputStream, uploader: String, contractClassNames: List<ContractClassName>? = null): AttachmentId {
|
||||
private fun importAttachmentInternal(jar: InputStream, uploader: String, contractClassNames: List<ContractClassName>? = null, attachmentId: AttachmentId? = null, signers: List<PublicKey> = emptyList()): AttachmentId {
|
||||
// JIS makes read()/readBytes() return bytes of the current file, but we want to hash the entire container here.
|
||||
require(jar !is JarInputStream)
|
||||
|
||||
val bytes = jar.readFully()
|
||||
|
||||
val sha256 = bytes.sha256()
|
||||
val sha256 = attachmentId ?: bytes.sha256()
|
||||
if (sha256 !in files.keys) {
|
||||
val baseAttachment = MockAttachment({ bytes }, sha256)
|
||||
val baseAttachment = MockAttachment({ bytes }, sha256, signers)
|
||||
val attachment = if (contractClassNames == null || contractClassNames.isEmpty()) baseAttachment else ContractAttachment(baseAttachment, contractClassNames.first(), contractClassNames.toSet(), uploader)
|
||||
_files[sha256] = Pair(attachment, bytes)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user