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-blobinspector_test" target="1.8" />
<module name="net.corda-finance-contracts-states_main" 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-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_main" target="1.8" />
<module name="net.corda-verifier_test" target="1.8" /> <module name="net.corda-verifier_test" target="1.8" />
<module name="net.corda_buildSrc_main" 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? // TODO: include notary timestamp?
@CordaSerializable @CordaSerializable
data class StateConsumptionDetails( data class StateConsumptionDetails(
val hashOfTransactionId: SecureHash, val hashOfTransactionId: SecureHash,
val type: ConsumedStateType 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]. * 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. * 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 @DeleteForDJVM
class ResolveTransactionsFlow(txHashesArg: Set<SecureHash>, class ResolveTransactionsFlow(txHashesArg: Set<SecureHash>, private val otherSide: FlowSession) : FlowLogic<Unit>() {
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. // Need it ordered in terms of iteration. Needs to be a variable for the check-pointing logic to work.
private val txHashes = txHashesArg.toList() 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. // If the componentGroup is a [LazyMappedList] it means that the original deserialized version is already available.
val components = group.components val components = group.components
if (!forceDeserialize && components is LazyMappedList<*, OpaqueBytes>) { if (!forceDeserialize && components is LazyMappedList<*, OpaqueBytes>) {
return components.originalList as List<T> return uncheckedCast(components.originalList)
} }
return components.lazyMapped { component, internalIndex -> return components.lazyMapped { component, internalIndex ->

View File

@ -66,8 +66,6 @@ private constructor(
checkBaseInvariants() checkBaseInvariants()
if (timeWindow != null) check(notary != null) { "Transactions with time-windows must be notarised" } if (timeWindow != null) check(notary != null) { "Transactions with time-windows must be notarised" }
checkNotaryWhitelisted() checkNotaryWhitelisted()
checkNoNotaryChange()
checkEncumbrancesValid()
} }
companion object { companion object {
@ -101,9 +99,6 @@ private constructor(
val inputStates: List<ContractState> get() = inputs.map { it.state.data } val inputStates: List<ContractState> get() = inputs.map { it.state.data }
val referenceStates: List<ContractState> get() = references.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 * Returns the typed input StateAndRef at the specified index
* @param index The index into the inputs. * @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. " + 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.") "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 -> AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(this.attachments) { transactionClassLoader ->
Verifier(createLtxForVerification(), transactionClassLoader).verify()
val internalTx = createLtxForVerification()
validateContractVersions(contractAttachmentsByContract)
validatePackageOwnership(contractAttachmentsByContract)
validateStatesAgainstContract(internalTx)
val hashToSignatureConstrainedContracts = verifyConstraintsValidity(internalTx, contractAttachmentsByContract, transactionClassLoader)
verifyConstraints(internalTx, contractAttachmentsByContract, hashToSignatureConstrainedContracts)
verifyContracts(internalTx)
} }
} }
/**
* 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 { private fun createLtxForVerification(): LedgerTransaction {
val serializedInputs = this.serializedInputs val serializedInputs = this.serializedInputs
val serializedReferences = this.serializedReferences val serializedReferences = this.serializedReferences
@ -368,7 +157,7 @@ private constructor(
privacySalt = this.privacySalt, privacySalt = this.privacySalt,
networkParameters = this.networkParameters, networkParameters = this.networkParameters,
references = deserializedReferences, references = deserializedReferences,
inputStatesContractClassNameToMaxVersion = emptyMap() inputStatesContractClassNameToMaxVersion = this.inputStatesContractClassNameToMaxVersion
) )
} else { } else {
// This branch is only present for backwards compatibility. // 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 * 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 * 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() |)""".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 // Stuff that we can't remove and so is deprecated instead
// //
@Deprecated("LedgerTransaction should not be created directly, use WireTransaction.toLedgerTransaction instead.") @Deprecated("LedgerTransaction should not be created directly, use WireTransaction.toLedgerTransaction instead.")

View File

@ -26,15 +26,15 @@ import kotlin.test.assertFailsWith
class AttachmentsClassLoaderSerializationTests { class AttachmentsClassLoaderSerializationTests {
companion object { companion object {
val ISOLATED_CONTRACTS_JAR_PATH: URL = AttachmentsClassLoaderSerializationTests::class.java.getResource("isolated.jar") 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" private const val ISOLATED_CONTRACT_CLASS_NAME = "net.corda.isolated.contracts.AnotherDummyContract"
} }
@Rule @Rule
@JvmField @JvmField
val testSerialization = SerializationEnvironmentRule() val testSerialization = SerializationEnvironmentRule()
val storage = MockAttachmentStorage() private val storage = MockAttachmentStorage()
@Test @Test
fun `Can serialize and deserialize with an attachment classloader`() { 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.Contract
import net.corda.core.contracts.TransactionVerificationException import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.internal.declaredField 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.core.serialization.internal.AttachmentsClassLoader
import net.corda.testing.core.internal.ContractJarTestUtils.signContractJar import net.corda.testing.core.internal.ContractJarTestUtils.signContractJar
import net.corda.testing.internal.fakeAttachment import net.corda.testing.internal.fakeAttachment
import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP
import net.corda.testing.services.MockAttachmentStorage import net.corda.testing.services.MockAttachmentStorage
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.net.URL import java.net.URL
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
class AttachmentsClassLoaderTests { class AttachmentsClassLoaderTests {
companion object { 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 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 const val ISOLATED_CONTRACT_CLASS_NAME = "net.corda.finance.contracts.isolated.AnotherDummyContract"
private fun readAttachment(attachment: Attachment, filepath: String): ByteArray { private fun readAttachment(attachment: Attachment, filepath: String): ByteArray {
ByteArrayOutputStream().use { return ByteArrayOutputStream().let {
attachment.extractFile(filepath, it) attachment.extractFile(filepath, it)
return it.toByteArray() it.toByteArray()
} }
} }
} }
val storage = MockAttachmentStorage() private val storage = MockAttachmentStorage()
@Test @Test
fun `Loading AnotherDummyContract without using the AttachmentsClassLoader fails`() { fun `Loading AnotherDummyContract without using the AttachmentsClassLoader fails`() {
@ -43,7 +46,7 @@ class AttachmentsClassLoaderTests {
@Test @Test
fun `Dynamically load AnotherDummyContract from isolated contracts jar using the AttachmentsClassLoader`() { 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 classloader = AttachmentsClassLoader(listOf(storage.openAttachment(isolatedId)!!))
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classloader) val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classloader)
@ -53,8 +56,8 @@ class AttachmentsClassLoaderTests {
@Test @Test
fun `Test non-overlapping contract jar`() { fun `Test non-overlapping contract jar`() {
val att1 = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar") val att1 = 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 att2 = importAttachment(ISOLATED_CONTRACTS_JAR_PATH_V4.openStream(), "app", "isolated-4.0.jar")
assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) { assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) {
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! }) AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
@ -63,9 +66,9 @@ class AttachmentsClassLoaderTests {
@Test @Test
fun `Test valid overlapping contract jar`() { 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 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 // does not throw OverlappingAttachments exception
AttachmentsClassLoader(arrayOf(isolatedId, isolatedSignedId).map { storage.openAttachment(it)!! }) AttachmentsClassLoader(arrayOf(isolatedId, isolatedSignedId).map { storage.openAttachment(it)!! })
@ -73,8 +76,8 @@ class AttachmentsClassLoaderTests {
@Test @Test
fun `Test non-overlapping different contract jars`() { fun `Test non-overlapping different contract jars`() {
val att1 = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar") val att1 = importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
val att2 = storage.importAttachment(FINANCE_JAR_PATH.openStream(), "app", "finance.jar") val att2 = importAttachment(FINANCE_CONTRACTS_CORDAPP.jarFile.inputStream(), "app", "finance.jar")
// does not throw OverlappingAttachments exception // does not throw OverlappingAttachments exception
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! }) AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
@ -82,8 +85,8 @@ class AttachmentsClassLoaderTests {
@Test @Test
fun `Load text resources from AttachmentsClassLoader`() { fun `Load text resources from AttachmentsClassLoader`() {
val att1 = storage.importAttachment(fakeAttachment("file1.txt", "some data").inputStream(), "app", "file1.jar") val att1 = 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 att2 = importAttachment(fakeAttachment("file2.txt", "some other data").inputStream(), "app", "file2.jar")
val cl = AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! }) val cl = AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name()) val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
@ -95,8 +98,8 @@ class AttachmentsClassLoaderTests {
@Test @Test
fun `Test valid overlapping file condition`() { fun `Test valid overlapping file condition`() {
val att1 = storage.importAttachment(fakeAttachment("file1.txt", "same data").inputStream(), "app", "file1.jar") val att1 = importAttachment(fakeAttachment("file1.txt", "same data").inputStream(), "app", "file1.jar")
val att2 = storage.importAttachment(fakeAttachment("file1.txt", "same data").inputStream(), "app", "file2.jar") val att2 = importAttachment(fakeAttachment("file1.txt", "same data").inputStream(), "app", "file2.jar")
val cl = AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! }) val cl = AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name()) val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
@ -106,8 +109,8 @@ class AttachmentsClassLoaderTests {
@Test @Test
fun `No overlapping exception thrown on certain META-INF files`() { 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 -> 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 att1 = importAttachment(fakeAttachment(path, "some data").inputStream(), "app", "file1.jar")
val att2 = storage.importAttachment(fakeAttachment(path, "some other data").inputStream(), "app", "file2.jar") val att2 = importAttachment(fakeAttachment(path, "some other data").inputStream(), "app", "file2.jar")
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! }) AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
} }
@ -115,10 +118,8 @@ class AttachmentsClassLoaderTests {
@Test @Test
fun `Check platform independent path handling in attachment jars`() { fun `Check platform independent path handling in attachment jars`() {
val storage = MockAttachmentStorage() 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 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 data1a = readAttachment(storage.openAttachment(att1)!!, "/folder1/foldera/file1.txt") val data1a = readAttachment(storage.openAttachment(att1)!!, "/folder1/foldera/file1.txt")
assertArrayEquals("some data".toByteArray(), data1a) assertArrayEquals("some data".toByteArray(), data1a)
@ -132,4 +133,8 @@ class AttachmentsClassLoaderTests {
val data2b = readAttachment(storage.openAttachment(att2)!!, "/folder1/folderb/file2.txt") val data2b = readAttachment(storage.openAttachment(att2)!!, "/folder1/folderb/file2.txt")
assertArrayEquals("some other data".toByteArray(), data2b) 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.internal.rigorousMock
import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices
import net.corda.testing.node.ledger 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.Rule
import org.junit.Test import org.junit.Test
import java.time.Instant import java.time.Instant
@ -330,12 +330,13 @@ class TransactionEncumbranceTests {
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY2, 0, AutomaticHashConstraint) .addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY2, 0, AutomaticHashConstraint)
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey) .addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
.toLedgerTransaction(ledgerServices) .toLedgerTransaction(ledgerServices)
.verify()
} }
// More complex encumbrance (full cycle of size 4) where one of the encumbered states is assigned to a different notary. // 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 // 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. // 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 { .isThrownBy {
TransactionBuilder() TransactionBuilder()
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 1, AutomaticHashConstraint) .addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 1, AutomaticHashConstraint)
@ -344,13 +345,15 @@ class TransactionEncumbranceTests {
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 2, AutomaticHashConstraint) .addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 2, AutomaticHashConstraint)
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey) .addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
.toLedgerTransaction(ledgerServices) .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. // 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 // 0 -> 1, 1 -> 0, 2 -> 3, 3 -> 2 where encumbered states with indices 2 and 3, respectively, are assigned
// to different notaries. // to different notaries.
AssertionsForClassTypes.assertThatExceptionOfType(TransactionVerificationException.TransactionNotaryMismatchEncumbranceException::class.java) assertThatExceptionOfType(TransactionVerificationException.TransactionNotaryMismatchEncumbranceException::class.java)
.isThrownBy { .isThrownBy {
TransactionBuilder() TransactionBuilder()
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 1, AutomaticHashConstraint) .addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 1, AutomaticHashConstraint)
@ -359,7 +362,9 @@ class TransactionEncumbranceTests {
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY2, 2, AutomaticHashConstraint) .addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY2, 2, AutomaticHashConstraint)
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey) .addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
.toLedgerTransaction(ledgerServices) .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 id = SecureHash.randomSHA256()
val timeWindow: TimeWindow? = null val timeWindow: TimeWindow? = null
val privacySalt = PrivacySalt() val privacySalt = PrivacySalt()
fun buildTransaction() = LedgerTransaction.create( fun buildTransaction() = LedgerTransaction.create(
inputs, inputs,
outputs, outputs,
@ -184,7 +185,7 @@ class TransactionTests {
inputStatesContractClassNameToMaxVersion = emptyMap() inputStatesContractClassNameToMaxVersion = emptyMap()
) )
assertFailsWith<TransactionVerificationException.NotaryChangeInWrongTransactionType> { buildTransaction() } assertFailsWith<TransactionVerificationException.NotaryChangeInWrongTransactionType> { buildTransaction().verify() }
} }
@Test @Test

View File

@ -6,7 +6,7 @@ import net.corda.core.identity.Party
import net.corda.core.transactions.TransactionBuilder 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 * We will actually link [AnotherDummyContract] against this interface rather
* than the one inside isolated.jar, which means we won't need to use reflection * than the one inside isolated.jar, which means we won't need to use reflection
* to execute the contract's generateInitial() method. * 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.contracts.*
import net.corda.core.identity.AbstractParty 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.core.transactions.TransactionBuilder
import net.corda.nodeapi.DummyContractBackdoor import net.corda.nodeapi.DummyContractBackdoor
const val ANOTHER_DUMMY_PROGRAM_ID = "net.corda.finance.contracts.isolated.AnotherDummyContract"
@Suppress("UNUSED") @Suppress("UNUSED")
class AnotherDummyContract : Contract, DummyContractBackdoor { class AnotherDummyContract : Contract, DummyContractBackdoor {
val magicString = "helloworld" val magicString = "helloworld"
@ -32,4 +30,8 @@ class AnotherDummyContract : Contract, DummyContractBackdoor {
} }
override fun inspectState(state: ContractState): Int = (state as State).magicNumber 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(':client:jfx')
testCompile project(':finance:contracts') testCompile project(':finance:contracts')
testCompile project(':finance:workflows') testCompile project(':finance:workflows')
testCompile project(':finance:isolated')
// sample test schemas // sample test schemas
testCompile project(path: ':finance:contracts', configuration: 'testArtifacts') testCompile project(path: ':finance:contracts', configuration: 'testArtifacts')

View File

@ -1,117 +1,123 @@
package net.corda.node.services package net.corda.node.services
import com.nhaarman.mockito_kotlin.any import co.paralleluniverse.fibers.Suspendable
import com.nhaarman.mockito_kotlin.doReturn import net.corda.core.contracts.ContractState
import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.StateRef
import net.corda.core.CordaRuntimeException import net.corda.core.flows.*
import net.corda.core.contracts.*
import net.corda.core.cordapp.CordappProvider
import net.corda.core.flows.FlowLogic
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.toLedgerTransaction import net.corda.core.internal.*
import net.corda.core.node.NetworkParameters import net.corda.core.internal.concurrent.transpose
import net.corda.core.node.ServicesForResolution import net.corda.core.messaging.startFlow
import net.corda.core.node.services.AttachmentStorage import net.corda.core.serialization.MissingAttachmentsException
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.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.node.VersionInfo import net.corda.core.utilities.unwrap
import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.testing.common.internal.checkNotOnClasspath
import net.corda.node.internal.cordapp.JarScanningCordappLoader import net.corda.testing.core.*
import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.driver.DriverDSL
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.testing.driver.DriverParameters import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.NodeParameters
import net.corda.testing.driver.driver import net.corda.testing.driver.driver
import net.corda.testing.internal.MockCordappConfigProvider import net.corda.testing.node.NotarySpec
import net.corda.testing.internal.rigorousMock
import net.corda.testing.internal.withoutTestSerialization
import net.corda.testing.node.internal.cordappsForPackages import net.corda.testing.node.internal.cordappsForPackages
import net.corda.testing.services.MockAttachmentStorage import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test import org.junit.Test
import java.net.URL
import java.net.URLClassLoader import java.net.URLClassLoader
import kotlin.test.assertFailsWith
class AttachmentLoadingTests { 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 { private companion object {
val isolatedJAR = AttachmentLoadingTests::class.java.getResource("isolated.jar")!! val isolatedJar: URL = AttachmentLoadingTests::class.java.getResource("/isolated.jar")
const val ISOLATED_CONTRACT_ID = "net.corda.finance.contracts.isolated.AnotherDummyContract" val isolatedClassLoader = URLClassLoader(arrayOf(isolatedJar))
val issuanceFlowClass: Class<FlowLogic<StateRef>> = uncheckedCast(loadFromIsolated("net.corda.isolated.workflows.IsolatedIssuanceFlow"))
val bankAName = CordaX500Name("BankA", "Zurich", "CH") init {
val bankBName = CordaX500Name("BankB", "Zurich", "CH") checkNotOnClasspath("net.corda.isolated.contracts.AnotherDummyContract") {
val flowInitiatorClass: Class<out FlowLogic<*>> = "isolated module cannot be on the classpath as otherwise it will be pulled into the nodes the driver creates and " +
Class.forName("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator", true, URLClassLoader(arrayOf(isolatedJAR))) "contaminate the tests. This is a known issue with the driver and we must work around it until it's fixed."
.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()
}
} }
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) session.save(attachment)
attachmentCount.inc() attachmentCount.inc()
log.info("Stored new attachment $id") log.info("Stored new attachment: id=$id uploader=$uploader filename=$filename")
contractClassNames.forEach { contractsCache.invalidate(it) } contractClassNames.forEach { contractsCache.invalidate(it) }
return@withContractsInJar id return@withContractsInJar id
} }

View File

@ -15,10 +15,10 @@ import java.net.URL
class CordappProviderImplTests { class CordappProviderImplTests {
private companion object { 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 // TODO: Cordapp name should differ from the JAR name
const val isolatedCordappName = "isolated" 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 validConfig: Config = ConfigFactory.parseString("key=value")
val stubConfigProvider = object : CordappConfigProvider { val stubConfigProvider = object : CordappConfigProvider {
@ -52,7 +52,7 @@ class CordappProviderImplTests {
@Test @Test
fun `test that we find a cordapp class that is loaded into the store`() { fun `test that we find a cordapp class that is loaded into the store`() {
val provider = newCordappProvider(isolatedJAR) 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 expected = provider.cordapps.first()
val actual = provider.getCordappForClass(className) val actual = provider.getCordappForClass(className)
@ -62,9 +62,9 @@ class CordappProviderImplTests {
} }
@Test @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 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 expected = provider.getAppContext(provider.cordapps.first()).attachmentId
val actual = provider.getContractAttachmentID(className) val actual = provider.getContractAttachmentID(className)

View File

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

Binary file not shown.

View File

@ -115,11 +115,12 @@ class CordaClassResolverTests {
val emptyListClass = listOf<Any>().javaClass val emptyListClass = listOf<Any>().javaClass
val emptySetClass = setOf<Any>().javaClass val emptySetClass = setOf<Any>().javaClass
val emptyMapClass = mapOf<Any, 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 emptyWhitelistContext: CheckpointSerializationContext = CheckpointSerializationContextImpl(this.javaClass.classLoader, EmptyWhitelist, emptyMap(), true, null)
private val allButBlacklistedContext: CheckpointSerializationContext = CheckpointSerializationContextImpl(this.javaClass.classLoader, AllButBlacklisted, emptyMap(), true, null) private val allButBlacklistedContext: CheckpointSerializationContext = CheckpointSerializationContextImpl(this.javaClass.classLoader, AllButBlacklisted, emptyMap(), true, null)
@Test @Test
fun `Annotation on enum works for specialised entries`() { fun `Annotation on enum works for specialised entries`() {
CordaClassResolver(emptyWhitelistContext).getRegistration(Foo.Bar::class.java) CordaClassResolver(emptyWhitelistContext).getRegistration(Foo.Bar::class.java)
@ -212,7 +213,7 @@ class CordaClassResolverTests {
val storage = MockAttachmentStorage() val storage = MockAttachmentStorage()
val attachmentHash = importJar(storage) val attachmentHash = importJar(storage)
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! }) 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) CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
} }
@ -221,7 +222,7 @@ class CordaClassResolverTests {
val storage = MockAttachmentStorage() val storage = MockAttachmentStorage()
val attachmentHash = importJar(storage, "some_uploader") val attachmentHash = importJar(storage, "some_uploader")
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! }) 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) 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' // maintained for backwards compatibility only
include 'finance:contracts' include 'finance:contracts'
include 'finance:workflows' include 'finance:workflows'
include 'finance:isolated' include 'isolated'
include 'core' include 'core'
include 'docs' include 'docs'
include 'node-api' include 'node-api'

View File

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

View File

@ -21,7 +21,9 @@ interface TestCordappInternal : TestCordapp {
fun withOnlyJarContents(): TestCordappInternal fun withOnlyJarContents(): TestCordappInternal
companion object { 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) val nodeSpecificCordappsWithoutMeta = checkNoConflicts(nodeSpecificCordapps)
checkNoConflicts(generalCordapps) 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") val FINANCE_WORKFLOWS_CORDAPP: TestCordappImpl = findCordapp("net.corda.finance.flows")
@JvmField @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()) 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.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme
import net.corda.core.serialization.internal.SerializationEnvironment 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.amqp.AMQPServerSerializationScheme
import net.corda.node.serialization.kryo.KRYO_CHECKPOINT_CONTEXT import net.corda.node.serialization.kryo.KRYO_CHECKPOINT_CONTEXT
import net.corda.node.serialization.kryo.KryoCheckpointSerializer import net.corda.node.serialization.kryo.KryoCheckpointSerializer
import net.corda.serialization.internal.* import net.corda.serialization.internal.*
import net.corda.testing.common.internal.asContextEnv import net.corda.testing.common.internal.asContextEnv
import net.corda.testing.core.SerializationEnvironmentRule
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
val inVMExecutors = ConcurrentHashMap<SerializationEnvironment, 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 { fun createTestSerializationEnv(): SerializationEnvironment {
val factory = SerializationFactoryImpl().apply { val factory = SerializationFactoryImpl().apply {
registerScheme(AMQPClientSerializationScheme(emptyList())) registerScheme(AMQPClientSerializationScheme(emptyList()))