diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 69f9013678..c05ee4b314 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -17,11 +17,13 @@ import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationFactory import net.corda.core.utilities.contextLogger import java.io.NotSerializableException +import java.lang.Exception import java.security.PublicKey import java.time.Duration import java.time.Instant import java.util.ArrayDeque import java.util.UUID +import java.util.regex.Pattern import kotlin.collections.ArrayList import kotlin.collections.component1 import kotlin.collections.component2 @@ -67,6 +69,10 @@ open class TransactionBuilder( private fun defaultLockId() = (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID() private val log = contextLogger() 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>() @@ -168,42 +174,52 @@ open class TransactionBuilder( * @return true if a new dependency was successfully added. */ private fun addMissingDependency(services: ServicesForResolution, wireTx: WireTransaction): Boolean { - try { + return try { wireTx.toLedgerTransaction(services).verify() - } catch (e: NoClassDefFoundError) { - val missingClass = e.message ?: throw e - addMissingAttachment(missingClass, services) - return true - } catch (e: TransactionDeserialisationException) { - if (e.cause is NotSerializableException && e.cause.cause is ClassNotFoundException) { - val missingClass = e.cause.cause!!.message ?: throw e - addMissingAttachment(missingClass.replace(".", "/"), services) - return true + // The transaction verified successfully without adding any extra dependency. + false + } catch (e: Throwable) { + val rootError = e.rootCause + 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) + + // Ignore these exceptions as they will break unit tests. + // The point here is only to detect missing dependencies. The other exceptions are irrelevant. + e is TransactionVerificationException -> false + e is TransactionResolutionException -> false + e is IllegalStateException -> false + 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 - } 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. - // The point here is only to detect missing dependencies. The other exceptions are irrelevant. - } catch (tve: TransactionVerificationException) { - } catch (tre: TransactionResolutionException) { - } catch (ise: IllegalStateException) { - } catch (ise: IllegalArgumentException) { } - 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) - ?: 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. Please contact the developer of the CorDapp for further instructions. """.trimIndent()) + throw originalException + } log.warnOnce("""The transaction currently built is missing an attachment for class: $missingClass. Automatically attaching contract dependency $attachment. @@ -211,6 +227,7 @@ open class TransactionBuilder( """.trimIndent()) addAttachment(attachment.id) + return true } /** diff --git a/samples/simm-valuation-demo/contracts-states/src/main/kotlin/net/corda/vega/plugin/customserializers/UnusedFinanceSerializers.kt b/samples/simm-valuation-demo/contracts-states/src/main/kotlin/net/corda/vega/plugin/customserializers/UnusedFinanceSerializers.kt new file mode 100644 index 0000000000..fb2bb2ce37 --- /dev/null +++ b/samples/simm-valuation-demo/contracts-states/src/main/kotlin/net/corda/vega/plugin/customserializers/UnusedFinanceSerializers.kt @@ -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 { + class Proxy + override fun toProxy(obj: CashSchema): Proxy =Proxy() + override fun fromProxy(proxy: Proxy): CashSchema = CashSchema +} + +class Unused +@Suppress("UNUSED") +class UnusedFinanceSerializer1 : SerializationCustomSerializer { + init { + // Just instantiate some finance class. + Cash() + } + class Proxy + override fun toProxy(obj: Unused): Proxy =Proxy() + override fun fromProxy(proxy: Proxy): Unused = Unused() +}