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:
tudor.malene@gmail.com 2018-11-14 11:50:19 +00:00
commit 1e27f0cbe0
37 changed files with 1082 additions and 271 deletions

View File

@ -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)

View File

@ -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)),

View File

@ -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.")
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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.

View File

@ -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>()

View File

@ -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.

View File

@ -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. */

View File

@ -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)
}
}

View File

@ -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")
}
}

View File

@ -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
}
}

View File

@ -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()

View File

@ -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()

View File

@ -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`() {

View File

@ -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)

View File

@ -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 {

View File

@ -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())

View File

@ -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
-------

View File

@ -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();
});

View File

@ -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()
}

View File

@ -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))
}
}

View File

@ -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>>,

View File

@ -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))
)

View File

@ -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)
}
}

View File

@ -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

View File

@ -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> {

View File

@ -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()

View File

@ -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))
}
}

View File

@ -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

View File

@ -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))
}

View File

@ -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,

View File

@ -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(

View File

@ -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.

View File

@ -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,

View File

@ -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

View File

@ -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