diff --git a/.ci/api-current.txt b/.ci/api-current.txt index b42420c07c..53601aaf1f 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -455,6 +455,9 @@ public final class net.corda.core.contracts.AutomaticHashConstraint extends java public boolean isSatisfiedBy(net.corda.core.contracts.Attachment) public static final net.corda.core.contracts.AutomaticHashConstraint INSTANCE ## +public @interface net.corda.core.contracts.BelongsToContract + public abstract Class value() +## @CordaSerializable public final class net.corda.core.contracts.Command extends java.lang.Object public (T, java.security.PublicKey) diff --git a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt index 3f5546b2e9..e08b166dcc 100644 --- a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt +++ b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt @@ -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() + doReturn(attachmentStorage).whenever(services).attachments + val attachment = rigorousMock() + doReturn(attachment).whenever(attachmentStorage).openAttachment(attachmentId) + doReturn(attachmentId).whenever(attachment).id + doReturn(emptyList()).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)), diff --git a/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt b/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt index 55be99325b..f3022c319f 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt @@ -5,17 +5,78 @@ import net.corda.core.KeepForDJVM import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint.isSatisfiedBy 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. */ @@ -47,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.") } } diff --git a/core/src/main/kotlin/net/corda/core/contracts/BelongsToContract.kt b/core/src/main/kotlin/net/corda/core/contracts/BelongsToContract.kt new file mode 100644 index 0000000000..b5683f51aa --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/contracts/BelongsToContract.kt @@ -0,0 +1,32 @@ +package net.corda.core.contracts +import kotlin.reflect.KClass +/** + * This annotation is required by any [ContractState] which needs to ensure that it is only ever processed as part of a + * [TransactionState] referencing the specified [Contract]. It may be omitted in the case that the [ContractState] class + * is defined as an inner class of its owning [Contract] class, in which case the "X belongs to Y" relationship is taken + * to be implicitly declared. + * + * During verification of transactions, prior to their being written into the ledger, all input and output states are + * checked to ensure that their [ContractState]s match with their [Contract]s as specified either by this annotation, or + * by their inner/outer class relationship. + * + * The transaction will write a warning to the log if any mismatch is detected. + * + * @param value The class of the [Contract] to which states of the annotated [ContractState] belong. + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.CLASS) +annotation class BelongsToContract(val value: KClass) +/** + * Obtain the typename of the required [ContractClass] associated with the target [ContractState], using the + * [BelongsToContract] annotation by default, but falling through to checking the state's enclosing class if there is + * one and it inherits from [Contract]. + */ +val ContractState.requiredContractClassName: String? get() { + val annotation = javaClass.getAnnotation(BelongsToContract::class.java) + if (annotation != null) { + return annotation.value.java.typeName + } + val enclosingClass = javaClass.enclosingClass ?: return null + return if (Contract::class.java.isAssignableFrom(enclosingClass)) enclosingClass.typeName else null +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt index 7ad5129538..dadda29255 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt @@ -4,6 +4,7 @@ package net.corda.core.contracts import net.corda.core.KeepForDJVM import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable +import net.corda.core.utilities.loggerFor // DOCSTART 1 typealias ContractClassName = String @@ -26,7 +27,14 @@ data class TransactionState @JvmOverloads constructor( * sent across, and run, from the network from within a sandbox environment. */ // TODO: Implement the contract sandbox loading of the contract attachments - val contract: ContractClassName, + val contract: ContractClassName = requireNotNull(data.requiredContractClassName) { + //TODO: add link to docsite page, when there is one. + """ + Unable to infer Contract class name because state class ${data::class.java.name} is not annotated with + @BelongsToContract, and does not have an enclosing class which implements Contract. Either annotate ${data::class.java.name} + with @BelongsToContract, or supply an explicit contract parameter to TransactionState(). + """.trimIndent().replace('\n', ' ') + }, /** Identity of the notary that ensures the state is not used as an input to a transaction more than once */ val notary: Party, /** @@ -50,5 +58,29 @@ data class TransactionState @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>() + } + + init { + when { + data.requiredContractClassName == null -> logger.warn( + """ + State class ${data::class.java.name} is not annotated with @BelongsToContract, + and does not have an enclosing class which implements Contract. Annotate ${data::class.java.simpleName} + with @BelongsToContract(${contract.split("\\.\\$").last()}.class) to remove this warning. + """.trimIndent().replace('\n', ' ') + ) + data.requiredContractClassName != contract -> logger.warn( + """ + State class ${data::class.java.name} belongs to contract ${data.requiredContractClassName}, + but is bundled with contract $contract in TransactionState. Annotate ${data::class.java.simpleName} + with @BelongsToContract(${contract.split("\\.\\$").last()}.class) to remove this warning. + """.trimIndent().replace('\n', ' ') + ) + } + } +} // DOCEND 1 diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt index 970c35ad9a..4df42e63c9 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt @@ -37,7 +37,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) { /** @@ -51,6 +51,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. diff --git a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt index d84154eb26..33034efa07 100644 --- a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt @@ -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() \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index c9557bf7c7..c72221a204 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -12,9 +12,11 @@ import net.corda.core.internal.uncheckedCast import net.corda.core.node.NetworkParameters import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.Try +import net.corda.core.utilities.loggerFor import java.util.* import java.util.function.Predicate import kotlin.collections.HashSet +import net.corda.core.utilities.warnOnce /** * A LedgerTransaction is derived from a [WireTransaction]. It is the result of doing the following operations: @@ -57,6 +59,7 @@ data class LedgerTransaction @JvmOverloads constructor( } private companion object { + val logger = loggerFor() private fun contractClassFor(className: ContractClassName, classLoader: ClassLoader?): Try> { return Try.on { (classLoader ?: this::class.java.classLoader) @@ -70,10 +73,6 @@ data class LedgerTransaction @JvmOverloads constructor( } } - // Input reference state contracts are not required for verification. - private val contracts: Map>> = (inputs.map { it.state } + outputs) - .map { it.contract to stateToContractClass(it) }.toMap() - val inputStates: List get() = inputs.map { it.state.data } val referenceStates: List get() = references.map { it.state.data } @@ -96,8 +95,11 @@ data class LedgerTransaction @JvmOverloads constructor( fun verify() { val contractAttachmentsByContract: Map = getUniqueContractAttachmentsByContract() + // TODO - verify for version downgrade validatePackageOwnership(contractAttachmentsByContract) - verifyConstraints() + validateStatesAgainstContract() + verifyConstraintsValidity(contractAttachmentsByContract) + verifyConstraints(contractAttachmentsByContract) verifyContracts() } @@ -126,32 +128,75 @@ data class LedgerTransaction @JvmOverloads constructor( } /** - * Verify that all contract constraints are valid for each state before running any contract code + * 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. * - * 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. + * A warning will be written to the log if any mismatch is detected. + */ + private fun validateStatesAgainstContract() = allStates.forEach(::validateStateAgainstContract) + + private fun validateStateAgainstContract(state: TransactionState) { + state.data.requiredContractClassName?.let { requiredContractClassName -> + if (state.contract != requiredContractClassName) + logger.warnOnce(""" + State of class ${state.data::class.java.typeName} belongs to contract $requiredContractClassName, but + is bundled in TransactionState with ${state.contract}. + """.trimIndent().replace('\n', ' ')) + } + } + + /** + * 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) { + // 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. + * + * This check is running the [AttachmentConstraint.isSatisfiedBy] method for each corresponding [ContractAttachment]. * * @throws TransactionVerificationException if the constraints fail to verify */ - private fun verifyConstraints() { - val contractAttachments = attachments.filterIsInstance() - (inputs.map { it.state } + outputs).forEach { state -> - val stateAttachments = contractAttachments.filter { state.contract in it.allContracts } - if (stateAttachments.isEmpty()) throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract) + private fun verifyConstraints(contractAttachmentsByContract: Map) { + for (state in allStates) { + val contractAttachment = contractAttachmentsByContract[state.contract] + ?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract) - val uniqueAttachmentsForStateContract = stateAttachments.distinctBy { it.id } + val constraintAttachment = AttachmentWithContext(contractAttachment, state.contract, + networkParameters?.whitelistedContractImplementations) - // In case multiple attachments have been added for the same contract, fail because this transaction will not be able to be verified - // because it will break the no-overlap rule that we have implemented in our Classloaders - if (uniqueAttachmentsForStateContract.size > 1) { - throw TransactionVerificationException.ConflictingAttachmentsRejection(id, state.contract) - } - - val contractAttachment = uniqueAttachmentsForStateContract.first() - val constraintAttachment = AttachmentWithContext(contractAttachment, state.contract, networkParameters?.whitelistedContractImplementations) if (state.constraint is SignatureAttachmentConstraint) checkMinimumPlatformVersion(networkParameters?.minimumPlatformVersion ?: 1, 4, "Signature constraints") + if (!state.constraint.isSatisfiedBy(constraintAttachment)) { throw TransactionVerificationException.ContractConstraintRejection(id, state.contract) } @@ -185,29 +230,35 @@ 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() { - val contractInstances = ArrayList(contracts.size) - for ((key, result) in contracts) { - when (result) { - is Try.Failure -> throw TransactionVerificationException.ContractCreationError(id, key, result.exception) - is Try.Success -> { - try { - contractInstances.add(result.value.newInstance()) - } catch (e: Exception) { - throw TransactionVerificationException.ContractCreationError(id, result.value.name, e) - } - } - } - } - contractInstances.forEach { contract -> - try { - contract.verify(this) - } catch (e: Exception) { - throw TransactionVerificationException.ContractRejection(id, contract, e) - } + private fun verifyContracts() = inputAndOutputStates.forEach { ts -> + val contractClass = getContractClass(ts) + val contract = createContractInstance(contractClass) + + try { + contract.verify(this) + } catch (e: Exception) { + throw TransactionVerificationException.ContractRejection(id, contract, e) } } + // Obtain the contract class from the class name, wrapping any exception as a [ContractCreationError] + private fun getContractClass(ts: TransactionState): Class = + try { + (ts.data::class.java.classLoader ?: this::class.java.classLoader) + .loadClass(ts.contract) + .asSubclass(Contract::class.java) + } catch (e: Exception) { + throw TransactionVerificationException.ContractCreationError(id, ts.contract, e) + } + + // Obtain an instance of the contract class, wrapping any exception as a [ContractCreationError] + private fun createContractInstance(contractClass: Class): Contract = + try { + contractClass.newInstance() + } catch (e: Exception) { + 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. diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 159a62750d..31cf573072 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -4,15 +4,13 @@ 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.CompositeKey -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.SignableData -import net.corda.core.crypto.SignatureMetadata +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.StatePointerSearch 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 @@ -21,6 +19,7 @@ import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationFactory +import net.corda.core.utilities.loggerFor import net.corda.core.utilities.contextLogger import java.security.PublicKey import java.time.Duration @@ -53,6 +52,11 @@ open class TransactionBuilder @JvmOverloads constructor( protected val references: MutableList = arrayListOf(), protected val serviceHub: ServiceHub? = (Strand.currentStrand() as? FlowStateMachine<*>)?.serviceHub ) { + + private companion object { + val logger = loggerFor() + } + private val inputsWithTransactionState = arrayListOf>() private val referencesWithTransactionState = arrayListOf>() @@ -82,7 +86,7 @@ open class TransactionBuilder @JvmOverloads constructor( // DOCSTART 1 /** A more convenient way to add items to this transaction that calls the add* methods for you based on type */ - fun withItems(vararg items: Any): TransactionBuilder { + fun withItems(vararg items: Any) = apply { for (t in items) { when (t) { is StateAndRef<*> -> addInputState(t) @@ -98,13 +102,11 @@ open class TransactionBuilder @JvmOverloads constructor( else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}") } } - return this } // 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]. * @@ -120,21 +122,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, resolvedOutputs: List>) = 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) { @@ -143,65 +135,264 @@ 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.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, List>> { - private fun makeAttachmentConstraint(services: ServicesForResolution, state: TransactionState): AttachmentConstraint { - val attachmentId = services.cordappProvider.getContractAttachmentID(state.contract) - ?: throw MissingContractAttachments(listOf(state)) - - val attachmentSigners = services.attachments.openAttachment(attachmentId)?.signers - ?: throw MissingContractAttachments(listOf(state)) - - return when { - attachmentSigners.isEmpty() -> HashAttachmentConstraint(attachmentId) - else -> { - // Auto downgrade: signature constraints only available with a corda network minimum platform version of >= 4 - if (services.networkParameters.minimumPlatformVersion < 4) { - log.warn("Signature constraints not available on network requiring a minimum platform version of ${services.networkParameters.minimumPlatformVersion}") - if (useWhitelistedByZoneAttachmentConstraint(state.contract, services.networkParameters)) { - log.warn("Reverting back to using whitelisted zone constraints") - WhitelistedByZoneAttachmentConstraint - } - else { - log.warn("Reverting back to using hash constraints") - HashAttachmentConstraint(attachmentId) - } + // Determine the explicitly set contract attachments. + val explicitAttachmentContracts: List> = this.attachments + .map(services.attachments::openAttachment) + .mapNotNull { it as? ContractAttachment } + .flatMap { attch -> + attch.allContracts.map { it to attch.id } } - else makeSignatureAttachmentConstraint(attachmentSigners) + + // 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." } + + val explicitAttachmentContractsMap: Map = explicitAttachmentContracts.toMap() + + val inputContractGroups: Map>> = inputsWithTransactionState.groupBy { it.contract } + val outputContractGroups: Map>> = outputs.groupBy { it.contract } + + val allContracts: Set = 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>> = referencesWithTransactionState.groupBy { it.contract } + val refStateContractAttachments: List = 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>?>> = allContracts.toSet().map { ctr -> + handleContract(ctr, inputContractGroups[ctr], outputContractGroups[ctr], explicitAttachmentContractsMap[ctr], serializationContext, services) + } + + val resolvedStates: List> = contractAttachmentsAndResolvedOutputStates.mapNotNull { it.second }.flatten() + + // The output states need to preserve the order in which they were added. + val resolvedOutputStatesInTheOriginalOrder: List> = outputStates().map { os -> resolvedStates.find { rs -> rs.data == os.data && rs.encumbrance == os.encumbrance}!! } + + val attachments: Collection = 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>?, + outputStates: List>?, + explicitContractAttachment: AttachmentId?, + serializationContext: SerializationContext?, + services: ServicesForResolution + ): Pair>?> { + 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>?, + attachmentToUse: ContractAttachment, + services: ServicesForResolution): AttachmentConstraint = when { + inputStates != null -> attachmentConstraintsTransition(inputStates.groupBy { it.constraint }.keys, attachmentToUse) + useWhitelistedByZoneAttachmentConstraint(contractClassName, services.networkParameters) -> WhitelistedByZoneAttachmentConstraint + attachmentToUse.signers.isNotEmpty() -> { + // Auto downgrade: signature constraints only available with a corda network minimum platform version of >= 4 + if (services.networkParameters.minimumPlatformVersion < 4) { + log.warn("Signature constraints not available on network requiring a minimum platform version of ${services.networkParameters.minimumPlatformVersion}") + if (useWhitelistedByZoneAttachmentConstraint(state.contract, services.networkParameters)) { + log.warn("Reverting back to using whitelisted zone constraints") + WhitelistedByZoneAttachmentConstraint + } + else { + log.warn("Reverting back to using hash constraints") + HashAttachmentConstraint(attachmentId) + } + } + else makeSignatureAttachmentConstraint(attachmentSigners) + } + 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, 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) = 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 { - // 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, 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) @@ -275,7 +466,7 @@ open class TransactionBuilder @JvmOverloads constructor( * Note: Reference states are only supported on Corda networks running a minimum platform version of 4. * [toWireTransaction] will throw an [IllegalStateException] if called in such an environment. */ - open fun addReferenceState(referencedStateAndRef: ReferencedStateAndRef<*>): TransactionBuilder { + open fun addReferenceState(referencedStateAndRef: ReferencedStateAndRef<*>) = apply { val stateAndRef = referencedStateAndRef.stateAndRef referencesWithTransactionState.add(stateAndRef.state) @@ -302,11 +493,10 @@ open class TransactionBuilder @JvmOverloads constructor( checkNotary(stateAndRef) references.add(stateAndRef.ref) checkForInputsAndReferencesOverlap() - return this } /** Adds an input [StateRef] to the transaction. */ - open fun addInputState(stateAndRef: StateAndRef<*>): TransactionBuilder { + open fun addInputState(stateAndRef: StateAndRef<*>) = apply { checkNotary(stateAndRef) inputs.add(stateAndRef.ref) inputsWithTransactionState.add(stateAndRef.state) @@ -315,13 +505,12 @@ open class TransactionBuilder @JvmOverloads constructor( } /** Adds an attachment with the specified hash to the TransactionBuilder. */ - fun addAttachment(attachmentId: SecureHash): TransactionBuilder { + fun addAttachment(attachmentId: SecureHash) = apply { attachments.add(attachmentId) - return this } /** Adds an output state to the transaction. */ - fun addOutputState(state: TransactionState<*>): TransactionBuilder { + fun addOutputState(state: TransactionState<*>) = apply { outputs.add(state) resolveStatePointers(state) return this @@ -331,30 +520,41 @@ open class TransactionBuilder @JvmOverloads constructor( @JvmOverloads fun addOutputState( state: ContractState, - contract: ContractClassName, + contract: ContractClassName = requireNotNull(state.requiredContractClassName) { + //TODO: add link to docsite page, when there is one. + """ +Unable to infer Contract class name because state class ${state::class.java.name} is not annotated with +@BelongsToContract, and does not have an enclosing class which implements Contract. Either annotate ${state::class.java.name} +with @BelongsToContract, or supply an explicit contract parameter to addOutputState(). +""".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 fun addOutputState( - state: ContractState, contract: ContractClassName, - constraint: AttachmentConstraint = AutomaticHashConstraint - ): TransactionBuilder { + state: ContractState, + contract: ContractClassName = requireNotNull(state.requiredContractClassName) { + //TODO: add link to docsite page, when there is one. + """ +Unable to infer Contract class name because state class ${state::class.java.name} is not annotated with +@BelongsToContract, and does not have an enclosing class which implements Contract. Either annotate ${state::class.java.name} +with @BelongsToContract, or supply an explicit contract parameter to addOutputState(). +""".trimIndent().replace('\n', ' ') + }, + constraint: AttachmentConstraint = AutomaticPlaceholderConstraint + ) = apply { checkNotNull(notary) { "Need to specify a notary for the state, or set a default one on TransactionBuilder initialisation" } addOutputState(state, contract, notary!!, constraint = constraint) - return this } /** Adds a [Command] to the transaction. */ - fun addCommand(arg: Command<*>): TransactionBuilder { + fun addCommand(arg: Command<*>) = apply { commands.add(arg) - return this } /** @@ -370,10 +570,9 @@ open class TransactionBuilder @JvmOverloads constructor( * transaction must then be signed by the notary service within this window of time. In this way, the notary acts as * the Timestamp Authority. */ - fun setTimeWindow(timeWindow: TimeWindow): TransactionBuilder { + fun setTimeWindow(timeWindow: TimeWindow) = apply { check(notary != null) { "Only notarised transactions can have a time-window" } window = timeWindow - return this } /** @@ -385,9 +584,8 @@ open class TransactionBuilder @JvmOverloads constructor( */ fun setTimeWindow(time: Instant, timeTolerance: Duration) = setTimeWindow(TimeWindow.withTolerance(time, timeTolerance)) - fun setPrivacySalt(privacySalt: PrivacySalt): TransactionBuilder { + fun setPrivacySalt(privacySalt: PrivacySalt) = apply { this.privacySalt = privacySalt - return this } /** Returns an immutable list of input [StateRef]s. */ diff --git a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt index 8e4a2b8f2a..8ef7904a75 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt @@ -11,6 +11,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 @@ -146,3 +147,20 @@ fun Future.getOrThrow(timeout: Duration? = null): V = try { * Size is very cheap as it doesn't call [transform]. */ fun List.lazyMapped(transform: (T, Int) -> U): List = LazyMappedList(this, transform) + +private const val MAX_SIZE = 100 +private val warnings = Collections.newSetFromMap(object : LinkedHashMap() { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry?) = size > MAX_SIZE +}) + +/** + * Utility to help log a warning message only once. + * 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) + this.warn(warning) + } +} \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/UtilsTest.kt b/core/src/test/kotlin/net/corda/core/UtilsTest.kt index 717356f4d4..064fd0c016 100644 --- a/core/src/test/kotlin/net/corda/core/UtilsTest.kt +++ b/core/src/test/kotlin/net/corda/core/UtilsTest.kt @@ -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.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") + } } diff --git a/core/src/test/kotlin/net/corda/core/contracts/ConstraintsPropagationTests.kt b/core/src/test/kotlin/net/corda/core/contracts/ConstraintsPropagationTests.kt new file mode 100644 index 0000000000..9417f14caa --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/contracts/ConstraintsPropagationTests.kt @@ -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().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 { + 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 { + 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() + 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 { HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(AutomaticPlaceholderConstraint, attachment) } + assertFailsWith { AutomaticPlaceholderConstraint.canBeTransitionedFrom(AutomaticPlaceholderConstraint, attachment) } + } +} + +@BelongsToContract(NoPropagationContract::class) +class NoPropagationContractState : ContractState { + override val participants: List + get() = emptyList() +} + +@NoConstraintPropagation +class NoPropagationContract : Contract { + interface Commands : CommandData + class Create : Commands + + override fun verify(tx: LedgerTransaction) { + //do nothing + } +} diff --git a/core/src/test/kotlin/net/corda/core/contracts/PackageOwnershipVerificationTests.kt b/core/src/test/kotlin/net/corda/core/contracts/PackageOwnershipVerificationTests.kt index 98fe6683bd..ecbb8f8374 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/PackageOwnershipVerificationTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/PackageOwnershipVerificationTests.kt @@ -8,6 +8,9 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.node.JavaPackageName import net.corda.core.transactions.LedgerTransaction +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 @@ -74,6 +77,7 @@ class PackageOwnershipVerificationTests { } +@BelongsToContract(DummyContract::class) class DummyContractState : ContractState { override val participants: List get() = emptyList() diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index 889b8ceeee..a6e7f5d106 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -159,6 +159,7 @@ class ContractUpgradeFlowTest : WithContracts, WithFinality { class Move : TypeOnlyCommandData() + @BelongsToContract(CashV2::class) data class State(override val amount: Amount>, val owners: List) : FungibleAsset { override val owner: AbstractParty = owners.first() override val exitKeys = (owners + amount.token.issuer.party).map { it.owningKey }.toSet() diff --git a/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt b/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt index 0b9db73064..750a76af04 100644 --- a/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt +++ b/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt @@ -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`() { diff --git a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt index fa50d3f273..6504ae07cb 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt @@ -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) diff --git a/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt b/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt index f91eb9c344..45bf4e4c5d 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt @@ -46,11 +46,12 @@ class LedgerTransactionQueryTests { data class Cmd3(val id: Int) : CommandData, Commands // Unused command, required for command not-present checks. } - + @BelongsToContract(DummyContract::class) private class StringTypeDummyState(val data: String) : ContractState { override val participants: List = emptyList() } + @BelongsToContract(DummyContract::class) private class IntTypeDummyState(val data: Int) : ContractState { override val participants: List = emptyList() } @@ -72,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 { diff --git a/core/src/test/kotlin/net/corda/core/transactions/ReferenceInputStateTests.kt b/core/src/test/kotlin/net/corda/core/transactions/ReferenceInputStateTests.kt index 8b09b574ca..db120fa2e1 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/ReferenceInputStateTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/ReferenceInputStateTests.kt @@ -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()) diff --git a/docs/source/api-contract-constraints.rst b/docs/source/api-contract-constraints.rst index 3611118906..d288ef5eb1 100644 --- a/docs/source/api-contract-constraints.rst +++ b/docs/source/api-contract-constraints.rst @@ -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: @@ -92,18 +101,19 @@ by Corda development key by default, an external keystore can be configured or s An auto downgrade rule applies to signed CorDapps built and tested with Corda 4 but running on a Corda network of a lower version: if the associated contract class is whitelisted in the network parameters then zone constraints are applied, otherwise hash constraints are used. -**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 @@ -124,48 +134,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: @@ -174,6 +174,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 ------- diff --git a/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java b/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java index 3f5a91fe83..ae35a39668 100644 --- a/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java +++ b/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/TutorialTestDSL.java @@ -250,7 +250,7 @@ public class TutorialTestDSL { // Some CP is issued onto the ledger by MegaCorp. l.transaction("Issuance", tx -> { - tx.output(Cash.PROGRAM_ID, "paper", getPaper()); + tx.output(JCP_PROGRAM_ID, "paper", getPaper()); tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Issue()); tx.attachments(JCP_PROGRAM_ID); tx.timeWindow(TEST_TX_TIME); @@ -296,7 +296,7 @@ public class TutorialTestDSL { // Some CP is issued onto the ledger by MegaCorp. l.transaction("Issuance", tx -> { - tx.output(Cash.PROGRAM_ID, "paper", getPaper()); + tx.output(JCP_PROGRAM_ID, "paper", getPaper()); tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Issue()); tx.attachments(JCP_PROGRAM_ID); tx.timeWindow(TEST_TX_TIME); @@ -309,7 +309,7 @@ public class TutorialTestDSL { tx.output(Cash.PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), megaCorp.getParty())); JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(alice.getParty())); - tx.command(alice.getPublicKey(), new Cash.Commands.Move()); + tx.command(alice.getPublicKey(), new Cash.Commands.Move(JavaCommercialPaper.class)); tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Move()); return tx.verifies(); }); diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt index 4eee343231..5069e8c11c 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/tutorial/testdsl/TutorialTestDSL.kt @@ -293,7 +293,7 @@ class TutorialTestDSL { input("alice's $900") output(Cash.PROGRAM_ID, "borrowed $900", 900.DOLLARS.CASH issuedBy issuer ownedBy megaCorp.party) output(CP_PROGRAM_ID, "alice's paper", "paper".output().withOwner(alice.party)) - command(alice.publicKey, Cash.Commands.Move()) + command(alice.publicKey, Cash.Commands.Move(CommercialPaper::class.java)) command(megaCorp.publicKey, CommercialPaper.Commands.Move()) verifies() } diff --git a/experimental/notary-raft/src/test/kotlin/net/corda/notary/raft/RaftNotaryServiceTests.kt b/experimental/notary-raft/src/test/kotlin/net/corda/notary/raft/RaftNotaryServiceTests.kt index 35f6fc3f89..d9a388cea7 100644 --- a/experimental/notary-raft/src/test/kotlin/net/corda/notary/raft/RaftNotaryServiceTests.kt +++ b/experimental/notary-raft/src/test/kotlin/net/corda/notary/raft/RaftNotaryServiceTests.kt @@ -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)) } } diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt index 58d5bae36c..34e47a890f 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt @@ -51,6 +51,7 @@ class Cash : OnLedgerAsset() { // DOCSTART 1 /** A state representing a cash claim against some party. */ + @BelongsToContract(Cash::class) data class State( override val amount: Amount>, diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt index 6ae37494af..a271d13af2 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt @@ -501,7 +501,7 @@ class CashTests { private fun makeCash(amount: Amount, 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)) ) diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt index 7c5edaeea1..e9e1ebf0d4 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt @@ -96,11 +96,11 @@ class ObligationTests { group: LedgerDSL ) = group.apply { unverifiedTransaction { - attachments(Obligation.PROGRAM_ID) + attachments(Obligation.PROGRAM_ID, Cash.PROGRAM_ID) output(Obligation.PROGRAM_ID, "Alice's $1,000,000 obligation to Bob", oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)) output(Obligation.PROGRAM_ID, "Bob's $1,000,000 obligation to Alice", oneMillionDollars.OBLIGATION between Pair(BOB, ALICE)) output(Obligation.PROGRAM_ID, "MegaCorp's $1,000,000 obligation to Bob", oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, BOB)) - output(Obligation.PROGRAM_ID, "Alice's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy ALICE) + output(Cash.PROGRAM_ID, "Alice's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy ALICE) } } @@ -166,11 +166,11 @@ class ObligationTests { transaction { attachments(Obligation.PROGRAM_ID) output(Obligation.PROGRAM_ID, - Obligation.State( - obligor = MINI_CORP, - quantity = 1000.DOLLARS.quantity, - beneficiary = CHARLIE, - template = megaCorpDollarSettlement)) + Obligation.State( + obligor = MINI_CORP, + quantity = 1000.DOLLARS.quantity, + beneficiary = CHARLIE, + template = megaCorpDollarSettlement)) command(MINI_CORP_PUBKEY, Obligation.Commands.Issue()) this.verifies() } @@ -314,7 +314,7 @@ class ObligationTests { } private inline fun getStateAndRef(state: T, contractClassName: ContractClassName): StateAndRef { - val txState = TransactionState(state, contractClassName, DUMMY_NOTARY) + val txState = TransactionState(state, contractClassName, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint) return StateAndRef(txState, StateRef(SecureHash.randomSHA256(), 0)) } @@ -506,10 +506,10 @@ class ObligationTests { ledgerServices.ledger(DUMMY_NOTARY) { cashObligationTestRoots(this) transaction("Settlement") { - attachments(Obligation.PROGRAM_ID) + attachments(Obligation.PROGRAM_ID, Cash.PROGRAM_ID) input("Alice's $1,000,000 obligation to Bob") input("Alice's $1,000,000") - output(Obligation.PROGRAM_ID, "Bob's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB) + output(Cash.PROGRAM_ID, "Bob's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB) command(ALICE_PUBKEY, Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token))) command(ALICE_PUBKEY, Cash.Commands.Move(Obligation::class.java)) attachment(attachment(cashContractBytes.inputStream())) @@ -525,7 +525,7 @@ class ObligationTests { input(Obligation.PROGRAM_ID, oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)) input(Cash.PROGRAM_ID, 500000.DOLLARS.CASH issuedBy defaultIssuer ownedBy ALICE) output(Obligation.PROGRAM_ID, "Alice's $500,000 obligation to Bob", halfAMillionDollars.OBLIGATION between Pair(ALICE, BOB)) - output(Obligation.PROGRAM_ID, "Bob's $500,000", 500000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB) + output(Cash.PROGRAM_ID, "Bob's $500,000", 500000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB) command(ALICE_PUBKEY, Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token))) command(ALICE_PUBKEY, Cash.Commands.Move(Obligation::class.java)) attachment(attachment(cashContractBytes.inputStream())) @@ -540,7 +540,7 @@ class ObligationTests { attachments(Obligation.PROGRAM_ID, Cash.PROGRAM_ID) input(Obligation.PROGRAM_ID, defaultedObligation) // Alice's defaulted $1,000,000 obligation to Bob input(Cash.PROGRAM_ID, 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy ALICE) - output(Obligation.PROGRAM_ID, "Bob's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB) + output(Cash.PROGRAM_ID, "Bob's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB) command(ALICE_PUBKEY, Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token))) command(ALICE_PUBKEY, Cash.Commands.Move(Obligation::class.java)) this `fails with` "all inputs are in the normal state" @@ -554,7 +554,7 @@ class ObligationTests { attachments(Obligation.PROGRAM_ID) input("Alice's $1,000,000 obligation to Bob") input("Alice's $1,000,000") - output(Obligation.PROGRAM_ID, "Bob's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB) + output(Cash.PROGRAM_ID, "Bob's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB) command(ALICE_PUBKEY, Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token))) command(ALICE_PUBKEY, Cash.Commands.Move(Obligation::class.java)) attachment(attachment(cashContractBytes.inputStream())) @@ -701,10 +701,10 @@ class ObligationTests { attachments(Obligation.PROGRAM_ID) input(Obligation.PROGRAM_ID, inState) input(Obligation.PROGRAM_ID, - inState.copy( - quantity = 15000, - template = megaCorpPoundSettlement, - beneficiary = AnonymousParty(BOB_PUBKEY))) + inState.copy( + quantity = 15000, + template = megaCorpPoundSettlement, + beneficiary = AnonymousParty(BOB_PUBKEY))) output(Obligation.PROGRAM_ID, outState.copy(quantity = 115000)) command(MINI_CORP_PUBKEY, Obligation.Commands.Move()) this `fails with` "the amounts balance" @@ -962,4 +962,4 @@ class ObligationTests { get() = Obligation.Terms(NonEmptySet.of(cashContractBytes.sha256() as SecureHash), NonEmptySet.of(this), TEST_TX_TIME) private val Amount>.OBLIGATION: Obligation.State get() = Obligation.State(Obligation.Lifecycle.NORMAL, DUMMY_OBLIGATION_ISSUER, token.OBLIGATION_DEF, quantity, NULL_PARTY) -} +} \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt index f7b7f49cfe..c0ffa1403d 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt @@ -44,7 +44,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 { @@ -80,7 +80,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() + doReturn(attachmentStorage).whenever(it).attachments + val attachment = rigorousMock() + 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()).whenever(attachment).signers } @Test diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt index 01522caef5..3f9e9bb8f5 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt @@ -2,6 +2,7 @@ package net.corda.node.internal.cordapp import io.github.classgraph.ClassGraph import io.github.classgraph.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 @@ -188,7 +189,11 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: } private fun findContractClassNames(scanResult: RestrictedScanResult): List { - 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 { diff --git a/node/src/test/kotlin/net/corda/node/services/schema/PersistentStateServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/schema/PersistentStateServiceTests.kt index 9fac402e43..cf2a5f9158 100644 --- a/node/src/test/kotlin/net/corda/node/services/schema/PersistentStateServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/schema/PersistentStateServiceTests.kt @@ -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 @@ -67,7 +64,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() diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt index 4815ace9d8..a9b27b5b51 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt @@ -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)) } } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 7f55504ee9..70dbfe79fe 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -7,9 +7,9 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.packageName import net.corda.core.node.services.* +import net.corda.core.node.services.Vault.ConstraintInfo.Type.* import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.QueryCriteria.* -import net.corda.core.node.services.Vault.ConstraintInfo.Type.* import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.* @@ -481,7 +481,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties { vaultFiller.fillWithSomeTestLinearStates(1, constraint = AlwaysAcceptAttachmentConstraint).states.first().state.constraint vaultFiller.fillWithSomeTestLinearStates(1, constraint = WhitelistedByZoneAttachmentConstraint).states.first().state.constraint // hash constraint - val linearStateHash = vaultFiller.fillWithSomeTestLinearStates(1, constraint = HashAttachmentConstraint(SecureHash.randomSHA256())) + val linearStateHash = vaultFiller.fillWithSomeTestLinearStates(1, constraint = AutomaticPlaceholderConstraint) // defaults to the HashConstraint val constraintHash = linearStateHash.states.first().state.constraint as HashAttachmentConstraint // signature constraint (single key) val linearStateSignature = vaultFiller.fillWithSomeTestLinearStates(1, constraint = SignatureAttachmentConstraint(alice.publicKey)) @@ -504,7 +504,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties { val constraintTypeCriteria2 = VaultQueryCriteria(constraintTypes = setOf(HASH)) val constraintResults2 = vaultService.queryBy(constraintTypeCriteria2) assertThat(constraintResults2.states).hasSize(2) - assertThat(constraintResults2.states.map { it.state.constraint }).containsOnlyOnce(constraintHash) + assertThat(constraintResults2.states.map { it.state.constraint }.toSet()).isEqualTo(setOf(constraintHash)) // search for states with [Vault.ConstraintInfo.Type] either HASH or CZ_WHITELISED // DOCSTART VaultQueryExample30 @@ -536,7 +536,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties { val alwaysAcceptConstraint = vaultFiller.fillWithSomeTestLinearStates(1, constraint = AlwaysAcceptAttachmentConstraint).states.first().state.constraint vaultFiller.fillWithSomeTestLinearStates(1, constraint = WhitelistedByZoneAttachmentConstraint) // hash constraint - val linearStateHash = vaultFiller.fillWithSomeTestLinearStates(1, constraint = HashAttachmentConstraint(SecureHash.randomSHA256())) + val linearStateHash = vaultFiller.fillWithSomeTestLinearStates(1, constraint = AutomaticPlaceholderConstraint) // defaults to the hash constraint. val constraintHash = linearStateHash.states.first().state.constraint as HashAttachmentConstraint // signature constraint (single key) val linearStateSignature = vaultFiller.fillWithSomeTestLinearStates(1, constraint = SignatureAttachmentConstraint(alice.publicKey)) @@ -559,7 +559,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties { // search for states for a specific HashAttachmentConstraint val constraintsCriteria2 = VaultQueryCriteria(constraints = setOf(Vault.ConstraintInfo(constraintHash))) val constraintResults2 = vaultService.queryBy(constraintsCriteria2) - assertThat(constraintResults2.states).hasSize(1) + assertThat(constraintResults2.states).hasSize(2) assertThat(constraintResults2.states.first().state.constraint).isEqualTo(constraintHash) // search for states with a specific SignatureAttachmentConstraint constraint @@ -574,7 +574,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties { Vault.ConstraintInfo(constraintSignatureCompositeKey), Vault.ConstraintInfo(constraintHash))) val constraintResults = vaultService.queryBy(constraintCriteria) // DOCEND VaultQueryExample31 - assertThat(constraintResults.states).hasSize(3) + assertThat(constraintResults.states).hasSize(4) assertThat(constraintResults.states.map { it.state.constraint }).containsAll(listOf(constraintHash, constraintSignature, constraintSignatureCompositeKey)) // exercise enriched query @@ -1474,8 +1474,9 @@ abstract class VaultQueryTestsBase : VaultQueryParties { vaultFiller.fillWithSomeTestLinearStates(1, linearNumber = it.toLong(), linearString = it.toString()) } val max = builder { DummyLinearStateSchemaV1.PersistentDummyLinearState::linearTimestamp.max( - groupByColumns = listOf(DummyLinearStateSchemaV1.PersistentDummyLinearState::linearNumber) - ) + groupByColumns = listOf(DummyLinearStateSchemaV1.PersistentDummyLinearState::linearNumber), + orderBy = Sort.Direction.ASC + ) } val maxCriteria = VaultCustomQueryCriteria(max) val pageSpec = PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE) @@ -2336,7 +2337,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties { database.transaction { vaultFiller.fillWithSomeTestLinearStates(1, constraint = WhitelistedByZoneAttachmentConstraint) vaultFiller.fillWithSomeTestLinearStates(1, constraint = SignatureAttachmentConstraint(alice.publicKey)) - vaultFiller.fillWithSomeTestLinearStates(1, constraint = HashAttachmentConstraint( SecureHash.randomSHA256())) + vaultFiller.fillWithSomeTestLinearStates(1, constraint = AutomaticPlaceholderConstraint) // this defaults to the HashConstraint vaultFiller.fillWithSomeTestLinearStates(1, constraint = AlwaysAcceptAttachmentConstraint) // Base criteria diff --git a/samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/contract/IRS.kt b/samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/contract/IRS.kt index ba307993e3..fb999ba542 100644 --- a/samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/contract/IRS.kt +++ b/samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/contract/IRS.kt @@ -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)) } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt index a358427b08..bb3b451aad 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt @@ -1,11 +1,13 @@ package net.corda.testing.contracts +import net.corda.core.contracts.BelongsToContract import net.corda.core.contracts.ContractState import net.corda.core.identity.AbstractParty /** * Dummy state for use in testing. Not part of any contract, not even the [DummyContract]. */ +@BelongsToContract(DummyContract::class) data class DummyState @JvmOverloads constructor ( /** Some information that the state represents for test purposes. **/ val magicNumber: Int = 0, diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt index e174e0545b..c98a026365 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt @@ -154,6 +154,7 @@ data class TestTransactionDSLInterpreter private constructor( override fun _attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List){ attachment((services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage, attachmentId, signers)) } + } data class TestLedgerDSLInterpreter private constructor( diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt index 00eac984b2..e47a8037e5 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt @@ -4,6 +4,7 @@ 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 @@ -147,37 +148,37 @@ class TransactionDSL(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. diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt index 1494a38715..a6847b8fa2 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt @@ -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 @@ -12,6 +13,7 @@ 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( cordappLoader: CordappLoader, diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt index eb46690305..baba1b0cfe 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt @@ -103,7 +103,7 @@ class VaultFiller @JvmOverloads constructor( linearNumber: Long = 0L, linearBoolean: Boolean = false, linearTimestamp: Instant = now(), - constraint: AttachmentConstraint = AutomaticHashConstraint): Vault { + constraint: AttachmentConstraint = AutomaticPlaceholderConstraint): Vault { val myKey: PublicKey = services.myInfo.chooseIdentity().owningKey val me = AnonymousParty(myKey) val issuerKey = defaultNotary.keyPair @@ -134,12 +134,12 @@ class VaultFiller @JvmOverloads constructor( @JvmOverloads fun fillWithSomeTestLinearAndDealStates(numberToCreate: Int, - externalId: String? = null, - participants: List = emptyList(), - linearString: String = "", - linearNumber: Long = 0L, - linearBoolean: Boolean = false, - linearTimestamp: Instant = now()): Vault { + externalId: String? = null, + participants: List = emptyList(), + linearString: String = "", + linearNumber: Long = 0L, + linearBoolean: Boolean = false, + linearTimestamp: Instant = now()): Vault { val myKey: PublicKey = services.myInfo.chooseIdentity().owningKey val me = AnonymousParty(myKey) val issuerKey = defaultNotary.keyPair diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt index 35a59c37b2..727ca1fd48 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt @@ -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.JarSignatureCollector import net.corda.core.internal.UNKNOWN_UPLOADER