diff --git a/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt b/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt index 3410aed39b..c0e227c9be 100644 --- a/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt +++ b/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt @@ -1,5 +1,7 @@ package net.corda.core.node +import com.esotericsoftware.kryo.Kryo + /** * Implement this interface on a class advertised in a META-INF/services/net.corda.core.node.CordaPluginRegistry file * to extend a Corda node with additional application services. @@ -35,4 +37,16 @@ abstract class CordaPluginRegistry { * allow access to the protocol factory and protocol initiation entry points there. */ open val servicePlugins: List<Class<*>> = emptyList() + + /** + * Optionally register types with [Kryo] for use over RPC, as we lock down the types that can be serialised in this + * particular use case. + * For example, if you add an RPC interface that carries some contract states back and forth, you need to register + * those classes here using the [register] method on Kryo. + * + * TODO: Kryo and likely the requirement to register classes here will go away when we replace the serialization implementation. + * + * @return true if you register types, otherwise you will be filtered out of the list of plugins considered in future. + */ + open fun registerRPCKryoTypes(kryo: Kryo): Boolean = false } diff --git a/docs/source/clientrpc.rst b/docs/source/clientrpc.rst index e918b116d4..0cb10d1077 100644 --- a/docs/source/clientrpc.rst +++ b/docs/source/clientrpc.rst @@ -80,5 +80,17 @@ Wire protocol The client RPC wire protocol is not currently documented. To use it you must use the client library provided. This is likely to change in a future release. +Registering Classes With RPC Kryo +--------------------------------- + +In the present implementation of the node we use Kryo to generate the *on the wire* representation of contracts states +or any other classes that form part of the RPC arguments or response. To avoid the RPC interface being wide open to all +classes on the classpath, Cordapps will currently have to register any classes or custom serialisers they require with Kryo +if they are not one of those registered by default in ``RPCKryo`` via the plugin architecture. See :doc:`creating-a-cordapp`. +This will require some familiarity with Kryo. An example is shown in :doc:`tutorial-clientrpc-api`. + +.. warning:: We will be replacing the use of Kryo in RPC with a stable message format and this will mean that this plugin + customisation point will either go away completely or change. + .. _CordaRPCClient: api/net.corda.client/-corda-r-p-c-client/index.html .. _CordaRPCOps: api/net.corda.node.services.messaging/-corda-r-p-c-ops/index.html diff --git a/docs/source/creating-a-cordapp.rst b/docs/source/creating-a-cordapp.rst index b05d76e3d1..bca9ddfa1b 100644 --- a/docs/source/creating-a-cordapp.rst +++ b/docs/source/creating-a-cordapp.rst @@ -16,6 +16,7 @@ specific details of the implementation, but you can extend the server in the fol 2. Service plugins: Register your services (see below). 3. Web APIs: You may register your own endpoints under /api/ of the built-in web server. 4. Static web endpoints: You may register your own static serving directories for serving web content. +5. Registering your additional classes used in RPC. Services -------- diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt index 871bd45924..364d14693f 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt @@ -1,5 +1,6 @@ package net.corda.docs +import com.esotericsoftware.kryo.Kryo import net.corda.client.CordaRPCClient import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount @@ -7,6 +8,7 @@ import net.corda.core.contracts.Issued import net.corda.core.contracts.PartyAndReference import net.corda.core.contracts.USD import net.corda.core.div +import net.corda.core.node.CordaPluginRegistry import net.corda.core.node.services.ServiceInfo import net.corda.core.serialization.OpaqueBytes import net.corda.core.transactions.SignedTransaction @@ -135,4 +137,17 @@ fun generateTransactions(proxy: CordaRPCOps) { } } } -// END 6 \ No newline at end of file +// END 6 + +// START 7 +data class ExampleRPCValue(val foo: String) + +class ExampleRPCCordaPluginRegistry : CordaPluginRegistry() { + override fun registerRPCKryoTypes(kryo: Kryo): Boolean { + // Add classes like this. + kryo.register(ExampleRPCValue::class.java) + // You should return true, otherwise your plugin will be ignored for registering classes with Kryo. + return true + } +} +// END 7 diff --git a/docs/source/tutorial-clientrpc-api.rst b/docs/source/tutorial-clientrpc-api.rst index c9419f716b..89abcf45a1 100644 --- a/docs/source/tutorial-clientrpc-api.rst +++ b/docs/source/tutorial-clientrpc-api.rst @@ -79,3 +79,19 @@ Now let's try to visualise the transaction graph. We will use a graph drawing li :end-before: END 5 If we run the client with ``Visualise`` we should see a simple random graph being drawn as new transactions are being created. + +Registering classes from your Cordapp with RPC Kryo +-------------------------------------------------- + +As described in :doc:`clientrpc`, you currently have to register any additional classes you add that are needed in RPC +requests or responses with the `Kryo` instance RPC uses. Here's an example of how you do this for an example class. + +.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt + :language: kotlin + :start-after: START 7 + :end-before: END 7 + +See more on plugins in :doc:`creating-a-cordapp`. + +.. warning:: We will be replacing the use of Kryo in RPC with a stable message format and this will mean that this plugin + customisation point will either go away completely or change. \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RPCStructures.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RPCStructures.kt index 053459f1f8..beff338c18 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/RPCStructures.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/RPCStructures.kt @@ -20,10 +20,7 @@ import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.Party import net.corda.core.crypto.PublicKeyTree import net.corda.core.crypto.SecureHash -import net.corda.core.node.NodeInfo -import net.corda.core.node.PhysicalLocation -import net.corda.core.node.ServiceEntry -import net.corda.core.node.WorldCoordinate +import net.corda.core.node.* import net.corda.core.node.services.* import net.corda.core.protocols.StateMachineRunId import net.corda.core.serialization.* @@ -117,6 +114,14 @@ class PermissionException(msg: String) : RuntimeException(msg) // This is annoying to write out, but will make it easier to formalise the wire protocol when the time comes, // because we can see everything we're using in one place. private class RPCKryo(observableSerializer: Serializer<Observable<Any>>? = null) : Kryo() { + companion object { + private val pluginRegistries: List<CordaPluginRegistry> by lazy { + val unusedKryo = Kryo() + // Sorting required to give a stable ordering, as Kryo allocates integer tokens for each registered class. + ServiceLoader.load(CordaPluginRegistry::class.java).toList().filter { it.registerRPCKryoTypes(unusedKryo) }.sortedBy { it.javaClass.name } + } + } + init { isRegistrationRequired = true // Allow construction of objects using a JVM backdoor that skips invoking the constructors, if there is no @@ -215,6 +220,7 @@ private class RPCKryo(observableSerializer: Serializer<Observable<Any>>? = null) register(ProtocolHandle::class.java) register(KryoException::class.java) register(StringBuffer::class.java) + pluginRegistries.forEach { it.registerRPCKryoTypes(this) } } // Helper method, attempt to reduce boiler plate code