ENT-2923 - remove db access code from the verification thread pool (#4504)

* ENT-2923 - remove db access code from the verification thread pool

* Remove worker pool for tx verification and disable db access.

* Address code review comments
This commit is contained in:
Tudor Malene 2019-01-08 14:37:26 +00:00 committed by GitHub
parent ce250cfafd
commit d33cb16c5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 435 additions and 407 deletions

View File

@ -2,8 +2,13 @@ package net.corda.core.internal
import net.corda.core.DeleteForDJVM
import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.*
import net.corda.core.contracts.TransactionVerificationException.TransactionContractConflictException
import net.corda.core.crypto.isFulfilledBy
import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.internal.rules.StateContractValidationEnforcementRule
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.utilities.contextLogger
@DeleteForDJVM
interface TransactionVerifierServiceInternal {
@ -17,4 +22,387 @@ interface TransactionVerifierServiceInternal {
/**
* Defined here for visibility reasons.
*/
fun LedgerTransaction.verify(extraAttachments: List<Attachment>) = this.verifyInternal(extraAttachments)
fun LedgerTransaction.prepareVerify(extraAttachments: List<Attachment>) = this.internalPrepareVerify(extraAttachments)
/**
* Because we create a separate [LedgerTransaction] onto which we need to perform verification, it becomes important we don't verify the
* wrong object instance. This class helps avoid that.
*/
class Verifier(val ltx: LedgerTransaction, val transactionClassLoader: ClassLoader, private val inputStatesContractClassNameToMaxVersion: 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()
}
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()
validateContractVersions()
validatePackageOwnership()
validateStatesAgainstContract()
val hashToSignatureConstrainedContracts = verifyConstraintsValidity()
verifyConstraints(hashToSignatureConstrainedContracts)
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.
*/
private fun getContractAttachmentsByContract(): Map<ContractClassName, Set<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)
}
}
return result
}
/**
* Make sure the notary has stayed the same. As we can't tell how inputs and outputs connect, if there
* are any inputs or reference inputs, all outputs must have the same notary.
*
* TODO: Is that the correct set of restrictions? May need to come back to this, see if we can be more
* flexible on output notaries.
*/
private fun checkNoNotaryChange() {
if (ltx.notary != null && (ltx.inputs.isNotEmpty() || ltx.references.isNotEmpty())) {
ltx.outputs.forEach {
if (it.notary != ltx.notary) {
throw TransactionVerificationException.NotaryChangeInWrongTransactionType(ltx.id, ltx.notary, it.notary)
}
}
}
}
private fun checkEncumbrancesValid() {
// Validate that all encumbrances exist within the set of input states.
ltx.inputs
.filter { it.state.encumbrance != null }
.forEach { (state, ref) -> checkInputEncumbranceStateExists(state, ref) }
// Check that in the outputs,
// a) an encumbered state does not refer to itself as the encumbrance
// b) the number of outputs can contain the encumbrance
// c) the bi-directionality (full cycle) property is satisfied
// d) encumbered output states are assigned to the same notary.
val statesAndEncumbrance = ltx.outputs
.withIndex()
.filter { it.value.encumbrance != null }
.map { Pair(it.index, it.value.encumbrance!!) }
if (!statesAndEncumbrance.isEmpty()) {
checkBidirectionalOutputEncumbrances(statesAndEncumbrance)
checkNotariesOutputEncumbrance(statesAndEncumbrance)
}
}
private fun checkInputEncumbranceStateExists(state: TransactionState<ContractState>, ref: StateRef) {
val encumbranceStateExists = ltx.inputs.any {
it.ref.txhash == ref.txhash && it.ref.index == state.encumbrance
}
if (!encumbranceStateExists) {
throw TransactionVerificationException.TransactionMissingEncumbranceException(
ltx.id,
state.encumbrance!!,
TransactionVerificationException.Direction.INPUT
)
}
}
// Using basic graph theory, a full cycle of encumbered (co-dependent) states should exist to achieve bi-directional
// encumbrances. This property is important to ensure that no states involved in an encumbrance-relationship
// can be spent on their own. Briefly, if any of the states is having more than one encumbrance references by
// other states, a full cycle detection will fail. As a result, all of the encumbered states must be present
// as "from" and "to" only once (or zero times if no encumbrance takes place). For instance,
// a -> b
// c -> b and a -> b
// b -> a b -> c
// do not satisfy the bi-directionality (full cycle) property.
//
// In the first example "b" appears twice in encumbrance ("to") list and "c" exists in the encumbered ("from") list only.
// Due the above, one could consume "a" and "b" in the same transaction and then, because "b" is already consumed, "c" cannot be spent.
//
// Similarly, the second example does not form a full cycle because "a" and "c" exist in one of the lists only.
// As a result, one can consume "b" and "c" in the same transactions, which will make "a" impossible to be spent.
//
// On other hand the following are valid constructions:
// a -> b a -> c
// b -> c and c -> b
// c -> a b -> a
// and form a full cycle, meaning that the bi-directionality property is satisfied.
private fun checkBidirectionalOutputEncumbrances(statesAndEncumbrance: List<Pair<Int, Int>>) {
// [Set] of "from" (encumbered states).
val encumberedSet = mutableSetOf<Int>()
// [Set] of "to" (encumbrance states).
val encumbranceSet = mutableSetOf<Int>()
// Update both [Set]s.
statesAndEncumbrance.forEach { (statePosition, encumbrance) ->
// Check it does not refer to itself.
if (statePosition == encumbrance || encumbrance >= ltx.outputs.size) {
throw TransactionVerificationException.TransactionMissingEncumbranceException(
ltx.id,
encumbrance,
TransactionVerificationException.Direction.OUTPUT
)
} else {
encumberedSet.add(statePosition) // Guaranteed to have unique elements.
if (!encumbranceSet.add(encumbrance)) {
throw TransactionVerificationException.TransactionDuplicateEncumbranceException(ltx.id, encumbrance)
}
}
}
// At this stage we have ensured that "from" and "to" [Set]s are equal in size, but we should check their
// elements do indeed match. If they don't match, we return their symmetric difference (disjunctive union).
val symmetricDifference = (encumberedSet union encumbranceSet).subtract(encumberedSet intersect encumbranceSet)
if (symmetricDifference.isNotEmpty()) {
// At least one encumbered state is not in the [encumbranceSet] and vice versa.
throw TransactionVerificationException.TransactionNonMatchingEncumbranceException(ltx.id, symmetricDifference)
}
}
// Method to check if all encumbered states are assigned to the same notary Party.
// This method should be invoked after [checkBidirectionalOutputEncumbrances], because it assumes that the
// bi-directionality property is already satisfied.
private fun checkNotariesOutputEncumbrance(statesAndEncumbrance: List<Pair<Int, Int>>) {
// We only check for transactions in which notary is null (i.e., issuing transactions).
// Note that if a notary is defined for a transaction, we already check if all outputs are assigned
// to the same notary (transaction's notary) in [checkNoNotaryChange()].
if (ltx.notary == null) {
// indicesAlreadyChecked is used to bypass already checked indices and to avoid cycles.
val indicesAlreadyChecked = HashSet<Int>()
statesAndEncumbrance.forEach {
checkNotary(it.first, indicesAlreadyChecked)
}
}
}
private tailrec fun checkNotary(index: Int, indicesAlreadyChecked: HashSet<Int>) {
if (indicesAlreadyChecked.add(index)) {
val encumbranceIndex = ltx.outputs[index].encumbrance!!
if (ltx.outputs[index].notary != ltx.outputs[encumbranceIndex].notary) {
throw TransactionVerificationException.TransactionNotaryMismatchEncumbranceException(
ltx.id,
index,
encumbranceIndex,
ltx.outputs[index].notary,
ltx.outputs[encumbranceIndex].notary
)
} else {
checkNotary(encumbranceIndex, indicesAlreadyChecked)
}
}
}
/**
* Verify that contract class versions of output states are not lower that versions of relevant input states.
*/
private fun validateContractVersions() {
contractAttachmentsByContract.forEach { contractClassName, attachments ->
val outputVersion = attachments.signed?.version ?: attachments.unsigned?.version ?: CordappImpl.DEFAULT_CORDAPP_VERSION
inputStatesContractClassNameToMaxVersion[contractClassName]?.let {
if (it > outputVersion) {
throw TransactionVerificationException.TransactionVerificationVersionException(ltx.id, contractClassName, "$it", "$outputVersion")
}
}
}
}
/**
* Verify that for each contract the network wide package owner is respected.
*
* TODO - revisit once transaction contains network parameters. - UPDATE: It contains them, but because of the API stability and the fact that
* LedgerTransaction was data class i.e. exposed constructors that shouldn't had been exposed, we still need to keep them nullable :/
*/
private fun validatePackageOwnership() {
val contractsAndOwners = allStates.mapNotNull { transactionState ->
val contractClassName = transactionState.contract
ltx.networkParameters!!.getPackageOwnerOf(contractClassName)?.let { contractClassName to it }
}.toMap()
contractsAndOwners.forEach { contract, owner ->
contractAttachmentsByContract[contract]?.filter { it.isSigned }?.forEach { attachment ->
if (!owner.isFulfilledBy(attachment.signerKeys))
throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(ltx.id, attachment.id, contract)
} ?: throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(ltx.id, ltx.id, contract)
}
}
/**
* 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.
*
* 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.
*/
private fun validateStatesAgainstContract() = allStates.forEach(::validateStateAgainstContract)
private fun validateStateAgainstContract(state: TransactionState<ContractState>) {
val shouldEnforce = StateContractValidationEnforcementRule.shouldEnforce(state.data)
val requiredContractClassName = state.data.requiredContractClassName
?: if (shouldEnforce) throw TransactionVerificationException.TransactionRequiredContractUnspecifiedException(ltx.id, state) else return
if (state.contract != requiredContractClassName)
if (shouldEnforce) {
throw TransactionContractConflictException(ltx.id, state, requiredContractClassName)
} else {
logger.warnOnce("""
State of class ${state.data::class.java.typeName} belongs to contract $requiredContractClassName, but
is bundled in TransactionState with ${state.contract}.
For details see: https://docs.corda.net/api-contract-constraints.html#contract-state-agreement
""".trimIndent().replace('\n', ' '))
}
}
/**
* Enforces the validity of the actual constraints.
* * 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
*/
private fun verifyConstraintsValidity(): MutableSet<ContractClassName> {
// First check that the constraints are valid.
for (state in allStates) {
checkConstraintValidity(state)
}
// Group the inputs and outputs by contract, and for each contract verify the constraints propagation logic.
// This is not required for reference states as there is nothing to propagate.
val inputContractGroups = 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]?.map { it.state.constraint }?.toSet()
val outputConstraints = outputContractGroups[contractClassName]?.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)
}
}
} 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)
}
}
/**
* Verify that all contract constraints are passing before running any contract code.
*
* This check is running the [AttachmentConstraint.isSatisfiedBy] method for each corresponding [ContractAttachment].
*
* @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")
}
val constraintAttachment = if (state.contract in hashToSignatureConstrainedContracts) {
// hash to to signature constraint migration logic:
// pass the unsigned attachment when verifying the constraint of the input state, and the signed attachment when verifying
// the constraint of the output state.
val unsignedAttachment = contractAttachmentsByContract[state.contract].unsigned
?: throw TransactionVerificationException.MissingAttachmentRejection(ltx.id, state.contract)
val signedAttachment = contractAttachmentsByContract[state.contract].signed
?: throw TransactionVerificationException.MissingAttachmentRejection(ltx.id, state.contract)
when {
// use unsigned attachment if hash-constrained input state
state.data in ltx.inputStates -> AttachmentWithContext(unsignedAttachment, state.contract, ltx.networkParameters!!)
// use signed attachment if signature-constrained output state
state.data in ltx.outputStates -> AttachmentWithContext(signedAttachment, state.contract, ltx.networkParameters!!)
else -> throw IllegalStateException("${state.contract} must use either signed or unsigned attachment in hash to signature constraints migration")
}
} else {
// standard processing logic
val contractAttachment = contractAttachmentsByContract[state.contract]?.firstOrNull()
?: throw TransactionVerificationException.MissingAttachmentRejection(ltx.id, state.contract)
AttachmentWithContext(contractAttachment, state.contract, ltx.networkParameters!!)
}
if (!state.constraint.isSatisfiedBy(constraintAttachment)) {
throw TransactionVerificationException.ContractConstraintRejection(ltx.id, state.contract)
}
}
}
/**
* Check the transaction is contract-valid by running the verify() for each input and output state contract.
* If any contract fails to verify, the whole transaction is considered to be invalid.
*/
private fun verifyContracts() {
val contractClasses = (inputStates + ltx.outputs).toSet()
.map { it.contract to contractClassFor(it.contract, it.data.javaClass.classLoader) }
val contractInstances = contractClasses.map { (contractClassName, contractClass) ->
try {
contractClass.newInstance()
} catch (e: Exception) {
throw TransactionVerificationException.ContractCreationError(ltx.id, contractClassName, e)
}
}
contractInstances.forEach { contract ->
try {
contract.verify(ltx)
} catch (e: Exception) {
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

@ -3,14 +3,9 @@ package net.corda.core.transactions
import net.corda.core.CordaInternal
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.crypto.SecureHash
import net.corda.core.crypto.isFulfilledBy
import net.corda.core.identity.Party
import net.corda.core.internal.*
import net.corda.core.internal.cordapp.CordappImpl.Companion.DEFAULT_CORDAPP_VERSION
import net.corda.core.internal.rules.StateContractValidationEnforcementRule
import net.corda.core.node.NetworkParameters
import net.corda.core.serialization.ConstructorForDeserialization
import net.corda.core.serialization.CordaSerializable
@ -19,7 +14,6 @@ import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
import net.corda.core.utilities.contextLogger
import java.util.*
import java.util.function.Predicate
import kotlin.collections.HashSet
/**
* A LedgerTransaction is derived from a [WireTransaction]. It is the result of doing the following operations:
@ -54,7 +48,7 @@ private constructor(
/** Network parameters that were in force when the transaction was notarised. */
override val networkParameters: NetworkParameters?,
override val references: List<StateAndRef<ContractState>>,
private val inputStatesContractClassNameToMaxVersion: Map<ContractClassName,Version>
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
@ -118,23 +112,22 @@ private constructor(
* @throws TransactionVerificationException if anything goes wrong.
*/
@Throws(TransactionVerificationException::class)
fun verify() = verifyInternal(emptyList())
/**
* Verifies the transaction but takes a list of [extraAttachments] which are used to form the classpath.
* Used to work around a Corda 3 bug as there might be transactions out there that don't contain all the necessary dependencies in the attachments list.
*/
@CordaInternal
internal fun verifyInternal(extraAttachments: List<Attachment>) {
fun verify() {
if (networkParameters == null) {
// For backwards compatibility only.
logger.warn("Network parameters on the LedgerTransaction with id: $id are null. Please don't use deprecated constructors of the LedgerTransaction. " +
"Use WireTransaction.toLedgerTransaction instead. The result of the verify method might not be accurate.")
}
val verifier = internalPrepareVerify(emptyList())
verifier.verify()
}
AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(this.attachments + extraAttachments) { transactionClassLoader ->
Verifier(createLtxForVerification(), transactionClassLoader).verify()
}
/**
* This method has to be called in a context where it has access to the database.
*/
@CordaInternal
internal fun internalPrepareVerify(extraAttachments: List<Attachment>) = AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(this.attachments + extraAttachments) { transactionClassLoader ->
Verifier(createLtxForVerification(), transactionClassLoader, inputStatesContractClassNameToMaxVersion)
}
private fun createLtxForVerification(): LedgerTransaction {
@ -515,385 +508,6 @@ private constructor(
|)""".trimMargin()
}
/**
* Because we create a separate [LedgerTransaction] onto which we need to perform verification, it becomes important we don't verify the
* wrong object instance. This class helps avoid that.
*/
private class Verifier(private val ltx: LedgerTransaction, private val transactionClassLoader: ClassLoader) {
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()
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()
validateContractVersions()
validatePackageOwnership()
validateStatesAgainstContract()
val hashToSignatureConstrainedContracts = verifyConstraintsValidity()
verifyConstraints(hashToSignatureConstrainedContracts)
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.
*/
private fun getContractAttachmentsByContract(): Map<ContractClassName, Set<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)
}
}
return result
}
/**
* Make sure the notary has stayed the same. As we can't tell how inputs and outputs connect, if there
* are any inputs or reference inputs, all outputs must have the same notary.
*
* TODO: Is that the correct set of restrictions? May need to come back to this, see if we can be more
* flexible on output notaries.
*/
private fun checkNoNotaryChange() {
if (ltx.notary != null && (ltx.inputs.isNotEmpty() || ltx.references.isNotEmpty())) {
ltx.outputs.forEach {
if (it.notary != ltx.notary) {
throw TransactionVerificationException.NotaryChangeInWrongTransactionType(ltx.id, ltx.notary, it.notary)
}
}
}
}
private fun checkEncumbrancesValid() {
// Validate that all encumbrances exist within the set of input states.
ltx.inputs
.filter { it.state.encumbrance != null }
.forEach { (state, ref) -> checkInputEncumbranceStateExists(state, ref) }
// Check that in the outputs,
// a) an encumbered state does not refer to itself as the encumbrance
// b) the number of outputs can contain the encumbrance
// c) the bi-directionality (full cycle) property is satisfied
// d) encumbered output states are assigned to the same notary.
val statesAndEncumbrance = ltx.outputs
.withIndex()
.filter { it.value.encumbrance != null }
.map { Pair(it.index, it.value.encumbrance!!) }
if (!statesAndEncumbrance.isEmpty()) {
checkBidirectionalOutputEncumbrances(statesAndEncumbrance)
checkNotariesOutputEncumbrance(statesAndEncumbrance)
}
}
private fun checkInputEncumbranceStateExists(state: TransactionState<ContractState>, ref: StateRef) {
val encumbranceStateExists = ltx.inputs.any {
it.ref.txhash == ref.txhash && it.ref.index == state.encumbrance
}
if (!encumbranceStateExists) {
throw TransactionVerificationException.TransactionMissingEncumbranceException(
ltx.id,
state.encumbrance!!,
TransactionVerificationException.Direction.INPUT
)
}
}
// Using basic graph theory, a full cycle of encumbered (co-dependent) states should exist to achieve bi-directional
// encumbrances. This property is important to ensure that no states involved in an encumbrance-relationship
// can be spent on their own. Briefly, if any of the states is having more than one encumbrance references by
// other states, a full cycle detection will fail. As a result, all of the encumbered states must be present
// as "from" and "to" only once (or zero times if no encumbrance takes place). For instance,
// a -> b
// c -> b and a -> b
// b -> a b -> c
// do not satisfy the bi-directionality (full cycle) property.
//
// In the first example "b" appears twice in encumbrance ("to") list and "c" exists in the encumbered ("from") list only.
// Due the above, one could consume "a" and "b" in the same transaction and then, because "b" is already consumed, "c" cannot be spent.
//
// Similarly, the second example does not form a full cycle because "a" and "c" exist in one of the lists only.
// As a result, one can consume "b" and "c" in the same transactions, which will make "a" impossible to be spent.
//
// On other hand the following are valid constructions:
// a -> b a -> c
// b -> c and c -> b
// c -> a b -> a
// and form a full cycle, meaning that the bi-directionality property is satisfied.
private fun checkBidirectionalOutputEncumbrances(statesAndEncumbrance: List<Pair<Int, Int>>) {
// [Set] of "from" (encumbered states).
val encumberedSet = mutableSetOf<Int>()
// [Set] of "to" (encumbrance states).
val encumbranceSet = mutableSetOf<Int>()
// Update both [Set]s.
statesAndEncumbrance.forEach { (statePosition, encumbrance) ->
// Check it does not refer to itself.
if (statePosition == encumbrance || encumbrance >= ltx.outputs.size) {
throw TransactionVerificationException.TransactionMissingEncumbranceException(
ltx.id,
encumbrance,
TransactionVerificationException.Direction.OUTPUT
)
} else {
encumberedSet.add(statePosition) // Guaranteed to have unique elements.
if (!encumbranceSet.add(encumbrance)) {
throw TransactionVerificationException.TransactionDuplicateEncumbranceException(ltx.id, encumbrance)
}
}
}
// At this stage we have ensured that "from" and "to" [Set]s are equal in size, but we should check their
// elements do indeed match. If they don't match, we return their symmetric difference (disjunctive union).
val symmetricDifference = (encumberedSet union encumbranceSet).subtract(encumberedSet intersect encumbranceSet)
if (symmetricDifference.isNotEmpty()) {
// At least one encumbered state is not in the [encumbranceSet] and vice versa.
throw TransactionVerificationException.TransactionNonMatchingEncumbranceException(ltx.id, symmetricDifference)
}
}
// Method to check if all encumbered states are assigned to the same notary Party.
// This method should be invoked after [checkBidirectionalOutputEncumbrances], because it assumes that the
// bi-directionality property is already satisfied.
private fun checkNotariesOutputEncumbrance(statesAndEncumbrance: List<Pair<Int, Int>>) {
// We only check for transactions in which notary is null (i.e., issuing transactions).
// Note that if a notary is defined for a transaction, we already check if all outputs are assigned
// to the same notary (transaction's notary) in [checkNoNotaryChange()].
if (ltx.notary == null) {
// indicesAlreadyChecked is used to bypass already checked indices and to avoid cycles.
val indicesAlreadyChecked = HashSet<Int>()
statesAndEncumbrance.forEach {
checkNotary(it.first, indicesAlreadyChecked)
}
}
}
private tailrec fun checkNotary(index: Int, indicesAlreadyChecked: HashSet<Int>) {
if (indicesAlreadyChecked.add(index)) {
val encumbranceIndex = ltx.outputs[index].encumbrance!!
if (ltx.outputs[index].notary != ltx.outputs[encumbranceIndex].notary) {
throw TransactionVerificationException.TransactionNotaryMismatchEncumbranceException(
ltx.id,
index,
encumbranceIndex,
ltx.outputs[index].notary,
ltx.outputs[encumbranceIndex].notary
)
} else {
checkNotary(encumbranceIndex, indicesAlreadyChecked)
}
}
}
/**
* Verify that contract class versions of output states are not lower that versions of relevant input states.
*/
private fun validateContractVersions() {
contractAttachmentsByContract.forEach { contractClassName, attachments ->
val outputVersion = attachments.signed?.version ?: attachments.unsigned?.version ?: DEFAULT_CORDAPP_VERSION
ltx.inputStatesContractClassNameToMaxVersion[contractClassName]?.let {
if (it > outputVersion) {
throw TransactionVerificationException.TransactionVerificationVersionException(ltx.id, contractClassName, "$it", "$outputVersion")
}
}
}
}
/**
* Verify that for each contract the network wide package owner is respected.
*
* TODO - revisit once transaction contains network parameters. - UPDATE: It contains them, but because of the API stability and the fact that
* LedgerTransaction was data class i.e. exposed constructors that shouldn't had been exposed, we still need to keep them nullable :/
*/
private fun validatePackageOwnership() {
val contractsAndOwners = allStates.mapNotNull { transactionState ->
val contractClassName = transactionState.contract
ltx.networkParameters!!.getPackageOwnerOf(contractClassName)?.let { contractClassName to it }
}.toMap()
contractsAndOwners.forEach { contract, owner ->
contractAttachmentsByContract[contract]?.filter { it.isSigned }?.forEach { attachment ->
if (!owner.isFulfilledBy(attachment.signerKeys))
throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(ltx.id, attachment.id, contract)
} ?: throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(ltx.id, ltx.id, contract)
}
}
/**
* 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.
*
* 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.
*/
private fun validateStatesAgainstContract() = allStates.forEach(::validateStateAgainstContract)
private fun validateStateAgainstContract(state: TransactionState<ContractState>) {
val shouldEnforce = StateContractValidationEnforcementRule.shouldEnforce(state.data)
val requiredContractClassName = state.data.requiredContractClassName
?: if (shouldEnforce) throw TransactionRequiredContractUnspecifiedException(ltx.id, state) else return
if (state.contract != requiredContractClassName)
if (shouldEnforce) {
throw TransactionContractConflictException(ltx.id, state, requiredContractClassName)
} else {
logger.warnOnce("""
State of class ${state.data::class.java.typeName} belongs to contract $requiredContractClassName, but
is bundled in TransactionState with ${state.contract}.
For details see: https://docs.corda.net/api-contract-constraints.html#contract-state-agreement
""".trimIndent().replace('\n', ' '))
}
}
/**
* Enforces the validity of the actual constraints.
* * 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
*/
private fun verifyConstraintsValidity(): MutableSet<ContractClassName> {
// First check that the constraints are valid.
for (state in allStates) {
checkConstraintValidity(state)
}
// Group the inputs and outputs by contract, and for each contract verify the constraints propagation logic.
// This is not required for reference states as there is nothing to propagate.
val inputContractGroups = 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]?.map { it.state.constraint }?.toSet()
val outputConstraints = outputContractGroups[contractClassName]?.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)
}
}
} 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)
}
}
/**
* Verify that all contract constraints are passing before running any contract code.
*
* This check is running the [AttachmentConstraint.isSatisfiedBy] method for each corresponding [ContractAttachment].
*
* @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")
}
val constraintAttachment = if (state.contract in hashToSignatureConstrainedContracts) {
// hash to to signature constraint migration logic:
// pass the unsigned attachment when verifying the constraint of the input state, and the signed attachment when verifying
// the constraint of the output state.
val unsignedAttachment = contractAttachmentsByContract[state.contract].unsigned
?: throw TransactionVerificationException.MissingAttachmentRejection(ltx.id, state.contract)
val signedAttachment = contractAttachmentsByContract[state.contract].signed
?: throw TransactionVerificationException.MissingAttachmentRejection(ltx.id, state.contract)
when {
// use unsigned attachment if hash-constrained input state
state.data in ltx.inputStates -> AttachmentWithContext(unsignedAttachment, state.contract, ltx.networkParameters!!)
// use signed attachment if signature-constrained output state
state.data in ltx.outputStates -> AttachmentWithContext(signedAttachment, state.contract, ltx.networkParameters!!)
else -> throw IllegalStateException("${state.contract} must use either signed or unsigned attachment in hash to signature constraints migration")
}
} else {
// standard processing logic
val contractAttachment = contractAttachmentsByContract[state.contract]?.firstOrNull()
?: throw TransactionVerificationException.MissingAttachmentRejection(ltx.id, state.contract)
AttachmentWithContext(contractAttachment, state.contract, ltx.networkParameters!!)
}
if (!state.constraint.isSatisfiedBy(constraintAttachment)) {
throw TransactionVerificationException.ContractConstraintRejection(ltx.id, state.contract)
}
}
}
/**
* Check the transaction is contract-valid by running the verify() for each input and output state contract.
* If any contract fails to verify, the whole transaction is considered to be invalid.
*/
private fun verifyContracts() {
val contractClasses = (inputStates + ltx.outputs).toSet()
.map { it.contract to contractClassFor(it.contract, it.data.javaClass.classLoader) }
val contractInstances = contractClasses.map { (contractClassName, contractClass) ->
try {
contractClass.newInstance()
} catch (e: Exception) {
throw TransactionVerificationException.ContractCreationError(ltx.id, contractClassName, e)
}
}
contractInstances.forEach { contract ->
try {
contract.verify(ltx)
} catch (e: Exception) {
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 }
}
// Stuff that we can't remove and so is deprecated instead
//
@Deprecated("LedgerTransaction should not be created directly, use WireTransaction.toLedgerTransaction instead.")

View File

@ -64,6 +64,22 @@ private val _contextDatabase = InheritableThreadLocal<CordaPersistence>()
var contextDatabase: CordaPersistence
get() = _contextDatabase.get() ?: error("Was expecting to find CordaPersistence set on current thread: ${Strand.currentStrand()}")
set(database) = _contextDatabase.set(database)
private val _prohibitDatabaseAccess = ThreadLocal.withInitial { false }
/**
* The logic in the [block] will be prevented from opening a database transaction.
*/
fun <T> withoutDatabaseAccess(block: () -> T): T {
val oldValue = _prohibitDatabaseAccess.get()
_prohibitDatabaseAccess.set(true)
try {
return block()
} finally {
_prohibitDatabaseAccess.set(oldValue)
}
}
val contextDatabaseOrNull: CordaPersistence? get() = _contextDatabase.get()
class CordaPersistence(
@ -115,6 +131,9 @@ class CordaPersistence(
private val liveTransactions = ConcurrentHashMap<UUID, DatabaseTransaction>()
fun newTransaction(isolation: TransactionIsolationLevel = defaultIsolationLevel): DatabaseTransaction {
if(_prohibitDatabaseAccess.get()){
throw IllegalAccessException("Database access is not allowed in the current context.")
}
val outerTransaction = contextTransactionOrNull
return DatabaseTransaction(isolation.jdbcValue, contextTransactionOrNull, this).also {
contextTransactionOrNull = it

View File

@ -1,21 +1,28 @@
package net.corda.node.services.transactions
import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.Attachment
import net.corda.core.internal.TransactionVerifierServiceInternal
import net.corda.core.internal.concurrent.fork
import net.corda.core.internal.verify
import net.corda.core.internal.concurrent.openFuture
import net.corda.core.internal.prepareVerify
import net.corda.core.node.services.TransactionVerifierService
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.transactions.LedgerTransaction
import java.util.concurrent.Executors
import net.corda.nodeapi.internal.persistence.withoutDatabaseAccess
class InMemoryTransactionVerifierService(numberOfWorkers: Int) : SingletonSerializeAsToken(), TransactionVerifierService, TransactionVerifierServiceInternal, AutoCloseable {
private val workerPool = Executors.newFixedThreadPool(numberOfWorkers)
override fun verify(transaction: LedgerTransaction) = workerPool.fork(transaction::verify)
override fun verify(transaction: LedgerTransaction): CordaFuture<Unit> = this.verify(transaction, emptyList())
override fun verify(transaction: LedgerTransaction, extraAttachments: List<Attachment>) = workerPool.fork {
transaction.verify(extraAttachments)
override fun verify(transaction: LedgerTransaction, extraAttachments: List<Attachment>): CordaFuture<Unit> {
return openFuture<Unit>().apply {
capture {
val verifier = transaction.prepareVerify(extraAttachments)
withoutDatabaseAccess {
verifier.verify()
}
}
}
}
override fun close() = workerPool.shutdown()
override fun close() {}
}