Merge pull request #4747 from corda/feature/CORDA-2570/revert-to-one-attachment-per-contract

CORDA-2570 revert multiple contract attachments per transaction
This commit is contained in:
Mike Hearn 2019-02-13 16:42:30 +01:00 committed by GitHub
commit b65ebdd116
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 151 additions and 197 deletions

View File

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

View File

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

View File

@ -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<String, List<AttachmentId>>
) : Attachment by contractAttachment {
init {
require(contract in contractAttachment.allContracts) {

View File

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

View File

@ -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<ContractClassName>): 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<String>): Boolean {
return packages.all { outer -> packages.none { inner -> inner != outer && inner.startsWith("$outer.") } }

View File

@ -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<Attachment> ): CordaFuture<*>
fun verify(transaction: LedgerTransaction, extraAttachments: List<Attachment>): CordaFuture<*>
}
/**
@ -29,44 +29,84 @@ fun LedgerTransaction.prepareVerify(extraAttachments: List<Attachment>) = 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<ContractClassName, Version>) {
private val inputStates: List<TransactionState<*>> = ltx.inputs.map { it.state }
private val allStates: List<TransactionState<*>> = inputStates + ltx.references.map { it.state } + ltx.outputs
private val contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>> = 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<ContractClassName, Set<ContractAttachment>> {
private fun getUniqueContractAttachmentsByContract(): Map<ContractClassName, ContractAttachment> {
val contractClasses = allStates.map { it.contract }.toSet()
val result = mutableMapOf<ContractClassName, Set<ContractAttachment>>()
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<Pair<ContractClassName, ContractAttachment>> = 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<ContractClassName> {
private fun verifyConstraintsValidity(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) {
// 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<ContractClassName>()
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<ContractClassName>) {
for (state in allStates) {
if (state.constraint is SignatureAttachmentConstraint) {
checkMinimumPlatformVersion(ltx.networkParameters!!.minimumPlatformVersion, 4, "Signature constraints")
}
private fun verifyConstraints(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) {
// 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<ContractClassName, Class<out Contract>> = (inputStates + ltx.outputs)
.map { it.contract }
.toSet()
.map { contract -> contract to contractClassFor(contract) }
.toMap()
val contractInstances: List<Contract> = 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<out Contract> {
return try {
classLoader.loadClass(className).asSubclass(Contract::class.java)
} catch (e: Exception) {
throw TransactionVerificationException.ContractCreationError(ltx.id, className, e)
}
}
private val Set<ContractAttachment>?.unsigned: ContractAttachment? get() = this?.firstOrNull { !it.isSigned }
private val Set<ContractAttachment>?.signed: ContractAttachment? get() = this?.firstOrNull { it.isSigned }
}

View File

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

View File

@ -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<Pair<Set<AttachmentId>, List<TransactionState<ContractState>>?>> = allContracts.toSet()
val contractAttachmentsAndResolvedOutputStates: List<Pair<AttachmentId, List<TransactionState<ContractState>>?>> = 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<TransactionState<ContractState>> = outputStates().map { os -> resolvedStates.find { rs -> rs.data == os.data && rs.encumbrance == os.encumbrance }!! }
val attachments: Collection<AttachmentId> = contractAttachmentsAndResolvedOutputStates.flatMap { it.first } + refStateContractAttachments
val attachments: Collection<AttachmentId> = contractAttachmentsAndResolvedOutputStates.map { it.first } + refStateContractAttachments
return Pair(attachments, resolvedOutputStatesInTheOriginalOrder)
}
@ -277,40 +277,9 @@ open class TransactionBuilder(
outputStates: List<TransactionState<ContractState>>?,
explicitContractAttachment: AttachmentId?,
services: ServicesForResolution
): Pair<Set<AttachmentId>, List<TransactionState<ContractState>>?> {
): Pair<AttachmentId, List<TransactionState<ContractState>>?> {
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)
}
/**

View File

@ -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<AttachmentWithContext>()
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<ContractAttachment>()
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<AttachmentWithContext>()
whenever(attachment.networkParameters).thenReturn(netParams)
whenever(attachment.contractAttachment).thenReturn(attachmentSigned)
val attachment = mock<ContractAttachment>()
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))