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.CordaInternal
import net.corda.core.KeepForDJVM import net.corda.core.KeepForDJVM
import net.corda.core.internal.cordapp.CordappImpl.Companion.DEFAULT_CORDAPP_VERSION import net.corda.core.internal.cordapp.CordappImpl.Companion.DEFAULT_CORDAPP_VERSION
import net.corda.core.serialization.CordaSerializable
import java.security.PublicKey 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") 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]. */ /** Whether the inputs or outputs list contains an encumbrance issue, see [TransactionMissingEncumbranceException]. */
@CordaSerializable @CordaSerializable
@KeepForDJVM @KeepForDJVM

View File

@ -1,13 +1,16 @@
@file:Suppress("TooManyFunctions")
package net.corda.core.internal package net.corda.core.internal
import net.corda.core.DeleteForDJVM import net.corda.core.DeleteForDJVM
import net.corda.core.contracts.Attachment import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractClassName import net.corda.core.contracts.ContractClassName
import net.corda.core.cordapp.CordappProvider
import net.corda.core.flows.DataVendingFlow import net.corda.core.flows.DataVendingFlow
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.node.NetworkParameters import net.corda.core.node.NetworkParameters
import net.corda.core.node.ServicesForResolution import net.corda.core.node.ServicesForResolution
import net.corda.core.node.ZoneVersionTooLowException 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.AttachmentStorage
import net.corda.core.node.services.vault.AttachmentQueryCriteria import net.corda.core.node.services.vault.AttachmentQueryCriteria
import net.corda.core.node.services.vault.AttachmentSort 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 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]. * Scans trusted (installed locally) attachments to find all that contain the [className].
* This is required as a workaround until explicit cordapp dependencies are implemented. * 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 @DeleteForDJVM
interface TransactionVerifierServiceInternal { interface TransactionVerifierServiceInternal {
/** fun reverifyWithFixups(transaction: LedgerTransaction, missingClass: String?): CordaFuture<*>
* 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<*>
} }
/** /**
* Defined here for visibility reasons. * 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 * 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 import java.nio.file.FileAlreadyExistsException
typealias AttachmentId = SecureHash typealias AttachmentId = SecureHash
typealias AttachmentFixup = Pair<Set<AttachmentId>, Set<AttachmentId>>
/** /**
* An attachment store records potentially large binary objects, identified by their hash. * 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.DeleteForDJVM
import net.corda.core.DoNotImplement import net.corda.core.DoNotImplement
import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.Attachment
import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.LedgerTransaction
/** /**

View File

@ -325,7 +325,6 @@ object AttachmentsClassLoaderBuilder {
val serializers = createInstancesOfClassesImplementing(transactionClassLoader, SerializationCustomSerializer::class.java) val serializers = createInstancesOfClassesImplementing(transactionClassLoader, SerializationCustomSerializer::class.java)
val whitelistedClasses = ServiceLoader.load(SerializationWhitelist::class.java, transactionClassLoader) val whitelistedClasses = ServiceLoader.load(SerializationWhitelist::class.java, transactionClassLoader)
.flatMap(SerializationWhitelist::whitelist) .flatMap(SerializationWhitelist::whitelist)
.toList()
// Create a new serializationContext for the current transaction. In this context we will forbid // 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 // 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. * 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. * 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. * This classloader is only used during verification and does not leak to the client code.
* *
@ -204,18 +204,18 @@ private constructor(
*/ */
@Throws(TransactionVerificationException::class) @Throws(TransactionVerificationException::class)
fun verify() { fun verify() {
internalPrepareVerify(emptyList()).verify() internalPrepareVerify(attachments).verify()
} }
/** /**
* This method has to be called in a context where it has access to the database. * This method has to be called in a context where it has access to the database.
*/ */
@CordaInternal @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 // 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. // like no-overlap, package namespace ownership and (in future) deterministic Java.
return AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext( return AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(
attachments + extraAttachments, txAttachments,
getParamsWithGoo(), getParamsWithGoo(),
id, id,
isAttachmentTrusted = isAttachmentTrusted) { transactionClassLoader -> 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.TransactionDeserialisationException
import net.corda.core.internal.TransactionVerifierServiceInternal import net.corda.core.internal.TransactionVerifierServiceInternal
import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.VisibleForTesting
import net.corda.core.internal.internalFindTrustedAttachmentForClass
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
import net.corda.core.node.ServicesForResolution import net.corda.core.node.ServicesForResolution
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import net.corda.core.serialization.internal.MissingSerializerException
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
@ -228,51 +228,64 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
// TODO: allow non-blocking verification. // TODO: allow non-blocking verification.
services.transactionVerifierService.verify(ltx).getOrThrow() services.transactionVerifierService.verify(ltx).getOrThrow()
} catch (e: NoClassDefFoundError) { } catch (e: NoClassDefFoundError) {
if (e.message != null) { checkReverifyAllowed(e)
verifyWithExtraDependency(e.message!!, ltx, services, e) val missingClass = e.message ?: throw e
} else { log.warn("Transaction {} has missing class: {}", ltx.id, missingClass)
throw e reverifyWithFixups(ltx, services, missingClass)
}
} catch (e: NotSerializableException) { } catch (e: NotSerializableException) {
if (e.cause is ClassNotFoundException && e.cause!!.message != null) { checkReverifyAllowed(e)
verifyWithExtraDependency(e.cause!!.message!!.replace(".", "/"), ltx, services, e) retryVerification(e, e, ltx, services)
} else {
throw e
}
} catch (e: TransactionDeserialisationException) { } catch (e: TransactionDeserialisationException) {
if (e.cause is NotSerializableException && e.cause.cause is ClassNotFoundException && e.cause.cause!!.message != null) { checkReverifyAllowed(e)
verifyWithExtraDependency(e.cause.cause!!.message!!.replace(".", "/"), ltx, services, e) retryVerification(e.cause, e, ltx, services)
} else {
throw e
} }
} }
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. // 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. // This code has detected a missing custom serializer - probably located inside a workflow CorDapp.
// When it finds one, it instructs the verifier to use it to create the transaction classloader. // We need to extract this CorDapp from AttachmentStorage and try verifying this transaction again.
private fun verifyWithExtraDependency(missingClass: String, ltx: LedgerTransaction, services: ServiceHub, exception: Throwable) { @DeleteForDJVM
// If that transaction was created with and after Corda 4 then just fail. private fun reverifyWithFixups(ltx: LedgerTransaction, services: ServiceHub, missingClass: String?) {
// The lenient dependency verification is only supported for Corda 3 transactions. log.warn("""Detected that transaction $id does not contain all cordapp dependencies.
// 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 may be the result of a bug in a previous version of Corda. |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()) |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 package net.corda.core.transactions
import co.paralleluniverse.strands.Strand import co.paralleluniverse.strands.Strand
@ -5,7 +6,6 @@ import net.corda.core.CordaInternal
import net.corda.core.DeleteForDJVM import net.corda.core.DeleteForDJVM
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignableData
import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.SignatureMetadata
import net.corda.core.identity.Party import net.corda.core.identity.Party
@ -47,7 +47,7 @@ open class TransactionBuilder(
var notary: Party? = null, var notary: Party? = null,
var lockId: UUID = defaultLockId(), var lockId: UUID = defaultLockId(),
protected val inputs: MutableList<StateRef> = arrayListOf(), 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 outputs: MutableList<TransactionState<ContractState>> = arrayListOf(),
protected val commands: MutableList<Command<*>> = arrayListOf(), protected val commands: MutableList<Command<*>> = arrayListOf(),
protected var window: TimeWindow? = null, protected var window: TimeWindow? = null,
@ -58,7 +58,7 @@ open class TransactionBuilder(
constructor(notary: Party? = null, constructor(notary: Party? = null,
lockId: UUID = defaultLockId(), lockId: UUID = defaultLockId(),
inputs: MutableList<StateRef> = arrayListOf(), inputs: MutableList<StateRef> = arrayListOf(),
attachments: MutableList<SecureHash> = arrayListOf(), attachments: MutableList<AttachmentId> = arrayListOf(),
outputs: MutableList<TransactionState<ContractState>> = arrayListOf(), outputs: MutableList<TransactionState<ContractState>> = arrayListOf(),
commands: MutableList<Command<*>> = arrayListOf(), commands: MutableList<Command<*>> = arrayListOf(),
window: TimeWindow? = null, window: TimeWindow? = null,
@ -70,14 +70,23 @@ open class TransactionBuilder(
private companion object { private companion object {
private fun defaultLockId() = (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID() private fun defaultLockId() = (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID()
private val log = contextLogger() 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 const val ID_PATTERN = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*"
private val FQCP: Pattern = Pattern.compile("$ID_PATTERN(/$ID_PATTERN)+") private val FQCP: Pattern = Pattern.compile("$ID_PATTERN(/$ID_PATTERN)+")
private fun isValidJavaClass(identifier: String) = FQCP.matcher(identifier).matches() 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 inputsWithTransactionState = arrayListOf<StateAndRef<ContractState>>()
private val referencesWithTransactionState = arrayListOf<TransactionState<ContractState>>() private val referencesWithTransactionState = arrayListOf<TransactionState<ContractState>>()
private val excludedAttachments = arrayListOf<AttachmentId>()
/** /**
* Creates a copy of the builder. * Creates a copy of the builder.
@ -106,7 +115,7 @@ open class TransactionBuilder(
when (t) { when (t) {
is StateAndRef<*> -> addInputState(t) is StateAndRef<*> -> addInputState(t)
is ReferencedStateAndRef<*> -> addReferenceState(t) is ReferencedStateAndRef<*> -> addReferenceState(t)
is SecureHash -> addAttachment(t) is AttachmentId -> addAttachment(t)
is TransactionState<*> -> addOutputState(t) is TransactionState<*> -> addOutputState(t)
is StateAndContract -> addOutputState(t.state, t.contract) is StateAndContract -> addOutputState(t.state, t.contract)
is ContractState -> throw UnsupportedOperationException("Removed as of V1: please use a StateAndContract instead") 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 ZoneVersionTooLowException if there are reference states and the zone minimum platform version is less than 4.
*/ */
@Throws(MissingContractAttachments::class) @Throws(MissingContractAttachments::class)
fun toWireTransaction(services: ServicesForResolution): WireTransaction = toWireTransactionWithContext(services) fun toWireTransaction(services: ServicesForResolution): WireTransaction = toWireTransactionWithContext(services, null)
@CordaInternal @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() val referenceStates = referenceStates()
if (referenceStates.isNotEmpty()) { if (referenceStates.isNotEmpty()) {
services.ensureMinimumPlatformVersion(4, "Reference states") 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. // Final sanity check that all states have the correct constraints.
for (state in (inputsWithTransactionState.map { it.state } + resolvedOutputs)) { for (state in (inputsWithTransactionState.map { it.state } + resolvedOutputs)) {
@ -150,7 +169,8 @@ open class TransactionBuilder(
inputStates(), inputStates(),
resolvedOutputs, resolvedOutputs,
commands(), 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, notary,
window, window,
referenceStates, referenceStates,
@ -163,10 +183,10 @@ open class TransactionBuilder(
// This is a workaround as the current version of Corda does not support cordapp dependencies. // 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. // 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. // TODO - remove once proper support for cordapp dependencies is added.
val addedDependency = addMissingDependency(services, wireTx) val addedDependency = addMissingDependency(services, wireTx, tryCount)
return if (addedDependency) return if (addedDependency)
toWireTransactionWithContext(services, serializationContext) toWireTransactionWithContext(services, serializationContext, tryCount + 1)
else else
wireTx wireTx
} }
@ -181,7 +201,7 @@ open class TransactionBuilder(
/** /**
* @return true if a new dependency was successfully added. * @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 { return try {
wireTx.toLedgerTransaction(services).verify() wireTx.toLedgerTransaction(services).verify()
// The transaction verified successfully without adding any extra dependency. // The transaction verified successfully without adding any extra dependency.
@ -192,8 +212,14 @@ open class TransactionBuilder(
when { when {
// Handle various exceptions that can be thrown during verification and drill down the wrappings. // Handle various exceptions that can be thrown during verification and drill down the wrappings.
// Note: this is a best effort to preserve backwards compatibility. // Note: this is a best effort to preserve backwards compatibility.
rootError is ClassNotFoundException -> addMissingAttachment((rootError.message ?: throw e).replace(".", "/"), services, e) rootError is ClassNotFoundException -> {
rootError is NoClassDefFoundError -> addMissingAttachment(rootError.message ?: throw e, services, e) ((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. // Ignore these exceptions as they will break unit tests.
// The point here is only to detect missing dependencies. The other exceptions are irrelevant. // 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 { private fun addMissingAttachment(missingClass: String, services: ServicesForResolution, originalException: Throwable): Boolean {
if (!isValidJavaClass(missingClass)) { if (!isValidJavaClass(missingClass)) {
log.warn("Could not autodetect a valid attachment for the transaction being built.") log.warn("Could not autodetect a valid attachment for the transaction being built.")
throw originalException 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) 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. * This method is responsible for selecting the contract versions to be used for the current transaction and resolve the output state
* The contract attachments are used to create a deterministic Classloader to deserialise the transaction and to run the contract verification. * [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) * TODO also on the versions of the attachments of the transactions generating the input states. ( after we add versioning)
*/ */
private fun selectContractAttachmentsAndOutputStateConstraints( private fun selectContractAttachmentsAndOutputStateConstraints(
services: ServicesForResolution, services: ServicesForResolution,
@Suppress("UNUSED_PARAMETER") serializationContext: SerializationContext? @Suppress("UNUSED_PARAMETER") serializationContext: SerializationContext?
): Pair<Collection<SecureHash>, List<TransactionState<ContractState>>> { ): Pair<Collection<AttachmentId>, List<TransactionState<ContractState>>> {
// Determine the explicitly set contract attachments. // 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) .map(services.attachments::openAttachment)
.mapNotNull { it as? ContractAttachment } .mapNotNull { it as? ContractAttachment }
.flatMap { attch -> .flatMap { attch ->
@ -260,9 +326,12 @@ open class TransactionBuilder(
} }
// And fail early if there's more than 1 for a contract. // 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 } val inputContractGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = inputsWithTransactionState.map { it.state }
.groupBy { it.contract } .groupBy { it.contract }
@ -272,7 +341,8 @@ open class TransactionBuilder(
// Handle reference states. // Handle reference states.
// Filter out all contracts that might have been already used by 'normal' input or output 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 val refStateContractAttachments: List<AttachmentId> = referenceStateGroups
.filterNot { it.key in allContracts } .filterNot { it.key in allContracts }
.map { refStateEntry -> .map { refStateEntry ->
@ -659,7 +729,7 @@ open class TransactionBuilder(
} }
/** Adds an attachment with the specified hash to the TransactionBuilder. */ /** Adds an attachment with the specified hash to the TransactionBuilder. */
fun addAttachment(attachmentId: SecureHash) = apply { fun addAttachment(attachmentId: AttachmentId) = apply {
attachments.add(attachmentId) attachments.add(attachmentId)
} }
@ -750,7 +820,7 @@ open class TransactionBuilder(
fun referenceStates(): List<StateRef> = ArrayList(references) fun referenceStates(): List<StateRef> = ArrayList(references)
/** Returns an immutable list of attachment hashes. */ /** 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. */ /** Returns an immutable list of output [TransactionState]s. */
fun outputStates(): List<TransactionState<*>> = ArrayList(outputs) fun outputStates(): List<TransactionState<*>> = ArrayList(outputs)

View File

@ -7,6 +7,11 @@ release, see :doc:`app-upgrade-notes`.
Unreleased 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 * ``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. 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") @Suppress("unused")
class CustomSerializerContract : Contract { class CustomSerializerContract : Contract {
companion object { companion object {
const val MAX_CURRANT = 2000 const val MAX_CURRANT = 2000L
} }
override fun verify(tx: LedgerTransaction) { override fun verify(tx: LedgerTransaction) {
val currantsyData = tx.outputsOfType(CurrantsyState::class.java) val currantsyData = tx.outputsOfType<CurrantsyState>()
require(currantsyData.isNotEmpty()) { require(currantsyData.isNotEmpty()) {
"Requires at least one currantsy state" "Requires at least one currantsy state"
} }
currantsyData.forEach { currantsyData.forEach {
require(it.currantsy.currants in 0..MAX_CURRANT) { require(it.currantsy in Currantsy(0)..Currantsy(MAX_CURRANT)) {
"Too many currants! ${it.currantsy.currants} is unraisinable!" "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.client.rpc.CordaRPCClient
import net.corda.contracts.serialization.custom.Currantsy 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.messaging.startFlow
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.flows.serialization.custom.CustomSerializerFlow 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.driver.internal.incrementalPortAllocation
import net.corda.testing.node.NotarySpec import net.corda.testing.node.NotarySpec
import net.corda.testing.node.User import net.corda.testing.node.User
import net.corda.testing.node.internal.CustomCordapp
import net.corda.testing.node.internal.cordappWithPackages import net.corda.testing.node.internal.cordappWithPackages
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.BeforeClass
import org.junit.Test import org.junit.Test
import org.junit.jupiter.api.assertThrows import kotlin.test.assertFailsWith
@Suppress("FunctionName") @Suppress("FunctionName")
class ContractWithCustomSerializerTest { class ContractWithCustomSerializerTest {
companion object { companion object {
const val CURRANTS = 5000L const val CURRANTS = 5000L
@BeforeClass
@JvmStatic
fun checkData() {
assertNotCordaSerializable<Currantsy>()
}
} }
@Test @Test
@ -34,23 +40,21 @@ class ContractWithCustomSerializerTest {
startNodesInProcess = false, startNodesInProcess = false,
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = true)), notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = true)),
cordappsForAllNodes = listOf( cordappsForAllNodes = listOf(
cordappWithPackages("net.corda.flows.serialization.custom"), cordappWithPackages("net.corda.flows.serialization.custom").signed(),
CustomCordapp( cordappWithPackages("net.corda.contracts.serialization.custom").signed()
packages = setOf("net.corda.contracts.serialization.custom"),
name = "has-custom-serializer"
).signed()
) )
)) { )) {
val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
val ex = assertThrows<TransactionVerificationException> { val ex = assertFailsWith<ContractRejection> {
CordaRPCClient(hostAndPort = alice.rpcAddress) CordaRPCClient(hostAndPort = alice.rpcAddress)
.start(user.username, user.password) .start(user.username, user.password)
.proxy .use { client ->
.startFlow(::CustomSerializerFlow, Currantsy(CURRANTS)) client.proxy.startFlow(::CustomSerializerFlow, Currantsy(CURRANTS))
.returnValue .returnValue
.getOrThrow() .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 networkParametersStorage
).closeOnStop() ).closeOnStop()
@Suppress("LeakingThis") @Suppress("LeakingThis")
val transactionVerifierService = InMemoryTransactionVerifierService(transactionVerifierWorkerCount).tokenize() val transactionVerifierService = InMemoryTransactionVerifierService(
numberOfWorkers = transactionVerifierWorkerCount,
cordappProvider = cordappProvider,
attachments = attachments
).tokenize()
val verifierFactoryService: VerifierFactoryService = if (djvmCordaSource != null) { val verifierFactoryService: VerifierFactoryService = if (djvmCordaSource != null) {
DeterministicVerifierFactoryService(djvmBootstrapSource, djvmCordaSource).apply { DeterministicVerifierFactoryService(djvmBootstrapSource, djvmCordaSource).apply {
log.info("DJVM sandbox enabled for deterministic contract verification.") 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.DemoClock
import net.corda.node.utilities.errorAndTerminate import net.corda.node.utilities.errorAndTerminate
import net.corda.nodeapi.internal.ArtemisMessagingClient import net.corda.nodeapi.internal.ArtemisMessagingClient
import net.corda.nodeapi.internal.ArtemisMessagingComponent
import net.corda.nodeapi.internal.ShutdownHook import net.corda.nodeapi.internal.ShutdownHook
import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.addShutdownHook
import net.corda.nodeapi.internal.bridging.BridgeControlListener import net.corda.nodeapi.internal.bridging.BridgeControlListener

View File

@ -1,6 +1,7 @@
package net.corda.node.internal.cordapp package net.corda.node.internal.cordapp
import com.google.common.collect.HashBiMap import com.google.common.collect.HashBiMap
import net.corda.core.contracts.Attachment
import net.corda.core.contracts.ContractClassName import net.corda.core.contracts.ContractClassName
import net.corda.core.cordapp.Cordapp import net.corda.core.cordapp.Cordapp
import net.corda.core.cordapp.CordappContext 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.flows.FlowLogic
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
import net.corda.core.internal.cordapp.CordappImpl 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.AttachmentId
import net.corda.core.node.services.AttachmentStorage import net.corda.core.node.services.AttachmentStorage
import net.corda.core.serialization.MissingAttachmentsException
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.node.services.persistence.AttachmentStorageInternal import net.corda.node.services.persistence.AttachmentStorageInternal
import net.corda.nodeapi.internal.cordapp.CordappLoader import net.corda.nodeapi.internal.cordapp.CordappLoader
import java.net.JarURLConnection
import java.net.URL import java.net.URL
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.jar.JarFile
/** /**
* Cordapp provider and store. For querying CorDapps for their attachment and vice versa. * 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 contextCache = ConcurrentHashMap<Cordapp, CordappContext>()
private val cordappAttachments = HashBiMap.create<SecureHash, URL>() private val cordappAttachments = HashBiMap.create<SecureHash, URL>()
private val attachmentFixups = arrayListOf<AttachmentFixup>()
/** /**
* Current known CorDapps loaded on this node * Current known CorDapps loaded on this node
@ -38,6 +45,8 @@ open class CordappProviderImpl(val cordappLoader: CordappLoader,
fun start() { fun start() {
cordappAttachments.putAll(loadContractsIntoAttachmentStore()) cordappAttachments.putAll(loadContractsIntoAttachmentStore())
verifyInstalledCordapps() verifyInstalledCordapps()
// Load the fix-ups after uploading any new contracts into attachment storage.
attachmentFixups.addAll(loadAttachmentFixups())
} }
private fun verifyInstalledCordapps() { private fun verifyInstalledCordapps() {
@ -95,6 +104,92 @@ open class CordappProviderImpl(val cordappLoader: CordappLoader,
} to cordapp.jarPath } to cordapp.jarPath
}.toMap() }.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 * Get the current cordapp context for the given CorDapp
* *

View File

@ -1,11 +1,14 @@
package net.corda.node.internal.cordapp package net.corda.node.internal.cordapp
import net.corda.core.contracts.Attachment
import net.corda.core.cordapp.Cordapp import net.corda.core.cordapp.Cordapp
import net.corda.core.cordapp.CordappProvider import net.corda.core.cordapp.CordappProvider
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.internal.CordappFixupInternal
import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.internal.cordapp.CordappImpl
interface CordappProviderInternal : CordappProvider { interface CordappProviderInternal : CordappProvider, CordappFixupInternal {
val cordapps: List<CordappImpl> val cordapps: List<CordappImpl>
fun getCordappForFlow(flowLogic: FlowLogic<*>): Cordapp? 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 session = currentDBSession()
val criteriaBuilder = session.criteriaBuilder val criteriaBuilder = session.criteriaBuilder
val criteriaQuery = criteriaBuilder.createQuery(Long::class.java) 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 val count = session.createQuery(criteriaQuery).singleResult
attachmentCount.inc(count) attachmentCount.inc(count)
} }
@ -278,9 +278,9 @@ class NodeAttachmentService @JvmOverloads constructor(
loadFunction = { Optional.ofNullable(loadAttachmentContent(it)) } loadFunction = { Optional.ofNullable(loadAttachmentContent(it)) }
) )
private fun loadAttachmentContent(id: SecureHash): Pair<Attachment, ByteArray>? { private fun loadAttachmentContent(id: AttachmentId): Pair<Attachment, ByteArray>? {
return database.transaction { 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 ?: return@transaction null
Pair(createAttachmentFromDatabase(attachment), attachment.content) Pair(createAttachmentFromDatabase(attachment), attachment.content)
} }
@ -363,7 +363,7 @@ class NodeAttachmentService @JvmOverloads constructor(
} }
override fun hasAttachment(attachmentId: AttachmentId): Boolean = database.transaction { 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) = private fun increaseDefaultVersionIfWhitelistedAttachment(contractClassNames: List<ContractClassName>, contractVersionFromFile: Int, attachmentId: AttachmentId) =
@ -400,7 +400,7 @@ class NodeAttachmentService @JvmOverloads constructor(
val jarSigners = getSigners(bytes) val jarSigners = getSigners(bytes)
val contractVersion = increaseDefaultVersionIfWhitelistedAttachment(contractClassNames, getVersion(bytes), id) val contractVersion = increaseDefaultVersionIfWhitelistedAttachment(contractClassNames, getVersion(bytes), id)
val session = currentDBSession() val session = currentDBSession()
val attachment = NodeAttachmentService.DBAttachment( val attachment = DBAttachment(
attId = id.toString(), attId = id.toString(),
content = bytes, content = bytes,
uploader = uploader, uploader = uploader,
@ -417,7 +417,7 @@ class NodeAttachmentService @JvmOverloads constructor(
} }
if (isUploaderTrusted(uploader)) { if (isUploaderTrusted(uploader)) {
val session = currentDBSession() 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) // update the `uploader` field (as the existing attachment may have been resolved from a peer)
if (attachment.uploader != uploader) { if (attachment.uploader != uploader) {
attachment.uploader = uploader attachment.uploader = uploader
@ -438,7 +438,7 @@ class NodeAttachmentService @JvmOverloads constructor(
} }
private fun getSigners(attachmentBytes: ByteArray) = private fun getSigners(attachmentBytes: ByteArray) =
JarSignatureCollector.collectSigners(JarInputStream(attachmentBytes.inputStream())) JarInputStream(attachmentBytes.inputStream()).use(JarSignatureCollector::collectSigners)
private fun getVersion(attachmentBytes: ByteArray) = private fun getVersion(attachmentBytes: ByteArray) =
JarInputStream(attachmentBytes.inputStream()).use { 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.concurrent.CordaFuture
import net.corda.core.contracts.Attachment import net.corda.core.contracts.Attachment
import net.corda.core.contracts.TransactionVerificationException.BrokenTransactionException
import net.corda.core.internal.TransactionVerifierServiceInternal import net.corda.core.internal.TransactionVerifierServiceInternal
import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.concurrent.openFuture
import net.corda.core.internal.internalFindTrustedAttachmentForClass
import net.corda.core.internal.prepareVerify import net.corda.core.internal.prepareVerify
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.node.services.TransactionVerifierService import net.corda.core.node.services.TransactionVerifierService
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.transactions.LedgerTransaction 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 import net.corda.nodeapi.internal.persistence.withoutDatabaseAccess
class InMemoryTransactionVerifierService(@Suppress("UNUSED_PARAMETER") numberOfWorkers: Int) : SingletonSerializeAsToken(), TransactionVerifierService, TransactionVerifierServiceInternal, AutoCloseable { class InMemoryTransactionVerifierService(
override fun verify(transaction: LedgerTransaction): CordaFuture<Unit> = this.verify(transaction, emptyList()) @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 { return openFuture<Unit>().apply {
capture { 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 { withoutDatabaseAccess {
verifier.verify() verifier.verify()
} }

View File

@ -3,6 +3,24 @@ apply plugin: 'net.corda.plugins.cordapp'
def javaHome = System.getProperty('java.home') def javaHome = System.getProperty('java.home')
def shrinkJar = file("$buildDir/libs/${project.name}-${project.version}-tiny.jar") 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 { cordapp {
targetPlatformVersion = corda_platform_version.toInteger() targetPlatformVersion = corda_platform_version.toInteger()
minimumPlatformVersion 1 minimumPlatformVersion 1
@ -42,6 +60,18 @@ dependencies {
compile "com.opengamma.strata:strata-market:$strata_version" 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 { jar {
classifier = 'fat' 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 { override fun generateAgreement(notary: Party): TransactionBuilder {
val state = IRSState(swap, buyer, seller) 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 { 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 { 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.addInputState(oldState)
tx.addOutputState(copy(portfolio = portfolio, valuation = valuation), PORTFOLIO_SWAP_PROGRAM_ID) tx.addOutputState(copy(portfolio = portfolio, valuation = valuation), PORTFOLIO_SWAP_PROGRAM_ID)
tx.addCommand(PortfolioSwap.Commands.Update(), participants.map { it.owningKey }) tx.addCommand(PortfolioSwap.Commands.Update(), participants.map { it.owningKey })
tx.withExternalCordapps()
return tx return tx
} }
} }

View File

@ -24,7 +24,7 @@ data class SerializationFactoryCacheKey(val classWhitelist: ClassWhitelist,
val preventDataLoss: Boolean, val preventDataLoss: Boolean,
val customSerializers: Set<SerializationCustomSerializer<*, *>>) val customSerializers: Set<SerializationCustomSerializer<*, *>>)
fun SerializerFactory.addToWhitelist(vararg types: Class<*>) { fun SerializerFactory.addToWhitelist(types: Collection<Class<*>>) {
require(types.toSet().size == types.size) { require(types.toSet().size == types.size) {
val duplicates = types.toMutableList() val duplicates = types.toMutableList()
types.toSet().forEach { duplicates -= it } types.toSet().forEach { duplicates -= it }
@ -71,11 +71,11 @@ abstract class AbstractAMQPSerializationScheme(
@DeleteForDJVM @DeleteForDJVM
val List<Cordapp>.customSerializers val List<Cordapp>.customSerializers
get() = flatMap { it.serializationCustomSerializers }.toSet() get() = flatMapTo(LinkedHashSet(), Cordapp::serializationCustomSerializers)
@DeleteForDJVM @DeleteForDJVM
val List<Cordapp>.serializationWhitelists val List<Cordapp>.serializationWhitelists
get() = flatMap { it.serializationWhitelists }.toSet() get() = flatMapTo(LinkedHashSet(), Cordapp::serializationWhitelists)
} }
private fun registerCustomSerializers(context: SerializationContext, factory: SerializerFactory) { private fun registerCustomSerializers(context: SerializationContext, factory: SerializerFactory) {
@ -93,8 +93,12 @@ abstract class AbstractAMQPSerializationScheme(
factory.registerExternal(CorDappCustomSerializer(customSerializer, factory)) factory.registerExternal(CorDappCustomSerializer(customSerializer, factory))
} }
cordappCustomSerializers.forEach { customSerializer -> cordappCustomSerializers.forEach { customSerializer ->
// 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)) factory.registerExternal(CorDappCustomSerializer(customSerializer, factory))
} }
}
context.properties[ContextPropertyKeys.SERIALIZERS]?.apply { context.properties[ContextPropertyKeys.SERIALIZERS]?.apply {
uncheckedCast<Any, List<CustomSerializer<out Any>>>(this).forEach { uncheckedCast<Any, List<CustomSerializer<out Any>>>(this).forEach {
@ -105,12 +109,10 @@ abstract class AbstractAMQPSerializationScheme(
private fun registerCustomWhitelists(factory: SerializerFactory) { private fun registerCustomWhitelists(factory: SerializerFactory) {
serializationWhitelists.forEach { serializationWhitelists.forEach {
factory.addToWhitelist(*it.whitelist.toTypedArray()) factory.addToWhitelist(it.whitelist)
} }
cordappSerializationWhitelists.forEach { cordappSerializationWhitelists.forEach {
it.whitelist.forEach { clazz -> factory.addToWhitelist(it.whitelist)
factory.addToWhitelist(clazz)
}
} }
} }

View File

@ -1,6 +1,7 @@
package net.corda.serialization.internal.amqp package net.corda.serialization.internal.amqp
import net.corda.core.serialization.SerializationContext 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.LocalConstructorInformation
import net.corda.serialization.internal.model.LocalPropertyInformation import net.corda.serialization.internal.model.LocalPropertyInformation
import net.corda.serialization.internal.model.LocalTypeInformation import net.corda.serialization.internal.model.LocalTypeInformation
@ -21,7 +22,11 @@ interface ObjectSerializer : AMQPSerializer<Any> {
companion object { companion object {
fun make(typeInformation: LocalTypeInformation, factory: LocalSerializerFactory): ObjectSerializer { fun make(typeInformation: LocalTypeInformation, factory: LocalSerializerFactory): ObjectSerializer {
if (typeInformation is LocalTypeInformation.NonComposable) { 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) val typeDescriptor = factory.createDescriptor(typeInformation)

View File

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

View File

@ -1,7 +1,6 @@
package net.corda.serialization.internal.model package net.corda.serialization.internal.model
import java.lang.reflect.* import java.lang.reflect.*
import kotlin.reflect.KFunction
import java.util.* import java.util.*
typealias PropertyName = String 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. * Get the map of [LocalPropertyInformation], for all types that have it, or an empty map otherwise.
*/ */
val propertiesOrEmptyMap: Map<PropertyName, LocalPropertyInformation> get() = when(this) { val propertiesOrEmptyMap: Map<PropertyName, LocalPropertyInformation> get() = when(this) {
is LocalTypeInformation.Composable -> properties is Composable -> properties
is LocalTypeInformation.Abstract -> properties is Abstract -> properties
is LocalTypeInformation.AnInterface -> properties is AnInterface -> properties
is LocalTypeInformation.NonComposable -> properties is NonComposable -> properties
is LocalTypeInformation.Opaque -> wrapped.propertiesOrEmptyMap is Opaque -> wrapped.propertiesOrEmptyMap
else -> emptyMap() 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. * Get the list of interfaces, for all types that have them, or an empty list otherwise.
*/ */
val interfacesOrEmptyList: List<LocalTypeInformation> get() = when(this) { val interfacesOrEmptyList: List<LocalTypeInformation> get() = when(this) {
is LocalTypeInformation.Composable -> interfaces is Composable -> interfaces
is LocalTypeInformation.Abstract -> interfaces is Abstract -> interfaces
is LocalTypeInformation.AnInterface -> interfaces is AnInterface -> interfaces
is LocalTypeInformation.NonComposable -> interfaces is NonComposable -> interfaces
else -> emptyList() 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 * 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. * creating a new instance from a dictionary of property values.
* *
* @param constructor [LocalConstructorInformation] for the constructor of this type, if there is one. * @property constructor [LocalConstructorInformation] for the constructor of this type, if there is one.
* @param properties [LocalPropertyInformation] for the properties of the interface. * @property properties [LocalPropertyInformation] for the properties of the interface.
* @param superclass [LocalTypeInformation] for the superclass of the underlying class of this type. * @property superclass [LocalTypeInformation] for the superclass of the underlying class of this type.
* @param interfaces [LocalTypeInformation] for the interfaces extended by this interface. * @property interfaces [LocalTypeInformation] for the interfaces extended by this interface.
* @param typeParameters [LocalTypeInformation] for the resolved type parameters of the type. * @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( data class NonComposable(
override val observedType: Type, override val observedType: Type,
@ -260,8 +260,11 @@ sealed class LocalTypeInformation {
val superclass: LocalTypeInformation, val superclass: LocalTypeInformation,
val interfaces: List<LocalTypeInformation>, val interfaces: List<LocalTypeInformation>,
val typeParameters: List<LocalTypeInformation>, val typeParameters: List<LocalTypeInformation>,
private val nonComposableSubtypes: Set<NonComposable>,
val reason: String, 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. * 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.internal.kotlinObjectInstance
import net.corda.core.serialization.ConstructorForDeserialization import net.corda.core.serialization.ConstructorForDeserialization
import net.corda.core.serialization.DeprecatedConstructorForDeserialization import net.corda.core.serialization.DeprecatedConstructorForDeserialization
import net.corda.core.utilities.contextLogger
import net.corda.serialization.internal.NotSerializableDetailedException import net.corda.serialization.internal.NotSerializableDetailedException
import net.corda.serialization.internal.amqp.* import net.corda.serialization.internal.amqp.*
import java.io.NotSerializableException import java.io.NotSerializableException
@ -38,11 +37,6 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
var visited: Set<TypeIdentifier> = emptySet(), var visited: Set<TypeIdentifier> = emptySet(),
val cycles: MutableList<LocalTypeInformation.Cycle> = mutableListOf(), val cycles: MutableList<LocalTypeInformation.Cycle> = mutableListOf(),
var validateProperties: Boolean = true) { 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 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]. * if the [LocalTypeInformation] for that type (or any of its related types) is [LocalTypeInformation.NonComposable].
@ -202,10 +196,7 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
private fun buildNonAtomic(rawType: Class<*>, type: Type, typeIdentifier: TypeIdentifier, typeParameterInformation: List<LocalTypeInformation>): LocalTypeInformation { private fun buildNonAtomic(rawType: Class<*>, type: Type, typeIdentifier: TypeIdentifier, typeParameterInformation: List<LocalTypeInformation>): LocalTypeInformation {
val superclassInformation = buildSuperclassInformation(type) val superclassInformation = buildSuperclassInformation(type)
val interfaceInformation = buildInterfaceInformation(type) val interfaceInformation = buildInterfaceInformation(type)
val observedConstructor = constructorForDeserialization(type) val observedConstructor = constructorForDeserialization(type) ?: return LocalTypeInformation.NonComposable(
if (observedConstructor == null) {
return LocalTypeInformation.NonComposable(
observedType = type, observedType = type,
typeIdentifier = typeIdentifier, typeIdentifier = typeIdentifier,
constructor = null, constructor = null,
@ -218,16 +209,17 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
superclass = superclassInformation, superclass = superclassInformation,
interfaces = interfaceInformation, interfaces = interfaceInformation,
typeParameters = typeParameterInformation, typeParameters = typeParameterInformation,
nonComposableSubtypes = emptySet(),
reason = "No unique deserialization constructor can be identified", 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" remedy = "Either annotate a constructor for this type with @ConstructorForDeserialization, or provide a custom serializer for it"
) )
}
val constructorInformation = buildConstructorInformation(type, observedConstructor) val constructorInformation = buildConstructorInformation(type, observedConstructor)
val properties = buildObjectProperties(rawType, constructorInformation) val properties = buildObjectProperties(rawType, constructorInformation)
if (!propertiesSatisfyConstructor(constructorInformation, properties)) { 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( return LocalTypeInformation.NonComposable(
observedType = type, observedType = type,
typeIdentifier = typeIdentifier, typeIdentifier = typeIdentifier,
@ -236,6 +228,8 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
superclass = superclassInformation, superclass = superclassInformation,
interfaces = interfaceInformation, interfaces = interfaceInformation,
typeParameters = typeParameterInformation, typeParameters = typeParameterInformation,
nonComposableSubtypes = missingConstructorProperties
.filterIsInstanceTo(LinkedHashSet(), LocalTypeInformation.NonComposable::class.java),
reason = "Mandatory constructor parameters $missingParameters are missing from the readable properties ${properties.keys}", 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" 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, superclass = superclassInformation,
interfaces = interfaceInformation, interfaces = interfaceInformation,
typeParameters = typeParameterInformation, typeParameters = typeParameterInformation,
nonComposableSubtypes = nonComposableProperties.values.mapTo(LinkedHashSet()) {
it.type as LocalTypeInformation.NonComposable
},
reason = nonComposablePropertiesErrorReason(nonComposableProperties), reason = nonComposablePropertiesErrorReason(nonComposableProperties),
remedy = "Either ensure that the properties ${nonComposableProperties.keys} are serializable, or provide a custom serializer for this type" 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() }.toSet()
return (0 until constructorInformation.parameters.size).none { index -> return (constructorInformation.parameters.indices).none { index ->
constructorInformation.parameters[index].isMandatory && index !in indicesAddressedByProperties constructorInformation.parameters[index].isMandatory && index !in indicesAddressedByProperties
} }
} }
@ -298,7 +295,7 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
} }
}.toSet() }.toSet()
return (0 until constructorInformation.parameters.size).mapNotNull { index -> return (constructorInformation.parameters.indices).mapNotNull { index ->
val parameter = constructorInformation.parameters[index] val parameter = constructorInformation.parameters[index]
when { when {
constructorInformation.parameters[index].isMandatory && index !in indicesAddressedByProperties -> parameter constructorInformation.parameters[index].isMandatory && index !in indicesAddressedByProperties -> parameter

View File

@ -77,7 +77,7 @@ open class MockServices private constructor(
private val cordappLoader: CordappLoader, private val cordappLoader: CordappLoader,
override val validatedTransactions: TransactionStorage, override val validatedTransactions: TransactionStorage,
override val identityService: IdentityService, override val identityService: IdentityService,
private val initialNetworkParameters: NetworkParameters, initialNetworkParameters: NetworkParameters,
private val initialIdentity: TestIdentity, private val initialIdentity: TestIdentity,
private val moreKeys: Array<out KeyPair>, private val moreKeys: Array<out KeyPair>,
override val keyManagementService: KeyManagementService = MockKeyManagementService( override val keyManagementService: KeyManagementService = MockKeyManagementService(
@ -425,10 +425,14 @@ open class MockServices private constructor(
get() { get() {
return NodeInfo(listOf(NetworkHostAndPort("mock.node.services", 10000)), listOf(initialIdentity.identity), 1, serial = 1L) 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 { private val mockCordappProvider: MockCordappProvider = MockCordappProvider(cordappLoader, attachments).also {
it.start() it.start()
} }
override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(
numberOfWorkers = 2,
cordappProvider = mockCordappProvider,
attachments = attachments
)
override val cordappProvider: CordappProvider get() = mockCordappProvider override val cordappProvider: CordappProvider get() = mockCordappProvider
override var networkParametersService: NetworkParametersService = MockNetworkParametersStorage(initialNetworkParameters) override var networkParametersService: NetworkParametersService = MockNetworkParametersStorage(initialNetworkParameters)
override val diagnosticsService: DiagnosticsService = NodeDiagnosticsService() 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.*
import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.internal.cordapp.set import net.corda.core.internal.cordapp.set
import net.corda.core.node.services.AttachmentFixup
import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializationWhitelist
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug import net.corda.core.utilities.debug
@ -32,18 +33,21 @@ data class CustomCordapp(
val versionId: Int = 1, val versionId: Int = 1,
val targetPlatformVersion: Int = PLATFORM_VERSION, val targetPlatformVersion: Int = PLATFORM_VERSION,
val classes: Set<Class<*>> = emptySet(), val classes: Set<Class<*>> = emptySet(),
val fixups: List<AttachmentFixup> = emptyList(),
val signingInfo: SigningInfo? = null, val signingInfo: SigningInfo? = null,
override val config: Map<String, Any> = emptyMap() override val config: Map<String, Any> = emptyMap()
) : TestCordappInternal() { ) : TestCordappInternal() {
init { 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 val jarFile: Path get() = getJarFile(this)
override fun withConfig(config: Map<String, Any>): CustomCordapp = copy(config = config) 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 = fun signed(keyStorePath: Path? = null, numberOfSignatures: Int = 1, keyAlgorithm: String = "RSA"): CustomCordapp =
copy(signingInfo = SigningInfo(keyStorePath, numberOfSignatures, keyAlgorithm)) copy(signingInfo = SigningInfo(keyStorePath, numberOfSignatures, keyAlgorithm))
@ -83,20 +87,36 @@ data class CustomCordapp(
} }
} }
private fun signJar(jarFile: Path) { internal fun createFixupJar(file: Path) {
if (signingInfo != null) { JarOutputStream(file.outputStream()).use { jos ->
val keyStorePathToUse = if (signingInfo.keyStorePath != null) { jos.addEntry(testEntry(JarFile.MANIFEST_NAME)) {
signingInfo.keyStorePath createTestManifest(name, versionId, targetPlatformVersion).write(jos)
} else { }
defaultJarSignerDirectory.createDirectories() jos.addEntry(testEntry("META-INF/Corda-Fixups")) {
defaultJarSignerDirectory 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 = signingInfo.keyStorePath ?: defaultJarSignerDirectory.createDirectories()
for (i in 1 .. signingInfo.numberOfSignatures) { for (i in 1 .. signingInfo.numberOfSignatures) {
val alias = "alias$i" val alias = "alias$i"
val pwd = "secret!" 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) 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) val pk = keyStorePathToUse.signJar(jarFile.toString(), alias, pwd)
logger.debug { "Signed Jar: $jarFile with public key $pk" } 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 epochFileTime = FileTime.from(Instant.EPOCH)
private val cordappsDirectory: Path private val cordappsDirectory: Path
private val defaultJarSignerDirectory: Path private val defaultJarSignerDirectory: Path
private val whitespace = "\\s".toRegex() private val whitespace = "\\s++".toRegex()
private val cache = ConcurrentHashMap<CustomCordapp, Path>() private val cache = ConcurrentHashMap<CustomCordapp, Path>()
init { init {
@ -156,7 +176,11 @@ data class CustomCordapp(
return cache.computeIfAbsent(cordapp.copy(config = emptyMap())) { return cache.computeIfAbsent(cordapp.copy(config = emptyMap())) {
val filename = it.run { "${name.replace(whitespace, "-")}_${versionId}_${targetPlatformVersion}_${UUID.randomUUID()}.jar" } val filename = it.run { "${name.replace(whitespace, "-")}_${versionId}_${targetPlatformVersion}_${UUID.randomUUID()}.jar" }
val jarFile = cordappsDirectory.createDirectories() / filename val jarFile = cordappsDirectory.createDirectories() / filename
if (it.fixups.isNotEmpty()) {
it.createFixupJar(jarFile)
} else {
it.packageAsJar(jarFile) it.packageAsJar(jarFile)
}
it.signJar(jarFile) it.signJar(jarFile)
logger.debug { "$it packaged into $jarFile" } logger.debug { "$it packaged into $jarFile" }
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.div
import net.corda.core.internal.times import net.corda.core.internal.times
import net.corda.core.messaging.CordaRPCOps 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.SerializationEnvironment
import net.corda.core.serialization.internal._allEnabledSerializationEnvs import net.corda.core.serialization.internal._allEnabledSerializationEnvs
import net.corda.core.serialization.internal._driverSerializationEnv import net.corda.core.serialization.internal._driverSerializationEnv
@ -94,6 +95,8 @@ fun cordappWithPackages(vararg packageNames: String): CustomCordapp = CustomCord
// TODO Rename to cordappWithClasses // TODO Rename to cordappWithClasses
fun cordappForClasses(vararg classes: Class<*>): CustomCordapp = CustomCordapp(packages = emptySet(), classes = classes.toSet()) 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 * 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]. * [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 override val jarFile: Path
get() { get() {
val jars = TestCordappImpl.findJars(scanPackage) val jars = findJars(scanPackage)
when (jars.size) { when (jars.size) {
0 -> throw IllegalArgumentException("There are no CorDapps containing the package $scanPackage on the classpath. Make sure " + 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.") "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> { private fun findRootPaths(scanPackage: String): Set<Path> {
return packageToRootPaths.computeIfAbsent(scanPackage) { return packageToRootPaths.computeIfAbsent(scanPackage) {
val classGraph = ClassGraph().whitelistPaths(scanPackage.replace('.', '/')) val classGraph = ClassGraph().whitelistPaths(scanPackage.replace('.', '/'))
classGraph.pooledScan().use { classGraph.pooledScan().use { scanResult ->
it.allResources scanResult.allResources
.asSequence() .asSequence()
.map { it.classpathElementFile.toPath() } .map { it.classpathElementFile.toPath() }
.filterNot { it.toString().endsWith("-tests.jar") } .filterNot { it.toString().endsWith("-tests.jar") }
.map { if (it.toString().endsWith(".jar")) it else findProjectRoot(it) } .mapTo(LinkedHashSet()) { if (it.toString().endsWith(".jar")) it else findProjectRoot(it) }
.toSet()
} }
} }
} }