Fixed incorrect attachment loading integration test (#4453)

* Fixed incorrect attachment loading integration test

`AttachmentLoadingTests.test that attachments retrieved over the network are not used for code` was a false-positive - it was incorrect on multiple levels. Fixing it required updating the finance:isolated CorDapp, at which point it was given the new MANIFEST metadata for V4, and moved out of the net.corda.finance namespace to avoid package sealing issues.

The new test exposed a bug in the LedgerTransaction verification logic. This was cleaned up as it was too easy to verify on the wrong instance.
This commit is contained in:
Shams Asari 2018-12-31 15:02:11 +00:00 committed by GitHub
parent bbbe08ab1b
commit b4c3fa1948
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 615 additions and 582 deletions

2
.idea/compiler.xml generated
View File

@ -158,6 +158,8 @@
<module name="net.corda-blobinspector_test" target="1.8" />
<module name="net.corda-finance-contracts-states_main" target="1.8" />
<module name="net.corda-finance-contracts-states_test" target="1.8" />
<module name="net.corda-isolated_main" target="1.8" />
<module name="net.corda-isolated_test" target="1.8" />
<module name="net.corda-verifier_main" target="1.8" />
<module name="net.corda-verifier_test" target="1.8" />
<module name="net.corda_buildSrc_main" target="1.8" />

View File

@ -76,7 +76,7 @@ sealed class NotaryError {
*/
// TODO: include notary timestamp?
@CordaSerializable
data class StateConsumptionDetails(
data class StateConsumptionDetails(
val hashOfTransactionId: SecureHash,
val type: ConsumedStateType
) {

View File

@ -20,12 +20,9 @@ import kotlin.math.min
/**
* Resolves transactions for the specified [txHashes] along with their full history (dependency graph) from [otherSide].
* Each retrieved transaction is validated and inserted into the local transaction storage.
*
* @return a list of verified [SignedTransaction] objects, in a depth-first order.
*/
@DeleteForDJVM
class ResolveTransactionsFlow(txHashesArg: Set<SecureHash>,
private val otherSide: FlowSession) : FlowLogic<Unit>() {
class ResolveTransactionsFlow(txHashesArg: Set<SecureHash>, private val otherSide: FlowSession) : FlowLogic<Unit>() {
// Need it ordered in terms of iteration. Needs to be a variable for the check-pointing logic to work.
private val txHashes = txHashesArg.toList()

View File

@ -70,7 +70,7 @@ fun <T : Any> deserialiseComponentGroup(componentGroups: List<ComponentGroup>,
// If the componentGroup is a [LazyMappedList] it means that the original deserialized version is already available.
val components = group.components
if (!forceDeserialize && components is LazyMappedList<*, OpaqueBytes>) {
return components.originalList as List<T>
return uncheckedCast(components.originalList)
}
return components.lazyMapped { component, internalIndex ->

View File

@ -66,8 +66,6 @@ private constructor(
checkBaseInvariants()
if (timeWindow != null) check(notary != null) { "Transactions with time-windows must be notarised" }
checkNotaryWhitelisted()
checkNoNotaryChange()
checkEncumbrancesValid()
}
companion object {
@ -101,9 +99,6 @@ private constructor(
val inputStates: List<ContractState> get() = inputs.map { it.state.data }
val referenceStates: List<ContractState> get() = references.map { it.state.data }
private val inputAndOutputStates = inputs.map { it.state } + outputs
private val allStates = inputAndOutputStates + references.map { it.state }
/**
* Returns the typed input StateAndRef at the specified index
* @param index The index into the inputs.
@ -129,218 +124,12 @@ private constructor(
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 contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>> = getContractAttachmentsByContract(allStates.map { it.contract }.toSet())
AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(this.attachments) { transactionClassLoader ->
val internalTx = createLtxForVerification()
validateContractVersions(contractAttachmentsByContract)
validatePackageOwnership(contractAttachmentsByContract)
validateStatesAgainstContract(internalTx)
val hashToSignatureConstrainedContracts = verifyConstraintsValidity(internalTx, contractAttachmentsByContract, transactionClassLoader)
verifyConstraints(internalTx, contractAttachmentsByContract, hashToSignatureConstrainedContracts)
verifyContracts(internalTx)
Verifier(createLtxForVerification(), transactionClassLoader).verify()
}
}
/**
* Verify that contract class versions of output states are not lower that versions of relevant input states.
*/
@Throws(TransactionVerificationException::class)
private fun validateContractVersions(contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>) {
contractAttachmentsByContract.forEach { contractClassName, attachments ->
val outputVersion = attachments.signed?.version ?: attachments.unsigned?.version ?: DEFAULT_CORDAPP_VERSION
inputStatesContractClassNameToMaxVersion[contractClassName]?.let {
if (it > outputVersion) {
throw TransactionVerificationException.TransactionVerificationVersionException(this.id, contractClassName, "$it", "$outputVersion")
}
}
}
}
/**
* For all input and output [TransactionState]s, validates that the wrapped [ContractState] matches up with the
* wrapped [Contract], as declared by the [BelongsToContract] annotation on the [ContractState]'s class.
*
* 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(internalTx: LedgerTransaction) =
internalTx.allStates.forEach(::validateStateAgainstContract)
private fun validateStateAgainstContract(state: TransactionState<ContractState>) {
val shouldEnforce = StateContractValidationEnforcementRule.shouldEnforce(state.data)
val requiredContractClassName = state.data.requiredContractClassName ?:
if (shouldEnforce) throw TransactionRequiredContractUnspecifiedException(id, state)
else return
if (state.contract != requiredContractClassName)
if (shouldEnforce) {
throw TransactionContractConflictException(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', ' '))
}
}
/**
* 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(contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>) {
val contractsAndOwners = allStates.mapNotNull { transactionState ->
val contractClassName = transactionState.contract
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(this.id, id, contract)
} ?: throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(this.id, id, contract)
}
}
/**
* 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(internalTx: LedgerTransaction, contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>, transactionClassLoader: ClassLoader): MutableSet<ContractClassName> {
// First check that the constraints are valid.
for (state in internalTx.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 = internalTx.inputs.groupBy { it.state.contract }
val outputContractGroups = internalTx.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, contractAttachmentsByContract)
if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, constraintAttachment))) {
throw TransactionVerificationException.ConstraintPropagationRejection(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, contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>): AttachmentWithContext {
val unsignedAttachment = contractAttachmentsByContract[contractClassName]!!.filter { !it.isSigned }.firstOrNull()
val signedAttachment = contractAttachmentsByContract[contractClassName]!!.filter { it.isSigned }.firstOrNull()
return when {
(unsignedAttachment != null && signedAttachment != null) -> AttachmentWithContext(signedAttachment, contractClassName, networkParameters!!)
(unsignedAttachment != null) -> AttachmentWithContext(unsignedAttachment, contractClassName, networkParameters!!)
(signedAttachment != null) -> AttachmentWithContext(signedAttachment, contractClassName, networkParameters!!)
else -> throw TransactionVerificationException.ContractConstraintRejection(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(internalTx: LedgerTransaction, contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>, hashToSignatureConstrainedContracts: MutableSet<ContractClassName>) {
for (state in internalTx.allStates) {
if (state.constraint is SignatureAttachmentConstraint)
checkMinimumPlatformVersion(networkParameters!!.minimumPlatformVersion, 4, "Signature constraints")
val constraintAttachment =
// 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.
if (state.contract in hashToSignatureConstrainedContracts) {
val unsignedAttachment = contractAttachmentsByContract[state.contract].unsigned
?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
val signedAttachment = contractAttachmentsByContract[state.contract].signed
?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
when {
// use unsigned attachment if hash-constrained input state
state.data in inputStates -> AttachmentWithContext(unsignedAttachment, state.contract, networkParameters!!)
// use signed attachment if signature-constrained output state
state.data in outputStates -> AttachmentWithContext(signedAttachment, state.contract, networkParameters!!)
else -> throw IllegalStateException("${state.contract} must use either signed or unsigned attachment in hash to signature constraints migration")
}
}
// standard processing logic
else {
val contractAttachment = contractAttachmentsByContract[state.contract]?.firstOrNull()
?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
AttachmentWithContext(contractAttachment, state.contract, networkParameters!!)
}
if (!state.constraint.isSatisfiedBy(constraintAttachment)) {
throw TransactionVerificationException.ContractConstraintRejection(id, state.contract)
}
}
}
private val Set<ContractAttachment>?.unsigned: ContractAttachment?
get() {
return this?.filter { !it.isSigned }?.firstOrNull()
}
private val Set<ContractAttachment>?.signed: ContractAttachment?
get() {
return this?.filter { it.isSigned }?.firstOrNull()
}
// 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(contractClasses: Set<ContractClassName>): Map<ContractClassName, Set<ContractAttachment>> {
val result = mutableMapOf<ContractClassName, Set<ContractAttachment>>()
for (attachment in attachments) {
if (attachment !is ContractAttachment) continue
for (contract in contractClasses) {
if (!attachment.allContracts.contains(contract)) continue
result[contract] = result.getOrDefault(contract, setOf(attachment)).plus(attachment)
}
}
return result
}
private fun contractClassFor(className: ContractClassName, classLoader: ClassLoader): Class<out Contract> = try {
classLoader.loadClass(className).asSubclass(Contract::class.java)
} catch (e: Exception) {
throw TransactionVerificationException.ContractCreationError(id, className, e)
}
private fun createLtxForVerification(): LedgerTransaction {
val serializedInputs = this.serializedInputs
val serializedReferences = this.serializedReferences
@ -368,7 +157,7 @@ private constructor(
privacySalt = this.privacySalt,
networkParameters = this.networkParameters,
references = deserializedReferences,
inputStatesContractClassNameToMaxVersion = emptyMap()
inputStatesContractClassNameToMaxVersion = this.inputStatesContractClassNameToMaxVersion
)
} else {
// This branch is only present for backwards compatibility.
@ -378,156 +167,6 @@ private constructor(
}
}
/**
* 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(internalTx: LedgerTransaction) {
val contractClasses = (internalTx.inputs.map { it.state } + internalTx.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(id, contractClassName, e)
}
}
contractInstances.forEach { contract ->
try {
contract.verify(internalTx)
} catch (e: Exception) {
throw TransactionVerificationException.ContractRejection(id, contract, e)
}
}
}
/**
* 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 (notary != null && (inputs.isNotEmpty() || references.isNotEmpty())) {
outputs.forEach {
if (it.notary != notary) {
throw TransactionVerificationException.NotaryChangeInWrongTransactionType(id, notary, it.notary)
}
}
}
}
private fun checkEncumbrancesValid() {
// Validate that all encumbrances exist within the set of input states.
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 = outputs.withIndex().filter { it.value.encumbrance != null }
.map { Pair(it.index, it.value.encumbrance!!) }
if (!statesAndEncumbrance.isEmpty()) {
checkBidirectionalOutputEncumbrances(statesAndEncumbrance)
checkNotariesOutputEncumbrance(statesAndEncumbrance)
}
}
// 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 (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 = outputs[index].encumbrance!!
if (outputs[index].notary != outputs[encumbranceIndex].notary) {
throw TransactionVerificationException.TransactionNotaryMismatchEncumbranceException(id, index, encumbranceIndex, outputs[index].notary, outputs[encumbranceIndex].notary)
} else {
checkNotary(encumbranceIndex, indicesAlreadyChecked)
}
}
}
private fun checkInputEncumbranceStateExists(state: TransactionState<ContractState>, ref: StateRef) {
val encumbranceStateExists = inputs.any {
it.ref.txhash == ref.txhash && it.ref.index == state.encumbrance
}
if (!encumbranceStateExists) {
throw TransactionVerificationException.TransactionMissingEncumbranceException(
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 >= outputs.size) {
throw TransactionVerificationException.TransactionMissingEncumbranceException(
id,
encumbrance,
TransactionVerificationException.Direction.OUTPUT)
} else {
encumberedSet.add(statePosition) // Guaranteed to have unique elements.
if (!encumbranceSet.add(encumbrance)) {
throw TransactionVerificationException.TransactionDuplicateEncumbranceException(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(id, symmetricDifference)
}
}
/**
* Given a type and a function that returns a grouping key, associates inputs and outputs together so that they
* can be processed as one. The grouping key is any arbitrary object that can act as a map key (so must implement
@ -869,6 +508,385 @@ 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

@ -26,15 +26,15 @@ import kotlin.test.assertFailsWith
class AttachmentsClassLoaderSerializationTests {
companion object {
val ISOLATED_CONTRACTS_JAR_PATH: URL = AttachmentsClassLoaderSerializationTests::class.java.getResource("isolated.jar")
private const val ISOLATED_CONTRACT_CLASS_NAME = "net.corda.finance.contracts.isolated.AnotherDummyContract"
val ISOLATED_CONTRACTS_JAR_PATH: URL = AttachmentsClassLoaderSerializationTests::class.java.getResource("/isolated.jar")
private const val ISOLATED_CONTRACT_CLASS_NAME = "net.corda.isolated.contracts.AnotherDummyContract"
}
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
val storage = MockAttachmentStorage()
private val storage = MockAttachmentStorage()
@Test
fun `Can serialize and deserialize with an attachment classloader`() {

View File

@ -4,35 +4,38 @@ import net.corda.core.contracts.Attachment
import net.corda.core.contracts.Contract
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.internal.declaredField
import net.corda.core.internal.inputStream
import net.corda.core.node.services.AttachmentId
import net.corda.core.serialization.internal.AttachmentsClassLoader
import net.corda.testing.core.internal.ContractJarTestUtils.signContractJar
import net.corda.testing.internal.fakeAttachment
import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP
import net.corda.testing.services.MockAttachmentStorage
import org.apache.commons.io.IOUtils
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Test
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.net.URL
import kotlin.test.assertFailsWith
class AttachmentsClassLoaderTests {
companion object {
val ISOLATED_CONTRACTS_JAR_PATH: URL = AttachmentsClassLoaderTests::class.java.getResource("isolated.jar")
// TODO Update this test to use the new isolated.jar
val ISOLATED_CONTRACTS_JAR_PATH: URL = AttachmentsClassLoaderTests::class.java.getResource("old-isolated.jar")
val ISOLATED_CONTRACTS_JAR_PATH_V4: URL = AttachmentsClassLoaderTests::class.java.getResource("isolated-4.0.jar")
val FINANCE_JAR_PATH: URL = AttachmentsClassLoaderTests::class.java.getResource("finance.jar")
private const val ISOLATED_CONTRACT_CLASS_NAME = "net.corda.finance.contracts.isolated.AnotherDummyContract"
private fun readAttachment(attachment: Attachment, filepath: String): ByteArray {
ByteArrayOutputStream().use {
return ByteArrayOutputStream().let {
attachment.extractFile(filepath, it)
return it.toByteArray()
it.toByteArray()
}
}
}
val storage = MockAttachmentStorage()
private val storage = MockAttachmentStorage()
@Test
fun `Loading AnotherDummyContract without using the AttachmentsClassLoader fails`() {
@ -43,7 +46,7 @@ class AttachmentsClassLoaderTests {
@Test
fun `Dynamically load AnotherDummyContract from isolated contracts jar using the AttachmentsClassLoader`() {
val isolatedId = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
val isolatedId = importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
val classloader = AttachmentsClassLoader(listOf(storage.openAttachment(isolatedId)!!))
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classloader)
@ -53,8 +56,8 @@ class AttachmentsClassLoaderTests {
@Test
fun `Test non-overlapping contract jar`() {
val att1 = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
val att2 = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH_V4.openStream(), "app", "isolated-4.0.jar")
val att1 = importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
val att2 = importAttachment(ISOLATED_CONTRACTS_JAR_PATH_V4.openStream(), "app", "isolated-4.0.jar")
assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) {
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
@ -63,9 +66,9 @@ class AttachmentsClassLoaderTests {
@Test
fun `Test valid overlapping contract jar`() {
val isolatedId = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
val isolatedId = importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
val signedJar = signContractJar(ISOLATED_CONTRACTS_JAR_PATH, copyFirst = true)
val isolatedSignedId = storage.importAttachment(signedJar.first.toUri().toURL().openStream(), "app", "isolated-signed.jar")
val isolatedSignedId = importAttachment(signedJar.first.toUri().toURL().openStream(), "app", "isolated-signed.jar")
// does not throw OverlappingAttachments exception
AttachmentsClassLoader(arrayOf(isolatedId, isolatedSignedId).map { storage.openAttachment(it)!! })
@ -73,8 +76,8 @@ class AttachmentsClassLoaderTests {
@Test
fun `Test non-overlapping different contract jars`() {
val att1 = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
val att2 = storage.importAttachment(FINANCE_JAR_PATH.openStream(), "app", "finance.jar")
val att1 = importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
val att2 = importAttachment(FINANCE_CONTRACTS_CORDAPP.jarFile.inputStream(), "app", "finance.jar")
// does not throw OverlappingAttachments exception
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
@ -82,8 +85,8 @@ class AttachmentsClassLoaderTests {
@Test
fun `Load text resources from AttachmentsClassLoader`() {
val att1 = storage.importAttachment(fakeAttachment("file1.txt", "some data").inputStream(), "app", "file1.jar")
val att2 = storage.importAttachment(fakeAttachment("file2.txt", "some other data").inputStream(), "app", "file2.jar")
val att1 = importAttachment(fakeAttachment("file1.txt", "some data").inputStream(), "app", "file1.jar")
val att2 = importAttachment(fakeAttachment("file2.txt", "some other data").inputStream(), "app", "file2.jar")
val cl = AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
@ -95,8 +98,8 @@ class AttachmentsClassLoaderTests {
@Test
fun `Test valid overlapping file condition`() {
val att1 = storage.importAttachment(fakeAttachment("file1.txt", "same data").inputStream(), "app", "file1.jar")
val att2 = storage.importAttachment(fakeAttachment("file1.txt", "same data").inputStream(), "app", "file2.jar")
val att1 = importAttachment(fakeAttachment("file1.txt", "same data").inputStream(), "app", "file1.jar")
val att2 = importAttachment(fakeAttachment("file1.txt", "same data").inputStream(), "app", "file2.jar")
val cl = AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
@ -106,8 +109,8 @@ class AttachmentsClassLoaderTests {
@Test
fun `No overlapping exception thrown on certain META-INF files`() {
listOf("meta-inf/manifest.mf", "meta-inf/license", "meta-inf/test.dsa", "meta-inf/test.sf").forEach { path ->
val att1 = storage.importAttachment(fakeAttachment(path, "some data").inputStream(), "app", "file1.jar")
val att2 = storage.importAttachment(fakeAttachment(path, "some other data").inputStream(), "app", "file2.jar")
val att1 = importAttachment(fakeAttachment(path, "some data").inputStream(), "app", "file1.jar")
val att2 = importAttachment(fakeAttachment(path, "some other data").inputStream(), "app", "file2.jar")
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
}
@ -115,10 +118,8 @@ class AttachmentsClassLoaderTests {
@Test
fun `Check platform independent path handling in attachment jars`() {
val storage = MockAttachmentStorage()
val att1 = storage.importAttachment(fakeAttachment("/folder1/foldera/file1.txt", "some data").inputStream(), "app", "file1.jar")
val att2 = storage.importAttachment(fakeAttachment("\\folder1\\folderb\\file2.txt", "some other data").inputStream(), "app", "file2.jar")
val att1 = importAttachment(fakeAttachment("/folder1/foldera/file1.txt", "some data").inputStream(), "app", "file1.jar")
val att2 = importAttachment(fakeAttachment("\\folder1\\folderb\\file2.txt", "some other data").inputStream(), "app", "file2.jar")
val data1a = readAttachment(storage.openAttachment(att1)!!, "/folder1/foldera/file1.txt")
assertArrayEquals("some data".toByteArray(), data1a)
@ -132,4 +133,8 @@ class AttachmentsClassLoaderTests {
val data2b = readAttachment(storage.openAttachment(att2)!!, "/folder1/folderb/file2.txt")
assertArrayEquals("some other data".toByteArray(), data2b)
}
private fun importAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId {
return jar.use { storage.importAttachment(jar, uploader, filename) }
}
}

View File

@ -17,7 +17,7 @@ import net.corda.testing.core.TestIdentity
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.MockServices
import net.corda.testing.node.ledger
import org.assertj.core.api.AssertionsForClassTypes
import org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType
import org.junit.Rule
import org.junit.Test
import java.time.Instant
@ -330,12 +330,13 @@ class TransactionEncumbranceTests {
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY2, 0, AutomaticHashConstraint)
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
.toLedgerTransaction(ledgerServices)
.verify()
}
// More complex encumbrance (full cycle of size 4) where one of the encumbered states is assigned to a different notary.
// 0 -> 1, 1 -> 3, 3 -> 2, 2 -> 0
// We expect that state at index 3 cannot be encumbered with the state at index 2, due to mismatched notaries.
AssertionsForClassTypes.assertThatExceptionOfType(TransactionVerificationException.TransactionNotaryMismatchEncumbranceException::class.java)
assertThatExceptionOfType(TransactionVerificationException.TransactionNotaryMismatchEncumbranceException::class.java)
.isThrownBy {
TransactionBuilder()
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 1, AutomaticHashConstraint)
@ -344,13 +345,15 @@ class TransactionEncumbranceTests {
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 2, AutomaticHashConstraint)
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
.toLedgerTransaction(ledgerServices)
.verify()
}
.withMessageContaining("index 3 is assigned to notary [O=Notary Service, L=Zurich, C=CH], while its encumbrance with index 2 is assigned to notary [O=Notary Service2, L=Zurich, C=CH]")
.withMessageContaining("index 3 is assigned to notary [O=Notary Service, L=Zurich, C=CH], while its encumbrance with " +
"index 2 is assigned to notary [O=Notary Service2, L=Zurich, C=CH]")
// Two different encumbrance chains, where only one fails due to mismatched notary.
// 0 -> 1, 1 -> 0, 2 -> 3, 3 -> 2 where encumbered states with indices 2 and 3, respectively, are assigned
// to different notaries.
AssertionsForClassTypes.assertThatExceptionOfType(TransactionVerificationException.TransactionNotaryMismatchEncumbranceException::class.java)
assertThatExceptionOfType(TransactionVerificationException.TransactionNotaryMismatchEncumbranceException::class.java)
.isThrownBy {
TransactionBuilder()
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 1, AutomaticHashConstraint)
@ -359,7 +362,9 @@ class TransactionEncumbranceTests {
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY2, 2, AutomaticHashConstraint)
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
.toLedgerTransaction(ledgerServices)
.verify()
}
.withMessageContaining("index 2 is assigned to notary [O=Notary Service, L=Zurich, C=CH], while its encumbrance with index 3 is assigned to notary [O=Notary Service2, L=Zurich, C=CH]")
.withMessageContaining("index 2 is assigned to notary [O=Notary Service, L=Zurich, C=CH], while its encumbrance with " +
"index 3 is assigned to notary [O=Notary Service2, L=Zurich, C=CH]")
}
}

View File

@ -170,6 +170,7 @@ class TransactionTests {
val id = SecureHash.randomSHA256()
val timeWindow: TimeWindow? = null
val privacySalt = PrivacySalt()
fun buildTransaction() = LedgerTransaction.create(
inputs,
outputs,
@ -184,7 +185,7 @@ class TransactionTests {
inputStatesContractClassNameToMaxVersion = emptyMap()
)
assertFailsWith<TransactionVerificationException.NotaryChangeInWrongTransactionType> { buildTransaction() }
assertFailsWith<TransactionVerificationException.NotaryChangeInWrongTransactionType> { buildTransaction().verify() }
}
@Test

View File

@ -6,7 +6,7 @@ import net.corda.core.identity.Party
import net.corda.core.transactions.TransactionBuilder
/**
* This interface deliberately mirrors the one in the finance:isolated module.
* This interface deliberately mirrors the one in the isolated module.
* We will actually link [AnotherDummyContract] against this interface rather
* than the one inside isolated.jar, which means we won't need to use reflection
* to execute the contract's generateInitial() method.

Binary file not shown.

View File

@ -1,6 +0,0 @@
apply plugin: 'kotlin'
apply plugin: CanonicalizerPlugin
dependencies {
compileOnly project(':core')
}

View File

@ -1,35 +0,0 @@
package net.corda.finance.contracts.isolated
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.*
import net.corda.core.identity.Party
/**
* Just sends a dummy state to the other side: used for testing whether attachments with code in them are being
* loaded or blocked.
*/
class IsolatedDummyFlow {
@StartableByRPC
@InitiatingFlow
class Initiator(val toWhom: Party) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val tx = AnotherDummyContract().generateInitial(
serviceHub.myInfo.legalIdentities.first().ref(0),
1234,
serviceHub.networkMapCache.notaryIdentities.first()
)
val stx = serviceHub.signInitialTransaction(tx)
subFlow(SendTransactionFlow(initiateFlow(toWhom), stx))
}
}
@InitiatedBy(Initiator::class)
class Acceptor(val session: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val stx = subFlow(ReceiveTransactionFlow(session, checkSufficientSignatures = false))
stx.verify(serviceHub)
}
}
}

26
isolated/build.gradle Normal file
View File

@ -0,0 +1,26 @@
apply plugin: 'kotlin'
apply plugin: CanonicalizerPlugin
apply plugin: 'net.corda.plugins.cordapp'
description 'Isolated CorDapp for testing'
dependencies {
cordaCompile project(':core')
}
cordapp {
targetPlatformVersion corda_platform_version.toInteger()
minimumPlatformVersion 1
contract {
name "Isolated Test CorDapp"
versionId 1
vendor "R3"
licence "Open Source (Apache 2)"
}
workflow {
name "Isolated Test CorDapp"
versionId 1
vendor "R3"
licence "Open Source (Apache 2)"
}
}

View File

@ -1,4 +1,4 @@
package net.corda.finance.contracts.isolated
package net.corda.isolated.contracts
import net.corda.core.contracts.*
import net.corda.core.identity.AbstractParty
@ -7,8 +7,6 @@ import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.nodeapi.DummyContractBackdoor
const val ANOTHER_DUMMY_PROGRAM_ID = "net.corda.finance.contracts.isolated.AnotherDummyContract"
@Suppress("UNUSED")
class AnotherDummyContract : Contract, DummyContractBackdoor {
val magicString = "helloworld"
@ -32,4 +30,8 @@ class AnotherDummyContract : Contract, DummyContractBackdoor {
}
override fun inspectState(state: ContractState): Int = (state as State).magicNumber
companion object {
const val ANOTHER_DUMMY_PROGRAM_ID = "net.corda.isolated.contracts.AnotherDummyContract"
}
}

View File

@ -0,0 +1,25 @@
package net.corda.isolated.workflows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateRef
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC
import net.corda.isolated.contracts.AnotherDummyContract
@StartableByRPC
class IsolatedIssuanceFlow(private val magicNumber: Int) : FlowLogic<StateRef>() {
@Suspendable
override fun call(): StateRef {
val stx = serviceHub.signInitialTransaction(
AnotherDummyContract().generateInitial(
ourIdentity.ref(0),
magicNumber,
serviceHub.networkMapCache.notaryIdentities.first()
)
)
stx.verify(serviceHub)
serviceHub.recordTransactions(stx)
return stx.tx.outRef<ContractState>(0).ref
}
}

View File

@ -130,7 +130,6 @@ dependencies {
testCompile project(':client:jfx')
testCompile project(':finance:contracts')
testCompile project(':finance:workflows')
testCompile project(':finance:isolated')
// sample test schemas
testCompile project(path: ':finance:contracts', configuration: 'testArtifacts')

View File

@ -1,117 +1,123 @@
package net.corda.node.services
import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.CordaRuntimeException
import net.corda.core.contracts.*
import net.corda.core.cordapp.CordappProvider
import net.corda.core.flows.FlowLogic
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateRef
import net.corda.core.flows.*
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.toLedgerTransaction
import net.corda.core.node.NetworkParameters
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.node.services.IdentityService
import net.corda.core.node.services.NetworkParametersStorage
import net.corda.core.serialization.SerializationFactory
import net.corda.core.serialization.serialize
import net.corda.core.internal.*
import net.corda.core.internal.concurrent.transpose
import net.corda.core.messaging.startFlow
import net.corda.core.serialization.MissingAttachmentsException
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.getOrThrow
import net.corda.node.VersionInfo
import net.corda.node.internal.cordapp.CordappProviderImpl
import net.corda.node.internal.cordapp.JarScanningCordappLoader
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.common.internal.addNotary
import net.corda.testing.core.DUMMY_BANK_A_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.TestIdentity
import net.corda.core.utilities.unwrap
import net.corda.testing.common.internal.checkNotOnClasspath
import net.corda.testing.core.*
import net.corda.testing.driver.DriverDSL
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.NodeParameters
import net.corda.testing.driver.driver
import net.corda.testing.internal.MockCordappConfigProvider
import net.corda.testing.internal.rigorousMock
import net.corda.testing.internal.withoutTestSerialization
import net.corda.testing.node.NotarySpec
import net.corda.testing.node.internal.cordappsForPackages
import net.corda.testing.services.MockAttachmentStorage
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Test
import java.net.URL
import java.net.URLClassLoader
import kotlin.test.assertFailsWith
class AttachmentLoadingTests {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
private val attachments = MockAttachmentStorage()
private val provider = CordappProviderImpl(JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR), VersionInfo.UNKNOWN), MockCordappConfigProvider(), attachments).apply {
start(testNetworkParameters().whitelistedContractImplementations)
}
private val cordapp get() = provider.cordapps.first()
private val attachmentId get() = provider.getCordappAttachmentId(cordapp)!!
private val appContext get() = provider.getAppContext(cordapp)
private companion object {
val isolatedJAR = AttachmentLoadingTests::class.java.getResource("isolated.jar")!!
const val ISOLATED_CONTRACT_ID = "net.corda.finance.contracts.isolated.AnotherDummyContract"
val isolatedJar: URL = AttachmentLoadingTests::class.java.getResource("/isolated.jar")
val isolatedClassLoader = URLClassLoader(arrayOf(isolatedJar))
val issuanceFlowClass: Class<FlowLogic<StateRef>> = uncheckedCast(loadFromIsolated("net.corda.isolated.workflows.IsolatedIssuanceFlow"))
val bankAName = CordaX500Name("BankA", "Zurich", "CH")
val bankBName = CordaX500Name("BankB", "Zurich", "CH")
val flowInitiatorClass: Class<out FlowLogic<*>> =
Class.forName("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator", true, URLClassLoader(arrayOf(isolatedJAR)))
.asSubclass(FlowLogic::class.java)
val DUMMY_BANK_A = TestIdentity(DUMMY_BANK_A_NAME, 40).party
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
}
private val services = object : ServicesForResolution {
private val testNetworkParameters = testNetworkParameters().addNotary(DUMMY_NOTARY)
override fun loadState(stateRef: StateRef): TransactionState<*> = throw NotImplementedError()
override fun loadStates(stateRefs: Set<StateRef>): Set<StateAndRef<ContractState>> = throw NotImplementedError()
override fun loadContractAttachment(stateRef: StateRef, interestedContractClassName : ContractClassName?): Attachment = throw NotImplementedError()
override val identityService = rigorousMock<IdentityService>().apply {
doReturn(null).whenever(this).partyFromKey(DUMMY_BANK_A.owningKey)
}
override val attachments: AttachmentStorage get() = this@AttachmentLoadingTests.attachments
override val cordappProvider: CordappProvider get() = this@AttachmentLoadingTests.provider
override val networkParameters: NetworkParameters = testNetworkParameters
override val networkParametersStorage: NetworkParametersStorage get() = rigorousMock<NetworkParametersStorage>().apply {
doReturn(testNetworkParameters.serialize().hash).whenever(this).currentHash
doReturn(testNetworkParameters).whenever(this).lookup(any())
}
}
@Test
fun `test a wire transaction has loaded the correct attachment`() {
val appClassLoader = appContext.classLoader
val contractClass = appClassLoader.loadClass(ISOLATED_CONTRACT_ID).asSubclass(Contract::class.java)
val generateInitialMethod = contractClass.getDeclaredMethod("generateInitial", PartyAndReference::class.java, Integer.TYPE, Party::class.java)
val contract = contractClass.newInstance()
val txBuilder = generateInitialMethod.invoke(contract, DUMMY_BANK_A.ref(1), 1, DUMMY_NOTARY) as TransactionBuilder
val context = SerializationFactory.defaultFactory.defaultContext.withClassLoader(appClassLoader)
val ledgerTx = txBuilder.toLedgerTransaction(services, context)
contract.verify(ledgerTx)
val actual = ledgerTx.attachments.first()
val expected = attachments.openAttachment(attachmentId)!!
assertEquals(expected, actual)
}
@Test
fun `test that attachments retrieved over the network are not used for code`() {
withoutTestSerialization {
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptySet())) {
val additionalCordapps = cordappsForPackages("net.corda.finance.contracts.isolated")
val bankA = startNode(NodeParameters(providedName = bankAName, additionalCordapps = additionalCordapps)).getOrThrow()
val bankB = startNode(NodeParameters(providedName = bankBName, additionalCordapps = additionalCordapps)).getOrThrow()
assertFailsWith<CordaRuntimeException>("Party C=CH,L=Zurich,O=BankB rejected session request: Don't know net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator") {
bankA.rpc.startFlowDynamic(flowInitiatorClass, bankB.nodeInfo.legalIdentities.first()).returnValue.getOrThrow()
}
init {
checkNotOnClasspath("net.corda.isolated.contracts.AnotherDummyContract") {
"isolated module cannot be on the classpath as otherwise it will be pulled into the nodes the driver creates and " +
"contaminate the tests. This is a known issue with the driver and we must work around it until it's fixed."
}
Unit
}
fun loadFromIsolated(className: String): Class<*> = Class.forName(className, false, isolatedClassLoader)
}
@Test
fun `contracts downloaded from the network are not executed without the DJVM`() {
driver(DriverParameters(
startNodesInProcess = false,
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = false)),
cordappsForAllNodes = cordappsForPackages(javaClass.packageName)
)) {
installIsolatedCordapp(ALICE_NAME)
val (alice, bob) = listOf(
startNode(providedName = ALICE_NAME),
startNode(providedName = BOB_NAME)
).transpose().getOrThrow()
val stateRef = alice.rpc.startFlowDynamic(issuanceFlowClass, 1234).returnValue.getOrThrow()
// The exception that we actually want is MissingAttachmentsException, but this is thrown in a responder flow on Bob. To work
// around that it's re-thrown as a FlowException so that it can be propagated to Alice where we pick it here.
assertThatThrownBy {
alice.rpc.startFlow(::ConsumeAndBroadcastFlow, stateRef, bob.nodeInfo.singleIdentity()).returnValue.getOrThrow()
}.hasMessage("Attempting to load Contract Attachments downloaded from the network")
}
}
}
@Test
fun `contract is executed if installed locally`() {
driver(DriverParameters(
startNodesInProcess = false,
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = false)),
cordappsForAllNodes = cordappsForPackages(javaClass.packageName)
)) {
installIsolatedCordapp(ALICE_NAME)
installIsolatedCordapp(BOB_NAME)
val (alice, bob) = listOf(
startNode(providedName = ALICE_NAME),
startNode(providedName = BOB_NAME)
).transpose().getOrThrow()
val stateRef = alice.rpc.startFlowDynamic(issuanceFlowClass, 1234).returnValue.getOrThrow()
alice.rpc.startFlow(::ConsumeAndBroadcastFlow, stateRef, bob.nodeInfo.singleIdentity()).returnValue.getOrThrow()
}
}
private fun DriverDSL.installIsolatedCordapp(name: CordaX500Name) {
val cordappsDir = (baseDirectory(name) / "cordapps").createDirectories()
isolatedJar.toPath().copyToDirectory(cordappsDir)
}
@InitiatingFlow
@StartableByRPC
class ConsumeAndBroadcastFlow(private val stateRef: StateRef, private val otherSide: Party) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val notary = serviceHub.networkMapCache.notaryIdentities[0]
val stateAndRef = serviceHub.toStateAndRef<ContractState>(stateRef)
val stx = serviceHub.signInitialTransaction(
TransactionBuilder(notary).addInputState(stateAndRef).addCommand(dummyCommand(ourIdentity.owningKey))
)
stx.verify(serviceHub, checkSufficientSignatures = false)
val session = initiateFlow(otherSide)
subFlow(FinalityFlow(stx, session))
// It's important we wait on this dummy receive, as otherwise it's possible we miss any errors the other side throws
session.receive<String>().unwrap { require(it == "OK") { "Not OK: $it"} }
}
}
@InitiatedBy(ConsumeAndBroadcastFlow::class)
class ConsumeAndBroadcastResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
try {
subFlow(ReceiveFinalityFlow(otherSide))
} catch (e: MissingAttachmentsException) {
throw FlowException(e.message)
}
otherSide.send("OK")
}
}
}

Binary file not shown.

View File

@ -335,7 +335,7 @@ class NodeAttachmentService(
)
session.save(attachment)
attachmentCount.inc()
log.info("Stored new attachment $id")
log.info("Stored new attachment: id=$id uploader=$uploader filename=$filename")
contractClassNames.forEach { contractsCache.invalidate(it) }
return@withContractsInJar id
}

View File

@ -15,10 +15,10 @@ import java.net.URL
class CordappProviderImplTests {
private companion object {
val isolatedJAR = this::class.java.getResource("isolated.jar")!!
val isolatedJAR: URL = this::class.java.getResource("/isolated.jar")
// TODO: Cordapp name should differ from the JAR name
const val isolatedCordappName = "isolated"
val emptyJAR = this::class.java.getResource("empty.jar")!!
val emptyJAR: URL = this::class.java.getResource("empty.jar")
val validConfig: Config = ConfigFactory.parseString("key=value")
val stubConfigProvider = object : CordappConfigProvider {
@ -52,7 +52,7 @@ class CordappProviderImplTests {
@Test
fun `test that we find a cordapp class that is loaded into the store`() {
val provider = newCordappProvider(isolatedJAR)
val className = "net.corda.finance.contracts.isolated.AnotherDummyContract"
val className = "net.corda.isolated.contracts.AnotherDummyContract"
val expected = provider.cordapps.first()
val actual = provider.getCordappForClass(className)
@ -62,9 +62,9 @@ class CordappProviderImplTests {
}
@Test
fun `test that we find an attachment for a cordapp contrat class`() {
fun `test that we find an attachment for a cordapp contract class`() {
val provider = newCordappProvider(isolatedJAR)
val className = "net.corda.finance.contracts.isolated.AnotherDummyContract"
val className = "net.corda.isolated.contracts.AnotherDummyContract"
val expected = provider.getAppContext(provider.cordapps.first()).attachmentId
val actual = provider.getContractAttachmentID(className)

View File

@ -36,8 +36,8 @@ class DummyRPCFlow : FlowLogic<Unit>() {
class JarScanningCordappLoaderTest {
private companion object {
const val isolatedContractId = "net.corda.finance.contracts.isolated.AnotherDummyContract"
const val isolatedFlowName = "net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator"
const val isolatedContractId = "net.corda.isolated.contracts.AnotherDummyContract"
const val isolatedFlowName = "net.corda.isolated.workflows.IsolatedIssuanceFlow"
}
@Test
@ -49,15 +49,15 @@ class JarScanningCordappLoaderTest {
@Test
fun `isolated JAR contains a CorDapp with a contract and plugin`() {
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("isolated.jar")!!
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("/isolated.jar")
val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR))
assertThat(loader.cordapps).hasSize(1)
val actualCordapp = loader.cordapps.single()
assertThat(actualCordapp.contractClassNames).isEqualTo(listOf(isolatedContractId))
assertThat(actualCordapp.initiatedFlows.first().name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Acceptor")
assertThat(actualCordapp.rpcFlows).isEmpty()
assertThat(actualCordapp.initiatedFlows).isEmpty()
assertThat(actualCordapp.rpcFlows.first().name).isEqualTo(isolatedFlowName)
assertThat(actualCordapp.schedulableFlows).isEmpty()
assertThat(actualCordapp.services).isEmpty()
assertThat(actualCordapp.serializationWhitelists).hasSize(1)
@ -83,7 +83,7 @@ class JarScanningCordappLoaderTest {
// being used internally. Later iterations will use a classloader per cordapp and this test can be retired.
@Test
fun `cordapp classloader can load cordapp classes`() {
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("isolated.jar")!!
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("/isolated.jar")
val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR), VersionInfo.UNKNOWN)
loader.appClassLoader.loadClass(isolatedContractId)

Binary file not shown.

View File

@ -115,11 +115,12 @@ class CordaClassResolverTests {
val emptyListClass = listOf<Any>().javaClass
val emptySetClass = setOf<Any>().javaClass
val emptyMapClass = mapOf<Any, Any>().javaClass
val ISOLATED_CONTRACTS_JAR_PATH: URL = CordaClassResolverTests::class.java.getResource("isolated.jar")
val ISOLATED_CONTRACTS_JAR_PATH: URL = CordaClassResolverTests::class.java.getResource("/isolated.jar")
}
private val emptyWhitelistContext: CheckpointSerializationContext = CheckpointSerializationContextImpl(this.javaClass.classLoader, EmptyWhitelist, emptyMap(), true, null)
private val allButBlacklistedContext: CheckpointSerializationContext = CheckpointSerializationContextImpl(this.javaClass.classLoader, AllButBlacklisted, emptyMap(), true, null)
@Test
fun `Annotation on enum works for specialised entries`() {
CordaClassResolver(emptyWhitelistContext).getRegistration(Foo.Bar::class.java)
@ -212,7 +213,7 @@ class CordaClassResolverTests {
val storage = MockAttachmentStorage()
val attachmentHash = importJar(storage)
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! })
val attachedClass = Class.forName("net.corda.finance.contracts.isolated.AnotherDummyContract", true, classLoader)
val attachedClass = Class.forName("net.corda.isolated.contracts.AnotherDummyContract", true, classLoader)
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
}
@ -221,7 +222,7 @@ class CordaClassResolverTests {
val storage = MockAttachmentStorage()
val attachmentHash = importJar(storage, "some_uploader")
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! })
val attachedClass = Class.forName("net.corda.finance.contracts.isolated.AnotherDummyContract", true, classLoader)
val attachedClass = Class.forName("net.corda.isolated.contracts.AnotherDummyContract", true, classLoader)
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
}

Binary file not shown.

View File

@ -5,7 +5,7 @@ include 'confidential-identities'
include 'finance' // maintained for backwards compatibility only
include 'finance:contracts'
include 'finance:workflows'
include 'finance:isolated'
include 'isolated'
include 'core'
include 'docs'
include 'node-api'

View File

@ -49,7 +49,9 @@ data class CustomCordapp(
@VisibleForTesting
internal fun packageAsJar(file: Path) {
val scanResult = ClassGraph()
val classGraph = ClassGraph()
classes.forEach { classGraph.addClassLoader(it.classLoader) }
val scanResult = classGraph
.whitelistPackages(*packages.toTypedArray())
.whitelistClasses(*classes.map { it.name }.toTypedArray())
.scan()

View File

@ -21,7 +21,9 @@ interface TestCordappInternal : TestCordapp {
fun withOnlyJarContents(): TestCordappInternal
companion object {
fun installCordapps(baseDirectory: Path, nodeSpecificCordapps: Set<TestCordappInternal>, generalCordapps: Set<TestCordappInternal>) {
fun installCordapps(baseDirectory: Path,
nodeSpecificCordapps: Set<TestCordappInternal>,
generalCordapps: Set<TestCordappInternal> = emptySet()) {
val nodeSpecificCordappsWithoutMeta = checkNoConflicts(nodeSpecificCordapps)
checkNoConflicts(generalCordapps)

View File

@ -23,7 +23,7 @@ val FINANCE_CONTRACTS_CORDAPP: TestCordappImpl = findCordapp("net.corda.finance.
val FINANCE_WORKFLOWS_CORDAPP: TestCordappImpl = findCordapp("net.corda.finance.flows")
@JvmField
val FINANCE_CORDAPPS: List<TestCordappInternal> = listOf(FINANCE_CONTRACTS_CORDAPP, FINANCE_WORKFLOWS_CORDAPP)
val FINANCE_CORDAPPS: Set<TestCordappInternal> = setOf(FINANCE_CONTRACTS_CORDAPP, FINANCE_WORKFLOWS_CORDAPP)
fun cordappsForPackages(vararg packageNames: String): Set<CustomCordapp> = cordappsForPackages(packageNames.asList())

View File

@ -2,33 +2,16 @@ package net.corda.testing.internal
import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme
import net.corda.core.serialization.internal.SerializationEnvironment
import net.corda.core.serialization.internal._contextSerializationEnv
import net.corda.core.serialization.internal._inheritableContextSerializationEnv
import net.corda.node.serialization.amqp.AMQPServerSerializationScheme
import net.corda.node.serialization.kryo.KRYO_CHECKPOINT_CONTEXT
import net.corda.node.serialization.kryo.KryoCheckpointSerializer
import net.corda.serialization.internal.*
import net.corda.testing.common.internal.asContextEnv
import net.corda.testing.core.SerializationEnvironmentRule
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService
val inVMExecutors = ConcurrentHashMap<SerializationEnvironment, ExecutorService>()
/**
* For example your test class uses [SerializationEnvironmentRule] but you want to turn it off for one method.
* Use sparingly, ideally a test class shouldn't mix serializers init mechanisms.
*/
fun <T> withoutTestSerialization(callable: () -> T): T { // TODO: Delete this, see CORDA-858.
val (property, env) = listOf(_contextSerializationEnv, _inheritableContextSerializationEnv).map { Pair(it, it.get()) }.single { it.second != null }
property.set(null)
try {
return callable()
} finally {
property.set(env)
}
}
fun createTestSerializationEnv(): SerializationEnvironment {
val factory = SerializationFactoryImpl().apply {
registerScheme(AMQPClientSerializationScheme(emptyList()))