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-blobinspector_test" target="1.8" />
|
||||||
<module name="net.corda-finance-contracts-states_main" target="1.8" />
|
<module name="net.corda-finance-contracts-states_main" target="1.8" />
|
||||||
<module name="net.corda-finance-contracts-states_test" target="1.8" />
|
<module name="net.corda-finance-contracts-states_test" target="1.8" />
|
||||||
|
<module name="net.corda-isolated_main" target="1.8" />
|
||||||
|
<module name="net.corda-isolated_test" target="1.8" />
|
||||||
<module name="net.corda-verifier_main" target="1.8" />
|
<module name="net.corda-verifier_main" target="1.8" />
|
||||||
<module name="net.corda-verifier_test" target="1.8" />
|
<module name="net.corda-verifier_test" target="1.8" />
|
||||||
<module name="net.corda_buildSrc_main" target="1.8" />
|
<module name="net.corda_buildSrc_main" target="1.8" />
|
||||||
|
@ -76,7 +76,7 @@ sealed class NotaryError {
|
|||||||
*/
|
*/
|
||||||
// TODO: include notary timestamp?
|
// TODO: include notary timestamp?
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
data class StateConsumptionDetails(
|
data class StateConsumptionDetails(
|
||||||
val hashOfTransactionId: SecureHash,
|
val hashOfTransactionId: SecureHash,
|
||||||
val type: ConsumedStateType
|
val type: ConsumedStateType
|
||||||
) {
|
) {
|
||||||
|
@ -20,12 +20,9 @@ import kotlin.math.min
|
|||||||
/**
|
/**
|
||||||
* Resolves transactions for the specified [txHashes] along with their full history (dependency graph) from [otherSide].
|
* Resolves transactions for the specified [txHashes] along with their full history (dependency graph) from [otherSide].
|
||||||
* Each retrieved transaction is validated and inserted into the local transaction storage.
|
* Each retrieved transaction is validated and inserted into the local transaction storage.
|
||||||
*
|
|
||||||
* @return a list of verified [SignedTransaction] objects, in a depth-first order.
|
|
||||||
*/
|
*/
|
||||||
@DeleteForDJVM
|
@DeleteForDJVM
|
||||||
class ResolveTransactionsFlow(txHashesArg: Set<SecureHash>,
|
class ResolveTransactionsFlow(txHashesArg: Set<SecureHash>, private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||||
private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
|
||||||
|
|
||||||
// Need it ordered in terms of iteration. Needs to be a variable for the check-pointing logic to work.
|
// Need it ordered in terms of iteration. Needs to be a variable for the check-pointing logic to work.
|
||||||
private val txHashes = txHashesArg.toList()
|
private val txHashes = txHashesArg.toList()
|
||||||
|
@ -70,7 +70,7 @@ fun <T : Any> deserialiseComponentGroup(componentGroups: List<ComponentGroup>,
|
|||||||
// If the componentGroup is a [LazyMappedList] it means that the original deserialized version is already available.
|
// If the componentGroup is a [LazyMappedList] it means that the original deserialized version is already available.
|
||||||
val components = group.components
|
val components = group.components
|
||||||
if (!forceDeserialize && components is LazyMappedList<*, OpaqueBytes>) {
|
if (!forceDeserialize && components is LazyMappedList<*, OpaqueBytes>) {
|
||||||
return components.originalList as List<T>
|
return uncheckedCast(components.originalList)
|
||||||
}
|
}
|
||||||
|
|
||||||
return components.lazyMapped { component, internalIndex ->
|
return components.lazyMapped { component, internalIndex ->
|
||||||
|
@ -66,8 +66,6 @@ private constructor(
|
|||||||
checkBaseInvariants()
|
checkBaseInvariants()
|
||||||
if (timeWindow != null) check(notary != null) { "Transactions with time-windows must be notarised" }
|
if (timeWindow != null) check(notary != null) { "Transactions with time-windows must be notarised" }
|
||||||
checkNotaryWhitelisted()
|
checkNotaryWhitelisted()
|
||||||
checkNoNotaryChange()
|
|
||||||
checkEncumbrancesValid()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -101,9 +99,6 @@ private constructor(
|
|||||||
val inputStates: List<ContractState> get() = inputs.map { it.state.data }
|
val inputStates: List<ContractState> get() = inputs.map { it.state.data }
|
||||||
val referenceStates: List<ContractState> get() = references.map { it.state.data }
|
val referenceStates: List<ContractState> get() = references.map { it.state.data }
|
||||||
|
|
||||||
private val inputAndOutputStates = inputs.map { it.state } + outputs
|
|
||||||
private val allStates = inputAndOutputStates + references.map { it.state }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the typed input StateAndRef at the specified index
|
* Returns the typed input StateAndRef at the specified index
|
||||||
* @param index The index into the inputs.
|
* @param index The index into the inputs.
|
||||||
@ -129,218 +124,12 @@ private constructor(
|
|||||||
logger.warn("Network parameters on the LedgerTransaction with id: $id are null. Please don't use deprecated constructors of the LedgerTransaction. " +
|
logger.warn("Network parameters on the LedgerTransaction with id: $id are null. Please don't use deprecated constructors of the LedgerTransaction. " +
|
||||||
"Use WireTransaction.toLedgerTransaction instead. The result of the verify method might not be accurate.")
|
"Use WireTransaction.toLedgerTransaction instead. The result of the verify method might not be accurate.")
|
||||||
}
|
}
|
||||||
val contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>> = getContractAttachmentsByContract(allStates.map { it.contract }.toSet())
|
|
||||||
|
|
||||||
AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(this.attachments) { transactionClassLoader ->
|
AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(this.attachments) { transactionClassLoader ->
|
||||||
|
Verifier(createLtxForVerification(), transactionClassLoader).verify()
|
||||||
val internalTx = createLtxForVerification()
|
|
||||||
|
|
||||||
validateContractVersions(contractAttachmentsByContract)
|
|
||||||
validatePackageOwnership(contractAttachmentsByContract)
|
|
||||||
validateStatesAgainstContract(internalTx)
|
|
||||||
val hashToSignatureConstrainedContracts = verifyConstraintsValidity(internalTx, contractAttachmentsByContract, transactionClassLoader)
|
|
||||||
verifyConstraints(internalTx, contractAttachmentsByContract, hashToSignatureConstrainedContracts)
|
|
||||||
verifyContracts(internalTx)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify that contract class versions of output states are not lower that versions of relevant input states.
|
|
||||||
*/
|
|
||||||
@Throws(TransactionVerificationException::class)
|
|
||||||
private fun validateContractVersions(contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>) {
|
|
||||||
contractAttachmentsByContract.forEach { contractClassName, attachments ->
|
|
||||||
val outputVersion = attachments.signed?.version ?: attachments.unsigned?.version ?: DEFAULT_CORDAPP_VERSION
|
|
||||||
inputStatesContractClassNameToMaxVersion[contractClassName]?.let {
|
|
||||||
if (it > outputVersion) {
|
|
||||||
throw TransactionVerificationException.TransactionVerificationVersionException(this.id, contractClassName, "$it", "$outputVersion")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For all input and output [TransactionState]s, validates that the wrapped [ContractState] matches up with the
|
|
||||||
* wrapped [Contract], as declared by the [BelongsToContract] annotation on the [ContractState]'s class.
|
|
||||||
*
|
|
||||||
* If the target platform version of the current CorDapp is lower than 4.0, a warning will be written to the log
|
|
||||||
* if any mismatch is detected. If it is 4.0 or later, then [TransactionContractConflictException] will be thrown.
|
|
||||||
*/
|
|
||||||
private fun validateStatesAgainstContract(internalTx: LedgerTransaction) =
|
|
||||||
internalTx.allStates.forEach(::validateStateAgainstContract)
|
|
||||||
|
|
||||||
private fun validateStateAgainstContract(state: TransactionState<ContractState>) {
|
|
||||||
val shouldEnforce = StateContractValidationEnforcementRule.shouldEnforce(state.data)
|
|
||||||
|
|
||||||
val requiredContractClassName = state.data.requiredContractClassName ?:
|
|
||||||
if (shouldEnforce) throw TransactionRequiredContractUnspecifiedException(id, state)
|
|
||||||
else return
|
|
||||||
|
|
||||||
if (state.contract != requiredContractClassName)
|
|
||||||
if (shouldEnforce) {
|
|
||||||
throw TransactionContractConflictException(id, state, requiredContractClassName)
|
|
||||||
} else {
|
|
||||||
logger.warnOnce("""
|
|
||||||
State of class ${state.data::class.java.typeName} belongs to contract $requiredContractClassName, but
|
|
||||||
is bundled in TransactionState with ${state.contract}.
|
|
||||||
|
|
||||||
For details see: https://docs.corda.net/api-contract-constraints.html#contract-state-agreement
|
|
||||||
""".trimIndent().replace('\n', ' '))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify that for each contract the network wide package owner is respected.
|
|
||||||
*
|
|
||||||
* TODO - revisit once transaction contains network parameters. - UPDATE: It contains them, but because of the API stability and the fact that
|
|
||||||
* LedgerTransaction was data class i.e. exposed constructors that shouldn't had been exposed, we still need to keep them nullable :/
|
|
||||||
*/
|
|
||||||
private fun validatePackageOwnership(contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>) {
|
|
||||||
val contractsAndOwners = allStates.mapNotNull { transactionState ->
|
|
||||||
val contractClassName = transactionState.contract
|
|
||||||
networkParameters!!.getPackageOwnerOf(contractClassName)?.let { contractClassName to it }
|
|
||||||
}.toMap()
|
|
||||||
|
|
||||||
contractsAndOwners.forEach { contract, owner ->
|
|
||||||
contractAttachmentsByContract[contract]?.filter { it.isSigned }?.forEach { attachment ->
|
|
||||||
if (!owner.isFulfilledBy(attachment.signerKeys))
|
|
||||||
throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(this.id, id, contract)
|
|
||||||
} ?: throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(this.id, id, contract)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enforces the validity of the actual constraints.
|
|
||||||
* * Constraints should be one of the valid supported ones.
|
|
||||||
* * Constraints should propagate correctly if not marked otherwise.
|
|
||||||
*
|
|
||||||
* Returns set of contract classes that identify hash -> signature constraint switchover
|
|
||||||
*/
|
|
||||||
private fun verifyConstraintsValidity(internalTx: LedgerTransaction, contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>, transactionClassLoader: ClassLoader): MutableSet<ContractClassName> {
|
|
||||||
// First check that the constraints are valid.
|
|
||||||
for (state in internalTx.allStates) {
|
|
||||||
checkConstraintValidity(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group the inputs and outputs by contract, and for each contract verify the constraints propagation logic.
|
|
||||||
// This is not required for reference states as there is nothing to propagate.
|
|
||||||
val inputContractGroups = internalTx.inputs.groupBy { it.state.contract }
|
|
||||||
val outputContractGroups = internalTx.outputs.groupBy { it.contract }
|
|
||||||
|
|
||||||
// identify any contract classes where input-output pair are transitioning from hash to signature constraints.
|
|
||||||
val hashToSignatureConstrainedContracts = mutableSetOf<ContractClassName>()
|
|
||||||
|
|
||||||
for (contractClassName in (inputContractGroups.keys + outputContractGroups.keys)) {
|
|
||||||
if (contractClassName.contractHasAutomaticConstraintPropagation(transactionClassLoader)) {
|
|
||||||
// Verify that the constraints of output states have at least the same level of restriction as the constraints of the corresponding input states.
|
|
||||||
val inputConstraints = inputContractGroups[contractClassName]?.map { it.state.constraint }?.toSet()
|
|
||||||
val outputConstraints = outputContractGroups[contractClassName]?.map { it.constraint }?.toSet()
|
|
||||||
outputConstraints?.forEach { outputConstraint ->
|
|
||||||
inputConstraints?.forEach { inputConstraint ->
|
|
||||||
val constraintAttachment = resolveAttachment(contractClassName, contractAttachmentsByContract)
|
|
||||||
if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, constraintAttachment))) {
|
|
||||||
throw TransactionVerificationException.ConstraintPropagationRejection(id, contractClassName, inputConstraint, outputConstraint)
|
|
||||||
}
|
|
||||||
// Hash to signature constraints auto-migration
|
|
||||||
if (outputConstraint is SignatureAttachmentConstraint && inputConstraint is HashAttachmentConstraint)
|
|
||||||
hashToSignatureConstrainedContracts.add(contractClassName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
contractClassName.warnContractWithoutConstraintPropagation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return hashToSignatureConstrainedContracts
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resolveAttachment(contractClassName: ContractClassName, contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>): AttachmentWithContext {
|
|
||||||
val unsignedAttachment = contractAttachmentsByContract[contractClassName]!!.filter { !it.isSigned }.firstOrNull()
|
|
||||||
val signedAttachment = contractAttachmentsByContract[contractClassName]!!.filter { it.isSigned }.firstOrNull()
|
|
||||||
return when {
|
|
||||||
(unsignedAttachment != null && signedAttachment != null) -> AttachmentWithContext(signedAttachment, contractClassName, networkParameters!!)
|
|
||||||
(unsignedAttachment != null) -> AttachmentWithContext(unsignedAttachment, contractClassName, networkParameters!!)
|
|
||||||
(signedAttachment != null) -> AttachmentWithContext(signedAttachment, contractClassName, networkParameters!!)
|
|
||||||
else -> throw TransactionVerificationException.ContractConstraintRejection(id, contractClassName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify that all contract constraints are passing before running any contract code.
|
|
||||||
*
|
|
||||||
* This check is running the [AttachmentConstraint.isSatisfiedBy] method for each corresponding [ContractAttachment].
|
|
||||||
*
|
|
||||||
* @throws TransactionVerificationException if the constraints fail to verify
|
|
||||||
*/
|
|
||||||
private fun verifyConstraints(internalTx: LedgerTransaction, contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>>, hashToSignatureConstrainedContracts: MutableSet<ContractClassName>) {
|
|
||||||
for (state in internalTx.allStates) {
|
|
||||||
if (state.constraint is SignatureAttachmentConstraint)
|
|
||||||
checkMinimumPlatformVersion(networkParameters!!.minimumPlatformVersion, 4, "Signature constraints")
|
|
||||||
|
|
||||||
val constraintAttachment =
|
|
||||||
// hash to to signature constraint migration logic:
|
|
||||||
// pass the unsigned attachment when verifying the constraint of the input state, and the signed attachment when verifying the constraint of the output state.
|
|
||||||
if (state.contract in hashToSignatureConstrainedContracts) {
|
|
||||||
val unsignedAttachment = contractAttachmentsByContract[state.contract].unsigned
|
|
||||||
?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
|
|
||||||
val signedAttachment = contractAttachmentsByContract[state.contract].signed
|
|
||||||
?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
|
|
||||||
when {
|
|
||||||
// use unsigned attachment if hash-constrained input state
|
|
||||||
state.data in inputStates -> AttachmentWithContext(unsignedAttachment, state.contract, networkParameters!!)
|
|
||||||
// use signed attachment if signature-constrained output state
|
|
||||||
state.data in outputStates -> AttachmentWithContext(signedAttachment, state.contract, networkParameters!!)
|
|
||||||
else -> throw IllegalStateException("${state.contract} must use either signed or unsigned attachment in hash to signature constraints migration")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// standard processing logic
|
|
||||||
else {
|
|
||||||
val contractAttachment = contractAttachmentsByContract[state.contract]?.firstOrNull()
|
|
||||||
?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
|
|
||||||
AttachmentWithContext(contractAttachment, state.contract, networkParameters!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.constraint.isSatisfiedBy(constraintAttachment)) {
|
|
||||||
throw TransactionVerificationException.ContractConstraintRejection(id, state.contract)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val Set<ContractAttachment>?.unsigned: ContractAttachment?
|
|
||||||
get() {
|
|
||||||
return this?.filter { !it.isSigned }?.firstOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val Set<ContractAttachment>?.signed: ContractAttachment?
|
|
||||||
get() {
|
|
||||||
return this?.filter { it.isSigned }?.firstOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: revisit to include contract version information
|
|
||||||
/**
|
|
||||||
* This method may return more than one attachment for a given contract class.
|
|
||||||
* Specifically, this is the case for transactions combining hash and signature constraints where the hash constrained contract jar
|
|
||||||
* will be unsigned, and the signature constrained counterpart will be signed.
|
|
||||||
*/
|
|
||||||
private fun getContractAttachmentsByContract(contractClasses: Set<ContractClassName>): Map<ContractClassName, Set<ContractAttachment>> {
|
|
||||||
val result = mutableMapOf<ContractClassName, Set<ContractAttachment>>()
|
|
||||||
|
|
||||||
for (attachment in attachments) {
|
|
||||||
if (attachment !is ContractAttachment) continue
|
|
||||||
for (contract in contractClasses) {
|
|
||||||
if (!attachment.allContracts.contains(contract)) continue
|
|
||||||
result[contract] = result.getOrDefault(contract, setOf(attachment)).plus(attachment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun contractClassFor(className: ContractClassName, classLoader: ClassLoader): Class<out Contract> = try {
|
|
||||||
classLoader.loadClass(className).asSubclass(Contract::class.java)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw TransactionVerificationException.ContractCreationError(id, className, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createLtxForVerification(): LedgerTransaction {
|
private fun createLtxForVerification(): LedgerTransaction {
|
||||||
val serializedInputs = this.serializedInputs
|
val serializedInputs = this.serializedInputs
|
||||||
val serializedReferences = this.serializedReferences
|
val serializedReferences = this.serializedReferences
|
||||||
@ -368,7 +157,7 @@ private constructor(
|
|||||||
privacySalt = this.privacySalt,
|
privacySalt = this.privacySalt,
|
||||||
networkParameters = this.networkParameters,
|
networkParameters = this.networkParameters,
|
||||||
references = deserializedReferences,
|
references = deserializedReferences,
|
||||||
inputStatesContractClassNameToMaxVersion = emptyMap()
|
inputStatesContractClassNameToMaxVersion = this.inputStatesContractClassNameToMaxVersion
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// This branch is only present for backwards compatibility.
|
// This branch is only present for backwards compatibility.
|
||||||
@ -378,156 +167,6 @@ private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check the transaction is contract-valid by running the verify() for each input and output state contract.
|
|
||||||
* If any contract fails to verify, the whole transaction is considered to be invalid.
|
|
||||||
*/
|
|
||||||
private fun verifyContracts(internalTx: LedgerTransaction) {
|
|
||||||
val contractClasses = (internalTx.inputs.map { it.state } + internalTx.outputs).toSet()
|
|
||||||
.map { it.contract to contractClassFor(it.contract, it.data.javaClass.classLoader) }
|
|
||||||
|
|
||||||
val contractInstances = contractClasses.map { (contractClassName, contractClass) ->
|
|
||||||
try {
|
|
||||||
contractClass.newInstance()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw TransactionVerificationException.ContractCreationError(id, contractClassName, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
contractInstances.forEach { contract ->
|
|
||||||
try {
|
|
||||||
contract.verify(internalTx)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw TransactionVerificationException.ContractRejection(id, contract, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make sure the notary has stayed the same. As we can't tell how inputs and outputs connect, if there
|
|
||||||
* are any inputs or reference inputs, all outputs must have the same notary.
|
|
||||||
*
|
|
||||||
* TODO: Is that the correct set of restrictions? May need to come back to this, see if we can be more
|
|
||||||
* flexible on output notaries.
|
|
||||||
*/
|
|
||||||
private fun checkNoNotaryChange() {
|
|
||||||
if (notary != null && (inputs.isNotEmpty() || references.isNotEmpty())) {
|
|
||||||
outputs.forEach {
|
|
||||||
if (it.notary != notary) {
|
|
||||||
throw TransactionVerificationException.NotaryChangeInWrongTransactionType(id, notary, it.notary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkEncumbrancesValid() {
|
|
||||||
// Validate that all encumbrances exist within the set of input states.
|
|
||||||
inputs.filter { it.state.encumbrance != null }
|
|
||||||
.forEach { (state, ref) -> checkInputEncumbranceStateExists(state, ref) }
|
|
||||||
|
|
||||||
// Check that in the outputs,
|
|
||||||
// a) an encumbered state does not refer to itself as the encumbrance
|
|
||||||
// b) the number of outputs can contain the encumbrance
|
|
||||||
// c) the bi-directionality (full cycle) property is satisfied
|
|
||||||
// d) encumbered output states are assigned to the same notary.
|
|
||||||
val statesAndEncumbrance = outputs.withIndex().filter { it.value.encumbrance != null }
|
|
||||||
.map { Pair(it.index, it.value.encumbrance!!) }
|
|
||||||
if (!statesAndEncumbrance.isEmpty()) {
|
|
||||||
checkBidirectionalOutputEncumbrances(statesAndEncumbrance)
|
|
||||||
checkNotariesOutputEncumbrance(statesAndEncumbrance)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method to check if all encumbered states are assigned to the same notary Party.
|
|
||||||
// This method should be invoked after [checkBidirectionalOutputEncumbrances], because it assumes that the
|
|
||||||
// bi-directionality property is already satisfied.
|
|
||||||
private fun checkNotariesOutputEncumbrance(statesAndEncumbrance: List<Pair<Int, Int>>) {
|
|
||||||
// We only check for transactions in which notary is null (i.e., issuing transactions).
|
|
||||||
// Note that if a notary is defined for a transaction, we already check if all outputs are assigned
|
|
||||||
// to the same notary (transaction's notary) in [checkNoNotaryChange()].
|
|
||||||
if (notary == null) {
|
|
||||||
// indicesAlreadyChecked is used to bypass already checked indices and to avoid cycles.
|
|
||||||
val indicesAlreadyChecked = HashSet<Int>()
|
|
||||||
statesAndEncumbrance.forEach {
|
|
||||||
checkNotary(it.first, indicesAlreadyChecked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private tailrec fun checkNotary(index: Int, indicesAlreadyChecked: HashSet<Int>) {
|
|
||||||
if (indicesAlreadyChecked.add(index)) {
|
|
||||||
val encumbranceIndex = outputs[index].encumbrance!!
|
|
||||||
if (outputs[index].notary != outputs[encumbranceIndex].notary) {
|
|
||||||
throw TransactionVerificationException.TransactionNotaryMismatchEncumbranceException(id, index, encumbranceIndex, outputs[index].notary, outputs[encumbranceIndex].notary)
|
|
||||||
} else {
|
|
||||||
checkNotary(encumbranceIndex, indicesAlreadyChecked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkInputEncumbranceStateExists(state: TransactionState<ContractState>, ref: StateRef) {
|
|
||||||
val encumbranceStateExists = inputs.any {
|
|
||||||
it.ref.txhash == ref.txhash && it.ref.index == state.encumbrance
|
|
||||||
}
|
|
||||||
if (!encumbranceStateExists) {
|
|
||||||
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
|
||||||
id,
|
|
||||||
state.encumbrance!!,
|
|
||||||
TransactionVerificationException.Direction.INPUT
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Using basic graph theory, a full cycle of encumbered (co-dependent) states should exist to achieve bi-directional
|
|
||||||
// encumbrances. This property is important to ensure that no states involved in an encumbrance-relationship
|
|
||||||
// can be spent on their own. Briefly, if any of the states is having more than one encumbrance references by
|
|
||||||
// other states, a full cycle detection will fail. As a result, all of the encumbered states must be present
|
|
||||||
// as "from" and "to" only once (or zero times if no encumbrance takes place). For instance,
|
|
||||||
// a -> b
|
|
||||||
// c -> b and a -> b
|
|
||||||
// b -> a b -> c
|
|
||||||
// do not satisfy the bi-directionality (full cycle) property.
|
|
||||||
//
|
|
||||||
// In the first example "b" appears twice in encumbrance ("to") list and "c" exists in the encumbered ("from") list only.
|
|
||||||
// Due the above, one could consume "a" and "b" in the same transaction and then, because "b" is already consumed, "c" cannot be spent.
|
|
||||||
//
|
|
||||||
// Similarly, the second example does not form a full cycle because "a" and "c" exist in one of the lists only.
|
|
||||||
// As a result, one can consume "b" and "c" in the same transactions, which will make "a" impossible to be spent.
|
|
||||||
//
|
|
||||||
// On other hand the following are valid constructions:
|
|
||||||
// a -> b a -> c
|
|
||||||
// b -> c and c -> b
|
|
||||||
// c -> a b -> a
|
|
||||||
// and form a full cycle, meaning that the bi-directionality property is satisfied.
|
|
||||||
private fun checkBidirectionalOutputEncumbrances(statesAndEncumbrance: List<Pair<Int, Int>>) {
|
|
||||||
// [Set] of "from" (encumbered states).
|
|
||||||
val encumberedSet = mutableSetOf<Int>()
|
|
||||||
// [Set] of "to" (encumbrance states).
|
|
||||||
val encumbranceSet = mutableSetOf<Int>()
|
|
||||||
// Update both [Set]s.
|
|
||||||
statesAndEncumbrance.forEach { (statePosition, encumbrance) ->
|
|
||||||
// Check it does not refer to itself.
|
|
||||||
if (statePosition == encumbrance || encumbrance >= outputs.size) {
|
|
||||||
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
|
||||||
id,
|
|
||||||
encumbrance,
|
|
||||||
TransactionVerificationException.Direction.OUTPUT)
|
|
||||||
} else {
|
|
||||||
encumberedSet.add(statePosition) // Guaranteed to have unique elements.
|
|
||||||
if (!encumbranceSet.add(encumbrance)) {
|
|
||||||
throw TransactionVerificationException.TransactionDuplicateEncumbranceException(id, encumbrance)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// At this stage we have ensured that "from" and "to" [Set]s are equal in size, but we should check their
|
|
||||||
// elements do indeed match. If they don't match, we return their symmetric difference (disjunctive union).
|
|
||||||
val symmetricDifference = (encumberedSet union encumbranceSet).subtract(encumberedSet intersect encumbranceSet)
|
|
||||||
if (symmetricDifference.isNotEmpty()) {
|
|
||||||
// At least one encumbered state is not in the [encumbranceSet] and vice versa.
|
|
||||||
throw TransactionVerificationException.TransactionNonMatchingEncumbranceException(id, symmetricDifference)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a type and a function that returns a grouping key, associates inputs and outputs together so that they
|
* Given a type and a function that returns a grouping key, associates inputs and outputs together so that they
|
||||||
* can be processed as one. The grouping key is any arbitrary object that can act as a map key (so must implement
|
* can be processed as one. The grouping key is any arbitrary object that can act as a map key (so must implement
|
||||||
@ -869,6 +508,385 @@ private constructor(
|
|||||||
|)""".trimMargin()
|
|)""".trimMargin()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Because we create a separate [LedgerTransaction] onto which we need to perform verification, it becomes important we don't verify the
|
||||||
|
* wrong object instance. This class helps avoid that.
|
||||||
|
*/
|
||||||
|
private class Verifier(private val ltx: LedgerTransaction, private val transactionClassLoader: ClassLoader) {
|
||||||
|
private val inputStates: List<TransactionState<*>> = ltx.inputs.map { it.state }
|
||||||
|
private val allStates: List<TransactionState<*>> = inputStates + ltx.references.map { it.state } + ltx.outputs
|
||||||
|
private val contractAttachmentsByContract: Map<ContractClassName, Set<ContractAttachment>> = getContractAttachmentsByContract()
|
||||||
|
|
||||||
|
fun verify() {
|
||||||
|
// checkNoNotaryChange and checkEncumbrancesValid are called here, and not in the c'tor, as they need access to the "outputs"
|
||||||
|
// list, the contents of which need to be deserialized under the correct classloader.
|
||||||
|
checkNoNotaryChange()
|
||||||
|
checkEncumbrancesValid()
|
||||||
|
validateContractVersions()
|
||||||
|
validatePackageOwnership()
|
||||||
|
validateStatesAgainstContract()
|
||||||
|
val hashToSignatureConstrainedContracts = verifyConstraintsValidity()
|
||||||
|
verifyConstraints(hashToSignatureConstrainedContracts)
|
||||||
|
verifyContracts()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: revisit to include contract version information
|
||||||
|
/**
|
||||||
|
* This method may return more than one attachment for a given contract class.
|
||||||
|
* Specifically, this is the case for transactions combining hash and signature constraints where the hash constrained contract jar
|
||||||
|
* will be unsigned, and the signature constrained counterpart will be signed.
|
||||||
|
*/
|
||||||
|
private fun getContractAttachmentsByContract(): Map<ContractClassName, Set<ContractAttachment>> {
|
||||||
|
val contractClasses = allStates.map { it.contract }.toSet()
|
||||||
|
val result = mutableMapOf<ContractClassName, Set<ContractAttachment>>()
|
||||||
|
|
||||||
|
for (attachment in ltx.attachments) {
|
||||||
|
if (attachment !is ContractAttachment) continue
|
||||||
|
for (contract in contractClasses) {
|
||||||
|
if (contract !in attachment.allContracts) continue
|
||||||
|
result[contract] = result.getOrDefault(contract, setOf(attachment)).plus(attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make sure the notary has stayed the same. As we can't tell how inputs and outputs connect, if there
|
||||||
|
* are any inputs or reference inputs, all outputs must have the same notary.
|
||||||
|
*
|
||||||
|
* TODO: Is that the correct set of restrictions? May need to come back to this, see if we can be more
|
||||||
|
* flexible on output notaries.
|
||||||
|
*/
|
||||||
|
private fun checkNoNotaryChange() {
|
||||||
|
if (ltx.notary != null && (ltx.inputs.isNotEmpty() || ltx.references.isNotEmpty())) {
|
||||||
|
ltx.outputs.forEach {
|
||||||
|
if (it.notary != ltx.notary) {
|
||||||
|
throw TransactionVerificationException.NotaryChangeInWrongTransactionType(ltx.id, ltx.notary, it.notary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkEncumbrancesValid() {
|
||||||
|
// Validate that all encumbrances exist within the set of input states.
|
||||||
|
ltx.inputs
|
||||||
|
.filter { it.state.encumbrance != null }
|
||||||
|
.forEach { (state, ref) -> checkInputEncumbranceStateExists(state, ref) }
|
||||||
|
|
||||||
|
// Check that in the outputs,
|
||||||
|
// a) an encumbered state does not refer to itself as the encumbrance
|
||||||
|
// b) the number of outputs can contain the encumbrance
|
||||||
|
// c) the bi-directionality (full cycle) property is satisfied
|
||||||
|
// d) encumbered output states are assigned to the same notary.
|
||||||
|
val statesAndEncumbrance = ltx.outputs
|
||||||
|
.withIndex()
|
||||||
|
.filter { it.value.encumbrance != null }
|
||||||
|
.map { Pair(it.index, it.value.encumbrance!!) }
|
||||||
|
if (!statesAndEncumbrance.isEmpty()) {
|
||||||
|
checkBidirectionalOutputEncumbrances(statesAndEncumbrance)
|
||||||
|
checkNotariesOutputEncumbrance(statesAndEncumbrance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkInputEncumbranceStateExists(state: TransactionState<ContractState>, ref: StateRef) {
|
||||||
|
val encumbranceStateExists = ltx.inputs.any {
|
||||||
|
it.ref.txhash == ref.txhash && it.ref.index == state.encumbrance
|
||||||
|
}
|
||||||
|
if (!encumbranceStateExists) {
|
||||||
|
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
||||||
|
ltx.id,
|
||||||
|
state.encumbrance!!,
|
||||||
|
TransactionVerificationException.Direction.INPUT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using basic graph theory, a full cycle of encumbered (co-dependent) states should exist to achieve bi-directional
|
||||||
|
// encumbrances. This property is important to ensure that no states involved in an encumbrance-relationship
|
||||||
|
// can be spent on their own. Briefly, if any of the states is having more than one encumbrance references by
|
||||||
|
// other states, a full cycle detection will fail. As a result, all of the encumbered states must be present
|
||||||
|
// as "from" and "to" only once (or zero times if no encumbrance takes place). For instance,
|
||||||
|
// a -> b
|
||||||
|
// c -> b and a -> b
|
||||||
|
// b -> a b -> c
|
||||||
|
// do not satisfy the bi-directionality (full cycle) property.
|
||||||
|
//
|
||||||
|
// In the first example "b" appears twice in encumbrance ("to") list and "c" exists in the encumbered ("from") list only.
|
||||||
|
// Due the above, one could consume "a" and "b" in the same transaction and then, because "b" is already consumed, "c" cannot be spent.
|
||||||
|
//
|
||||||
|
// Similarly, the second example does not form a full cycle because "a" and "c" exist in one of the lists only.
|
||||||
|
// As a result, one can consume "b" and "c" in the same transactions, which will make "a" impossible to be spent.
|
||||||
|
//
|
||||||
|
// On other hand the following are valid constructions:
|
||||||
|
// a -> b a -> c
|
||||||
|
// b -> c and c -> b
|
||||||
|
// c -> a b -> a
|
||||||
|
// and form a full cycle, meaning that the bi-directionality property is satisfied.
|
||||||
|
private fun checkBidirectionalOutputEncumbrances(statesAndEncumbrance: List<Pair<Int, Int>>) {
|
||||||
|
// [Set] of "from" (encumbered states).
|
||||||
|
val encumberedSet = mutableSetOf<Int>()
|
||||||
|
// [Set] of "to" (encumbrance states).
|
||||||
|
val encumbranceSet = mutableSetOf<Int>()
|
||||||
|
// Update both [Set]s.
|
||||||
|
statesAndEncumbrance.forEach { (statePosition, encumbrance) ->
|
||||||
|
// Check it does not refer to itself.
|
||||||
|
if (statePosition == encumbrance || encumbrance >= ltx.outputs.size) {
|
||||||
|
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
||||||
|
ltx.id,
|
||||||
|
encumbrance,
|
||||||
|
TransactionVerificationException.Direction.OUTPUT
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
encumberedSet.add(statePosition) // Guaranteed to have unique elements.
|
||||||
|
if (!encumbranceSet.add(encumbrance)) {
|
||||||
|
throw TransactionVerificationException.TransactionDuplicateEncumbranceException(ltx.id, encumbrance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// At this stage we have ensured that "from" and "to" [Set]s are equal in size, but we should check their
|
||||||
|
// elements do indeed match. If they don't match, we return their symmetric difference (disjunctive union).
|
||||||
|
val symmetricDifference = (encumberedSet union encumbranceSet).subtract(encumberedSet intersect encumbranceSet)
|
||||||
|
if (symmetricDifference.isNotEmpty()) {
|
||||||
|
// At least one encumbered state is not in the [encumbranceSet] and vice versa.
|
||||||
|
throw TransactionVerificationException.TransactionNonMatchingEncumbranceException(ltx.id, symmetricDifference)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to check if all encumbered states are assigned to the same notary Party.
|
||||||
|
// This method should be invoked after [checkBidirectionalOutputEncumbrances], because it assumes that the
|
||||||
|
// bi-directionality property is already satisfied.
|
||||||
|
private fun checkNotariesOutputEncumbrance(statesAndEncumbrance: List<Pair<Int, Int>>) {
|
||||||
|
// We only check for transactions in which notary is null (i.e., issuing transactions).
|
||||||
|
// Note that if a notary is defined for a transaction, we already check if all outputs are assigned
|
||||||
|
// to the same notary (transaction's notary) in [checkNoNotaryChange()].
|
||||||
|
if (ltx.notary == null) {
|
||||||
|
// indicesAlreadyChecked is used to bypass already checked indices and to avoid cycles.
|
||||||
|
val indicesAlreadyChecked = HashSet<Int>()
|
||||||
|
statesAndEncumbrance.forEach {
|
||||||
|
checkNotary(it.first, indicesAlreadyChecked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private tailrec fun checkNotary(index: Int, indicesAlreadyChecked: HashSet<Int>) {
|
||||||
|
if (indicesAlreadyChecked.add(index)) {
|
||||||
|
val encumbranceIndex = ltx.outputs[index].encumbrance!!
|
||||||
|
if (ltx.outputs[index].notary != ltx.outputs[encumbranceIndex].notary) {
|
||||||
|
throw TransactionVerificationException.TransactionNotaryMismatchEncumbranceException(
|
||||||
|
ltx.id,
|
||||||
|
index,
|
||||||
|
encumbranceIndex,
|
||||||
|
ltx.outputs[index].notary,
|
||||||
|
ltx.outputs[encumbranceIndex].notary
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
checkNotary(encumbranceIndex, indicesAlreadyChecked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that contract class versions of output states are not lower that versions of relevant input states.
|
||||||
|
*/
|
||||||
|
private fun validateContractVersions() {
|
||||||
|
contractAttachmentsByContract.forEach { contractClassName, attachments ->
|
||||||
|
val outputVersion = attachments.signed?.version ?: attachments.unsigned?.version ?: DEFAULT_CORDAPP_VERSION
|
||||||
|
ltx.inputStatesContractClassNameToMaxVersion[contractClassName]?.let {
|
||||||
|
if (it > outputVersion) {
|
||||||
|
throw TransactionVerificationException.TransactionVerificationVersionException(ltx.id, contractClassName, "$it", "$outputVersion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that for each contract the network wide package owner is respected.
|
||||||
|
*
|
||||||
|
* TODO - revisit once transaction contains network parameters. - UPDATE: It contains them, but because of the API stability and the fact that
|
||||||
|
* LedgerTransaction was data class i.e. exposed constructors that shouldn't had been exposed, we still need to keep them nullable :/
|
||||||
|
*/
|
||||||
|
private fun validatePackageOwnership() {
|
||||||
|
val contractsAndOwners = allStates.mapNotNull { transactionState ->
|
||||||
|
val contractClassName = transactionState.contract
|
||||||
|
ltx.networkParameters!!.getPackageOwnerOf(contractClassName)?.let { contractClassName to it }
|
||||||
|
}.toMap()
|
||||||
|
|
||||||
|
contractsAndOwners.forEach { contract, owner ->
|
||||||
|
contractAttachmentsByContract[contract]?.filter { it.isSigned }?.forEach { attachment ->
|
||||||
|
if (!owner.isFulfilledBy(attachment.signerKeys))
|
||||||
|
throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(ltx.id, attachment.id, contract)
|
||||||
|
} ?: throw TransactionVerificationException.ContractAttachmentNotSignedByPackageOwnerException(ltx.id, ltx.id, contract)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For all input and output [TransactionState]s, validates that the wrapped [ContractState] matches up with the
|
||||||
|
* wrapped [Contract], as declared by the [BelongsToContract] annotation on the [ContractState]'s class.
|
||||||
|
*
|
||||||
|
* If the target platform version of the current CorDapp is lower than 4.0, a warning will be written to the log
|
||||||
|
* if any mismatch is detected. If it is 4.0 or later, then [TransactionContractConflictException] will be thrown.
|
||||||
|
*/
|
||||||
|
private fun validateStatesAgainstContract() = allStates.forEach(::validateStateAgainstContract)
|
||||||
|
|
||||||
|
private fun validateStateAgainstContract(state: TransactionState<ContractState>) {
|
||||||
|
val shouldEnforce = StateContractValidationEnforcementRule.shouldEnforce(state.data)
|
||||||
|
|
||||||
|
val requiredContractClassName = state.data.requiredContractClassName
|
||||||
|
?: if (shouldEnforce) throw TransactionRequiredContractUnspecifiedException(ltx.id, state) else return
|
||||||
|
|
||||||
|
if (state.contract != requiredContractClassName)
|
||||||
|
if (shouldEnforce) {
|
||||||
|
throw TransactionContractConflictException(ltx.id, state, requiredContractClassName)
|
||||||
|
} else {
|
||||||
|
logger.warnOnce("""
|
||||||
|
State of class ${state.data::class.java.typeName} belongs to contract $requiredContractClassName, but
|
||||||
|
is bundled in TransactionState with ${state.contract}.
|
||||||
|
|
||||||
|
For details see: https://docs.corda.net/api-contract-constraints.html#contract-state-agreement
|
||||||
|
""".trimIndent().replace('\n', ' '))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enforces the validity of the actual constraints.
|
||||||
|
* * Constraints should be one of the valid supported ones.
|
||||||
|
* * Constraints should propagate correctly if not marked otherwise.
|
||||||
|
*
|
||||||
|
* Returns set of contract classes that identify hash -> signature constraint switchover
|
||||||
|
*/
|
||||||
|
private fun verifyConstraintsValidity(): MutableSet<ContractClassName> {
|
||||||
|
// First check that the constraints are valid.
|
||||||
|
for (state in allStates) {
|
||||||
|
checkConstraintValidity(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group the inputs and outputs by contract, and for each contract verify the constraints propagation logic.
|
||||||
|
// This is not required for reference states as there is nothing to propagate.
|
||||||
|
val inputContractGroups = ltx.inputs.groupBy { it.state.contract }
|
||||||
|
val outputContractGroups = ltx.outputs.groupBy { it.contract }
|
||||||
|
|
||||||
|
// identify any contract classes where input-output pair are transitioning from hash to signature constraints.
|
||||||
|
val hashToSignatureConstrainedContracts = mutableSetOf<ContractClassName>()
|
||||||
|
|
||||||
|
for (contractClassName in (inputContractGroups.keys + outputContractGroups.keys)) {
|
||||||
|
if (contractClassName.contractHasAutomaticConstraintPropagation(transactionClassLoader)) {
|
||||||
|
// Verify that the constraints of output states have at least the same level of restriction as the constraints of the
|
||||||
|
// corresponding input states.
|
||||||
|
val inputConstraints = inputContractGroups[contractClassName]?.map { it.state.constraint }?.toSet()
|
||||||
|
val outputConstraints = outputContractGroups[contractClassName]?.map { it.constraint }?.toSet()
|
||||||
|
outputConstraints?.forEach { outputConstraint ->
|
||||||
|
inputConstraints?.forEach { inputConstraint ->
|
||||||
|
val constraintAttachment = resolveAttachment(contractClassName)
|
||||||
|
if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, constraintAttachment))) {
|
||||||
|
throw TransactionVerificationException.ConstraintPropagationRejection(
|
||||||
|
ltx.id,
|
||||||
|
contractClassName,
|
||||||
|
inputConstraint,
|
||||||
|
outputConstraint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Hash to signature constraints auto-migration
|
||||||
|
if (outputConstraint is SignatureAttachmentConstraint && inputConstraint is HashAttachmentConstraint)
|
||||||
|
hashToSignatureConstrainedContracts.add(contractClassName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
contractClassName.warnContractWithoutConstraintPropagation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hashToSignatureConstrainedContracts
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveAttachment(contractClassName: ContractClassName): AttachmentWithContext {
|
||||||
|
val unsignedAttachment = contractAttachmentsByContract[contractClassName]!!.firstOrNull { !it.isSigned }
|
||||||
|
val signedAttachment = contractAttachmentsByContract[contractClassName]!!.firstOrNull { it.isSigned }
|
||||||
|
return when {
|
||||||
|
(unsignedAttachment != null && signedAttachment != null) -> AttachmentWithContext(signedAttachment, contractClassName, ltx.networkParameters!!)
|
||||||
|
(unsignedAttachment != null) -> AttachmentWithContext(unsignedAttachment, contractClassName, ltx.networkParameters!!)
|
||||||
|
(signedAttachment != null) -> AttachmentWithContext(signedAttachment, contractClassName, ltx.networkParameters!!)
|
||||||
|
else -> throw TransactionVerificationException.ContractConstraintRejection(ltx.id, contractClassName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that all contract constraints are passing before running any contract code.
|
||||||
|
*
|
||||||
|
* This check is running the [AttachmentConstraint.isSatisfiedBy] method for each corresponding [ContractAttachment].
|
||||||
|
*
|
||||||
|
* @throws TransactionVerificationException if the constraints fail to verify
|
||||||
|
*/
|
||||||
|
private fun verifyConstraints(hashToSignatureConstrainedContracts: MutableSet<ContractClassName>) {
|
||||||
|
for (state in allStates) {
|
||||||
|
if (state.constraint is SignatureAttachmentConstraint) {
|
||||||
|
checkMinimumPlatformVersion(ltx.networkParameters!!.minimumPlatformVersion, 4, "Signature constraints")
|
||||||
|
}
|
||||||
|
|
||||||
|
val constraintAttachment = if (state.contract in hashToSignatureConstrainedContracts) {
|
||||||
|
// hash to to signature constraint migration logic:
|
||||||
|
// pass the unsigned attachment when verifying the constraint of the input state, and the signed attachment when verifying
|
||||||
|
// the constraint of the output state.
|
||||||
|
val unsignedAttachment = contractAttachmentsByContract[state.contract].unsigned
|
||||||
|
?: throw TransactionVerificationException.MissingAttachmentRejection(ltx.id, state.contract)
|
||||||
|
val signedAttachment = contractAttachmentsByContract[state.contract].signed
|
||||||
|
?: throw TransactionVerificationException.MissingAttachmentRejection(ltx.id, state.contract)
|
||||||
|
when {
|
||||||
|
// use unsigned attachment if hash-constrained input state
|
||||||
|
state.data in ltx.inputStates -> AttachmentWithContext(unsignedAttachment, state.contract, ltx.networkParameters!!)
|
||||||
|
// use signed attachment if signature-constrained output state
|
||||||
|
state.data in ltx.outputStates -> AttachmentWithContext(signedAttachment, state.contract, ltx.networkParameters!!)
|
||||||
|
else -> throw IllegalStateException("${state.contract} must use either signed or unsigned attachment in hash to signature constraints migration")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// standard processing logic
|
||||||
|
val contractAttachment = contractAttachmentsByContract[state.contract]?.firstOrNull()
|
||||||
|
?: throw TransactionVerificationException.MissingAttachmentRejection(ltx.id, state.contract)
|
||||||
|
AttachmentWithContext(contractAttachment, state.contract, ltx.networkParameters!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.constraint.isSatisfiedBy(constraintAttachment)) {
|
||||||
|
throw TransactionVerificationException.ContractConstraintRejection(ltx.id, state.contract)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the transaction is contract-valid by running the verify() for each input and output state contract.
|
||||||
|
* If any contract fails to verify, the whole transaction is considered to be invalid.
|
||||||
|
*/
|
||||||
|
private fun verifyContracts() {
|
||||||
|
val contractClasses = (inputStates + ltx.outputs).toSet()
|
||||||
|
.map { it.contract to contractClassFor(it.contract, it.data.javaClass.classLoader) }
|
||||||
|
|
||||||
|
val contractInstances = contractClasses.map { (contractClassName, contractClass) ->
|
||||||
|
try {
|
||||||
|
contractClass.newInstance()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw TransactionVerificationException.ContractCreationError(ltx.id, contractClassName, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contractInstances.forEach { contract ->
|
||||||
|
try {
|
||||||
|
contract.verify(ltx)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw TransactionVerificationException.ContractRejection(ltx.id, contract, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun contractClassFor(className: ContractClassName, classLoader: ClassLoader): Class<out Contract> {
|
||||||
|
return try {
|
||||||
|
classLoader.loadClass(className).asSubclass(Contract::class.java)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw TransactionVerificationException.ContractCreationError(ltx.id, className, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val Set<ContractAttachment>?.unsigned: ContractAttachment? get() = this?.firstOrNull { !it.isSigned }
|
||||||
|
private val Set<ContractAttachment>?.signed: ContractAttachment? get() = this?.firstOrNull { it.isSigned }
|
||||||
|
}
|
||||||
|
|
||||||
// Stuff that we can't remove and so is deprecated instead
|
// Stuff that we can't remove and so is deprecated instead
|
||||||
//
|
//
|
||||||
@Deprecated("LedgerTransaction should not be created directly, use WireTransaction.toLedgerTransaction instead.")
|
@Deprecated("LedgerTransaction should not be created directly, use WireTransaction.toLedgerTransaction instead.")
|
||||||
|
@ -26,15 +26,15 @@ import kotlin.test.assertFailsWith
|
|||||||
class AttachmentsClassLoaderSerializationTests {
|
class AttachmentsClassLoaderSerializationTests {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val ISOLATED_CONTRACTS_JAR_PATH: URL = AttachmentsClassLoaderSerializationTests::class.java.getResource("isolated.jar")
|
val ISOLATED_CONTRACTS_JAR_PATH: URL = AttachmentsClassLoaderSerializationTests::class.java.getResource("/isolated.jar")
|
||||||
private const val ISOLATED_CONTRACT_CLASS_NAME = "net.corda.finance.contracts.isolated.AnotherDummyContract"
|
private const val ISOLATED_CONTRACT_CLASS_NAME = "net.corda.isolated.contracts.AnotherDummyContract"
|
||||||
}
|
}
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
@JvmField
|
@JvmField
|
||||||
val testSerialization = SerializationEnvironmentRule()
|
val testSerialization = SerializationEnvironmentRule()
|
||||||
|
|
||||||
val storage = MockAttachmentStorage()
|
private val storage = MockAttachmentStorage()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Can serialize and deserialize with an attachment classloader`() {
|
fun `Can serialize and deserialize with an attachment classloader`() {
|
||||||
|
@ -4,35 +4,38 @@ import net.corda.core.contracts.Attachment
|
|||||||
import net.corda.core.contracts.Contract
|
import net.corda.core.contracts.Contract
|
||||||
import net.corda.core.contracts.TransactionVerificationException
|
import net.corda.core.contracts.TransactionVerificationException
|
||||||
import net.corda.core.internal.declaredField
|
import net.corda.core.internal.declaredField
|
||||||
|
import net.corda.core.internal.inputStream
|
||||||
|
import net.corda.core.node.services.AttachmentId
|
||||||
import net.corda.core.serialization.internal.AttachmentsClassLoader
|
import net.corda.core.serialization.internal.AttachmentsClassLoader
|
||||||
import net.corda.testing.core.internal.ContractJarTestUtils.signContractJar
|
import net.corda.testing.core.internal.ContractJarTestUtils.signContractJar
|
||||||
import net.corda.testing.internal.fakeAttachment
|
import net.corda.testing.internal.fakeAttachment
|
||||||
|
import net.corda.testing.node.internal.FINANCE_CONTRACTS_CORDAPP
|
||||||
import net.corda.testing.services.MockAttachmentStorage
|
import net.corda.testing.services.MockAttachmentStorage
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
import org.junit.Assert.assertArrayEquals
|
import org.junit.Assert.assertArrayEquals
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.InputStream
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import kotlin.test.assertFailsWith
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
class AttachmentsClassLoaderTests {
|
class AttachmentsClassLoaderTests {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val ISOLATED_CONTRACTS_JAR_PATH: URL = AttachmentsClassLoaderTests::class.java.getResource("isolated.jar")
|
// TODO Update this test to use the new isolated.jar
|
||||||
|
val ISOLATED_CONTRACTS_JAR_PATH: URL = AttachmentsClassLoaderTests::class.java.getResource("old-isolated.jar")
|
||||||
val ISOLATED_CONTRACTS_JAR_PATH_V4: URL = AttachmentsClassLoaderTests::class.java.getResource("isolated-4.0.jar")
|
val ISOLATED_CONTRACTS_JAR_PATH_V4: URL = AttachmentsClassLoaderTests::class.java.getResource("isolated-4.0.jar")
|
||||||
val FINANCE_JAR_PATH: URL = AttachmentsClassLoaderTests::class.java.getResource("finance.jar")
|
|
||||||
private const val ISOLATED_CONTRACT_CLASS_NAME = "net.corda.finance.contracts.isolated.AnotherDummyContract"
|
private const val ISOLATED_CONTRACT_CLASS_NAME = "net.corda.finance.contracts.isolated.AnotherDummyContract"
|
||||||
|
|
||||||
private fun readAttachment(attachment: Attachment, filepath: String): ByteArray {
|
private fun readAttachment(attachment: Attachment, filepath: String): ByteArray {
|
||||||
ByteArrayOutputStream().use {
|
return ByteArrayOutputStream().let {
|
||||||
attachment.extractFile(filepath, it)
|
attachment.extractFile(filepath, it)
|
||||||
return it.toByteArray()
|
it.toByteArray()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val storage = MockAttachmentStorage()
|
private val storage = MockAttachmentStorage()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Loading AnotherDummyContract without using the AttachmentsClassLoader fails`() {
|
fun `Loading AnotherDummyContract without using the AttachmentsClassLoader fails`() {
|
||||||
@ -43,7 +46,7 @@ class AttachmentsClassLoaderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Dynamically load AnotherDummyContract from isolated contracts jar using the AttachmentsClassLoader`() {
|
fun `Dynamically load AnotherDummyContract from isolated contracts jar using the AttachmentsClassLoader`() {
|
||||||
val isolatedId = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
|
val isolatedId = importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
|
||||||
|
|
||||||
val classloader = AttachmentsClassLoader(listOf(storage.openAttachment(isolatedId)!!))
|
val classloader = AttachmentsClassLoader(listOf(storage.openAttachment(isolatedId)!!))
|
||||||
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classloader)
|
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classloader)
|
||||||
@ -53,8 +56,8 @@ class AttachmentsClassLoaderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Test non-overlapping contract jar`() {
|
fun `Test non-overlapping contract jar`() {
|
||||||
val att1 = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
|
val att1 = importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
|
||||||
val att2 = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH_V4.openStream(), "app", "isolated-4.0.jar")
|
val att2 = importAttachment(ISOLATED_CONTRACTS_JAR_PATH_V4.openStream(), "app", "isolated-4.0.jar")
|
||||||
|
|
||||||
assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) {
|
assertFailsWith(TransactionVerificationException.OverlappingAttachmentsException::class) {
|
||||||
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
||||||
@ -63,9 +66,9 @@ class AttachmentsClassLoaderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Test valid overlapping contract jar`() {
|
fun `Test valid overlapping contract jar`() {
|
||||||
val isolatedId = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
|
val isolatedId = importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
|
||||||
val signedJar = signContractJar(ISOLATED_CONTRACTS_JAR_PATH, copyFirst = true)
|
val signedJar = signContractJar(ISOLATED_CONTRACTS_JAR_PATH, copyFirst = true)
|
||||||
val isolatedSignedId = storage.importAttachment(signedJar.first.toUri().toURL().openStream(), "app", "isolated-signed.jar")
|
val isolatedSignedId = importAttachment(signedJar.first.toUri().toURL().openStream(), "app", "isolated-signed.jar")
|
||||||
|
|
||||||
// does not throw OverlappingAttachments exception
|
// does not throw OverlappingAttachments exception
|
||||||
AttachmentsClassLoader(arrayOf(isolatedId, isolatedSignedId).map { storage.openAttachment(it)!! })
|
AttachmentsClassLoader(arrayOf(isolatedId, isolatedSignedId).map { storage.openAttachment(it)!! })
|
||||||
@ -73,8 +76,8 @@ class AttachmentsClassLoaderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Test non-overlapping different contract jars`() {
|
fun `Test non-overlapping different contract jars`() {
|
||||||
val att1 = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
|
val att1 = importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
|
||||||
val att2 = storage.importAttachment(FINANCE_JAR_PATH.openStream(), "app", "finance.jar")
|
val att2 = importAttachment(FINANCE_CONTRACTS_CORDAPP.jarFile.inputStream(), "app", "finance.jar")
|
||||||
|
|
||||||
// does not throw OverlappingAttachments exception
|
// does not throw OverlappingAttachments exception
|
||||||
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
||||||
@ -82,8 +85,8 @@ class AttachmentsClassLoaderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Load text resources from AttachmentsClassLoader`() {
|
fun `Load text resources from AttachmentsClassLoader`() {
|
||||||
val att1 = storage.importAttachment(fakeAttachment("file1.txt", "some data").inputStream(), "app", "file1.jar")
|
val att1 = importAttachment(fakeAttachment("file1.txt", "some data").inputStream(), "app", "file1.jar")
|
||||||
val att2 = storage.importAttachment(fakeAttachment("file2.txt", "some other data").inputStream(), "app", "file2.jar")
|
val att2 = importAttachment(fakeAttachment("file2.txt", "some other data").inputStream(), "app", "file2.jar")
|
||||||
|
|
||||||
val cl = AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
val cl = AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
||||||
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
|
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
|
||||||
@ -95,8 +98,8 @@ class AttachmentsClassLoaderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Test valid overlapping file condition`() {
|
fun `Test valid overlapping file condition`() {
|
||||||
val att1 = storage.importAttachment(fakeAttachment("file1.txt", "same data").inputStream(), "app", "file1.jar")
|
val att1 = importAttachment(fakeAttachment("file1.txt", "same data").inputStream(), "app", "file1.jar")
|
||||||
val att2 = storage.importAttachment(fakeAttachment("file1.txt", "same data").inputStream(), "app", "file2.jar")
|
val att2 = importAttachment(fakeAttachment("file1.txt", "same data").inputStream(), "app", "file2.jar")
|
||||||
|
|
||||||
val cl = AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
val cl = AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
||||||
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
|
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
|
||||||
@ -106,8 +109,8 @@ class AttachmentsClassLoaderTests {
|
|||||||
@Test
|
@Test
|
||||||
fun `No overlapping exception thrown on certain META-INF files`() {
|
fun `No overlapping exception thrown on certain META-INF files`() {
|
||||||
listOf("meta-inf/manifest.mf", "meta-inf/license", "meta-inf/test.dsa", "meta-inf/test.sf").forEach { path ->
|
listOf("meta-inf/manifest.mf", "meta-inf/license", "meta-inf/test.dsa", "meta-inf/test.sf").forEach { path ->
|
||||||
val att1 = storage.importAttachment(fakeAttachment(path, "some data").inputStream(), "app", "file1.jar")
|
val att1 = importAttachment(fakeAttachment(path, "some data").inputStream(), "app", "file1.jar")
|
||||||
val att2 = storage.importAttachment(fakeAttachment(path, "some other data").inputStream(), "app", "file2.jar")
|
val att2 = importAttachment(fakeAttachment(path, "some other data").inputStream(), "app", "file2.jar")
|
||||||
|
|
||||||
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
||||||
}
|
}
|
||||||
@ -115,10 +118,8 @@ class AttachmentsClassLoaderTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Check platform independent path handling in attachment jars`() {
|
fun `Check platform independent path handling in attachment jars`() {
|
||||||
val storage = MockAttachmentStorage()
|
val att1 = importAttachment(fakeAttachment("/folder1/foldera/file1.txt", "some data").inputStream(), "app", "file1.jar")
|
||||||
|
val att2 = importAttachment(fakeAttachment("\\folder1\\folderb\\file2.txt", "some other data").inputStream(), "app", "file2.jar")
|
||||||
val att1 = storage.importAttachment(fakeAttachment("/folder1/foldera/file1.txt", "some data").inputStream(), "app", "file1.jar")
|
|
||||||
val att2 = storage.importAttachment(fakeAttachment("\\folder1\\folderb\\file2.txt", "some other data").inputStream(), "app", "file2.jar")
|
|
||||||
|
|
||||||
val data1a = readAttachment(storage.openAttachment(att1)!!, "/folder1/foldera/file1.txt")
|
val data1a = readAttachment(storage.openAttachment(att1)!!, "/folder1/foldera/file1.txt")
|
||||||
assertArrayEquals("some data".toByteArray(), data1a)
|
assertArrayEquals("some data".toByteArray(), data1a)
|
||||||
@ -132,4 +133,8 @@ class AttachmentsClassLoaderTests {
|
|||||||
val data2b = readAttachment(storage.openAttachment(att2)!!, "/folder1/folderb/file2.txt")
|
val data2b = readAttachment(storage.openAttachment(att2)!!, "/folder1/folderb/file2.txt")
|
||||||
assertArrayEquals("some other data".toByteArray(), data2b)
|
assertArrayEquals("some other data".toByteArray(), data2b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun importAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId {
|
||||||
|
return jar.use { storage.importAttachment(jar, uploader, filename) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ import net.corda.testing.core.TestIdentity
|
|||||||
import net.corda.testing.internal.rigorousMock
|
import net.corda.testing.internal.rigorousMock
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
import net.corda.testing.node.ledger
|
import net.corda.testing.node.ledger
|
||||||
import org.assertj.core.api.AssertionsForClassTypes
|
import org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@ -330,12 +330,13 @@ class TransactionEncumbranceTests {
|
|||||||
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY2, 0, AutomaticHashConstraint)
|
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY2, 0, AutomaticHashConstraint)
|
||||||
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
|
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
|
||||||
.toLedgerTransaction(ledgerServices)
|
.toLedgerTransaction(ledgerServices)
|
||||||
|
.verify()
|
||||||
}
|
}
|
||||||
|
|
||||||
// More complex encumbrance (full cycle of size 4) where one of the encumbered states is assigned to a different notary.
|
// More complex encumbrance (full cycle of size 4) where one of the encumbered states is assigned to a different notary.
|
||||||
// 0 -> 1, 1 -> 3, 3 -> 2, 2 -> 0
|
// 0 -> 1, 1 -> 3, 3 -> 2, 2 -> 0
|
||||||
// We expect that state at index 3 cannot be encumbered with the state at index 2, due to mismatched notaries.
|
// We expect that state at index 3 cannot be encumbered with the state at index 2, due to mismatched notaries.
|
||||||
AssertionsForClassTypes.assertThatExceptionOfType(TransactionVerificationException.TransactionNotaryMismatchEncumbranceException::class.java)
|
assertThatExceptionOfType(TransactionVerificationException.TransactionNotaryMismatchEncumbranceException::class.java)
|
||||||
.isThrownBy {
|
.isThrownBy {
|
||||||
TransactionBuilder()
|
TransactionBuilder()
|
||||||
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 1, AutomaticHashConstraint)
|
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 1, AutomaticHashConstraint)
|
||||||
@ -344,13 +345,15 @@ class TransactionEncumbranceTests {
|
|||||||
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 2, AutomaticHashConstraint)
|
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 2, AutomaticHashConstraint)
|
||||||
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
|
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
|
||||||
.toLedgerTransaction(ledgerServices)
|
.toLedgerTransaction(ledgerServices)
|
||||||
|
.verify()
|
||||||
}
|
}
|
||||||
.withMessageContaining("index 3 is assigned to notary [O=Notary Service, L=Zurich, C=CH], while its encumbrance with index 2 is assigned to notary [O=Notary Service2, L=Zurich, C=CH]")
|
.withMessageContaining("index 3 is assigned to notary [O=Notary Service, L=Zurich, C=CH], while its encumbrance with " +
|
||||||
|
"index 2 is assigned to notary [O=Notary Service2, L=Zurich, C=CH]")
|
||||||
|
|
||||||
// Two different encumbrance chains, where only one fails due to mismatched notary.
|
// Two different encumbrance chains, where only one fails due to mismatched notary.
|
||||||
// 0 -> 1, 1 -> 0, 2 -> 3, 3 -> 2 where encumbered states with indices 2 and 3, respectively, are assigned
|
// 0 -> 1, 1 -> 0, 2 -> 3, 3 -> 2 where encumbered states with indices 2 and 3, respectively, are assigned
|
||||||
// to different notaries.
|
// to different notaries.
|
||||||
AssertionsForClassTypes.assertThatExceptionOfType(TransactionVerificationException.TransactionNotaryMismatchEncumbranceException::class.java)
|
assertThatExceptionOfType(TransactionVerificationException.TransactionNotaryMismatchEncumbranceException::class.java)
|
||||||
.isThrownBy {
|
.isThrownBy {
|
||||||
TransactionBuilder()
|
TransactionBuilder()
|
||||||
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 1, AutomaticHashConstraint)
|
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY, 1, AutomaticHashConstraint)
|
||||||
@ -359,7 +362,9 @@ class TransactionEncumbranceTests {
|
|||||||
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY2, 2, AutomaticHashConstraint)
|
.addOutputState(stateWithNewOwner, Cash.PROGRAM_ID, DUMMY_NOTARY2, 2, AutomaticHashConstraint)
|
||||||
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
|
.addCommand(Cash.Commands.Issue(), MEGA_CORP.owningKey)
|
||||||
.toLedgerTransaction(ledgerServices)
|
.toLedgerTransaction(ledgerServices)
|
||||||
|
.verify()
|
||||||
}
|
}
|
||||||
.withMessageContaining("index 2 is assigned to notary [O=Notary Service, L=Zurich, C=CH], while its encumbrance with index 3 is assigned to notary [O=Notary Service2, L=Zurich, C=CH]")
|
.withMessageContaining("index 2 is assigned to notary [O=Notary Service, L=Zurich, C=CH], while its encumbrance with " +
|
||||||
|
"index 3 is assigned to notary [O=Notary Service2, L=Zurich, C=CH]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -170,6 +170,7 @@ class TransactionTests {
|
|||||||
val id = SecureHash.randomSHA256()
|
val id = SecureHash.randomSHA256()
|
||||||
val timeWindow: TimeWindow? = null
|
val timeWindow: TimeWindow? = null
|
||||||
val privacySalt = PrivacySalt()
|
val privacySalt = PrivacySalt()
|
||||||
|
|
||||||
fun buildTransaction() = LedgerTransaction.create(
|
fun buildTransaction() = LedgerTransaction.create(
|
||||||
inputs,
|
inputs,
|
||||||
outputs,
|
outputs,
|
||||||
@ -184,7 +185,7 @@ class TransactionTests {
|
|||||||
inputStatesContractClassNameToMaxVersion = emptyMap()
|
inputStatesContractClassNameToMaxVersion = emptyMap()
|
||||||
)
|
)
|
||||||
|
|
||||||
assertFailsWith<TransactionVerificationException.NotaryChangeInWrongTransactionType> { buildTransaction() }
|
assertFailsWith<TransactionVerificationException.NotaryChangeInWrongTransactionType> { buildTransaction().verify() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -6,7 +6,7 @@ import net.corda.core.identity.Party
|
|||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This interface deliberately mirrors the one in the finance:isolated module.
|
* This interface deliberately mirrors the one in the isolated module.
|
||||||
* We will actually link [AnotherDummyContract] against this interface rather
|
* We will actually link [AnotherDummyContract] against this interface rather
|
||||||
* than the one inside isolated.jar, which means we won't need to use reflection
|
* than the one inside isolated.jar, which means we won't need to use reflection
|
||||||
* to execute the contract's generateInitial() method.
|
* to execute the contract's generateInitial() method.
|
||||||
|
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.contracts.*
|
||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
@ -7,8 +7,6 @@ import net.corda.core.transactions.LedgerTransaction
|
|||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.nodeapi.DummyContractBackdoor
|
import net.corda.nodeapi.DummyContractBackdoor
|
||||||
|
|
||||||
const val ANOTHER_DUMMY_PROGRAM_ID = "net.corda.finance.contracts.isolated.AnotherDummyContract"
|
|
||||||
|
|
||||||
@Suppress("UNUSED")
|
@Suppress("UNUSED")
|
||||||
class AnotherDummyContract : Contract, DummyContractBackdoor {
|
class AnotherDummyContract : Contract, DummyContractBackdoor {
|
||||||
val magicString = "helloworld"
|
val magicString = "helloworld"
|
||||||
@ -32,4 +30,8 @@ class AnotherDummyContract : Contract, DummyContractBackdoor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun inspectState(state: ContractState): Int = (state as State).magicNumber
|
override fun inspectState(state: ContractState): Int = (state as State).magicNumber
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ANOTHER_DUMMY_PROGRAM_ID = "net.corda.isolated.contracts.AnotherDummyContract"
|
||||||
|
}
|
||||||
}
|
}
|
@ -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(':client:jfx')
|
||||||
testCompile project(':finance:contracts')
|
testCompile project(':finance:contracts')
|
||||||
testCompile project(':finance:workflows')
|
testCompile project(':finance:workflows')
|
||||||
testCompile project(':finance:isolated')
|
|
||||||
|
|
||||||
// sample test schemas
|
// sample test schemas
|
||||||
testCompile project(path: ':finance:contracts', configuration: 'testArtifacts')
|
testCompile project(path: ':finance:contracts', configuration: 'testArtifacts')
|
||||||
|
@ -1,117 +1,123 @@
|
|||||||
package net.corda.node.services
|
package net.corda.node.services
|
||||||
|
|
||||||
import com.nhaarman.mockito_kotlin.any
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import com.nhaarman.mockito_kotlin.doReturn
|
import net.corda.core.contracts.ContractState
|
||||||
import com.nhaarman.mockito_kotlin.whenever
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.CordaRuntimeException
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.contracts.*
|
|
||||||
import net.corda.core.cordapp.CordappProvider
|
|
||||||
import net.corda.core.flows.FlowLogic
|
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.toLedgerTransaction
|
import net.corda.core.internal.*
|
||||||
import net.corda.core.node.NetworkParameters
|
import net.corda.core.internal.concurrent.transpose
|
||||||
import net.corda.core.node.ServicesForResolution
|
import net.corda.core.messaging.startFlow
|
||||||
import net.corda.core.node.services.AttachmentStorage
|
import net.corda.core.serialization.MissingAttachmentsException
|
||||||
import net.corda.core.node.services.IdentityService
|
|
||||||
import net.corda.core.node.services.NetworkParametersStorage
|
|
||||||
import net.corda.core.serialization.SerializationFactory
|
|
||||||
import net.corda.core.serialization.serialize
|
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.node.VersionInfo
|
import net.corda.core.utilities.unwrap
|
||||||
import net.corda.node.internal.cordapp.CordappProviderImpl
|
import net.corda.testing.common.internal.checkNotOnClasspath
|
||||||
import net.corda.node.internal.cordapp.JarScanningCordappLoader
|
import net.corda.testing.core.*
|
||||||
import net.corda.testing.common.internal.testNetworkParameters
|
import net.corda.testing.driver.DriverDSL
|
||||||
import net.corda.testing.common.internal.addNotary
|
|
||||||
import net.corda.testing.core.DUMMY_BANK_A_NAME
|
|
||||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
|
||||||
import net.corda.testing.core.SerializationEnvironmentRule
|
|
||||||
import net.corda.testing.core.TestIdentity
|
|
||||||
import net.corda.testing.driver.DriverParameters
|
import net.corda.testing.driver.DriverParameters
|
||||||
import net.corda.testing.driver.NodeParameters
|
|
||||||
import net.corda.testing.driver.driver
|
import net.corda.testing.driver.driver
|
||||||
import net.corda.testing.internal.MockCordappConfigProvider
|
import net.corda.testing.node.NotarySpec
|
||||||
import net.corda.testing.internal.rigorousMock
|
|
||||||
import net.corda.testing.internal.withoutTestSerialization
|
|
||||||
import net.corda.testing.node.internal.cordappsForPackages
|
import net.corda.testing.node.internal.cordappsForPackages
|
||||||
import net.corda.testing.services.MockAttachmentStorage
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import java.net.URL
|
||||||
import java.net.URLClassLoader
|
import java.net.URLClassLoader
|
||||||
import kotlin.test.assertFailsWith
|
|
||||||
|
|
||||||
class AttachmentLoadingTests {
|
class AttachmentLoadingTests {
|
||||||
@Rule
|
|
||||||
@JvmField
|
|
||||||
val testSerialization = SerializationEnvironmentRule()
|
|
||||||
private val attachments = MockAttachmentStorage()
|
|
||||||
private val provider = CordappProviderImpl(JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR), VersionInfo.UNKNOWN), MockCordappConfigProvider(), attachments).apply {
|
|
||||||
start(testNetworkParameters().whitelistedContractImplementations)
|
|
||||||
}
|
|
||||||
private val cordapp get() = provider.cordapps.first()
|
|
||||||
private val attachmentId get() = provider.getCordappAttachmentId(cordapp)!!
|
|
||||||
private val appContext get() = provider.getAppContext(cordapp)
|
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
val isolatedJAR = AttachmentLoadingTests::class.java.getResource("isolated.jar")!!
|
val isolatedJar: URL = AttachmentLoadingTests::class.java.getResource("/isolated.jar")
|
||||||
const val ISOLATED_CONTRACT_ID = "net.corda.finance.contracts.isolated.AnotherDummyContract"
|
val isolatedClassLoader = URLClassLoader(arrayOf(isolatedJar))
|
||||||
|
val issuanceFlowClass: Class<FlowLogic<StateRef>> = uncheckedCast(loadFromIsolated("net.corda.isolated.workflows.IsolatedIssuanceFlow"))
|
||||||
|
|
||||||
val bankAName = CordaX500Name("BankA", "Zurich", "CH")
|
init {
|
||||||
val bankBName = CordaX500Name("BankB", "Zurich", "CH")
|
checkNotOnClasspath("net.corda.isolated.contracts.AnotherDummyContract") {
|
||||||
val flowInitiatorClass: Class<out FlowLogic<*>> =
|
"isolated module cannot be on the classpath as otherwise it will be pulled into the nodes the driver creates and " +
|
||||||
Class.forName("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator", true, URLClassLoader(arrayOf(isolatedJAR)))
|
"contaminate the tests. This is a known issue with the driver and we must work around it until it's fixed."
|
||||||
.asSubclass(FlowLogic::class.java)
|
|
||||||
val DUMMY_BANK_A = TestIdentity(DUMMY_BANK_A_NAME, 40).party
|
|
||||||
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
|
||||||
}
|
|
||||||
|
|
||||||
private val services = object : ServicesForResolution {
|
|
||||||
private val testNetworkParameters = testNetworkParameters().addNotary(DUMMY_NOTARY)
|
|
||||||
override fun loadState(stateRef: StateRef): TransactionState<*> = throw NotImplementedError()
|
|
||||||
override fun loadStates(stateRefs: Set<StateRef>): Set<StateAndRef<ContractState>> = throw NotImplementedError()
|
|
||||||
override fun loadContractAttachment(stateRef: StateRef, interestedContractClassName : ContractClassName?): Attachment = throw NotImplementedError()
|
|
||||||
override val identityService = rigorousMock<IdentityService>().apply {
|
|
||||||
doReturn(null).whenever(this).partyFromKey(DUMMY_BANK_A.owningKey)
|
|
||||||
}
|
|
||||||
override val attachments: AttachmentStorage get() = this@AttachmentLoadingTests.attachments
|
|
||||||
override val cordappProvider: CordappProvider get() = this@AttachmentLoadingTests.provider
|
|
||||||
override val networkParameters: NetworkParameters = testNetworkParameters
|
|
||||||
override val networkParametersStorage: NetworkParametersStorage get() = rigorousMock<NetworkParametersStorage>().apply {
|
|
||||||
doReturn(testNetworkParameters.serialize().hash).whenever(this).currentHash
|
|
||||||
doReturn(testNetworkParameters).whenever(this).lookup(any())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test a wire transaction has loaded the correct attachment`() {
|
|
||||||
val appClassLoader = appContext.classLoader
|
|
||||||
val contractClass = appClassLoader.loadClass(ISOLATED_CONTRACT_ID).asSubclass(Contract::class.java)
|
|
||||||
val generateInitialMethod = contractClass.getDeclaredMethod("generateInitial", PartyAndReference::class.java, Integer.TYPE, Party::class.java)
|
|
||||||
val contract = contractClass.newInstance()
|
|
||||||
val txBuilder = generateInitialMethod.invoke(contract, DUMMY_BANK_A.ref(1), 1, DUMMY_NOTARY) as TransactionBuilder
|
|
||||||
val context = SerializationFactory.defaultFactory.defaultContext.withClassLoader(appClassLoader)
|
|
||||||
val ledgerTx = txBuilder.toLedgerTransaction(services, context)
|
|
||||||
contract.verify(ledgerTx)
|
|
||||||
|
|
||||||
val actual = ledgerTx.attachments.first()
|
|
||||||
val expected = attachments.openAttachment(attachmentId)!!
|
|
||||||
assertEquals(expected, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test that attachments retrieved over the network are not used for code`() {
|
|
||||||
withoutTestSerialization {
|
|
||||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptySet())) {
|
|
||||||
val additionalCordapps = cordappsForPackages("net.corda.finance.contracts.isolated")
|
|
||||||
val bankA = startNode(NodeParameters(providedName = bankAName, additionalCordapps = additionalCordapps)).getOrThrow()
|
|
||||||
val bankB = startNode(NodeParameters(providedName = bankBName, additionalCordapps = additionalCordapps)).getOrThrow()
|
|
||||||
assertFailsWith<CordaRuntimeException>("Party C=CH,L=Zurich,O=BankB rejected session request: Don't know net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator") {
|
|
||||||
bankA.rpc.startFlowDynamic(flowInitiatorClass, bankB.nodeInfo.legalIdentities.first()).returnValue.getOrThrow()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Unit
|
}
|
||||||
|
|
||||||
|
fun loadFromIsolated(className: String): Class<*> = Class.forName(className, false, isolatedClassLoader)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `contracts downloaded from the network are not executed without the DJVM`() {
|
||||||
|
driver(DriverParameters(
|
||||||
|
startNodesInProcess = false,
|
||||||
|
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = false)),
|
||||||
|
cordappsForAllNodes = cordappsForPackages(javaClass.packageName)
|
||||||
|
)) {
|
||||||
|
installIsolatedCordapp(ALICE_NAME)
|
||||||
|
|
||||||
|
val (alice, bob) = listOf(
|
||||||
|
startNode(providedName = ALICE_NAME),
|
||||||
|
startNode(providedName = BOB_NAME)
|
||||||
|
).transpose().getOrThrow()
|
||||||
|
|
||||||
|
val stateRef = alice.rpc.startFlowDynamic(issuanceFlowClass, 1234).returnValue.getOrThrow()
|
||||||
|
|
||||||
|
// The exception that we actually want is MissingAttachmentsException, but this is thrown in a responder flow on Bob. To work
|
||||||
|
// around that it's re-thrown as a FlowException so that it can be propagated to Alice where we pick it here.
|
||||||
|
assertThatThrownBy {
|
||||||
|
alice.rpc.startFlow(::ConsumeAndBroadcastFlow, stateRef, bob.nodeInfo.singleIdentity()).returnValue.getOrThrow()
|
||||||
|
}.hasMessage("Attempting to load Contract Attachments downloaded from the network")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@Test
|
||||||
|
fun `contract is executed if installed locally`() {
|
||||||
|
driver(DriverParameters(
|
||||||
|
startNodesInProcess = false,
|
||||||
|
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = false)),
|
||||||
|
cordappsForAllNodes = cordappsForPackages(javaClass.packageName)
|
||||||
|
)) {
|
||||||
|
installIsolatedCordapp(ALICE_NAME)
|
||||||
|
installIsolatedCordapp(BOB_NAME)
|
||||||
|
|
||||||
|
val (alice, bob) = listOf(
|
||||||
|
startNode(providedName = ALICE_NAME),
|
||||||
|
startNode(providedName = BOB_NAME)
|
||||||
|
).transpose().getOrThrow()
|
||||||
|
|
||||||
|
val stateRef = alice.rpc.startFlowDynamic(issuanceFlowClass, 1234).returnValue.getOrThrow()
|
||||||
|
alice.rpc.startFlow(::ConsumeAndBroadcastFlow, stateRef, bob.nodeInfo.singleIdentity()).returnValue.getOrThrow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DriverDSL.installIsolatedCordapp(name: CordaX500Name) {
|
||||||
|
val cordappsDir = (baseDirectory(name) / "cordapps").createDirectories()
|
||||||
|
isolatedJar.toPath().copyToDirectory(cordappsDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitiatingFlow
|
||||||
|
@StartableByRPC
|
||||||
|
class ConsumeAndBroadcastFlow(private val stateRef: StateRef, private val otherSide: Party) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
val notary = serviceHub.networkMapCache.notaryIdentities[0]
|
||||||
|
val stateAndRef = serviceHub.toStateAndRef<ContractState>(stateRef)
|
||||||
|
val stx = serviceHub.signInitialTransaction(
|
||||||
|
TransactionBuilder(notary).addInputState(stateAndRef).addCommand(dummyCommand(ourIdentity.owningKey))
|
||||||
|
)
|
||||||
|
stx.verify(serviceHub, checkSufficientSignatures = false)
|
||||||
|
val session = initiateFlow(otherSide)
|
||||||
|
subFlow(FinalityFlow(stx, session))
|
||||||
|
// It's important we wait on this dummy receive, as otherwise it's possible we miss any errors the other side throws
|
||||||
|
session.receive<String>().unwrap { require(it == "OK") { "Not OK: $it"} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(ConsumeAndBroadcastFlow::class)
|
||||||
|
class ConsumeAndBroadcastResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
try {
|
||||||
|
subFlow(ReceiveFinalityFlow(otherSide))
|
||||||
|
} catch (e: MissingAttachmentsException) {
|
||||||
|
throw FlowException(e.message)
|
||||||
|
}
|
||||||
|
otherSide.send("OK")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
session.save(attachment)
|
||||||
attachmentCount.inc()
|
attachmentCount.inc()
|
||||||
log.info("Stored new attachment $id")
|
log.info("Stored new attachment: id=$id uploader=$uploader filename=$filename")
|
||||||
contractClassNames.forEach { contractsCache.invalidate(it) }
|
contractClassNames.forEach { contractsCache.invalidate(it) }
|
||||||
return@withContractsInJar id
|
return@withContractsInJar id
|
||||||
}
|
}
|
||||||
|
@ -15,10 +15,10 @@ import java.net.URL
|
|||||||
|
|
||||||
class CordappProviderImplTests {
|
class CordappProviderImplTests {
|
||||||
private companion object {
|
private companion object {
|
||||||
val isolatedJAR = this::class.java.getResource("isolated.jar")!!
|
val isolatedJAR: URL = this::class.java.getResource("/isolated.jar")
|
||||||
// TODO: Cordapp name should differ from the JAR name
|
// TODO: Cordapp name should differ from the JAR name
|
||||||
const val isolatedCordappName = "isolated"
|
const val isolatedCordappName = "isolated"
|
||||||
val emptyJAR = this::class.java.getResource("empty.jar")!!
|
val emptyJAR: URL = this::class.java.getResource("empty.jar")
|
||||||
val validConfig: Config = ConfigFactory.parseString("key=value")
|
val validConfig: Config = ConfigFactory.parseString("key=value")
|
||||||
|
|
||||||
val stubConfigProvider = object : CordappConfigProvider {
|
val stubConfigProvider = object : CordappConfigProvider {
|
||||||
@ -52,7 +52,7 @@ class CordappProviderImplTests {
|
|||||||
@Test
|
@Test
|
||||||
fun `test that we find a cordapp class that is loaded into the store`() {
|
fun `test that we find a cordapp class that is loaded into the store`() {
|
||||||
val provider = newCordappProvider(isolatedJAR)
|
val provider = newCordappProvider(isolatedJAR)
|
||||||
val className = "net.corda.finance.contracts.isolated.AnotherDummyContract"
|
val className = "net.corda.isolated.contracts.AnotherDummyContract"
|
||||||
|
|
||||||
val expected = provider.cordapps.first()
|
val expected = provider.cordapps.first()
|
||||||
val actual = provider.getCordappForClass(className)
|
val actual = provider.getCordappForClass(className)
|
||||||
@ -62,9 +62,9 @@ class CordappProviderImplTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test that we find an attachment for a cordapp contrat class`() {
|
fun `test that we find an attachment for a cordapp contract class`() {
|
||||||
val provider = newCordappProvider(isolatedJAR)
|
val provider = newCordappProvider(isolatedJAR)
|
||||||
val className = "net.corda.finance.contracts.isolated.AnotherDummyContract"
|
val className = "net.corda.isolated.contracts.AnotherDummyContract"
|
||||||
val expected = provider.getAppContext(provider.cordapps.first()).attachmentId
|
val expected = provider.getAppContext(provider.cordapps.first()).attachmentId
|
||||||
val actual = provider.getContractAttachmentID(className)
|
val actual = provider.getContractAttachmentID(className)
|
||||||
|
|
||||||
|
@ -36,8 +36,8 @@ class DummyRPCFlow : FlowLogic<Unit>() {
|
|||||||
|
|
||||||
class JarScanningCordappLoaderTest {
|
class JarScanningCordappLoaderTest {
|
||||||
private companion object {
|
private companion object {
|
||||||
const val isolatedContractId = "net.corda.finance.contracts.isolated.AnotherDummyContract"
|
const val isolatedContractId = "net.corda.isolated.contracts.AnotherDummyContract"
|
||||||
const val isolatedFlowName = "net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator"
|
const val isolatedFlowName = "net.corda.isolated.workflows.IsolatedIssuanceFlow"
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -49,15 +49,15 @@ class JarScanningCordappLoaderTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `isolated JAR contains a CorDapp with a contract and plugin`() {
|
fun `isolated JAR contains a CorDapp with a contract and plugin`() {
|
||||||
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("isolated.jar")!!
|
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("/isolated.jar")
|
||||||
val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR))
|
val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR))
|
||||||
|
|
||||||
assertThat(loader.cordapps).hasSize(1)
|
assertThat(loader.cordapps).hasSize(1)
|
||||||
|
|
||||||
val actualCordapp = loader.cordapps.single()
|
val actualCordapp = loader.cordapps.single()
|
||||||
assertThat(actualCordapp.contractClassNames).isEqualTo(listOf(isolatedContractId))
|
assertThat(actualCordapp.contractClassNames).isEqualTo(listOf(isolatedContractId))
|
||||||
assertThat(actualCordapp.initiatedFlows.first().name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Acceptor")
|
assertThat(actualCordapp.initiatedFlows).isEmpty()
|
||||||
assertThat(actualCordapp.rpcFlows).isEmpty()
|
assertThat(actualCordapp.rpcFlows.first().name).isEqualTo(isolatedFlowName)
|
||||||
assertThat(actualCordapp.schedulableFlows).isEmpty()
|
assertThat(actualCordapp.schedulableFlows).isEmpty()
|
||||||
assertThat(actualCordapp.services).isEmpty()
|
assertThat(actualCordapp.services).isEmpty()
|
||||||
assertThat(actualCordapp.serializationWhitelists).hasSize(1)
|
assertThat(actualCordapp.serializationWhitelists).hasSize(1)
|
||||||
@ -83,7 +83,7 @@ class JarScanningCordappLoaderTest {
|
|||||||
// being used internally. Later iterations will use a classloader per cordapp and this test can be retired.
|
// being used internally. Later iterations will use a classloader per cordapp and this test can be retired.
|
||||||
@Test
|
@Test
|
||||||
fun `cordapp classloader can load cordapp classes`() {
|
fun `cordapp classloader can load cordapp classes`() {
|
||||||
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("isolated.jar")!!
|
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("/isolated.jar")
|
||||||
val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR), VersionInfo.UNKNOWN)
|
val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR), VersionInfo.UNKNOWN)
|
||||||
|
|
||||||
loader.appClassLoader.loadClass(isolatedContractId)
|
loader.appClassLoader.loadClass(isolatedContractId)
|
||||||
|
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 emptyListClass = listOf<Any>().javaClass
|
||||||
val emptySetClass = setOf<Any>().javaClass
|
val emptySetClass = setOf<Any>().javaClass
|
||||||
val emptyMapClass = mapOf<Any, Any>().javaClass
|
val emptyMapClass = mapOf<Any, Any>().javaClass
|
||||||
val ISOLATED_CONTRACTS_JAR_PATH: URL = CordaClassResolverTests::class.java.getResource("isolated.jar")
|
val ISOLATED_CONTRACTS_JAR_PATH: URL = CordaClassResolverTests::class.java.getResource("/isolated.jar")
|
||||||
}
|
}
|
||||||
|
|
||||||
private val emptyWhitelistContext: CheckpointSerializationContext = CheckpointSerializationContextImpl(this.javaClass.classLoader, EmptyWhitelist, emptyMap(), true, null)
|
private val emptyWhitelistContext: CheckpointSerializationContext = CheckpointSerializationContextImpl(this.javaClass.classLoader, EmptyWhitelist, emptyMap(), true, null)
|
||||||
private val allButBlacklistedContext: CheckpointSerializationContext = CheckpointSerializationContextImpl(this.javaClass.classLoader, AllButBlacklisted, emptyMap(), true, null)
|
private val allButBlacklistedContext: CheckpointSerializationContext = CheckpointSerializationContextImpl(this.javaClass.classLoader, AllButBlacklisted, emptyMap(), true, null)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Annotation on enum works for specialised entries`() {
|
fun `Annotation on enum works for specialised entries`() {
|
||||||
CordaClassResolver(emptyWhitelistContext).getRegistration(Foo.Bar::class.java)
|
CordaClassResolver(emptyWhitelistContext).getRegistration(Foo.Bar::class.java)
|
||||||
@ -212,7 +213,7 @@ class CordaClassResolverTests {
|
|||||||
val storage = MockAttachmentStorage()
|
val storage = MockAttachmentStorage()
|
||||||
val attachmentHash = importJar(storage)
|
val attachmentHash = importJar(storage)
|
||||||
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! })
|
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! })
|
||||||
val attachedClass = Class.forName("net.corda.finance.contracts.isolated.AnotherDummyContract", true, classLoader)
|
val attachedClass = Class.forName("net.corda.isolated.contracts.AnotherDummyContract", true, classLoader)
|
||||||
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
|
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,7 +222,7 @@ class CordaClassResolverTests {
|
|||||||
val storage = MockAttachmentStorage()
|
val storage = MockAttachmentStorage()
|
||||||
val attachmentHash = importJar(storage, "some_uploader")
|
val attachmentHash = importJar(storage, "some_uploader")
|
||||||
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! })
|
val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! })
|
||||||
val attachedClass = Class.forName("net.corda.finance.contracts.isolated.AnotherDummyContract", true, classLoader)
|
val attachedClass = Class.forName("net.corda.isolated.contracts.AnotherDummyContract", true, classLoader)
|
||||||
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
|
CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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' // maintained for backwards compatibility only
|
||||||
include 'finance:contracts'
|
include 'finance:contracts'
|
||||||
include 'finance:workflows'
|
include 'finance:workflows'
|
||||||
include 'finance:isolated'
|
include 'isolated'
|
||||||
include 'core'
|
include 'core'
|
||||||
include 'docs'
|
include 'docs'
|
||||||
include 'node-api'
|
include 'node-api'
|
||||||
|
@ -49,7 +49,9 @@ data class CustomCordapp(
|
|||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
internal fun packageAsJar(file: Path) {
|
internal fun packageAsJar(file: Path) {
|
||||||
val scanResult = ClassGraph()
|
val classGraph = ClassGraph()
|
||||||
|
classes.forEach { classGraph.addClassLoader(it.classLoader) }
|
||||||
|
val scanResult = classGraph
|
||||||
.whitelistPackages(*packages.toTypedArray())
|
.whitelistPackages(*packages.toTypedArray())
|
||||||
.whitelistClasses(*classes.map { it.name }.toTypedArray())
|
.whitelistClasses(*classes.map { it.name }.toTypedArray())
|
||||||
.scan()
|
.scan()
|
||||||
|
@ -21,7 +21,9 @@ interface TestCordappInternal : TestCordapp {
|
|||||||
fun withOnlyJarContents(): TestCordappInternal
|
fun withOnlyJarContents(): TestCordappInternal
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun installCordapps(baseDirectory: Path, nodeSpecificCordapps: Set<TestCordappInternal>, generalCordapps: Set<TestCordappInternal>) {
|
fun installCordapps(baseDirectory: Path,
|
||||||
|
nodeSpecificCordapps: Set<TestCordappInternal>,
|
||||||
|
generalCordapps: Set<TestCordappInternal> = emptySet()) {
|
||||||
val nodeSpecificCordappsWithoutMeta = checkNoConflicts(nodeSpecificCordapps)
|
val nodeSpecificCordappsWithoutMeta = checkNoConflicts(nodeSpecificCordapps)
|
||||||
checkNoConflicts(generalCordapps)
|
checkNoConflicts(generalCordapps)
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ val FINANCE_CONTRACTS_CORDAPP: TestCordappImpl = findCordapp("net.corda.finance.
|
|||||||
val FINANCE_WORKFLOWS_CORDAPP: TestCordappImpl = findCordapp("net.corda.finance.flows")
|
val FINANCE_WORKFLOWS_CORDAPP: TestCordappImpl = findCordapp("net.corda.finance.flows")
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
val FINANCE_CORDAPPS: List<TestCordappInternal> = listOf(FINANCE_CONTRACTS_CORDAPP, FINANCE_WORKFLOWS_CORDAPP)
|
val FINANCE_CORDAPPS: Set<TestCordappInternal> = setOf(FINANCE_CONTRACTS_CORDAPP, FINANCE_WORKFLOWS_CORDAPP)
|
||||||
|
|
||||||
fun cordappsForPackages(vararg packageNames: String): Set<CustomCordapp> = cordappsForPackages(packageNames.asList())
|
fun cordappsForPackages(vararg packageNames: String): Set<CustomCordapp> = cordappsForPackages(packageNames.asList())
|
||||||
|
|
||||||
|
@ -2,33 +2,16 @@ package net.corda.testing.internal
|
|||||||
|
|
||||||
import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme
|
import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme
|
||||||
import net.corda.core.serialization.internal.SerializationEnvironment
|
import net.corda.core.serialization.internal.SerializationEnvironment
|
||||||
import net.corda.core.serialization.internal._contextSerializationEnv
|
|
||||||
import net.corda.core.serialization.internal._inheritableContextSerializationEnv
|
|
||||||
import net.corda.node.serialization.amqp.AMQPServerSerializationScheme
|
import net.corda.node.serialization.amqp.AMQPServerSerializationScheme
|
||||||
import net.corda.node.serialization.kryo.KRYO_CHECKPOINT_CONTEXT
|
import net.corda.node.serialization.kryo.KRYO_CHECKPOINT_CONTEXT
|
||||||
import net.corda.node.serialization.kryo.KryoCheckpointSerializer
|
import net.corda.node.serialization.kryo.KryoCheckpointSerializer
|
||||||
import net.corda.serialization.internal.*
|
import net.corda.serialization.internal.*
|
||||||
import net.corda.testing.common.internal.asContextEnv
|
import net.corda.testing.common.internal.asContextEnv
|
||||||
import net.corda.testing.core.SerializationEnvironmentRule
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.ExecutorService
|
import java.util.concurrent.ExecutorService
|
||||||
|
|
||||||
val inVMExecutors = ConcurrentHashMap<SerializationEnvironment, ExecutorService>()
|
val inVMExecutors = ConcurrentHashMap<SerializationEnvironment, ExecutorService>()
|
||||||
|
|
||||||
/**
|
|
||||||
* For example your test class uses [SerializationEnvironmentRule] but you want to turn it off for one method.
|
|
||||||
* Use sparingly, ideally a test class shouldn't mix serializers init mechanisms.
|
|
||||||
*/
|
|
||||||
fun <T> withoutTestSerialization(callable: () -> T): T { // TODO: Delete this, see CORDA-858.
|
|
||||||
val (property, env) = listOf(_contextSerializationEnv, _inheritableContextSerializationEnv).map { Pair(it, it.get()) }.single { it.second != null }
|
|
||||||
property.set(null)
|
|
||||||
try {
|
|
||||||
return callable()
|
|
||||||
} finally {
|
|
||||||
property.set(env)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createTestSerializationEnv(): SerializationEnvironment {
|
fun createTestSerializationEnv(): SerializationEnvironment {
|
||||||
val factory = SerializationFactoryImpl().apply {
|
val factory = SerializationFactoryImpl().apply {
|
||||||
registerScheme(AMQPClientSerializationScheme(emptyList()))
|
registerScheme(AMQPClientSerializationScheme(emptyList()))
|
||||||
|
Loading…
Reference in New Issue
Block a user