mirror of
https://github.com/corda/corda.git
synced 2025-04-16 07:27:17 +00:00
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:
commit
b65ebdd116
@ -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 {
|
||||
|
@ -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].
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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.") } }
|
||||
|
@ -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 }
|
||||
}
|
||||
|
@ -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) }) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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))
|
||||
|
Loading…
x
Reference in New Issue
Block a user