diff --git a/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt b/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt index 45d410abce..a7aea933ac 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt @@ -67,7 +67,7 @@ data class HashAttachmentConstraint(val attachmentId: SecureHash) : AttachmentCo object WhitelistedByZoneAttachmentConstraint : AttachmentConstraint { override fun isSatisfiedBy(attachment: Attachment): Boolean { return if (attachment is AttachmentWithContext) { - val whitelist = attachment.networkParameters.whitelistedContractImplementations + val whitelist = attachment.whitelistedContractImplementations log.debug("Checking ${attachment.contract} is in CZ whitelist $whitelist") attachment.id in (whitelist[attachment.contract] ?: emptyList()) } else { diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt index e03c91f887..7b9026e174 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt @@ -107,6 +107,13 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S class ConflictingAttachmentsRejection(txId: SecureHash, val contractClass: String) : TransactionVerificationException(txId, "Contract constraints failed for: $contractClass, because multiple attachments providing this contract were attached.", null) + /** + * Indicates that the same attachment has been added multiple times to a transaction. + */ + @KeepForDJVM + class DuplicateAttachmentsRejection(txId: SecureHash, val attachmentId: Attachment) + : TransactionVerificationException(txId, "The attachment: $attachmentId was added multiple times.", null) + /** * A [Contract] class named by a state could not be constructed. Most likely you do not have a no-argument * constructor, or the class doesn't subclass [Contract]. diff --git a/core/src/main/kotlin/net/corda/core/internal/AttachmentWithContext.kt b/core/src/main/kotlin/net/corda/core/internal/AttachmentWithContext.kt index 638f1d4ae5..fdfb135aca 100644 --- a/core/src/main/kotlin/net/corda/core/internal/AttachmentWithContext.kt +++ b/core/src/main/kotlin/net/corda/core/internal/AttachmentWithContext.kt @@ -1,7 +1,9 @@ package net.corda.core.internal -import net.corda.core.contracts.* -import net.corda.core.node.NetworkParameters +import net.corda.core.contracts.Attachment +import net.corda.core.contracts.ContractAttachment +import net.corda.core.contracts.ContractClassName +import net.corda.core.node.services.AttachmentId /** * Used only for passing to the Attachment constraint verification. @@ -9,8 +11,8 @@ import net.corda.core.node.NetworkParameters class AttachmentWithContext( val contractAttachment: ContractAttachment, val contract: ContractClassName, - /** Required for verifying [WhitelistedByZoneAttachmentConstraint] and [HashAttachmentConstraint] migration to [SignatureAttachmentConstraint] */ - val networkParameters: NetworkParameters + /** Required for verifying [WhitelistedByZoneAttachmentConstraint] */ + val whitelistedContractImplementations: Map> ) : Attachment by contractAttachment { init { require(contract in contractAttachment.allContracts) { diff --git a/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt b/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt index ed7f083815..ea8abc1d70 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ConstraintsUtils.kt @@ -46,14 +46,8 @@ val ContractState.requiredContractClassName: String? get() { * * You can transition from the [WhitelistedByZoneAttachmentConstraint] to the [SignatureAttachmentConstraint] only if all signers of the * JAR are required to sign in the future. * - * * You can transition from a [HashAttachmentConstraint] to a [SignatureAttachmentConstraint] when the following conditions are met: - * - * 1. Jar contents (per entry, by hashcode) of both original (unsigned) and signed contract jars are identical - * Note: this step is enforced in the [AttachmentsClassLoader] no overlap rule checking. - * - * 2. Java package namespace of signed contract jar is registered in the CZ network map with same public keys (as used to sign contract jar) */ -fun AttachmentConstraint.canBeTransitionedFrom(input: AttachmentConstraint, attachment: AttachmentWithContext): Boolean { +fun AttachmentConstraint.canBeTransitionedFrom(input: AttachmentConstraint, attachment: ContractAttachment): Boolean { val output = this return when { // These branches should not happen, as this has been already checked. @@ -80,21 +74,6 @@ fun AttachmentConstraint.canBeTransitionedFrom(input: AttachmentConstraint, atta input is WhitelistedByZoneAttachmentConstraint && output is SignatureAttachmentConstraint -> attachment.signerKeys.isNotEmpty() && output.key.keys.containsAll(attachment.signerKeys) - // Transition from Hash to Signature constraint requires - // signer(s) of signature-constrained output state is same as signer(s) of registered package namespace - input is HashAttachmentConstraint && output is SignatureAttachmentConstraint -> { - val packageOwnerPK = attachment.networkParameters.getPackageOwnerOf(attachment.contractAttachment.allContracts) - if (packageOwnerPK == null) { - log.warn("Missing registered java package owner for ${attachment.contractAttachment.contract} in network parameters: " + - "${attachment.networkParameters} (input constraint = $input, output constraint = $output)") - return false - } - else if (!packageOwnerPK.isFulfilledBy(output.key) ) { - log.warn("Java package owner keys do not match signature constrained output state keys") - return false - } - return true - } else -> false } } diff --git a/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt b/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt index f7234cdf61..9515c3c17d 100644 --- a/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/CordaUtils.kt @@ -100,17 +100,6 @@ internal fun NetworkParameters.getPackageOwnerOf(contractClassName: ContractClas return packageOwnership.entries.singleOrNull { owns(it.key, contractClassName) }?.value } -/** - * Returns the public key of the package owner if any of [contractClassNames] match, or null if not owned. - */ -internal fun NetworkParameters.getPackageOwnerOf(contractClassNames: Set): PublicKey? { - for (contractClassName in contractClassNames) { - val owner = getPackageOwnerOf(contractClassName) - if (owner != null) return owner - } - return null -} - // Make sure that packages don't overlap so that ownership is clear. fun noPackageOverlap(packages: Collection): Boolean { return packages.all { outer -> packages.none { inner -> inner != outer && inner.startsWith("$outer.") } } diff --git a/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt b/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt index 7b9155b38f..82b903817e 100644 --- a/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt +++ b/core/src/main/kotlin/net/corda/core/internal/TransactionVerifierServiceInternal.kt @@ -15,7 +15,7 @@ interface TransactionVerifierServiceInternal { * Verifies the [transaction] but adds some [extraAttachments] to the classpath. * Required for transactions built with Corda 3.x that might miss some dependencies due to a bug in that version. */ - fun verify(transaction: LedgerTransaction, extraAttachments: List ): CordaFuture<*> + fun verify(transaction: LedgerTransaction, extraAttachments: List): CordaFuture<*> } /** @@ -29,44 +29,84 @@ fun LedgerTransaction.prepareVerify(extraAttachments: List) = this.i * * @param inputVersions A map linking each contract class name to the advertised version of the JAR that defines it. Used for downgrade protection. */ -class Verifier(val ltx: LedgerTransaction, private val transactionClassLoader: ClassLoader, +class Verifier(val ltx: LedgerTransaction, + private val transactionClassLoader: ClassLoader, private val inputVersions: Map) { private val inputStates: List> = ltx.inputs.map { it.state } private val allStates: List> = inputStates + ltx.references.map { it.state } + ltx.outputs - private val contractAttachmentsByContract: Map> = getContractAttachmentsByContract() companion object { private val logger = contextLogger() } + /** + * This function is where the validity of transactions is determined. + * + * It is a critical piece of the security of the platform. + * + * @throws TransactionVerificationException + */ fun verify() { // checkNoNotaryChange and checkEncumbrancesValid are called here, and not in the c'tor, as they need access to the "outputs" // list, the contents of which need to be deserialized under the correct classloader. checkNoNotaryChange() checkEncumbrancesValid() + + // The following checks ensure the integrity of the current transaction and also of the future chain. + // See: https://docs.corda.net/head/api-contract-constraints.html + // A transaction contains both the data and the code that must be executed to validate the transition of the data. + // Transactions can be created by malicious adversaries, who can try to use code that allows them to create transactions that appear valid but are not. + + // 1. Check that there is one and only one attachment for each relevant contract. + val contractAttachmentsByContract = getUniqueContractAttachmentsByContract() + + // 2. Check that the attachments satisfy the constraints of the states. (The contract verification code is correct.) + verifyConstraints(contractAttachmentsByContract) + + // 3. Check that the actual state constraints are correct. This is necessary because transactions can be built by potentially malicious nodes + // who can create output states with a weaker constraint which can be exploited in a future transaction. + verifyConstraintsValidity(contractAttachmentsByContract) + + // 4. Check that the [TransactionState] objects are correctly formed. validateStatesAgainstContract() - val hashToSignatureConstrainedContracts = verifyConstraintsValidity() - verifyConstraints(hashToSignatureConstrainedContracts) + + // 5. Final step is to run the contract code. After the first 4 steps we are now sure that we are running the correct code. verifyContracts() } - // TODO: revisit to include contract version information /** - * This method may return more than one attachment for a given contract class. - * Specifically, this is the case for transactions combining hash and signature constraints where the hash constrained contract jar - * will be unsigned, and the signature constrained counterpart will be signed. + * This method returns the attachment with the code for each contract. + * It makes sure there is one and only one. + * This is an important piece of the security of transactions. */ - private fun getContractAttachmentsByContract(): Map> { + private fun getUniqueContractAttachmentsByContract(): Map { val contractClasses = allStates.map { it.contract }.toSet() - val result = mutableMapOf>() - for (attachment in ltx.attachments) { - if (attachment !is ContractAttachment) continue - for (contract in contractClasses) { - if (contract !in attachment.allContracts) continue - result[contract] = result.getOrDefault(contract, setOf(attachment)).plus(attachment) - } - } + // Check that there are no duplicate attachments added. + if (ltx.attachments.size != ltx.attachments.toSet().size) throw TransactionVerificationException.DuplicateAttachmentsRejection(ltx.id, ltx.attachments.groupBy { it }.filterValues { it.size > 1 }.keys.first()) + + // For each attachment this finds all the relevant state contracts that it provides. + // And then maps them to the attachment. + val contractAttachmentsPerContract: List> = ltx.attachments + .mapNotNull { it as? ContractAttachment } // only contract attachments are relevant. + .flatMap { attachment -> + // Find which relevant contracts are present in the current attachment and return them as a list + contractClasses + .filter { it in attachment.allContracts } + .map { it to attachment } + } + + // It is forbidden to add multiple attachments for the same contract. + val contractWithMultipleAttachments = contractAttachmentsPerContract + .groupBy { it.first } // Group by contract. + .filter { (_, attachments) -> attachments.size > 1 } // And only keep contracts that are in multiple attachments. It's guaranteed that attachments were unique by a previous check. + .keys.firstOrNull() // keep the first one - if any - to throw a meaningful exception. + if (contractWithMultipleAttachments != null) throw TransactionVerificationException.ConflictingAttachmentsRejection(ltx.id, contractWithMultipleAttachments) + + val result = contractAttachmentsPerContract.toMap() + + // Check that there is an attachment for each contract. + if (result.keys != contractClasses) throw TransactionVerificationException.MissingAttachmentRejection(ltx.id, contractClasses.minus(result.keys).first()) return result } @@ -207,11 +247,13 @@ class Verifier(val ltx: LedgerTransaction, private val transactionClassLoader: C } /** - * For all input and output [TransactionState]s, validates that the wrapped [ContractState] matches up with the + * For all input, output and reference [TransactionState]s, validates that the wrapped [ContractState] matches up with the * wrapped [Contract], as declared by the [BelongsToContract] annotation on the [ContractState]'s class. * * If the target platform version of the current CorDapp is lower than 4.0, a warning will be written to the log * if any mismatch is detected. If it is 4.0 or later, then [TransactionContractConflictException] will be thrown. + * + * Note: It should be enough to run this check only on the output states. Even more, it could be run only on distinct output contractClass/stateClass pairs. */ private fun validateStatesAgainstContract() = allStates.forEach(::validateStateAgainstContract) @@ -235,13 +277,12 @@ class Verifier(val ltx: LedgerTransaction, private val transactionClassLoader: C } /** - * Enforces the validity of the actual constraints. + * Enforces the validity of the actual constraints of the output states. * - Constraints should be one of the valid supported ones. - * - Constraints should propagate correctly if not marked otherwise. - * - * Returns set of contract classes that identify hash -> signature constraint switchover + * - Constraints should propagate correctly if not marked otherwise (in that case it is the responsibility of the contract to ensure that the output states are created properly). */ - private fun verifyConstraintsValidity(): MutableSet { + private fun verifyConstraintsValidity(contractAttachmentsByContract: Map) { + // First check that the constraints are valid. for (state in allStates) { checkConstraintValidity(state) @@ -252,47 +293,32 @@ class Verifier(val ltx: LedgerTransaction, private val transactionClassLoader: C val inputContractGroups = ltx.inputs.groupBy { it.state.contract } val outputContractGroups = ltx.outputs.groupBy { it.contract } - // identify any contract classes where input-output pair are transitioning from hash to signature constraints. - val hashToSignatureConstrainedContracts = mutableSetOf() - for (contractClassName in (inputContractGroups.keys + outputContractGroups.keys)) { - if (contractClassName.contractHasAutomaticConstraintPropagation(transactionClassLoader)) { - // 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] ?: emptyList()).map { it.state.constraint }.toSet() - val outputConstraints = (outputContractGroups[contractClassName] ?: emptyList()).map { it.constraint }.toSet() - outputConstraints.forEach { outputConstraint -> - inputConstraints.forEach { inputConstraint -> - val constraintAttachment = resolveAttachment(contractClassName) - if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, constraintAttachment))) { - throw TransactionVerificationException.ConstraintPropagationRejection( - ltx.id, - contractClassName, - inputConstraint, - outputConstraint - ) - } - // Hash to signature constraints auto-migration - if (outputConstraint is SignatureAttachmentConstraint && inputConstraint is HashAttachmentConstraint) - hashToSignatureConstrainedContracts.add(contractClassName) + + if (!contractClassName.contractHasAutomaticConstraintPropagation(transactionClassLoader)) { + contractClassName.warnContractWithoutConstraintPropagation() + continue + } + + val contractAttachment = contractAttachmentsByContract[contractClassName]!! + + // 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] ?: emptyList()).map { it.state.constraint }.toSet() + val outputConstraints = (outputContractGroups[contractClassName] ?: emptyList()).map { it.constraint }.toSet() + + outputConstraints.forEach { outputConstraint -> + inputConstraints.forEach { inputConstraint -> + if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, contractAttachment))) { + throw TransactionVerificationException.ConstraintPropagationRejection( + ltx.id, + contractClassName, + inputConstraint, + outputConstraint) } } - } else { - contractClassName.warnContractWithoutConstraintPropagation() } } - return hashToSignatureConstrainedContracts - } - - private fun resolveAttachment(contractClassName: ContractClassName): AttachmentWithContext { - val unsignedAttachment = contractAttachmentsByContract[contractClassName]!!.firstOrNull { !it.isSigned } - val signedAttachment = contractAttachmentsByContract[contractClassName]!!.firstOrNull { it.isSigned } - return when { - (unsignedAttachment != null && signedAttachment != null) -> AttachmentWithContext(signedAttachment, contractClassName, ltx.networkParameters!!) - (unsignedAttachment != null) -> AttachmentWithContext(unsignedAttachment, contractClassName, ltx.networkParameters!!) - (signedAttachment != null) -> AttachmentWithContext(signedAttachment, contractClassName, ltx.networkParameters!!) - else -> throw TransactionVerificationException.ContractConstraintRejection(ltx.id, contractClassName) - } } /** @@ -302,31 +328,19 @@ class Verifier(val ltx: LedgerTransaction, private val transactionClassLoader: C * * @throws TransactionVerificationException if the constraints fail to verify */ - private fun verifyConstraints(hashToSignatureConstrainedContracts: MutableSet) { - for (state in allStates) { - if (state.constraint is SignatureAttachmentConstraint) { - checkMinimumPlatformVersion(ltx.networkParameters!!.minimumPlatformVersion, 4, "Signature constraints") - } + private fun verifyConstraints(contractAttachmentsByContract: Map) { + // For each contract/constraint pair check that the relevant attachment is valid. + allStates.map { it.contract to it.constraint }.toSet().forEach { (contract, constraint) -> + if (constraint is SignatureAttachmentConstraint) + checkMinimumPlatformVersion(ltx.networkParameters?.minimumPlatformVersion ?: 1, 4, "Signature constraints") - val constraintAttachment = - if (state.contract in hashToSignatureConstrainedContracts && state.constraint is HashAttachmentConstraint) { - val unsignedAttachment = contractAttachmentsByContract[state.contract].unsigned - ?: throw TransactionVerificationException.MissingAttachmentRejection(ltx.id, state.contract) - AttachmentWithContext(unsignedAttachment, state.contract, ltx.networkParameters!!) - } else if (state.contract in hashToSignatureConstrainedContracts && state.constraint is SignatureAttachmentConstraint) { - val signedAttachment = contractAttachmentsByContract[state.contract].signed - ?: throw TransactionVerificationException.MissingAttachmentRejection(ltx.id, state.contract) - AttachmentWithContext(signedAttachment, state.contract, ltx.networkParameters!!) - } - else { - // standard processing logic - val contractAttachment = contractAttachmentsByContract[state.contract]?.firstOrNull() - ?: throw TransactionVerificationException.MissingAttachmentRejection(ltx.id, state.contract) - AttachmentWithContext(contractAttachment, state.contract, ltx.networkParameters!!) - } + // We already checked that there is one and only one attachment. + val contractAttachment = contractAttachmentsByContract[contract]!! - if (!state.constraint.isSatisfiedBy(constraintAttachment)) { - throw TransactionVerificationException.ContractConstraintRejection(ltx.id, state.contract) + val constraintAttachment = AttachmentWithContext(contractAttachment, contract, ltx.networkParameters!!.whitelistedContractImplementations) + + if (!constraint.isSatisfiedBy(constraintAttachment)) { + throw TransactionVerificationException.ContractConstraintRejection(ltx.id, contract) } } } @@ -334,12 +348,25 @@ class Verifier(val ltx: LedgerTransaction, private val transactionClassLoader: C /** * 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. + * + * Note: Reference states are not verified. */ private fun verifyContracts() { - val contractClasses = (inputStates + ltx.outputs).toSet() - .map { it.contract to contractClassFor(it.contract, it.data.javaClass.classLoader) } - val contractInstances = contractClasses.map { (contractClassName, contractClass) -> + // Loads the contract class from the transactionClassLoader. + fun contractClassFor(className: ContractClassName) = try { + transactionClassLoader.loadClass(className).asSubclass(Contract::class.java) + } catch (e: Exception) { + throw TransactionVerificationException.ContractCreationError(ltx.id, className, e) + } + + val contractClasses: Map> = (inputStates + ltx.outputs) + .map { it.contract } + .toSet() + .map { contract -> contract to contractClassFor(contract) } + .toMap() + + val contractInstances: List = contractClasses.map { (contractClassName, contractClass) -> try { contractClass.newInstance() } catch (e: Exception) { @@ -351,19 +378,9 @@ class Verifier(val ltx: LedgerTransaction, private val transactionClassLoader: C try { contract.verify(ltx) } catch (e: Exception) { + logger.error("Error validating transaction ${ltx.id}.", e) throw TransactionVerificationException.ContractRejection(ltx.id, contract, e) } } } - - private fun contractClassFor(className: ContractClassName, classLoader: ClassLoader): Class { - return try { - classLoader.loadClass(className).asSubclass(Contract::class.java) - } catch (e: Exception) { - throw TransactionVerificationException.ContractCreationError(ltx.id, className, e) - } - } - - private val Set?.unsigned: ContractAttachment? get() = this?.firstOrNull { !it.isSigned } - private val Set?.signed: ContractAttachment? get() = this?.firstOrNull { it.isSigned } } diff --git a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt index 0dace7b0a2..6aab69ae2b 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt @@ -265,7 +265,7 @@ data class ContractUpgradeLedgerTransaction( legacyContractAttachment as? ContractAttachment ?: ContractAttachment.create(legacyContractAttachment, legacyContractClassName, signerKeys = legacyContractAttachment.signerKeys), upgradedContract.legacyContract, - networkParameters) + networkParameters.whitelistedContractImplementations) // TODO: exclude encumbrance states from this check check(inputs.all { it.state.constraint.isSatisfiedBy(attachmentForConstraintVerification) }) { diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 59862de228..7311397c21 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -237,7 +237,7 @@ open class TransactionBuilder( .groupBy { it.first }.mapValues { it.value.map { e -> e.second }.toSet() } // For each contract, resolve the AutomaticPlaceholderConstraint, and select the attachment. - val contractAttachmentsAndResolvedOutputStates: List, List>?>> = allContracts.toSet() + val contractAttachmentsAndResolvedOutputStates: List>?>> = allContracts.toSet() .map { ctr -> handleContract(ctr, inputContractGroups[ctr], contractClassNameToInputStateRef[ctr], outputContractGroups[ctr], explicitAttachmentContractsMap[ctr], services) } @@ -248,7 +248,7 @@ open class TransactionBuilder( // The output states need to preserve the order in which they were added. val resolvedOutputStatesInTheOriginalOrder: List> = outputStates().map { os -> resolvedStates.find { rs -> rs.data == os.data && rs.encumbrance == os.encumbrance }!! } - val attachments: Collection = contractAttachmentsAndResolvedOutputStates.flatMap { it.first } + refStateContractAttachments + val attachments: Collection = contractAttachmentsAndResolvedOutputStates.map { it.first } + refStateContractAttachments return Pair(attachments, resolvedOutputStatesInTheOriginalOrder) } @@ -277,40 +277,9 @@ open class TransactionBuilder( outputStates: List>?, explicitContractAttachment: AttachmentId?, services: ServicesForResolution - ): Pair, List>?> { + ): Pair>?> { val inputsAndOutputs = (inputStates ?: emptyList()) + (outputStates ?: emptyList()) - // Hash to Signature constraints migration switchover (only applicable from version 4 onwards) - // identify if any input-output pairs are transitioning from hash to signature constraints: - // 1. output states contain implicitly selected hash constraint (pre-existing from set of unconsumed states in a nodes vault) or explicitly set SignatureConstraint - // 2. node has signed jar for associated contract class and version - if (services.networkParameters.minimumPlatformVersion >= 4) { - val inputsHashConstraints = inputStates?.filter { it.constraint is HashAttachmentConstraint } ?: emptyList() - val outputHashConstraints = outputStates?.filter { it.constraint is HashAttachmentConstraint } ?: emptyList() - val outputSignatureConstraints = outputStates?.filter { it.constraint is SignatureAttachmentConstraint } ?: emptyList() - if (inputsHashConstraints.isNotEmpty() && (outputHashConstraints.isNotEmpty() || outputSignatureConstraints.isNotEmpty())) { - val attachmentIds = services.attachments.getLatestContractAttachments(contractClassName) - // only switchover if we have both signed and unsigned attachments for the given contract class name - if (attachmentIds.isNotEmpty() && attachmentIds.size == 2) { - val attachmentsToUse = attachmentIds.map { - services.attachments.openAttachment(it)?.let { it as ContractAttachment } - ?: throw IllegalArgumentException("Contract attachment $it for $contractClassName is missing.") - } - val signedAttachment = attachmentsToUse.filter { it.isSigned }.firstOrNull() - ?: throw IllegalArgumentException("Signed contract attachment for $contractClassName is missing.") - val outputConstraints = - if (outputHashConstraints.isNotEmpty()) { - log.warn("Switching output states from hash to signed constraints using signers in signed contract attachment given by ${signedAttachment.id}") - val outputsSignatureConstraints = outputHashConstraints.map { it.copy(constraint = SignatureAttachmentConstraint(signedAttachment.signerKeys.first())) } - outputs.addAll(outputsSignatureConstraints) - outputs.removeAll(outputHashConstraints) - outputsSignatureConstraints - } else outputSignatureConstraints - return Pair(attachmentIds.toSet(), outputConstraints) - } - } - } - // 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 } @@ -328,6 +297,12 @@ open class TransactionBuilder( "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 @@ -346,12 +321,12 @@ open class TransactionBuilder( // For Exit transactions (no output states) there is no need to resolve the output constraints. if (outputStates == null) { - return Pair(setOf(selectedAttachmentId), 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(setOf(selectedAttachmentId), outputStates) + return Pair(selectedAttachmentId, outputStates) } // The final step is to resolve AutomaticPlaceholderConstraint. @@ -364,7 +339,7 @@ open class TransactionBuilder( val defaultOutputConstraint = selectAttachmentConstraint(contractClassName, inputStates, attachmentToUse, services) // Sanity check that the selected attachment actually passes. - val constraintAttachment = AttachmentWithContext(attachmentToUse, contractClassName, services.networkParameters) + val constraintAttachment = AttachmentWithContext(attachmentToUse, contractClassName, services.networkParameters.whitelistedContractImplementations) require(defaultOutputConstraint.isSatisfiedBy(constraintAttachment)) { "Selected output constraint: $defaultOutputConstraint not satisfying $selectedAttachmentId" } val resolvedOutputStates = outputStates.map { @@ -374,14 +349,14 @@ open class TransactionBuilder( } 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, constraintAttachment)) { "Output state constraint $outputConstraint cannot be transitioned from ${input.constraint}" } + require(outputConstraint.canBeTransitionedFrom(input.constraint, attachmentToUse)) { "Output state constraint $outputConstraint cannot be transitioned from ${input.constraint}" } } require(outputConstraint.isSatisfiedBy(constraintAttachment)) { "Output state constraint check fails. $outputConstraint" } it } } - return Pair(setOf(selectedAttachmentId), resolvedOutputStates) + return Pair(selectedAttachmentId, resolvedOutputStates) } /** diff --git a/core/src/test/kotlin/net/corda/core/contracts/ConstraintsPropagationTests.kt b/core/src/test/kotlin/net/corda/core/contracts/ConstraintsPropagationTests.kt index b63abbde7d..3496e66fe2 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/ConstraintsPropagationTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/ConstraintsPropagationTests.kt @@ -199,14 +199,14 @@ class ConstraintsPropagationTests { 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") + fails() }) 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") + fails() } } } @@ -268,7 +268,7 @@ class ConstraintsPropagationTests { 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") + fails() } } } @@ -319,36 +319,21 @@ class ConstraintsPropagationTests { val netParams = testNetworkParameters(minimumPlatformVersion = 4, packageOwnership = mapOf( "net.corda.core.contracts" to ALICE_PARTY.owningKey)) - // attachment with context (both unsigned and signed attachments representing same contract) - val attachmentWithContext = mock() - whenever(attachmentWithContext.contractAttachment).thenReturn(attachmentSigned) - whenever(attachmentWithContext.contract).thenReturn(propagatingContractClassName) - whenever(attachmentWithContext.networkParameters).thenReturn(netParams) - ledgerServices.attachments.importContractAttachment(attachmentIdSigned, attachmentSigned) ledgerServices.attachments.importContractAttachment(attachmentIdUnsigned, attachmentUnsigned) // propagation check - assertTrue(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(HashAttachmentConstraint(allOnesHash), attachmentWithContext)) + // TODO - enable once the logic to transition has been added. + assertFalse(SignatureAttachmentConstraint(ALICE_PUBKEY).canBeTransitionedFrom(HashAttachmentConstraint(allOnesHash), attachmentSigned)) } @Test fun `Attachment canBeTransitionedFrom behaves as expected`() { // signed attachment (for signature constraint) - val attachmentSigned = mock() - whenever(attachmentSigned.signerKeys).thenReturn(listOf(ALICE_PARTY.owningKey)) - whenever(attachmentSigned.allContracts).thenReturn(setOf(propagatingContractClassName)) - - // network parameters - val netParams = testNetworkParameters(minimumPlatformVersion = 4, - packageOwnership = mapOf(propagatingContractClassName to ALICE_PARTY.owningKey)) - - // attachment with context - val attachment = mock() - whenever(attachment.networkParameters).thenReturn(netParams) - whenever(attachment.contractAttachment).thenReturn(attachmentSigned) + val attachment = mock() whenever(attachment.signerKeys).thenReturn(listOf(ALICE_PARTY.owningKey)) + whenever(attachment.allContracts).thenReturn(setOf(propagatingContractClassName)) // Exhaustive positive check assertTrue(HashAttachmentConstraint(SecureHash.randomSHA256()).canBeTransitionedFrom(SignatureAttachmentConstraint(ALICE_PUBKEY), attachment))