mirror of
https://github.com/corda/corda.git
synced 2024-12-19 21:17:58 +00:00
Fixed incorrect attachment loading integration test (#4453)
* Fixed incorrect attachment loading integration test `AttachmentLoadingTests.test that attachments retrieved over the network are not used for code` was a false-positive - it was incorrect on multiple levels. Fixing it required updating the finance:isolated CorDapp, at which point it was given the new MANIFEST metadata for V4, and moved out of the net.corda.finance namespace to avoid package sealing issues. The new test exposed a bug in the LedgerTransaction verification logic. This was cleaned up as it was too easy to verify on the wrong instance.
This commit is contained in:
parent
bbbe08ab1b
commit
b4c3fa1948
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@ -158,6 +158,8 @@
|
||||
<module name="net.corda-blobinspector_test" target="1.8" />
|
||||
<module name="net.corda-finance-contracts-states_main" target="1.8" />
|
||||
<module name="net.corda-finance-contracts-states_test" target="1.8" />
|
||||
<module name="net.corda-isolated_main" target="1.8" />
|
||||
<module name="net.corda-isolated_test" target="1.8" />
|
||||
<module name="net.corda-verifier_main" target="1.8" />
|
||||
<module name="net.corda-verifier_test" target="1.8" />
|
||||
<module name="net.corda_buildSrc_main" target="1.8" />
|
||||
|
@ -76,7 +76,7 @@ sealed class NotaryError {
|
||||
*/
|
||||
// TODO: include notary timestamp?
|
||||
@CordaSerializable
|
||||
data class StateConsumptionDetails(
|
||||
data class StateConsumptionDetails(
|
||||
val hashOfTransactionId: SecureHash,
|
||||
val type: ConsumedStateType
|
||||
) {
|
||||
|
@ -20,12 +20,9 @@ import kotlin.math.min
|
||||
/**
|
||||
* Resolves transactions for the specified [txHashes] along with their full history (dependency graph) from [otherSide].
|
||||
* Each retrieved transaction is validated and inserted into the local transaction storage.
|
||||
*
|
||||
* @return a list of verified [SignedTransaction] objects, in a depth-first order.
|
||||
*/
|
||||
@DeleteForDJVM
|
||||
class ResolveTransactionsFlow(txHashesArg: Set<SecureHash>,
|
||||
private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
class ResolveTransactionsFlow(txHashesArg: Set<SecureHash>, private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
|
||||
// Need it ordered in terms of iteration. Needs to be a variable for the check-pointing logic to work.
|
||||
private val txHashes = txHashesArg.toList()
|
||||
|
@ -70,7 +70,7 @@ fun <T : Any> deserialiseComponentGroup(componentGroups: List<ComponentGroup>,
|
||||
// If the componentGroup is a [LazyMappedList] it means that the original deserialized version is already available.
|
||||
val components = group.components
|
||||
if (!forceDeserialize && components is LazyMappedList<*, OpaqueBytes>) {
|
||||
return components.originalList as List<T>
|
||||
return uncheckedCast(components.originalList)
|
||||
}
|
||||
|
||||
return components.lazyMapped { component, internalIndex ->
|
||||
|
@ -66,8 +66,6 @@ private constructor(
|
||||
checkBaseInvariants()
|
||||
if (timeWindow != null) check(notary != null) { "Transactions with time-windows must be notarised" }
|
||||
checkNotaryWhitelisted()
|
||||
checkNoNotaryChange()
|
||||
checkEncumbrancesValid()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -101,9 +99,6 @@ private constructor(
|
||||
val inputStates: List<ContractState> get() = inputs.map { it.state.data }
|
||||
val referenceStates: List<ContractState> get() = references.map { it.state.data }
|
||||
|
||||
private val inputAndOutputStates = inputs.map { it.state } + outputs
|
||||
private val allStates = inputAndOutputStates + references.map { it.state }
|
||||
|
||||
/**
|
||||
* Returns the typed input StateAndRef at the specified index
|
||||
* @param index The index into the inputs.
|
||||
@ -129,218 +124,12 @@ private constructor(
|
||||
logger.warn("Network parameters on the LedgerTransaction with id: $id are null. Please don't use deprecated constructors of the LedgerTransaction. " +
|
||||
"Use WireTransaction.toLedgerTransaction instead. The result of the verify method might not be accurate.")
|
||||
}
|
||||
val contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>> = getContractAttachmentsByContract(allStates.map { it.contract }.toSet())
|
||||
|
||||
AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(this.attachments) { transactionClassLoader ->
|
||||
|
||||
val internalTx = createLtxForVerification()
|
||||
|
||||
validateContractVersions(contractAttachmentsByContract)
|
||||
validatePackageOwnership(contractAttachmentsByContract)
|
||||
validateStatesAgainstContract(internalTx)
|
||||
val hashToSignatureConstrainedContracts = verifyConstraintsValidity(internalTx, contractAttachmentsByContract, transactionClassLoader)
|
||||
verifyConstraints(internalTx, contractAttachmentsByContract, hashToSignatureConstrainedContracts)
|
||||
verifyContracts(internalTx)
|
||||
Verifier(createLtxForVerification(), transactionClassLoader).verify()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that contract class versions of output states are not lower that versions of relevant input states.
|
||||
*/
|
||||
@Throws(TransactionVerificationException::class)
|
||||
private fun validateContractVersions(contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>) {
|
||||
contractAttachmentsByContract.forEach { contractClassName, attachments ->
|
||||
val outputVersion = attachments.signed?.version ?: attachments.unsigned?.version ?: DEFAULT_CORDAPP_VERSION
|
||||
inputStatesContractClassNameToMaxVersion[contractClassName]?.let {
|
||||
if (it > outputVersion) {
|
||||
throw TransactionVerificationException.TransactionVerificationVersionException(this.id, contractClassName, "$it", "$outputVersion")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For all input and output [TransactionState]s, validates that the wrapped [ContractState] matches up with the
|
||||
* wrapped [Contract], as declared by the [BelongsToContract] annotation on the [ContractState]'s class.
|
||||
*
|
||||
* If the target platform version of the current CorDapp is lower than 4.0, a warning will be written to the log
|
||||
* if any mismatch is detected. If it is 4.0 or later, then [TransactionContractConflictException] will be thrown.
|
||||
*/
|
||||
private fun validateStatesAgainstContract(internalTx: LedgerTransaction) =
|
||||
internalTx.allStates.forEach(::validateStateAgainstContract)
|
||||
|
||||
private fun validateStateAgainstContract(state: TransactionState<ContractState>) {
|
||||
val shouldEnforce = StateContractValidationEnforcementRule.shouldEnforce(state.data)
|
||||
|
||||
val requiredContractClassName = state.data.requiredContractClassName ?:
|
||||
if (shouldEnforce) throw TransactionRequiredContractUnspecifiedException(id, state)
|
||||
else return
|
||||
|
||||
if (state.contract != requiredContractClassName)
|
||||
if (shouldEnforce) {
|
||||
throw TransactionContractConflictException(id, state, requiredContractClassName)
|
||||
} else {
|
||||
logger.warnOnce("""
|
||||
State of class ${state.data::class.java.typeName} belongs to contract $requiredContractClassName, but
|
||||
is bundled in TransactionState with ${state.contract}.
|
||||
|
||||
For details see: https://docs.corda.net/api-contract-constraints.html#contract-state-agreement
|
||||
""".trimIndent().replace('\n', ' '))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that for each contract the network wide package owner is respected.
|
||||
*
|
||||
* TODO - revisit once transaction contains network parameters. - UPDATE: It contains them, but because of the API stability and the fact that
|
||||
* LedgerTransaction was data class i.e. exposed constructors that shouldn't had been exposed, we still need to keep them nullable :/
|
||||
*/
|
||||
private fun validatePackageOwnership(contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>) {
|
||||
val contractsAndOwners = allStates.mapNotNull { transactionState ->
|
||||
val contractClassName = transactionState.contract
|
||||
networkParameters!!.getPackageOwnerOf(contractClassName)?.let { contractClassName to it }
|
||||
}.toMap()
|
||||
|
||||
contractsAndOwners.forEach { contract, owner ->
|
||||
contractAttachmentsByContract[contract]?.filter { it.isSigned }?.forEach { attachment ->
|
||||
if (!owner.isFulfilledBy(attachment.signerKeys))
|
||||
throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(this.id, id, contract)
|
||||
} ?: throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(this.id, id, contract)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforces the validity of the actual constraints.
|
||||
* * Constraints should be one of the valid supported ones.
|
||||
* * Constraints should propagate correctly if not marked otherwise.
|
||||
*
|
||||
* Returns set of contract classes that identify hash -> signature constraint switchover
|
||||
*/
|
||||
private fun verifyConstraintsValidity(internalTx: LedgerTransaction, contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>, transactionClassLoader: ClassLoader): MutableSet<ContractClassName> {
|
||||
// First check that the constraints are valid.
|
||||
for (state in internalTx.allStates) {
|
||||
checkConstraintValidity(state)
|
||||
}
|
||||
|
||||
// Group the inputs and outputs by contract, and for each contract verify the constraints propagation logic.
|
||||
// This is not required for reference states as there is nothing to propagate.
|
||||
val inputContractGroups = internalTx.inputs.groupBy { it.state.contract }
|
||||
val outputContractGroups = internalTx.outputs.groupBy { it.contract }
|
||||
|
||||
// identify any contract classes where input-output pair are transitioning from hash to signature constraints.
|
||||
val hashToSignatureConstrainedContracts = mutableSetOf<ContractClassName>()
|
||||
|
||||
for (contractClassName in (inputContractGroups.keys + outputContractGroups.keys)) {
|
||||
if (contractClassName.contractHasAutomaticConstraintPropagation(transactionClassLoader)) {
|
||||
// Verify that the constraints of output states have at least the same level of restriction as the constraints of the corresponding input states.
|
||||
val inputConstraints = inputContractGroups[contractClassName]?.map { it.state.constraint }?.toSet()
|
||||
val outputConstraints = outputContractGroups[contractClassName]?.map { it.constraint }?.toSet()
|
||||
outputConstraints?.forEach { outputConstraint ->
|
||||
inputConstraints?.forEach { inputConstraint ->
|
||||
val constraintAttachment = resolveAttachment(contractClassName, contractAttachmentsByContract)
|
||||
if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, constraintAttachment))) {
|
||||
throw TransactionVerificationException.ConstraintPropagationRejection(id, contractClassName, inputConstraint, outputConstraint)
|
||||
}
|
||||
// Hash to signature constraints auto-migration
|
||||
if (outputConstraint is SignatureAttachmentConstraint && inputConstraint is HashAttachmentConstraint)
|
||||
hashToSignatureConstrainedContracts.add(contractClassName)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
contractClassName.warnContractWithoutConstraintPropagation()
|
||||
}
|
||||
}
|
||||
return hashToSignatureConstrainedContracts
|
||||
}
|
||||
|
||||
private fun resolveAttachment(contractClassName: ContractClassName, contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>): AttachmentWithContext {
|
||||
val unsignedAttachment = contractAttachmentsByContract[contractClassName]!!.filter { !it.isSigned }.firstOrNull()
|
||||
val signedAttachment = contractAttachmentsByContract[contractClassName]!!.filter { it.isSigned }.firstOrNull()
|
||||
return when {
|
||||
(unsignedAttachment != null && signedAttachment != null) -> AttachmentWithContext(signedAttachment, contractClassName, networkParameters!!)
|
||||
(unsignedAttachment != null) -> AttachmentWithContext(unsignedAttachment, contractClassName, networkParameters!!)
|
||||
(signedAttachment != null) -> AttachmentWithContext(signedAttachment, contractClassName, networkParameters!!)
|
||||
else -> throw TransactionVerificationException.ContractConstraintRejection(id, contractClassName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that all contract constraints are passing before running any contract code.
|
||||
*
|
||||
* This check is running the [AttachmentConstraint.isSatisfiedBy] method for each corresponding [ContractAttachment].
|
||||
*
|
||||
* @throws TransactionVerificationException if the constraints fail to verify
|
||||
*/
|
||||
private fun verifyConstraints(internalTx: LedgerTransaction, contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>, hashToSignatureConstrainedContracts: MutableSet<ContractClassName>) {
|
||||
for (state in internalTx.allStates) {
|
||||
if (state.constraint is SignatureAttachmentConstraint)
|
||||
checkMinimumPlatformVersion(networkParameters!!.minimumPlatformVersion, 4, "Signature constraints")
|
||||
|
||||
val constraintAttachment =
|
||||
// hash to to signature constraint migration logic:
|
||||
// pass the unsigned attachment when verifying the constraint of the input state, and the signed attachment when verifying the constraint of the output state.
|
||||
if (state.contract in hashToSignatureConstrainedContracts) {
|
||||
val unsignedAttachment = contractAttachmentsByContract[state.contract].unsigned
|
||||
?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
|
||||
val signedAttachment = contractAttachmentsByContract[state.contract].signed
|
||||
?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
|
||||
when {
|
||||
// use unsigned attachment if hash-constrained input state
|
||||
state.data in inputStates -> AttachmentWithContext(unsignedAttachment, state.contract, networkParameters!!)
|
||||
// use signed attachment if signature-constrained output state
|
||||
state.data in outputStates -> AttachmentWithContext(signedAttachment, state.contract, networkParameters!!)
|
||||
else -> throw IllegalStateException("${state.contract} must use either signed or unsigned attachment in hash to signature constraints migration")
|
||||
}
|
||||
}
|
||||
// standard processing logic
|
||||
else {
|
||||
val contractAttachment = contractAttachmentsByContract[state.contract]?.firstOrNull()
|
||||
?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
|
||||
AttachmentWithContext(contractAttachment, state.contract, networkParameters!!)
|
||||
}
|
||||
|
||||
if (!state.constraint.isSatisfiedBy(constraintAttachment)) {
|
||||
throw TransactionVerificationException.ContractConstraintRejection(id, state.contract)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val Set<ContractAttachment>?.unsigned: ContractAttachment?
|
||||
get() {
|
||||
return this?.filter { !it.isSigned }?.firstOrNull()
|
||||
}
|
||||
|
||||
private val Set<ContractAttachment>?.signed: ContractAttachment?
|
||||
get() {
|
||||
return this?.filter { it.isSigned }?.firstOrNull()
|
||||
}
|
||||
|
||||
// TODO: revisit to include contract version information
|
||||
/**
|
||||
* This method may return more than one attachment for a given contract class.
|
||||
* Specifically, this is the case for transactions combining hash and signature constraints where the hash constrained contract jar
|
||||
* will be unsigned, and the signature constrained counterpart will be signed.
|
||||
*/
|
||||
private fun getContractAttachmentsByContract(contractClasses: Set<ContractClassName>): Map<ContractClassName, Set<ContractAttachment>> {
|
||||
val result = mutableMapOf<ContractClassName, Set<ContractAttachment>>()
|
||||
|
||||
for (attachment in attachments) {
|
||||
if (attachment !is ContractAttachment) continue
|
||||
for (contract in contractClasses) {
|
||||
if (!attachment.allContracts.contains(contract)) continue
|
||||
result[contract] = result.getOrDefault(contract, setOf(attachment)).plus(attachment)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun contractClassFor(className: ContractClassName, classLoader: ClassLoader): Class<out Contract> = try {
|
||||
classLoader.loadClass(className).asSubclass(Contract::class.java)
|
||||
} catch (e: Exception) {
|
||||
throw TransactionVerificationException.ContractCreationError(id, className, e)
|
||||
}
|
||||
|
||||
private fun createLtxForVerification(): LedgerTransaction {
|
||||
val serializedInputs = this.serializedInputs
|
||||
val serializedReferences = this.serializedReferences
|
||||
@ -368,7 +157,7 @@ private constructor(
|
||||
privacySalt = this.privacySalt,
|
||||
networkParameters = this.networkParameters,
|
||||
references = deserializedReferences,
|
||||
inputStatesContractClassNameToMaxVersion = emptyMap()
|
||||
inputStatesContractClassNameToMaxVersion = this.inputStatesContractClassNameToMaxVersion
|
||||
)
|
||||
} else {
|
||||
// This branch is only present for backwards compatibility.
|
||||
@ -378,156 +167,6 @@ private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the transaction is contract-valid by running the verify() for each input and output state contract.
|
||||
* If any contract fails to verify, the whole transaction is considered to be invalid.
|
||||
*/
|
||||
private fun verifyContracts(internalTx: LedgerTransaction) {
|
||||
val contractClasses = (internalTx.inputs.map { it.state } + internalTx.outputs).toSet()
|
||||
.map { it.contract to contractClassFor(it.contract, it.data.javaClass.classLoader) }
|
||||
|
||||
val contractInstances = contractClasses.map { (contractClassName, contractClass) ->
|
||||
try {
|
||||
contractClass.newInstance()
|
||||
} catch (e: Exception) {
|
||||
throw TransactionVerificationException.ContractCreationError(id, contractClassName, e)
|
||||
}
|
||||
}
|
||||
|
||||
contractInstances.forEach { contract ->
|
||||
try {
|
||||
contract.verify(internalTx)
|
||||
} catch (e: Exception) {
|
||||
throw TransactionVerificationException.ContractRejection(id, contract, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the notary has stayed the same. As we can't tell how inputs and outputs connect, if there
|
||||
* are any inputs or reference inputs, all outputs must have the same notary.
|
||||
*
|
||||
* TODO: Is that the correct set of restrictions? May need to come back to this, see if we can be more
|
||||
* flexible on output notaries.
|
||||
*/
|
||||
private fun checkNoNotaryChange() {
|
||||
if (notary != null && (inputs.isNotEmpty() || references.isNotEmpty())) {
|
||||
outputs.forEach {
|
||||
if (it.notary != notary) {
|
||||
throw TransactionVerificationException.NotaryChangeInWrongTransactionType(id, notary, it.notary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkEncumbrancesValid() {
|
||||
// Validate that all encumbrances exist within the set of input states.
|
||||
inputs.filter { it.state.encumbrance != null }
|
||||
.forEach { (state, ref) -> checkInputEncumbranceStateExists(state, ref) }
|
||||
|
||||
// Check that in the outputs,
|
||||
// a) an encumbered state does not refer to itself as the encumbrance
|
||||
// b) the number of outputs can contain the encumbrance
|
||||
// c) the bi-directionality (full cycle) property is satisfied
|
||||
// d) encumbered output states are assigned to the same notary.
|
||||
val statesAndEncumbrance = outputs.withIndex().filter { it.value.encumbrance != null }
|
||||
.map { Pair(it.index, it.value.encumbrance!!) }
|
||||
if (!statesAndEncumbrance.isEmpty()) {
|
||||
checkBidirectionalOutputEncumbrances(statesAndEncumbrance)
|
||||
checkNotariesOutputEncumbrance(statesAndEncumbrance)
|
||||
}
|
||||
}
|
||||
|
||||
// Method to check if all encumbered states are assigned to the same notary Party.
|
||||
// This method should be invoked after [checkBidirectionalOutputEncumbrances], because it assumes that the
|
||||
// bi-directionality property is already satisfied.
|
||||
private fun checkNotariesOutputEncumbrance(statesAndEncumbrance: List<Pair<Int, Int>>) {
|
||||
// We only check for transactions in which notary is null (i.e., issuing transactions).
|
||||
// Note that if a notary is defined for a transaction, we already check if all outputs are assigned
|
||||
// to the same notary (transaction's notary) in [checkNoNotaryChange()].
|
||||
if (notary == null) {
|
||||
// indicesAlreadyChecked is used to bypass already checked indices and to avoid cycles.
|
||||
val indicesAlreadyChecked = HashSet<Int>()
|
||||
statesAndEncumbrance.forEach {
|
||||
checkNotary(it.first, indicesAlreadyChecked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private tailrec fun checkNotary(index: Int, indicesAlreadyChecked: HashSet<Int>) {
|
||||
if (indicesAlreadyChecked.add(index)) {
|
||||
val encumbranceIndex = outputs[index].encumbrance!!
|
||||
if (outputs[index].notary != outputs[encumbranceIndex].notary) {
|
||||
throw TransactionVerificationException.TransactionNotaryMismatchEncumbranceException(id, index, encumbranceIndex, outputs[index].notary, outputs[encumbranceIndex].notary)
|
||||
} else {
|
||||
checkNotary(encumbranceIndex, indicesAlreadyChecked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkInputEncumbranceStateExists(state: TransactionState<ContractState>, ref: StateRef) {
|
||||
val encumbranceStateExists = inputs.any {
|
||||
it.ref.txhash == ref.txhash && it.ref.index == state.encumbrance
|
||||
}
|
||||
if (!encumbranceStateExists) {
|
||||
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
||||
id,
|
||||
state.encumbrance!!,
|
||||
TransactionVerificationException.Direction.INPUT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Using basic graph theory, a full cycle of encumbered (co-dependent) states should exist to achieve bi-directional
|
||||
// encumbrances. This property is important to ensure that no states involved in an encumbrance-relationship
|
||||
// can be spent on their own. Briefly, if any of the states is having more than one encumbrance references by
|
||||
// other states, a full cycle detection will fail. As a result, all of the encumbered states must be present
|
||||
// as "from" and "to" only once (or zero times if no encumbrance takes place). For instance,
|
||||
// a -> b
|
||||
// c -> b and a -> b
|
||||
// b -> a b -> c
|
||||
// do not satisfy the bi-directionality (full cycle) property.
|
||||
//
|
||||
// In the first example "b" appears twice in encumbrance ("to") list and "c" exists in the encumbered ("from") list only.
|
||||
// Due the above, one could consume "a" and "b" in the same transaction and then, because "b" is already consumed, "c" cannot be spent.
|
||||
//
|
||||
// Similarly, the second example does not form a full cycle because "a" and "c" exist in one of the lists only.
|
||||
// As a result, one can consume "b" and "c" in the same transactions, which will make "a" impossible to be spent.
|
||||
//
|
||||
// On other hand the following are valid constructions:
|
||||
// a -> b a -> c
|
||||
// b -> c and c -> b
|
||||
// c -> a b -> a
|
||||
// and form a full cycle, meaning that the bi-directionality property is satisfied.
|
||||
private fun checkBidirectionalOutputEncumbrances(statesAndEncumbrance: List<Pair<Int, Int>>) {
|
||||
// [Set] of "from" (encumbered states).
|
||||
val encumberedSet = mutableSetOf<Int>()
|
||||
// [Set] of "to" (encumbrance states).
|
||||
val encumbranceSet = mutableSetOf<Int>()
|
||||
// Update both [Set]s.
|
||||
statesAndEncumbrance.forEach { (statePosition, encumbrance) ->
|
||||
// Check it does not refer to itself.
|
||||
if (statePosition == encumbrance || encumbrance >= outputs.size) {
|
||||
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
||||
id,
|
||||
encumbrance,
|
||||
TransactionVerificationException.Direction.OUTPUT)
|
||||
} else {
|
||||
encumberedSet.add(statePosition) // Guaranteed to have unique elements.
|
||||
if (!encumbranceSet.add(encumbrance)) {
|
||||
throw TransactionVerificationException.TransactionDuplicateEncumbranceException(id, encumbrance)
|
||||
}
|
||||
}
|
||||
}
|
||||
// At this stage we have ensured that "from" and "to" [Set]s are equal in size, but we should check their
|
||||
// elements do indeed match. If they don't match, we return their symmetric difference (disjunctive union).
|
||||
val symmetricDifference = (encumberedSet union encumbranceSet).subtract(encumberedSet intersect encumbranceSet)
|
||||
if (symmetricDifference.isNotEmpty()) {
|
||||
// At least one encumbered state is not in the [encumbranceSet] and vice versa.
|
||||
throw TransactionVerificationException.TransactionNonMatchingEncumbranceException(id, symmetricDifference)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a type and a function that returns a grouping key, associates inputs and outputs together so that they
|
||||
* can be processed as one. The grouping key is any arbitrary object that can act as a map key (so must implement
|
||||
@ -869,6 +508,385 @@ private constructor(
|
||||
|)""".trimMargin()
|
||||
}
|
||||
|
||||
/**
|
||||
* Because we create a separate [LedgerTransaction] onto which we need to perform verification, it becomes important we don't verify the
|
||||
* wrong object instance. This class helps avoid that.
|
||||
*/
|
||||
private class Verifier(private val ltx: LedgerTransaction, private val transactionClassLoader: ClassLoader) {
|
||||
private val inputStates: List<TransactionState<*>> = ltx.inputs.map { it.state }
|
||||
private val allStates: List<TransactionState<*>> = inputStates + ltx.references.map { it.state } + ltx.outputs
|
||||
private val contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>> = getContractAttachmentsByContract()
|
||||
|
||||
fun verify() {
|
||||
// checkNoNotaryChange and checkEncumbrancesValid are called here, and not in the c'tor, as they need access to the "outputs"
|
||||
// list, the contents of which need to be deserialized under the correct classloader.
|
||||
checkNoNotaryChange()
|
||||
checkEncumbrancesValid()
|
||||
validateContractVersions()
|
||||
validatePackageOwnership()
|
||||
validateStatesAgainstContract()
|
||||
val hashToSignatureConstrainedContracts = verifyConstraintsValidity()
|
||||
verifyConstraints(hashToSignatureConstrainedContracts)
|
||||
verifyContracts()
|
||||
}
|
||||
|
||||
// TODO: revisit to include contract version information
|
||||
/**
|
||||
* This method may return more than one attachment for a given contract class.
|
||||
* Specifically, this is the case for transactions combining hash and signature constraints where the hash constrained contract jar
|
||||
* will be unsigned, and the signature constrained counterpart will be signed.
|
||||
*/
|
||||
private fun getContractAttachmentsByContract(): Map<ContractClassName, Set<ContractAttachment>> {
|
||||
val contractClasses = allStates.map { it.contract }.toSet()
|
||||
val result = mutableMapOf<ContractClassName, Set<ContractAttachment>>()
|
||||
|
||||
for (attachment in ltx.attachments) {
|
||||
if (attachment !is ContractAttachment) continue
|
||||
for (contract in contractClasses) {
|
||||
if (contract !in attachment.allContracts) continue
|
||||
result[contract] = result.getOrDefault(contract, setOf(attachment)).plus(attachment)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the notary has stayed the same. As we can't tell how inputs and outputs connect, if there
|
||||
* are any inputs or reference inputs, all outputs must have the same notary.
|
||||
*
|
||||
* TODO: Is that the correct set of restrictions? May need to come back to this, see if we can be more
|
||||
* flexible on output notaries.
|
||||
*/
|
||||
private fun checkNoNotaryChange() {
|
||||
if (ltx.notary != null && (ltx.inputs.isNotEmpty() || ltx.references.isNotEmpty())) {
|
||||
ltx.outputs.forEach {
|
||||
if (it.notary != ltx.notary) {
|
||||
throw TransactionVerificationException.NotaryChangeInWrongTransactionType(ltx.id, ltx.notary, it.notary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkEncumbrancesValid() {
|
||||
// Validate that all encumbrances exist within the set of input states.
|
||||
ltx.inputs
|
||||
.filter { it.state.encumbrance != null }
|
||||
.forEach { (state, ref) -> checkInputEncumbranceStateExists(state, ref) }
|
||||
|
||||
// Check that in the outputs,
|
||||
// a) an encumbered state does not refer to itself as the encumbrance
|
||||
// b) the number of outputs can contain the encumbrance
|
||||
// c) the bi-directionality (full cycle) property is satisfied
|
||||
// d) encumbered output states are assigned to the same notary.
|
||||
val statesAndEncumbrance = ltx.outputs
|
||||
.withIndex()
|
||||
.filter { it.value.encumbrance != null }
|
||||
.map { Pair(it.index, it.value.encumbrance!!) }
|
||||
if (!statesAndEncumbrance.isEmpty()) {
|
||||
checkBidirectionalOutputEncumbrances(statesAndEncumbrance)
|
||||
checkNotariesOutputEncumbrance(statesAndEncumbrance)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkInputEncumbranceStateExists(state: TransactionState<ContractState>, ref: StateRef) {
|
||||
val encumbranceStateExists = ltx.inputs.any {
|
||||
it.ref.txhash == ref.txhash && it.ref.index == state.encumbrance
|
||||
}
|
||||
if (!encumbranceStateExists) {
|
||||
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
||||
ltx.id,
|
||||
state.encumbrance!!,
|
||||
TransactionVerificationException.Direction.INPUT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Using basic graph theory, a full cycle of encumbered (co-dependent) states should exist to achieve bi-directional
|
||||
// encumbrances. This property is important to ensure that no states involved in an encumbrance-relationship
|
||||
// can be spent on their own. Briefly, if any of the states is having more than one encumbrance references by
|
||||
// other states, a full cycle detection will fail. As a result, all of the encumbered states must be present
|
||||
// as "from" and "to" only once (or zero times if no encumbrance takes place). For instance,
|
||||
// a -> b
|
||||
// c -> b and a -> b
|
||||
// b -> a b -> c
|
||||
// do not satisfy the bi-directionality (full cycle) property.
|
||||
//
|
||||
// In the first example "b" appears twice in encumbrance ("to") list and "c" exists in the encumbered ("from") list only.
|
||||
// Due the above, one could consume "a" and "b" in the same transaction and then, because "b" is already consumed, "c" cannot be spent.
|
||||
//
|
||||
// Similarly, the second example does not form a full cycle because "a" and "c" exist in one of the lists only.
|
||||
// As a result, one can consume "b" and "c" in the same transactions, which will make "a" impossible to be spent.
|
||||
//
|
||||
// On other hand the following are valid constructions:
|
||||
// a -> b a -> c
|
||||
// b -> c and c -> b
|
||||
// c -> a b -> a
|
||||
// and form a full cycle, meaning that the bi-directionality property is satisfied.
|
||||
private fun checkBidirectionalOutputEncumbrances(statesAndEncumbrance: List<Pair<Int, Int>>) {
|
||||
// [Set] of "from" (encumbered states).
|
||||
val encumberedSet = mutableSetOf<Int>()
|
||||
// [Set] of "to" (encumbrance states).
|
||||
val encumbranceSet = mutableSetOf<Int>()
|
||||
// Update both [Set]s.
|
||||
statesAndEncumbrance.forEach { (statePosition, encumbrance) ->
|
||||
// Check it does not refer to itself.
|
||||
if (statePosition == encumbrance || encumbrance >= ltx.outputs.size) {
|
||||
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
||||
ltx.id,
|
||||
encumbrance,
|
||||
TransactionVerificationException.Direction.OUTPUT
|
||||
)
|
||||
} else {
|
||||
encumberedSet.add(statePosition) // Guaranteed to have unique elements.
|
||||
if (!encumbranceSet.add(encumbrance)) {
|
||||
throw TransactionVerificationException.TransactionDuplicateEncumbranceException(ltx.id, encumbrance)
|
||||
}
|
||||
}
|
||||
}
|
||||
// At this stage we have ensured that "from" and "to" [Set]s are equal in size, but we should check their
|
||||
// elements do indeed match. If they don't match, we return their symmetric difference (disjunctive union).
|
||||
val symmetricDifference = (encumberedSet union encumbranceSet).subtract(encumberedSet intersect encumbranceSet)
|
||||
if (symmetricDifference.isNotEmpty()) {
|
||||
// At least one encumbered state is not in the [encumbranceSet] and vice versa.
|
||||
throw TransactionVerificationException.TransactionNonMatchingEncumbranceException(ltx.id, symmetricDifference)
|
||||
}
|
||||
}
|
||||
|
||||
// Method to check if all encumbered states are assigned to the same notary Party.
|
||||
// This method should be invoked after [checkBidirectionalOutputEncumbrances], because it assumes that the
|
||||
// bi-directionality property is already satisfied.
|
||||
private fun checkNotariesOutputEncumbrance(statesAndEncumbrance: List<Pair<Int, Int>>) {
|
||||
// We only check for transactions in which notary is null (i.e., issuing transactions).
|
||||
// Note that if a notary is defined for a transaction, we already check if all outputs are assigned
|
||||
// to the same notary (transaction's notary) in [checkNoNotaryChange()].
|
||||
if (ltx.notary == null) {
|
||||
// indicesAlreadyChecked is used to bypass already checked indices and to avoid cycles.
|
||||
val indicesAlreadyChecked = HashSet<Int>()
|
||||
statesAndEncumbrance.forEach {
|
||||
checkNotary(it.first, indicesAlreadyChecked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private tailrec fun checkNotary(index: Int, indicesAlreadyChecked: HashSet<Int>) {
|
||||
if (indicesAlreadyChecked.add(index)) {
|
||||
val encumbranceIndex = ltx.outputs[index].encumbrance!!
|
||||
if (ltx.outputs[index].notary != ltx.outputs[encumbranceIndex].notary) {
|
||||
throw TransactionVerificationException.TransactionNotaryMismatchEncumbranceException(
|
||||
ltx.id,
|
||||
index,
|
||||
encumbranceIndex,
|
||||
ltx.outputs[index].notary,
|
||||
ltx.outputs[encumbranceIndex].notary
|
||||
)
|
||||
} else {
|
||||
checkNotary(encumbranceIndex, indicesAlreadyChecked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that contract class versions of output states are not lower that versions of relevant input states.
|
||||
*/
|
||||
private fun validateContractVersions() {
|
||||
contractAttachmentsByContract.forEach { contractClassName, attachments ->
|
||||
val outputVersion = attachments.signed?.version ?: attachments.unsigned?.version ?: DEFAULT_CORDAPP_VERSION
|
||||
ltx.inputStatesContractClassNameToMaxVersion[contractClassName]?.let {
|
||||
if (it > outputVersion) {
|
||||
throw TransactionVerificationException.TransactionVerificationVersionException(ltx.id, contractClassName, "$it", "$outputVersion")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that for each contract the network wide package owner is respected.
|
||||
*
|
||||
* TODO - revisit once transaction contains network parameters. - UPDATE: It contains them, but because of the API stability and the fact that
|
||||
* LedgerTransaction was data class i.e. exposed constructors that shouldn't had been exposed, we still need to keep them nullable :/
|
||||
*/
|
||||
private fun validatePackageOwnership() {
|
||||
val contractsAndOwners = allStates.mapNotNull { transactionState ->
|
||||
val contractClassName = transactionState.contract
|
||||
ltx.networkParameters!!.getPackageOwnerOf(contractClassName)?.let { contractClassName to it }
|
||||
}.toMap()
|
||||
|
||||
contractsAndOwners.forEach { contract, owner ->
|
||||
contractAttachmentsByContract[contract]?.filter { it.isSigned }?.forEach { attachment ->
|
||||
if (!owner.isFulfilledBy(attachment.signerKeys))
|
||||
throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(ltx.id, attachment.id, contract)
|
||||
} ?: throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(ltx.id, ltx.id, contract)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For all input and output [TransactionState]s, validates that the wrapped [ContractState] matches up with the
|
||||
* wrapped [Contract], as declared by the [BelongsToContract] annotation on the [ContractState]'s class.
|
||||
*
|
||||
* If the target platform version of the current CorDapp is lower than 4.0, a warning will be written to the log
|
||||
* if any mismatch is detected. If it is 4.0 or later, then [TransactionContractConflictException] will be thrown.
|
||||
*/
|
||||
private fun validateStatesAgainstContract() = allStates.forEach(::validateStateAgainstContract)
|
||||
|
||||
private fun validateStateAgainstContract(state: TransactionState<ContractState>) {
|
||||
val shouldEnforce = StateContractValidationEnforcementRule.shouldEnforce(state.data)
|
||||
|
||||
val requiredContractClassName = state.data.requiredContractClassName
|
||||
?: if (shouldEnforce) throw TransactionRequiredContractUnspecifiedException(ltx.id, state) else return
|
||||
|
||||
if (state.contract != requiredContractClassName)
|
||||
if (shouldEnforce) {
|
||||
throw TransactionContractConflictException(ltx.id, state, requiredContractClassName)
|
||||
} else {
|
||||
logger.warnOnce("""
|
||||
State of class ${state.data::class.java.typeName} belongs to contract $requiredContractClassName, but
|
||||
is bundled in TransactionState with ${state.contract}.
|
||||
|
||||
For details see: https://docs.corda.net/api-contract-constraints.html#contract-state-agreement
|
||||
""".trimIndent().replace('\n', ' '))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforces the validity of the actual constraints.
|
||||
* * Constraints should be one of the valid supported ones.
|
||||
* * Constraints should propagate correctly if not marked otherwise.
|
||||
*
|
||||
* Returns set of contract classes that identify hash -> signature constraint switchover
|
||||
*/
|
||||
private fun verifyConstraintsValidity(): MutableSet<ContractClassName> {
|
||||
// First check that the constraints are valid.
|
||||
for (state in allStates) {
|
||||
checkConstraintValidity(state)
|
||||
}
|
||||
|
||||
// Group the inputs and outputs by contract, and for each contract verify the constraints propagation logic.
|
||||
// This is not required for reference states as there is nothing to propagate.
|
||||
val inputContractGroups = ltx.inputs.groupBy { it.state.contract }
|
||||
val outputContractGroups = ltx.outputs.groupBy { it.contract }
|
||||
|
||||
// identify any contract classes where input-output pair are transitioning from hash to signature constraints.
|
||||
val hashToSignatureConstrainedContracts = mutableSetOf<ContractClassName>()
|
||||
|
||||
for (contractClassName in (inputContractGroups.keys + outputContractGroups.keys)) {
|
||||
if (contractClassName.contractHasAutomaticConstraintPropagation(transactionClassLoader)) {
|
||||
// Verify that the constraints of output states have at least the same level of restriction as the constraints of the
|
||||
// corresponding input states.
|
||||
val inputConstraints = inputContractGroups[contractClassName]?.map { it.state.constraint }?.toSet()
|
||||
val outputConstraints = outputContractGroups[contractClassName]?.map { it.constraint }?.toSet()
|
||||
outputConstraints?.forEach { outputConstraint ->
|
||||
inputConstraints?.forEach { inputConstraint ->
|
||||
val constraintAttachment = resolveAttachment(contractClassName)
|
||||
if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, constraintAttachment))) {
|
||||
throw TransactionVerificationException.ConstraintPropagationRejection(
|
||||
ltx.id,
|
||||
contractClassName,
|
||||
inputConstraint,
|
||||
outputConstraint
|
||||
)
|
||||
}
|
||||
// Hash to signature constraints auto-migration
|
||||
if (outputConstraint is SignatureAttachmentConstraint && inputConstraint is HashAttachmentConstraint)
|
||||
hashToSignatureConstrainedContracts.add(contractClassName)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
contractClassName.warnContractWithoutConstraintPropagation()
|
||||
}
|
||||
}
|
||||
return hashToSignatureConstrainedContracts
|
||||
}
|
||||
|
||||
private fun resolveAttachment(contractClassName: ContractClassName): AttachmentWithContext {
|
||||
val unsignedAttachment = contractAttachmentsByContract[contractClassName]!!.firstOrNull { !it.isSigned }
|
||||
val signedAttachment = contractAttachmentsByContract[contractClassName]!!.firstOrNull { it.isSigned }
|
||||
return when {
|
||||
(unsignedAttachment != null && signedAttachment != null) -> AttachmentWithContext(signedAttachment, contractClassName, ltx.networkParameters!!)
|
||||
(unsignedAttachment != null) -> AttachmentWithContext(unsignedAttachment, contractClassName, ltx.networkParameters!!)
|
||||
(signedAttachment != null) -> AttachmentWithContext(signedAttachment, contractClassName, ltx.networkParameters!!)
|
||||
else -> throw TransactionVerificationException.ContractConstraintRejection(ltx.id, contractClassName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that all contract constraints are passing before running any contract code.
|
||||
*
|
||||
* This check is running the [AttachmentConstraint.isSatisfiedBy] method for each corresponding [ContractAttachment].
|
||||
*
|
||||
* @throws TransactionVerificationException if the constraints fail to verify
|
||||
*/
|
||||
private fun verifyConstraints(hashToSignatureConstrainedContracts: MutableSet<ContractClassName>) {
|
||||
for (state in allStates) {
|
||||
if (state.constraint is SignatureAttachmentConstraint) {
|
||||
checkMinimumPlatformVersion(ltx.networkParameters!!.minimumPlatformVersion, 4, "Signature constraints")
|
||||
}
|
||||
|
||||
val constraintAttachment = if (state.contract in hashToSignatureConstrainedContracts) {
|
||||
// hash to to signature constraint migration logic:
|
||||
// pass the unsigned attachment when verifying the constraint of the input state, and the signed attachment when verifying
|
||||
// the constraint of the output state.
|
||||
val unsignedAttachment = contractAttachmentsByContract[state.contract].unsigned
|
||||
?: throw TransactionVerificationException.MissingAttachmentRejection(ltx.id, state.contract)
|
||||
val signedAttachment = contractAttachmentsByContract[state.contract].signed
|
||||
?: throw TransactionVerificationException.MissingAttachmentRejection(ltx.id, state.contract)
|
||||
when {
|
||||
// use unsigned attachment if hash-constrained input state
|
||||
state.data in ltx.inputStates -> AttachmentWithContext(unsignedAttachment, state.contract, ltx.networkParameters!!)
|
||||
// use signed attachment if signature-constrained output state
|
||||
state.data in ltx.outputStates -> AttachmentWithContext(signedAttachment, state.contract, ltx.networkParameters!!)
|
||||
else -> throw IllegalStateException("${state.contract} must use either signed or unsigned attachment in hash to signature constraints migration")
|
||||
}
|
||||
} else {
|
||||
// standard processing logic
|
||||
val contractAttachment = contractAttachmentsByContract[state.contract]?.firstOrNull()
|
||||
?: throw TransactionVerificationException.MissingAttachmentRejection(ltx.id, state.contract)
|
||||
AttachmentWithContext(contractAttachment, state.contract, ltx.networkParameters!!)
|
||||
}
|
||||
|
||||
if (!state.constraint.isSatisfiedBy(constraintAttachment)) {
|
||||
throw TransactionVerificationException.ContractConstraintRejection(ltx.id, state.contract)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the transaction is contract-valid by running the verify() for each input and output state contract.
|
||||
* If any contract fails to verify, the whole transaction is considered to be invalid.
|
||||
*/
|
||||
private fun verifyContracts() {
|
||||
val contractClasses = (inputStates + ltx.outputs).toSet()
|
||||
.map { it.contract to contractClassFor(it.contract, it.data.javaClass.classLoader) }
|
||||
|
||||
val contractInstances = contractClasses.map { (contractClassName, contractClass) ->
|
||||
try {
|
||||
contractClass.newInstance()
|
||||
} catch (e: Exception) {
|
||||
throw TransactionVerificationException.ContractCreationError(ltx.id, contractClassName, e)
|
||||
}
|
||||
}
|
||||
|
||||
contractInstances.forEach { contract ->
|
||||
try {
|
||||
contract.verify(ltx)
|
||||
} catch (e: Exception) {
|
||||
throw TransactionVerificationException.ContractRejection(ltx.id, contract, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun contractClassFor(className: ContractClassName, classLoader: ClassLoader): Class<out Contract> {
|
||||
return try {
|
||||
classLoader.loadClass(className).asSubclass(Contract::class.java)
|
||||
} catch (e: Exception) {
|
||||
throw TransactionVerificationException.ContractCreationError(ltx.id, className, e)
|
||||
}
|
||||
}
|
||||
|
||||
private val Set<ContractAttachment>?.unsigned: ContractAttachment? get() = this?.firstOrNull { !it.isSigned }
|
||||
private val Set<ContractAttachment>?.signed: ContractAttachment? get() = this?.firstOrNull { it.isSigned }
|
||||
}
|
||||
|
||||
// Stuff that we can't remove and so is deprecated instead
|
||||
//
|
||||
@Deprecated("LedgerTransaction should not be created directly, use WireTransaction.toLedgerTransaction instead.")
|
||||
|
@ -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`() {
|
||||
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
@ -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]")
|
||||
}
|
||||
}
|
||||
|
@ -170,6 +170,7 @@ class TransactionTests {
|
||||
val id = SecureHash.randomSHA256()
|
||||
val timeWindow: TimeWindow? = null
|
||||
val privacySalt = PrivacySalt()
|
||||
|
||||
fun buildTransaction() = LedgerTransaction.create(
|
||||
inputs,
|
||||
outputs,
|
||||
@ -184,7 +185,7 @@ class TransactionTests {
|
||||
inputStatesContractClassNameToMaxVersion = emptyMap()
|
||||
)
|
||||
|
||||
assertFailsWith<TransactionVerificationException.NotaryChangeInWrongTransactionType> { buildTransaction() }
|
||||
assertFailsWith<TransactionVerificationException.NotaryChangeInWrongTransactionType> { buildTransaction().verify() }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -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.
|
||||
|
BIN
core/src/test/resources/isolated.jar
Normal file
BIN
core/src/test/resources/isolated.jar
Normal file
Binary file not shown.
Binary file not shown.
@ -1,6 +0,0 @@
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: CanonicalizerPlugin
|
||||
|
||||
dependencies {
|
||||
compileOnly project(':core')
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
package net.corda.finance.contracts.isolated
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
|
||||
/**
|
||||
* Just sends a dummy state to the other side: used for testing whether attachments with code in them are being
|
||||
* loaded or blocked.
|
||||
*/
|
||||
class IsolatedDummyFlow {
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class Initiator(val toWhom: Party) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val tx = AnotherDummyContract().generateInitial(
|
||||
serviceHub.myInfo.legalIdentities.first().ref(0),
|
||||
1234,
|
||||
serviceHub.networkMapCache.notaryIdentities.first()
|
||||
)
|
||||
val stx = serviceHub.signInitialTransaction(tx)
|
||||
subFlow(SendTransactionFlow(initiateFlow(toWhom), stx))
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(Initiator::class)
|
||||
class Acceptor(val session: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val stx = subFlow(ReceiveTransactionFlow(session, checkSufficientSignatures = false))
|
||||
stx.verify(serviceHub)
|
||||
}
|
||||
}
|
||||
}
|
26
isolated/build.gradle
Normal file
26
isolated/build.gradle
Normal file
@ -0,0 +1,26 @@
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: CanonicalizerPlugin
|
||||
apply plugin: 'net.corda.plugins.cordapp'
|
||||
|
||||
description 'Isolated CorDapp for testing'
|
||||
|
||||
dependencies {
|
||||
cordaCompile project(':core')
|
||||
}
|
||||
|
||||
cordapp {
|
||||
targetPlatformVersion corda_platform_version.toInteger()
|
||||
minimumPlatformVersion 1
|
||||
contract {
|
||||
name "Isolated Test CorDapp"
|
||||
versionId 1
|
||||
vendor "R3"
|
||||
licence "Open Source (Apache 2)"
|
||||
}
|
||||
workflow {
|
||||
name "Isolated Test CorDapp"
|
||||
versionId 1
|
||||
vendor "R3"
|
||||
licence "Open Source (Apache 2)"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package net.corda.isolated.workflows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.isolated.contracts.AnotherDummyContract
|
||||
|
||||
@StartableByRPC
|
||||
class IsolatedIssuanceFlow(private val magicNumber: Int) : FlowLogic<StateRef>() {
|
||||
@Suspendable
|
||||
override fun call(): StateRef {
|
||||
val stx = serviceHub.signInitialTransaction(
|
||||
AnotherDummyContract().generateInitial(
|
||||
ourIdentity.ref(0),
|
||||
magicNumber,
|
||||
serviceHub.networkMapCache.notaryIdentities.first()
|
||||
)
|
||||
)
|
||||
stx.verify(serviceHub)
|
||||
serviceHub.recordTransactions(stx)
|
||||
return stx.tx.outRef<ContractState>(0).ref
|
||||
}
|
||||
}
|
@ -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')
|
||||
|
@ -1,117 +1,123 @@
|
||||
package net.corda.node.services
|
||||
|
||||
import com.nhaarman.mockito_kotlin.any
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.CordaRuntimeException
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.cordapp.CordappProvider
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.toLedgerTransaction
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.node.services.AttachmentStorage
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.node.services.NetworkParametersStorage
|
||||
import net.corda.core.serialization.SerializationFactory
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.concurrent.transpose
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.serialization.MissingAttachmentsException
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.VersionInfo
|
||||
import net.corda.node.internal.cordapp.CordappProviderImpl
|
||||
import net.corda.node.internal.cordapp.JarScanningCordappLoader
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.common.internal.addNotary
|
||||
import net.corda.testing.core.DUMMY_BANK_A_NAME
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.core.TestIdentity
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.testing.common.internal.checkNotOnClasspath
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.driver.DriverDSL
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.NodeParameters
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.internal.MockCordappConfigProvider
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.internal.withoutTestSerialization
|
||||
import net.corda.testing.node.NotarySpec
|
||||
import net.corda.testing.node.internal.cordappsForPackages
|
||||
import net.corda.testing.services.MockAttachmentStorage
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Test
|
||||
import java.net.URL
|
||||
import java.net.URLClassLoader
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class AttachmentLoadingTests {
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
private val attachments = MockAttachmentStorage()
|
||||
private val provider = CordappProviderImpl(JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR), VersionInfo.UNKNOWN), MockCordappConfigProvider(), attachments).apply {
|
||||
start(testNetworkParameters().whitelistedContractImplementations)
|
||||
}
|
||||
private val cordapp get() = provider.cordapps.first()
|
||||
private val attachmentId get() = provider.getCordappAttachmentId(cordapp)!!
|
||||
private val appContext get() = provider.getAppContext(cordapp)
|
||||
|
||||
private companion object {
|
||||
val isolatedJAR = AttachmentLoadingTests::class.java.getResource("isolated.jar")!!
|
||||
const val ISOLATED_CONTRACT_ID = "net.corda.finance.contracts.isolated.AnotherDummyContract"
|
||||
val isolatedJar: URL = AttachmentLoadingTests::class.java.getResource("/isolated.jar")
|
||||
val isolatedClassLoader = URLClassLoader(arrayOf(isolatedJar))
|
||||
val issuanceFlowClass: Class<FlowLogic<StateRef>> = uncheckedCast(loadFromIsolated("net.corda.isolated.workflows.IsolatedIssuanceFlow"))
|
||||
|
||||
val bankAName = CordaX500Name("BankA", "Zurich", "CH")
|
||||
val bankBName = CordaX500Name("BankB", "Zurich", "CH")
|
||||
val flowInitiatorClass: Class<out FlowLogic<*>> =
|
||||
Class.forName("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator", true, URLClassLoader(arrayOf(isolatedJAR)))
|
||||
.asSubclass(FlowLogic::class.java)
|
||||
val DUMMY_BANK_A = TestIdentity(DUMMY_BANK_A_NAME, 40).party
|
||||
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
||||
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."
|
||||
}
|
||||
}
|
||||
|
||||
private val services = object : ServicesForResolution {
|
||||
private val testNetworkParameters = testNetworkParameters().addNotary(DUMMY_NOTARY)
|
||||
override fun loadState(stateRef: StateRef): TransactionState<*> = throw NotImplementedError()
|
||||
override fun loadStates(stateRefs: Set<StateRef>): Set<StateAndRef<ContractState>> = throw NotImplementedError()
|
||||
override fun loadContractAttachment(stateRef: StateRef, interestedContractClassName : ContractClassName?): Attachment = throw NotImplementedError()
|
||||
override val identityService = rigorousMock<IdentityService>().apply {
|
||||
doReturn(null).whenever(this).partyFromKey(DUMMY_BANK_A.owningKey)
|
||||
fun loadFromIsolated(className: String): Class<*> = Class.forName(className, false, isolatedClassLoader)
|
||||
}
|
||||
override val attachments: AttachmentStorage get() = this@AttachmentLoadingTests.attachments
|
||||
override val cordappProvider: CordappProvider get() = this@AttachmentLoadingTests.provider
|
||||
override val networkParameters: NetworkParameters = testNetworkParameters
|
||||
override val networkParametersStorage: NetworkParametersStorage get() = rigorousMock<NetworkParametersStorage>().apply {
|
||||
doReturn(testNetworkParameters.serialize().hash).whenever(this).currentHash
|
||||
doReturn(testNetworkParameters).whenever(this).lookup(any())
|
||||
|
||||
@Test
|
||||
fun `contracts downloaded from the network are not executed without the DJVM`() {
|
||||
driver(DriverParameters(
|
||||
startNodesInProcess = false,
|
||||
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = false)),
|
||||
cordappsForAllNodes = cordappsForPackages(javaClass.packageName)
|
||||
)) {
|
||||
installIsolatedCordapp(ALICE_NAME)
|
||||
|
||||
val (alice, bob) = listOf(
|
||||
startNode(providedName = ALICE_NAME),
|
||||
startNode(providedName = BOB_NAME)
|
||||
).transpose().getOrThrow()
|
||||
|
||||
val stateRef = alice.rpc.startFlowDynamic(issuanceFlowClass, 1234).returnValue.getOrThrow()
|
||||
|
||||
// The exception that we actually want is MissingAttachmentsException, but this is thrown in a responder flow on Bob. To work
|
||||
// around that it's re-thrown as a FlowException so that it can be propagated to Alice where we pick it here.
|
||||
assertThatThrownBy {
|
||||
alice.rpc.startFlow(::ConsumeAndBroadcastFlow, stateRef, bob.nodeInfo.singleIdentity()).returnValue.getOrThrow()
|
||||
}.hasMessage("Attempting to load Contract Attachments downloaded from the network")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `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)
|
||||
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 actual = ledgerTx.attachments.first()
|
||||
val expected = attachments.openAttachment(attachmentId)!!
|
||||
assertEquals(expected, actual)
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test that attachments retrieved over the network are not used for code`() {
|
||||
withoutTestSerialization {
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptySet())) {
|
||||
val additionalCordapps = cordappsForPackages("net.corda.finance.contracts.isolated")
|
||||
val bankA = startNode(NodeParameters(providedName = bankAName, additionalCordapps = additionalCordapps)).getOrThrow()
|
||||
val bankB = startNode(NodeParameters(providedName = bankBName, additionalCordapps = additionalCordapps)).getOrThrow()
|
||||
assertFailsWith<CordaRuntimeException>("Party C=CH,L=Zurich,O=BankB rejected session request: Don't know net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator") {
|
||||
bankA.rpc.startFlowDynamic(flowInitiatorClass, bankB.nodeInfo.legalIdentities.first()).returnValue.getOrThrow()
|
||||
private fun DriverDSL.installIsolatedCordapp(name: CordaX500Name) {
|
||||
val cordappsDir = (baseDirectory(name) / "cordapps").createDirectories()
|
||||
isolatedJar.toPath().copyToDirectory(cordappsDir)
|
||||
}
|
||||
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class ConsumeAndBroadcastFlow(private val stateRef: StateRef, private val otherSide: Party) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val notary = serviceHub.networkMapCache.notaryIdentities[0]
|
||||
val stateAndRef = serviceHub.toStateAndRef<ContractState>(stateRef)
|
||||
val stx = serviceHub.signInitialTransaction(
|
||||
TransactionBuilder(notary).addInputState(stateAndRef).addCommand(dummyCommand(ourIdentity.owningKey))
|
||||
)
|
||||
stx.verify(serviceHub, checkSufficientSignatures = false)
|
||||
val session = initiateFlow(otherSide)
|
||||
subFlow(FinalityFlow(stx, session))
|
||||
// It's important we wait on this dummy receive, as otherwise it's possible we miss any errors the other side throws
|
||||
session.receive<String>().unwrap { require(it == "OK") { "Not OK: $it"} }
|
||||
}
|
||||
}
|
||||
Unit
|
||||
|
||||
@InitiatedBy(ConsumeAndBroadcastFlow::class)
|
||||
class ConsumeAndBroadcastResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
try {
|
||||
subFlow(ReceiveFinalityFlow(otherSide))
|
||||
} catch (e: MissingAttachmentsException) {
|
||||
throw FlowException(e.message)
|
||||
}
|
||||
otherSide.send("OK")
|
||||
}
|
||||
}
|
||||
}
|
BIN
node/src/integration-test/resources/isolated.jar
Normal file
BIN
node/src/integration-test/resources/isolated.jar
Normal file
Binary file not shown.
Binary file not shown.
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -36,8 +36,8 @@ class DummyRPCFlow : FlowLogic<Unit>() {
|
||||
|
||||
class JarScanningCordappLoaderTest {
|
||||
private companion object {
|
||||
const val isolatedContractId = "net.corda.finance.contracts.isolated.AnotherDummyContract"
|
||||
const val isolatedFlowName = "net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator"
|
||||
const val isolatedContractId = "net.corda.isolated.contracts.AnotherDummyContract"
|
||||
const val isolatedFlowName = "net.corda.isolated.workflows.IsolatedIssuanceFlow"
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -49,15 +49,15 @@ class JarScanningCordappLoaderTest {
|
||||
|
||||
@Test
|
||||
fun `isolated JAR contains a CorDapp with a contract and plugin`() {
|
||||
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("isolated.jar")!!
|
||||
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("/isolated.jar")
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR))
|
||||
|
||||
assertThat(loader.cordapps).hasSize(1)
|
||||
|
||||
val actualCordapp = loader.cordapps.single()
|
||||
assertThat(actualCordapp.contractClassNames).isEqualTo(listOf(isolatedContractId))
|
||||
assertThat(actualCordapp.initiatedFlows.first().name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Acceptor")
|
||||
assertThat(actualCordapp.rpcFlows).isEmpty()
|
||||
assertThat(actualCordapp.initiatedFlows).isEmpty()
|
||||
assertThat(actualCordapp.rpcFlows.first().name).isEqualTo(isolatedFlowName)
|
||||
assertThat(actualCordapp.schedulableFlows).isEmpty()
|
||||
assertThat(actualCordapp.services).isEmpty()
|
||||
assertThat(actualCordapp.serializationWhitelists).hasSize(1)
|
||||
@ -83,7 +83,7 @@ class JarScanningCordappLoaderTest {
|
||||
// being used internally. Later iterations will use a classloader per cordapp and this test can be retired.
|
||||
@Test
|
||||
fun `cordapp classloader can load cordapp classes`() {
|
||||
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("isolated.jar")!!
|
||||
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("/isolated.jar")
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR), VersionInfo.UNKNOWN)
|
||||
|
||||
loader.appClassLoader.loadClass(isolatedContractId)
|
||||
|
BIN
node/src/test/resources/isolated.jar
Normal file
BIN
node/src/test/resources/isolated.jar
Normal file
Binary file not shown.
Binary file not shown.
@ -115,11 +115,12 @@ class CordaClassResolverTests {
|
||||
val emptyListClass = listOf<Any>().javaClass
|
||||
val emptySetClass = setOf<Any>().javaClass
|
||||
val emptyMapClass = mapOf<Any, Any>().javaClass
|
||||
val ISOLATED_CONTRACTS_JAR_PATH: URL = CordaClassResolverTests::class.java.getResource("isolated.jar")
|
||||
val ISOLATED_CONTRACTS_JAR_PATH: URL = CordaClassResolverTests::class.java.getResource("/isolated.jar")
|
||||
}
|
||||
|
||||
private val emptyWhitelistContext: CheckpointSerializationContext = CheckpointSerializationContextImpl(this.javaClass.classLoader, EmptyWhitelist, emptyMap(), true, null)
|
||||
private val allButBlacklistedContext: CheckpointSerializationContext = CheckpointSerializationContextImpl(this.javaClass.classLoader, AllButBlacklisted, emptyMap(), true, null)
|
||||
|
||||
@Test
|
||||
fun `Annotation on enum works for specialised entries`() {
|
||||
CordaClassResolver(emptyWhitelistContext).getRegistration(Foo.Bar::class.java)
|
||||
@ -212,7 +213,7 @@ class CordaClassResolverTests {
|
||||
val storage = MockAttachmentStorage()
|
||||
val attachmentHash = importJar(storage)
|
||||
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! })
|
||||
val attachedClass = Class.forName("net.corda.finance.contracts.isolated.AnotherDummyContract", true, classLoader)
|
||||
val attachedClass = Class.forName("net.corda.isolated.contracts.AnotherDummyContract", true, classLoader)
|
||||
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
|
||||
}
|
||||
|
||||
@ -221,7 +222,7 @@ class CordaClassResolverTests {
|
||||
val storage = MockAttachmentStorage()
|
||||
val attachmentHash = importJar(storage, "some_uploader")
|
||||
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! })
|
||||
val attachedClass = Class.forName("net.corda.finance.contracts.isolated.AnotherDummyContract", true, classLoader)
|
||||
val attachedClass = Class.forName("net.corda.isolated.contracts.AnotherDummyContract", true, classLoader)
|
||||
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
|
||||
}
|
||||
|
||||
|
BIN
serialization/src/test/resources/isolated.jar
Normal file
BIN
serialization/src/test/resources/isolated.jar
Normal file
Binary file not shown.
Binary file not shown.
@ -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'
|
||||
|
@ -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()
|
||||
|
@ -21,7 +21,9 @@ interface TestCordappInternal : TestCordapp {
|
||||
fun withOnlyJarContents(): TestCordappInternal
|
||||
|
||||
companion object {
|
||||
fun installCordapps(baseDirectory: Path, nodeSpecificCordapps: Set<TestCordappInternal>, generalCordapps: Set<TestCordappInternal>) {
|
||||
fun installCordapps(baseDirectory: Path,
|
||||
nodeSpecificCordapps: Set<TestCordappInternal>,
|
||||
generalCordapps: Set<TestCordappInternal> = emptySet()) {
|
||||
val nodeSpecificCordappsWithoutMeta = checkNoConflicts(nodeSpecificCordapps)
|
||||
checkNoConflicts(generalCordapps)
|
||||
|
||||
|
@ -23,7 +23,7 @@ val FINANCE_CONTRACTS_CORDAPP: TestCordappImpl = findCordapp("net.corda.finance.
|
||||
val FINANCE_WORKFLOWS_CORDAPP: TestCordappImpl = findCordapp("net.corda.finance.flows")
|
||||
|
||||
@JvmField
|
||||
val FINANCE_CORDAPPS: List<TestCordappInternal> = listOf(FINANCE_CONTRACTS_CORDAPP, FINANCE_WORKFLOWS_CORDAPP)
|
||||
val FINANCE_CORDAPPS: Set<TestCordappInternal> = setOf(FINANCE_CONTRACTS_CORDAPP, FINANCE_WORKFLOWS_CORDAPP)
|
||||
|
||||
fun cordappsForPackages(vararg packageNames: String): Set<CustomCordapp> = cordappsForPackages(packageNames.asList())
|
||||
|
||||
|
@ -2,33 +2,16 @@ package net.corda.testing.internal
|
||||
|
||||
import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme
|
||||
import net.corda.core.serialization.internal.SerializationEnvironment
|
||||
import net.corda.core.serialization.internal._contextSerializationEnv
|
||||
import net.corda.core.serialization.internal._inheritableContextSerializationEnv
|
||||
import net.corda.node.serialization.amqp.AMQPServerSerializationScheme
|
||||
import net.corda.node.serialization.kryo.KRYO_CHECKPOINT_CONTEXT
|
||||
import net.corda.node.serialization.kryo.KryoCheckpointSerializer
|
||||
import net.corda.serialization.internal.*
|
||||
import net.corda.testing.common.internal.asContextEnv
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ExecutorService
|
||||
|
||||
val inVMExecutors = ConcurrentHashMap<SerializationEnvironment, ExecutorService>()
|
||||
|
||||
/**
|
||||
* For example your test class uses [SerializationEnvironmentRule] but you want to turn it off for one method.
|
||||
* Use sparingly, ideally a test class shouldn't mix serializers init mechanisms.
|
||||
*/
|
||||
fun <T> withoutTestSerialization(callable: () -> T): T { // TODO: Delete this, see CORDA-858.
|
||||
val (property, env) = listOf(_contextSerializationEnv, _inheritableContextSerializationEnv).map { Pair(it, it.get()) }.single { it.second != null }
|
||||
property.set(null)
|
||||
try {
|
||||
return callable()
|
||||
} finally {
|
||||
property.set(env)
|
||||
}
|
||||
}
|
||||
|
||||
fun createTestSerializationEnv(): SerializationEnvironment {
|
||||
val factory = SerializationFactoryImpl().apply {
|
||||
registerScheme(AMQPClientSerializationScheme(emptyList()))
|
||||
|
Loading…
Reference in New Issue
Block a user