CORDA-2847 -Support custom serialisers when attaching missing attachments to txs (#5046) (#5062)

(cherry picked from commit 2e4c5d79f6d98122e51849f3fd2a46ad2aad22e5)
This commit is contained in:
Tudor Malene 2019-05-01 11:13:31 +01:00 committed by Katelyn Baker
parent a29f417a67
commit bb41d2941d
2 changed files with 71 additions and 27 deletions

View File

@ -17,11 +17,13 @@ import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationFactory import net.corda.core.serialization.SerializationFactory
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import java.io.NotSerializableException import java.io.NotSerializableException
import java.lang.Exception
import java.security.PublicKey import java.security.PublicKey
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
import java.util.ArrayDeque import java.util.ArrayDeque
import java.util.UUID import java.util.UUID
import java.util.regex.Pattern
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.collections.component1 import kotlin.collections.component1
import kotlin.collections.component2 import kotlin.collections.component2
@ -67,6 +69,10 @@ open class TransactionBuilder(
private fun defaultLockId() = (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID() private fun defaultLockId() = (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID()
private val log = contextLogger() private val log = contextLogger()
private const val CORDA_VERSION_THAT_INTRODUCED_FLATTENED_COMMANDS = 4 private const val CORDA_VERSION_THAT_INTRODUCED_FLATTENED_COMMANDS = 4
private val ID_PATTERN = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*"
private val FQCP = Pattern.compile("$ID_PATTERN(/$ID_PATTERN)+")
private fun isValidJavaClass(identifier: String) = FQCP.matcher(identifier).matches()
} }
private val inputsWithTransactionState = arrayListOf<StateAndRef<ContractState>>() private val inputsWithTransactionState = arrayListOf<StateAndRef<ContractState>>()
@ -168,42 +174,52 @@ open class TransactionBuilder(
* @return true if a new dependency was successfully added. * @return true if a new dependency was successfully added.
*/ */
private fun addMissingDependency(services: ServicesForResolution, wireTx: WireTransaction): Boolean { private fun addMissingDependency(services: ServicesForResolution, wireTx: WireTransaction): Boolean {
try { return try {
wireTx.toLedgerTransaction(services).verify() wireTx.toLedgerTransaction(services).verify()
} catch (e: NoClassDefFoundError) { // The transaction verified successfully without adding any extra dependency.
val missingClass = e.message ?: throw e false
addMissingAttachment(missingClass, services) } catch (e: Throwable) {
return true val rootError = e.rootCause
} catch (e: TransactionDeserialisationException) { when {
if (e.cause is NotSerializableException && e.cause.cause is ClassNotFoundException) { // Handle various exceptions that can be thrown during verification and drill down the wrappings.
val missingClass = e.cause.cause!!.message ?: throw e // Note: this is a best effort to preserve backwards compatibility.
addMissingAttachment(missingClass.replace(".", "/"), services) rootError is ClassNotFoundException -> addMissingAttachment((rootError.message ?: throw e).replace(".", "/"), services, e)
return true rootError is NoClassDefFoundError -> addMissingAttachment(rootError.message ?: throw e, services, e)
}
return false
} catch (e: NotSerializableException) {
if (e.cause is ClassNotFoundException) {
val missingClass = e.cause!!.message ?: throw e
addMissingAttachment(missingClass.replace(".", "/"), services)
return true
}
return false
// Ignore these exceptions as they will break unit tests. // Ignore these exceptions as they will break unit tests.
// The point here is only to detect missing dependencies. The other exceptions are irrelevant. // The point here is only to detect missing dependencies. The other exceptions are irrelevant.
} catch (tve: TransactionVerificationException) { e is TransactionVerificationException -> false
} catch (tre: TransactionResolutionException) { e is TransactionResolutionException -> false
} catch (ise: IllegalStateException) { e is IllegalStateException -> false
} catch (ise: IllegalArgumentException) { e is IllegalArgumentException -> false
// Fail early if none of the expected scenarios were hit.
else -> {
log.error("""The transaction currently built will not validate because of an unknown error most likely caused by a
missing dependency in the transaction attachments.
Please contact the developer of the CorDapp for further instructions.
""".trimIndent(), e)
throw e
}
}
} }
return false
} }
private fun addMissingAttachment(missingClass: String, services: ServicesForResolution) { 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
}
val attachment = services.attachments.internalFindTrustedAttachmentForClass(missingClass) val attachment = services.attachments.internalFindTrustedAttachmentForClass(missingClass)
?: throw IllegalArgumentException("""The transaction currently built is missing an attachment for class: $missingClass.
if (attachment == null) {
log.error("""The transaction currently built is missing an attachment for class: $missingClass.
Attempted to find a suitable attachment but could not find any in the storage. Attempted to find a suitable attachment but could not find any in the storage.
Please contact the developer of the CorDapp for further instructions. Please contact the developer of the CorDapp for further instructions.
""".trimIndent()) """.trimIndent())
throw originalException
}
log.warnOnce("""The transaction currently built is missing an attachment for class: $missingClass. log.warnOnce("""The transaction currently built is missing an attachment for class: $missingClass.
Automatically attaching contract dependency $attachment. Automatically attaching contract dependency $attachment.
@ -211,6 +227,7 @@ open class TransactionBuilder(
""".trimIndent()) """.trimIndent())
addAttachment(attachment.id) addAttachment(attachment.id)
return true
} }
/** /**

View File

@ -0,0 +1,27 @@
package net.corda.vega.plugin.customserializers
import net.corda.core.serialization.SerializationCustomSerializer
import net.corda.finance.contracts.asset.Cash
import net.corda.finance.schemas.CashSchema
/**
* This just references a random class from the finance Cordapp for testing purposes.
*/
@Suppress("UNUSED")
class UnusedFinanceSerializer : SerializationCustomSerializer<CashSchema, UnusedFinanceSerializer.Proxy> {
class Proxy
override fun toProxy(obj: CashSchema): Proxy =Proxy()
override fun fromProxy(proxy: Proxy): CashSchema = CashSchema
}
class Unused
@Suppress("UNUSED")
class UnusedFinanceSerializer1 : SerializationCustomSerializer<Unused, UnusedFinanceSerializer1.Proxy> {
init {
// Just instantiate some finance class.
Cash()
}
class Proxy
override fun toProxy(obj: Unused): Proxy =Proxy()
override fun fromProxy(proxy: Proxy): Unused = Unused()
}