Skip to content

Commit

Permalink
CORDA-2150 signature constraints non-downgrade rule (corda#4262)
Browse files Browse the repository at this point in the history
Contract class version non-downgrade rule is check by LedgerTransaction.verify().
TransactionBuilder.toWireTransaction(services: ServicesForResolution) selects attachments for the transaction which obey non downgrade rule.
New ServiceHub method loadAttachmentConstraint(stateRef: StateRef, forContractClassName: ContractClassName? = null) retrieves the attachment contract related to transaction output states of given contract class name.
  • Loading branch information
szymonsztuka authored Dec 11, 2018
1 parent b14f3d6 commit 4799df9
Show file tree
Hide file tree
Showing 32 changed files with 628 additions and 91 deletions.
1 change: 1 addition & 0 deletions .ci/api-current.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4946,6 +4946,7 @@ public static final class net.corda.core.transactions.LedgerTransaction$InOutGro
@CordaSerializable
public final class net.corda.core.transactions.MissingContractAttachments extends net.corda.core.flows.FlowException
public <init>(java.util.List<? extends net.corda.core.contracts.TransactionState<? extends net.corda.core.contracts.ContractState>>)
public <init>(java.util.List<? extends net.corda.core.contracts.TransactionState<? extends net.corda.core.contracts.ContractState>>, Integer)
@NotNull
public final java.util.List<net.corda.core.contracts.TransactionState<net.corda.core.contracts.ContractState>> getStates()
##
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.WireTransaction

@Suppress("MemberVisibilityCanBePrivate")
//TODO the use of deprecated toLedgerTransaction need to be revisited as resolveContractAttachment requires attachments of the transactions which created input states...
//TODO ...to check contract version non downgrade rule, curretly dummy Attachment if not fund is used which sets contract version to '1'
@CordaSerializable
class TransactionVerificationRequest(val wtxToVerify: SerializedBytes<WireTransaction>,
val dependencies: Array<SerializedBytes<WireTransaction>>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,13 @@ class ContractAttachment @JvmOverloads constructor(
override fun toString(): String {
return "ContractAttachment(attachment=${attachment.id}, contracts='$allContracts', uploader='$uploader', signed='$isSigned', version='$version')"
}

companion object {
fun getContractVersion(attachment: Attachment) : Version =
if (attachment is ContractAttachment) {
attachment.version
} else {
DEFAULT_CORDAPP_VERSION
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -238,4 +238,9 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S
@CordaSerializable
@KeepForDJVM
class OverlappingAttachmentsException(path: String) : Exception("Multiple attachments define a file at path `$path`.")

@KeepForDJVM
class TransactionVerificationVersionException(txId: SecureHash, contractClassName: ContractClassName, inputVersion: String, outputVersion: String)
: TransactionVerificationException(txId, " No-Downgrade Rule has been breached for contract class $contractClassName. " +
"The output state contract version '$outputVersion' is lower that the version of the input state '$inputVersion'.", null)
}
6 changes: 6 additions & 0 deletions core/src/main/kotlin/net/corda/core/contracts/Version.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package net.corda.core.contracts

/**
* Contract version and flow versions are integers.
*/
typealias Version = Int
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const val RPC_UPLOADER = "rpc"
const val P2P_UPLOADER = "p2p"
const val UNKNOWN_UPLOADER = "unknown"

private val TRUSTED_UPLOADERS = listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER)
val TRUSTED_UPLOADERS = listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER)

fun isUploaderTrusted(uploader: String?): Boolean = uploader in TRUSTED_UPLOADERS

Expand Down
3 changes: 3 additions & 0 deletions core/src/main/kotlin/net/corda/core/node/ServiceHub.kt
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ interface ServicesForResolution {
// as the existing transaction store will become encrypted at some point
@Throws(TransactionResolutionException::class)
fun loadStates(stateRefs: Set<StateRef>): Set<StateAndRef<ContractState>>

@Throws(TransactionResolutionException::class, AttachmentResolutionException::class)
fun loadContractAttachment(stateRef: StateRef, forContractClassName: ContractClassName? = null): Attachment
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import net.corda.core.KeepForDJVM
import net.corda.core.contracts.*
import net.corda.core.contracts.TransactionVerificationException.TransactionContractConflictException
import net.corda.core.contracts.TransactionVerificationException.TransactionRequiredContractUnspecifiedException
import net.corda.core.contracts.Version
import net.corda.core.cordapp.DEFAULT_CORDAPP_VERSION
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.isFulfilledBy
import net.corda.core.identity.Party
Expand Down Expand Up @@ -54,7 +56,8 @@ private constructor(
val privacySalt: PrivacySalt,
/** Network parameters that were in force when the transaction was notarised. */
override val networkParameters: NetworkParameters?,
override val references: List<StateAndRef<ContractState>>
override val references: List<StateAndRef<ContractState>>,
private val inputStatesContractClassNameToMaxVersion: Map<ContractClassName,Version>
//DOCEND 1
) : FullTransaction() {
// These are not part of the c'tor above as that defines LedgerTransaction's serialisation format
Expand Down Expand Up @@ -87,9 +90,10 @@ private constructor(
references: List<StateAndRef<ContractState>>,
componentGroups: List<ComponentGroup>? = null,
serializedInputs: List<SerializedStateAndRef>? = null,
serializedReferences: List<SerializedStateAndRef>? = null
serializedReferences: List<SerializedStateAndRef>? = null,
inputStatesContractClassNameToMaxVersion: Map<ContractClassName,Version>
): LedgerTransaction {
return LedgerTransaction(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, references).apply {
return LedgerTransaction(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, references, inputStatesContractClassNameToMaxVersion).apply {
this.componentGroups = componentGroups
this.serializedInputs = serializedInputs
this.serializedReferences = serializedReferences
Expand Down Expand Up @@ -134,7 +138,7 @@ private constructor(

val internalTx = createLtxForVerification()

// TODO - verify for version downgrade
validateContractVersions(contractAttachmentsByContract)
validatePackageOwnership(contractAttachmentsByContract)
validateStatesAgainstContract(internalTx)
val hashToSignatureConstrainedContracts = verifyConstraintsValidity(internalTx, contractAttachmentsByContract, transactionClassLoader)
Expand All @@ -143,6 +147,21 @@ private constructor(
}
}

/**
* Verify that contract class versions of output states are not lower that versions of relevant input states.
*/
@Throws(TransactionVerificationException::class)
private fun validateContractVersions(contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>) {
contractAttachmentsByContract.forEach { contractClassName, attachments ->
val outputVersion = attachments.signed?.version ?: attachments.unsigned?.version ?: DEFAULT_CORDAPP_VERSION
inputStatesContractClassNameToMaxVersion[contractClassName]?.let {
if (it > outputVersion) {
throw TransactionVerificationException.TransactionVerificationVersionException(this.id, contractClassName, "$it", "$outputVersion")
}
}
}
}

/**
* For all input and output [TransactionState]s, validates that the wrapped [ContractState] matches up with the
* wrapped [Contract], as declared by the [BelongsToContract] annotation on the [ContractState]'s class.
Expand Down Expand Up @@ -351,7 +370,8 @@ private constructor(
timeWindow = this.timeWindow,
privacySalt = this.privacySalt,
networkParameters = this.networkParameters,
references = deserializedReferences
references = deserializedReferences,
inputStatesContractClassNameToMaxVersion = emptyMap()
)
} else {
// This branch is only present for backwards compatibility.
Expand Down Expand Up @@ -864,7 +884,7 @@ private constructor(
notary: Party?,
timeWindow: TimeWindow?,
privacySalt: PrivacySalt
) : this(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, null, emptyList())
) : this(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, null, emptyList(), emptyMap())

@Deprecated("LedgerTransaction should not be created directly, use WireTransaction.toLedgerTransaction instead.")
@DeprecatedConstructorForDeserialization(1)
Expand All @@ -878,7 +898,7 @@ private constructor(
timeWindow: TimeWindow?,
privacySalt: PrivacySalt,
networkParameters: NetworkParameters
) : this(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, emptyList())
) : this(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, emptyList(), emptyMap())

@Deprecated("LedgerTransactions should not be created directly, use WireTransaction.toLedgerTransaction instead.")
fun copy(inputs: List<StateAndRef<ContractState>>,
Expand All @@ -900,7 +920,8 @@ private constructor(
timeWindow = timeWindow,
privacySalt = privacySalt,
networkParameters = networkParameters,
references = references
references = references,
inputStatesContractClassNameToMaxVersion = emptyMap()
)
}

Expand All @@ -925,7 +946,8 @@ private constructor(
timeWindow = timeWindow,
privacySalt = privacySalt,
networkParameters = networkParameters,
references = references
references = references,
inputStatesContractClassNameToMaxVersion = emptyMap()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import net.corda.core.contracts.ContractState
import net.corda.core.contracts.TransactionState
import net.corda.core.flows.FlowException
import net.corda.core.serialization.CordaSerializable

import net.corda.core.contracts.Version
/**
* A contract attachment was missing when trying to automatically attach all known contract attachments
*
* @property states States which have contracts that do not have corresponding attachments in the attachment store.
*/
@CordaSerializable
@KeepForDJVM
class MissingContractAttachments(val states: List<TransactionState<ContractState>>)
: FlowException("Cannot find contract attachments for ${states.map { it.contract }.distinct()}. " +
class MissingContractAttachments @JvmOverloads constructor (val states: List<TransactionState<ContractState>>, minimumRequiredContractClassVersion: Version? = null)
: FlowException("Cannot find contract attachments for ${states.map { it.contract }.distinct()}${minimumRequiredContractClassVersion?.let { ", minimum required contract class version $minimumRequiredContractClassVersion"}}. " +
"See https://docs.corda.net/api-contract-constraints.html#debugging")
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import net.corda.core.CordaInternal
import net.corda.core.DeleteForDJVM
import net.corda.core.contracts.*
import net.corda.core.cordapp.DEFAULT_CORDAPP_VERSION
import net.corda.core.contracts.ContractAttachment.Companion.getContractVersion
import net.corda.core.crypto.*
import net.corda.core.identity.Party
import net.corda.core.internal.*
Expand Down Expand Up @@ -58,7 +59,7 @@ open class TransactionBuilder @JvmOverloads constructor(
private val log = contextLogger()
}

private val inputsWithTransactionState = arrayListOf<TransactionState<ContractState>>()
private val inputsWithTransactionState = arrayListOf<StateAndRef<ContractState>>()
private val referencesWithTransactionState = arrayListOf<TransactionState<ContractState>>()

/**
Expand Down Expand Up @@ -122,7 +123,7 @@ open class TransactionBuilder @JvmOverloads constructor(
val (allContractAttachments: Collection<SecureHash>, resolvedOutputs: List<TransactionState<ContractState>>) = selectContractAttachmentsAndOutputStateConstraints(services, serializationContext)

// Final sanity check that all states have the correct constraints.
for (state in (inputsWithTransactionState + resolvedOutputs)) {
for (state in (inputsWithTransactionState.map { it.state } + resolvedOutputs)) {
checkConstraintValidity(state)
}

Expand Down Expand Up @@ -165,7 +166,7 @@ open class TransactionBuilder @JvmOverloads constructor(

val explicitAttachmentContractsMap: Map<ContractClassName, SecureHash> = explicitAttachmentContracts.toMap()

val inputContractGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = inputsWithTransactionState.groupBy { it.contract }
val inputContractGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = inputsWithTransactionState.map {it.state}.groupBy { it.contract }
val outputContractGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = outputs.groupBy { it.contract }

val allContracts: Set<ContractClassName> = inputContractGroups.keys + outputContractGroups.keys
Expand All @@ -176,13 +177,15 @@ open class TransactionBuilder @JvmOverloads constructor(
val refStateContractAttachments: List<AttachmentId> = referenceStateGroups
.filterNot { it.key in allContracts }
.map { refStateEntry ->
selectAttachmentThatSatisfiesConstraints(true, refStateEntry.key, refStateEntry.value, services)
selectAttachmentThatSatisfiesConstraints(true, refStateEntry.key, refStateEntry.value, emptySet(), services)
}

val contractClassNameToInputStateRef : Map<ContractClassName, Set<StateRef>> = inputsWithTransactionState.map { Pair(it.state.contract,it.ref) }.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()
.map { ctr ->
handleContract(ctr, inputContractGroups[ctr], outputContractGroups[ctr], explicitAttachmentContractsMap[ctr], services)
handleContract(ctr, inputContractGroups[ctr], contractClassNameToInputStateRef[ctr], outputContractGroups[ctr], explicitAttachmentContractsMap[ctr], services)
}

val resolvedStates: List<TransactionState<ContractState>> = contractAttachmentsAndResolvedOutputStates.mapNotNull { it.second }
Expand Down Expand Up @@ -216,6 +219,7 @@ open class TransactionBuilder @JvmOverloads constructor(
private fun handleContract(
contractClassName: ContractClassName,
inputStates: List<TransactionState<ContractState>>?,
inputStateRefs: Set<StateRef>?,
outputStates: List<TransactionState<ContractState>>?,
explicitContractAttachment: AttachmentId?,
services: ServicesForResolution
Expand Down Expand Up @@ -275,6 +279,7 @@ open class TransactionBuilder @JvmOverloads constructor(
false,
contractClassName,
inputsAndOutputs.filterNot { it.constraint in automaticConstraints },
inputStateRefs,
services)

// This will contain the hash of the JAR that will be used by this Transaction.
Expand Down Expand Up @@ -399,23 +404,20 @@ open class TransactionBuilder @JvmOverloads constructor(

/**
* This method should only be called for upgradeable contracts.
*
* For now we use the currently installed CorDapp version.
*/
private fun selectAttachmentThatSatisfiesConstraints(isReference: Boolean, contractClassName: String, states: List<TransactionState<ContractState>>, services: ServicesForResolution): AttachmentId {
private fun selectAttachmentThatSatisfiesConstraints(isReference: Boolean, contractClassName: String, states: List<TransactionState<ContractState>>, stateRefs: Set<StateRef>?, services: ServicesForResolution): AttachmentId {
val constraints = states.map { it.constraint }
require(constraints.none { it in automaticConstraints })
require(isReference || constraints.none { it is HashAttachmentConstraint })

//TODO will be set by the code pending in the other PR
val minimumRequiredContractClassVersion = DEFAULT_CORDAPP_VERSION

//TODO consider move it to attachment service method e.g. getContractAttachmentWithHighestVersion(contractClassName, minContractVersion)
val minimumRequiredContractClassVersion = stateRefs?.map { getContractVersion(services.loadContractAttachment(it)) }?.max() ?: DEFAULT_CORDAPP_VERSION
//TODO could be moved as a single method of the attachment service method e.g. getContractAttachmentWithHighestContractVersion(contractClassName, minContractVersion)
val attachmentQueryCriteria = AttachmentQueryCriteria.AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf(contractClassName)),
versionCondition = Builder.greaterThanOrEqual(minimumRequiredContractClassVersion))
versionCondition = Builder.greaterThanOrEqual(minimumRequiredContractClassVersion),
uploaderCondition = Builder.`in`(TRUSTED_UPLOADERS))
val attachmentSort = AttachmentSort(listOf(AttachmentSort.AttachmentSortColumn(AttachmentSort.AttachmentSortAttribute.VERSION, Sort.Direction.DESC)))

return services.attachments.queryAttachments(attachmentQueryCriteria, attachmentSort).firstOrNull() ?: throw MissingContractAttachments(states)
return services.attachments.queryAttachments(attachmentQueryCriteria, attachmentSort).firstOrNull() ?: throw MissingContractAttachments(states, minimumRequiredContractClassVersion)
}

private fun useWhitelistedByZoneAttachmentConstraint(contractClassName: ContractClassName, networkParameters: NetworkParameters) = contractClassName in networkParameters.whitelistedContractImplementations.keys
Expand Down Expand Up @@ -526,7 +528,7 @@ open class TransactionBuilder @JvmOverloads constructor(
open fun addInputState(stateAndRef: StateAndRef<*>) = apply {
checkNotary(stateAndRef)
inputs.add(stateAndRef.ref)
inputsWithTransactionState.add(stateAndRef.state)
inputsWithTransactionState.add(stateAndRef)
resolveStatePointers(stateAndRef.state)
return this
}
Expand Down
Loading

0 comments on commit 4799df9

Please sign in to comment.