diff --git a/node/src/integration-test/kotlin/net/corda/contracts/serialization/generics/DataObject.kt b/node/src/integration-test/kotlin/net/corda/contracts/serialization/generics/DataObject.kt new file mode 100644 index 0000000000..6384bd3900 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/contracts/serialization/generics/DataObject.kt @@ -0,0 +1,14 @@ +package net.corda.contracts.serialization.generics + +import net.corda.core.serialization.CordaSerializable + +@CordaSerializable +data class DataObject(val value: Long) : Comparable { + override fun toString(): String { + return "$value data points" + } + + override fun compareTo(other: DataObject): Int { + return value.compareTo(other.value) + } +} diff --git a/node/src/integration-test/kotlin/net/corda/contracts/serialization/generics/GenericTypeContract.kt b/node/src/integration-test/kotlin/net/corda/contracts/serialization/generics/GenericTypeContract.kt new file mode 100644 index 0000000000..4fcdae9da3 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/contracts/serialization/generics/GenericTypeContract.kt @@ -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() + 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 = 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) : CommandData +} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/flows/serialization/generics/GenericTypeFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/serialization/generics/GenericTypeFlow.kt new file mode 100644 index 0000000000..2325d767b0 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/flows/serialization/generics/GenericTypeFlow.kt @@ -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() { + @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 + } +} diff --git a/node/src/integration-test/kotlin/net/corda/node/ContractWithGenericTypeTest.kt b/node/src/integration-test/kotlin/net/corda/node/ContractWithGenericTypeTest.kt new file mode 100644 index 0000000000..4a093de5ba --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/ContractWithGenericTypeTest.kt @@ -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() + } + + @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") + } + } +} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithGenericTypeTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithGenericTypeTest.kt new file mode 100644 index 0000000000..b788091232 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/services/DeterministicContractWithGenericTypeTest.kt @@ -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() + + @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") + } + } +} \ No newline at end of file diff --git a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/SandboxSerializerFactoryFactory.kt b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/SandboxSerializerFactoryFactory.kt index bc614f2f95..96f8c44a03 100644 --- a/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/SandboxSerializerFactoryFactory.kt +++ b/serialization-djvm/src/main/kotlin/net/corda/serialization/djvm/SandboxSerializerFactoryFactory.kt @@ -75,7 +75,7 @@ class SandboxSerializerFactoryFactory( ) ) - val fingerPrinter = TypeModellingFingerPrinter(customSerializerRegistry) + val fingerPrinter = TypeModellingFingerPrinter(customSerializerRegistry, classLoader) val localSerializerFactory = DefaultLocalSerializerFactory( whitelist = context.whitelist, diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/LocalSerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/LocalSerializerFactory.kt index 9cf064a70b..9b0ce7b9ae 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/LocalSerializerFactory.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/LocalSerializerFactory.kt @@ -7,7 +7,6 @@ import net.corda.core.utilities.debug import net.corda.core.utilities.trace import net.corda.serialization.internal.model.* import net.corda.serialization.internal.model.TypeIdentifier.* -import net.corda.serialization.internal.model.TypeIdentifier.Companion.classLoaderFor import org.apache.qpid.proton.amqp.Symbol import java.lang.reflect.ParameterizedType import java.lang.reflect.Type @@ -161,7 +160,7 @@ class DefaultLocalSerializerFactory( val declaredGenericType = if (declaredType !is ParameterizedType && localTypeInformation.typeIdentifier is Parameterised && declaredClass != Class::class.java) { - localTypeInformation.typeIdentifier.getLocalType(classLoaderFor(declaredClass)) + localTypeInformation.typeIdentifier.getLocalType(classloader) } else { declaredType } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactoryBuilder.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactoryBuilder.kt index 27650621d7..e1d0aaee77 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactoryBuilder.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactoryBuilder.kt @@ -101,7 +101,7 @@ object SerializerFactoryBuilder { val localTypeModel = ConfigurableLocalTypeModel(typeModelConfiguration) val fingerPrinter = overrideFingerPrinter ?: - TypeModellingFingerPrinter(customSerializerRegistry) + TypeModellingFingerPrinter(customSerializerRegistry, classCarpenter.classloader) val localSerializerFactory = DefaultLocalSerializerFactory( whitelist, diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt index 2697b107a8..3477c02a48 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt @@ -45,12 +45,12 @@ sealed class TypeIdentifier { * Obtain a nicely-formatted representation of the identified type, for help with debugging. */ fun prettyPrint(simplifyClassNames: Boolean = true): String = when(this) { - is TypeIdentifier.UnknownType -> "?" - is TypeIdentifier.TopType -> "*" - is TypeIdentifier.Unparameterised -> name.simplifyClassNameIfRequired(simplifyClassNames) - is TypeIdentifier.Erased -> "${name.simplifyClassNameIfRequired(simplifyClassNames)} (erased)" - is TypeIdentifier.ArrayOf -> "${componentType.prettyPrint(simplifyClassNames)}[]" - is TypeIdentifier.Parameterised -> + is UnknownType -> "?" + is TopType -> "*" + is Unparameterised -> name.simplifyClassNameIfRequired(simplifyClassNames) + is Erased -> "${name.simplifyClassNameIfRequired(simplifyClassNames)} (erased)" + is ArrayOf -> "${componentType.prettyPrint(simplifyClassNames)}[]" + is Parameterised -> name.simplifyClassNameIfRequired(simplifyClassNames) + parameters.joinToString(", ", "<", ">") { it.prettyPrint(simplifyClassNames) } @@ -63,8 +63,6 @@ sealed class TypeIdentifier { // This method has locking. So we memo the value here. private val systemClassLoader: ClassLoader = ClassLoader.getSystemClassLoader() - fun classLoaderFor(clazz: Class<*>): ClassLoader = clazz.classLoader ?: systemClassLoader - /** * 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 * [java.lang.reflect.Parameter.getAnnotatedType], * [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 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 && Arrays.equals(other.actualTypeArguments, actualTypeArguments) 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) } \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeModellingFingerPrinter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeModellingFingerPrinter.kt index c5d79ed41f..8965a5c8e1 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeModellingFingerPrinter.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeModellingFingerPrinter.kt @@ -5,7 +5,6 @@ import net.corda.core.utilities.contextLogger import net.corda.core.utilities.toBase64 import net.corda.serialization.internal.amqp.* import net.corda.serialization.internal.model.TypeIdentifier.* -import net.corda.serialization.internal.model.TypeIdentifier.Companion.classLoaderFor import java.lang.reflect.ParameterizedType /** @@ -31,6 +30,7 @@ interface FingerPrinter { */ class TypeModellingFingerPrinter( private val customTypeDescriptorLookup: CustomSerializerRegistry, + private val classLoader: ClassLoader, private val debugEnabled: Boolean = false) : FingerPrinter { private val cache: MutableMap = DefaultCacheProvider.createCache() @@ -42,7 +42,7 @@ class TypeModellingFingerPrinter( * the Fingerprinter cannot guarantee that. */ cache.getOrPut(typeInformation.typeIdentifier) { - FingerPrintingState(customTypeDescriptorLookup, FingerprintWriter(debugEnabled)) + FingerPrintingState(customTypeDescriptorLookup, classLoader, FingerprintWriter(debugEnabled)) .fingerprint(typeInformation) } } @@ -95,6 +95,7 @@ internal class FingerprintWriter(debugEnabled: Boolean = false) { */ private class FingerPrintingState( private val customSerializerRegistry: CustomSerializerRegistry, + private val classLoader: ClassLoader, private val writer: FingerprintWriter) { companion object { @@ -200,7 +201,7 @@ private class FingerPrintingState( private fun fingerprintName(type: LocalTypeInformation) { val identifier = type.typeIdentifier when (identifier) { - is TypeIdentifier.ArrayOf -> writer.write(identifier.componentType.name).writeArray() + is ArrayOf -> writer.write(identifier.componentType.name).writeArray() else -> writer.write(identifier.name) } } @@ -239,7 +240,7 @@ private class FingerPrintingState( val observedGenericType = if (observedType !is ParameterizedType && type.typeIdentifier is Parameterised && observedClass != Class::class.java) { - type.typeIdentifier.getLocalType(classLoaderFor(observedClass)) + type.typeIdentifier.getLocalType(classLoader) } else { observedType } @@ -259,6 +260,5 @@ private class FingerPrintingState( // 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 // true of Any, where we need Example and Example to have the same fingerprint - private fun hasSeen(type: TypeIdentifier) = (type in typesSeen) - && (type != TypeIdentifier.UnknownType) + private fun hasSeen(type: TypeIdentifier) = (type in typesSeen) && (type != UnknownType) } diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/TypeModellingFingerPrinterTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/TypeModellingFingerPrinterTests.kt index 362972afc7..84c3a27e63 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/TypeModellingFingerPrinterTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/TypeModellingFingerPrinterTests.kt @@ -12,7 +12,7 @@ class TypeModellingFingerPrinterTests { val descriptorBasedSerializerRegistry = DefaultDescriptorBasedSerializerRegistry() 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 @Test(timeout=300_000)