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()))