mirror of
https://github.com/corda/corda.git
synced 2025-01-18 02:39:51 +00:00
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:
parent
4669a699c0
commit
a7147c1ffd
@ -3,7 +3,6 @@ package net.corda.core.contracts
|
||||
import net.corda.core.CordaInternal
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.internal.cordapp.CordappImpl.Companion.DEFAULT_CORDAPP_VERSION
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
|
@ -267,6 +267,13 @@ abstract class TransactionVerificationException(val txId: SecureHash, message: S
|
||||
this(txId, "Couldn't find network parameters with hash: $missingNetworkParametersHash related to this transaction: $txId")
|
||||
}
|
||||
|
||||
/**
|
||||
* @param txId Id of the transaction that Corda is no longer able to verify.
|
||||
*/
|
||||
@KeepForDJVM
|
||||
class BrokenTransactionException(txId: SecureHash, message: String)
|
||||
: TransactionVerificationException(txId, message, null)
|
||||
|
||||
/** Whether the inputs or outputs list contains an encumbrance issue, see [TransactionMissingEncumbranceException]. */
|
||||
@CordaSerializable
|
||||
@KeepForDJVM
|
||||
|
@ -1,13 +1,16 @@
|
||||
@file:Suppress("TooManyFunctions")
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.cordapp.CordappProvider
|
||||
import net.corda.core.flows.DataVendingFlow
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.node.ZoneVersionTooLowException
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.node.services.AttachmentStorage
|
||||
import net.corda.core.node.services.vault.AttachmentQueryCriteria
|
||||
import net.corda.core.node.services.vault.AttachmentSort
|
||||
@ -109,6 +112,13 @@ fun noPackageOverlap(packages: Collection<String>): Boolean {
|
||||
return packages.all { outer -> packages.none { inner -> inner != outer && inner.startsWith("$outer.") } }
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The set of [AttachmentId]s after the node's fix-up rules have been applied to [attachmentIds].
|
||||
*/
|
||||
fun CordappProvider.internalFixupAttachmentIds(attachmentIds: Collection<AttachmentId>): Set<AttachmentId> {
|
||||
return (this as CordappFixupInternal).fixupAttachmentIds(attachmentIds)
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans trusted (installed locally) attachments to find all that contain the [className].
|
||||
* This is required as a workaround until explicit cordapp dependencies are implemented.
|
||||
|
@ -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>
|
||||
}
|
@ -14,17 +14,13 @@ import java.util.function.Function
|
||||
|
||||
@DeleteForDJVM
|
||||
interface TransactionVerifierServiceInternal {
|
||||
/**
|
||||
* Verifies the [transaction] but adds some [extraAttachments] to the classpath.
|
||||
* Required for transactions built with Corda 3.x that might miss some dependencies due to a bug in that version.
|
||||
*/
|
||||
fun verify(transaction: LedgerTransaction, extraAttachments: List<Attachment>): CordaFuture<*>
|
||||
fun reverifyWithFixups(transaction: LedgerTransaction, missingClass: String?): CordaFuture<*>
|
||||
}
|
||||
|
||||
/**
|
||||
* Defined here for visibility reasons.
|
||||
*/
|
||||
fun LedgerTransaction.prepareVerify(extraAttachments: List<Attachment>) = this.internalPrepareVerify(extraAttachments)
|
||||
fun LedgerTransaction.prepareVerify(attachments: List<Attachment>) = internalPrepareVerify(attachments)
|
||||
|
||||
/**
|
||||
* Because we create a separate [LedgerTransaction] onto which we need to perform verification, it becomes important we don't verify the
|
||||
|
@ -13,6 +13,7 @@ import java.io.InputStream
|
||||
import java.nio.file.FileAlreadyExistsException
|
||||
|
||||
typealias AttachmentId = SecureHash
|
||||
typealias AttachmentFixup = Pair<Set<AttachmentId>, Set<AttachmentId>>
|
||||
|
||||
/**
|
||||
* An attachment store records potentially large binary objects, identified by their hash.
|
||||
|
@ -3,7 +3,6 @@ package net.corda.core.node.services
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.DoNotImplement
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
|
||||
/**
|
||||
|
@ -325,7 +325,6 @@ object AttachmentsClassLoaderBuilder {
|
||||
val serializers = createInstancesOfClassesImplementing(transactionClassLoader, SerializationCustomSerializer::class.java)
|
||||
val whitelistedClasses = ServiceLoader.load(SerializationWhitelist::class.java, transactionClassLoader)
|
||||
.flatMap(SerializationWhitelist::whitelist)
|
||||
.toList()
|
||||
|
||||
// Create a new serializationContext for the current transaction. In this context we will forbid
|
||||
// deserialization of objects from the future, i.e. disable forwards compatibility. This is to ensure
|
||||
|
@ -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())
|
||||
}
|
@ -193,7 +193,7 @@ private constructor(
|
||||
|
||||
/**
|
||||
* Verifies this transaction and runs contract code. At this stage it is assumed that signatures have already been verified.
|
||||
|
||||
*
|
||||
* The contract verification logic is run in a custom classloader created for the current transaction.
|
||||
* This classloader is only used during verification and does not leak to the client code.
|
||||
*
|
||||
@ -204,18 +204,18 @@ private constructor(
|
||||
*/
|
||||
@Throws(TransactionVerificationException::class)
|
||||
fun verify() {
|
||||
internalPrepareVerify(emptyList()).verify()
|
||||
internalPrepareVerify(attachments).verify()
|
||||
}
|
||||
|
||||
/**
|
||||
* This method has to be called in a context where it has access to the database.
|
||||
*/
|
||||
@CordaInternal
|
||||
internal fun internalPrepareVerify(extraAttachments: List<Attachment>): Verifier {
|
||||
internal fun internalPrepareVerify(txAttachments: List<Attachment>): Verifier {
|
||||
// Switch thread local deserialization context to using a cached attachments classloader. This classloader enforces various rules
|
||||
// like no-overlap, package namespace ownership and (in future) deterministic Java.
|
||||
return AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(
|
||||
attachments + extraAttachments,
|
||||
txAttachments,
|
||||
getParamsWithGoo(),
|
||||
id,
|
||||
isAttachmentTrusted = isAttachmentTrusted) { transactionClassLoader ->
|
||||
|
@ -10,12 +10,12 @@ import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.TransactionDeserialisationException
|
||||
import net.corda.core.internal.TransactionVerifierServiceInternal
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.internal.internalFindTrustedAttachmentForClass
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.internal.MissingSerializerException
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
@ -228,51 +228,64 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
|
||||
// TODO: allow non-blocking verification.
|
||||
services.transactionVerifierService.verify(ltx).getOrThrow()
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
if (e.message != null) {
|
||||
verifyWithExtraDependency(e.message!!, ltx, services, e)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
checkReverifyAllowed(e)
|
||||
val missingClass = e.message ?: throw e
|
||||
log.warn("Transaction {} has missing class: {}", ltx.id, missingClass)
|
||||
reverifyWithFixups(ltx, services, missingClass)
|
||||
} catch (e: NotSerializableException) {
|
||||
if (e.cause is ClassNotFoundException && e.cause!!.message != null) {
|
||||
verifyWithExtraDependency(e.cause!!.message!!.replace(".", "/"), ltx, services, e)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
checkReverifyAllowed(e)
|
||||
retryVerification(e, e, ltx, services)
|
||||
} catch (e: TransactionDeserialisationException) {
|
||||
if (e.cause is NotSerializableException && e.cause.cause is ClassNotFoundException && e.cause.cause!!.message != null) {
|
||||
verifyWithExtraDependency(e.cause.cause!!.message!!.replace(".", "/"), ltx, services, e)
|
||||
} else {
|
||||
throw e
|
||||
checkReverifyAllowed(e)
|
||||
retryVerification(e.cause, e, ltx, services)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkReverifyAllowed(ex: Throwable) {
|
||||
// If that transaction was created with and after Corda 4 then just fail.
|
||||
// The lenient dependency verification is only supported for Corda 3 transactions.
|
||||
// To detect if the transaction was created before Corda 4 we check if the transaction has the NetworkParameters component group.
|
||||
if (networkParametersHash != null) {
|
||||
log.warn("TRANSACTION VERIFY FAILED - No attempt to auto-repair as TX is Corda 4+")
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteForDJVM
|
||||
@Suppress("ThrowsCount")
|
||||
private fun retryVerification(cause: Throwable?, ex: Throwable, ltx: LedgerTransaction, services: ServiceHub) {
|
||||
when (cause) {
|
||||
is MissingSerializerException -> {
|
||||
log.warn("Missing serializers: typeDescriptor={}, typeNames={}", cause.typeDescriptor ?: "<unknown>", cause.typeNames)
|
||||
reverifyWithFixups(ltx, services, null)
|
||||
}
|
||||
is NotSerializableException -> {
|
||||
val underlying = cause.cause
|
||||
if (underlying is ClassNotFoundException) {
|
||||
val missingClass = underlying.message?.replace('.', '/') ?: throw ex
|
||||
log.warn("Transaction {} has missing class: {}", ltx.id, missingClass)
|
||||
reverifyWithFixups(ltx, services, missingClass)
|
||||
} else {
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
else -> throw ex
|
||||
}
|
||||
}
|
||||
|
||||
// Transactions created before Corda 4 can be missing dependencies on other CorDapps.
|
||||
// This code attempts to find the missing dependency in the attachment storage among the trusted attachments.
|
||||
// When it finds one, it instructs the verifier to use it to create the transaction classloader.
|
||||
private fun verifyWithExtraDependency(missingClass: String, ltx: LedgerTransaction, services: ServiceHub, exception: Throwable) {
|
||||
// If that transaction was created with and after Corda 4 then just fail.
|
||||
// The lenient dependency verification is only supported for Corda 3 transactions.
|
||||
// To detect if the transaction was created before Corda 4 we check if the transaction has the NetworkParameters component group.
|
||||
if (this.networkParametersHash != null) {
|
||||
throw exception
|
||||
}
|
||||
|
||||
val attachment = requireNotNull(services.attachments.internalFindTrustedAttachmentForClass(missingClass)) {
|
||||
"""Transaction $ltx is incorrectly formed. Most likely it was created during version 3 of Corda when the verification logic was more lenient.
|
||||
|Attempted to find local dependency for class: $missingClass, but could not find one.
|
||||
|If you wish to verify this transaction, please contact the originator of the transaction and install the provided missing JAR.
|
||||
|You can install it using the RPC command: `uploadAttachment` without restarting the node.
|
||||
|""".trimMargin()
|
||||
}
|
||||
|
||||
log.warn("""Detected that transaction ${this.id} does not contain all cordapp dependencies.
|
||||
// This code has detected a missing custom serializer - probably located inside a workflow CorDapp.
|
||||
// We need to extract this CorDapp from AttachmentStorage and try verifying this transaction again.
|
||||
@DeleteForDJVM
|
||||
private fun reverifyWithFixups(ltx: LedgerTransaction, services: ServiceHub, missingClass: String?) {
|
||||
log.warn("""Detected that transaction $id does not contain all cordapp dependencies.
|
||||
|This may be the result of a bug in a previous version of Corda.
|
||||
|Attempting to verify using the additional trusted dependency: $attachment for class $missingClass.
|
||||
|Attempting to re-verify having applied this node's fix-up rules.
|
||||
|Please check with the originator that this is a valid transaction.""".trimMargin())
|
||||
|
||||
(services.transactionVerifierService as TransactionVerifierServiceInternal).verify(ltx, listOf(attachment)).getOrThrow()
|
||||
(services.transactionVerifierService as TransactionVerifierServiceInternal)
|
||||
.reverifyWithFixups(ltx, missingClass)
|
||||
.getOrThrow()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,3 +1,4 @@
|
||||
@file:Suppress("ThrowsCount", "ComplexMethod")
|
||||
package net.corda.core.transactions
|
||||
|
||||
import co.paralleluniverse.strands.Strand
|
||||
@ -5,7 +6,6 @@ import net.corda.core.CordaInternal
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.SignableData
|
||||
import net.corda.core.crypto.SignatureMetadata
|
||||
import net.corda.core.identity.Party
|
||||
@ -47,7 +47,7 @@ open class TransactionBuilder(
|
||||
var notary: Party? = null,
|
||||
var lockId: UUID = defaultLockId(),
|
||||
protected val inputs: MutableList<StateRef> = arrayListOf(),
|
||||
protected val attachments: MutableList<SecureHash> = arrayListOf(),
|
||||
protected val attachments: MutableList<AttachmentId> = arrayListOf(),
|
||||
protected val outputs: MutableList<TransactionState<ContractState>> = arrayListOf(),
|
||||
protected val commands: MutableList<Command<*>> = arrayListOf(),
|
||||
protected var window: TimeWindow? = null,
|
||||
@ -58,7 +58,7 @@ open class TransactionBuilder(
|
||||
constructor(notary: Party? = null,
|
||||
lockId: UUID = defaultLockId(),
|
||||
inputs: MutableList<StateRef> = arrayListOf(),
|
||||
attachments: MutableList<SecureHash> = arrayListOf(),
|
||||
attachments: MutableList<AttachmentId> = arrayListOf(),
|
||||
outputs: MutableList<TransactionState<ContractState>> = arrayListOf(),
|
||||
commands: MutableList<Command<*>> = arrayListOf(),
|
||||
window: TimeWindow? = null,
|
||||
@ -70,14 +70,23 @@ open class TransactionBuilder(
|
||||
private companion object {
|
||||
private fun defaultLockId() = (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID()
|
||||
private val log = contextLogger()
|
||||
private val MISSING_CLASS_DISABLED = java.lang.Boolean.getBoolean("net.corda.transactionbuilder.missingclass.disabled")
|
||||
|
||||
private const val ID_PATTERN = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*"
|
||||
private val FQCP: Pattern = Pattern.compile("$ID_PATTERN(/$ID_PATTERN)+")
|
||||
private fun isValidJavaClass(identifier: String) = FQCP.matcher(identifier).matches()
|
||||
private fun Collection<*>.deepEquals(other: Collection<*>): Boolean {
|
||||
return (size == other.size) && containsAll(other) && other.containsAll(this)
|
||||
}
|
||||
private fun Collection<AttachmentId>.toPrettyString(): String = sorted().joinToString(
|
||||
separator = System.lineSeparator(),
|
||||
prefix = System.lineSeparator()
|
||||
)
|
||||
}
|
||||
|
||||
private val inputsWithTransactionState = arrayListOf<StateAndRef<ContractState>>()
|
||||
private val referencesWithTransactionState = arrayListOf<TransactionState<ContractState>>()
|
||||
private val excludedAttachments = arrayListOf<AttachmentId>()
|
||||
|
||||
/**
|
||||
* Creates a copy of the builder.
|
||||
@ -106,7 +115,7 @@ open class TransactionBuilder(
|
||||
when (t) {
|
||||
is StateAndRef<*> -> addInputState(t)
|
||||
is ReferencedStateAndRef<*> -> addReferenceState(t)
|
||||
is SecureHash -> addAttachment(t)
|
||||
is AttachmentId -> addAttachment(t)
|
||||
is TransactionState<*> -> addOutputState(t)
|
||||
is StateAndContract -> addOutputState(t.state, t.contract)
|
||||
is ContractState -> throw UnsupportedOperationException("Removed as of V1: please use a StateAndContract instead")
|
||||
@ -128,16 +137,26 @@ open class TransactionBuilder(
|
||||
* @throws ZoneVersionTooLowException if there are reference states and the zone minimum platform version is less than 4.
|
||||
*/
|
||||
@Throws(MissingContractAttachments::class)
|
||||
fun toWireTransaction(services: ServicesForResolution): WireTransaction = toWireTransactionWithContext(services)
|
||||
fun toWireTransaction(services: ServicesForResolution): WireTransaction = toWireTransactionWithContext(services, null)
|
||||
|
||||
@CordaInternal
|
||||
internal fun toWireTransactionWithContext(services: ServicesForResolution, serializationContext: SerializationContext? = null): WireTransaction {
|
||||
internal fun toWireTransactionWithContext(
|
||||
services: ServicesForResolution,
|
||||
serializationContext: SerializationContext?
|
||||
) : WireTransaction = toWireTransactionWithContext(services, serializationContext, 0)
|
||||
|
||||
private tailrec fun toWireTransactionWithContext(
|
||||
services: ServicesForResolution,
|
||||
serializationContext: SerializationContext?,
|
||||
tryCount: Int
|
||||
): WireTransaction {
|
||||
val referenceStates = referenceStates()
|
||||
if (referenceStates.isNotEmpty()) {
|
||||
services.ensureMinimumPlatformVersion(4, "Reference states")
|
||||
}
|
||||
|
||||
val (allContractAttachments: Collection<SecureHash>, resolvedOutputs: List<TransactionState<ContractState>>) = selectContractAttachmentsAndOutputStateConstraints(services, serializationContext)
|
||||
val (allContractAttachments: Collection<AttachmentId>, resolvedOutputs: List<TransactionState<ContractState>>)
|
||||
= selectContractAttachmentsAndOutputStateConstraints(services, serializationContext)
|
||||
|
||||
// Final sanity check that all states have the correct constraints.
|
||||
for (state in (inputsWithTransactionState.map { it.state } + resolvedOutputs)) {
|
||||
@ -150,7 +169,8 @@ open class TransactionBuilder(
|
||||
inputStates(),
|
||||
resolvedOutputs,
|
||||
commands(),
|
||||
(allContractAttachments + attachments).toSortedSet().toList(), // Sort the attachments to ensure transaction builds are stable.
|
||||
// Sort the attachments to ensure transaction builds are stable.
|
||||
((allContractAttachments + attachments).toSortedSet() - excludedAttachments).toList(),
|
||||
notary,
|
||||
window,
|
||||
referenceStates,
|
||||
@ -163,10 +183,10 @@ open class TransactionBuilder(
|
||||
// This is a workaround as the current version of Corda does not support cordapp dependencies.
|
||||
// It works by running transaction validation and then scan the attachment storage for missing classes.
|
||||
// TODO - remove once proper support for cordapp dependencies is added.
|
||||
val addedDependency = addMissingDependency(services, wireTx)
|
||||
val addedDependency = addMissingDependency(services, wireTx, tryCount)
|
||||
|
||||
return if (addedDependency)
|
||||
toWireTransactionWithContext(services, serializationContext)
|
||||
toWireTransactionWithContext(services, serializationContext, tryCount + 1)
|
||||
else
|
||||
wireTx
|
||||
}
|
||||
@ -181,7 +201,7 @@ open class TransactionBuilder(
|
||||
/**
|
||||
* @return true if a new dependency was successfully added.
|
||||
*/
|
||||
private fun addMissingDependency(services: ServicesForResolution, wireTx: WireTransaction): Boolean {
|
||||
private fun addMissingDependency(services: ServicesForResolution, wireTx: WireTransaction, tryCount: Int): Boolean {
|
||||
return try {
|
||||
wireTx.toLedgerTransaction(services).verify()
|
||||
// The transaction verified successfully without adding any extra dependency.
|
||||
@ -192,8 +212,14 @@ open class TransactionBuilder(
|
||||
when {
|
||||
// Handle various exceptions that can be thrown during verification and drill down the wrappings.
|
||||
// Note: this is a best effort to preserve backwards compatibility.
|
||||
rootError is ClassNotFoundException -> addMissingAttachment((rootError.message ?: throw e).replace(".", "/"), services, e)
|
||||
rootError is NoClassDefFoundError -> addMissingAttachment(rootError.message ?: throw e, services, e)
|
||||
rootError is ClassNotFoundException -> {
|
||||
((tryCount == 0) && fixupAttachments(wireTx.attachments, services, e))
|
||||
|| addMissingAttachment((rootError.message ?: throw e).replace('.', '/'), services, e)
|
||||
}
|
||||
rootError is NoClassDefFoundError -> {
|
||||
((tryCount == 0) && fixupAttachments(wireTx.attachments, services, e))
|
||||
|| addMissingAttachment(rootError.message ?: throw e, services, e)
|
||||
}
|
||||
|
||||
// Ignore these exceptions as they will break unit tests.
|
||||
// The point here is only to detect missing dependencies. The other exceptions are irrelevant.
|
||||
@ -214,10 +240,48 @@ open class TransactionBuilder(
|
||||
}
|
||||
}
|
||||
|
||||
private fun fixupAttachments(
|
||||
txAttachments: List<AttachmentId>,
|
||||
services: ServicesForResolution,
|
||||
originalException: Throwable
|
||||
): Boolean {
|
||||
val replacementAttachments = services.cordappProvider.internalFixupAttachmentIds(txAttachments)
|
||||
if (replacementAttachments.deepEquals(txAttachments)) {
|
||||
return false
|
||||
}
|
||||
|
||||
val extraAttachments = replacementAttachments - txAttachments
|
||||
extraAttachments.forEach { id ->
|
||||
val attachment = services.attachments.openAttachment(id)
|
||||
if (attachment == null || !attachment.isUploaderTrusted()) {
|
||||
log.warn("""The node's fix-up rules suggest including attachment {}, which cannot be found either.
|
||||
|Please contact the developer of the CorDapp for further instructions.
|
||||
|""".trimMargin(), id)
|
||||
throw originalException
|
||||
}
|
||||
}
|
||||
|
||||
attachments.addAll(extraAttachments)
|
||||
with(excludedAttachments) {
|
||||
clear()
|
||||
addAll(txAttachments - replacementAttachments)
|
||||
}
|
||||
|
||||
log.warn("Attempting to rebuild transaction with these extra attachments:{}{}and these attachments removed:{}",
|
||||
extraAttachments.toPrettyString(),
|
||||
System.lineSeparator(),
|
||||
excludedAttachments.toPrettyString()
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun addMissingAttachment(missingClass: String, services: ServicesForResolution, originalException: Throwable): Boolean {
|
||||
if (!isValidJavaClass(missingClass)) {
|
||||
log.warn("Could not autodetect a valid attachment for the transaction being built.")
|
||||
throw originalException
|
||||
} else if (MISSING_CLASS_DISABLED) {
|
||||
log.warn("BROKEN TRANSACTION, BUT AUTOMATIC DETECTION OF {} IS DISABLED!", missingClass)
|
||||
throw originalException
|
||||
}
|
||||
|
||||
val attachment = services.attachments.internalFindTrustedAttachmentForClass(missingClass)
|
||||
@ -240,19 +304,21 @@ open class TransactionBuilder(
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is responsible for selecting the contract versions to be used for the current transaction and resolve the output state [AutomaticPlaceholderConstraint]s.
|
||||
* The contract attachments are used to create a deterministic Classloader to deserialise the transaction and to run the contract verification.
|
||||
* This method is responsible for selecting the contract versions to be used for the current transaction and resolve the output state
|
||||
* [AutomaticPlaceholderConstraint]s. The contract attachments are used to create a deterministic Classloader to deserialise the
|
||||
* transaction and to run the contract verification.
|
||||
*
|
||||
* The selection logic depends on the Attachment Constraints of the input, output and reference states, also on the explicitly set attachments.
|
||||
* The selection logic depends on the Attachment Constraints of the input, output and reference states, also on the explicitly
|
||||
* set attachments.
|
||||
* TODO also on the versions of the attachments of the transactions generating the input states. ( after we add versioning)
|
||||
*/
|
||||
private fun selectContractAttachmentsAndOutputStateConstraints(
|
||||
services: ServicesForResolution,
|
||||
@Suppress("UNUSED_PARAMETER") serializationContext: SerializationContext?
|
||||
): Pair<Collection<SecureHash>, List<TransactionState<ContractState>>> {
|
||||
): Pair<Collection<AttachmentId>, List<TransactionState<ContractState>>> {
|
||||
|
||||
// Determine the explicitly set contract attachments.
|
||||
val explicitAttachmentContracts: List<Pair<ContractClassName, SecureHash>> = this.attachments
|
||||
val explicitAttachmentContracts: List<Pair<ContractClassName, AttachmentId>> = this.attachments
|
||||
.map(services.attachments::openAttachment)
|
||||
.mapNotNull { it as? ContractAttachment }
|
||||
.flatMap { attch ->
|
||||
@ -260,9 +326,12 @@ open class TransactionBuilder(
|
||||
}
|
||||
|
||||
// And fail early if there's more than 1 for a contract.
|
||||
require(explicitAttachmentContracts.isEmpty() || explicitAttachmentContracts.groupBy { (ctr, _) -> ctr }.all { (_, groups) -> groups.size == 1 }) { "Multiple attachments set for the same contract." }
|
||||
require(explicitAttachmentContracts.isEmpty()
|
||||
|| explicitAttachmentContracts.groupBy { (ctr, _) -> ctr }.all { (_, groups) -> groups.size == 1 }) {
|
||||
"Multiple attachments set for the same contract."
|
||||
}
|
||||
|
||||
val explicitAttachmentContractsMap: Map<ContractClassName, SecureHash> = explicitAttachmentContracts.toMap()
|
||||
val explicitAttachmentContractsMap: Map<ContractClassName, AttachmentId> = explicitAttachmentContracts.toMap()
|
||||
|
||||
val inputContractGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = inputsWithTransactionState.map { it.state }
|
||||
.groupBy { it.contract }
|
||||
@ -272,7 +341,8 @@ open class TransactionBuilder(
|
||||
|
||||
// Handle reference states.
|
||||
// Filter out all contracts that might have been already used by 'normal' input or output states.
|
||||
val referenceStateGroups: Map<ContractClassName, List<TransactionState<ContractState>>> = referencesWithTransactionState.groupBy { it.contract }
|
||||
val referenceStateGroups: Map<ContractClassName, List<TransactionState<ContractState>>>
|
||||
= referencesWithTransactionState.groupBy { it.contract }
|
||||
val refStateContractAttachments: List<AttachmentId> = referenceStateGroups
|
||||
.filterNot { it.key in allContracts }
|
||||
.map { refStateEntry ->
|
||||
@ -659,7 +729,7 @@ open class TransactionBuilder(
|
||||
}
|
||||
|
||||
/** Adds an attachment with the specified hash to the TransactionBuilder. */
|
||||
fun addAttachment(attachmentId: SecureHash) = apply {
|
||||
fun addAttachment(attachmentId: AttachmentId) = apply {
|
||||
attachments.add(attachmentId)
|
||||
}
|
||||
|
||||
@ -750,7 +820,7 @@ open class TransactionBuilder(
|
||||
fun referenceStates(): List<StateRef> = ArrayList(references)
|
||||
|
||||
/** Returns an immutable list of attachment hashes. */
|
||||
fun attachments(): List<SecureHash> = ArrayList(attachments)
|
||||
fun attachments(): List<AttachmentId> = ArrayList(attachments)
|
||||
|
||||
/** Returns an immutable list of output [TransactionState]s. */
|
||||
fun outputStates(): List<TransactionState<*>> = ArrayList(outputs)
|
||||
|
@ -7,6 +7,11 @@ release, see :doc:`app-upgrade-notes`.
|
||||
Unreleased
|
||||
----------
|
||||
|
||||
* Custom serializer classes implementing ``SerializationCustomSerializer`` should ideally be packaged in the same CorDapp as the
|
||||
contract classes. Corda 4 could therefore fail to verify transactions created with Corda 3 if their custom serializer classes
|
||||
had been packaged somewhere else. Add a "fallback mechanism" to Corda's transaction verification logic which will attempt to
|
||||
include any missing custom serializers from other CorDapps within ``AttachmentStorage``.
|
||||
|
||||
* ``AppServiceHub`` been extended to provide access to ``database`` which will enable the Service class to perform DB transactions
|
||||
from the threads managed by the custom Service.
|
||||
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -9,18 +9,18 @@ import net.corda.core.transactions.LedgerTransaction
|
||||
@Suppress("unused")
|
||||
class CustomSerializerContract : Contract {
|
||||
companion object {
|
||||
const val MAX_CURRANT = 2000
|
||||
const val MAX_CURRANT = 2000L
|
||||
}
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
val currantsyData = tx.outputsOfType(CurrantsyState::class.java)
|
||||
val currantsyData = tx.outputsOfType<CurrantsyState>()
|
||||
require(currantsyData.isNotEmpty()) {
|
||||
"Requires at least one currantsy state"
|
||||
}
|
||||
|
||||
currantsyData.forEach {
|
||||
require(it.currantsy.currants in 0..MAX_CURRANT) {
|
||||
"Too many currants! ${it.currantsy.currants} is unraisinable!"
|
||||
require(it.currantsy in Currantsy(0)..Currantsy(MAX_CURRANT)) {
|
||||
"Too many currants! $it is unraisinable!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ package net.corda.node
|
||||
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.contracts.serialization.custom.Currantsy
|
||||
import net.corda.core.contracts.TransactionVerificationException
|
||||
import net.corda.core.contracts.TransactionVerificationException.ContractRejection
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.flows.serialization.custom.CustomSerializerFlow
|
||||
@ -14,16 +14,22 @@ import net.corda.testing.driver.driver
|
||||
import net.corda.testing.driver.internal.incrementalPortAllocation
|
||||
import net.corda.testing.node.NotarySpec
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.node.internal.CustomCordapp
|
||||
import net.corda.testing.node.internal.cordappWithPackages
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
@Suppress("FunctionName")
|
||||
class ContractWithCustomSerializerTest {
|
||||
companion object {
|
||||
const val CURRANTS = 5000L
|
||||
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun checkData() {
|
||||
assertNotCordaSerializable<Currantsy>()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -34,23 +40,21 @@ class ContractWithCustomSerializerTest {
|
||||
startNodesInProcess = false,
|
||||
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = true)),
|
||||
cordappsForAllNodes = listOf(
|
||||
cordappWithPackages("net.corda.flows.serialization.custom"),
|
||||
CustomCordapp(
|
||||
packages = setOf("net.corda.contracts.serialization.custom"),
|
||||
name = "has-custom-serializer"
|
||||
).signed()
|
||||
cordappWithPackages("net.corda.flows.serialization.custom").signed(),
|
||||
cordappWithPackages("net.corda.contracts.serialization.custom").signed()
|
||||
)
|
||||
)) {
|
||||
val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
||||
val ex = assertThrows<TransactionVerificationException> {
|
||||
val ex = assertFailsWith<ContractRejection> {
|
||||
CordaRPCClient(hostAndPort = alice.rpcAddress)
|
||||
.start(user.username, user.password)
|
||||
.proxy
|
||||
.startFlow(::CustomSerializerFlow, Currantsy(CURRANTS))
|
||||
.returnValue
|
||||
.getOrThrow()
|
||||
.use { client ->
|
||||
client.proxy.startFlow(::CustomSerializerFlow, Currantsy(CURRANTS))
|
||||
.returnValue
|
||||
.getOrThrow()
|
||||
}
|
||||
}
|
||||
assertThat(ex).hasMessageContaining("Too many currants! $CURRANTS is unraisinable!")
|
||||
assertThat(ex).hasMessageContaining("Too many currants! $CURRANTS juicy currants is unraisinable!")
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -297,7 +297,11 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
networkParametersStorage
|
||||
).closeOnStop()
|
||||
@Suppress("LeakingThis")
|
||||
val transactionVerifierService = InMemoryTransactionVerifierService(transactionVerifierWorkerCount).tokenize()
|
||||
val transactionVerifierService = InMemoryTransactionVerifierService(
|
||||
numberOfWorkers = transactionVerifierWorkerCount,
|
||||
cordappProvider = cordappProvider,
|
||||
attachments = attachments
|
||||
).tokenize()
|
||||
val verifierFactoryService: VerifierFactoryService = if (djvmCordaSource != null) {
|
||||
DeterministicVerifierFactoryService(djvmBootstrapSource, djvmCordaSource).apply {
|
||||
log.info("DJVM sandbox enabled for deterministic contract verification.")
|
||||
|
@ -73,7 +73,6 @@ import net.corda.node.utilities.DefaultNamedCacheFactory
|
||||
import net.corda.node.utilities.DemoClock
|
||||
import net.corda.node.utilities.errorAndTerminate
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingClient
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent
|
||||
import net.corda.nodeapi.internal.ShutdownHook
|
||||
import net.corda.nodeapi.internal.addShutdownHook
|
||||
import net.corda.nodeapi.internal.bridging.BridgeControlListener
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.node.internal.cordapp
|
||||
|
||||
import com.google.common.collect.HashBiMap
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.cordapp.Cordapp
|
||||
import net.corda.core.cordapp.CordappContext
|
||||
@ -8,14 +9,19 @@ import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
|
||||
import net.corda.core.internal.cordapp.CordappImpl
|
||||
import net.corda.core.internal.isUploaderTrusted
|
||||
import net.corda.core.node.services.AttachmentFixup
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.node.services.AttachmentStorage
|
||||
import net.corda.core.serialization.MissingAttachmentsException
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.node.services.persistence.AttachmentStorageInternal
|
||||
import net.corda.nodeapi.internal.cordapp.CordappLoader
|
||||
import java.net.JarURLConnection
|
||||
import java.net.URL
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.jar.JarFile
|
||||
|
||||
/**
|
||||
* Cordapp provider and store. For querying CorDapps for their attachment and vice versa.
|
||||
@ -29,6 +35,7 @@ open class CordappProviderImpl(val cordappLoader: CordappLoader,
|
||||
|
||||
private val contextCache = ConcurrentHashMap<Cordapp, CordappContext>()
|
||||
private val cordappAttachments = HashBiMap.create<SecureHash, URL>()
|
||||
private val attachmentFixups = arrayListOf<AttachmentFixup>()
|
||||
|
||||
/**
|
||||
* Current known CorDapps loaded on this node
|
||||
@ -38,6 +45,8 @@ open class CordappProviderImpl(val cordappLoader: CordappLoader,
|
||||
fun start() {
|
||||
cordappAttachments.putAll(loadContractsIntoAttachmentStore())
|
||||
verifyInstalledCordapps()
|
||||
// Load the fix-ups after uploading any new contracts into attachment storage.
|
||||
attachmentFixups.addAll(loadAttachmentFixups())
|
||||
}
|
||||
|
||||
private fun verifyInstalledCordapps() {
|
||||
@ -95,6 +104,92 @@ open class CordappProviderImpl(val cordappLoader: CordappLoader,
|
||||
} to cordapp.jarPath
|
||||
}.toMap()
|
||||
|
||||
/**
|
||||
* Loads the "fixup" rules from all META-INF/Corda-Fixups files.
|
||||
* These files have the following format:
|
||||
* <AttachmentId>,<AttachmentId>...=><AttachmentId>,<AttachmentId>,...
|
||||
* where each <AttachmentId> is the SHA256 of a CorDapp JAR that
|
||||
* [net.corda.core.transactions.TransactionBuilder] will expect to find
|
||||
* inside [AttachmentStorage].
|
||||
*
|
||||
* These rules are for repairing broken CorDapps. A correctly written
|
||||
* CorDapp should not require them.
|
||||
*/
|
||||
private fun loadAttachmentFixups(): List<AttachmentFixup> {
|
||||
return cordappLoader.appClassLoader.getResources("META-INF/Corda-Fixups").asSequence()
|
||||
.mapNotNull { fixup ->
|
||||
fixup.openConnection() as? JarURLConnection
|
||||
}.filter { fixupConnection ->
|
||||
isValidFixup(fixupConnection.jarFile)
|
||||
}.flatMapTo(ArrayList()) { fixupConnection ->
|
||||
fixupConnection.inputStream.bufferedReader().useLines { lines ->
|
||||
lines.filter(String::isNotBlank).map { line ->
|
||||
val tokens = line.split("=>", limit = 2)
|
||||
require(tokens.size == 2) {
|
||||
"Invalid fix-up line '$line' in '${fixupConnection.jarFile.name}'"
|
||||
}
|
||||
val source = parseIds(tokens[0])
|
||||
require(source.isNotEmpty()) {
|
||||
"Forbidden empty list of source attachment IDs in '${fixupConnection.jarFile.name}'"
|
||||
}
|
||||
val target = parseIds(tokens[1])
|
||||
Pair(source, target)
|
||||
}.toList().asSequence()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValidFixup(jarFile: JarFile): Boolean {
|
||||
return jarFile.entries().asSequence().all { it.name.startsWith("META-INF/") }.also { isValid ->
|
||||
if (!isValid) {
|
||||
log.warn("FixUp '{}' contains files outside META-INF/ - IGNORING!", jarFile.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseIds(ids: String): Set<AttachmentId> {
|
||||
return ids.split(",").mapTo(LinkedHashSet()) {
|
||||
AttachmentId.parse(it.trim())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply this node's attachment fix-up rules to the given attachment IDs.
|
||||
*
|
||||
* @param attachmentIds A collection of [AttachmentId]s, e.g. as provided by a transaction.
|
||||
* @return The [attachmentIds] with the fix-up rules applied.
|
||||
*/
|
||||
override fun fixupAttachmentIds(attachmentIds: Collection<AttachmentId>): Set<AttachmentId> {
|
||||
val replacementIds = LinkedHashSet(attachmentIds)
|
||||
attachmentFixups.forEach { (source, target) ->
|
||||
if (replacementIds.containsAll(source)) {
|
||||
replacementIds.removeAll(source)
|
||||
replacementIds.addAll(target)
|
||||
}
|
||||
}
|
||||
return replacementIds
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply this node's attachment fix-up rules to the given attachments.
|
||||
*
|
||||
* @param attachments A collection of [Attachment] objects, e.g. as provided by a transaction.
|
||||
* @return The [attachments] with the node's fix-up rules applied.
|
||||
*/
|
||||
override fun fixupAttachments(attachments: Collection<Attachment>): Collection<Attachment> {
|
||||
val attachmentsById = attachments.associateByTo(LinkedHashMap(), Attachment::id)
|
||||
val replacementIds = fixupAttachmentIds(attachmentsById.keys)
|
||||
attachmentsById.keys.retainAll(replacementIds)
|
||||
(replacementIds - attachmentsById.keys).forEach { extraId ->
|
||||
val extraAttachment = attachmentStorage.openAttachment(extraId)
|
||||
if (extraAttachment == null || !extraAttachment.isUploaderTrusted()) {
|
||||
throw MissingAttachmentsException(listOf(extraId))
|
||||
}
|
||||
attachmentsById[extraId] = extraAttachment
|
||||
}
|
||||
return attachmentsById.values
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current cordapp context for the given CorDapp
|
||||
*
|
||||
|
@ -1,11 +1,14 @@
|
||||
package net.corda.node.internal.cordapp
|
||||
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.cordapp.Cordapp
|
||||
import net.corda.core.cordapp.CordappProvider
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.internal.CordappFixupInternal
|
||||
import net.corda.core.internal.cordapp.CordappImpl
|
||||
|
||||
interface CordappProviderInternal : CordappProvider {
|
||||
interface CordappProviderInternal : CordappProvider, CordappFixupInternal {
|
||||
val cordapps: List<CordappImpl>
|
||||
fun getCordappForFlow(flowLogic: FlowLogic<*>): Cordapp?
|
||||
fun fixupAttachments(attachments: Collection<Attachment>): Collection<Attachment>
|
||||
}
|
||||
|
@ -156,7 +156,7 @@ class NodeAttachmentService @JvmOverloads constructor(
|
||||
val session = currentDBSession()
|
||||
val criteriaBuilder = session.criteriaBuilder
|
||||
val criteriaQuery = criteriaBuilder.createQuery(Long::class.java)
|
||||
criteriaQuery.select(criteriaBuilder.count(criteriaQuery.from(NodeAttachmentService.DBAttachment::class.java)))
|
||||
criteriaQuery.select(criteriaBuilder.count(criteriaQuery.from(DBAttachment::class.java)))
|
||||
val count = session.createQuery(criteriaQuery).singleResult
|
||||
attachmentCount.inc(count)
|
||||
}
|
||||
@ -278,9 +278,9 @@ class NodeAttachmentService @JvmOverloads constructor(
|
||||
loadFunction = { Optional.ofNullable(loadAttachmentContent(it)) }
|
||||
)
|
||||
|
||||
private fun loadAttachmentContent(id: SecureHash): Pair<Attachment, ByteArray>? {
|
||||
private fun loadAttachmentContent(id: AttachmentId): Pair<Attachment, ByteArray>? {
|
||||
return database.transaction {
|
||||
val attachment = currentDBSession().get(NodeAttachmentService.DBAttachment::class.java, id.toString())
|
||||
val attachment = currentDBSession().get(DBAttachment::class.java, id.toString())
|
||||
?: return@transaction null
|
||||
Pair(createAttachmentFromDatabase(attachment), attachment.content)
|
||||
}
|
||||
@ -363,7 +363,7 @@ class NodeAttachmentService @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
override fun hasAttachment(attachmentId: AttachmentId): Boolean = database.transaction {
|
||||
currentDBSession().find(NodeAttachmentService.DBAttachment::class.java, attachmentId.toString()) != null
|
||||
currentDBSession().find(DBAttachment::class.java, attachmentId.toString()) != null
|
||||
}
|
||||
|
||||
private fun increaseDefaultVersionIfWhitelistedAttachment(contractClassNames: List<ContractClassName>, contractVersionFromFile: Int, attachmentId: AttachmentId) =
|
||||
@ -400,7 +400,7 @@ class NodeAttachmentService @JvmOverloads constructor(
|
||||
val jarSigners = getSigners(bytes)
|
||||
val contractVersion = increaseDefaultVersionIfWhitelistedAttachment(contractClassNames, getVersion(bytes), id)
|
||||
val session = currentDBSession()
|
||||
val attachment = NodeAttachmentService.DBAttachment(
|
||||
val attachment = DBAttachment(
|
||||
attId = id.toString(),
|
||||
content = bytes,
|
||||
uploader = uploader,
|
||||
@ -417,7 +417,7 @@ class NodeAttachmentService @JvmOverloads constructor(
|
||||
}
|
||||
if (isUploaderTrusted(uploader)) {
|
||||
val session = currentDBSession()
|
||||
val attachment = session.get(NodeAttachmentService.DBAttachment::class.java, id.toString())
|
||||
val attachment = session.get(DBAttachment::class.java, id.toString())
|
||||
// update the `uploader` field (as the existing attachment may have been resolved from a peer)
|
||||
if (attachment.uploader != uploader) {
|
||||
attachment.uploader = uploader
|
||||
@ -438,7 +438,7 @@ class NodeAttachmentService @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
private fun getSigners(attachmentBytes: ByteArray) =
|
||||
JarSignatureCollector.collectSigners(JarInputStream(attachmentBytes.inputStream()))
|
||||
JarInputStream(attachmentBytes.inputStream()).use(JarSignatureCollector::collectSigners)
|
||||
|
||||
private fun getVersion(attachmentBytes: ByteArray) =
|
||||
JarInputStream(attachmentBytes.inputStream()).use {
|
||||
|
@ -2,21 +2,113 @@ package net.corda.node.services.transactions
|
||||
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.TransactionVerificationException.BrokenTransactionException
|
||||
import net.corda.core.internal.TransactionVerifierServiceInternal
|
||||
import net.corda.core.internal.concurrent.openFuture
|
||||
import net.corda.core.internal.internalFindTrustedAttachmentForClass
|
||||
import net.corda.core.internal.prepareVerify
|
||||
import net.corda.core.node.services.AttachmentStorage
|
||||
import net.corda.core.node.services.TransactionVerifierService
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.node.internal.cordapp.CordappProviderInternal
|
||||
import net.corda.nodeapi.internal.persistence.withoutDatabaseAccess
|
||||
|
||||
class InMemoryTransactionVerifierService(@Suppress("UNUSED_PARAMETER") numberOfWorkers: Int) : SingletonSerializeAsToken(), TransactionVerifierService, TransactionVerifierServiceInternal, AutoCloseable {
|
||||
override fun verify(transaction: LedgerTransaction): CordaFuture<Unit> = this.verify(transaction, emptyList())
|
||||
class InMemoryTransactionVerifierService(
|
||||
@Suppress("UNUSED_PARAMETER") numberOfWorkers: Int,
|
||||
private val cordappProvider: CordappProviderInternal,
|
||||
private val attachments: AttachmentStorage
|
||||
) : SingletonSerializeAsToken(), TransactionVerifierService, TransactionVerifierServiceInternal, AutoCloseable {
|
||||
companion object {
|
||||
private val SEPARATOR = System.lineSeparator() + "-> "
|
||||
private val log = contextLogger()
|
||||
|
||||
override fun verify(transaction: LedgerTransaction, extraAttachments: List<Attachment>): CordaFuture<Unit> {
|
||||
fun Collection<*>.deepEquals(other: Collection<*>): Boolean {
|
||||
return size == other.size && containsAll(other) && other.containsAll(this)
|
||||
}
|
||||
|
||||
fun Collection<Attachment>.toPrettyString(): String {
|
||||
return joinToString(separator = SEPARATOR, prefix = SEPARATOR, postfix = System.lineSeparator()) { attachment ->
|
||||
attachment.id.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun verify(transaction: LedgerTransaction): CordaFuture<*> {
|
||||
return openFuture<Unit>().apply {
|
||||
capture {
|
||||
val verifier = transaction.prepareVerify(extraAttachments)
|
||||
val verifier = transaction.prepareVerify(transaction.attachments)
|
||||
withoutDatabaseAccess {
|
||||
verifier.verify()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeReplacementAttachmentsFor(ltx: LedgerTransaction, missingClass: String?): Collection<Attachment> {
|
||||
val replacements = cordappProvider.fixupAttachments(ltx.attachments)
|
||||
return if (replacements.deepEquals(ltx.attachments)) {
|
||||
/*
|
||||
* We cannot continue unless we have some idea which
|
||||
* class is missing from the attachments.
|
||||
*/
|
||||
if (missingClass == null) {
|
||||
throw BrokenTransactionException(
|
||||
txId = ltx.id,
|
||||
message = "No fix-up rules provided for broken attachments:${replacements.toPrettyString()}"
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* The Node's fix-up rules have not been able to adjust the transaction's attachments,
|
||||
* so resort to the original mechanism of trying to find an attachment that contains
|
||||
* the missing class. (Do you feel lucky, Punk?)
|
||||
*/
|
||||
val extraAttachment = requireNotNull(attachments.internalFindTrustedAttachmentForClass(missingClass)) {
|
||||
"""Transaction $ltx is incorrectly formed. Most likely it was created during version 3 of Corda
|
||||
|when the verification logic was more lenient. Attempted to find local dependency for class: $missingClass,
|
||||
|but could not find one.
|
||||
|If you wish to verify this transaction, please contact the originator of the transaction and install the
|
||||
|provided missing JAR.
|
||||
|You can install it using the RPC command: `uploadAttachment` without restarting the node.
|
||||
|""".trimMargin()
|
||||
}
|
||||
|
||||
replacements.toMutableSet().apply {
|
||||
/*
|
||||
* Check our transaction doesn't already contain this extra attachment.
|
||||
* It seems unlikely that we would, but better safe than sorry!
|
||||
*/
|
||||
if (!add(extraAttachment)) {
|
||||
throw BrokenTransactionException(
|
||||
txId = ltx.id,
|
||||
message = "Unlinkable class $missingClass inside broken attachments:${replacements.toPrettyString()}"
|
||||
)
|
||||
}
|
||||
|
||||
log.warn("""Detected that transaction $ltx does not contain all cordapp dependencies.
|
||||
|This may be the result of a bug in a previous version of Corda.
|
||||
|Attempting to verify using the additional trusted dependency: $extraAttachment for class $missingClass.
|
||||
|Please check with the originator that this is a valid transaction.
|
||||
|YOU ARE ONLY SEEING THIS MESSAGE BECAUSE THE CORDAPPS THAT CREATED THIS TRANSACTION ARE BROKEN!
|
||||
|WE HAVE TRIED TO REPAIR THE TRANSACTION AS BEST WE CAN, BUT CANNOT GUARANTEE WE HAVE SUCCEEDED!
|
||||
|PLEASE FIX THE CORDAPPS AND MIGRATE THESE BROKEN TRANSACTIONS AS SOON AS POSSIBLE!
|
||||
|THIS MESSAGE IS **SUPPOSED** TO BE SCARY!!
|
||||
|""".trimMargin()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
replacements
|
||||
}
|
||||
}
|
||||
|
||||
override fun reverifyWithFixups(transaction: LedgerTransaction, missingClass: String?): CordaFuture<*> {
|
||||
return openFuture<Unit>().apply {
|
||||
capture {
|
||||
val replacementAttachments = computeReplacementAttachmentsFor(transaction, missingClass)
|
||||
log.warn("Reverifying transaction {} with attachments:{}", transaction.id, replacementAttachments.toPrettyString())
|
||||
val verifier = transaction.prepareVerify(replacementAttachments.toList())
|
||||
withoutDatabaseAccess {
|
||||
verifier.verify()
|
||||
}
|
||||
|
@ -3,6 +3,24 @@ apply plugin: 'net.corda.plugins.cordapp'
|
||||
def javaHome = System.getProperty('java.home')
|
||||
def shrinkJar = file("$buildDir/libs/${project.name}-${project.version}-tiny.jar")
|
||||
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.MessageDigest
|
||||
|
||||
static String sha256(File jarFile) throws FileNotFoundException, NoSuchAlgorithmException {
|
||||
InputStream input = new FileInputStream(jarFile)
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256")
|
||||
byte[] buffer = new byte[8192]
|
||||
int bytesRead
|
||||
while ((bytesRead = input.read(buffer)) != -1) {
|
||||
digest.update(buffer, 0, bytesRead)
|
||||
}
|
||||
return digest.digest().encodeHex().toString()
|
||||
} finally {
|
||||
input.close()
|
||||
}
|
||||
}
|
||||
|
||||
cordapp {
|
||||
targetPlatformVersion = corda_platform_version.toInteger()
|
||||
minimumPlatformVersion 1
|
||||
@ -42,6 +60,18 @@ dependencies {
|
||||
compile "com.opengamma.strata:strata-market:$strata_version"
|
||||
}
|
||||
|
||||
def cordappDependencies = file("${sourceSets['main'].output.resourcesDir}/META-INF/Cordapp-Dependencies")
|
||||
task generateDependencies {
|
||||
dependsOn project(':finance:contracts').tasks.jar
|
||||
outputs.files(cordappDependencies)
|
||||
doLast {
|
||||
configurations.cordapp.forEach { cordapp ->
|
||||
cordappDependencies << sha256(cordapp) << System.lineSeparator()
|
||||
}
|
||||
}
|
||||
}
|
||||
processResources.finalizedBy generateDependencies
|
||||
|
||||
jar {
|
||||
classifier = 'fat'
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -23,6 +23,8 @@ data class IRSState(val swap: SwapData,
|
||||
|
||||
override fun generateAgreement(notary: Party): TransactionBuilder {
|
||||
val state = IRSState(swap, buyer, seller)
|
||||
return TransactionBuilder(notary).withItems(StateAndContract(state, IRS_PROGRAM_ID), Command(OGTrade.Commands.Agree(), participants.map { it.owningKey }))
|
||||
return TransactionBuilder(notary)
|
||||
.withItems(StateAndContract(state, IRS_PROGRAM_ID), Command(OGTrade.Commands.Agree(), participants.map { it.owningKey }))
|
||||
.withExternalCordapps()
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,10 @@ data class PortfolioState(val portfolio: List<StateRef>,
|
||||
}
|
||||
|
||||
override fun generateAgreement(notary: Party): TransactionBuilder {
|
||||
return TransactionBuilder(notary).withItems(StateAndContract(copy(), PORTFOLIO_SWAP_PROGRAM_ID), Command(PortfolioSwap.Commands.Agree(), participants.map { it.owningKey }))
|
||||
val command = Command(PortfolioSwap.Commands.Agree(), participants.map { it.owningKey })
|
||||
return TransactionBuilder(notary)
|
||||
.withItems(StateAndContract(copy(), PORTFOLIO_SWAP_PROGRAM_ID), command)
|
||||
.withExternalCordapps()
|
||||
}
|
||||
|
||||
override fun generateRevision(notary: Party, oldState: StateAndRef<*>, updatedValue: Update): TransactionBuilder {
|
||||
@ -49,6 +52,7 @@ data class PortfolioState(val portfolio: List<StateRef>,
|
||||
tx.addInputState(oldState)
|
||||
tx.addOutputState(copy(portfolio = portfolio, valuation = valuation), PORTFOLIO_SWAP_PROGRAM_ID)
|
||||
tx.addCommand(PortfolioSwap.Commands.Update(), participants.map { it.owningKey })
|
||||
tx.withExternalCordapps()
|
||||
return tx
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ data class SerializationFactoryCacheKey(val classWhitelist: ClassWhitelist,
|
||||
val preventDataLoss: Boolean,
|
||||
val customSerializers: Set<SerializationCustomSerializer<*, *>>)
|
||||
|
||||
fun SerializerFactory.addToWhitelist(vararg types: Class<*>) {
|
||||
fun SerializerFactory.addToWhitelist(types: Collection<Class<*>>) {
|
||||
require(types.toSet().size == types.size) {
|
||||
val duplicates = types.toMutableList()
|
||||
types.toSet().forEach { duplicates -= it }
|
||||
@ -71,11 +71,11 @@ abstract class AbstractAMQPSerializationScheme(
|
||||
|
||||
@DeleteForDJVM
|
||||
val List<Cordapp>.customSerializers
|
||||
get() = flatMap { it.serializationCustomSerializers }.toSet()
|
||||
get() = flatMapTo(LinkedHashSet(), Cordapp::serializationCustomSerializers)
|
||||
|
||||
@DeleteForDJVM
|
||||
val List<Cordapp>.serializationWhitelists
|
||||
get() = flatMap { it.serializationWhitelists }.toSet()
|
||||
get() = flatMapTo(LinkedHashSet(), Cordapp::serializationWhitelists)
|
||||
}
|
||||
|
||||
private fun registerCustomSerializers(context: SerializationContext, factory: SerializerFactory) {
|
||||
@ -93,7 +93,11 @@ abstract class AbstractAMQPSerializationScheme(
|
||||
factory.registerExternal(CorDappCustomSerializer(customSerializer, factory))
|
||||
}
|
||||
cordappCustomSerializers.forEach { customSerializer ->
|
||||
factory.registerExternal(CorDappCustomSerializer(customSerializer, factory))
|
||||
// We won't be able to use this custom serializer unless it also belongs to
|
||||
// the deserialization classloader.
|
||||
if (customSerializer::class.java.classLoader == context.deserializationClassLoader) {
|
||||
factory.registerExternal(CorDappCustomSerializer(customSerializer, factory))
|
||||
}
|
||||
}
|
||||
|
||||
context.properties[ContextPropertyKeys.SERIALIZERS]?.apply {
|
||||
@ -105,12 +109,10 @@ abstract class AbstractAMQPSerializationScheme(
|
||||
|
||||
private fun registerCustomWhitelists(factory: SerializerFactory) {
|
||||
serializationWhitelists.forEach {
|
||||
factory.addToWhitelist(*it.whitelist.toTypedArray())
|
||||
factory.addToWhitelist(it.whitelist)
|
||||
}
|
||||
cordappSerializationWhitelists.forEach {
|
||||
it.whitelist.forEach { clazz ->
|
||||
factory.addToWhitelist(clazz)
|
||||
}
|
||||
factory.addToWhitelist(it.whitelist)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.internal.MissingSerializerException
|
||||
import net.corda.serialization.internal.model.LocalConstructorInformation
|
||||
import net.corda.serialization.internal.model.LocalPropertyInformation
|
||||
import net.corda.serialization.internal.model.LocalTypeInformation
|
||||
@ -21,7 +22,11 @@ interface ObjectSerializer : AMQPSerializer<Any> {
|
||||
companion object {
|
||||
fun make(typeInformation: LocalTypeInformation, factory: LocalSerializerFactory): ObjectSerializer {
|
||||
if (typeInformation is LocalTypeInformation.NonComposable) {
|
||||
throw NotSerializableException(nonComposableExceptionMessage(typeInformation, factory))
|
||||
val typeNames = typeInformation.nonComposableTypes.map { it.observedType.typeName }
|
||||
throw MissingSerializerException(
|
||||
message = nonComposableExceptionMessage(typeInformation, factory),
|
||||
typeNames = typeNames
|
||||
)
|
||||
}
|
||||
|
||||
val typeDescriptor = factory.createDescriptor(typeInformation)
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.internal.MissingSerializerException
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.serialization.internal.model.*
|
||||
import java.io.NotSerializableException
|
||||
@ -79,8 +80,10 @@ class DefaultRemoteSerializerFactory(
|
||||
}
|
||||
|
||||
// Return the specific serializer the caller asked for.
|
||||
serializers[typeDescriptor] ?: throw NotSerializableException(
|
||||
"Could not find type matching descriptor $typeDescriptor.")
|
||||
serializers[typeDescriptor] ?: throw MissingSerializerException(
|
||||
message = "Could not find type matching descriptor $typeDescriptor.",
|
||||
typeDescriptor = typeDescriptor
|
||||
)
|
||||
}
|
||||
|
||||
private fun getUncached(
|
||||
|
@ -1,7 +1,6 @@
|
||||
package net.corda.serialization.internal.model
|
||||
|
||||
import java.lang.reflect.*
|
||||
import kotlin.reflect.KFunction
|
||||
import java.util.*
|
||||
|
||||
typealias PropertyName = String
|
||||
@ -87,11 +86,11 @@ sealed class LocalTypeInformation {
|
||||
* Get the map of [LocalPropertyInformation], for all types that have it, or an empty map otherwise.
|
||||
*/
|
||||
val propertiesOrEmptyMap: Map<PropertyName, LocalPropertyInformation> get() = when(this) {
|
||||
is LocalTypeInformation.Composable -> properties
|
||||
is LocalTypeInformation.Abstract -> properties
|
||||
is LocalTypeInformation.AnInterface -> properties
|
||||
is LocalTypeInformation.NonComposable -> properties
|
||||
is LocalTypeInformation.Opaque -> wrapped.propertiesOrEmptyMap
|
||||
is Composable -> properties
|
||||
is Abstract -> properties
|
||||
is AnInterface -> properties
|
||||
is NonComposable -> properties
|
||||
is Opaque -> wrapped.propertiesOrEmptyMap
|
||||
else -> emptyMap()
|
||||
}
|
||||
|
||||
@ -99,10 +98,10 @@ sealed class LocalTypeInformation {
|
||||
* Get the list of interfaces, for all types that have them, or an empty list otherwise.
|
||||
*/
|
||||
val interfacesOrEmptyList: List<LocalTypeInformation> get() = when(this) {
|
||||
is LocalTypeInformation.Composable -> interfaces
|
||||
is LocalTypeInformation.Abstract -> interfaces
|
||||
is LocalTypeInformation.AnInterface -> interfaces
|
||||
is LocalTypeInformation.NonComposable -> interfaces
|
||||
is Composable -> interfaces
|
||||
is Abstract -> interfaces
|
||||
is AnInterface -> interfaces
|
||||
is NonComposable -> interfaces
|
||||
else -> emptyList()
|
||||
}
|
||||
|
||||
@ -246,11 +245,12 @@ sealed class LocalTypeInformation {
|
||||
* we do not possess a method (such as a unique "deserialization constructor" satisfied by these properties) for
|
||||
* creating a new instance from a dictionary of property values.
|
||||
*
|
||||
* @param constructor [LocalConstructorInformation] for the constructor of this type, if there is one.
|
||||
* @param properties [LocalPropertyInformation] for the properties of the interface.
|
||||
* @param superclass [LocalTypeInformation] for the superclass of the underlying class of this type.
|
||||
* @param interfaces [LocalTypeInformation] for the interfaces extended by this interface.
|
||||
* @param typeParameters [LocalTypeInformation] for the resolved type parameters of the type.
|
||||
* @property constructor [LocalConstructorInformation] for the constructor of this type, if there is one.
|
||||
* @property properties [LocalPropertyInformation] for the properties of the interface.
|
||||
* @property superclass [LocalTypeInformation] for the superclass of the underlying class of this type.
|
||||
* @property interfaces [LocalTypeInformation] for the interfaces extended by this interface.
|
||||
* @property typeParameters [LocalTypeInformation] for the resolved type parameters of the type.
|
||||
* @param nonComposableSubtypes [NonComposable] for the type descriptors that make this type non-composable,
|
||||
*/
|
||||
data class NonComposable(
|
||||
override val observedType: Type,
|
||||
@ -260,8 +260,11 @@ sealed class LocalTypeInformation {
|
||||
val superclass: LocalTypeInformation,
|
||||
val interfaces: List<LocalTypeInformation>,
|
||||
val typeParameters: List<LocalTypeInformation>,
|
||||
private val nonComposableSubtypes: Set<NonComposable>,
|
||||
val reason: String,
|
||||
val remedy: String) : LocalTypeInformation()
|
||||
val remedy: String) : LocalTypeInformation() {
|
||||
val nonComposableTypes: Set<NonComposable> get() = nonComposableSubtypes.flatMapTo(LinkedHashSet()) { it.nonComposableTypes } + this
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a type whose underlying class is a collection class such as [List] with a single type parameter.
|
||||
|
@ -5,7 +5,6 @@ import net.corda.core.internal.isConcreteClass
|
||||
import net.corda.core.internal.kotlinObjectInstance
|
||||
import net.corda.core.serialization.ConstructorForDeserialization
|
||||
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.serialization.internal.NotSerializableDetailedException
|
||||
import net.corda.serialization.internal.amqp.*
|
||||
import java.io.NotSerializableException
|
||||
@ -38,11 +37,6 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
|
||||
var visited: Set<TypeIdentifier> = emptySet(),
|
||||
val cycles: MutableList<LocalTypeInformation.Cycle> = mutableListOf(),
|
||||
var validateProperties: Boolean = true) {
|
||||
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
|
||||
/**
|
||||
* If we are examining the type of a read-only property, or a type flagged as [Opaque], then we do not need to warn
|
||||
* if the [LocalTypeInformation] for that type (or any of its related types) is [LocalTypeInformation.NonComposable].
|
||||
@ -202,32 +196,30 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
|
||||
private fun buildNonAtomic(rawType: Class<*>, type: Type, typeIdentifier: TypeIdentifier, typeParameterInformation: List<LocalTypeInformation>): LocalTypeInformation {
|
||||
val superclassInformation = buildSuperclassInformation(type)
|
||||
val interfaceInformation = buildInterfaceInformation(type)
|
||||
val observedConstructor = constructorForDeserialization(type)
|
||||
|
||||
if (observedConstructor == null) {
|
||||
return LocalTypeInformation.NonComposable(
|
||||
observedType = type,
|
||||
typeIdentifier = typeIdentifier,
|
||||
constructor = null,
|
||||
properties = if (rawType == Class::class.java) {
|
||||
// Do NOT drill down into the internals of java.lang.Class.
|
||||
emptyMap()
|
||||
} else {
|
||||
buildReadOnlyProperties(rawType)
|
||||
},
|
||||
superclass = superclassInformation,
|
||||
interfaces = interfaceInformation,
|
||||
typeParameters = typeParameterInformation,
|
||||
reason = "No unique deserialization constructor can be identified",
|
||||
remedy = "Either annotate a constructor for this type with @ConstructorForDeserialization, or provide a custom serializer for it"
|
||||
)
|
||||
}
|
||||
val observedConstructor = constructorForDeserialization(type) ?: return LocalTypeInformation.NonComposable(
|
||||
observedType = type,
|
||||
typeIdentifier = typeIdentifier,
|
||||
constructor = null,
|
||||
properties = if (rawType == Class::class.java) {
|
||||
// Do NOT drill down into the internals of java.lang.Class.
|
||||
emptyMap()
|
||||
} else {
|
||||
buildReadOnlyProperties(rawType)
|
||||
},
|
||||
superclass = superclassInformation,
|
||||
interfaces = interfaceInformation,
|
||||
typeParameters = typeParameterInformation,
|
||||
nonComposableSubtypes = emptySet(),
|
||||
reason = "No unique deserialization constructor can be identified",
|
||||
remedy = "Either annotate a constructor for this type with @ConstructorForDeserialization, or provide a custom serializer for it"
|
||||
)
|
||||
|
||||
val constructorInformation = buildConstructorInformation(type, observedConstructor)
|
||||
val properties = buildObjectProperties(rawType, constructorInformation)
|
||||
|
||||
if (!propertiesSatisfyConstructor(constructorInformation, properties)) {
|
||||
val missingParameters = missingMandatoryConstructorProperties(constructorInformation, properties).map { it.name }
|
||||
val missingConstructorProperties = missingMandatoryConstructorProperties(constructorInformation, properties)
|
||||
val missingParameters = missingConstructorProperties.map(LocalConstructorParameterInformation::name)
|
||||
return LocalTypeInformation.NonComposable(
|
||||
observedType = type,
|
||||
typeIdentifier = typeIdentifier,
|
||||
@ -236,6 +228,8 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
|
||||
superclass = superclassInformation,
|
||||
interfaces = interfaceInformation,
|
||||
typeParameters = typeParameterInformation,
|
||||
nonComposableSubtypes = missingConstructorProperties
|
||||
.filterIsInstanceTo(LinkedHashSet(), LocalTypeInformation.NonComposable::class.java),
|
||||
reason = "Mandatory constructor parameters $missingParameters are missing from the readable properties ${properties.keys}",
|
||||
remedy = "Either provide getters or readable fields for $missingParameters, or provide a custom serializer for this type"
|
||||
)
|
||||
@ -252,6 +246,9 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
|
||||
superclass = superclassInformation,
|
||||
interfaces = interfaceInformation,
|
||||
typeParameters = typeParameterInformation,
|
||||
nonComposableSubtypes = nonComposableProperties.values.mapTo(LinkedHashSet()) {
|
||||
it.type as LocalTypeInformation.NonComposable
|
||||
},
|
||||
reason = nonComposablePropertiesErrorReason(nonComposableProperties),
|
||||
remedy = "Either ensure that the properties ${nonComposableProperties.keys} are serializable, or provide a custom serializer for this type"
|
||||
)
|
||||
@ -279,7 +276,7 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
|
||||
}
|
||||
}.toSet()
|
||||
|
||||
return (0 until constructorInformation.parameters.size).none { index ->
|
||||
return (constructorInformation.parameters.indices).none { index ->
|
||||
constructorInformation.parameters[index].isMandatory && index !in indicesAddressedByProperties
|
||||
}
|
||||
}
|
||||
@ -298,7 +295,7 @@ internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup,
|
||||
}
|
||||
}.toSet()
|
||||
|
||||
return (0 until constructorInformation.parameters.size).mapNotNull { index ->
|
||||
return (constructorInformation.parameters.indices).mapNotNull { index ->
|
||||
val parameter = constructorInformation.parameters[index]
|
||||
when {
|
||||
constructorInformation.parameters[index].isMandatory && index !in indicesAddressedByProperties -> parameter
|
||||
|
@ -77,7 +77,7 @@ open class MockServices private constructor(
|
||||
private val cordappLoader: CordappLoader,
|
||||
override val validatedTransactions: TransactionStorage,
|
||||
override val identityService: IdentityService,
|
||||
private val initialNetworkParameters: NetworkParameters,
|
||||
initialNetworkParameters: NetworkParameters,
|
||||
private val initialIdentity: TestIdentity,
|
||||
private val moreKeys: Array<out KeyPair>,
|
||||
override val keyManagementService: KeyManagementService = MockKeyManagementService(
|
||||
@ -425,10 +425,14 @@ open class MockServices private constructor(
|
||||
get() {
|
||||
return NodeInfo(listOf(NetworkHostAndPort("mock.node.services", 10000)), listOf(initialIdentity.identity), 1, serial = 1L)
|
||||
}
|
||||
override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2)
|
||||
private val mockCordappProvider: MockCordappProvider = MockCordappProvider(cordappLoader, attachments).also {
|
||||
it.start()
|
||||
}
|
||||
override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(
|
||||
numberOfWorkers = 2,
|
||||
cordappProvider = mockCordappProvider,
|
||||
attachments = attachments
|
||||
)
|
||||
override val cordappProvider: CordappProvider get() = mockCordappProvider
|
||||
override var networkParametersService: NetworkParametersService = MockNetworkParametersStorage(initialNetworkParameters)
|
||||
override val diagnosticsService: DiagnosticsService = NodeDiagnosticsService()
|
||||
|
@ -4,6 +4,7 @@ import io.github.classgraph.ClassGraph
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.cordapp.CordappImpl
|
||||
import net.corda.core.internal.cordapp.set
|
||||
import net.corda.core.node.services.AttachmentFixup
|
||||
import net.corda.core.serialization.SerializationWhitelist
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
@ -32,18 +33,21 @@ data class CustomCordapp(
|
||||
val versionId: Int = 1,
|
||||
val targetPlatformVersion: Int = PLATFORM_VERSION,
|
||||
val classes: Set<Class<*>> = emptySet(),
|
||||
val fixups: List<AttachmentFixup> = emptyList(),
|
||||
val signingInfo: SigningInfo? = null,
|
||||
override val config: Map<String, Any> = emptyMap()
|
||||
) : TestCordappInternal() {
|
||||
init {
|
||||
require(packages.isNotEmpty() || classes.isNotEmpty()) { "At least one package or class must be specified" }
|
||||
require(packages.isNotEmpty() || classes.isNotEmpty() || fixups.isNotEmpty()) {
|
||||
"At least one package or class must be specified"
|
||||
}
|
||||
}
|
||||
|
||||
override val jarFile: Path get() = getJarFile(this)
|
||||
|
||||
override fun withConfig(config: Map<String, Any>): CustomCordapp = copy(config = config)
|
||||
|
||||
override fun withOnlyJarContents(): CustomCordapp = CustomCordapp(packages = packages, classes = classes)
|
||||
override fun withOnlyJarContents(): CustomCordapp = CustomCordapp(packages = packages, classes = classes, fixups = fixups)
|
||||
|
||||
fun signed(keyStorePath: Path? = null, numberOfSignatures: Int = 1, keyAlgorithm: String = "RSA"): CustomCordapp =
|
||||
copy(signingInfo = SigningInfo(keyStorePath, numberOfSignatures, keyAlgorithm))
|
||||
@ -83,20 +87,36 @@ data class CustomCordapp(
|
||||
}
|
||||
}
|
||||
|
||||
internal fun createFixupJar(file: Path) {
|
||||
JarOutputStream(file.outputStream()).use { jos ->
|
||||
jos.addEntry(testEntry(JarFile.MANIFEST_NAME)) {
|
||||
createTestManifest(name, versionId, targetPlatformVersion).write(jos)
|
||||
}
|
||||
jos.addEntry(testEntry("META-INF/Corda-Fixups")) {
|
||||
fixups.filter { it.first.isNotEmpty() }.forEach { (source, target) ->
|
||||
val data = source.joinToString(
|
||||
separator = ",",
|
||||
postfix = target.joinToString(
|
||||
separator = ",",
|
||||
prefix = "=>",
|
||||
postfix = "\r\n"
|
||||
)
|
||||
)
|
||||
jos.write(data.toByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun signJar(jarFile: Path) {
|
||||
if (signingInfo != null) {
|
||||
val keyStorePathToUse = if (signingInfo.keyStorePath != null) {
|
||||
signingInfo.keyStorePath
|
||||
} else {
|
||||
defaultJarSignerDirectory.createDirectories()
|
||||
defaultJarSignerDirectory
|
||||
}
|
||||
|
||||
val keyStorePathToUse = signingInfo.keyStorePath ?: defaultJarSignerDirectory.createDirectories()
|
||||
for (i in 1 .. signingInfo.numberOfSignatures) {
|
||||
val alias = "alias$i"
|
||||
val pwd = "secret!"
|
||||
if (!keyStorePathToUse.containsKey(alias, pwd))
|
||||
if (!keyStorePathToUse.containsKey(alias, pwd)) {
|
||||
keyStorePathToUse.generateKey(alias, pwd, "O=Test Company Ltd $i,OU=Test,L=London,C=GB", signingInfo.keyAlgorithm)
|
||||
}
|
||||
val pk = keyStorePathToUse.signJar(jarFile.toString(), alias, pwd)
|
||||
logger.debug { "Signed Jar: $jarFile with public key $pk" }
|
||||
}
|
||||
@ -141,7 +161,7 @@ data class CustomCordapp(
|
||||
private val epochFileTime = FileTime.from(Instant.EPOCH)
|
||||
private val cordappsDirectory: Path
|
||||
private val defaultJarSignerDirectory: Path
|
||||
private val whitespace = "\\s".toRegex()
|
||||
private val whitespace = "\\s++".toRegex()
|
||||
private val cache = ConcurrentHashMap<CustomCordapp, Path>()
|
||||
|
||||
init {
|
||||
@ -156,7 +176,11 @@ data class CustomCordapp(
|
||||
return cache.computeIfAbsent(cordapp.copy(config = emptyMap())) {
|
||||
val filename = it.run { "${name.replace(whitespace, "-")}_${versionId}_${targetPlatformVersion}_${UUID.randomUUID()}.jar" }
|
||||
val jarFile = cordappsDirectory.createDirectories() / filename
|
||||
it.packageAsJar(jarFile)
|
||||
if (it.fixups.isNotEmpty()) {
|
||||
it.createFixupJar(jarFile)
|
||||
} else {
|
||||
it.packageAsJar(jarFile)
|
||||
}
|
||||
it.signJar(jarFile)
|
||||
logger.debug { "$it packaged into $jarFile" }
|
||||
jarFile
|
||||
|
@ -13,6 +13,7 @@ import net.corda.core.internal.concurrent.openFuture
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.times
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.node.services.AttachmentFixup
|
||||
import net.corda.core.serialization.internal.SerializationEnvironment
|
||||
import net.corda.core.serialization.internal._allEnabledSerializationEnvs
|
||||
import net.corda.core.serialization.internal._driverSerializationEnv
|
||||
@ -94,6 +95,8 @@ fun cordappWithPackages(vararg packageNames: String): CustomCordapp = CustomCord
|
||||
// TODO Rename to cordappWithClasses
|
||||
fun cordappForClasses(vararg classes: Class<*>): CustomCordapp = CustomCordapp(packages = emptySet(), classes = classes.toSet())
|
||||
|
||||
fun cordappWithFixups(fixups: List<AttachmentFixup>) = CustomCordapp(fixups = fixups)
|
||||
|
||||
/**
|
||||
* Find the single CorDapp jar on the current classpath which contains the given package. This is a convenience method for
|
||||
* [TestCordapp.findCordapp] but returns the internal [TestCordappImpl].
|
||||
|
@ -28,7 +28,7 @@ data class TestCordappImpl(val scanPackage: String, override val config: Map<Str
|
||||
|
||||
override val jarFile: Path
|
||||
get() {
|
||||
val jars = TestCordappImpl.findJars(scanPackage)
|
||||
val jars = findJars(scanPackage)
|
||||
when (jars.size) {
|
||||
0 -> throw IllegalArgumentException("There are no CorDapps containing the package $scanPackage on the classpath. Make sure " +
|
||||
"the package name is correct and that the CorDapp is added as a gradle dependency.")
|
||||
@ -57,13 +57,12 @@ data class TestCordappImpl(val scanPackage: String, override val config: Map<Str
|
||||
private fun findRootPaths(scanPackage: String): Set<Path> {
|
||||
return packageToRootPaths.computeIfAbsent(scanPackage) {
|
||||
val classGraph = ClassGraph().whitelistPaths(scanPackage.replace('.', '/'))
|
||||
classGraph.pooledScan().use {
|
||||
it.allResources
|
||||
.asSequence()
|
||||
.map { it.classpathElementFile.toPath() }
|
||||
.filterNot { it.toString().endsWith("-tests.jar") }
|
||||
.map { if (it.toString().endsWith(".jar")) it else findProjectRoot(it) }
|
||||
.toSet()
|
||||
classGraph.pooledScan().use { scanResult ->
|
||||
scanResult.allResources
|
||||
.asSequence()
|
||||
.map { it.classpathElementFile.toPath() }
|
||||
.filterNot { it.toString().endsWith("-tests.jar") }
|
||||
.mapTo(LinkedHashSet()) { if (it.toString().endsWith(".jar")) it else findProjectRoot(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user