ENT-5430: Fix deserialisation of commands containing generic types. (#6359)

This commit is contained in:
Chris Rankin 2020-06-17 17:28:26 +01:00 committed by GitHub
parent 2b7c220522
commit d0c0a1d9ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 207 additions and 21 deletions

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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")
}
}
}

View File

@ -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")
}
}
}

View File

@ -75,7 +75,7 @@ class SandboxSerializerFactoryFactory(
)
)
val fingerPrinter = TypeModellingFingerPrinter(customSerializerRegistry)
val fingerPrinter = TypeModellingFingerPrinter(customSerializerRegistry, classLoader)
val localSerializerFactory = DefaultLocalSerializerFactory(
whitelist = context.whitelist,

View File

@ -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
}

View File

@ -103,7 +103,7 @@ object SerializerFactoryBuilder {
customSerializerRegistry))
val fingerPrinter = overrideFingerPrinter ?:
TypeModellingFingerPrinter(customSerializerRegistry)
TypeModellingFingerPrinter(customSerializerRegistry, classCarpenter.classloader)
val localSerializerFactory = DefaultLocalSerializerFactory(
whitelist,

View File

@ -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)
}

View File

@ -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<TypeIdentifier, String> = 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<A, B> 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)
}

View File

@ -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)