mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
Merge remote-tracking branch 'private/master' into feature/tudor_constraints
# Conflicts: # core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt # core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt # core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt # node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt
This commit is contained in:
commit
1e27f0cbe0
@ -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<? extends net.corda.core.contracts.Contract> value()
|
||||
##
|
||||
@CordaSerializable
|
||||
public final class net.corda.core.contracts.Command extends java.lang.Object
|
||||
public <init>(T, java.security.PublicKey)
|
||||
|
@ -229,6 +229,15 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
|
||||
fun `SignedTransaction (WireTransaction)`() {
|
||||
val attachmentId = SecureHash.randomSHA256()
|
||||
doReturn(attachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID)
|
||||
val attachmentStorage = rigorousMock<AttachmentStorage>()
|
||||
doReturn(attachmentStorage).whenever(services).attachments
|
||||
val attachment = rigorousMock<ContractAttachment>()
|
||||
doReturn(attachment).whenever(attachmentStorage).openAttachment(attachmentId)
|
||||
doReturn(attachmentId).whenever(attachment).id
|
||||
doReturn(emptyList<Party>()).whenever(attachment).signers
|
||||
doReturn(setOf(DummyContract.PROGRAM_ID)).whenever(attachment).allContracts
|
||||
doReturn("app").whenever(attachment).uploader
|
||||
|
||||
val wtx = TransactionBuilder(
|
||||
notary = DUMMY_NOTARY,
|
||||
inputs = mutableListOf(StateRef(SecureHash.randomSHA256(), 1)),
|
||||
|
@ -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.")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<out Contract>)
|
||||
/**
|
||||
* 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
|
||||
}
|
@ -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<out T : ContractState> @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<out T : ContractState> @JvmOverloads constructor(
|
||||
/**
|
||||
* A validator for the contract attachments on the transaction.
|
||||
*/
|
||||
val constraint: AttachmentConstraint = AutomaticHashConstraint)
|
||||
val constraint: AttachmentConstraint = AutomaticPlaceholderConstraint) {
|
||||
|
||||
private companion object {
|
||||
val logger = loggerFor<TransactionState<*>>()
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -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.
|
||||
|
@ -119,7 +119,7 @@ open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any)
|
||||
/**
|
||||
* This is a wildcard payload to be used by the invoker of the [DataVendingFlow] to allow unlimited access to its vault.
|
||||
*
|
||||
* Todo Fails with a serialization exception if it is not a list. Why?
|
||||
* TODO Fails with a serialization exception if it is not a list. Why?
|
||||
*/
|
||||
@CordaSerializable
|
||||
object RetrieveAnyTransactionPayload : ArrayList<Any>()
|
@ -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<LedgerTransaction>()
|
||||
private fun contractClassFor(className: ContractClassName, classLoader: ClassLoader?): Try<Class<out Contract>> {
|
||||
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<ContractClassName, Try<Class<out Contract>>> = (inputs.map { it.state } + outputs)
|
||||
.map { it.contract to stateToContractClass(it) }.toMap()
|
||||
|
||||
val inputStates: List<ContractState> get() = inputs.map { it.state.data }
|
||||
val referenceStates: List<ContractState> get() = references.map { it.state.data }
|
||||
|
||||
@ -96,8 +95,11 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
fun verify() {
|
||||
val contractAttachmentsByContract: Map<ContractClassName, ContractAttachment> = 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<ContractState>) {
|
||||
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<ContractClassName, ContractAttachment>) {
|
||||
// First check that the constraints are valid.
|
||||
for (state in allStates) {
|
||||
checkConstraintValidity(state)
|
||||
}
|
||||
|
||||
// Group the inputs and outputs by contract, and for each contract verify the constraints propagation logic.
|
||||
// This is not required for reference states as there is nothing to propagate.
|
||||
val inputContractGroups = inputs.groupBy { it.state.contract }
|
||||
val outputContractGroups = outputs.groupBy { it.contract }
|
||||
|
||||
for (contractClassName in (inputContractGroups.keys + outputContractGroups.keys)) {
|
||||
if (contractClassName.contractHasAutomaticConstraintPropagation()) {
|
||||
// Verify that the constraints of output states have at least the same level of restriction as the constraints of the corresponding input states.
|
||||
val inputConstraints = inputContractGroups[contractClassName]?.map { it.state.constraint }?.toSet()
|
||||
val outputConstraints = outputContractGroups[contractClassName]?.map { it.constraint }?.toSet()
|
||||
outputConstraints?.forEach { outputConstraint ->
|
||||
inputConstraints?.forEach { inputConstraint ->
|
||||
if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, contractAttachmentsByContract[contractClassName]!! ))) {
|
||||
throw TransactionVerificationException.ConstraintPropagationRejection(id, contractClassName, inputConstraint, outputConstraint)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
contractClassName.warnContractWithoutConstraintPropagation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that all contract constraints are passing before running any contract code.
|
||||
*
|
||||
* 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<ContractAttachment>()
|
||||
(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<ContractClassName, ContractAttachment>) {
|
||||
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<Contract>(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<ContractState>): Class<out Contract> =
|
||||
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<out Contract>): 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.
|
||||
|
@ -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<StateRef> = arrayListOf(),
|
||||
protected val serviceHub: ServiceHub? = (Strand.currentStrand() as? FlowStateMachine<*>)?.serviceHub
|
||||
) {
|
||||
|
||||
private companion object {
|
||||
val logger = loggerFor<TransactionBuilder>()
|
||||
}
|
||||
|
||||
private val inputsWithTransactionState = arrayListOf<TransactionState<ContractState>>()
|
||||
private val referencesWithTransactionState = arrayListOf<TransactionState<ContractState>>()
|
||||
|
||||
@ -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<SecureHash>, resolvedOutputs: List<TransactionState<ContractState>>) = selectContractAttachmentsAndOutputStateConstraints(services, serializationContext)
|
||||
|
||||
// Final sanity check that all states have the correct constraints.
|
||||
for (state in (inputsWithTransactionState + resolvedOutputs)) {
|
||||
checkConstraintValidity(state)
|
||||
}
|
||||
|
||||
return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
|
||||
@ -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<ContractState>.withConstraint(newConstraint: AttachmentConstraint) =
|
||||
if (newConstraint == constraint) this else copy(constraint = newConstraint)
|
||||
/**
|
||||
* This method is responsible for selecting the contract versions to be used for the current transaction and resolve the output state [AutomaticPlaceholderConstraint]s.
|
||||
* The contract attachments are used to create a deterministic Classloader to deserialise the transaction and to run the contract verification.
|
||||
*
|
||||
* The selection logic depends on the Attachment Constraints of the input, output and reference states, also on the explicitly set attachments.
|
||||
* TODO also on the versions of the attachments of the transactions generating the input states. ( after we add versioning)
|
||||
*/
|
||||
private fun selectContractAttachmentsAndOutputStateConstraints(
|
||||
services: ServicesForResolution, serializationContext: SerializationContext?): Pair<Collection<SecureHash>, List<TransactionState<ContractState>>> {
|
||||
|
||||
private fun makeAttachmentConstraint(services: ServicesForResolution, state: TransactionState<ContractState>): AttachmentConstraint {
|
||||
val attachmentId = services.cordappProvider.getContractAttachmentID(state.contract)
|
||||
?: throw MissingContractAttachments(listOf(state))
|
||||
|
||||
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<Pair<ContractClassName, SecureHash>> = 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<ContractClassName, SecureHash> = explicitAttachmentContracts.toMap()
|
||||
|
||||
val inputContractGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = inputsWithTransactionState.groupBy { it.contract }
|
||||
val outputContractGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = outputs.groupBy { it.contract }
|
||||
|
||||
val allContracts: Set<ContractClassName> = inputContractGroups.keys + outputContractGroups.keys
|
||||
|
||||
// Handle reference states.
|
||||
// Filter out all contracts that might have been already used by 'normal' input or output states.
|
||||
val referenceStateGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = referencesWithTransactionState.groupBy { it.contract }
|
||||
val refStateContractAttachments: List<AttachmentId> = referenceStateGroups
|
||||
.filterNot { it.key in allContracts }
|
||||
.map { refStateEntry ->
|
||||
selectAttachmentThatSatisfiesConstraints(true, refStateEntry.key, refStateEntry.value.map { it.constraint }, services)
|
||||
}
|
||||
|
||||
// For each contract, resolve the AutomaticPlaceholderConstraint, and select the attachment.
|
||||
val contractAttachmentsAndResolvedOutputStates: List<Pair<AttachmentId, List<TransactionState<ContractState>>?>> = allContracts.toSet().map { ctr ->
|
||||
handleContract(ctr, inputContractGroups[ctr], outputContractGroups[ctr], explicitAttachmentContractsMap[ctr], serializationContext, services)
|
||||
}
|
||||
|
||||
val resolvedStates: List<TransactionState<ContractState>> = contractAttachmentsAndResolvedOutputStates.mapNotNull { it.second }.flatten()
|
||||
|
||||
// The output states need to preserve the order in which they were added.
|
||||
val resolvedOutputStatesInTheOriginalOrder: List<TransactionState<ContractState>> = outputStates().map { os -> resolvedStates.find { rs -> rs.data == os.data && rs.encumbrance == os.encumbrance}!! }
|
||||
|
||||
val attachments: Collection<AttachmentId> = contractAttachmentsAndResolvedOutputStates.map { it.first } + refStateContractAttachments
|
||||
|
||||
return Pair(attachments, resolvedOutputStatesInTheOriginalOrder)
|
||||
}
|
||||
|
||||
private val automaticConstraints = setOf(AutomaticPlaceholderConstraint, AutomaticHashConstraint)
|
||||
|
||||
/**
|
||||
* Selects an attachment and resolves the constraints for the output states with [AutomaticPlaceholderConstraint].
|
||||
*
|
||||
* This is the place where the complex logic of the upgradability of contracts and constraint propagation is handled.
|
||||
*
|
||||
* * For contracts that *are not* annotated with @[NoConstraintPropagation], this will attempt to determine a constraint for the output states
|
||||
* that is a valid transition from all the constraints of the input states.
|
||||
*
|
||||
* * For contracts that *are* annotated with @[NoConstraintPropagation], this enforces setting an explicit output constraint.
|
||||
*
|
||||
* * For states with the [HashAttachmentConstraint], if an attachment with that hash is installed on the current node, then it will be inherited by the output states and selected for the transaction.
|
||||
* Otherwise a [MissingContractAttachments] is thrown.
|
||||
*
|
||||
* * For input states with [WhitelistedByZoneAttachmentConstraint] or a [AlwaysAcceptAttachmentConstraint] implementations, then the currently installed cordapp version is used.
|
||||
*/
|
||||
private fun handleContract(
|
||||
contractClassName: ContractClassName,
|
||||
inputStates: List<TransactionState<ContractState>>?,
|
||||
outputStates: List<TransactionState<ContractState>>?,
|
||||
explicitContractAttachment: AttachmentId?,
|
||||
serializationContext: SerializationContext?,
|
||||
services: ServicesForResolution
|
||||
): Pair<AttachmentId, List<TransactionState<ContractState>>?> {
|
||||
val inputsAndOutputs = (inputStates ?: emptyList()) + (outputStates ?: emptyList())
|
||||
|
||||
// Determine if there are any HashConstraints that pin the version of a contract. If there are, check if we trust them.
|
||||
val hashAttachments = inputsAndOutputs
|
||||
.filter { it.constraint is HashAttachmentConstraint }
|
||||
.map { state ->
|
||||
val attachment = services.attachments.openAttachment((state.constraint as HashAttachmentConstraint).attachmentId)
|
||||
if (attachment == null || attachment !is ContractAttachment || !isUploaderTrusted(attachment.uploader)) {
|
||||
// This should never happen because these are input states that should have been validated already.
|
||||
throw MissingContractAttachments(listOf(state))
|
||||
}
|
||||
attachment
|
||||
}.toSet()
|
||||
|
||||
// Check that states with the HashConstraint don't conflict between themselves or with an explicitly set attachment.
|
||||
require(hashAttachments.size <= 1) {
|
||||
"Transaction was built with $contractClassName states with multiple HashConstraints. This is illegal, because it makes it impossible to validate with a single version of the contract code."
|
||||
}
|
||||
if (explicitContractAttachment != null && hashAttachments.singleOrNull() != null) {
|
||||
require(explicitContractAttachment == (hashAttachments.single() as ContractAttachment).attachment.id) {
|
||||
"An attachment has been explicitly set for contract $contractClassName in the transaction builder which conflicts with the HashConstraint of a state."
|
||||
}
|
||||
}
|
||||
|
||||
// This will contain the hash of the JAR that *has* to be used by this Transaction, because it is explicit. Or null if none.
|
||||
val forcedAttachmentId = explicitContractAttachment ?: hashAttachments.singleOrNull()?.id
|
||||
|
||||
fun selectAttachment() = selectAttachmentThatSatisfiesConstraints(
|
||||
false,
|
||||
contractClassName,
|
||||
inputsAndOutputs.map { it.constraint }.toSet().filterNot { it in automaticConstraints },
|
||||
services)
|
||||
|
||||
// This will contain the hash of the JAR that will be used by this Transaction.
|
||||
val selectedAttachmentId = forcedAttachmentId ?: selectAttachment()
|
||||
|
||||
val attachmentToUse = services.attachments.openAttachment(selectedAttachmentId)?.let { it as ContractAttachment }
|
||||
?: throw IllegalArgumentException("Contract attachment $selectedAttachmentId for $contractClassName is missing.")
|
||||
|
||||
// For Exit transactions (no output states) there is no need to resolve the output constraints.
|
||||
if (outputStates == null) {
|
||||
return Pair(selectedAttachmentId, null)
|
||||
}
|
||||
|
||||
// If there are no automatic constraints, there is nothing to resolve.
|
||||
if (outputStates.none { it.constraint in automaticConstraints }) {
|
||||
return Pair(selectedAttachmentId, outputStates)
|
||||
}
|
||||
|
||||
// The final step is to resolve AutomaticPlaceholderConstraint.
|
||||
val automaticConstraintPropagation = contractClassName.contractHasAutomaticConstraintPropagation(serializationContext?.deserializationClassLoader)
|
||||
|
||||
// When automaticConstraintPropagation is disabled for a contract, output states must an explicit Constraint.
|
||||
require(automaticConstraintPropagation) { "Contract $contractClassName was marked with @NoConstraintPropagation, which means the constraint of the output states has to be set explicitly." }
|
||||
|
||||
// This is the logic to determine the constraint which will replace the AutomaticPlaceholderConstraint.
|
||||
val defaultOutputConstraint = selectAttachmentConstraint(contractClassName, inputStates, attachmentToUse, services)
|
||||
|
||||
// Sanity check that the selected attachment actually passes.
|
||||
val constraintAttachment = AttachmentWithContext(attachmentToUse, contractClassName, services.networkParameters.whitelistedContractImplementations)
|
||||
require(defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) { "Selected output constraint: $defaultOutputConstraint not satisfying $selectedAttachmentId" }
|
||||
|
||||
val resolvedOutputStates = outputStates.map {
|
||||
val outputConstraint = it.constraint
|
||||
if (outputConstraint in automaticConstraints) {
|
||||
it.copy(constraint = defaultOutputConstraint)
|
||||
} else {
|
||||
// If the constraint on the output state is already set, and is not a valid transition or can't be transitioned, then fail early.
|
||||
inputStates?.forEach { input ->
|
||||
require(outputConstraint.canBeTransitionedFrom(input.constraint, attachmentToUse)) { "Output state constraint $outputConstraint cannot be transitions from ${input.constraint}" }
|
||||
}
|
||||
require(outputConstraint.isSatisfiedBy(constraintAttachment)) { "Output state constraint check fails. $outputConstraint" }
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
return Pair(selectedAttachmentId, resolvedOutputStates)
|
||||
}
|
||||
|
||||
/**
|
||||
* If there are multiple input states with different constraints then run the constraint intersection logic to determine the resulting output constraint.
|
||||
* For issuing transactions where the attachmentToUse is JarSigned, then default to the SignatureConstraint with all the signatures.
|
||||
* TODO - in the future this step can actually create a new ContractAttachment by merging 2 signed jars of the same version.
|
||||
*/
|
||||
private fun selectAttachmentConstraint(
|
||||
contractClassName: ContractClassName,
|
||||
inputStates: List<TransactionState<ContractState>>?,
|
||||
attachmentToUse: ContractAttachment,
|
||||
services: ServicesForResolution): AttachmentConstraint = when {
|
||||
inputStates != null -> attachmentConstraintsTransition(inputStates.groupBy { it.constraint }.keys, attachmentToUse)
|
||||
useWhitelistedByZoneAttachmentConstraint(contractClassName, services.networkParameters) -> WhitelistedByZoneAttachmentConstraint
|
||||
attachmentToUse.signers.isNotEmpty() -> {
|
||||
// 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<AttachmentConstraint>, attachmentToUse: ContractAttachment): AttachmentConstraint = when {
|
||||
|
||||
// Sanity check.
|
||||
constraints.isEmpty() -> throw IllegalArgumentException("Cannot transition from no constraints.")
|
||||
|
||||
// When all input states have the same constraint.
|
||||
constraints.size == 1 -> constraints.single()
|
||||
|
||||
// Fail when combining the insecure AlwaysAcceptAttachmentConstraint with something else. The size must be at least 2 at this point.
|
||||
constraints.any { it is AlwaysAcceptAttachmentConstraint } ->
|
||||
throw IllegalArgumentException("Can't mix the AlwaysAcceptAttachmentConstraint with a secure constraint in the same transaction. This can be used to hide insecure transitions.")
|
||||
|
||||
// Multiple states with Hash constraints with different hashes. This should not happen as we checked already.
|
||||
constraints.all { it is HashAttachmentConstraint } ->
|
||||
throw IllegalArgumentException("Cannot mix HashConstraints with different hashes in the same transaction.")
|
||||
|
||||
// The HashAttachmentConstraint is the strongest constraint, so it wins when mixed with anything. As long as the actual constraints pass.
|
||||
// TODO - this could change if we decide to introduce a way to gracefully migrate from the Hash Constraint to the Signature Constraint.
|
||||
constraints.any { it is HashAttachmentConstraint } -> constraints.find { it is HashAttachmentConstraint }!!
|
||||
|
||||
// TODO, we don't currently support mixing signature constraints with different signers. This will change once we introduce third party signers.
|
||||
constraints.all { it is SignatureAttachmentConstraint } ->
|
||||
throw IllegalArgumentException("Cannot mix SignatureAttachmentConstraints signed by different parties in the same transaction.")
|
||||
|
||||
// This ensures a smooth migration from the Whitelist Constraint, given that for the transaction to be valid it still has to pass both constraints.
|
||||
// The transition is possible only when the SignatureConstraint contains ALL signers from the attachment.
|
||||
constraints.any { it is SignatureAttachmentConstraint } && constraints.any { it is WhitelistedByZoneAttachmentConstraint } -> {
|
||||
val signatureConstraint = constraints.mapNotNull { it as? SignatureAttachmentConstraint }.single()
|
||||
when {
|
||||
attachmentToUse.signers.isEmpty() -> throw IllegalArgumentException("Cannot mix a state with the WhitelistedByZoneAttachmentConstraint and a state with the SignatureAttachmentConstraint, when the latest attachment is not signed. Please contact your Zone operator.")
|
||||
signatureConstraint.key.keys.containsAll(attachmentToUse.signers) -> signatureConstraint
|
||||
else -> throw IllegalArgumentException("Attempting to transition a WhitelistedByZoneAttachmentConstraint state backed by an attachment signed by multiple parties to a weaker SignatureConstraint that does not require all those signatures. Please contact your Zone operator.")
|
||||
}
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("Unexpected constraints $constraints.")
|
||||
}
|
||||
|
||||
private fun makeSignatureAttachmentConstraint(attachmentSigners: List<PublicKey>) =
|
||||
SignatureAttachmentConstraint(CompositeKey.Builder().addKeys(attachmentSigners.map { it }).build())
|
||||
|
||||
private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters) =
|
||||
contractClassName in networkParameters.whitelistedContractImplementations.keys
|
||||
|
||||
/**
|
||||
* The attachments added to the current transaction contain only the hashes of the current cordapps.
|
||||
* NOT the hashes of the cordapps that were used when the input states were created ( in case they changed in the meantime)
|
||||
* TODO - review this logic
|
||||
* This method should only be called for upgradeable contracts.
|
||||
*
|
||||
* For now we use the currently installed CorDapp version.
|
||||
* TODO - When the SignatureConstraint and contract version logic is in, this will need to query the attachments table and find the latest one that satisfies all constraints.
|
||||
* TODO - select a version of the contract that is no older than the one from the previous transactions.
|
||||
*/
|
||||
private fun makeContractAttachments(cordappProvider: CordappProvider): List<AttachmentId> {
|
||||
// Reference inputs not included as it is not necessary to verify them.
|
||||
return (inputsWithTransactionState + outputs).map { state ->
|
||||
cordappProvider.getContractAttachmentID(state.contract)
|
||||
?: throw MissingContractAttachments(listOf(state))
|
||||
}.distinct()
|
||||
private fun selectAttachmentThatSatisfiesConstraints(isReference: Boolean, contractClassName: String, constraints: List<AttachmentConstraint>, services: ServicesForResolution): AttachmentId {
|
||||
require(constraints.none { it in automaticConstraints })
|
||||
require(isReference || constraints.none { it is HashAttachmentConstraint })
|
||||
return services.cordappProvider.getContractAttachmentID(contractClassName)!!
|
||||
}
|
||||
|
||||
private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters) = contractClassName in networkParameters.whitelistedContractImplementations.keys
|
||||
|
||||
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class)
|
||||
fun toLedgerTransaction(services: ServiceHub) = toWireTransaction(services).toLedgerTransaction(services)
|
||||
|
||||
@ -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. */
|
||||
|
@ -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 <V> Future<V>.getOrThrow(timeout: Duration? = null): V = try {
|
||||
* Size is very cheap as it doesn't call [transform].
|
||||
*/
|
||||
fun <T, U> List<T>.lazyMapped(transform: (T, Int) -> U): List<U> = LazyMappedList(this, transform)
|
||||
|
||||
private const val MAX_SIZE = 100
|
||||
private val warnings = Collections.newSetFromMap(object : LinkedHashMap<String, Boolean>() {
|
||||
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, Boolean>?) = size > MAX_SIZE
|
||||
})
|
||||
|
||||
/**
|
||||
* Utility to help log a warning message only once.
|
||||
* It 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)
|
||||
}
|
||||
}
|
@ -1,8 +1,13 @@
|
||||
package net.corda.core
|
||||
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.times
|
||||
import com.nhaarman.mockito_kotlin.verify
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.warnOnce
|
||||
import org.assertj.core.api.Assertions.*
|
||||
import org.junit.Test
|
||||
import org.slf4j.Logger
|
||||
import rx.subjects.PublishSubject
|
||||
import java.util.*
|
||||
import java.util.concurrent.CancellationException
|
||||
@ -58,4 +63,24 @@ class UtilsTest {
|
||||
future.get()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `warnOnce works, but the backing cache grows only to a maximum size`() {
|
||||
val MAX_SIZE = 100
|
||||
|
||||
val logger = mock<Logger>()
|
||||
logger.warnOnce("a")
|
||||
logger.warnOnce("b")
|
||||
logger.warnOnce("b")
|
||||
|
||||
// This should cause the eviction of "a".
|
||||
(1..MAX_SIZE).forEach { logger.warnOnce("$it") }
|
||||
logger.warnOnce("a")
|
||||
|
||||
// "a" should be logged twice because it was evicted.
|
||||
verify(logger, times(2)).warn("a")
|
||||
|
||||
// "b" should be logged only once because there was no eviction.
|
||||
verify(logger, times(1)).warn("b")
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,277 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.MissingContractAttachments
|
||||
import net.corda.finance.POUNDS
|
||||
import net.corda.finance.`issued by`
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.node.services.api.IdentityServiceInternal
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.core.TestIdentity
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.ledger
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ConstraintsPropagationTests {
|
||||
|
||||
private companion object {
|
||||
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
||||
val ALICE = TestIdentity(CordaX500Name("ALICE", "London", "GB"))
|
||||
val ALICE_PARTY get() = ALICE.party
|
||||
val ALICE_PUBKEY get() = ALICE.publicKey
|
||||
val BOB = TestIdentity(CordaX500Name("BOB", "London", "GB"))
|
||||
val BOB_PARTY get() = BOB.party
|
||||
val BOB_PUBKEY get() = BOB.publicKey
|
||||
val noPropagationContractClassName = "net.corda.core.contracts.NoPropagationContract"
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
|
||||
private val ledgerServices = MockServices(
|
||||
cordappPackages = listOf("net.corda.finance.contracts.asset"),
|
||||
initialIdentity = ALICE,
|
||||
identityService = rigorousMock<IdentityServiceInternal>().also {
|
||||
doReturn(ALICE_PARTY).whenever(it).partyFromKey(ALICE_PUBKEY)
|
||||
doReturn(BOB_PARTY).whenever(it).partyFromKey(BOB_PUBKEY)
|
||||
},
|
||||
networkParameters = testNetworkParameters(minimumPlatformVersion = 4)
|
||||
.copy(whitelistedContractImplementations = mapOf(
|
||||
Cash.PROGRAM_ID to listOf(SecureHash.zeroHash, SecureHash.allOnesHash),
|
||||
noPropagationContractClassName to listOf(SecureHash.zeroHash)))
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `Happy path with the HashConstraint`() {
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.allOnesHash)
|
||||
output(Cash.PROGRAM_ID, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.allOnesHash), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Issue())
|
||||
verifies()
|
||||
}
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.allOnesHash)
|
||||
input("c1")
|
||||
output(Cash.PROGRAM_ID, "c2", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.allOnesHash), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Fail early in the TransactionBuilder when attempting to change the hash of the HashConstraint on the spending transaction`() {
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
output(Cash.PROGRAM_ID, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.zeroHash), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Issue())
|
||||
verifies()
|
||||
}
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.allOnesHash)
|
||||
input("c1")
|
||||
output(Cash.PROGRAM_ID, "c2", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.allOnesHash), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Transaction validation fails, when constraints do not propagate correctly`() {
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
output(Cash.PROGRAM_ID, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.zeroHash), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Issue())
|
||||
verifies()
|
||||
}
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
input("c1")
|
||||
output(Cash.PROGRAM_ID, "c2", DUMMY_NOTARY, null, WhitelistedByZoneAttachmentConstraint, Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
failsWith("are not propagated correctly")
|
||||
}
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
input("c1")
|
||||
output(Cash.PROGRAM_ID, "c3", DUMMY_NOTARY, null, SignatureAttachmentConstraint(ALICE_PUBKEY), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
failsWith("are not propagated correctly")
|
||||
}
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
input("c1")
|
||||
output(Cash.PROGRAM_ID, "c4", DUMMY_NOTARY, null, AlwaysAcceptAttachmentConstraint, Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
failsWith("are not propagated correctly")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `When the constraint of the output state is a valid transition from the input state, transaction validation works`() {
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
output(Cash.PROGRAM_ID, "c1", DUMMY_NOTARY, null, WhitelistedByZoneAttachmentConstraint, Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Issue())
|
||||
verifies()
|
||||
}
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
input("c1")
|
||||
output(Cash.PROGRAM_ID, "c2", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.zeroHash), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Switching from the WhitelistConstraint to the Signature Constraint is possible if the attachment satisfies both constraints, and the signature constraint inherits all jar signatures`() {
|
||||
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
output(Cash.PROGRAM_ID, "w1", DUMMY_NOTARY, null, WhitelistedByZoneAttachmentConstraint, Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Issue())
|
||||
verifies()
|
||||
}
|
||||
|
||||
// the attachment is signed
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.allOnesHash, listOf(ALICE_PARTY.owningKey))
|
||||
input("w1")
|
||||
output(Cash.PROGRAM_ID, "w2", DUMMY_NOTARY, null, SignatureAttachmentConstraint(ALICE_PUBKEY), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Switching from the WhitelistConstraint to the Signature Constraint fails if the signature constraint does not inherit all jar signatures`() {
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
output(Cash.PROGRAM_ID, "w1", DUMMY_NOTARY, null, WhitelistedByZoneAttachmentConstraint, Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), ALICE_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Issue())
|
||||
verifies()
|
||||
}
|
||||
|
||||
// the attachment is not signed
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID, SecureHash.zeroHash)
|
||||
input("w1")
|
||||
output(Cash.PROGRAM_ID, "w2", DUMMY_NOTARY, null, SignatureAttachmentConstraint(ALICE_PUBKEY), Cash.State(1000.POUNDS `issued by` ALICE_PARTY.ref(1), BOB_PARTY))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
// Note that it fails after the constraints propagation check, because the attachment is not signed.
|
||||
failsWith("are not propagated correctly")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `On contract annotated with NoConstraintPropagation there is no platform check for propagation, but the transaction builder can't use the AutomaticPlaceholderConstraint`() {
|
||||
ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
transaction {
|
||||
attachment(noPropagationContractClassName, SecureHash.zeroHash)
|
||||
output(noPropagationContractClassName, "c1", DUMMY_NOTARY, null, HashAttachmentConstraint(SecureHash.zeroHash), NoPropagationContractState())
|
||||
command(ALICE_PUBKEY, NoPropagationContract.Create())
|
||||
verifies()
|
||||
}
|
||||
transaction {
|
||||
attachment(noPropagationContractClassName, SecureHash.zeroHash)
|
||||
input("c1")
|
||||
output(noPropagationContractClassName, "c2", DUMMY_NOTARY, null, WhitelistedByZoneAttachmentConstraint, NoPropagationContractState())
|
||||
command(ALICE_PUBKEY, NoPropagationContract.Create())
|
||||
verifies()
|
||||
}
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
transaction {
|
||||
attachment(noPropagationContractClassName, SecureHash.zeroHash)
|
||||
input("c1")
|
||||
output(noPropagationContractClassName, "c3", DUMMY_NOTARY, null, AutomaticPlaceholderConstraint, NoPropagationContractState())
|
||||
command(ALICE_PUBKEY, NoPropagationContract.Create())
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Attachment canBeTransitionedFrom behaves as expected`() {
|
||||
|
||||
val attachment = mock<ContractAttachment>()
|
||||
whenever(attachment.signers).thenReturn(listOf(ALICE_PARTY.owningKey))
|
||||
|
||||
// Exhaustive positive check
|
||||
assertTrue(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment))
|
||||
assertTrue(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment))
|
||||
|
||||
assertTrue(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment))
|
||||
assertTrue(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment))
|
||||
|
||||
assertTrue(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment))
|
||||
|
||||
assertTrue(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment))
|
||||
|
||||
// Exhaustive negative check
|
||||
assertFalse(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment))
|
||||
assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment))
|
||||
assertFalse(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(AlwaysAcceptAttachmentConstraint, attachment))
|
||||
|
||||
assertFalse(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment))
|
||||
|
||||
assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment))
|
||||
assertFalse(WhitelistedByZoneAttachmentConstraint.canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment))
|
||||
|
||||
assertFalse(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment))
|
||||
assertFalse(SignatureAttachmentConstraint(BOB_PUBKEY).canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment))
|
||||
assertFalse(SignatureAttachmentConstraint(BOB_PUBKEY).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment))
|
||||
|
||||
assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment))
|
||||
assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(WhitelistedByZoneAttachmentConstraint, attachment))
|
||||
assertFalse(AlwaysAcceptAttachmentConstraint.canBeTransitionedFrom(HashAttachmentConstraint(SecureHash.randomSHA256()), attachment))
|
||||
|
||||
// Fail when encounter a AutomaticPlaceholderConstraint
|
||||
assertFailsWith<IllegalArgumentException> { HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(AutomaticPlaceholderConstraint, attachment) }
|
||||
assertFailsWith<IllegalArgumentException> { AutomaticPlaceholderConstraint.canBeTransitionedFrom(AutomaticPlaceholderConstraint, attachment) }
|
||||
}
|
||||
}
|
||||
|
||||
@BelongsToContract(NoPropagationContract::class)
|
||||
class NoPropagationContractState : ContractState {
|
||||
override val participants: List<AbstractParty>
|
||||
get() = emptyList()
|
||||
}
|
||||
|
||||
@NoConstraintPropagation
|
||||
class NoPropagationContract : Contract {
|
||||
interface Commands : CommandData
|
||||
class Create : Commands
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
//do nothing
|
||||
}
|
||||
}
|
@ -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<AbstractParty>
|
||||
get() = emptyList()
|
||||
|
@ -159,6 +159,7 @@ class ContractUpgradeFlowTest : WithContracts, WithFinality {
|
||||
|
||||
class Move : TypeOnlyCommandData()
|
||||
|
||||
@BelongsToContract(CashV2::class)
|
||||
data class State(override val amount: Amount<Issued<Currency>>, val owners: List<AbstractParty>) : FungibleAsset<Currency> {
|
||||
override val owner: AbstractParty = owners.first()
|
||||
override val exitKeys = (owners + amount.token.issuer.party).map { it.owningKey }.toSet()
|
||||
|
@ -35,11 +35,11 @@ class VaultUpdateTests {
|
||||
private val stateRef3 = StateRef(SecureHash.randomSHA256(), 3)
|
||||
private val stateRef4 = StateRef(SecureHash.randomSHA256(), 4)
|
||||
|
||||
private val stateAndRef0 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY), stateRef0)
|
||||
private val stateAndRef1 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY), stateRef1)
|
||||
private val stateAndRef2 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY), stateRef2)
|
||||
private val stateAndRef3 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY), stateRef3)
|
||||
private val stateAndRef4 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY), stateRef4)
|
||||
private val stateAndRef0 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), stateRef0)
|
||||
private val stateAndRef1 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), stateRef1)
|
||||
private val stateAndRef2 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), stateRef2)
|
||||
private val stateAndRef3 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), stateRef3)
|
||||
private val stateAndRef4 = StateAndRef(TransactionState(DummyState(), DUMMY_PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), stateRef4)
|
||||
|
||||
@Test
|
||||
fun `nothing plus nothing is nothing`() {
|
||||
|
@ -63,7 +63,7 @@ class TransactionSerializationTests {
|
||||
// It refers to a fake TX/state that we don't bother creating here.
|
||||
val depositRef = MINI_CORP.ref(1)
|
||||
val fakeStateRef = generateStateRef()
|
||||
val inputState = StateAndRef(TransactionState(TestCash.State(depositRef, 100.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY), fakeStateRef)
|
||||
val inputState = StateAndRef(TransactionState(TestCash.State(depositRef, 100.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), fakeStateRef )
|
||||
val outputState = TransactionState(TestCash.State(depositRef, 600.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY)
|
||||
val changeState = TransactionState(TestCash.State(depositRef, 400.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY)
|
||||
val megaCorpServices = MockServices(listOf("net.corda.core.serialization"), MEGA_CORP.name, rigorousMock(), MEGA_CORP_KEY)
|
||||
|
@ -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<AbstractParty> = emptyList()
|
||||
}
|
||||
|
||||
@BelongsToContract(DummyContract::class)
|
||||
private class IntTypeDummyState(val data: Int) : ContractState {
|
||||
override val participants: List<AbstractParty> = 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 {
|
||||
|
@ -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())
|
||||
|
@ -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
|
||||
-------
|
||||
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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<ICommercialPaperState>().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()
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
|
||||
|
||||
// DOCSTART 1
|
||||
/** A state representing a cash claim against some party. */
|
||||
@BelongsToContract(Cash::class)
|
||||
data class State(
|
||||
override val amount: Amount<Issued<Currency>>,
|
||||
|
||||
|
@ -501,7 +501,7 @@ class CashTests {
|
||||
|
||||
private fun makeCash(amount: Amount<Currency>, issuer: AbstractParty, depositRef: Byte = 1) =
|
||||
StateAndRef(
|
||||
TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), ourIdentity), Cash.PROGRAM_ID, dummyNotary.party),
|
||||
TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), ourIdentity), Cash.PROGRAM_ID, dummyNotary.party, constraint = AlwaysAcceptAttachmentConstraint),
|
||||
StateRef(SecureHash.randomSHA256(), Random().nextInt(32))
|
||||
)
|
||||
|
||||
|
@ -96,11 +96,11 @@ class ObligationTests {
|
||||
group: LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>
|
||||
) = 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 <reified T : ContractState> getStateAndRef(state: T, contractClassName: ContractClassName): StateAndRef<T> {
|
||||
val txState = TransactionState(state, contractClassName, DUMMY_NOTARY)
|
||||
val txState = TransactionState(state, contractClassName, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint)
|
||||
return StateAndRef(txState, StateRef(SecureHash.randomSHA256(), 0))
|
||||
|
||||
}
|
||||
@ -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<Issued<Currency>>.OBLIGATION: Obligation.State<Currency>
|
||||
get() = Obligation.State(Obligation.Lifecycle.NORMAL, DUMMY_OBLIGATION_ISSUER, token.OBLIGATION_DEF, quantity, NULL_PARTY)
|
||||
}
|
||||
}
|
@ -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<AttachmentStorage>()
|
||||
doReturn(attachmentStorage).whenever(it).attachments
|
||||
val attachment = rigorousMock<ContractAttachment>()
|
||||
doReturn(attachment).whenever(attachmentStorage).openAttachment(any())
|
||||
doReturn(it.cordappProvider.getContractAttachmentID(AttachmentDummyContract.ATTACHMENT_PROGRAM_ID)).whenever(attachment).id
|
||||
doReturn(setOf(AttachmentDummyContract.ATTACHMENT_PROGRAM_ID)).whenever(attachment).allContracts
|
||||
doReturn("app").whenever(attachment).uploader
|
||||
doReturn(emptyList<Party>()).whenever(attachment).signers
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -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<String> {
|
||||
return coreContractClasses.flatMap { scanResult.getNamesOfClassesImplementing(it) }.distinct()
|
||||
val contractClasses = coreContractClasses.flatMap { scanResult.getNamesOfClassesImplementing(it) }.distinct()
|
||||
for (contractClass in contractClasses) {
|
||||
contractClass.warnContractWithoutConstraintPropagation(appClassLoader)
|
||||
}
|
||||
return contractClasses
|
||||
}
|
||||
|
||||
private fun findPlugins(cordappJarPath: RestrictedURL): List<SerializationWhitelist> {
|
||||
|
@ -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()
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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<LinearState>(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<LinearState>(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<LinearState>(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
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -154,6 +154,7 @@ data class TestTransactionDSLInterpreter private constructor(
|
||||
override fun _attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List<PublicKey>){
|
||||
attachment((services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage, attachmentId, signers))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class TestLedgerDSLInterpreter private constructor(
|
||||
|
@ -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<out T : TransactionDSLInterpreter>(interpreter: T, private
|
||||
* Adds a labelled output to the transaction.
|
||||
*/
|
||||
fun output(contractClassName: ContractClassName, label: String, notary: Party, contractState: ContractState) =
|
||||
output(contractClassName, label, notary, null, AutomaticHashConstraint, contractState)
|
||||
output(contractClassName, label, notary, null, AutomaticPlaceholderConstraint, contractState)
|
||||
|
||||
/**
|
||||
* Adds a labelled output to the transaction.
|
||||
*/
|
||||
fun output(contractClassName: ContractClassName, label: String, encumbrance: Int, contractState: ContractState) =
|
||||
output(contractClassName, label, notary, encumbrance, AutomaticHashConstraint, contractState)
|
||||
output(contractClassName, label, notary, encumbrance, AutomaticPlaceholderConstraint, contractState)
|
||||
|
||||
/**
|
||||
* Adds a labelled output to the transaction.
|
||||
*/
|
||||
fun output(contractClassName: ContractClassName, label: String, contractState: ContractState) =
|
||||
output(contractClassName, label, notary, null, AutomaticHashConstraint, contractState)
|
||||
output(contractClassName, label, notary, null, AutomaticPlaceholderConstraint, contractState)
|
||||
|
||||
/**
|
||||
* Adds an output to the transaction.
|
||||
*/
|
||||
fun output(contractClassName: ContractClassName, notary: Party, contractState: ContractState) =
|
||||
output(contractClassName, null, notary, null, AutomaticHashConstraint, contractState)
|
||||
output(contractClassName, null, notary, null, AutomaticPlaceholderConstraint, contractState)
|
||||
|
||||
/**
|
||||
* Adds an output to the transaction.
|
||||
*/
|
||||
fun output(contractClassName: ContractClassName, encumbrance: Int, contractState: ContractState) =
|
||||
output(contractClassName, null, notary, encumbrance, AutomaticHashConstraint, contractState)
|
||||
output(contractClassName, null, notary, encumbrance, AutomaticPlaceholderConstraint, contractState)
|
||||
|
||||
/**
|
||||
* Adds an output to the transaction.
|
||||
*/
|
||||
fun output(contractClassName: ContractClassName, contractState: ContractState) =
|
||||
output(contractClassName, null, notary, null, AutomaticHashConstraint, contractState)
|
||||
output(contractClassName, null, notary, null, AutomaticPlaceholderConstraint, contractState)
|
||||
|
||||
/**
|
||||
* Adds a command to the transaction.
|
||||
|
@ -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,
|
||||
|
@ -103,7 +103,7 @@ class VaultFiller @JvmOverloads constructor(
|
||||
linearNumber: Long = 0L,
|
||||
linearBoolean: Boolean = false,
|
||||
linearTimestamp: Instant = now(),
|
||||
constraint: AttachmentConstraint = AutomaticHashConstraint): Vault<LinearState> {
|
||||
constraint: AttachmentConstraint = AutomaticPlaceholderConstraint): Vault<LinearState> {
|
||||
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<AbstractParty> = emptyList(),
|
||||
linearString: String = "",
|
||||
linearNumber: Long = 0L,
|
||||
linearBoolean: Boolean = false,
|
||||
linearTimestamp: Instant = now()): Vault<LinearState> {
|
||||
externalId: String? = null,
|
||||
participants: List<AbstractParty> = emptyList(),
|
||||
linearString: String = "",
|
||||
linearNumber: Long = 0L,
|
||||
linearBoolean: Boolean = false,
|
||||
linearTimestamp: Instant = now()): Vault<LinearState> {
|
||||
val myKey: PublicKey = services.myInfo.chooseIdentity().owningKey
|
||||
val me = AnonymousParty(myKey)
|
||||
val issuerKey = defaultNotary.keyPair
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user