ENT-4652: Provide an "attachment fixup" mechanism to repair broken transactions. (#5825)

* Do not register cordapp custom serialisers when using attachment classloader.

* Record the URLs of CorDapp JARs that contain custom serialisers. Include these JARs as extra attachments if we discover that we're missing a custom serialiser during transaction verification.

* Check for disabled serializer when explicitly requesting a custom serializer.
Refactor test case to force use of a custom serializer.

* Tidy up basic custom serializer test.

* Also test that TransactionBuilder rejects missing custom serializers.

* Remove test whitelists, which should not be needed with custom serialisers.

* Add changelog entry. Also align TestCordappImpl.findRoots() with OS backports.

* Second approach based around CorDapps inside AttachmentStorage - report missing type descriptor or any non-composable types.

* Initial implementation of Corda-Fixup rules inside a CorDapp jar.

* Replace original "automatic attachment fixing" mechanism completely.

* First review comments: restore "missing class" logic to TransactionBuilder.

* Restore "missing class" mechanism as fallback for SignedTransaction too.
This commit is contained in:
Chris Rankin 2020-01-14 15:18:51 +00:00 committed by Rick Parker
parent 4669a699c0
commit a7147c1ffd
50 changed files with 1144 additions and 191 deletions

View File

@ -3,7 +3,6 @@ package net.corda.core.contracts
import net.corda.core.CordaInternal
import net.corda.core.KeepForDJVM
import net.corda.core.internal.cordapp.CordappImpl.Companion.DEFAULT_CORDAPP_VERSION
import net.corda.core.serialization.CordaSerializable
import java.security.PublicKey
/**

View File

@ -267,6 +267,13 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S
this(txId, "Couldn't find network parameters with hash: $missingNetworkParametersHash related to this transaction: $txId")
}
/**
* @param txId Id of the transaction that Corda is no longer able to verify.
*/
@KeepForDJVM
class BrokenTransactionException(txId: SecureHash, message: String)
: TransactionVerificationException(txId, message, null)
/** Whether the inputs or outputs list contains an encumbrance issue, see [TransactionMissingEncumbranceException]. */
@CordaSerializable
@KeepForDJVM

View File

@ -1,13 +1,16 @@
@file:Suppress("TooManyFunctions")
package net.corda.core.internal
import net.corda.core.DeleteForDJVM
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractClassName
import net.corda.core.cordapp.CordappProvider
import net.corda.core.flows.DataVendingFlow
import net.corda.core.flows.FlowLogic
import net.corda.core.node.NetworkParameters
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.ZoneVersionTooLowException
import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.node.services.vault.AttachmentQueryCriteria
import net.corda.core.node.services.vault.AttachmentSort
@ -109,6 +112,13 @@ fun noPackageOverlap(packages: Collection<String>): Boolean {
return packages.all { outer -> packages.none { inner -> inner != outer && inner.startsWith("$outer.") } }
}
/**
* @return The set of [AttachmentId]s after the node's fix-up rules have been applied to [attachmentIds].
*/
fun CordappProvider.internalFixupAttachmentIds(attachmentIds: Collection<AttachmentId>): Set<AttachmentId> {
return (this as CordappFixupInternal).fixupAttachmentIds(attachmentIds)
}
/**
* Scans trusted (installed locally) attachments to find all that contain the [className].
* This is required as a workaround until explicit cordapp dependencies are implemented.

View File

@ -0,0 +1,9 @@
package net.corda.core.internal
import net.corda.core.DeleteForDJVM
import net.corda.core.node.services.AttachmentId
@DeleteForDJVM
interface CordappFixupInternal {
fun fixupAttachmentIds(attachmentIds: Collection<AttachmentId>): Set<AttachmentId>
}

View File

@ -14,17 +14,13 @@ import java.util.function.Function
@DeleteForDJVM
interface TransactionVerifierServiceInternal {
/**
* Verifies the [transaction] but adds some [extraAttachments] to the classpath.
* Required for transactions built with Corda 3.x that might miss some dependencies due to a bug in that version.
*/
fun verify(transaction: LedgerTransaction, extraAttachments: List<Attachment>): CordaFuture<*>
fun reverifyWithFixups(transaction: LedgerTransaction, missingClass: String?): CordaFuture<*>
}
/**
* Defined here for visibility reasons.
*/
fun LedgerTransaction.prepareVerify(extraAttachments: List<Attachment>) = this.internalPrepareVerify(extraAttachments)
fun LedgerTransaction.prepareVerify(attachments: List<Attachment>) = internalPrepareVerify(attachments)
/**
* Because we create a separate [LedgerTransaction] onto which we need to perform verification, it becomes important we don't verify the

View File

@ -13,6 +13,7 @@ import java.io.InputStream
import java.nio.file.FileAlreadyExistsException
typealias AttachmentId = SecureHash
typealias AttachmentFixup = Pair<Set<AttachmentId>, Set<AttachmentId>>
/**
* An attachment store records potentially large binary objects, identified by their hash.

View File

@ -3,7 +3,6 @@ package net.corda.core.node.services
import net.corda.core.DeleteForDJVM
import net.corda.core.DoNotImplement
import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.Attachment
import net.corda.core.transactions.LedgerTransaction
/**

View File

@ -325,7 +325,6 @@ object AttachmentsClassLoaderBuilder {
val serializers = createInstancesOfClassesImplementing(transactionClassLoader, SerializationCustomSerializer::class.java)
val whitelistedClasses = ServiceLoader.load(SerializationWhitelist::class.java, transactionClassLoader)
.flatMap(SerializationWhitelist::whitelist)
.toList()
// Create a new serializationContext for the current transaction. In this context we will forbid
// deserialization of objects from the future, i.e. disable forwards compatibility. This is to ensure

View File

@ -0,0 +1,24 @@
package net.corda.core.serialization.internal
import net.corda.core.KeepForDJVM
import java.io.NotSerializableException
/**
* Thrown by the serialization framework, probably indicating that a custom serializer
* needs to be included in a transaction.
*/
@KeepForDJVM
open class MissingSerializerException private constructor(
message: String,
val typeDescriptor: String?,
val typeNames: List<String>
) : NotSerializableException(message) {
constructor(message: String, typeDescriptor: String) : this(message, typeDescriptor, emptyList())
constructor(message: String, typeNames: List<String>) : this(message, null, typeNames)
/**
* This constructor allows instances of this exception to escape the DJVM sandbox.
*/
@Suppress("unused")
private constructor(message: String) : this(message, null, emptyList())
}

View File

@ -193,7 +193,7 @@ private constructor(
/**
* Verifies this transaction and runs contract code. At this stage it is assumed that signatures have already been verified.
*
* The contract verification logic is run in a custom classloader created for the current transaction.
* This classloader is only used during verification and does not leak to the client code.
*
@ -204,18 +204,18 @@ private constructor(
*/
@Throws(TransactionVerificationException::class)
fun verify() {
internalPrepareVerify(emptyList()).verify()
internalPrepareVerify(attachments).verify()
}
/**
* This method has to be called in a context where it has access to the database.
*/
@CordaInternal
internal fun internalPrepareVerify(extraAttachments: List<Attachment>): Verifier {
internal fun internalPrepareVerify(txAttachments: List<Attachment>): Verifier {
// Switch thread local deserialization context to using a cached attachments classloader. This classloader enforces various rules
// like no-overlap, package namespace ownership and (in future) deterministic Java.
return AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(
attachments + extraAttachments,
txAttachments,
getParamsWithGoo(),
id,
isAttachmentTrusted = isAttachmentTrusted) { transactionClassLoader ->

View File

@ -10,12 +10,12 @@ import net.corda.core.identity.Party
import net.corda.core.internal.TransactionDeserialisationException
import net.corda.core.internal.TransactionVerifierServiceInternal
import net.corda.core.internal.VisibleForTesting
import net.corda.core.internal.internalFindTrustedAttachmentForClass
import net.corda.core.node.ServiceHub
import net.corda.core.node.ServicesForResolution
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.internal.MissingSerializerException
import net.corda.core.serialization.serialize
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.getOrThrow
@ -228,51 +228,64 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
// TODO: allow non-blocking verification.
services.transactionVerifierService.verify(ltx).getOrThrow()
} catch (e: NoClassDefFoundError) {
if (e.message != null) {
verifyWithExtraDependency(e.message!!, ltx, services, e)
} else {
throw e
}
checkReverifyAllowed(e)
val missingClass = e.message ?: throw e
log.warn("Transaction {} has missing class: {}", ltx.id, missingClass)
reverifyWithFixups(ltx, services, missingClass)
} catch (e: NotSerializableException) {
if (e.cause is ClassNotFoundException && e.cause!!.message != null) {
verifyWithExtraDependency(e.cause!!.message!!.replace(".", "/"), ltx, services, e)
} else {
throw e
}
checkReverifyAllowed(e)
retryVerification(e, e, ltx, services)
} catch (e: TransactionDeserialisationException) {
if (e.cause is NotSerializableException && e.cause.cause is ClassNotFoundException && e.cause.cause!!.message != null) {
verifyWithExtraDependency(e.cause.cause!!.message!!.replace(".", "/"), ltx, services, e)
} else {
throw e
checkReverifyAllowed(e)
retryVerification(e.cause, e, ltx, services)
}
}
private fun checkReverifyAllowed(ex: Throwable) {
// If that transaction was created with and after Corda 4 then just fail.
// The lenient dependency verification is only supported for Corda 3 transactions.
// To detect if the transaction was created before Corda 4 we check if the transaction has the NetworkParameters component group.
if (networkParametersHash != null) {
log.warn("TRANSACTION VERIFY FAILED - No attempt to auto-repair as TX is Corda 4+")
throw ex
}
}
@DeleteForDJVM
@Suppress("ThrowsCount")
private fun retryVerification(cause: Throwable?, ex: Throwable, ltx: LedgerTransaction, services: ServiceHub) {
when (cause) {
is MissingSerializerException -> {
log.warn("Missing serializers: typeDescriptor={}, typeNames={}", cause.typeDescriptor ?: "<unknown>", cause.typeNames)
reverifyWithFixups(ltx, services, null)
}
is NotSerializableException -> {
val underlying = cause.cause
if (underlying is ClassNotFoundException) {
val missingClass = underlying.message?.replace('.', '/') ?: throw ex
log.warn("Transaction {} has missing class: {}", ltx.id, missingClass)
reverifyWithFixups(ltx, services, missingClass)
} else {
throw ex
}
}
else -> throw ex
}
}
// Transactions created before Corda 4 can be missing dependencies on other CorDapps.
// This code attempts to find the missing dependency in the attachment storage among the trusted attachments.
// When it finds one, it instructs the verifier to use it to create the transaction classloader.
private fun verifyWithExtraDependency(missingClass: String, ltx: LedgerTransaction, services: ServiceHub, exception: Throwable) {
// If that transaction was created with and after Corda 4 then just fail.
// The lenient dependency verification is only supported for Corda 3 transactions.
// To detect if the transaction was created before Corda 4 we check if the transaction has the NetworkParameters component group.
if (this.networkParametersHash != null) {
throw exception
}
val attachment = requireNotNull(services.attachments.internalFindTrustedAttachmentForClass(missingClass)) {
"""Transaction $ltx is incorrectly formed. Most likely it was created during version 3 of Corda when the verification logic was more lenient.
|Attempted to find local dependency for class: $missingClass, but could not find one.
|If you wish to verify this transaction, please contact the originator of the transaction and install the provided missing JAR.
|You can install it using the RPC command: `uploadAttachment` without restarting the node.
|""".trimMargin()
}
log.warn("""Detected that transaction ${this.id} does not contain all cordapp dependencies.
// This code has detected a missing custom serializer - probably located inside a workflow CorDapp.
// We need to extract this CorDapp from AttachmentStorage and try verifying this transaction again.
@DeleteForDJVM
private fun reverifyWithFixups(ltx: LedgerTransaction, services: ServiceHub, missingClass: String?) {
log.warn("""Detected that transaction $id does not contain all cordapp dependencies.
|This may be the result of a bug in a previous version of Corda.
|Attempting to verify using the additional trusted dependency: $attachment for class $missingClass.
|Attempting to re-verify having applied this node's fix-up rules.
|Please check with the originator that this is a valid transaction.""".trimMargin())
(services.transactionVerifierService as TransactionVerifierServiceInternal).verify(ltx, listOf(attachment)).getOrThrow()
(services.transactionVerifierService as TransactionVerifierServiceInternal)
.reverifyWithFixups(ltx, missingClass)
.getOrThrow()
}
/**

View File

@ -1,3 +1,4 @@
@file:Suppress("ThrowsCount", "ComplexMethod")
package net.corda.core.transactions
import co.paralleluniverse.strands.Strand
@ -5,7 +6,6 @@ import net.corda.core.CordaInternal
import net.corda.core.DeleteForDJVM
import net.corda.core.contracts.*
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignableData
import net.corda.core.crypto.SignatureMetadata
import net.corda.core.identity.Party
@ -47,7 +47,7 @@ open class TransactionBuilder(
var notary: Party? = null,
var lockId: UUID = defaultLockId(),
protected val inputs: MutableList<StateRef> = arrayListOf(),
protected val attachments: MutableList<SecureHash> = arrayListOf(),
protected val attachments: MutableList<AttachmentId> = arrayListOf(),
protected val outputs: MutableList<TransactionState<ContractState>> = arrayListOf(),
protected val commands: MutableList<Command<*>> = arrayListOf(),
protected var window: TimeWindow? = null,
@ -58,7 +58,7 @@ open class TransactionBuilder(
constructor(notary: Party? = null,
lockId: UUID = defaultLockId(),
inputs: MutableList<StateRef> = arrayListOf(),
attachments: MutableList<SecureHash> = arrayListOf(),
attachments: MutableList<AttachmentId> = arrayListOf(),
outputs: MutableList<TransactionState<ContractState>> = arrayListOf(),
commands: MutableList<Command<*>> = arrayListOf(),
window: TimeWindow? = null,
@ -70,14 +70,23 @@ open class TransactionBuilder(
private companion object {
private fun defaultLockId() = (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID()
private val log = contextLogger()
private val MISSING_CLASS_DISABLED = java.lang.Boolean.getBoolean("net.corda.transactionbuilder.missingclass.disabled")
private const val ID_PATTERN = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*"
private val FQCP: Pattern = Pattern.compile("$ID_PATTERN(/$ID_PATTERN)+")
private fun isValidJavaClass(identifier: String) = FQCP.matcher(identifier).matches()
private fun Collection<*>.deepEquals(other: Collection<*>): Boolean {
return (size == other.size) && containsAll(other) && other.containsAll(this)
}
private fun Collection<AttachmentId>.toPrettyString(): String = sorted().joinToString(
separator = System.lineSeparator(),
prefix = System.lineSeparator()
)
}
private val inputsWithTransactionState = arrayListOf<StateAndRef<ContractState>>()
private val referencesWithTransactionState = arrayListOf<TransactionState<ContractState>>()
private val excludedAttachments = arrayListOf<AttachmentId>()
/**
* Creates a copy of the builder.
@ -106,7 +115,7 @@ open class TransactionBuilder(
when (t) {
is StateAndRef<*> -> addInputState(t)
is ReferencedStateAndRef<*> -> addReferenceState(t)
is SecureHash -> addAttachment(t)
is AttachmentId -> addAttachment(t)
is TransactionState<*> -> addOutputState(t)
is StateAndContract -> addOutputState(t.state, t.contract)
is ContractState -> throw UnsupportedOperationException("Removed as of V1: please use a StateAndContract instead")
@ -128,16 +137,26 @@ open class TransactionBuilder(
* @throws ZoneVersionTooLowException if there are reference states and the zone minimum platform version is less than 4.
*/
@Throws(MissingContractAttachments::class)
fun toWireTransaction(services: ServicesForResolution): WireTransaction = toWireTransactionWithContext(services)
fun toWireTransaction(services: ServicesForResolution): WireTransaction = toWireTransactionWithContext(services, null)
@CordaInternal
internal fun toWireTransactionWithContext(services: ServicesForResolution, serializationContext: SerializationContext? = null): WireTransaction {
internal fun toWireTransactionWithContext(
services: ServicesForResolution,
serializationContext: SerializationContext?
) : WireTransaction = toWireTransactionWithContext(services, serializationContext, 0)
private tailrec fun toWireTransactionWithContext(
services: ServicesForResolution,
serializationContext: SerializationContext?,
tryCount: Int
): WireTransaction {
val referenceStates = referenceStates()
if (referenceStates.isNotEmpty()) {
services.ensureMinimumPlatformVersion(4, "Reference states")
}
val (allContractAttachments: Collection<SecureHash>, resolvedOutputs: List<TransactionState<ContractState>>) = selectContractAttachmentsAndOutputStateConstraints(services, serializationContext)
val (allContractAttachments: Collection<AttachmentId>, resolvedOutputs: List<TransactionState<ContractState>>)
= selectContractAttachmentsAndOutputStateConstraints(services, serializationContext)
// Final sanity check that all states have the correct constraints.
for (state in (inputsWithTransactionState.map { it.state } + resolvedOutputs)) {
@ -150,7 +169,8 @@ open class TransactionBuilder(
inputStates(),
resolvedOutputs,
commands(),
(allContractAttachments + attachments).toSortedSet().toList(), // Sort the attachments to ensure transaction builds are stable.
// Sort the attachments to ensure transaction builds are stable.
((allContractAttachments + attachments).toSortedSet() - excludedAttachments).toList(),
notary,
window,
referenceStates,
@ -163,10 +183,10 @@ open class TransactionBuilder(
// This is a workaround as the current version of Corda does not support cordapp dependencies.
// It works by running transaction validation and then scan the attachment storage for missing classes.
// TODO - remove once proper support for cordapp dependencies is added.
val addedDependency = addMissingDependency(services, wireTx)
val addedDependency = addMissingDependency(services, wireTx, tryCount)
return if (addedDependency)
toWireTransactionWithContext(services, serializationContext)
toWireTransactionWithContext(services, serializationContext, tryCount + 1)
else
wireTx
}
@ -181,7 +201,7 @@ open class TransactionBuilder(
/**
* @return true if a new dependency was successfully added.
*/
private fun addMissingDependency(services: ServicesForResolution, wireTx: WireTransaction): Boolean {
private fun addMissingDependency(services: ServicesForResolution, wireTx: WireTransaction, tryCount: Int): Boolean {
return try {
wireTx.toLedgerTransaction(services).verify()
// The transaction verified successfully without adding any extra dependency.
@ -192,8 +212,14 @@ open class TransactionBuilder(
when {
// Handle various exceptions that can be thrown during verification and drill down the wrappings.
// Note: this is a best effort to preserve backwards compatibility.
rootError is ClassNotFoundException -> addMissingAttachment((rootError.message ?: throw e).replace(".", "/"), services, e)
rootError is NoClassDefFoundError -> addMissingAttachment(rootError.message ?: throw e, services, e)
rootError is ClassNotFoundException -> {
((tryCount == 0) && fixupAttachments(wireTx.attachments, services, e))
|| addMissingAttachment((rootError.message ?: throw e).replace('.', '/'), services, e)
}
rootError is NoClassDefFoundError -> {
((tryCount == 0) && fixupAttachments(wireTx.attachments, services, e))
|| addMissingAttachment(rootError.message ?: throw e, services, e)
}
// Ignore these exceptions as they will break unit tests.
// The point here is only to detect missing dependencies. The other exceptions are irrelevant.
@ -214,10 +240,48 @@ open class TransactionBuilder(
}
}
private fun fixupAttachments(
txAttachments: List<AttachmentId>,
services: ServicesForResolution,
originalException: Throwable
): Boolean {
val replacementAttachments = services.cordappProvider.internalFixupAttachmentIds(txAttachments)
if (replacementAttachments.deepEquals(txAttachments)) {
return false
}
val extraAttachments = replacementAttachments - txAttachments
extraAttachments.forEach { id ->
val attachment = services.attachments.openAttachment(id)
if (attachment == null || !attachment.isUploaderTrusted()) {
log.warn("""The node's fix-up rules suggest including attachment {}, which cannot be found either.
|Please contact the developer of the CorDapp for further instructions.
|""".trimMargin(), id)
throw originalException
}
}
attachments.addAll(extraAttachments)
with(excludedAttachments) {
clear()
addAll(txAttachments - replacementAttachments)
}
log.warn("Attempting to rebuild transaction with these extra attachments:{}{}and these attachments removed:{}",
extraAttachments.toPrettyString(),
System.lineSeparator(),
excludedAttachments.toPrettyString()
)
return true
}
private fun addMissingAttachment(missingClass: String, services: ServicesForResolution, originalException: Throwable): Boolean {
if (!isValidJavaClass(missingClass)) {
log.warn("Could not autodetect a valid attachment for the transaction being built.")
throw originalException
} else if (MISSING_CLASS_DISABLED) {
log.warn("BROKEN TRANSACTION, BUT AUTOMATIC DETECTION OF {} IS DISABLED!", missingClass)
throw originalException
}
val attachment = services.attachments.internalFindTrustedAttachmentForClass(missingClass)
@ -240,19 +304,21 @@ open class TransactionBuilder(
}
/**
* This method is responsible for selecting the contract versions to be used for the current transaction and resolve the output state [AutomaticPlaceholderConstraint]s.
* The contract attachments are used to create a deterministic Classloader to deserialise the transaction and to run the contract verification.
* This method is responsible for selecting the contract versions to be used for the current transaction and resolve the output state
* [AutomaticPlaceholderConstraint]s. The contract attachments are used to create a deterministic Classloader to deserialise the
* transaction and to run the contract verification.
*
* The selection logic depends on the Attachment Constraints of the input, output and reference states, also on the explicitly set attachments.
* The selection logic depends on the Attachment Constraints of the input, output and reference states, also on the explicitly
* set attachments.
* TODO also on the versions of the attachments of the transactions generating the input states. ( after we add versioning)
*/
private fun selectContractAttachmentsAndOutputStateConstraints(
services: ServicesForResolution,
@Suppress("UNUSED_PARAMETER") serializationContext: SerializationContext?
): Pair<Collection<SecureHash>, List<TransactionState<ContractState>>> {
): Pair<Collection<AttachmentId>, List<TransactionState<ContractState>>> {
// Determine the explicitly set contract attachments.
val explicitAttachmentContracts: List<Pair<ContractClassName, SecureHash>> = this.attachments
val explicitAttachmentContracts: List<Pair<ContractClassName, AttachmentId>> = this.attachments
.map(services.attachments::openAttachment)
.mapNotNull { it as? ContractAttachment }
.flatMap { attch ->
@ -260,9 +326,12 @@ open class TransactionBuilder(
}
// And fail early if there's more than 1 for a contract.
require(explicitAttachmentContracts.isEmpty() || explicitAttachmentContracts.groupBy { (ctr, _) -> ctr }.all { (_, groups) -> groups.size == 1 }) { "Multiple attachments set for the same contract." }
require(explicitAttachmentContracts.isEmpty()
|| explicitAttachmentContracts.groupBy { (ctr, _) -> ctr }.all { (_, groups) -> groups.size == 1 }) {
"Multiple attachments set for the same contract."
}
val explicitAttachmentContractsMap: Map<ContractClassName, SecureHash> = explicitAttachmentContracts.toMap()
val explicitAttachmentContractsMap: Map<ContractClassName, AttachmentId> = explicitAttachmentContracts.toMap()
val inputContractGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = inputsWithTransactionState.map { it.state }
.groupBy { it.contract }
@ -272,7 +341,8 @@ open class TransactionBuilder(
// Handle reference states.
// Filter out all contracts that might have been already used by 'normal' input or output states.
val referenceStateGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = referencesWithTransactionState.groupBy { it.contract }
val referenceStateGroups: Map<ContractClassName, List<TransactionState<ContractState>>>
= referencesWithTransactionState.groupBy { it.contract }
val refStateContractAttachments: List<AttachmentId> = referenceStateGroups
.filterNot { it.key in allContracts }
.map { refStateEntry ->
@ -659,7 +729,7 @@ open class TransactionBuilder(
}
/** Adds an attachment with the specified hash to the TransactionBuilder. */
fun addAttachment(attachmentId: SecureHash) = apply {
fun addAttachment(attachmentId: AttachmentId) = apply {
attachments.add(attachmentId)
}
@ -750,7 +820,7 @@ open class TransactionBuilder(
fun referenceStates(): List<StateRef> = ArrayList(references)
/** Returns an immutable list of attachment hashes. */
fun attachments(): List<SecureHash> = ArrayList(attachments)
fun attachments(): List<AttachmentId> = ArrayList(attachments)
/** Returns an immutable list of output [TransactionState]s. */
fun outputStates(): List<TransactionState<*>> = ArrayList(outputs)

View File

@ -7,6 +7,11 @@ release, see :doc:`app-upgrade-notes`.
Unreleased
----------
* Custom serializer classes implementing ``SerializationCustomSerializer`` should ideally be packaged in the same CorDapp as the
contract classes. Corda 4 could therefore fail to verify transactions created with Corda 3 if their custom serializer classes
had been packaged somewhere else. Add a "fallback mechanism" to Corda's transaction verification logic which will attempt to
include any missing custom serializers from other CorDapps within ``AttachmentStorage``.
* ``AppServiceHub`` been extended to provide access to ``database`` which will enable the Service class to perform DB transactions
from the threads managed by the custom Service.

View File

@ -0,0 +1,38 @@
package net.corda.contracts.fixup.dependent
import net.corda.core.contracts.CommandData
import net.corda.core.contracts.Contract
import net.corda.core.contracts.ContractState
import net.corda.core.identity.AbstractParty
import net.corda.core.transactions.LedgerTransaction
class DependentContract : Contract {
companion object {
const val MAX_BEANS = 1000L
}
override fun verify(tx: LedgerTransaction) {
val states = tx.outputsOfType<State>()
require(states.isNotEmpty()) {
"Requires at least one dependent data state"
}
states.forEach { state ->
require(state.dependentData in DependentData(0)..DependentData(MAX_BEANS)) {
"Invalid data: $state"
}
}
}
@Suppress("CanBeParameter", "MemberVisibilityCanBePrivate")
class State(val owner: AbstractParty, val dependentData: DependentData) : ContractState {
override val participants: List<AbstractParty> = listOf(owner)
@Override
override fun toString(): String {
return dependentData.toString()
}
}
class Operate : CommandData
}

View File

@ -0,0 +1,11 @@
package net.corda.contracts.fixup.dependent
data class DependentData(val value: Long) : Comparable<DependentData> {
override fun toString(): String {
return "$value beans"
}
override fun compareTo(other: DependentData): Int {
return value.compareTo(other.value)
}
}

View File

@ -0,0 +1,11 @@
package net.corda.contracts.fixup.dependent
import net.corda.core.serialization.SerializationCustomSerializer
@Suppress("unused")
class DependentDataSerializer : SerializationCustomSerializer<DependentData, DependentDataSerializer.Proxy> {
data class Proxy(val value: Long)
override fun fromProxy(proxy: Proxy): DependentData = DependentData(proxy.value)
override fun toProxy(obj: DependentData) = Proxy(obj.value)
}

View File

@ -0,0 +1,16 @@
package net.corda.contracts.fixup.dependent
import net.corda.contracts.fixup.standalone.StandAloneData
import net.corda.core.serialization.SerializationCustomSerializer
/**
* This custom serializer has DELIBERATELY been added to the dependent CorDapp
* in order to make it DEPENDENT on the classes inside the standalone CorDapp.
*/
@Suppress("unused")
class StandAloneDataSerializer : SerializationCustomSerializer<StandAloneData, StandAloneDataSerializer.Proxy> {
data class Proxy(val value: Long)
override fun fromProxy(proxy: Proxy): StandAloneData = StandAloneData(proxy.value)
override fun toProxy(obj: StandAloneData) = Proxy(obj.value)
}

View File

@ -0,0 +1,16 @@
package net.corda.contracts.fixup.standalone
import net.corda.core.contracts.Contract
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.transactions.LedgerTransaction
/**
* Add a [Contract] to this CorDapp so that the Node will
* automatically upload this CorDapp into [AttachmentStorage].
*/
@Suppress("unused")
class StandAloneContract : Contract {
override fun verify(tx: LedgerTransaction) {
throw UnsupportedOperationException("Dummy contract - not used")
}
}

View File

@ -0,0 +1,11 @@
package net.corda.contracts.fixup.standalone
data class StandAloneData(val value: Long) : Comparable<StandAloneData> {
override fun toString(): String {
return "$value pods"
}
override fun compareTo(other: StandAloneData): Int {
return value.compareTo(other.value)
}
}

View File

@ -9,18 +9,18 @@ import net.corda.core.transactions.LedgerTransaction
@Suppress("unused")
class CustomSerializerContract : Contract {
companion object {
const val MAX_CURRANT = 2000
const val MAX_CURRANT = 2000L
}
override fun verify(tx: LedgerTransaction) {
val currantsyData = tx.outputsOfType(CurrantsyState::class.java)
val currantsyData = tx.outputsOfType<CurrantsyState>()
require(currantsyData.isNotEmpty()) {
"Requires at least one currantsy state"
}
currantsyData.forEach {
require(it.currantsy.currants in 0..MAX_CURRANT) {
"Too many currants! ${it.currantsy.currants} is unraisinable!"
require(it.currantsy in Currantsy(0)..Currantsy(MAX_CURRANT)) {
"Too many currants! $it is unraisinable!"
}
}
}

View File

@ -1,7 +0,0 @@
package net.corda.contracts.serialization.custom
import net.corda.core.serialization.SerializationWhitelist
class CustomSerializerRegistry : SerializationWhitelist {
override val whitelist: List<Class<*>> = listOf(Currantsy::class.java)
}

View File

@ -0,0 +1,19 @@
package net.corda.contracts.serialization.missing
/**
* This class REQUIRES a custom serializer because its
* constructor parameter cannot be mapped to a property
* automatically. THIS IS DELIBERATE!
*/
class CustomData(initialValue: Long) : Comparable<CustomData> {
// DO NOT MOVE THIS PROPERTY INTO THE CONSTRUCTOR!
val value: Long = initialValue
override fun toString(): String {
return "$value bobbins"
}
override fun compareTo(other: CustomData): Int {
return value.compareTo(other.value)
}
}

View File

@ -0,0 +1,39 @@
package net.corda.contracts.serialization.missing
import net.corda.core.contracts.CommandData
import net.corda.core.contracts.Contract
import net.corda.core.contracts.ContractState
import net.corda.core.identity.AbstractParty
import net.corda.core.transactions.LedgerTransaction
@Suppress("unused")
class MissingSerializerContract : Contract {
companion object {
const val MAX_VALUE = 2000L
}
override fun verify(tx: LedgerTransaction) {
val states = tx.outputsOfType<CustomDataState>()
require(states.isNotEmpty()) {
"Requires at least one custom data state"
}
states.forEach {
require(it.customData in CustomData(0)..CustomData(MAX_VALUE)) {
"CustomData $it exceeds maximum value!"
}
}
}
@Suppress("CanBeParameter", "MemberVisibilityCanBePrivate")
class CustomDataState(val owner: AbstractParty, val customData: CustomData) : ContractState {
override val participants: List<AbstractParty> = listOf(owner)
@Override
override fun toString(): String {
return customData.toString()
}
}
class Operate : CommandData
}

View File

@ -0,0 +1,28 @@
package net.corda.flows.fixup
import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.fixup.dependent.DependentContract.Operate
import net.corda.contracts.fixup.dependent.DependentContract.State
import net.corda.contracts.fixup.dependent.DependentData
import net.corda.core.contracts.Command
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.transactions.TransactionBuilder
@InitiatingFlow
@StartableByRPC
class CordappFixupFlow(private val data: DependentData) : FlowLogic<SecureHash>() {
@Suspendable
override fun call(): SecureHash {
val notary = serviceHub.networkMapCache.notaryIdentities[0]
val stx = serviceHub.signInitialTransaction(
TransactionBuilder(notary)
.addOutputState(State(ourIdentity, data))
.addCommand(Command(Operate(), ourIdentity.owningKey))
)
stx.verify(serviceHub, checkSufficientSignatures = false)
return stx.id
}
}

View File

@ -0,0 +1,12 @@
package net.corda.flows.serialization.missing
import net.corda.contracts.serialization.missing.CustomData
import net.corda.core.serialization.SerializationCustomSerializer
@Suppress("unused")
class CustomDataSerializer : SerializationCustomSerializer<CustomData, CustomDataSerializer.Proxy> {
data class Proxy(val value: Long)
override fun fromProxy(proxy: Proxy): CustomData = CustomData(proxy.value)
override fun toProxy(obj: CustomData) = Proxy(obj.value)
}

View File

@ -0,0 +1,28 @@
package net.corda.flows.serialization.missing
import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.serialization.missing.CustomData
import net.corda.contracts.serialization.missing.MissingSerializerContract.CustomDataState
import net.corda.contracts.serialization.missing.MissingSerializerContract.Operate
import net.corda.core.contracts.Command
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.transactions.TransactionBuilder
@InitiatingFlow
@StartableByRPC
class MissingSerializerBuilderFlow(private val value: Long) : FlowLogic<SecureHash>() {
@Suspendable
override fun call(): SecureHash {
val notary = serviceHub.networkMapCache.notaryIdentities[0]
val stx = serviceHub.signInitialTransaction(
TransactionBuilder(notary)
.addOutputState(CustomDataState(ourIdentity, CustomData(value)))
.addCommand(Command(Operate(), ourIdentity.owningKey))
)
stx.verify(serviceHub, checkSufficientSignatures = false)
return stx.id
}
}

View File

@ -0,0 +1,56 @@
package net.corda.flows.serialization.missing
import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.serialization.missing.CustomData
import net.corda.contracts.serialization.missing.MissingSerializerContract.CustomDataState
import net.corda.contracts.serialization.missing.MissingSerializerContract.Operate
import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint
import net.corda.core.contracts.Command
import net.corda.core.contracts.TransactionState
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignableData
import net.corda.core.crypto.SignatureMetadata
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.internal.createComponentGroups
import net.corda.core.internal.requiredContractClassName
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
@InitiatingFlow
@StartableByRPC
class MissingSerializerFlow(private val value: Long) : FlowLogic<SecureHash>() {
@Suspendable
override fun call(): SecureHash {
val notary = serviceHub.networkMapCache.notaryIdentities[0]
val legalIdentityKey = serviceHub.myInfo.legalIdentitiesAndCerts.first().owningKey
val customDataState = CustomDataState(ourIdentity, CustomData(value))
val wtx = WireTransaction(createComponentGroups(
inputs = emptyList(),
outputs = listOf(TransactionState(
data = customDataState,
notary = notary,
constraint = AlwaysAcceptAttachmentConstraint
)),
notary = notary,
commands = listOf(Command(Operate(), ourIdentity.owningKey)),
attachments = serviceHub.attachments.getLatestContractAttachments(customDataState.requiredContractClassName!!),
timeWindow = null,
references = emptyList(),
networkParametersHash = null
))
val signatureMetadata = SignatureMetadata(
platformVersion = serviceHub.myInfo.platformVersion,
schemeNumberID = Crypto.findSignatureScheme(legalIdentityKey).schemeNumberID
)
val signableData = SignableData(wtx.id, signatureMetadata)
val sig = serviceHub.keyManagementService.sign(signableData, legalIdentityKey)
return with(SignedTransaction(wtx, listOf(sig))) {
verify(serviceHub, checkSufficientSignatures = false)
id
}
}
}

View File

@ -0,0 +1,14 @@
@file:JvmName("Assertions")
package net.corda.node
import net.corda.core.serialization.CordaSerializable
import org.junit.Assert.assertNull
inline fun <reified T> assertNotCordaSerializable() {
assertNotCordaSerializable(T::class.java)
}
fun assertNotCordaSerializable(clazz: Class<*>) {
assertNull("$clazz must NOT be annotated as @CordaSerializable!",
clazz.getAnnotation(CordaSerializable::class.java))
}

View File

@ -0,0 +1,98 @@
package net.corda.node
import net.corda.client.rpc.CordaRPCClient
import net.corda.contracts.fixup.dependent.DependentData
import net.corda.contracts.fixup.standalone.StandAloneData
import net.corda.core.CordaRuntimeException
import net.corda.core.contracts.TransactionVerificationException.ContractRejection
import net.corda.core.internal.hash
import net.corda.core.messaging.startFlow
import net.corda.core.utilities.getOrThrow
import net.corda.flows.fixup.CordappFixupFlow
import net.corda.node.services.Permissions
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.driver
import net.corda.testing.driver.internal.incrementalPortAllocation
import net.corda.testing.node.NotarySpec
import net.corda.testing.node.TestCordapp
import net.corda.testing.node.User
import net.corda.testing.node.internal.cordappWithFixups
import net.corda.testing.node.internal.cordappWithPackages
import org.assertj.core.api.Assertions.assertThat
import org.junit.BeforeClass
import org.junit.Test
import kotlin.test.assertFailsWith
@Suppress("FunctionName")
class ContractWithCordappFixupTest {
companion object {
const val BEANS = 10001L
val user = User("u", "p", setOf(Permissions.all()))
val flowCorDapp = cordappWithPackages("net.corda.flows.fixup").signed()
val dependentContractCorDapp = cordappWithPackages("net.corda.contracts.fixup.dependent").signed()
val standaloneContractCorDapp = cordappWithPackages("net.corda.contracts.fixup.standalone").signed()
fun driverParameters(cordapps: List<TestCordapp>): DriverParameters {
return DriverParameters(
portAllocation = incrementalPortAllocation(),
startNodesInProcess = false,
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = true)),
cordappsForAllNodes = cordapps,
systemProperties = mapOf("net.corda.transactionbuilder.missingclass.disabled" to true.toString())
)
}
@BeforeClass
@JvmStatic
fun checkData() {
assertNotCordaSerializable<DependentData>()
assertNotCordaSerializable<StandAloneData>()
}
}
/*
* Test that we can still build a transaction for a CorDapp with an implicit dependency.
*/
@Test
fun `flow with missing cordapp dependency with fixup`() {
val dependentContractId = dependentContractCorDapp.jarFile.hash
val standaloneContractId = standaloneContractCorDapp.jarFile.hash
val fixupCorDapp = cordappWithFixups(listOf(
setOf(dependentContractId) to setOf(dependentContractId, standaloneContractId)
)).signed()
val data = DependentData(BEANS)
driver(driverParameters(listOf(flowCorDapp, dependentContractCorDapp, standaloneContractCorDapp, fixupCorDapp))) {
val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
val ex = assertFailsWith<ContractRejection> {
CordaRPCClient(hostAndPort = alice.rpcAddress)
.start(user.username, user.password)
.use { client ->
client.proxy.startFlow(::CordappFixupFlow, data).returnValue.getOrThrow()
}
}
assertThat(ex).hasMessageContaining("Invalid data: $data")
}
}
/**
* Test that our dependency is indeed missing and so requires fixing up.
*/
@Test
fun `flow with missing cordapp dependency without fixup`() {
driver(driverParameters(listOf(flowCorDapp, dependentContractCorDapp, standaloneContractCorDapp))) {
val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
val ex = assertFailsWith<CordaRuntimeException> {
CordaRPCClient(hostAndPort = alice.rpcAddress)
.start(user.username, user.password)
.use { client ->
client.proxy.startFlow(::CordappFixupFlow, DependentData(BEANS)).returnValue.getOrThrow()
}
}
assertThat(ex).hasMessageContaining("Type ${StandAloneData::class.java.name} not present")
}
}
}

View File

@ -2,7 +2,7 @@ package net.corda.node
import net.corda.client.rpc.CordaRPCClient
import net.corda.contracts.serialization.custom.Currantsy
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.contracts.TransactionVerificationException.ContractRejection
import net.corda.core.messaging.startFlow
import net.corda.core.utilities.getOrThrow
import net.corda.flows.serialization.custom.CustomSerializerFlow
@ -14,16 +14,22 @@ import net.corda.testing.driver.driver
import net.corda.testing.driver.internal.incrementalPortAllocation
import net.corda.testing.node.NotarySpec
import net.corda.testing.node.User
import net.corda.testing.node.internal.CustomCordapp
import net.corda.testing.node.internal.cordappWithPackages
import org.assertj.core.api.Assertions.assertThat
import org.junit.BeforeClass
import org.junit.Test
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertFailsWith
@Suppress("FunctionName")
class ContractWithCustomSerializerTest {
companion object {
const val CURRANTS = 5000L
@BeforeClass
@JvmStatic
fun checkData() {
assertNotCordaSerializable<Currantsy>()
}
}
@Test
@ -34,23 +40,21 @@ class ContractWithCustomSerializerTest {
startNodesInProcess = false,
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = true)),
cordappsForAllNodes = listOf(
cordappWithPackages("net.corda.flows.serialization.custom"),
CustomCordapp(
packages = setOf("net.corda.contracts.serialization.custom"),
name = "has-custom-serializer"
).signed()
cordappWithPackages("net.corda.flows.serialization.custom").signed(),
cordappWithPackages("net.corda.contracts.serialization.custom").signed()
)
)) {
val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
val ex = assertThrows<TransactionVerificationException> {
val ex = assertFailsWith<ContractRejection> {
CordaRPCClient(hostAndPort = alice.rpcAddress)
.start(user.username, user.password)
.proxy
.startFlow(::CustomSerializerFlow, Currantsy(CURRANTS))
.returnValue
.getOrThrow()
.use { client ->
client.proxy.startFlow(::CustomSerializerFlow, Currantsy(CURRANTS))
.returnValue
.getOrThrow()
}
}
assertThat(ex).hasMessageContaining("Too many currants! $CURRANTS is unraisinable!")
assertThat(ex).hasMessageContaining("Too many currants! $CURRANTS juicy currants is unraisinable!")
}
}
}

View File

@ -0,0 +1,124 @@
package net.corda.node
import net.corda.client.rpc.CordaRPCClient
import net.corda.contracts.serialization.missing.CustomData
import net.corda.contracts.serialization.missing.MissingSerializerContract.CustomDataState
import net.corda.core.CordaRuntimeException
import net.corda.core.contracts.TransactionVerificationException.BrokenTransactionException
import net.corda.core.contracts.TransactionVerificationException.ContractRejection
import net.corda.core.internal.hash
import net.corda.core.internal.inputStream
import net.corda.core.messaging.startFlow
import net.corda.core.utilities.getOrThrow
import net.corda.flows.serialization.missing.MissingSerializerBuilderFlow
import net.corda.flows.serialization.missing.MissingSerializerFlow
import net.corda.node.services.Permissions
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.driver
import net.corda.testing.driver.internal.incrementalPortAllocation
import net.corda.testing.node.NotarySpec
import net.corda.testing.node.TestCordapp
import net.corda.testing.node.User
import net.corda.testing.node.internal.cordappWithFixups
import net.corda.testing.node.internal.cordappWithPackages
import org.assertj.core.api.Assertions.assertThat
import org.junit.BeforeClass
import org.junit.Test
import kotlin.test.assertFailsWith
@Suppress("FunctionName")
class ContractWithMissingCustomSerializerTest {
companion object {
const val BOBBINS = 5000L
val user = User("u", "p", setOf(Permissions.all()))
val flowCorDapp = cordappWithPackages("net.corda.flows.serialization.missing").signed()
val contractCorDapp = cordappWithPackages("net.corda.contracts.serialization.missing").signed()
fun driverParameters(cordapps: List<TestCordapp>): DriverParameters {
return DriverParameters(
portAllocation = incrementalPortAllocation(),
startNodesInProcess = false,
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = true)),
cordappsForAllNodes = cordapps
)
}
@BeforeClass
@JvmStatic
fun checkData() {
assertNotCordaSerializable<CustomData>()
}
}
/*
* Test that we can still verify a transaction that is missing a custom serializer.
*/
@Test
fun `flow with missing custom serializer and fixup`() {
val contractId = contractCorDapp.jarFile.hash
val flowId = flowCorDapp.jarFile.hash
val fixupCorDapp = cordappWithFixups(listOf(setOf(contractId) to setOf(contractId, flowId))).signed()
driver(driverParameters(listOf(flowCorDapp, contractCorDapp, fixupCorDapp))) {
val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
val ex = assertFailsWith<ContractRejection> {
CordaRPCClient(hostAndPort = alice.rpcAddress)
.start(user.username, user.password)
.use { client ->
with(client.proxy) {
uploadAttachment(flowCorDapp.jarFile.inputStream())
startFlow(::MissingSerializerFlow, BOBBINS).returnValue.getOrThrow()
}
}
}
assertThat(ex).hasMessageContaining("Data $BOBBINS bobbins exceeds maximum value!")
}
}
/*
* Test we fail properly when we cannot fix-up a missing serializer.
*/
@Test
fun `flow with missing custom serializer but without fixup`() {
driver(driverParameters(listOf(flowCorDapp, contractCorDapp))) {
val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
val ex = assertFailsWith<BrokenTransactionException> {
CordaRPCClient(hostAndPort = alice.rpcAddress)
.start(user.username, user.password)
.use { client ->
client.proxy.startFlow(::MissingSerializerFlow, BOBBINS)
.returnValue
.getOrThrow()
}
}
assertThat(ex).hasMessageContaining("No fix-up rules provided for broken attachments:")
}
}
/*
* Test that TransactionBuilder prevents us from creating a
* transaction that has a custom serializer missing.
*/
@Test
fun `transaction builder flow with missing custom serializer by rpc`() {
driver(driverParameters(listOf(flowCorDapp, contractCorDapp))) {
val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
val ex = assertFailsWith<CordaRuntimeException> {
CordaRPCClient(hostAndPort = alice.rpcAddress)
.start(user.username, user.password)
.use { client ->
client.proxy.startFlow(::MissingSerializerBuilderFlow, BOBBINS)
.returnValue
.getOrThrow()
}
}
assertThat(ex)
.hasMessageContaining("TransactionDeserialisationException:")
.hasMessageContaining(CustomDataState::class.java.name)
.hasMessageContaining(CustomData::class.java.name)
}
}
}

View File

@ -297,7 +297,11 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
networkParametersStorage
).closeOnStop()
@Suppress("LeakingThis")
val transactionVerifierService = InMemoryTransactionVerifierService(transactionVerifierWorkerCount).tokenize()
val transactionVerifierService = InMemoryTransactionVerifierService(
numberOfWorkers = transactionVerifierWorkerCount,
cordappProvider = cordappProvider,
attachments = attachments
).tokenize()
val verifierFactoryService: VerifierFactoryService = if (djvmCordaSource != null) {
DeterministicVerifierFactoryService(djvmBootstrapSource, djvmCordaSource).apply {
log.info("DJVM sandbox enabled for deterministic contract verification.")

View File

@ -73,7 +73,6 @@ import net.corda.node.utilities.DefaultNamedCacheFactory
import net.corda.node.utilities.DemoClock
import net.corda.node.utilities.errorAndTerminate
import net.corda.nodeapi.internal.ArtemisMessagingClient
import net.corda.nodeapi.internal.ArtemisMessagingComponent
import net.corda.nodeapi.internal.ShutdownHook
import net.corda.nodeapi.internal.addShutdownHook
import net.corda.nodeapi.internal.bridging.BridgeControlListener

View File

@ -1,6 +1,7 @@
package net.corda.node.internal.cordapp
import com.google.common.collect.HashBiMap
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractClassName
import net.corda.core.cordapp.Cordapp
import net.corda.core.cordapp.CordappContext
@ -8,14 +9,19 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.internal.isUploaderTrusted
import net.corda.core.node.services.AttachmentFixup
import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.serialization.MissingAttachmentsException
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.contextLogger
import net.corda.node.services.persistence.AttachmentStorageInternal
import net.corda.nodeapi.internal.cordapp.CordappLoader
import java.net.JarURLConnection
import java.net.URL
import java.util.concurrent.ConcurrentHashMap
import java.util.jar.JarFile
/**
* Cordapp provider and store. For querying CorDapps for their attachment and vice versa.
@ -29,6 +35,7 @@ open class CordappProviderImpl(val cordappLoader: CordappLoader,
private val contextCache = ConcurrentHashMap<Cordapp, CordappContext>()
private val cordappAttachments = HashBiMap.create<SecureHash, URL>()
private val attachmentFixups = arrayListOf<AttachmentFixup>()
/**
* Current known CorDapps loaded on this node
@ -38,6 +45,8 @@ open class CordappProviderImpl(val cordappLoader: CordappLoader,
fun start() {
cordappAttachments.putAll(loadContractsIntoAttachmentStore())
verifyInstalledCordapps()
// Load the fix-ups after uploading any new contracts into attachment storage.
attachmentFixups.addAll(loadAttachmentFixups())
}
private fun verifyInstalledCordapps() {
@ -95,6 +104,92 @@ open class CordappProviderImpl(val cordappLoader: CordappLoader,
} to cordapp.jarPath
}.toMap()
/**
* Loads the "fixup" rules from all META-INF/Corda-Fixups files.
* These files have the following format:
* <AttachmentId>,<AttachmentId>...=><AttachmentId>,<AttachmentId>,...
* where each <AttachmentId> is the SHA256 of a CorDapp JAR that
* [net.corda.core.transactions.TransactionBuilder] will expect to find
* inside [AttachmentStorage].
*
* These rules are for repairing broken CorDapps. A correctly written
* CorDapp should not require them.
*/
private fun loadAttachmentFixups(): List<AttachmentFixup> {
return cordappLoader.appClassLoader.getResources("META-INF/Corda-Fixups").asSequence()
.mapNotNull { fixup ->
fixup.openConnection() as? JarURLConnection
}.filter { fixupConnection ->
isValidFixup(fixupConnection.jarFile)
}.flatMapTo(ArrayList()) { fixupConnection ->
fixupConnection.inputStream.bufferedReader().useLines { lines ->
lines.filter(String::isNotBlank).map { line ->
val tokens = line.split("=>", limit = 2)
require(tokens.size == 2) {
"Invalid fix-up line '$line' in '${fixupConnection.jarFile.name}'"
}
val source = parseIds(tokens[0])
require(source.isNotEmpty()) {
"Forbidden empty list of source attachment IDs in '${fixupConnection.jarFile.name}'"
}
val target = parseIds(tokens[1])
Pair(source, target)
}.toList().asSequence()
}
}
}
private fun isValidFixup(jarFile: JarFile): Boolean {
return jarFile.entries().asSequence().all { it.name.startsWith("META-INF/") }.also { isValid ->
if (!isValid) {
log.warn("FixUp '{}' contains files outside META-INF/ - IGNORING!", jarFile.name)
}
}
}
private fun parseIds(ids: String): Set<AttachmentId> {
return ids.split(",").mapTo(LinkedHashSet()) {
AttachmentId.parse(it.trim())
}
}
/**
* Apply this node's attachment fix-up rules to the given attachment IDs.
*
* @param attachmentIds A collection of [AttachmentId]s, e.g. as provided by a transaction.
* @return The [attachmentIds] with the fix-up rules applied.
*/
override fun fixupAttachmentIds(attachmentIds: Collection<AttachmentId>): Set<AttachmentId> {
val replacementIds = LinkedHashSet(attachmentIds)
attachmentFixups.forEach { (source, target) ->
if (replacementIds.containsAll(source)) {
replacementIds.removeAll(source)
replacementIds.addAll(target)
}
}
return replacementIds
}
/**
* Apply this node's attachment fix-up rules to the given attachments.
*
* @param attachments A collection of [Attachment] objects, e.g. as provided by a transaction.
* @return The [attachments] with the node's fix-up rules applied.
*/
override fun fixupAttachments(attachments: Collection<Attachment>): Collection<Attachment> {
val attachmentsById = attachments.associateByTo(LinkedHashMap(), Attachment::id)
val replacementIds = fixupAttachmentIds(attachmentsById.keys)
attachmentsById.keys.retainAll(replacementIds)
(replacementIds - attachmentsById.keys).forEach { extraId ->
val extraAttachment = attachmentStorage.openAttachment(extraId)
if (extraAttachment == null || !extraAttachment.isUploaderTrusted()) {
throw MissingAttachmentsException(listOf(extraId))
}
attachmentsById[extraId] = extraAttachment
}
return attachmentsById.values
}
/**
* Get the current cordapp context for the given CorDapp
*

View File

@ -1,11 +1,14 @@
package net.corda.node.internal.cordapp
import net.corda.core.contracts.Attachment
import net.corda.core.cordapp.Cordapp
import net.corda.core.cordapp.CordappProvider
import net.corda.core.flows.FlowLogic
import net.corda.core.internal.CordappFixupInternal
import net.corda.core.internal.cordapp.CordappImpl
interface CordappProviderInternal : CordappProvider {
interface CordappProviderInternal : CordappProvider, CordappFixupInternal {
val cordapps: List<CordappImpl>
fun getCordappForFlow(flowLogic: FlowLogic<*>): Cordapp?
fun fixupAttachments(attachments: Collection<Attachment>): Collection<Attachment>
}

View File

@ -156,7 +156,7 @@ class NodeAttachmentService @JvmOverloads constructor(
val session = currentDBSession()
val criteriaBuilder = session.criteriaBuilder
val criteriaQuery = criteriaBuilder.createQuery(Long::class.java)
criteriaQuery.select(criteriaBuilder.count(criteriaQuery.from(NodeAttachmentService.DBAttachment::class.java)))
criteriaQuery.select(criteriaBuilder.count(criteriaQuery.from(DBAttachment::class.java)))
val count = session.createQuery(criteriaQuery).singleResult
attachmentCount.inc(count)
}
@ -278,9 +278,9 @@ class NodeAttachmentService @JvmOverloads constructor(
loadFunction = { Optional.ofNullable(loadAttachmentContent(it)) }
)
private fun loadAttachmentContent(id: SecureHash): Pair<Attachment, ByteArray>? {
private fun loadAttachmentContent(id: AttachmentId): Pair<Attachment, ByteArray>? {
return database.transaction {
val attachment = currentDBSession().get(NodeAttachmentService.DBAttachment::class.java, id.toString())
val attachment = currentDBSession().get(DBAttachment::class.java, id.toString())
?: return@transaction null
Pair(createAttachmentFromDatabase(attachment), attachment.content)
}
@ -363,7 +363,7 @@ class NodeAttachmentService @JvmOverloads constructor(
}
override fun hasAttachment(attachmentId: AttachmentId): Boolean = database.transaction {
currentDBSession().find(NodeAttachmentService.DBAttachment::class.java, attachmentId.toString()) != null
currentDBSession().find(DBAttachment::class.java, attachmentId.toString()) != null
}
private fun increaseDefaultVersionIfWhitelistedAttachment(contractClassNames: List<ContractClassName>, contractVersionFromFile: Int, attachmentId: AttachmentId) =
@ -400,7 +400,7 @@ class NodeAttachmentService @JvmOverloads constructor(
val jarSigners = getSigners(bytes)
val contractVersion = increaseDefaultVersionIfWhitelistedAttachment(contractClassNames, getVersion(bytes), id)
val session = currentDBSession()
val attachment = NodeAttachmentService.DBAttachment(
val attachment = DBAttachment(
attId = id.toString(),
content = bytes,
uploader = uploader,
@ -417,7 +417,7 @@ class NodeAttachmentService @JvmOverloads constructor(
}
if (isUploaderTrusted(uploader)) {
val session = currentDBSession()
val attachment = session.get(NodeAttachmentService.DBAttachment::class.java, id.toString())
val attachment = session.get(DBAttachment::class.java, id.toString())
// update the `uploader` field (as the existing attachment may have been resolved from a peer)
if (attachment.uploader != uploader) {
attachment.uploader = uploader
@ -438,7 +438,7 @@ class NodeAttachmentService @JvmOverloads constructor(
}
private fun getSigners(attachmentBytes: ByteArray) =
JarSignatureCollector.collectSigners(JarInputStream(attachmentBytes.inputStream()))
JarInputStream(attachmentBytes.inputStream()).use(JarSignatureCollector::collectSigners)
private fun getVersion(attachmentBytes: ByteArray) =
JarInputStream(attachmentBytes.inputStream()).use {

View File

@ -2,21 +2,113 @@ package net.corda.node.services.transactions
import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.TransactionVerificationException.BrokenTransactionException
import net.corda.core.internal.TransactionVerifierServiceInternal
import net.corda.core.internal.concurrent.openFuture
import net.corda.core.internal.internalFindTrustedAttachmentForClass
import net.corda.core.internal.prepareVerify
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.node.services.TransactionVerifierService
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.utilities.contextLogger
import net.corda.node.internal.cordapp.CordappProviderInternal
import net.corda.nodeapi.internal.persistence.withoutDatabaseAccess
class InMemoryTransactionVerifierService(@Suppress("UNUSED_PARAMETER") numberOfWorkers: Int) : SingletonSerializeAsToken(), TransactionVerifierService, TransactionVerifierServiceInternal, AutoCloseable {
override fun verify(transaction: LedgerTransaction): CordaFuture<Unit> = this.verify(transaction, emptyList())
class InMemoryTransactionVerifierService(
@Suppress("UNUSED_PARAMETER") numberOfWorkers: Int,
private val cordappProvider: CordappProviderInternal,
private val attachments: AttachmentStorage
) : SingletonSerializeAsToken(), TransactionVerifierService, TransactionVerifierServiceInternal, AutoCloseable {
companion object {
private val SEPARATOR = System.lineSeparator() + "-> "
private val log = contextLogger()
override fun verify(transaction: LedgerTransaction, extraAttachments: List<Attachment>): CordaFuture<Unit> {
fun Collection<*>.deepEquals(other: Collection<*>): Boolean {
return size == other.size && containsAll(other) && other.containsAll(this)
}
fun Collection<Attachment>.toPrettyString(): String {
return joinToString(separator = SEPARATOR, prefix = SEPARATOR, postfix = System.lineSeparator()) { attachment ->
attachment.id.toString()
}
}
}
override fun verify(transaction: LedgerTransaction): CordaFuture<*> {
return openFuture<Unit>().apply {
capture {
val verifier = transaction.prepareVerify(extraAttachments)
val verifier = transaction.prepareVerify(transaction.attachments)
withoutDatabaseAccess {
verifier.verify()
}
}
}
}
private fun computeReplacementAttachmentsFor(ltx: LedgerTransaction, missingClass: String?): Collection<Attachment> {
val replacements = cordappProvider.fixupAttachments(ltx.attachments)
return if (replacements.deepEquals(ltx.attachments)) {
/*
* We cannot continue unless we have some idea which
* class is missing from the attachments.
*/
if (missingClass == null) {
throw BrokenTransactionException(
txId = ltx.id,
message = "No fix-up rules provided for broken attachments:${replacements.toPrettyString()}"
)
}
/*
* The Node's fix-up rules have not been able to adjust the transaction's attachments,
* so resort to the original mechanism of trying to find an attachment that contains
* the missing class. (Do you feel lucky, Punk?)
*/
val extraAttachment = requireNotNull(attachments.internalFindTrustedAttachmentForClass(missingClass)) {
"""Transaction $ltx is incorrectly formed. Most likely it was created during version 3 of Corda
|when the verification logic was more lenient. Attempted to find local dependency for class: $missingClass,
|but could not find one.
|If you wish to verify this transaction, please contact the originator of the transaction and install the
|provided missing JAR.
|You can install it using the RPC command: `uploadAttachment` without restarting the node.
|""".trimMargin()
}
replacements.toMutableSet().apply {
/*
* Check our transaction doesn't already contain this extra attachment.
* It seems unlikely that we would, but better safe than sorry!
*/
if (!add(extraAttachment)) {
throw BrokenTransactionException(
txId = ltx.id,
message = "Unlinkable class $missingClass inside broken attachments:${replacements.toPrettyString()}"
)
}
log.warn("""Detected that transaction $ltx does not contain all cordapp dependencies.
|This may be the result of a bug in a previous version of Corda.
|Attempting to verify using the additional trusted dependency: $extraAttachment for class $missingClass.
|Please check with the originator that this is a valid transaction.
|YOU ARE ONLY SEEING THIS MESSAGE BECAUSE THE CORDAPPS THAT CREATED THIS TRANSACTION ARE BROKEN!
|WE HAVE TRIED TO REPAIR THE TRANSACTION AS BEST WE CAN, BUT CANNOT GUARANTEE WE HAVE SUCCEEDED!
|PLEASE FIX THE CORDAPPS AND MIGRATE THESE BROKEN TRANSACTIONS AS SOON AS POSSIBLE!
|THIS MESSAGE IS **SUPPOSED** TO BE SCARY!!
|""".trimMargin()
)
}
} else {
replacements
}
}
override fun reverifyWithFixups(transaction: LedgerTransaction, missingClass: String?): CordaFuture<*> {
return openFuture<Unit>().apply {
capture {
val replacementAttachments = computeReplacementAttachmentsFor(transaction, missingClass)
log.warn("Reverifying transaction {} with attachments:{}", transaction.id, replacementAttachments.toPrettyString())
val verifier = transaction.prepareVerify(replacementAttachments.toList())
withoutDatabaseAccess {
verifier.verify()
}

View File

@ -3,6 +3,24 @@ apply plugin: 'net.corda.plugins.cordapp'
def javaHome = System.getProperty('java.home')
def shrinkJar = file("$buildDir/libs/${project.name}-${project.version}-tiny.jar")
import java.security.NoSuchAlgorithmException
import java.security.MessageDigest
static String sha256(File jarFile) throws FileNotFoundException, NoSuchAlgorithmException {
InputStream input = new FileInputStream(jarFile)
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256")
byte[] buffer = new byte[8192]
int bytesRead
while ((bytesRead = input.read(buffer)) != -1) {
digest.update(buffer, 0, bytesRead)
}
return digest.digest().encodeHex().toString()
} finally {
input.close()
}
}
cordapp {
targetPlatformVersion = corda_platform_version.toInteger()
minimumPlatformVersion 1
@ -42,6 +60,18 @@ dependencies {
compile "com.opengamma.strata:strata-market:$strata_version"
}
def cordappDependencies = file("${sourceSets['main'].output.resourcesDir}/META-INF/Cordapp-Dependencies")
task generateDependencies {
dependsOn project(':finance:contracts').tasks.jar
outputs.files(cordappDependencies)
doLast {
configurations.cordapp.forEach { cordapp ->
cordappDependencies << sha256(cordapp) << System.lineSeparator()
}
}
}
processResources.finalizedBy generateDependencies
jar {
classifier = 'fat'
}

View File

@ -0,0 +1,34 @@
@file:JvmName("CordappDependencies")
package net.corda.vega.contracts
import net.corda.core.crypto.SecureHash
import net.corda.core.transactions.TransactionBuilder
import java.net.URLClassLoader
/**
* This is a crude, home-grown Cordapp dependency mechanism.
*/
private val EXTERNAL_CORDAPPS: List<SecureHash> by lazy {
loadDependencies()
}
/**
* Locate any META-INF/Cordapp-Dependencies file that
* is part of this particular CorDapp.
*/
private fun loadDependencies(): List<SecureHash> {
val cordappURL = object {}::class.java.protectionDomain.codeSource.location
return URLClassLoader(arrayOf(cordappURL), null).use { cl ->
val deps = cl.getResource("META-INF/Cordapp-Dependencies") ?: return emptyList()
deps.openStream().bufferedReader().useLines { lines ->
lines.filterNot(String::isBlank).map(SecureHash.Companion::parse).toList()
}
}
}
fun TransactionBuilder.withExternalCordapps(): TransactionBuilder {
for (cordapp in EXTERNAL_CORDAPPS) {
addAttachment(cordapp)
}
return this
}

View File

@ -23,6 +23,8 @@ data class IRSState(val swap: SwapData,
override fun generateAgreement(notary: Party): TransactionBuilder {
val state = IRSState(swap, buyer, seller)
return TransactionBuilder(notary).withItems(StateAndContract(state, IRS_PROGRAM_ID), Command(OGTrade.Commands.Agree(), participants.map { it.owningKey }))
return TransactionBuilder(notary)
.withItems(StateAndContract(state, IRS_PROGRAM_ID), Command(OGTrade.Commands.Agree(), participants.map { it.owningKey }))
.withExternalCordapps()
}
}

View File

@ -37,7 +37,10 @@ data class PortfolioState(val portfolio: List<StateRef>,
}
override fun generateAgreement(notary: Party): TransactionBuilder {
return TransactionBuilder(notary).withItems(StateAndContract(copy(), PORTFOLIO_SWAP_PROGRAM_ID), Command(PortfolioSwap.Commands.Agree(), participants.map { it.owningKey }))
val command = Command(PortfolioSwap.Commands.Agree(), participants.map { it.owningKey })
return TransactionBuilder(notary)
.withItems(StateAndContract(copy(), PORTFOLIO_SWAP_PROGRAM_ID), command)
.withExternalCordapps()
}
override fun generateRevision(notary: Party, oldState: StateAndRef<*>, updatedValue: Update): TransactionBuilder {
@ -49,6 +52,7 @@ data class PortfolioState(val portfolio: List<StateRef>,
tx.addInputState(oldState)
tx.addOutputState(copy(portfolio = portfolio, valuation = valuation), PORTFOLIO_SWAP_PROGRAM_ID)
tx.addCommand(PortfolioSwap.Commands.Update(), participants.map { it.owningKey })
tx.withExternalCordapps()
return tx
}
}

View File

@ -24,7 +24,7 @@ data class SerializationFactoryCacheKey(val classWhitelist: ClassWhitelist,
val preventDataLoss: Boolean,
val customSerializers: Set<SerializationCustomSerializer<*, *>>)
fun SerializerFactory.addToWhitelist(vararg types: Class<*>) {
fun SerializerFactory.addToWhitelist(types: Collection<Class<*>>) {
require(types.toSet().size == types.size) {
val duplicates = types.toMutableList()
types.toSet().forEach { duplicates -= it }
@ -71,11 +71,11 @@ abstract class AbstractAMQPSerializationScheme(
@DeleteForDJVM
val List<Cordapp>.customSerializers
get() = flatMap { it.serializationCustomSerializers }.toSet()
get() = flatMapTo(LinkedHashSet(), Cordapp::serializationCustomSerializers)
@DeleteForDJVM
val List<Cordapp>.serializationWhitelists
get() = flatMap { it.serializationWhitelists }.toSet()
get() = flatMapTo(LinkedHashSet(), Cordapp::serializationWhitelists)
}
private fun registerCustomSerializers(context: SerializationContext, factory: SerializerFactory) {
@ -93,7 +93,11 @@ abstract class AbstractAMQPSerializationScheme(
factory.registerExternal(CorDappCustomSerializer(customSerializer, factory))
}
cordappCustomSerializers.forEach { customSerializer ->
factory.registerExternal(CorDappCustomSerializer(customSerializer, factory))
// We won't be able to use this custom serializer unless it also belongs to
// the deserialization classloader.
if (customSerializer::class.java.classLoader == context.deserializationClassLoader) {
factory.registerExternal(CorDappCustomSerializer(customSerializer, factory))
}
}
context.properties[ContextPropertyKeys.SERIALIZERS]?.apply {
@ -105,12 +109,10 @@ abstract class AbstractAMQPSerializationScheme(
private fun registerCustomWhitelists(factory: SerializerFactory) {
serializationWhitelists.forEach {
factory.addToWhitelist(*it.whitelist.toTypedArray())
factory.addToWhitelist(it.whitelist)
}
cordappSerializationWhitelists.forEach {
it.whitelist.forEach { clazz ->
factory.addToWhitelist(clazz)
}
factory.addToWhitelist(it.whitelist)
}
}

View File

@ -1,6 +1,7 @@
package net.corda.serialization.internal.amqp
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.internal.MissingSerializerException
import net.corda.serialization.internal.model.LocalConstructorInformation
import net.corda.serialization.internal.model.LocalPropertyInformation
import net.corda.serialization.internal.model.LocalTypeInformation
@ -21,7 +22,11 @@ interface ObjectSerializer : AMQPSerializer<Any> {
companion object {
fun make(typeInformation: LocalTypeInformation, factory: LocalSerializerFactory): ObjectSerializer {
if (typeInformation is LocalTypeInformation.NonComposable) {
throw NotSerializableException(nonComposableExceptionMessage(typeInformation, factory))
val typeNames = typeInformation.nonComposableTypes.map { it.observedType.typeName }
throw MissingSerializerException(
message = nonComposableExceptionMessage(typeInformation, factory),
typeNames = typeNames
)
}
val typeDescriptor = factory.createDescriptor(typeInformation)

View File

@ -1,6 +1,7 @@
package net.corda.serialization.internal.amqp
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.internal.MissingSerializerException
import net.corda.core.utilities.contextLogger
import net.corda.serialization.internal.model.*
import java.io.NotSerializableException
@ -79,8 +80,10 @@ class DefaultRemoteSerializerFactory(
}
// Return the specific serializer the caller asked for.
serializers[typeDescriptor] ?: throw NotSerializableException(
"Could not find type matching descriptor $typeDescriptor.")
serializers[typeDescriptor] ?: throw MissingSerializerException(
message = "Could not find type matching descriptor $typeDescriptor.",
typeDescriptor = typeDescriptor
)
}
private fun getUncached(

View File

@ -1,7 +1,6 @@
package net.corda.serialization.internal.model
import java.lang.reflect.*
import kotlin.reflect.KFunction
import java.util.*
typealias PropertyName = String
@ -87,11 +86,11 @@ sealed class LocalTypeInformation {
* Get the map of [LocalPropertyInformation], for all types that have it, or an empty map otherwise.
*/
val propertiesOrEmptyMap: Map<PropertyName, LocalPropertyInformation> get() = when(this) {
is LocalTypeInformation.Composable -> properties
is LocalTypeInformation.Abstract -> properties
is LocalTypeInformation.AnInterface -> properties
is LocalTypeInformation.NonComposable -> properties
is LocalTypeInformation.Opaque -> wrapped.propertiesOrEmptyMap
is Composable -> properties
is Abstract -> properties
is AnInterface -> properties
is NonComposable -> properties
is Opaque -> wrapped.propertiesOrEmptyMap
else -> emptyMap()
}
@ -99,10 +98,10 @@ sealed class LocalTypeInformation {
* Get the list of interfaces, for all types that have them, or an empty list otherwise.
*/
val interfacesOrEmptyList: List<LocalTypeInformation> get() = when(this) {
is LocalTypeInformation.Composable -> interfaces
is LocalTypeInformation.Abstract -> interfaces
is LocalTypeInformation.AnInterface -> interfaces
is LocalTypeInformation.NonComposable -> interfaces
is Composable -> interfaces
is Abstract -> interfaces
is AnInterface -> interfaces
is NonComposable -> interfaces
else -> emptyList()
}
@ -246,11 +245,12 @@ sealed class LocalTypeInformation {
* we do not possess a method (such as a unique "deserialization constructor" satisfied by these properties) for
* creating a new instance from a dictionary of property values.
*
* @param constructor [LocalConstructorInformation] for the constructor of this type, if there is one.
* @param properties [LocalPropertyInformation] for the properties of the interface.
* @param superclass [LocalTypeInformation] for the superclass of the underlying class of this type.
* @param interfaces [LocalTypeInformation] for the interfaces extended by this interface.
* @param typeParameters [LocalTypeInformation] for the resolved type parameters of the type.
* @property constructor [LocalConstructorInformation] for the constructor of this type, if there is one.
* @property properties [LocalPropertyInformation] for the properties of the interface.
* @property superclass [LocalTypeInformation] for the superclass of the underlying class of this type.
* @property interfaces [LocalTypeInformation] for the interfaces extended by this interface.
* @property typeParameters [LocalTypeInformation] for the resolved type parameters of the type.
* @param nonComposableSubtypes [NonComposable] for the type descriptors that make this type non-composable,
*/
data class NonComposable(
override val observedType: Type,
@ -260,8 +260,11 @@ sealed class LocalTypeInformation {
val superclass: LocalTypeInformation,
val interfaces: List<LocalTypeInformation>,
val typeParameters: List<LocalTypeInformation>,
private val nonComposableSubtypes: Set<NonComposable>,
val reason: String,
val remedy: String) : LocalTypeInformation()
val remedy: String) : LocalTypeInformation() {
val nonComposableTypes: Set<NonComposable> get() = nonComposableSubtypes.flatMapTo(LinkedHashSet()) { it.nonComposableTypes } + this
}
/**
* Represents a type whose underlying class is a collection class such as [List] with a single type parameter.

View File

@ -5,7 +5,6 @@ import net.corda.core.internal.isConcreteClass
import net.corda.core.internal.kotlinObjectInstance
import net.corda.core.serialization.ConstructorForDeserialization
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
import net.corda.core.utilities.contextLogger
import net.corda.serialization.internal.NotSerializableDetailedException
import net.corda.serialization.internal.amqp.*
import java.io.NotSerializableException
@ -38,11 +37,6 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
var visited: Set<TypeIdentifier> = emptySet(),
val cycles: MutableList<LocalTypeInformation.Cycle> = mutableListOf(),
var validateProperties: Boolean = true) {
companion object {
private val logger = contextLogger()
}
/**
* If we are examining the type of a read-only property, or a type flagged as [Opaque], then we do not need to warn
* if the [LocalTypeInformation] for that type (or any of its related types) is [LocalTypeInformation.NonComposable].
@ -202,32 +196,30 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
private fun buildNonAtomic(rawType: Class<*>, type: Type, typeIdentifier: TypeIdentifier, typeParameterInformation: List<LocalTypeInformation>): LocalTypeInformation {
val superclassInformation = buildSuperclassInformation(type)
val interfaceInformation = buildInterfaceInformation(type)
val observedConstructor = constructorForDeserialization(type)
if (observedConstructor == null) {
return LocalTypeInformation.NonComposable(
observedType = type,
typeIdentifier = typeIdentifier,
constructor = null,
properties = if (rawType == Class::class.java) {
// Do NOT drill down into the internals of java.lang.Class.
emptyMap()
} else {
buildReadOnlyProperties(rawType)
},
superclass = superclassInformation,
interfaces = interfaceInformation,
typeParameters = typeParameterInformation,
reason = "No unique deserialization constructor can be identified",
remedy = "Either annotate a constructor for this type with @ConstructorForDeserialization, or provide a custom serializer for it"
)
}
val observedConstructor = constructorForDeserialization(type) ?: return LocalTypeInformation.NonComposable(
observedType = type,
typeIdentifier = typeIdentifier,
constructor = null,
properties = if (rawType == Class::class.java) {
// Do NOT drill down into the internals of java.lang.Class.
emptyMap()
} else {
buildReadOnlyProperties(rawType)
},
superclass = superclassInformation,
interfaces = interfaceInformation,
typeParameters = typeParameterInformation,
nonComposableSubtypes = emptySet(),
reason = "No unique deserialization constructor can be identified",
remedy = "Either annotate a constructor for this type with @ConstructorForDeserialization, or provide a custom serializer for it"
)
val constructorInformation = buildConstructorInformation(type, observedConstructor)
val properties = buildObjectProperties(rawType, constructorInformation)
if (!propertiesSatisfyConstructor(constructorInformation, properties)) {
val missingParameters = missingMandatoryConstructorProperties(constructorInformation, properties).map { it.name }
val missingConstructorProperties = missingMandatoryConstructorProperties(constructorInformation, properties)
val missingParameters = missingConstructorProperties.map(LocalConstructorParameterInformation::name)
return LocalTypeInformation.NonComposable(
observedType = type,
typeIdentifier = typeIdentifier,
@ -236,6 +228,8 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
superclass = superclassInformation,
interfaces = interfaceInformation,
typeParameters = typeParameterInformation,
nonComposableSubtypes = missingConstructorProperties
.filterIsInstanceTo(LinkedHashSet(), LocalTypeInformation.NonComposable::class.java),
reason = "Mandatory constructor parameters $missingParameters are missing from the readable properties ${properties.keys}",
remedy = "Either provide getters or readable fields for $missingParameters, or provide a custom serializer for this type"
)
@ -252,6 +246,9 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
superclass = superclassInformation,
interfaces = interfaceInformation,
typeParameters = typeParameterInformation,
nonComposableSubtypes = nonComposableProperties.values.mapTo(LinkedHashSet()) {
it.type as LocalTypeInformation.NonComposable
},
reason = nonComposablePropertiesErrorReason(nonComposableProperties),
remedy = "Either ensure that the properties ${nonComposableProperties.keys} are serializable, or provide a custom serializer for this type"
)
@ -279,7 +276,7 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
}
}.toSet()
return (0 until constructorInformation.parameters.size).none { index ->
return (constructorInformation.parameters.indices).none { index ->
constructorInformation.parameters[index].isMandatory && index !in indicesAddressedByProperties
}
}
@ -298,7 +295,7 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
}
}.toSet()
return (0 until constructorInformation.parameters.size).mapNotNull { index ->
return (constructorInformation.parameters.indices).mapNotNull { index ->
val parameter = constructorInformation.parameters[index]
when {
constructorInformation.parameters[index].isMandatory && index !in indicesAddressedByProperties -> parameter

View File

@ -77,7 +77,7 @@ open class MockServices private constructor(
private val cordappLoader: CordappLoader,
override val validatedTransactions: TransactionStorage,
override val identityService: IdentityService,
private val initialNetworkParameters: NetworkParameters,
initialNetworkParameters: NetworkParameters,
private val initialIdentity: TestIdentity,
private val moreKeys: Array<out KeyPair>,
override val keyManagementService: KeyManagementService = MockKeyManagementService(
@ -425,10 +425,14 @@ open class MockServices private constructor(
get() {
return NodeInfo(listOf(NetworkHostAndPort("mock.node.services", 10000)), listOf(initialIdentity.identity), 1, serial = 1L)
}
override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2)
private val mockCordappProvider: MockCordappProvider = MockCordappProvider(cordappLoader, attachments).also {
it.start()
}
override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(
numberOfWorkers = 2,
cordappProvider = mockCordappProvider,
attachments = attachments
)
override val cordappProvider: CordappProvider get() = mockCordappProvider
override var networkParametersService: NetworkParametersService = MockNetworkParametersStorage(initialNetworkParameters)
override val diagnosticsService: DiagnosticsService = NodeDiagnosticsService()

View File

@ -4,6 +4,7 @@ import io.github.classgraph.ClassGraph
import net.corda.core.internal.*
import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.internal.cordapp.set
import net.corda.core.node.services.AttachmentFixup
import net.corda.core.serialization.SerializationWhitelist
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
@ -32,18 +33,21 @@ data class CustomCordapp(
val versionId: Int = 1,
val targetPlatformVersion: Int = PLATFORM_VERSION,
val classes: Set<Class<*>> = emptySet(),
val fixups: List<AttachmentFixup> = emptyList(),
val signingInfo: SigningInfo? = null,
override val config: Map<String, Any> = emptyMap()
) : TestCordappInternal() {
init {
require(packages.isNotEmpty() || classes.isNotEmpty()) { "At least one package or class must be specified" }
require(packages.isNotEmpty() || classes.isNotEmpty() || fixups.isNotEmpty()) {
"At least one package or class must be specified"
}
}
override val jarFile: Path get() = getJarFile(this)
override fun withConfig(config: Map<String, Any>): CustomCordapp = copy(config = config)
override fun withOnlyJarContents(): CustomCordapp = CustomCordapp(packages = packages, classes = classes)
override fun withOnlyJarContents(): CustomCordapp = CustomCordapp(packages = packages, classes = classes, fixups = fixups)
fun signed(keyStorePath: Path? = null, numberOfSignatures: Int = 1, keyAlgorithm: String = "RSA"): CustomCordapp =
copy(signingInfo = SigningInfo(keyStorePath, numberOfSignatures, keyAlgorithm))
@ -83,20 +87,36 @@ data class CustomCordapp(
}
}
internal fun createFixupJar(file: Path) {
JarOutputStream(file.outputStream()).use { jos ->
jos.addEntry(testEntry(JarFile.MANIFEST_NAME)) {
createTestManifest(name, versionId, targetPlatformVersion).write(jos)
}
jos.addEntry(testEntry("META-INF/Corda-Fixups")) {
fixups.filter { it.first.isNotEmpty() }.forEach { (source, target) ->
val data = source.joinToString(
separator = ",",
postfix = target.joinToString(
separator = ",",
prefix = "=>",
postfix = "\r\n"
)
)
jos.write(data.toByteArray())
}
}
}
}
private fun signJar(jarFile: Path) {
if (signingInfo != null) {
val keyStorePathToUse = if (signingInfo.keyStorePath != null) {
signingInfo.keyStorePath
} else {
defaultJarSignerDirectory.createDirectories()
defaultJarSignerDirectory
}
val keyStorePathToUse = signingInfo.keyStorePath ?: defaultJarSignerDirectory.createDirectories()
for (i in 1 .. signingInfo.numberOfSignatures) {
val alias = "alias$i"
val pwd = "secret!"
if (!keyStorePathToUse.containsKey(alias, pwd))
if (!keyStorePathToUse.containsKey(alias, pwd)) {
keyStorePathToUse.generateKey(alias, pwd, "O=Test Company Ltd $i,OU=Test,L=London,C=GB", signingInfo.keyAlgorithm)
}
val pk = keyStorePathToUse.signJar(jarFile.toString(), alias, pwd)
logger.debug { "Signed Jar: $jarFile with public key $pk" }
}
@ -141,7 +161,7 @@ data class CustomCordapp(
private val epochFileTime = FileTime.from(Instant.EPOCH)
private val cordappsDirectory: Path
private val defaultJarSignerDirectory: Path
private val whitespace = "\\s".toRegex()
private val whitespace = "\\s++".toRegex()
private val cache = ConcurrentHashMap<CustomCordapp, Path>()
init {
@ -156,7 +176,11 @@ data class CustomCordapp(
return cache.computeIfAbsent(cordapp.copy(config = emptyMap())) {
val filename = it.run { "${name.replace(whitespace, "-")}_${versionId}_${targetPlatformVersion}_${UUID.randomUUID()}.jar" }
val jarFile = cordappsDirectory.createDirectories() / filename
it.packageAsJar(jarFile)
if (it.fixups.isNotEmpty()) {
it.createFixupJar(jarFile)
} else {
it.packageAsJar(jarFile)
}
it.signJar(jarFile)
logger.debug { "$it packaged into $jarFile" }
jarFile

View File

@ -13,6 +13,7 @@ import net.corda.core.internal.concurrent.openFuture
import net.corda.core.internal.div
import net.corda.core.internal.times
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.node.services.AttachmentFixup
import net.corda.core.serialization.internal.SerializationEnvironment
import net.corda.core.serialization.internal._allEnabledSerializationEnvs
import net.corda.core.serialization.internal._driverSerializationEnv
@ -94,6 +95,8 @@ fun cordappWithPackages(vararg packageNames: String): CustomCordapp = CustomCord
// TODO Rename to cordappWithClasses
fun cordappForClasses(vararg classes: Class<*>): CustomCordapp = CustomCordapp(packages = emptySet(), classes = classes.toSet())
fun cordappWithFixups(fixups: List<AttachmentFixup>) = CustomCordapp(fixups = fixups)
/**
* Find the single CorDapp jar on the current classpath which contains the given package. This is a convenience method for
* [TestCordapp.findCordapp] but returns the internal [TestCordappImpl].

View File

@ -28,7 +28,7 @@ data class TestCordappImpl(val scanPackage: String, override val config: Map<Str
override val jarFile: Path
get() {
val jars = TestCordappImpl.findJars(scanPackage)
val jars = findJars(scanPackage)
when (jars.size) {
0 -> throw IllegalArgumentException("There are no CorDapps containing the package $scanPackage on the classpath. Make sure " +
"the package name is correct and that the CorDapp is added as a gradle dependency.")
@ -57,13 +57,12 @@ data class TestCordappImpl(val scanPackage: String, override val config: Map<Str
private fun findRootPaths(scanPackage: String): Set<Path> {
return packageToRootPaths.computeIfAbsent(scanPackage) {
val classGraph = ClassGraph().whitelistPaths(scanPackage.replace('.', '/'))
classGraph.pooledScan().use {
it.allResources
.asSequence()
.map { it.classpathElementFile.toPath() }
.filterNot { it.toString().endsWith("-tests.jar") }
.map { if (it.toString().endsWith(".jar")) it else findProjectRoot(it) }
.toSet()
classGraph.pooledScan().use { scanResult ->
scanResult.allResources
.asSequence()
.map { it.classpathElementFile.toPath() }
.filterNot { it.toString().endsWith("-tests.jar") }
.mapTo(LinkedHashSet()) { if (it.toString().endsWith(".jar")) it else findProjectRoot(it) }
}
}
}