mirror of
https://github.com/corda/corda.git
synced 2025-01-18 02:39:51 +00:00
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:
parent
ce250cfafd
commit
d33cb16c5e
@ -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 }
|
||||
}
|
||||
|
@ -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.")
|
||||
|
@ -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
|
||||
|
@ -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() {}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user