diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 4ecd10ebf5..fb6bb6dacc 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -158,6 +158,8 @@
+
+
diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryError.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryError.kt
index 9ed781026c..d849f29324 100644
--- a/core/src/main/kotlin/net/corda/core/flows/NotaryError.kt
+++ b/core/src/main/kotlin/net/corda/core/flows/NotaryError.kt
@@ -76,7 +76,7 @@ sealed class NotaryError {
*/
// TODO: include notary timestamp?
@CordaSerializable
- data class StateConsumptionDetails(
+data class StateConsumptionDetails(
val hashOfTransactionId: SecureHash,
val type: ConsumedStateType
) {
diff --git a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt
index 4786d1f998..2fd98ab265 100644
--- a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt
+++ b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt
@@ -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,
- private val otherSide: FlowSession) : FlowLogic() {
+class ResolveTransactionsFlow(txHashesArg: Set, private val otherSide: FlowSession) : FlowLogic() {
// Need it ordered in terms of iteration. Needs to be a variable for the check-pointing logic to work.
private val txHashes = txHashesArg.toList()
diff --git a/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt b/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt
index cefa3da900..38df3ddd62 100644
--- a/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt
+++ b/core/src/main/kotlin/net/corda/core/internal/TransactionUtils.kt
@@ -70,7 +70,7 @@ fun deserialiseComponentGroup(componentGroups: List,
// 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
+ return uncheckedCast(components.originalList)
}
return components.lazyMapped { component, internalIndex ->
diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt
index 155bfd1f55..6371c89275 100644
--- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt
+++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt
@@ -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 get() = inputs.map { it.state.data }
val referenceStates: List 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> = 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>) {
- 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) {
- 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>) {
- 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>, transactionClassLoader: ClassLoader): MutableSet {
- // 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()
-
- 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>): 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>, hashToSignatureConstrainedContracts: MutableSet) {
- 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?.unsigned: ContractAttachment?
- get() {
- return this?.filter { !it.isSigned }?.firstOrNull()
- }
-
- private val Set?.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): Map> {
- val result = mutableMapOf>()
-
- 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 = 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>) {
- // 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()
- statesAndEncumbrance.forEach {
- checkNotary(it.first, indicesAlreadyChecked)
- }
- }
- }
-
- private tailrec fun checkNotary(index: Int, indicesAlreadyChecked: HashSet) {
- 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, 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>) {
- // [Set] of "from" (encumbered states).
- val encumberedSet = mutableSetOf()
- // [Set] of "to" (encumbrance states).
- val encumbranceSet = mutableSetOf()
- // 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> = ltx.inputs.map { it.state }
+ private val allStates: List> = inputStates + ltx.references.map { it.state } + ltx.outputs
+ private val contractAttachmentsByContract: Map> = 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> {
+ val contractClasses = allStates.map { it.contract }.toSet()
+ val result = mutableMapOf>()
+
+ 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, 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>) {
+ // [Set] of "from" (encumbered states).
+ val encumberedSet = mutableSetOf()
+ // [Set] of "to" (encumbrance states).
+ val encumbranceSet = mutableSetOf()
+ // 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>) {
+ // 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()
+ statesAndEncumbrance.forEach {
+ checkNotary(it.first, indicesAlreadyChecked)
+ }
+ }
+ }
+
+ private tailrec fun checkNotary(index: Int, indicesAlreadyChecked: HashSet) {
+ 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) {
+ 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 {
+ // 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()
+
+ 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) {
+ 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 {
+ return try {
+ classLoader.loadClass(className).asSubclass(Contract::class.java)
+ } catch (e: Exception) {
+ throw TransactionVerificationException.ContractCreationError(ltx.id, className, e)
+ }
+ }
+
+ private val Set?.unsigned: ContractAttachment? get() = this?.firstOrNull { !it.isSigned }
+ private val Set?.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.")
diff --git a/core/src/test/kotlin/net/corda/core/transactions/AttachmentsClassLoaderSerializationTests.kt b/core/src/test/kotlin/net/corda/core/transactions/AttachmentsClassLoaderSerializationTests.kt
index c3364eeba3..9bab097e2b 100644
--- a/core/src/test/kotlin/net/corda/core/transactions/AttachmentsClassLoaderSerializationTests.kt
+++ b/core/src/test/kotlin/net/corda/core/transactions/AttachmentsClassLoaderSerializationTests.kt
@@ -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`() {
diff --git a/core/src/test/kotlin/net/corda/core/transactions/AttachmentsClassLoaderTests.kt b/core/src/test/kotlin/net/corda/core/transactions/AttachmentsClassLoaderTests.kt
index 873b05f2ec..81d031689e 100644
--- a/core/src/test/kotlin/net/corda/core/transactions/AttachmentsClassLoaderTests.kt
+++ b/core/src/test/kotlin/net/corda/core/transactions/AttachmentsClassLoaderTests.kt
@@ -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) }
+ }
}
diff --git a/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt b/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt
index 599ad8c825..501ebd61e5 100644
--- a/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt
+++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt
@@ -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]")
}
}
diff --git a/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt b/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt
index 5d25980704..591f456737 100644
--- a/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt
+++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt
@@ -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 { buildTransaction() }
+ assertFailsWith { buildTransaction().verify() }
}
@Test
diff --git a/core/src/test/kotlin/net/corda/nodeapi/DummyContractBackdoor.kt b/core/src/test/kotlin/net/corda/nodeapi/DummyContractBackdoor.kt
index 5d7e069c6f..7bd73fc16b 100644
--- a/core/src/test/kotlin/net/corda/nodeapi/DummyContractBackdoor.kt
+++ b/core/src/test/kotlin/net/corda/nodeapi/DummyContractBackdoor.kt
@@ -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.
diff --git a/core/src/test/resources/isolated.jar b/core/src/test/resources/isolated.jar
new file mode 100644
index 0000000000..47372978cc
Binary files /dev/null and b/core/src/test/resources/isolated.jar differ
diff --git a/core/src/test/resources/net/corda/core/transactions/finance.jar b/core/src/test/resources/net/corda/core/transactions/finance.jar
deleted file mode 100644
index 44daa3b431..0000000000
Binary files a/core/src/test/resources/net/corda/core/transactions/finance.jar and /dev/null differ
diff --git a/core/src/test/resources/net/corda/core/transactions/isolated.jar b/core/src/test/resources/net/corda/core/transactions/old-isolated.jar
similarity index 100%
rename from core/src/test/resources/net/corda/core/transactions/isolated.jar
rename to core/src/test/resources/net/corda/core/transactions/old-isolated.jar
diff --git a/finance/isolated/build.gradle b/finance/isolated/build.gradle
deleted file mode 100644
index 44166a1102..0000000000
--- a/finance/isolated/build.gradle
+++ /dev/null
@@ -1,6 +0,0 @@
-apply plugin: 'kotlin'
-apply plugin: CanonicalizerPlugin
-
-dependencies {
- compileOnly project(':core')
-}
diff --git a/finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/IsolatedDummyFlow.kt b/finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/IsolatedDummyFlow.kt
deleted file mode 100644
index a065b49c43..0000000000
--- a/finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/IsolatedDummyFlow.kt
+++ /dev/null
@@ -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() {
- @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() {
- @Suspendable
- override fun call() {
- val stx = subFlow(ReceiveTransactionFlow(session, checkSufficientSignatures = false))
- stx.verify(serviceHub)
- }
- }
-}
diff --git a/isolated/build.gradle b/isolated/build.gradle
new file mode 100644
index 0000000000..575edbd599
--- /dev/null
+++ b/isolated/build.gradle
@@ -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)"
+ }
+}
diff --git a/finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/AnotherDummyContract.kt b/isolated/src/main/kotlin/net/corda/isolated/contracts/AnotherDummyContract.kt
similarity index 87%
rename from finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/AnotherDummyContract.kt
rename to isolated/src/main/kotlin/net/corda/isolated/contracts/AnotherDummyContract.kt
index d6e85290b8..3d3e3b09f7 100644
--- a/finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/AnotherDummyContract.kt
+++ b/isolated/src/main/kotlin/net/corda/isolated/contracts/AnotherDummyContract.kt
@@ -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"
+ }
}
\ No newline at end of file
diff --git a/isolated/src/main/kotlin/net/corda/isolated/workflows/IsolatedIssuanceFlow.kt b/isolated/src/main/kotlin/net/corda/isolated/workflows/IsolatedIssuanceFlow.kt
new file mode 100644
index 0000000000..cb06e95257
--- /dev/null
+++ b/isolated/src/main/kotlin/net/corda/isolated/workflows/IsolatedIssuanceFlow.kt
@@ -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() {
+ @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(0).ref
+ }
+}
diff --git a/finance/isolated/src/main/kotlin/net/corda/nodeapi/DummyContractBackdoor.kt b/isolated/src/main/kotlin/net/corda/nodeapi/DummyContractBackdoor.kt
similarity index 100%
rename from finance/isolated/src/main/kotlin/net/corda/nodeapi/DummyContractBackdoor.kt
rename to isolated/src/main/kotlin/net/corda/nodeapi/DummyContractBackdoor.kt
diff --git a/node/build.gradle b/node/build.gradle
index 81e1bb81ea..fc3e12c65b 100644
--- a/node/build.gradle
+++ b/node/build.gradle
@@ -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')
diff --git a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt
index 1b90941582..98dfa7bb48 100644
--- a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt
+++ b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt
@@ -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> = uncheckedCast(loadFromIsolated("net.corda.isolated.workflows.IsolatedIssuanceFlow"))
- val bankAName = CordaX500Name("BankA", "Zurich", "CH")
- val bankBName = CordaX500Name("BankB", "Zurich", "CH")
- val flowInitiatorClass: Class> =
- 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): Set> = throw NotImplementedError()
- override fun loadContractAttachment(stateRef: StateRef, interestedContractClassName : ContractClassName?): Attachment = throw NotImplementedError()
- override val identityService = rigorousMock().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().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("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")
}
}
-}
\ No newline at end of file
+
+ @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() {
+ @Suspendable
+ override fun call() {
+ val notary = serviceHub.networkMapCache.notaryIdentities[0]
+ val stateAndRef = serviceHub.toStateAndRef(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().unwrap { require(it == "OK") { "Not OK: $it"} }
+ }
+ }
+
+ @InitiatedBy(ConsumeAndBroadcastFlow::class)
+ class ConsumeAndBroadcastResponderFlow(private val otherSide: FlowSession) : FlowLogic() {
+ @Suspendable
+ override fun call() {
+ try {
+ subFlow(ReceiveFinalityFlow(otherSide))
+ } catch (e: MissingAttachmentsException) {
+ throw FlowException(e.message)
+ }
+ otherSide.send("OK")
+ }
+ }
+}
diff --git a/node/src/integration-test/resources/isolated.jar b/node/src/integration-test/resources/isolated.jar
new file mode 100644
index 0000000000..47372978cc
Binary files /dev/null and b/node/src/integration-test/resources/isolated.jar differ
diff --git a/node/src/integration-test/resources/net/corda/node/services/isolated.jar b/node/src/integration-test/resources/net/corda/node/services/isolated.jar
deleted file mode 100644
index 05544ab868..0000000000
Binary files a/node/src/integration-test/resources/net/corda/node/services/isolated.jar and /dev/null differ
diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt
index 985e7693fc..679e21c501 100644
--- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt
+++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt
@@ -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
}
diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt
index 77d97f5715..cff8ba4bca 100644
--- a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt
+++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt
@@ -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)
diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt
index a2f4121b72..58f36530a8 100644
--- a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt
+++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt
@@ -36,8 +36,8 @@ class DummyRPCFlow : FlowLogic() {
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)
diff --git a/node/src/test/resources/isolated.jar b/node/src/test/resources/isolated.jar
new file mode 100644
index 0000000000..47372978cc
Binary files /dev/null and b/node/src/test/resources/isolated.jar differ
diff --git a/node/src/test/resources/net/corda/node/internal/cordapp/isolated.jar b/node/src/test/resources/net/corda/node/internal/cordapp/isolated.jar
deleted file mode 100644
index 408e70145d..0000000000
Binary files a/node/src/test/resources/net/corda/node/internal/cordapp/isolated.jar and /dev/null differ
diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/CordaClassResolverTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/CordaClassResolverTests.kt
index e42724c66e..3bfed8a53b 100644
--- a/serialization/src/test/kotlin/net/corda/serialization/internal/CordaClassResolverTests.kt
+++ b/serialization/src/test/kotlin/net/corda/serialization/internal/CordaClassResolverTests.kt
@@ -115,11 +115,12 @@ class CordaClassResolverTests {
val emptyListClass = listOf().javaClass
val emptySetClass = setOf().javaClass
val emptyMapClass = mapOf().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)
}
diff --git a/serialization/src/test/resources/isolated.jar b/serialization/src/test/resources/isolated.jar
new file mode 100644
index 0000000000..47372978cc
Binary files /dev/null and b/serialization/src/test/resources/isolated.jar differ
diff --git a/serialization/src/test/resources/net/corda/serialization/internal/isolated.jar b/serialization/src/test/resources/net/corda/serialization/internal/isolated.jar
deleted file mode 100644
index 17bf0c2436..0000000000
Binary files a/serialization/src/test/resources/net/corda/serialization/internal/isolated.jar and /dev/null differ
diff --git a/settings.gradle b/settings.gradle
index ed8e05f0e8..34aa7dbc4e 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -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'
diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/CustomCordapp.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/CustomCordapp.kt
index 9832edbb60..531714dac5 100644
--- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/CustomCordapp.kt
+++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/CustomCordapp.kt
@@ -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()
diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappInternal.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappInternal.kt
index 13d7d94428..9c10b63c0c 100644
--- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappInternal.kt
+++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappInternal.kt
@@ -21,7 +21,9 @@ interface TestCordappInternal : TestCordapp {
fun withOnlyJarContents(): TestCordappInternal
companion object {
- fun installCordapps(baseDirectory: Path, nodeSpecificCordapps: Set, generalCordapps: Set) {
+ fun installCordapps(baseDirectory: Path,
+ nodeSpecificCordapps: Set,
+ generalCordapps: Set = emptySet()) {
val nodeSpecificCordappsWithoutMeta = checkNoConflicts(nodeSpecificCordapps)
checkNoConflicts(generalCordapps)
diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappsUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappsUtils.kt
index 250c38fbaa..258760c2b5 100644
--- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappsUtils.kt
+++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappsUtils.kt
@@ -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 = listOf(FINANCE_CONTRACTS_CORDAPP, FINANCE_WORKFLOWS_CORDAPP)
+val FINANCE_CORDAPPS: Set = setOf(FINANCE_CONTRACTS_CORDAPP, FINANCE_WORKFLOWS_CORDAPP)
fun cordappsForPackages(vararg packageNames: String): Set = cordappsForPackages(packageNames.asList())
diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt
index 5fe08e5f4a..24ecaa2386 100644
--- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt
+++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt
@@ -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()
-/**
- * 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 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()))