mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
ENT-5430: Fix deserialisation of commands containing generic types. (#6359)
This commit is contained in:
parent
2b7c220522
commit
d0c0a1d9ba
@ -0,0 +1,14 @@
|
|||||||
|
package net.corda.contracts.serialization.generics
|
||||||
|
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
|
data class DataObject(val value: Long) : Comparable<DataObject> {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "$value data points"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun compareTo(other: DataObject): Int {
|
||||||
|
return value.compareTo(other.value)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package net.corda.contracts.serialization.generics
|
||||||
|
|
||||||
|
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
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
class GenericTypeContract : Contract {
|
||||||
|
override fun verify(tx: LedgerTransaction) {
|
||||||
|
val state = tx.outputsOfType<State>()
|
||||||
|
require(state.isNotEmpty()) {
|
||||||
|
"Requires at least one data state"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("CanBeParameter", "MemberVisibilityCanBePrivate")
|
||||||
|
class State(val owner: AbstractParty, val data: DataObject) : ContractState {
|
||||||
|
override val participants: List<AbstractParty> = listOf(owner)
|
||||||
|
|
||||||
|
@Override
|
||||||
|
override fun toString(): String {
|
||||||
|
return data.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [price] field is the important feature of the [Purchase]
|
||||||
|
* class because its type is [Optional] with a CorDapp-specific
|
||||||
|
* generic type parameter. It does not matter that the [price]
|
||||||
|
* is not used; it only matters that the [Purchase] command
|
||||||
|
* must be serialized as part of building a new transaction.
|
||||||
|
*/
|
||||||
|
class Purchase(val price: Optional<DataObject>) : CommandData
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
package net.corda.flows.serialization.generics
|
||||||
|
|
||||||
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import net.corda.contracts.serialization.generics.DataObject
|
||||||
|
import net.corda.contracts.serialization.generics.GenericTypeContract.Purchase
|
||||||
|
import net.corda.contracts.serialization.generics.GenericTypeContract.State
|
||||||
|
import net.corda.core.contracts.Command
|
||||||
|
import net.corda.core.crypto.SecureHash
|
||||||
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.flows.StartableByRPC
|
||||||
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
|
@StartableByRPC
|
||||||
|
class GenericTypeFlow(private val purchase: DataObject) : FlowLogic<SecureHash>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call(): SecureHash {
|
||||||
|
val notary = serviceHub.networkMapCache.notaryIdentities[0]
|
||||||
|
val stx = serviceHub.signInitialTransaction(
|
||||||
|
TransactionBuilder(notary)
|
||||||
|
.addOutputState(State(ourIdentity, purchase))
|
||||||
|
.addCommand(Command(Purchase(Optional.of(purchase)), ourIdentity.owningKey))
|
||||||
|
)
|
||||||
|
stx.verify(serviceHub, checkSufficientSignatures = false)
|
||||||
|
return stx.id
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
package net.corda.node
|
||||||
|
|
||||||
|
import net.corda.client.rpc.CordaRPCClient
|
||||||
|
import net.corda.contracts.serialization.generics.DataObject
|
||||||
|
import net.corda.core.messaging.startFlow
|
||||||
|
import net.corda.core.utilities.getOrThrow
|
||||||
|
import net.corda.core.utilities.loggerFor
|
||||||
|
import net.corda.flows.serialization.generics.GenericTypeFlow
|
||||||
|
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.User
|
||||||
|
import net.corda.testing.node.internal.cordappWithPackages
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
class ContractWithGenericTypeTest {
|
||||||
|
companion object {
|
||||||
|
const val DATA_VALUE = 5000L
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val logger = loggerFor<ContractWithGenericTypeTest>()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout=300_000)
|
||||||
|
fun `flow with generic type`() {
|
||||||
|
val user = User("u", "p", setOf(Permissions.all()))
|
||||||
|
driver(DriverParameters(
|
||||||
|
portAllocation = incrementalPortAllocation(),
|
||||||
|
startNodesInProcess = false,
|
||||||
|
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = true)),
|
||||||
|
cordappsForAllNodes = listOf(
|
||||||
|
cordappWithPackages("net.corda.flows.serialization.generics").signed(),
|
||||||
|
cordappWithPackages("net.corda.contracts.serialization.generics").signed()
|
||||||
|
)
|
||||||
|
)) {
|
||||||
|
val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
||||||
|
val txID = CordaRPCClient(hostAndPort = alice.rpcAddress)
|
||||||
|
.start(user.username, user.password)
|
||||||
|
.use { client ->
|
||||||
|
client.proxy.startFlow(::GenericTypeFlow, DataObject(DATA_VALUE))
|
||||||
|
.returnValue
|
||||||
|
.getOrThrow()
|
||||||
|
}
|
||||||
|
logger.info("TX-ID=$txID")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
package net.corda.node.services
|
||||||
|
|
||||||
|
import net.corda.client.rpc.CordaRPCClient
|
||||||
|
import net.corda.contracts.serialization.generics.DataObject
|
||||||
|
import net.corda.core.messaging.startFlow
|
||||||
|
import net.corda.core.utilities.getOrThrow
|
||||||
|
import net.corda.core.utilities.loggerFor
|
||||||
|
import net.corda.flows.serialization.generics.GenericTypeFlow
|
||||||
|
import net.corda.node.DeterministicSourcesRule
|
||||||
|
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.User
|
||||||
|
import net.corda.testing.node.internal.cordappWithPackages
|
||||||
|
import org.junit.ClassRule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
class DeterministicContractWithGenericTypeTest {
|
||||||
|
companion object {
|
||||||
|
const val DATA_VALUE = 5000L
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val logger = loggerFor<DeterministicContractWithGenericTypeTest>()
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
@JvmField
|
||||||
|
val djvmSources = DeterministicSourcesRule()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout=300_000)
|
||||||
|
fun `test DJVM can deserialise command with generic type`() {
|
||||||
|
val user = User("u", "p", setOf(Permissions.all()))
|
||||||
|
driver(DriverParameters(
|
||||||
|
portAllocation = incrementalPortAllocation(),
|
||||||
|
startNodesInProcess = false,
|
||||||
|
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = true)),
|
||||||
|
cordappsForAllNodes = listOf(
|
||||||
|
cordappWithPackages("net.corda.flows.serialization.generics").signed(),
|
||||||
|
cordappWithPackages("net.corda.contracts.serialization.generics").signed()
|
||||||
|
),
|
||||||
|
djvmBootstrapSource = djvmSources.bootstrap,
|
||||||
|
djvmCordaSource = djvmSources.corda
|
||||||
|
)) {
|
||||||
|
val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
||||||
|
val txID = CordaRPCClient(hostAndPort = alice.rpcAddress)
|
||||||
|
.start(user.username, user.password)
|
||||||
|
.use { client ->
|
||||||
|
client.proxy.startFlow(::GenericTypeFlow, DataObject(DATA_VALUE))
|
||||||
|
.returnValue
|
||||||
|
.getOrThrow()
|
||||||
|
}
|
||||||
|
logger.info("TX-ID=$txID")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -75,7 +75,7 @@ class SandboxSerializerFactoryFactory(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val fingerPrinter = TypeModellingFingerPrinter(customSerializerRegistry)
|
val fingerPrinter = TypeModellingFingerPrinter(customSerializerRegistry, classLoader)
|
||||||
|
|
||||||
val localSerializerFactory = DefaultLocalSerializerFactory(
|
val localSerializerFactory = DefaultLocalSerializerFactory(
|
||||||
whitelist = context.whitelist,
|
whitelist = context.whitelist,
|
||||||
|
@ -7,7 +7,6 @@ import net.corda.core.utilities.debug
|
|||||||
import net.corda.core.utilities.trace
|
import net.corda.core.utilities.trace
|
||||||
import net.corda.serialization.internal.model.*
|
import net.corda.serialization.internal.model.*
|
||||||
import net.corda.serialization.internal.model.TypeIdentifier.*
|
import net.corda.serialization.internal.model.TypeIdentifier.*
|
||||||
import net.corda.serialization.internal.model.TypeIdentifier.Companion.classLoaderFor
|
|
||||||
import org.apache.qpid.proton.amqp.Symbol
|
import org.apache.qpid.proton.amqp.Symbol
|
||||||
import java.lang.reflect.ParameterizedType
|
import java.lang.reflect.ParameterizedType
|
||||||
import java.lang.reflect.Type
|
import java.lang.reflect.Type
|
||||||
@ -161,7 +160,7 @@ class DefaultLocalSerializerFactory(
|
|||||||
val declaredGenericType = if (declaredType !is ParameterizedType
|
val declaredGenericType = if (declaredType !is ParameterizedType
|
||||||
&& localTypeInformation.typeIdentifier is Parameterised
|
&& localTypeInformation.typeIdentifier is Parameterised
|
||||||
&& declaredClass != Class::class.java) {
|
&& declaredClass != Class::class.java) {
|
||||||
localTypeInformation.typeIdentifier.getLocalType(classLoaderFor(declaredClass))
|
localTypeInformation.typeIdentifier.getLocalType(classloader)
|
||||||
} else {
|
} else {
|
||||||
declaredType
|
declaredType
|
||||||
}
|
}
|
||||||
|
@ -103,7 +103,7 @@ object SerializerFactoryBuilder {
|
|||||||
customSerializerRegistry))
|
customSerializerRegistry))
|
||||||
|
|
||||||
val fingerPrinter = overrideFingerPrinter ?:
|
val fingerPrinter = overrideFingerPrinter ?:
|
||||||
TypeModellingFingerPrinter(customSerializerRegistry)
|
TypeModellingFingerPrinter(customSerializerRegistry, classCarpenter.classloader)
|
||||||
|
|
||||||
val localSerializerFactory = DefaultLocalSerializerFactory(
|
val localSerializerFactory = DefaultLocalSerializerFactory(
|
||||||
whitelist,
|
whitelist,
|
||||||
|
@ -45,12 +45,12 @@ sealed class TypeIdentifier {
|
|||||||
* Obtain a nicely-formatted representation of the identified type, for help with debugging.
|
* Obtain a nicely-formatted representation of the identified type, for help with debugging.
|
||||||
*/
|
*/
|
||||||
fun prettyPrint(simplifyClassNames: Boolean = true): String = when(this) {
|
fun prettyPrint(simplifyClassNames: Boolean = true): String = when(this) {
|
||||||
is TypeIdentifier.UnknownType -> "?"
|
is UnknownType -> "?"
|
||||||
is TypeIdentifier.TopType -> "*"
|
is TopType -> "*"
|
||||||
is TypeIdentifier.Unparameterised -> name.simplifyClassNameIfRequired(simplifyClassNames)
|
is Unparameterised -> name.simplifyClassNameIfRequired(simplifyClassNames)
|
||||||
is TypeIdentifier.Erased -> "${name.simplifyClassNameIfRequired(simplifyClassNames)} (erased)"
|
is Erased -> "${name.simplifyClassNameIfRequired(simplifyClassNames)} (erased)"
|
||||||
is TypeIdentifier.ArrayOf -> "${componentType.prettyPrint(simplifyClassNames)}[]"
|
is ArrayOf -> "${componentType.prettyPrint(simplifyClassNames)}[]"
|
||||||
is TypeIdentifier.Parameterised ->
|
is Parameterised ->
|
||||||
name.simplifyClassNameIfRequired(simplifyClassNames) + parameters.joinToString(", ", "<", ">") {
|
name.simplifyClassNameIfRequired(simplifyClassNames) + parameters.joinToString(", ", "<", ">") {
|
||||||
it.prettyPrint(simplifyClassNames)
|
it.prettyPrint(simplifyClassNames)
|
||||||
}
|
}
|
||||||
@ -63,8 +63,6 @@ sealed class TypeIdentifier {
|
|||||||
// This method has locking. So we memo the value here.
|
// This method has locking. So we memo the value here.
|
||||||
private val systemClassLoader: ClassLoader = ClassLoader.getSystemClassLoader()
|
private val systemClassLoader: ClassLoader = ClassLoader.getSystemClassLoader()
|
||||||
|
|
||||||
fun classLoaderFor(clazz: Class<*>): ClassLoader = clazz.classLoader ?: systemClassLoader
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtain the [TypeIdentifier] for an erased Java class.
|
* Obtain the [TypeIdentifier] for an erased Java class.
|
||||||
*
|
*
|
||||||
@ -81,7 +79,7 @@ sealed class TypeIdentifier {
|
|||||||
* Obtain the [TypeIdentifier] for a Java [Type] (typically obtained by calling one of
|
* Obtain the [TypeIdentifier] for a Java [Type] (typically obtained by calling one of
|
||||||
* [java.lang.reflect.Parameter.getAnnotatedType],
|
* [java.lang.reflect.Parameter.getAnnotatedType],
|
||||||
* [java.lang.reflect.Field.getGenericType] or
|
* [java.lang.reflect.Field.getGenericType] or
|
||||||
* [java.lang.reflect.Method.getGenericReturnType]). Wildcard types and type variables are converted to [Unknown].
|
* [java.lang.reflect.Method.getGenericReturnType]). Wildcard types and type variables are converted to [UnknownType].
|
||||||
*
|
*
|
||||||
* @param type The [Type] to obtain a [TypeIdentifier] for.
|
* @param type The [Type] to obtain a [TypeIdentifier] for.
|
||||||
* @param resolutionContext Optionally, a [Type] which can be used to resolve type variables, for example a
|
* @param resolutionContext Optionally, a [Type] which can be used to resolve type variables, for example a
|
||||||
@ -273,5 +271,5 @@ private class ReconstitutedParameterizedType(
|
|||||||
other.ownerType == ownerType &&
|
other.ownerType == ownerType &&
|
||||||
Arrays.equals(other.actualTypeArguments, actualTypeArguments)
|
Arrays.equals(other.actualTypeArguments, actualTypeArguments)
|
||||||
override fun hashCode(): Int =
|
override fun hashCode(): Int =
|
||||||
Arrays.hashCode(actualTypeArguments) xor Objects.hashCode(ownerType) xor Objects.hashCode(rawType)
|
actualTypeArguments.contentHashCode() xor Objects.hashCode(ownerType) xor Objects.hashCode(rawType)
|
||||||
}
|
}
|
@ -5,7 +5,6 @@ import net.corda.core.utilities.contextLogger
|
|||||||
import net.corda.core.utilities.toBase64
|
import net.corda.core.utilities.toBase64
|
||||||
import net.corda.serialization.internal.amqp.*
|
import net.corda.serialization.internal.amqp.*
|
||||||
import net.corda.serialization.internal.model.TypeIdentifier.*
|
import net.corda.serialization.internal.model.TypeIdentifier.*
|
||||||
import net.corda.serialization.internal.model.TypeIdentifier.Companion.classLoaderFor
|
|
||||||
import java.lang.reflect.ParameterizedType
|
import java.lang.reflect.ParameterizedType
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -31,6 +30,7 @@ interface FingerPrinter {
|
|||||||
*/
|
*/
|
||||||
class TypeModellingFingerPrinter(
|
class TypeModellingFingerPrinter(
|
||||||
private val customTypeDescriptorLookup: CustomSerializerRegistry,
|
private val customTypeDescriptorLookup: CustomSerializerRegistry,
|
||||||
|
private val classLoader: ClassLoader,
|
||||||
private val debugEnabled: Boolean = false) : FingerPrinter {
|
private val debugEnabled: Boolean = false) : FingerPrinter {
|
||||||
|
|
||||||
private val cache: MutableMap<TypeIdentifier, String> = DefaultCacheProvider.createCache()
|
private val cache: MutableMap<TypeIdentifier, String> = DefaultCacheProvider.createCache()
|
||||||
@ -42,7 +42,7 @@ class TypeModellingFingerPrinter(
|
|||||||
* the Fingerprinter cannot guarantee that.
|
* the Fingerprinter cannot guarantee that.
|
||||||
*/
|
*/
|
||||||
cache.getOrPut(typeInformation.typeIdentifier) {
|
cache.getOrPut(typeInformation.typeIdentifier) {
|
||||||
FingerPrintingState(customTypeDescriptorLookup, FingerprintWriter(debugEnabled))
|
FingerPrintingState(customTypeDescriptorLookup, classLoader, FingerprintWriter(debugEnabled))
|
||||||
.fingerprint(typeInformation)
|
.fingerprint(typeInformation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -95,6 +95,7 @@ internal class FingerprintWriter(debugEnabled: Boolean = false) {
|
|||||||
*/
|
*/
|
||||||
private class FingerPrintingState(
|
private class FingerPrintingState(
|
||||||
private val customSerializerRegistry: CustomSerializerRegistry,
|
private val customSerializerRegistry: CustomSerializerRegistry,
|
||||||
|
private val classLoader: ClassLoader,
|
||||||
private val writer: FingerprintWriter) {
|
private val writer: FingerprintWriter) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -200,7 +201,7 @@ private class FingerPrintingState(
|
|||||||
private fun fingerprintName(type: LocalTypeInformation) {
|
private fun fingerprintName(type: LocalTypeInformation) {
|
||||||
val identifier = type.typeIdentifier
|
val identifier = type.typeIdentifier
|
||||||
when (identifier) {
|
when (identifier) {
|
||||||
is TypeIdentifier.ArrayOf -> writer.write(identifier.componentType.name).writeArray()
|
is ArrayOf -> writer.write(identifier.componentType.name).writeArray()
|
||||||
else -> writer.write(identifier.name)
|
else -> writer.write(identifier.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -239,7 +240,7 @@ private class FingerPrintingState(
|
|||||||
val observedGenericType = if (observedType !is ParameterizedType
|
val observedGenericType = if (observedType !is ParameterizedType
|
||||||
&& type.typeIdentifier is Parameterised
|
&& type.typeIdentifier is Parameterised
|
||||||
&& observedClass != Class::class.java) {
|
&& observedClass != Class::class.java) {
|
||||||
type.typeIdentifier.getLocalType(classLoaderFor(observedClass))
|
type.typeIdentifier.getLocalType(classLoader)
|
||||||
} else {
|
} else {
|
||||||
observedType
|
observedType
|
||||||
}
|
}
|
||||||
@ -259,6 +260,5 @@ private class FingerPrintingState(
|
|||||||
// and deserializing (assuming deserialization is occurring in a factory that didn't
|
// and deserializing (assuming deserialization is occurring in a factory that didn't
|
||||||
// serialise the object in the first place (and thus the cache lookup fails). This is also
|
// serialise the object in the first place (and thus the cache lookup fails). This is also
|
||||||
// true of Any, where we need Example<A, B> and Example<?, ?> to have the same fingerprint
|
// true of Any, where we need Example<A, B> and Example<?, ?> to have the same fingerprint
|
||||||
private fun hasSeen(type: TypeIdentifier) = (type in typesSeen)
|
private fun hasSeen(type: TypeIdentifier) = (type in typesSeen) && (type != UnknownType)
|
||||||
&& (type != TypeIdentifier.UnknownType)
|
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ class TypeModellingFingerPrinterTests {
|
|||||||
|
|
||||||
val descriptorBasedSerializerRegistry = DefaultDescriptorBasedSerializerRegistry()
|
val descriptorBasedSerializerRegistry = DefaultDescriptorBasedSerializerRegistry()
|
||||||
val customRegistry = CachingCustomSerializerRegistry(descriptorBasedSerializerRegistry)
|
val customRegistry = CachingCustomSerializerRegistry(descriptorBasedSerializerRegistry)
|
||||||
val fingerprinter = TypeModellingFingerPrinter(customRegistry, true)
|
val fingerprinter = TypeModellingFingerPrinter(customRegistry, ClassLoader.getSystemClassLoader(), true)
|
||||||
|
|
||||||
// See https://r3-cev.atlassian.net/browse/CORDA-2266
|
// See https://r3-cev.atlassian.net/browse/CORDA-2266
|
||||||
@Test(timeout=300_000)
|
@Test(timeout=300_000)
|
||||||
|
Loading…
Reference in New Issue
Block a user