ENT-2222 Constraints propagation

ENT-2222 Fix tests

ENT-2222 Fix tests

ENT-2222 Add ledger transaction verification logic

ENT-2222 Fixed IRS test

ENT-2222 Fixed IRS test

ENT-2222 Fixed unit test

ENT-2222 Better kdocs

ENT-2222 Support for reference states

ENT-2222 Fix support for reference states

ENT-2222 Revert wrong change

ENT-2222 Fix Kdoc

ENT-2222 Fix Kdoc

ENT-2222 Better docs

ENT-2222 Address code review comments

ENT-2222 Fix test

ENT-2222 Fix rebase

ENT-2222 Add documentation around constraint propagation

ENT-2222 Add tests for contract propagation

ENT-2222 Add Signature Constraints propagation - first draft

ENT-2222 fix tests

ENT-2222 more tests

ENT-2222 unseal the TransactionVerificationException

ENT-2222 unseal the TransactionVerificationException

ENT-2222 more docs

ENT-2222 address code review comments

ENT-2222 address code review comments

ENT-2222 re-implement transition logic

ENT-2222 better comments and checks

ENT-2222 Fix tests

ENT-2222 merge fixes
This commit is contained in:
Tudor Malene 2018-08-15 18:38:35 +01:00 committed by tudor.malene@gmail.com
parent 7902d758bd
commit f96a59932c
28 changed files with 925 additions and 195 deletions

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

@ -3,20 +3,80 @@ package net.corda.core.contracts
import net.corda.core.DoNotImplement
import net.corda.core.KeepForDJVM
import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint.isSatisfiedBy
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.isFulfilledBy
import net.corda.core.crypto.keys
import net.corda.core.internal.AttachmentWithContext
import net.corda.core.internal.isUploaderTrusted
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.warnOnce
import org.slf4j.LoggerFactory
import java.lang.annotation.Inherited
import java.security.PublicKey
/** Constrain which contract-code-containing attachment can be used with a [ContractState]. */
/**
* This annotation should only be added to [Contract] classes.
* If the annotation is present, then we assume that [Contract.verify] will ensure that the output states have an acceptable constraint.
* If the annotation is missing, then the default - secure - constraint propagation logic is enforced by the platform.
*/
@Target(AnnotationTarget.CLASS)
@Inherited
annotation class NoConstraintPropagation
/**
* Constrain which contract-code-containing attachment can be used with a [Contract].
* */
@CordaSerializable
@DoNotImplement
interface AttachmentConstraint {
/** Returns whether the given contract attachment can be used with the [ContractState] associated with this constraint object. */
fun isSatisfiedBy(attachment: Attachment): Boolean
/**
* This method will be used in conjunction with [NoConstraintPropagation]. It is run during transaction verification when the contract is not annotated with [NoConstraintPropagation].
* When constraints propagation is enabled, constraints set on output states need to follow certain rules with regards to constraints of input states.
*
* Rules:
* * It is allowed for output states to inherit the exact same constraint as the input states.
* * The [AlwaysAcceptAttachmentConstraint] is not allowed to transition to a different constraint, as that could be used to hide malicious behaviour.
* * Nothing can be migrated from the [HashAttachmentConstraint] except a [HashAttachmentConstraint] with the same hash.
* * Anything (except the [AlwaysAcceptAttachmentConstraint]) can be transitioned to a [HashAttachmentConstraint].
* * You can transition from the [WhitelistedByZoneAttachmentConstraint] to the [SignatureAttachmentConstraint] only if all signers of the JAR are required to sign in the future.
*
* TODO - SignatureConstraint third party signers.
*/
fun canBeTransitionedFrom(input: AttachmentConstraint, attachment: ContractAttachment): Boolean {
val output = this
return when {
// These branches should not happen, as this has been already checked.
input is AutomaticPlaceholderConstraint || output is AutomaticPlaceholderConstraint -> throw IllegalArgumentException("Illegal constraint: AutomaticPlaceholderConstraint.")
input is AutomaticHashConstraint || output is AutomaticHashConstraint -> throw IllegalArgumentException("Illegal constraint: AutomaticHashConstraint.")
// Transition to the same constraint.
input == output -> true
// You can't transition from the AlwaysAcceptAttachmentConstraint to anything else, as it could hide something illegal.
input is AlwaysAcceptAttachmentConstraint && output !is AlwaysAcceptAttachmentConstraint -> false
// Nothing can be migrated from the HashConstraint except a HashConstraint with the same Hash. (This check is redundant, but added for clarity)
// TODO - this might change if we decide to allow migration to the SignatureConstraint.
input is HashAttachmentConstraint && output is HashAttachmentConstraint -> input == output
input is HashAttachmentConstraint && output !is HashAttachmentConstraint -> false
// Anything (except the AlwaysAcceptAttachmentConstraint) can be transformed to a HashAttachmentConstraint.
input !is HashAttachmentConstraint && output is HashAttachmentConstraint -> true
// The SignatureAttachmentConstraint allows migration from a Signature constraint with the same key.
// TODO - we don't support currently third party signers. When we do, the output key will have to be stronger then the input key.
input is SignatureAttachmentConstraint && output is SignatureAttachmentConstraint -> input.key == output.key
// You can transition from the WhitelistConstraint to the SignatureConstraint only if all signers of the JAR are required to sign in the future.
input is WhitelistedByZoneAttachmentConstraint && output is SignatureAttachmentConstraint ->
attachment.signers.isNotEmpty() && output.key.keys.containsAll(attachment.signers)
else -> false
}
}
}
/** An [AttachmentConstraint] where [isSatisfiedBy] always returns true. */
@ -48,26 +108,64 @@ data class HashAttachmentConstraint(val attachmentId: SecureHash) : AttachmentCo
object WhitelistedByZoneAttachmentConstraint : AttachmentConstraint {
override fun isSatisfiedBy(attachment: Attachment): Boolean {
return if (attachment is AttachmentWithContext) {
val whitelist = attachment.whitelistedContractImplementations ?: throw IllegalStateException("Unable to verify WhitelistedByZoneAttachmentConstraint - whitelist not specified")
val whitelist = attachment.whitelistedContractImplementations
?: throw IllegalStateException("Unable to verify WhitelistedByZoneAttachmentConstraint - whitelist not specified")
attachment.id in (whitelist[attachment.stateContract] ?: emptyList())
} else false
}
}
/**
* This [AttachmentConstraint] is a convenience class that will be automatically resolved to a [HashAttachmentConstraint].
* The resolution occurs in [TransactionBuilder.toWireTransaction] and uses the [TransactionState.contract] value
* to find a corresponding loaded [Cordapp] that contains such a contract, and then uses that [Cordapp] as the
* [Attachment].
*
* If, for any reason, this class is not automatically resolved the default implementation is to fail, because the
* intent of this class is that it should be replaced by a correct [HashAttachmentConstraint] and verify against an
* actual [Attachment].
*/
@KeepForDJVM
@Deprecated("The name is no longer valid as multiple constraints were added.", replaceWith = ReplaceWith("AutomaticPlaceholderConstraint"), level = DeprecationLevel.WARNING)
object AutomaticHashConstraint : AttachmentConstraint {
override fun isSatisfiedBy(attachment: Attachment): Boolean {
throw UnsupportedOperationException("Contracts cannot be satisfied by an AutomaticHashConstraint placeholder")
throw UnsupportedOperationException("Contracts cannot be satisfied by an AutomaticHashConstraint placeholder.")
}
}
/**
* This [AttachmentConstraint] is a convenience class that acts as a placeholder and will be automatically resolved by the platform when set on an output state.
* It is the default constraint of all output states.
*
* The resolution occurs in [TransactionBuilder.toWireTransaction] and is based on the input states and the attachments.
* If the [Contract] was not annotated with [NoConstraintPropagation], then the platform will ensure the correct constraint propagation.
*/
@KeepForDJVM
object AutomaticPlaceholderConstraint : AttachmentConstraint {
override fun isSatisfiedBy(attachment: Attachment): Boolean {
throw UnsupportedOperationException("Contracts cannot be satisfied by an AutomaticPlaceholderConstraint placeholder.")
}
}
private val logger = LoggerFactory.getLogger(AttachmentConstraint::class.java)
private val validConstraints = setOf(
AlwaysAcceptAttachmentConstraint::class,
HashAttachmentConstraint::class,
WhitelistedByZoneAttachmentConstraint::class,
SignatureAttachmentConstraint::class)
/**
* Fails if the constraint is not of a known type.
* Only the Corda core is allowed to implement the [AttachmentConstraint] interface.
*/
internal fun checkConstraintValidity(state: TransactionState<*>) {
require(state.constraint::class in validConstraints) { "Found state ${state.contract} with an illegal constraint: ${state.constraint}" }
if (state.constraint is AlwaysAcceptAttachmentConstraint) {
logger.warnOnce("Found state ${state.contract} that is constrained by the insecure: AlwaysAcceptAttachmentConstraint.")
}
}
/**
* Check for the [NoConstraintPropagation] annotation on the contractClassName.
* If it's present it means that the automatic secure core behaviour is not applied, and it's up to the contract developer to enforce a secure propagation logic.
*/
internal fun ContractClassName.contractHasAutomaticConstraintPropagation(classLoader: ClassLoader? = null) =
(classLoader ?: NoConstraintPropagation::class.java.classLoader)
.loadClass(this).getAnnotation(NoConstraintPropagation::class.java) == null
fun ContractClassName.warnContractWithoutConstraintPropagation(classLoader: ClassLoader? = null) {
if (!this.contractHasAutomaticConstraintPropagation(classLoader)) {
logger.warnOnce("Found contract $this with automatic constraint propagation disabled.")
}
}

View File

@ -58,7 +58,8 @@ data class TransactionState<out T : ContractState> @JvmOverloads constructor(
/**
* A validator for the contract attachments on the transaction.
*/
val constraint: AttachmentConstraint = AutomaticHashConstraint) {
val constraint: AttachmentConstraint = AutomaticPlaceholderConstraint) {
private companion object {
val logger = loggerFor<TransactionState<*>>()
}

View File

@ -36,7 +36,7 @@ class AttachmentResolutionException(val hash: SecureHash) : FlowException("Attac
*/
@Suppress("MemberVisibilityCanBePrivate")
@CordaSerializable
sealed class TransactionVerificationException(val txId: SecureHash, message: String, cause: Throwable?)
abstract class TransactionVerificationException(val txId: SecureHash, message: String, cause: Throwable?)
: FlowException("$message, transaction: $txId", cause) {
/**
@ -50,6 +50,19 @@ sealed class TransactionVerificationException(val txId: SecureHash, message: Str
constructor(txId: SecureHash, contract: Contract, cause: Throwable) : this(txId, contract.javaClass.name, cause)
}
/**
* This exception happens when a transaction was not built correctly.
* When a contract is not annotated with [NoConstraintPropagation], then the platform ensures that the constraints of output states transition correctly from input states.
*
* @property txId The transaction.
* @property contractClass The fully qualified class name of the failing contract.
* @property inputConstraint The constraint of the input state.
* @property outputConstraint The constraint of the outputs state.
*/
@KeepForDJVM
class ConstraintPropagationRejection(txId: SecureHash, val contractClass: String, inputConstraint: AttachmentConstraint, outputConstraint: AttachmentConstraint)
: TransactionVerificationException(txId, "Contract constraints for $contractClass are not propagated correctly. The outputConstraint: $outputConstraint is not a valid transition from the input constraint: $inputConstraint.", null)
/**
* The transaction attachment that contains the [contractClass] class didn't meet the constraints specified by
* the [TransactionState.constraint] object. This usually implies a version mismatch of some kind.

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

@ -61,6 +61,9 @@ data class LedgerTransaction @JvmOverloads constructor(
val inputStates: List<ContractState> get() = inputs.map { it.state.data }
val referenceStates: List<ContractState> get() = references.map { it.state.data }
private val inputAndOutputStates = inputs.asSequence().map { it.state } + outputs.asSequence()
private val allStates = inputAndOutputStates + references.asSequence().map { it.state }
/**
* Returns the typed input StateAndRef at the specified index
* @param index The index into the inputs.
@ -75,20 +78,22 @@ data class LedgerTransaction @JvmOverloads constructor(
*/
@Throws(TransactionVerificationException::class)
fun verify() {
val contractAttachmentsByContract: Map<ContractClassName, ContractAttachment> = getUniqueContractAttachmentsByContract()
// TODO - verify for version downgrade
validateStatesAgainstContract()
verifyConstraints()
verifyConstraintsValidity(contractAttachmentsByContract)
verifyConstraints(contractAttachmentsByContract)
verifyContracts()
}
private fun allStates() = inputs.asSequence().map { it.state } + outputs.asSequence()
/**
* For all input and output [TransactionState]s, validates that the wrapped [ContractState] matches up with the
* wrapped [Contract], as declared by the [BelongsToContract] annotation on the [ContractState]'s class.
*
* A warning will be written to the log if any mismatch is detected.
*/
private fun validateStatesAgainstContract() = allStates().forEach(::validateStateAgainstContract)
private fun validateStatesAgainstContract() = allStates.forEach(::validateStateAgainstContract)
private fun validateStateAgainstContract(state: TransactionState<ContractState>) {
state.data.requiredContractClassName?.let { requiredContractClassName ->
@ -101,20 +106,50 @@ data class LedgerTransaction @JvmOverloads constructor(
}
/**
* Verify that all contract constraints are valid for each state before running any contract code
* Enforces the validity of the actual constraints.
* * Constraints should be one of the valid supported ones.
* * Constraints should propagate correctly if not marked otherwise.
*/
private fun verifyConstraintsValidity(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) {
// First check that the constraints are valid.
for (state in allStates) {
checkConstraintValidity(state)
}
// Group the inputs and outputs by contract, and for each contract verify the constraints propagation logic.
// This is not required for reference states as there is nothing to propagate.
val inputContractGroups = inputs.groupBy { it.state.contract }
val outputContractGroups = outputs.groupBy { it.contract }
for (contractClassName in (inputContractGroups.keys + outputContractGroups.keys)) {
if (contractClassName.contractHasAutomaticConstraintPropagation()) {
// Verify that the constraints of output states have at least the same level of restriction as the constraints of the corresponding input states.
val inputConstraints = inputContractGroups[contractClassName]?.map { it.state.constraint }?.toSet()
val outputConstraints = outputContractGroups[contractClassName]?.map { it.constraint }?.toSet()
outputConstraints?.forEach { outputConstraint ->
inputConstraints?.forEach { inputConstraint ->
if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, contractAttachmentsByContract[contractClassName]!! ))) {
throw TransactionVerificationException.ConstraintPropagationRejection(id, contractClassName, inputConstraint, outputConstraint)
}
}
}
} else {
contractClassName.warnContractWithoutConstraintPropagation()
}
}
}
/**
* Verify that all contract constraints are passing before running any contract code.
*
* In case the transaction was created on this node then the attachments will contain the hash of the current cordapp jars.
* In case this verifies an older transaction or one originated on a different node, then this verifies that the attachments
* are valid.
* This check is running the [AttachmentConstraint.isSatisfiedBy] method for each corresponding [ContractAttachment].
*
* @throws TransactionVerificationException if the constraints fail to verify
*/
private fun verifyConstraints() {
val contractAttachmentsByContract = getUniqueContractAttachmentsByContract()
for (state in allStates()) {
val contractAttachment = contractAttachmentsByContract[state.contract] ?:
throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
private fun verifyConstraints(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) {
for (state in allStates) {
val contractAttachment = contractAttachmentsByContract[state.contract]
?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
val constraintAttachment = AttachmentWithContext(contractAttachment, state.contract,
networkParameters?.whitelistedContractImplementations)
@ -152,7 +187,7 @@ data class LedgerTransaction @JvmOverloads constructor(
* Check the transaction is contract-valid by running the verify() for each input and output state contract.
* If any contract fails to verify, the whole transaction is considered to be invalid.
*/
private fun verifyContracts() = allStates().forEach { ts ->
private fun verifyContracts() = inputAndOutputStates.forEach { ts ->
val contractClass = getContractClass(ts)
val contract = createContractInstance(contractClass)
@ -181,7 +216,6 @@ data class LedgerTransaction @JvmOverloads constructor(
throw TransactionVerificationException.ContractCreationError(id, contractClass.name, e)
}
/**
* Make sure the notary has stayed the same. As we can't tell how inputs and outputs connect, if there
* are any inputs or reference inputs, all outputs must have the same notary.

View File

@ -4,11 +4,12 @@ import co.paralleluniverse.strands.Strand
import net.corda.core.CordaInternal
import net.corda.core.DeleteForDJVM
import net.corda.core.contracts.*
import net.corda.core.cordapp.CordappProvider
import net.corda.core.crypto.*
import net.corda.core.identity.Party
import net.corda.core.internal.AttachmentWithContext
import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.ensureMinimumPlatformVersion
import net.corda.core.internal.isUploaderTrusted
import net.corda.core.node.NetworkParameters
import net.corda.core.node.ServiceHub
import net.corda.core.node.ServicesForResolution
@ -97,8 +98,7 @@ open class TransactionBuilder @JvmOverloads constructor(
// DOCEND 1
/**
* Generates a [WireTransaction] from this builder and resolves any [AutomaticHashConstraint] on contracts to
* [HashAttachmentConstraint].
* Generates a [WireTransaction] from this builder, resolves any [AutomaticPlaceholderConstraint], and selects the attachments to use for this transaction.
*
* @returns A new [WireTransaction] that will be unaffected by further changes to this [TransactionBuilder].
*
@ -114,21 +114,11 @@ open class TransactionBuilder @JvmOverloads constructor(
services.ensureMinimumPlatformVersion(4, "Reference states")
}
/**
* Resolves the [AutomaticHashConstraint]s to [HashAttachmentConstraint]s,
* [WhitelistedByZoneAttachmentConstraint]s or [SignatureAttachmentConstraint]s based on a global parameter.
*
* The [AutomaticHashConstraint] allows for less boiler plate when constructing transactions since for the
* typical case the named contract will be available when building the transaction. In exceptional cases the
* [TransactionStates] must be created with an explicit [AttachmentConstraint]
*/
val resolvedOutputs = outputs.map { state ->
state.withConstraint(when {
state.constraint !== AutomaticHashConstraint -> state.constraint
useWhitelistedByZoneAttachmentConstraint(state.contract, services.networkParameters) ->
WhitelistedByZoneAttachmentConstraint
else -> makeAttachmentConstraint(services, state)
})
val (allContractAttachments: Collection<SecureHash>, resolvedOutputs: List<TransactionState<ContractState>>) = selectContractAttachmentsAndOutputStateConstraints(services, serializationContext)
// Final sanity check that all states have the correct constraints.
for (state in (inputsWithTransactionState + resolvedOutputs)) {
checkConstraintValidity(state)
}
return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) {
@ -137,51 +127,250 @@ open class TransactionBuilder @JvmOverloads constructor(
inputStates(),
resolvedOutputs,
commands,
attachments + makeContractAttachments(services.cordappProvider),
(allContractAttachments + attachments).toSortedSet().toList(), // Sort the attachments to ensure transaction builds are stable.
notary,
window,
referenceStates
),
referenceStates),
privacySalt
)
}
}
private fun TransactionState<ContractState>.withConstraint(newConstraint: AttachmentConstraint) =
if (newConstraint == constraint) this else copy(constraint = newConstraint)
/**
* This method is responsible for selecting the contract versions to be used for the current transaction and resolve the output state [AutomaticPlaceholderConstraint]s.
* The contract attachments are used to create a deterministic Classloader to deserialise the transaction and to run the contract verification.
*
* The selection logic depends on the Attachment Constraints of the input, output and reference states, also on the explicitly set attachments.
* TODO also on the versions of the attachments of the transactions generating the input states. ( after we add versioning)
*/
private fun selectContractAttachmentsAndOutputStateConstraints(
services: ServicesForResolution, serializationContext: SerializationContext?): Pair<Collection<SecureHash>, List<TransactionState<ContractState>>> {
private fun makeAttachmentConstraint(services: ServicesForResolution, state: TransactionState<ContractState>): AttachmentConstraint {
val attachmentId = services.cordappProvider.getContractAttachmentID(state.contract)
?: throw MissingContractAttachments(listOf(state))
// Determine the explicitly set contract attachments.
val explicitAttachmentContracts: List<Pair<ContractClassName, SecureHash>> = this.attachments
.map(services.attachments::openAttachment)
.mapNotNull { it as? ContractAttachment }
.flatMap { attch ->
attch.allContracts.map { it to attch.id }
}
val attachmentSigners = services.attachments.openAttachment(attachmentId)?.signers
?: throw MissingContractAttachments(listOf(state))
// And fail early if there's more than 1 for a contract.
require(explicitAttachmentContracts.isEmpty() || explicitAttachmentContracts.groupBy { (ctr, _) -> ctr }.all { (_, groups) -> groups.size == 1 }) { "Multiple attachments set for the same contract." }
return when {
attachmentSigners.isEmpty() -> HashAttachmentConstraint(attachmentId)
else -> makeSignatureAttachmentConstraint(attachmentSigners)
val explicitAttachmentContractsMap: Map<ContractClassName, SecureHash> = explicitAttachmentContracts.toMap()
val inputContractGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = inputsWithTransactionState.groupBy { it.contract }
val outputContractGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = outputs.groupBy { it.contract }
val allContracts: Set<ContractClassName> = inputContractGroups.keys + outputContractGroups.keys
// Handle reference states.
// Filter out all contracts that might have been already used by 'normal' input or output states.
val referenceStateGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = referencesWithTransactionState.groupBy { it.contract }
val refStateContractAttachments: List<AttachmentId> = referenceStateGroups
.filterNot { it.key in allContracts }
.map { refStateEntry ->
selectAttachmentThatSatisfiesConstraints(true, refStateEntry.key, refStateEntry.value.map { it.constraint }, services)
}
// For each contract, resolve the AutomaticPlaceholderConstraint, and select the attachment.
val contractAttachmentsAndResolvedOutputStates: List<Pair<AttachmentId, List<TransactionState<ContractState>>?>> = allContracts.toSet().map { ctr ->
handleContract(ctr, inputContractGroups[ctr], outputContractGroups[ctr], explicitAttachmentContractsMap[ctr], serializationContext, services)
}
val resolvedStates: List<TransactionState<ContractState>> = contractAttachmentsAndResolvedOutputStates.mapNotNull { it.second }.flatten()
// The output states need to preserve the order in which they were added.
val resolvedOutputStatesInTheOriginalOrder: List<TransactionState<ContractState>> = outputStates().map { os -> resolvedStates.find { rs -> rs.data == os.data }!! }
val attachments: Collection<AttachmentId> = contractAttachmentsAndResolvedOutputStates.map { it.first } + refStateContractAttachments
return Pair(attachments, resolvedOutputStatesInTheOriginalOrder)
}
private val automaticConstraints = setOf(AutomaticPlaceholderConstraint, AutomaticHashConstraint)
/**
* Selects an attachment and resolves the constraints for the output states with [AutomaticPlaceholderConstraint].
*
* This is the place where the complex logic of the upgradability of contracts and constraint propagation is handled.
*
* * For contracts that *are not* annotated with @[NoConstraintPropagation], this will attempt to determine a constraint for the output states
* that is a valid transition from all the constraints of the input states.
*
* * For contracts that *are* annotated with @[NoConstraintPropagation], this enforces setting an explicit output constraint.
*
* * For states with the [HashAttachmentConstraint], if an attachment with that hash is installed on the current node, then it will be inherited by the output states and selected for the transaction.
* Otherwise a [MissingContractAttachments] is thrown.
*
* * For input states with [WhitelistedByZoneAttachmentConstraint] or a [AlwaysAcceptAttachmentConstraint] implementations, then the currently installed cordapp version is used.
*/
private fun handleContract(
contractClassName: ContractClassName,
inputStates: List<TransactionState<ContractState>>?,
outputStates: List<TransactionState<ContractState>>?,
explicitContractAttachment: AttachmentId?,
serializationContext: SerializationContext?,
services: ServicesForResolution
): Pair<AttachmentId, List<TransactionState<ContractState>>?> {
val inputsAndOutputs = (inputStates ?: emptyList()) + (outputStates ?: emptyList())
// Determine if there are any HashConstraints that pin the version of a contract. If there are, check if we trust them.
val hashAttachments = inputsAndOutputs
.filter { it.constraint is HashAttachmentConstraint }
.map { state ->
val attachment = services.attachments.openAttachment((state.constraint as HashAttachmentConstraint).attachmentId)
if (attachment == null || attachment !is ContractAttachment || !isUploaderTrusted(attachment.uploader)) {
// This should never happen because these are input states that should have been validated already.
throw MissingContractAttachments(listOf(state))
}
attachment
}.toSet()
// Check that states with the HashConstraint don't conflict between themselves or with an explicitly set attachment.
require(hashAttachments.size <= 1) {
"Transaction was built with $contractClassName states with multiple HashConstraints. This is illegal, because it makes it impossible to validate with a single version of the contract code."
}
if (explicitContractAttachment != null && hashAttachments.singleOrNull() != null) {
require(explicitContractAttachment == (hashAttachments.single() as ContractAttachment).attachment.id) {
"An attachment has been explicitly set for contract $contractClassName in the transaction builder which conflicts with the HashConstraint of a state."
}
}
// This will contain the hash of the JAR that *has* to be used by this Transaction, because it is explicit. Or null if none.
val forcedAttachmentId = explicitContractAttachment ?: hashAttachments.singleOrNull()?.id
fun selectAttachment() = selectAttachmentThatSatisfiesConstraints(
false,
contractClassName,
inputsAndOutputs.map { it.constraint }.toSet().filterNot { it in automaticConstraints },
services)
// This will contain the hash of the JAR that will be used by this Transaction.
val selectedAttachmentId = forcedAttachmentId ?: selectAttachment()
val attachmentToUse = services.attachments.openAttachment(selectedAttachmentId)?.let { it as ContractAttachment }
?: throw IllegalArgumentException("Contract attachment $selectedAttachmentId for $contractClassName is missing.")
// For Exit transactions (no output states) there is no need to resolve the output constraints.
if (outputStates == null) {
return Pair(selectedAttachmentId, null)
}
// If there are no automatic constraints, there is nothing to resolve.
if (outputStates.none { it.constraint in automaticConstraints }) {
return Pair(selectedAttachmentId, outputStates)
}
// The final step is to resolve AutomaticPlaceholderConstraint.
val automaticConstraintPropagation = contractClassName.contractHasAutomaticConstraintPropagation(serializationContext?.deserializationClassLoader)
// When automaticConstraintPropagation is disabled for a contract, output states must an explicit Constraint.
require(automaticConstraintPropagation) { "Contract $contractClassName was marked with @NoConstraintPropagation, which means the constraint of the output states has to be set explicitly." }
// This is the logic to determine the constraint which will replace the AutomaticPlaceholderConstraint.
val defaultOutputConstraint = selectAttachmentConstraint(contractClassName, inputStates, attachmentToUse, services)
// Sanity check that the selected attachment actually passes.
val constraintAttachment = AttachmentWithContext(attachmentToUse, contractClassName, services.networkParameters.whitelistedContractImplementations)
require(defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) { "Selected output constraint: $defaultOutputConstraint not satisfying $selectedAttachmentId" }
val resolvedOutputStates = outputStates.map {
val outputConstraint = it.constraint
if (outputConstraint in automaticConstraints) {
it.copy(constraint = defaultOutputConstraint)
} else {
// If the constraint on the output state is already set, and is not a valid transition or can't be transitioned, then fail early.
inputStates?.forEach { input ->
require(outputConstraint.canBeTransitionedFrom(input.constraint, attachmentToUse)) { "Output state constraint $outputConstraint cannot be transitions from ${input.constraint}" }
}
require(outputConstraint.isSatisfiedBy(constraintAttachment)) { "Output state constraint check fails. $outputConstraint" }
it
}
}
return Pair(selectedAttachmentId, resolvedOutputStates)
}
/**
* If there are multiple input states with different constraints then run the constraint intersection logic to determine the resulting output constraint.
* For issuing transactions where the attachmentToUse is JarSigned, then default to the SignatureConstraint with all the signatures.
* TODO - in the future this step can actually create a new ContractAttachment by merging 2 signed jars of the same version.
*/
private fun selectAttachmentConstraint(
contractClassName: ContractClassName,
inputStates: List<TransactionState<ContractState>>?,
attachmentToUse: ContractAttachment,
services: ServicesForResolution): AttachmentConstraint = when {
inputStates != null -> attachmentConstraintsTransition(inputStates.groupBy { it.constraint }.keys, attachmentToUse)
useWhitelistedByZoneAttachmentConstraint(contractClassName, services.networkParameters) -> WhitelistedByZoneAttachmentConstraint
attachmentToUse.signers.isNotEmpty() -> makeSignatureAttachmentConstraint(attachmentToUse.signers)
else -> HashAttachmentConstraint(attachmentToUse.id)
}
/**
* Given a set of [AttachmentConstraint]s, this function implements the rules on how constraints can evolve.
*
* This should be an exhaustive check, and should mirror [AttachmentConstraint.canBeTransitionedFrom].
*
* TODO - once support for third party signing is added, it should be implemented here. ( a constraint with 2 signatures is less restrictive than a constraint with 1 more signature)
*/
private fun attachmentConstraintsTransition(constraints: Set<AttachmentConstraint>, attachmentToUse: ContractAttachment): AttachmentConstraint = when {
// Sanity check.
constraints.isEmpty() -> throw IllegalArgumentException("Cannot transition from no constraints.")
// When all input states have the same constraint.
constraints.size == 1 -> constraints.single()
// Fail when combining the insecure AlwaysAcceptAttachmentConstraint with something else. The size must be at least 2 at this point.
constraints.any { it is AlwaysAcceptAttachmentConstraint } ->
throw IllegalArgumentException("Can't mix the AlwaysAcceptAttachmentConstraint with a secure constraint in the same transaction. This can be used to hide insecure transitions.")
// Multiple states with Hash constraints with different hashes. This should not happen as we checked already.
constraints.all { it is HashAttachmentConstraint } ->
throw IllegalArgumentException("Cannot mix HashConstraints with different hashes in the same transaction.")
// The HashAttachmentConstraint is the strongest constraint, so it wins when mixed with anything. As long as the actual constraints pass.
// TODO - this could change if we decide to introduce a way to gracefully migrate from the Hash Constraint to the Signature Constraint.
constraints.any { it is HashAttachmentConstraint } -> constraints.find { it is HashAttachmentConstraint }!!
// TODO, we don't currently support mixing signature constraints with different signers. This will change once we introduce third party signers.
constraints.all { it is SignatureAttachmentConstraint } ->
throw IllegalArgumentException("Cannot mix SignatureAttachmentConstraints signed by different parties in the same transaction.")
// This ensures a smooth migration from the Whitelist Constraint, given that for the transaction to be valid it still has to pass both constraints.
// The transition is possible only when the SignatureConstraint contains ALL signers from the attachment.
constraints.any { it is SignatureAttachmentConstraint } && constraints.any { it is WhitelistedByZoneAttachmentConstraint } -> {
val signatureConstraint = constraints.mapNotNull { it as? SignatureAttachmentConstraint }.single()
when {
attachmentToUse.signers.isEmpty() -> throw IllegalArgumentException("Cannot mix a state with the WhitelistedByZoneAttachmentConstraint and a state with the SignatureAttachmentConstraint, when the latest attachment is not signed. Please contact your Zone operator.")
signatureConstraint.key.keys.containsAll(attachmentToUse.signers) -> signatureConstraint
else -> throw IllegalArgumentException("Attempting to transition a WhitelistedByZoneAttachmentConstraint state backed by an attachment signed by multiple parties to a weaker SignatureConstraint that does not require all those signatures. Please contact your Zone operator.")
}
}
else -> throw IllegalArgumentException("Unexpected constraints $constraints.")
}
private fun makeSignatureAttachmentConstraint(attachmentSigners: List<PublicKey>) =
SignatureAttachmentConstraint(CompositeKey.Builder().addKeys(attachmentSigners.map { it }).build())
private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters) =
contractClassName in networkParameters.whitelistedContractImplementations.keys
/**
* The attachments added to the current transaction contain only the hashes of the current cordapps.
* NOT the hashes of the cordapps that were used when the input states were created ( in case they changed in the meantime)
* TODO - review this logic
* This method should only be called for upgradeable contracts.
*
* For now we use the currently installed CorDapp version.
* TODO - When the SignatureConstraint and contract version logic is in, this will need to query the attachments table and find the latest one that satisfies all constraints.
* TODO - select a version of the contract that is no older than the one from the previous transactions.
*/
private fun makeContractAttachments(cordappProvider: CordappProvider): List<AttachmentId> {
// Reference inputs not included as it is not necessary to verify them.
return (inputsWithTransactionState + outputs).map { state ->
cordappProvider.getContractAttachmentID(state.contract)
?: throw MissingContractAttachments(listOf(state))
}.distinct()
private fun selectAttachmentThatSatisfiesConstraints(isReference: Boolean, contractClassName: String, constraints: List<AttachmentConstraint>, services: ServicesForResolution): AttachmentId {
require(constraints.none { it in automaticConstraints })
require(isReference || constraints.none { it is HashAttachmentConstraint })
return services.cordappProvider.getContractAttachmentID(contractClassName)!!
}
private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters) = contractClassName in networkParameters.whitelistedContractImplementations.keys
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class)
fun toLedgerTransaction(services: ServiceHub) = toWireTransaction(services).toLedgerTransaction(services)
@ -272,10 +461,8 @@ with @BelongsToContract, or supply an explicit contract parameter to addOutputSt
""".trimIndent().replace('\n', ' ')
},
notary: Party, encumbrance: Int? = null,
constraint: AttachmentConstraint = AutomaticHashConstraint
): TransactionBuilder {
return addOutputState(TransactionState(state, contract, notary, encumbrance, constraint))
}
constraint: AttachmentConstraint = AutomaticPlaceholderConstraint
) = addOutputState(TransactionState(state, contract, notary, encumbrance, constraint))
/** A default notary must be specified during builder construction to use this method */
@JvmOverloads
@ -289,7 +476,7 @@ Unable to infer Contract class name because state class ${state::class.java.name
with @BelongsToContract, or supply an explicit contract parameter to addOutputState().
""".trimIndent().replace('\n', ' ')
},
constraint: AttachmentConstraint = AutomaticHashConstraint
constraint: AttachmentConstraint = AutomaticPlaceholderConstraint
) = apply {
checkNotNull(notary) {
"Need to specify a notary for the state, or set a default one on TransactionBuilder initialisation"

View File

@ -9,6 +9,7 @@ import net.corda.core.serialization.CordaSerializable
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.time.Duration
import java.util.*
import java.util.concurrent.ExecutionException
import java.util.concurrent.Future
import kotlin.reflect.KProperty
@ -135,12 +136,16 @@ fun <V> Future<V>.getOrThrow(timeout: Duration? = null): V = try {
throw e.cause!!
}
private val warnings = mutableSetOf<String>()
private const val MAX_SIZE = 100
private val warnings = Collections.newSetFromMap(object : LinkedHashMap<String, Boolean>() {
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, Boolean>?) = size > MAX_SIZE
})
/**
* Utility to help log a warning message only once.
* It's not thread safe, as in the worst case the warning will be logged twice.
* It implements an ad hoc Fifo cache because there's none available in the standard libraries.
*/
@Synchronized
fun Logger.warnOnce(warning: String) {
if (warning !in warnings) {
warnings.add(warning)

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

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

@ -73,7 +73,7 @@ class LedgerTransactionQueryTests {
)
services.recordTransactions(fakeIssueTx)
val dummyStateRef = StateRef(fakeIssueTx.id, 0)
return StateAndRef(TransactionState(dummyState, DummyContract.PROGRAM_ID, DUMMY_NOTARY, null), dummyStateRef)
return StateAndRef(TransactionState(dummyState, DummyContract.PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint), dummyStateRef)
}
private fun makeDummyTransaction(): LedgerTransaction {

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

@ -41,7 +41,15 @@ class TransactionBuilderTest {
doReturn(cordappProvider).whenever(services).cordappProvider
doReturn(contractAttachmentId).whenever(cordappProvider).getContractAttachmentID(DummyContract.PROGRAM_ID)
doReturn(testNetworkParameters()).whenever(services).networkParameters
doReturn(attachments).whenever(services).attachments
val attachmentStorage = rigorousMock<AttachmentStorage>()
doReturn(attachmentStorage).whenever(services).attachments
val attachment = rigorousMock<ContractAttachment>()
doReturn(attachment).whenever(attachmentStorage).openAttachment(contractAttachmentId)
doReturn(contractAttachmentId).whenever(attachment).id
doReturn(setOf(DummyContract.PROGRAM_ID)).whenever(attachment).allContracts
doReturn("app").whenever(attachment).uploader
doReturn(emptyList<Party>()).whenever(attachment).signers
}
@Test
@ -104,6 +112,7 @@ class TransactionBuilderTest {
assertTrue(expectedConstraint.isSatisfiedBy(signedAttachment))
assertFalse(expectedConstraint.isSatisfiedBy(unsignedAttachment))
doReturn(attachments).whenever(services).attachments
doReturn(signedAttachment).whenever(attachments).openAttachment(contractAttachmentId)
val outputState = TransactionState(data = DummyState(), contract = DummyContract.PROGRAM_ID, notary = notary)
@ -113,19 +122,17 @@ class TransactionBuilderTest {
val wtx = builder.toWireTransaction(services)
assertThat(wtx.outputs).containsOnly(outputState.copy(constraint = expectedConstraint))
}
private val unsignedAttachment = object : AbstractAttachment({ byteArrayOf() }) {
private val unsignedAttachment = ContractAttachment(object : AbstractAttachment({ byteArrayOf() }) {
override val id: SecureHash get() = throw UnsupportedOperationException()
override val signers: List<PublicKey> get() = emptyList()
}
}, DummyContract.PROGRAM_ID)
private fun signedAttachment(vararg parties: Party) = object : AbstractAttachment({ byteArrayOf() }) {
private fun signedAttachment(vararg parties: Party) = ContractAttachment(object : AbstractAttachment({ byteArrayOf() }) {
override val id: SecureHash get() = throw UnsupportedOperationException()
override val signers: List<PublicKey> get() = parties.map { it.owningKey }
}
}, DummyContract.PROGRAM_ID)
}

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:
@ -84,18 +93,19 @@ time effectively stop being a part of the network.
signed by a specified identity, via the regular Java ``jarsigner`` tool. This will be the most flexible type
and the smoothest to deploy: no restarts or contract upgrade transactions are needed.
**Defaults.** The default constraint type is either a zone constraint, if the network parameters in effect when the
transaction is built contain an entry for that contract class, or a hash constraint if not.
**Defaults.** Currently, the default constraint type is either a zone constraint, if the network parameters in effect when the
transaction is built contain an entry for that contract class, or a hash constraint if not. Once the Signature Constraints are introduced,
the default constraint will be the Signature Constraint if the jar is signed.
A ``TransactionState`` has a ``constraint`` field that represents that state's attachment constraint. When a party
constructs a ``TransactionState``, or adds a state using ``TransactionBuilder.addOutput(ContractState)`` without
specifying the constraint parameter, a default value (``AutomaticHashConstraint``) is used. This default will be
specifying the constraint parameter, a default value (``AutomaticPlaceholderConstraint``) is used. This default will be
automatically resolved to a specific ``HashAttachmentConstraint`` or a ``WhitelistedByZoneAttachmentConstraint``.
This automatic resolution occurs when a ``TransactionBuilder`` is converted to a ``WireTransaction``. This reduces
the boilerplate that would otherwise be involved.
Finally, an ``AlwaysAcceptAttachmentConstraint`` can be used which accepts anything, though this is intended for
testing only.
testing only, and a warning will be shown if used by a contract.
Please note that the ``AttachmentConstraint`` interface is marked as ``@DoNotImplement``. You are not allowed to write
new constraint types. Only the platform may implement this interface. If you tried, other nodes would not understand
@ -116,48 +126,38 @@ a flow:
TransactionBuilder tx = new TransactionBuilder();
Party notaryParty = ... // a notary party
tx.addInputState(...)
tx.addInputState(...)
DummyState contractState = new DummyState();
SecureHash myAttachmentHash = SecureHash.parse("2b4042aed7e0e39d312c4c477dca1d96ec5a878ddcfd5583251a8367edbd4a5f");
TransactionState transactionState = new TransactionState(contractState, DummyContract.Companion.getPROGRAMID(), notaryParty, new AttachmentHashConstraint(myAttachmentHash));
TransactionState transactionState = new TransactionState(contractState, DummyContract.Companion.getPROGRAMID(), notaryParty, null, HashAttachmentConstraint(myhash));
tx.addOutputState(transactionState);
WireTransaction wtx = tx.toWireTransaction(serviceHub); // This is where an automatic constraint would be resolved.
LedgerTransaction ltx = wtx.toLedgerTransaction(serviceHub);
ltx.verify(); // Verifies both the attachment constraints and contracts
Hard-coding the hash of your app in the code itself can be pretty awkward, so the API also offers the ``AutomaticHashConstraint``.
This isn't a real constraint that will appear in a transaction: it acts as a marker to the ``TransactionBuilder`` that
you require the hash of the node's installed app which supplies the specified contract to be used. In practice, when using
hash constraints, you almost always want "whatever the current code is" and not a hard-coded hash. So this automatic
constraint placeholder is useful.
FinalityFlow
------------
Issues when using the HashAttachmentConstraint
----------------------------------------------
It's possible to encounter contract constraint issues when notarising transactions with the ``FinalityFlow`` on a network
containing multiple versions of the same CorDapp. This will happen when using hash constraints or with zone constraints
if the zone whitelist has missing CorDapp versions. If a participating party fails to validate the **notarised** transaction
then we have a scenario where the members of the network do not have a consistent view of the ledger.
When setting up a new network, it is possible to encounter errors when states are issued with the ``HashAttachmentConstraint``,
but not all nodes have that same version of the CorDapp installed locally.
Therefore, if the finality handler flow (which is run on the counter-party) errors for any reason it will always be sent to
the flow hospital. From there it's suspended waiting to be retried on node restart. This gives the node operator the opportunity
to recover from those errors, which in the case of contract constraint violations means either updating the CorDapp or
adding its hash to the zone whitelist.
In this case, flows will fail with a ``ContractConstraintRejection``, and the failed flow will be sent to the flow hospital.
From there it's suspended waiting to be retried on node restart.
This gives the node operator the opportunity to recover from those errors, which in the case of constraint violations means
adding the right cordapp jar to the ``cordapps`` folder.
.. note:: This is a temporary issue in the current version of Corda, until we implement some missing features which will
enable a seamless handling of differences in CorDapp versions.
CorDapps as attachments
-----------------------
CorDapp JARs (see :doc:`cordapp-overview`) that are installed to the node and contain classes implementing the ``Contract``
interface are automatically loaded into the ``AttachmentStorage`` of a node at startup.
After CorDapps are loaded into the attachment store the node creates a link between contract classes and the attachment
that they were loaded from. This makes it possible to find the attachment for any given contract. This is how the
automatic resolution of attachments is done by the ``TransactionBuilder`` and how, when verifying the constraints and
contracts, attachments are associated with their respective contracts.
CorDapp JARs (see :doc:`cordapp-overview`) that contain classes implementing the ``Contract`` interface are automatically
loaded into the ``AttachmentStorage`` of a node, and made available as ``ContractAttachments``.
They are retrievable by hash using ``AttachmentStorage.openAttachment``.
These JARs can either be installed on the node or fetched from the network using the ``FetchAttachmentsFlow``.
.. note:: The obvious way to write a CorDapp is to put all you states, contracts, flows and support code into a single
Java module. This will work but it will effectively publish your entire app onto the ledger. That has two problems:
@ -166,6 +166,51 @@ contracts, attachments are associated with their respective contracts.
app into multiple modules: one which contains just states, contracts and core data types. And another which contains
the rest of the app. See :ref:`cordapp-structure`.
Constraints propagation
-----------------------
As was mentioned above, the TransactionBuilder API gives the CorDapp developer or even malicious node owner the possibility
to construct output states with a constraint of his choosing.
Also, as listed above, some constraints are more restrictive then others.
For example, the ``HashAttachmentConstraint`` is the most restrictive, basically reducing the universe of possible attachments
to 1, while the ``AlwaysAcceptAttachmentConstraint`` allows any attachment to be selected.
For the ledger to remain in a consistent state, the expected behavior is for output state to inherit the constraints of input states.
This guarantees that for example, a transaction can't output a state with the ``AlwaysAcceptAttachmentConstraint`` when the
corresponding input state was the ``HashAttachmentConstraint``. Translated, this means that if this rule is enforced, it ensures
that the output state will be spent under similar conditions as it was created.
Before version 4, the constraint propagation logic was expected to be enforced in the contract verify code, as it has access to the entire Transaction.
Starting with version 4 of Corda, the constraint propagation logic has been implemented and enforced directly by the platform,
unless disabled using ``@NoConstraintPropagation`` - which reverts to the previous behavior.
For Contracts that are not annotated with ``@NoConstraintPropagation``, the platform implements a fairly simple constraint transition policy
to ensure security and also allow the possibility to transition to the new SignatureAttachmentConstraint.
During transaction building the ``AutomaticPlaceholderConstraint`` for output states will be resolved and the best contract attachment versions
will be selected based on a variety of factors so that the above holds true.
If it can't find attachments in storage or there are no possible constraints, the Transaction Builder will fail early.
For example:
- In the simple case, if a ``MyContract`` input state is constrained by the ``HashAttachmentConstraint``, then the constraints of all output states of that type will be resolved
to the ``HashAttachmentConstraint`` with the same hash, and the attachment with that hash will be selected.
- For upgradeable constraints like the ``WhitelistedByZoneAttachmentConstraint``, the output states will inherit the same,
and the selected attachment will be the latest version installed on the node.
- A more complex case is when for ``MyContract``, one input state is constrained by the ``HashAttachmentConstraint``, while another
state by the ``WhitelistedByZoneAttachmentConstraint``. To respect the rule from above, if the hash of the ``HashAttachmentConstraint``
is whitelisted by the network, then the output states will inherit the ``HashAttachmentConstraint``, as it is more restrictive.
If the hash was not whitelisted, then the builder will fail as it is unable to select a correct constraint.
- The ``SignatureAttachmentConstraint`` is an upgradeable constraint, same as the ``WhitelistedByZoneAttachmentConstraint``.
By convention we allow states to transition to the ``SignatureAttachmentConstraint`` from the ``WhitelistedByZoneAttachmentConstraint`` as long as the Signatures
from new constraints are all the jarsigners from the whitelisted attachment.
For Contracts that are annotated with ``@NoConstraintPropagation``, the platform requires that the Transaction Builder specifies
an actual constraint for the output states (the ``AutomaticPlaceholderConstraint`` can't be used) .
Testing
-------

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

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

View File

@ -47,7 +47,7 @@ class AttachmentsClassLoaderStaticContractTests {
class AttachmentDummyContract : Contract {
companion object {
private const val ATTACHMENT_PROGRAM_ID = "net.corda.nodeapi.internal.AttachmentsClassLoaderStaticContractTests\$AttachmentDummyContract"
const val ATTACHMENT_PROGRAM_ID = "net.corda.nodeapi.internal.AttachmentsClassLoaderStaticContractTests\$AttachmentDummyContract"
}
data class State(val magicNumber: Int = 0) : ContractState {
@ -83,7 +83,14 @@ class AttachmentsClassLoaderStaticContractTests {
cordappProviderImpl.start(testNetworkParameters().whitelistedContractImplementations)
doReturn(cordappProviderImpl).whenever(it).cordappProvider
doReturn(testNetworkParameters()).whenever(it).networkParameters
doReturn(attachments).whenever(it).attachments
val attachmentStorage = rigorousMock<AttachmentStorage>()
doReturn(attachmentStorage).whenever(it).attachments
val attachment = rigorousMock<ContractAttachment>()
doReturn(attachment).whenever(attachmentStorage).openAttachment(any())
doReturn(it.cordappProvider.getContractAttachmentID(AttachmentDummyContract.ATTACHMENT_PROGRAM_ID)).whenever(attachment).id
doReturn(setOf(AttachmentDummyContract.ATTACHMENT_PROGRAM_ID)).whenever(attachment).allContracts
doReturn("app").whenever(attachment).uploader
doReturn(emptyList<Party>()).whenever(attachment).signers
}
@Test

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

@ -2,6 +2,7 @@ package net.corda.node.internal.cordapp
import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner
import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult
import net.corda.core.contracts.warnContractWithoutConstraintPropagation
import net.corda.core.cordapp.Cordapp
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
@ -67,7 +68,7 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
*/
fun fromJarUrls(scanJars: List<URL>) = JarScanningCordappLoader(scanJars.map { it.restricted() })
private fun URL.restricted(rootPackageName: String? = null) = RestrictedURL(this, rootPackageName)
private fun URL.restricted(rootPackageName: String? = null) = RestrictedURL(this, rootPackageName)
private fun jarUrlsInDirectory(directory: Path): List<URL> {
@ -169,7 +170,11 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
}
private fun findContractClassNames(scanResult: RestrictedScanResult): List<String> {
return coreContractClasses.flatMap { scanResult.getNamesOfClassesImplementing(it) }.distinct()
val contractClasses = coreContractClasses.flatMap { scanResult.getNamesOfClassesImplementing(it) }.distinct()
for (contractClass in contractClasses) {
contractClass.warnContractWithoutConstraintPropagation(appClassLoader)
}
return contractClasses
}
private fun findPlugins(cordappJarPath: RestrictedURL): List<SerializationWhitelist> {
@ -273,7 +278,9 @@ abstract class CordappLoaderTemplate : CordappLoader {
cordapps.flatMap { corDapp -> corDapp.allFlows.map { flow -> flow to corDapp } }
.groupBy { it.first }
.mapValues {
if(it.value.size > 1) { throw MultipleCordappsForFlowException("There are multiple CorDapp JARs on the classpath for flow ${it.value.first().first.name}: [ ${it.value.joinToString { it.second.name }} ].") }
if (it.value.size > 1) {
throw MultipleCordappsForFlowException("There are multiple CorDapp JARs on the classpath for flow ${it.value.first().first.name}: [ ${it.value.joinToString { it.second.name }} ].")
}
it.value.single().second
}
}

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
@ -70,7 +67,7 @@ class PersistentStateServiceTests {
val persistentStateService = PersistentStateService(schemaService)
database.transaction {
val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party
persistentStateService.persist(setOf(StateAndRef(TransactionState(TestState(), DummyContract.PROGRAM_ID, MEGA_CORP), StateRef(SecureHash.sha256("dummy"), 0))))
persistentStateService.persist(setOf(StateAndRef(TransactionState(TestState(), DummyContract.PROGRAM_ID, MEGA_CORP, constraint = AlwaysAcceptAttachmentConstraint), StateRef(SecureHash.sha256("dummy"), 0))))
currentDBSession().flush()
val parentRowCountResult = connection.prepareStatement("select count(*) from Parents").executeQuery()
parentRowCountResult.next()

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

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

@ -11,6 +11,7 @@ import net.corda.core.internal.UNKNOWN_UPLOADER
import net.corda.core.internal.uncheckedCast
import net.corda.core.node.ServiceHub
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.AttachmentId
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction
@ -147,8 +148,13 @@ data class TestTransactionDSLInterpreter private constructor(
override fun _tweak(dsl: TransactionDSLInterpreter.() -> EnforceVerifyOrFail) = copy().dsl()
override fun _attachment(contractClassName: ContractClassName) {
(services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage)
attachment((services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage))
}
override fun _attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List<PublicKey>){
attachment((services.cordappProvider as MockCordappProvider).addMockCordapp(contractClassName, services.attachments as MockAttachmentStorage, attachmentId, signers))
}
}
data class TestLedgerDSLInterpreter private constructor(

View File

@ -1,9 +1,19 @@
package net.corda.testing.dsl
import net.corda.core.DoNotImplement
import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.AttachmentConstraint
import net.corda.core.contracts.AutomaticPlaceholderConstraint
import net.corda.core.contracts.CommandData
import net.corda.core.contracts.ContractClassName
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TimeWindow
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party
import net.corda.core.node.services.AttachmentId
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.seconds
import java.security.PublicKey
@ -80,6 +90,13 @@ interface TransactionDSLInterpreter : Verifies, OutputStateLookup {
* @param contractClassName The contract class to attach
*/
fun _attachment(contractClassName: ContractClassName)
/**
* Attaches an attachment containing the named contract to the transaction
* @param contractClassName The contract class to attach
* @param attachmentId The attachment
*/
fun _attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List<PublicKey>)
}
/**
@ -131,37 +148,37 @@ class TransactionDSL<out T : TransactionDSLInterpreter>(interpreter: T, private
* Adds a labelled output to the transaction.
*/
fun output(contractClassName: ContractClassName, label: String, notary: Party, contractState: ContractState) =
output(contractClassName, label, notary, null, AutomaticHashConstraint, contractState)
output(contractClassName, label, notary, null, AutomaticPlaceholderConstraint, contractState)
/**
* Adds a labelled output to the transaction.
*/
fun output(contractClassName: ContractClassName, label: String, encumbrance: Int, contractState: ContractState) =
output(contractClassName, label, notary, encumbrance, AutomaticHashConstraint, contractState)
output(contractClassName, label, notary, encumbrance, AutomaticPlaceholderConstraint, contractState)
/**
* Adds a labelled output to the transaction.
*/
fun output(contractClassName: ContractClassName, label: String, contractState: ContractState) =
output(contractClassName, label, notary, null, AutomaticHashConstraint, contractState)
output(contractClassName, label, notary, null, AutomaticPlaceholderConstraint, contractState)
/**
* Adds an output to the transaction.
*/
fun output(contractClassName: ContractClassName, notary: Party, contractState: ContractState) =
output(contractClassName, null, notary, null, AutomaticHashConstraint, contractState)
output(contractClassName, null, notary, null, AutomaticPlaceholderConstraint, contractState)
/**
* Adds an output to the transaction.
*/
fun output(contractClassName: ContractClassName, encumbrance: Int, contractState: ContractState) =
output(contractClassName, null, notary, encumbrance, AutomaticHashConstraint, contractState)
output(contractClassName, null, notary, encumbrance, AutomaticPlaceholderConstraint, contractState)
/**
* Adds an output to the transaction.
*/
fun output(contractClassName: ContractClassName, contractState: ContractState) =
output(contractClassName, null, notary, null, AutomaticHashConstraint, contractState)
output(contractClassName, null, notary, null, AutomaticPlaceholderConstraint, contractState)
/**
* Adds a command to the transaction.
@ -186,5 +203,8 @@ class TransactionDSL<out T : TransactionDSLInterpreter>(interpreter: T, private
*/
fun attachment(contractClassName: ContractClassName) = _attachment(contractClassName)
fun attachment(contractClassName: ContractClassName, attachmentId: AttachmentId, signers: List<PublicKey>) = _attachment(contractClassName, attachmentId, signers)
fun attachment(contractClassName: ContractClassName, attachmentId: AttachmentId) = _attachment(contractClassName, attachmentId, emptyList())
fun attachments(vararg contractClassNames: ContractClassName) = contractClassNames.forEach { attachment(it) }
}

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
@ -11,6 +12,7 @@ import net.corda.node.cordapp.CordappLoader
import net.corda.node.internal.cordapp.CordappProviderImpl
import net.corda.testing.services.MockAttachmentStorage
import java.nio.file.Paths
import java.security.PublicKey
import java.util.*
class MockCordappProvider(
@ -21,7 +23,7 @@ class MockCordappProvider(
private val cordappRegistry = mutableListOf<Pair<Cordapp, AttachmentId>>()
fun addMockCordapp(contractClassName: ContractClassName, attachments: MockAttachmentStorage) {
fun addMockCordapp(contractClassName: ContractClassName, attachments: MockAttachmentStorage, contractHash: AttachmentId? = null, signers: List<PublicKey> = emptyList()): AttachmentId {
val cordapp = CordappImpl(
contractClassNames = listOf(contractClassName),
initiatedFlows = emptyList(),
@ -35,21 +37,23 @@ class MockCordappProvider(
jarPath = Paths.get("").toUri().toURL(),
allFlows = emptyList(),
jarHash = SecureHash.allOnesHash)
if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) }) {
cordappRegistry.add(Pair(cordapp, findOrImportAttachment(listOf(contractClassName), contractClassName.toByteArray(), attachments)))
if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) && it.second == contractHash }) {
cordappRegistry.add(Pair(cordapp, findOrImportAttachment(listOf(contractClassName), contractClassName.toByteArray(), attachments, contractHash, signers)))
}
return cordappRegistry.findLast { contractClassName in it.first.contractClassNames }?.second!!
}
override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? = cordappRegistry.find { it.first.contractClassNames.contains(contractClassName) }?.second ?: super.getContractAttachmentID(contractClassName)
override fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId? = cordappRegistry.find { it.first.contractClassNames.contains(contractClassName) }?.second
?: super.getContractAttachmentID(contractClassName)
private fun findOrImportAttachment(contractClassNames: List<ContractClassName>, data: ByteArray, attachments: MockAttachmentStorage): AttachmentId {
val existingAttachment = attachments.files.filter {
Arrays.equals(it.value.second, data)
private fun findOrImportAttachment(contractClassNames: List<ContractClassName>, data: ByteArray, attachments: MockAttachmentStorage, contractHash: AttachmentId?, signers: List<PublicKey>): AttachmentId {
val existingAttachment = attachments.files.filter { (attachmentId, content) ->
contractHash == attachmentId
}
return if (!existingAttachment.isEmpty()) {
existingAttachment.keys.first()
} else {
attachments.importContractAttachment(contractClassNames, DEPLOYED_CORDAPP_UPLOADER, data.inputStream())
attachments.importContractAttachment(contractClassNames, DEPLOYED_CORDAPP_UPLOADER, data.inputStream(), contractHash, signers)
}
}
}

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.UNKNOWN_UPLOADER
import net.corda.core.internal.readFully
@ -15,6 +16,7 @@ import net.corda.core.node.services.vault.AttachmentSort
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.nodeapi.internal.withContractsInJar
import java.io.InputStream
import java.security.PublicKey
import java.util.*
import java.util.jar.JarInputStream
@ -53,21 +55,22 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() {
}
}
fun importContractAttachment(contractClassNames: List<ContractClassName>, uploader: String, jar: InputStream): AttachmentId = importAttachmentInternal(jar, uploader, contractClassNames)
@JvmOverloads
fun importContractAttachment(contractClassNames: List<ContractClassName>, uploader: String, jar: InputStream, attachmentId: AttachmentId? = null, signers: List<PublicKey> = emptyList()): AttachmentId = importAttachmentInternal(jar, uploader, contractClassNames, attachmentId, signers)
fun getAttachmentIdAndBytes(jar: InputStream): Pair<AttachmentId, ByteArray> = jar.readFully().let { bytes -> Pair(bytes.sha256(), bytes) }
private class MockAttachment(dataLoader: () -> ByteArray, override val id: SecureHash) : AbstractAttachment(dataLoader)
private class MockAttachment(dataLoader: () -> ByteArray, override val id: SecureHash, override val signers: List<PublicKey>) : AbstractAttachment(dataLoader)
private fun importAttachmentInternal(jar: InputStream, uploader: String, contractClassNames: List<ContractClassName>? = null): AttachmentId {
private fun importAttachmentInternal(jar: InputStream, uploader: String, contractClassNames: List<ContractClassName>? = null, attachmentId: AttachmentId? = null, signers: List<PublicKey> = emptyList()): AttachmentId {
// JIS makes read()/readBytes() return bytes of the current file, but we want to hash the entire container here.
require(jar !is JarInputStream)
val bytes = jar.readFully()
val sha256 = bytes.sha256()
val sha256 = attachmentId ?: bytes.sha256()
if (sha256 !in files.keys) {
val baseAttachment = MockAttachment({ bytes }, sha256)
val baseAttachment = MockAttachment({ bytes }, sha256, signers)
val attachment = if (contractClassNames == null || contractClassNames.isEmpty()) baseAttachment else ContractAttachment(baseAttachment, contractClassNames.first(), contractClassNames.toSet(), uploader)
_files[sha256] = Pair(attachment, bytes)
}