From 48f58b6dbccc29a5e4f1339b444a4beb9ee7813c Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Wed, 10 May 2017 11:28:25 +0100 Subject: [PATCH] Introducing StartableByRPC and SchedulableFlow annotations, needed by flows started via RPC and schedulable flows respectively. CordaPluginRegistry.requiredFlows is no longer needed as a result. --- .../net/corda/core/flows/FlowLogicRef.kt | 5 +- .../net/corda/core/flows/SchedulableFlow.kt | 14 +++ .../net/corda/core/flows/StartableByRPC.kt | 15 +++ .../net/corda/core/messaging/CordaRPCOps.kt | 4 +- .../corda/core/node/CordaPluginRegistry.kt | 56 +++++----- .../net/corda/flows/ContractUpgradeFlow.kt | 2 + .../core/flows/ContractUpgradeFlowTest.kt | 24 ++-- docs/source/changelog.rst | 7 +- docs/source/corda-plugins.rst | 16 +-- docs/source/creating-a-cordapp.rst | 9 +- docs/source/event-scheduling.rst | 8 +- docs/source/flow-state-machines.rst | 43 ++++++-- docs/source/tutorial-clientrpc-api.rst | 43 +++++--- .../kotlin/net/corda/flows/CashIssueFlow.kt | 2 + .../kotlin/net/corda/flows/CashPaymentFlow.kt | 2 + .../main/kotlin/net/corda/flows/IssuerFlow.kt | 2 + node/build.gradle | 3 + .../kotlin/net/corda/node/BootTests.kt | 13 +-- .../net.corda.core.node.CordaPluginRegistry | 1 - .../net/corda/node/internal/AbstractNode.kt | 103 ++++++++++++------ .../corda/node/internal/CordaRPCOpsImpl.kt | 23 ++-- .../node/services/api/ServiceHubInternal.kt | 17 ++- .../services/events/NodeSchedulerService.kt | 8 +- .../events/ScheduledActivityObserver.kt | 12 +- .../statemachine/FlowLogicRefFactoryImpl.kt | 96 ++++------------ .../net/corda/node/shell/InteractiveShell.kt | 17 +-- .../events/FlowLogicRefFromJavaTest.java | 19 +--- .../net/corda/node/CordaRPCOpsImplTest.kt | 22 +++- .../node/services/MockServiceHubInternal.kt | 6 +- .../node/services/events/FlowLogicRefTest.kt | 61 ++++------- .../events/NodeSchedulerServiceTest.kt | 14 +-- .../services/events/ScheduledFlowTests.kt | 16 +-- .../attachmentdemo/AttachmentDemoTest.kt | 5 +- .../corda/attachmentdemo/AttachmentDemo.kt | 51 ++++++--- .../plugin/AttachmentDemoPlugin.kt | 12 -- .../net.corda.core.node.CordaPluginRegistry | 2 - .../corda/bank/plugin/BankOfCordaPlugin.kt | 4 - .../net/corda/irs/api/NodeInterestRates.kt | 6 +- .../net/corda/irs/flows/AutoOfferFlow.kt | 2 + .../kotlin/net/corda/irs/flows/FixingFlow.kt | 2 + .../corda/irs/flows/UpdateBusinessDayFlow.kt | 2 + .../kotlin/net/corda/irs/plugin/IRSPlugin.kt | 11 -- .../notarydemo/plugin/NotaryDemoPlugin.kt | 15 --- .../net.corda.core.node.CordaPluginRegistry | 2 - .../net/corda/vega/flows/IRSTradeFlow.kt | 2 + .../kotlin/net/corda/vega/flows/SimmFlow.kt | 2 + .../net/corda/vega/flows/SimmRevaluation.kt | 4 + .../net/corda/vega/services/SimmService.kt | 8 -- .../net/corda/traderdemo/flow/SellerFlow.kt | 2 + .../traderdemo/plugin/TraderDemoPlugin.kt | 6 - .../corda/explorer/plugin/ExplorerPlugin.kt | 4 - .../webserver/servlets/CorDappInfoServlet.kt | 10 +- 52 files changed, 401 insertions(+), 434 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/flows/SchedulableFlow.kt create mode 100644 core/src/main/kotlin/net/corda/core/flows/StartableByRPC.kt delete mode 100644 node/src/integration-test/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry delete mode 100644 samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/plugin/AttachmentDemoPlugin.kt delete mode 100644 samples/attachment-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry delete mode 100644 samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/plugin/NotaryDemoPlugin.kt delete mode 100644 samples/raft-notary-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt index 7b4ff2e6ce..3f9996c081 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt @@ -9,11 +9,12 @@ import net.corda.core.serialization.CordaSerializable * the flow to run at the scheduled time. */ interface FlowLogicRefFactory { - fun create(type: Class>, vararg args: Any?): FlowLogicRef + fun create(flowClass: Class>, vararg args: Any?): FlowLogicRef } @CordaSerializable -class IllegalFlowLogicException(type: Class<*>, msg: String) : IllegalArgumentException("${FlowLogicRef::class.java.simpleName} cannot be constructed for ${FlowLogic::class.java.simpleName} of type ${type.name} $msg") +class IllegalFlowLogicException(type: Class<*>, msg: String) : IllegalArgumentException( + "${FlowLogicRef::class.java.simpleName} cannot be constructed for ${FlowLogic::class.java.simpleName} of type ${type.name} $msg") /** * A handle interface representing a [FlowLogic] instance which would be possible to safely pass out of the contract sandbox. diff --git a/core/src/main/kotlin/net/corda/core/flows/SchedulableFlow.kt b/core/src/main/kotlin/net/corda/core/flows/SchedulableFlow.kt new file mode 100644 index 0000000000..729299ee1a --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/flows/SchedulableFlow.kt @@ -0,0 +1,14 @@ +package net.corda.core.flows + +import java.lang.annotation.Inherited +import kotlin.annotation.AnnotationTarget.CLASS + +/** + * Any [FlowLogic] which is schedulable and is designed to be invoked by a [net.corda.core.contracts.SchedulableState] + * must have this annotation. If it's missing [FlowLogicRefFactory.create] will throw an exception when it comes time + * to schedule the next activity in [net.corda.core.contracts.SchedulableState.nextScheduledActivity]. + */ +@Target(CLASS) +@Inherited +@MustBeDocumented +annotation class SchedulableFlow \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/flows/StartableByRPC.kt b/core/src/main/kotlin/net/corda/core/flows/StartableByRPC.kt new file mode 100644 index 0000000000..6eafd3d699 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/flows/StartableByRPC.kt @@ -0,0 +1,15 @@ +package net.corda.core.flows + +import java.lang.annotation.Inherited +import kotlin.annotation.AnnotationTarget.CLASS + +/** + * Any [FlowLogic] which is to be started by the RPC interface ([net.corda.core.messaging.CordaRPCOps.startFlowDynamic] + * and [net.corda.core.messaging.CordaRPCOps.startTrackedFlowDynamic]) must have this annotation. If it's missing the + * flow will not be allowed to start and an exception will be thrown. + */ +@Target(CLASS) +@Inherited +@MustBeDocumented +// TODO Consider a different name, something along the lines of SchedulableFlow +annotation class StartableByRPC \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index d5c4a02de3..190f2456a0 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -148,14 +148,14 @@ interface CordaRPCOps : RPCOps { fun networkMapUpdates(): Pair, Observable> /** - * Start the given flow with the given arguments. + * Start the given flow with the given arguments. [logicType] must be annotated with [net.corda.core.flows.StartableByRPC]. */ @RPCReturnsObservables fun startFlowDynamic(logicType: Class>, vararg args: Any?): FlowHandle /** * Start the given flow with the given arguments, returning an [Observable] with a single observation of the - * result of running the flow. + * result of running the flow. [logicType] must be annotated with [net.corda.core.flows.StartableByRPC]. */ @RPCReturnsObservables fun startTrackedFlowDynamic(logicType: Class>, vararg args: Any?): FlowProgressHandle 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 e2ed48355b..6d73439665 100644 --- a/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt +++ b/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt @@ -8,42 +8,38 @@ import java.util.function.Function * 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. */ -abstract class CordaPluginRegistry( - /** - * List of lambdas returning JAX-RS objects. They may only depend on the RPC interface, as the webserver should - * potentially be able to live in a process separate from the node itself. - */ - open val webApis: List> = emptyList(), +abstract class CordaPluginRegistry { + /** + * List of lambdas returning JAX-RS objects. They may only depend on the RPC interface, as the webserver should + * potentially be able to live in a process separate from the node itself. + */ + open val webApis: List> get() = emptyList() - /** - * Map of static serving endpoints to the matching resource directory. All endpoints will be prefixed with "/web" and postfixed with "\*. - * Resource directories can be either on disk directories (especially when debugging) in the form "a/b/c". Serving from a JAR can - * be specified with: javaClass.getResource("").toExternalForm() - */ - open val staticServeDirs: Map = emptyMap(), + /** + * Map of static serving endpoints to the matching resource directory. All endpoints will be prefixed with "/web" and postfixed with "\*. + * Resource directories can be either on disk directories (especially when debugging) in the form "a/b/c". Serving from a JAR can + * be specified with: javaClass.getResource("").toExternalForm() + */ + open val staticServeDirs: Map get() = emptyMap() - /** - * A Map with an entry for each consumed Flow used by the webAPIs. - * The key of each map entry should contain the FlowLogic class name. - * The associated map values are the union of all concrete class names passed to the Flow constructor. - * Standard java.lang.* and kotlin.* types do not need to be included explicitly. - * This is used to extend the white listed Flows that can be initiated from the ServiceHub invokeFlowAsync method. - */ - open val requiredFlows: Map> = emptyMap(), + @Suppress("unused") + @Deprecated("This is no longer needed. Instead annotate any flows that need to be invoked via RPC with " + + "@StartableByRPC and any scheduled flows with @SchedulableFlow", level = DeprecationLevel.ERROR) + open val requiredFlows: Map> get() = emptyMap() + + /** + * List of lambdas constructing additional long lived services to be hosted within the node. + * They expect a single [PluginServiceHub] parameter as input. + * The [PluginServiceHub] will be fully constructed before the plugin service is created and will + * allow access to the Flow factory and Flow initiation entry points there. + */ + open val servicePlugins: List> get() = emptyList() - /** - * List of lambdas constructing additional long lived services to be hosted within the node. - * They expect a single [PluginServiceHub] parameter as input. - * The [PluginServiceHub] will be fully constructed before the plugin service is created and will - * allow access to the Flow factory and Flow initiation entry points there. - */ - open val servicePlugins: List> = emptyList() -) { /** * Optionally whitelist types for use in object serialization, as we lock down the types that can be serialized. * - * For example, if you add a new [ContractState] it needs to be whitelisted. You can do that either by - * adding the @CordaSerializable annotation or via this method. + * For example, if you add a new [net.corda.core.contracts.ContractState] it needs to be whitelisted. You can do that + * either by adding the [net.corda.core.serialization.CordaSerializable] annotation or via this method. ** * @return true if you register types, otherwise you will be filtered out of the list of plugins considered in future. */ diff --git a/core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt b/core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt index fd73271e45..8c1cb1e437 100644 --- a/core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt @@ -2,6 +2,7 @@ package net.corda.flows import net.corda.core.contracts.* import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import java.security.PublicKey @@ -15,6 +16,7 @@ import java.security.PublicKey * use the new updated state for future transactions. */ @InitiatingFlow +@StartableByRPC class ContractUpgradeFlow( originalState: StateAndRef, newContractClass: Class> diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index 738802dbf2..032a979672 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -1,5 +1,6 @@ package net.corda.core.flows +import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.Cash import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash @@ -9,6 +10,7 @@ import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow import net.corda.core.node.services.unconsumedStates import net.corda.core.serialization.OpaqueBytes +import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.Emoji import net.corda.flows.CashIssueFlow import net.corda.flows.ContractUpgradeFlow @@ -27,7 +29,6 @@ import org.junit.Before import org.junit.Test import java.security.PublicKey import java.util.* -import java.util.concurrent.ExecutionException import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertTrue @@ -69,10 +70,10 @@ class ContractUpgradeFlowTest { requireNotNull(atx) requireNotNull(btx) - // The request is expected to be rejected because party B haven't authorise the upgrade yet. + // The request is expected to be rejected because party B hasn't authorised the upgrade yet. val rejectedFuture = a.services.startFlow(ContractUpgradeFlow(atx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture mockNet.runNetwork() - assertFailsWith(ExecutionException::class) { rejectedFuture.get() } + assertFailsWith(FlowSessionException::class) { rejectedFuture.getOrThrow() } // Party B authorise the contract state upgrade. b.services.vaultService.authoriseContractUpgrade(btx!!.tx.outRef(0), DummyContractV2::class.java) @@ -81,7 +82,7 @@ class ContractUpgradeFlowTest { val resultFuture = a.services.startFlow(ContractUpgradeFlow(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture mockNet.runNetwork() - val result = resultFuture.get() + val result = resultFuture.getOrThrow() fun check(node: MockNetwork.MockNode) { val nodeStx = node.database.transaction { @@ -124,12 +125,12 @@ class ContractUpgradeFlowTest { .toSignedTransaction() val user = rpcTestUser.copy(permissions = setOf( - startFlowPermission(), + startFlowPermission(), startFlowPermission>() )) val rpcA = startProxy(a, user) val rpcB = startProxy(b, user) - val handle = rpcA.startFlow(::FinalityFlow, stx, setOf(a.info.legalIdentity, b.info.legalIdentity)) + val handle = rpcA.startFlow(::FinalityInvoker, stx, setOf(a.info.legalIdentity, b.info.legalIdentity)) mockNet.runNetwork() handle.returnValue.getOrThrow() @@ -143,7 +144,7 @@ class ContractUpgradeFlowTest { DummyContractV2::class.java).returnValue mockNet.runNetwork() - assertFailsWith(ExecutionException::class) { rejectedFuture.get() } + assertFailsWith(FlowSessionException::class) { rejectedFuture.getOrThrow() } // Party B authorise the contract state upgrade. rpcB.authoriseContractUpgrade(btx!!.tx.outRef(0), DummyContractV2::class.java) @@ -154,7 +155,7 @@ class ContractUpgradeFlowTest { DummyContractV2::class.java).returnValue mockNet.runNetwork() - val result = resultFuture.get() + val result = resultFuture.getOrThrow() // Check results. listOf(a, b).forEach { val signedTX = a.database.transaction { a.services.storageService.validatedTransactions.getTransaction(result.ref.txhash) } @@ -210,4 +211,11 @@ class ContractUpgradeFlowTest { // Dummy Cash contract for testing. override val legalContractReference = SecureHash.sha256("") } + + @StartableByRPC + class FinalityInvoker(val transaction: SignedTransaction, + val extraRecipients: Set) : FlowLogic>() { + @Suspendable + override fun call(): List = subFlow(FinalityFlow(transaction, extraRecipients)) + } } diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 967834c1b1..1a9f333340 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -8,8 +8,11 @@ UNRELEASED ---------- * API changes: - * Initiating flows (i.e. those which initiate flows in a counterparty) are now required to be annotated with - ``InitiatingFlow``. + * ``CordaPluginRegistry.requiredFlows`` is no longer needed. Instead annotate any flows you wish to start via RPC with + ``@StartableByRPC`` and any scheduled flows with ``@SchedulableFlow``. + + * Flows which initiate flows in their counterparties (an example of which is the ``NotaryFlow.Client``) are now + required to be annotated with ``@InitiatingFlow``. * ``PluginServiceHub.registerFlowInitiator`` has been deprecated and replaced by ``registerServiceFlow`` with the marker Class restricted to ``FlowLogic``. In line with the introduction of ``InitiatingFlow``, it throws an diff --git a/docs/source/corda-plugins.rst b/docs/source/corda-plugins.rst index 033b79a771..75a893449a 100644 --- a/docs/source/corda-plugins.rst +++ b/docs/source/corda-plugins.rst @@ -45,19 +45,7 @@ extensions to be created, or registered at startup. In particular: jars. These static serving directories will not be available if the bundled web server is not started. - c. The ``requiredFlows`` property is used to declare new protocols in - the plugin jar. Specifically the property must return a map with a key - naming each exposed top level flow class and a value which is a set - naming every parameter class that will be passed to the flow's - constructor. Standard ``java.lang.*`` and ``kotlin.*`` types do not need - to be included, but all other parameter types, or concrete interface - implementations need declaring. Declaring a specific flow in this map - white lists it for activation by the ``FlowLogicRefFactory``. White - listing is not strictly required for ``subFlows`` used internally, but - is required for any top level flow, or a flow which is invoked through - the scheduler. - - d. The ``servicePlugins`` property returns a list of classes which will + c. The ``servicePlugins`` property returns a list of classes which will be instantiated once during the ``AbstractNode.start`` call. These classes must provide a single argument constructor which will receive a ``PluginServiceHub`` reference. They must also extend the abstract class @@ -90,7 +78,7 @@ extensions to be created, or registered at startup. In particular: functions inside the node, for instance to initiate workflows when certain conditions are met. - e. The ``customizeSerialization`` function allows classes to be whitelisted + d. The ``customizeSerialization`` function allows classes to be whitelisted for object serialisation, over and above those tagged with the ``@CordaSerializable`` annotation. In general the annotation should be preferred. For instance new state types will need to be explicitly registered. This will be called at diff --git a/docs/source/creating-a-cordapp.rst b/docs/source/creating-a-cordapp.rst index dcff06d0de..c4c808aeb5 100644 --- a/docs/source/creating-a-cordapp.rst +++ b/docs/source/creating-a-cordapp.rst @@ -12,11 +12,10 @@ App plugins To create an app plugin you must extend from `CordaPluginRegistry`_. The JavaDoc contains specific details of the implementation, but you can extend the server in the following ways: -1. Required flows: Specify which flows will be whitelisted for use in your RPC calls. -2. Service plugins: Register your services (see below). -3. Web APIs: You may register your own endpoints under /api/ of the bundled web server. -4. Static web endpoints: You may register your own static serving directories for serving web content from the web server. -5. Whitelisting your additional contract, state and other classes for object serialization. Any class that forms part +1. Service plugins: Register your services (see below). +2. Web APIs: You may register your own endpoints under /api/ of the bundled web server. +3. Static web endpoints: You may register your own static serving directories for serving web content from the web server. +4. Whitelisting your additional contract, state and other classes for object serialization. Any class that forms part of a persisted state, that is used in messaging between flows or in RPC needs to be whitelisted. Services diff --git a/docs/source/event-scheduling.rst b/docs/source/event-scheduling.rst index 2eede8bbd9..b5df1bcf1c 100644 --- a/docs/source/event-scheduling.rst +++ b/docs/source/event-scheduling.rst @@ -42,7 +42,8 @@ There are two main steps to implementing scheduled events: ``nextScheduledActivity`` to be implemented which returns an optional ``ScheduledActivity`` instance. ``ScheduledActivity`` captures what ``FlowLogic`` instance each node will run, to perform the activity, and when it will run is described by a ``java.time.Instant``. Once your state implements this interface and is tracked by the - wallet, it can expect to be queried for the next activity when committed to the wallet. + wallet, it can expect to be queried for the next activity when committed to the wallet. The ``FlowLogic`` must be + annotated with ``@SchedulableFlow``. * If nothing suitable exists, implement a ``FlowLogic`` to be executed by each node as the activity itself. The important thing to remember is that in the current implementation, each node that is party to the transaction will execute the same ``FlowLogic``, so it needs to establish roles in the business process based on the contract @@ -90,10 +91,7 @@ business process and to take on those roles. That ``FlowLogic`` will be handed rate swap ``State`` in question, as well as a tolerance ``Duration`` of how long to wait after the activity is triggered for the interest rate before indicating an error. -.. note:: This is a way to create a reference to the FlowLogic class and its constructor parameters to - instantiate. The reference can be checked against a per-node whitelist of approved and allowable types as - part of our overall security sandboxing. - +.. note:: This is a way to create a reference to the FlowLogic class and its constructor parameters to instantiate. As previously mentioned, we currently need a small network handler to assist with session setup until the work to automate that is complete. See the interest rate swap specific implementation ``FixingSessionInitiationHandler`` which diff --git a/docs/source/flow-state-machines.rst b/docs/source/flow-state-machines.rst index abe1afa90d..8bb774eb6b 100644 --- a/docs/source/flow-state-machines.rst +++ b/docs/source/flow-state-machines.rst @@ -206,9 +206,10 @@ how to register handlers with the messaging system (see ":doc:`messaging`") and when messages arrive. It provides the send/receive/sendAndReceive calls that let the code request network interaction and it will save/restore serialised versions of the fiber at the right times. -Flows can be invoked in several ways. For instance, they can be triggered by scheduled events, -see ":doc:`event-scheduling`" to learn more about this. Or they can be triggered directly via the Java-level node RPC -APIs from your app code. +Flows can be invoked in several ways. For instance, they can be triggered by scheduled events (in which case they need to +be annotated with ``@SchedulableFlow``), see ":doc:`event-scheduling`" to learn more about this. They can also be triggered +directly via the node's RPC API from your app code (in which case they need to be annotated with `StartableByRPC`). It's +possible for a flow to be of both types. You request a flow to be invoked by using the ``CordaRPCOps.startFlowDynamic`` method. This takes a Java reflection ``Class`` object that describes the flow class to use (in this case, either ``Buyer`` or ``Seller``). @@ -399,15 +400,35 @@ This code is longer but no more complicated. Here are some things to pay attenti As you can see, the flow logic is straightforward and does not contain any callbacks or network glue code, despite the fact that it takes minimal resources and can survive node restarts. -Initiating communication ------------------------- +Flow sessions +------------- -Now that we have both sides of the deal negotation implemented as flows we need a way to start things off. We do this by -having one side initiate communication and the other respond to it and start their flow. Initiation is typically done using -RPC with the ``startFlowDynamic`` method. The initiating flow has be to annotated with ``InitiatingFlow``. In our example -it doesn't matter which flow is the initiator and which is the initiated, which is why neither ``Buyer`` nor ``Seller`` -are annotated with it. For example, if we choose the seller side as the initiator then we need a seller starter flow that -might look something like this: +Before going any further it will be useful to describe how flows communicate with each other. A node may have many flows +running at the same time, and perhaps communicating with the same counterparty node but for different purposes. Therefore +flows need a way to segregate communication channels so that concurrent conversations between flows on the same set of nodes +do not interfere with each other. + +To achieve this the flow framework initiates a new flow session each time a flow starts communicating with a ``Party`` +for the first time. A session is simply a pair of IDs, one for each side, to allow the node to route received messages to +the correct flow. If the other side accepts the session request then subsequent sends and receives to that same ``Party`` +will use the same session. A session ends when either flow ends, whether as expected or pre-maturely. If a flow ends +pre-maturely then the other side will be notified of that and they will also end, as the whole point of flows is a known +sequence of message transfers. Flows end pre-maturely due to exceptions, and as described above, if that exception is +``FlowException`` or a sub-type then it will propagate to the other side. Any other exception will not propagate. + +Taking a step back, we mentioned that the other side has to accept the session request for there to be a communication +channel. A node accepts a session request if it has registered the flow type (the fully-qualified class name) that is +making the request - each session initiation includes the initiating flow type. The registration is done by a CorDapp +which has made available the particular flow communication, using ``PluginServiceHub.registerServiceFlow``. This method +specifies a flow factory for generating the counter-flow to any given initiating flow. If this registration doesn't exist +then no further communication takes place and the initiating flow ends with an exception. The initiating flow has to be +annotated with ``InitiatingFlow``. + +Going back to our buyer and seller flows, we need a way to initiate communication between the two. This is typically done +with one side started manually using the ``startFlowDynamic`` RPC and this initiates the counter-flow on the other side. +In this case it doesn't matter which flow is the initiator and which is the initiated, which is why neither ``Buyer`` nor +``Seller`` are annotated with ``InitiatingFlow``. For example, if we choose the seller side as the initiator then we need +to create a simple seller starter flow that has the annotation we need: .. container:: codeset diff --git a/docs/source/tutorial-clientrpc-api.rst b/docs/source/tutorial-clientrpc-api.rst index 69b297c68c..32fb39c465 100644 --- a/docs/source/tutorial-clientrpc-api.rst +++ b/docs/source/tutorial-clientrpc-api.rst @@ -3,13 +3,13 @@ Client RPC API tutorial ======================= -In this tutorial we will build a simple command line utility that -connects to a node, creates some Cash transactions and meanwhile dumps -the transaction graph to the standard output. We will then put some -simple visualisation on top. For an explanation on how the RPC works -see :doc:`clientrpc`. +In this tutorial we will build a simple command line utility that connects to a node, creates some Cash transactions and +meanwhile dumps the transaction graph to the standard output. We will then put some simple visualisation on top. For an +explanation on how the RPC works see :doc:`clientrpc`. -We start off by connecting to the node itself. For the purposes of the tutorial we will use the Driver to start up a notary and a node that issues/exits and moves Cash around for herself. To authenticate we will use the certificates of the nodes directly. +We start off by connecting to the node itself. For the purposes of the tutorial we will use the Driver to start up a notary +and a node that issues/exits and moves Cash around for herself. To authenticate we will use the certificates of the nodes +directly. Note how we configure the node to create a user that has permission to start the CashFlow. @@ -25,14 +25,16 @@ Now we can connect to the node itself using a valid RPC login. We login using th :start-after: START 2 :end-before: END 2 -We start generating transactions in a different thread (``generateTransactions`` to be defined later) using ``proxy``, which exposes the full RPC interface of the node: +We start generating transactions in a different thread (``generateTransactions`` to be defined later) using ``proxy``, +which exposes the full RPC interface of the node: .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt :language: kotlin :start-after: interface CordaRPCOps :end-before: } -.. warning:: This API is evolving and will continue to grow as new functionality and features added to Corda are made available to RPC clients. +.. warning:: This API is evolving and will continue to grow as new functionality and features added to Corda are made + available to RPC clients. The one we need in order to dump the transaction graph is ``verifiedTransactions``. The type signature tells us that the RPC will return a list of transactions and an Observable stream. This is a general pattern, we query some data and the @@ -61,13 +63,19 @@ Now we just need to create the transactions themselves! :start-after: START 6 :end-before: END 6 -We utilise several RPC functions here to query things like the notaries in the node cluster or our own vault. These RPC functions also return ``Observable`` objects so that the node can send us updated values. However, we don't need updates here and so we mark these observables as ``notUsed``. (As a rule, you should always either subscribe to an ``Observable`` or mark it as not used. Failing to do this will leak resources in the node.) +We utilise several RPC functions here to query things like the notaries in the node cluster or our own vault. These RPC +functions also return ``Observable`` objects so that the node can send us updated values. However, we don't need updates +here and so we mark these observables as ``notUsed``. (As a rule, you should always either subscribe to an ``Observable`` +or mark it as not used. Failing to do this will leak resources in the node.) Then in a loop we generate randomly either an Issue, a Pay or an Exit transaction. -The RPC we need to initiate a Cash transaction is ``startFlowDynamic`` which may start an arbitrary flow, given sufficient permissions to do so. We won't use this function directly, but rather a type-safe wrapper around it ``startFlow`` that type-checks the arguments for us. +The RPC we need to initiate a Cash transaction is ``startFlowDynamic`` which may start an arbitrary flow, given sufficient +permissions to do so. We won't use this function directly, but rather a type-safe wrapper around it ``startFlow`` that +type-checks the arguments for us. -Finally we have everything in place: we start a couple of nodes, connect to them, and start creating transactions while listening on successfully created ones, which are dumped to the console. We just need to run it!: +Finally we have everything in place: we start a couple of nodes, connect to them, and start creating transactions while +listening on successfully created ones, which are dumped to the console. We just need to run it!: .. code-block:: text @@ -106,9 +114,10 @@ RPC credentials associated with a Client must match the permission set configure This refers to both authentication (username and password) and role-based authorisation (a permissioned set of RPC operations an authenticated user is entitled to run). -.. note:: Permissions are represented as *String's* to allow RPC implementations to add their own permissioning. - Currently the only permission type defined is *StartFlow*, which defines a list of whitelisted flows an authenticated use may execute. - An administrator user (or a developer) may also be assigned the ``ALL`` permission, which grants access to any flow. +.. note:: Permissions are represented as *String's* to allow RPC implementations to add their own permissioning. Currently + the only permission type defined is *StartFlow*, which defines a list of whitelisted flows an authenticated use may + execute. An administrator user (or a developer) may also be assigned the ``ALL`` permission, which grants access to + any flow. In the instructions above the server node permissions are configured programmatically in the driver code: @@ -126,7 +135,8 @@ When starting a standalone node using a configuration file we must supply the RP { username=user, password=password, permissions=[ StartFlow.net.corda.flows.CashFlow ] } ] -When using the gradle Cordformation plugin to configure and deploy a node you must supply the RPC credentials in a similar manner: +When using the gradle Cordformation plugin to configure and deploy a node you must supply the RPC credentials in a similar +manner: .. code-block:: text @@ -148,5 +158,8 @@ You can then deploy and launch the nodes (Notary and Alice) as follows: ./docs/source/example-code/build/install/docs/source/example-code/bin/client-rpc-tutorial Print ./docs/source/example-code/build/install/docs/source/example-code/bin/client-rpc-tutorial Visualise +With regards to the start flow RPCs, there is an extra layer of security whereby the flow to be executed has to be +annotated with ``@StartableByRPC``. Flows without this annotation cannot execute using RPC. + See more on security in :doc:`secure-coding-guidelines`, node configuration in :doc:`corda-configuration-file` and Cordformation in :doc:`creating-a-cordapp` diff --git a/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt index c44df1e20a..a90667ff71 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt @@ -6,6 +6,7 @@ import net.corda.core.contracts.Amount import net.corda.core.contracts.TransactionType import net.corda.core.contracts.issuedBy import net.corda.core.identity.Party +import net.corda.core.flows.StartableByRPC import net.corda.core.serialization.OpaqueBytes import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder @@ -20,6 +21,7 @@ import java.util.* * @param recipient the party who should own the currency after it is issued. * @param notary the notary to set on the output states. */ +@StartableByRPC class CashIssueFlow(val amount: Amount, val issueRef: OpaqueBytes, val recipient: Party, diff --git a/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt index ec212257ea..ff6fe84355 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt @@ -6,6 +6,7 @@ import net.corda.core.contracts.InsufficientBalanceException import net.corda.core.contracts.TransactionType import net.corda.core.crypto.expandedCompositeKeys import net.corda.core.crypto.toStringShort +import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder @@ -20,6 +21,7 @@ import java.util.* * @param recipient the party to pay the currency to. * @param issuerConstraint if specified, the payment will be made using only cash issued by the given parties. */ +@StartableByRPC open class CashPaymentFlow( val amount: Amount, val recipient: Party, diff --git a/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt b/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt index 3360bb5630..ab6b4a8883 100644 --- a/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt @@ -6,6 +6,7 @@ import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.Party +import net.corda.core.flows.StartableByRPC import net.corda.core.node.PluginServiceHub import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.OpaqueBytes @@ -30,6 +31,7 @@ object IssuerFlow { * Returns the transaction created by the Issuer to move the cash to the Requester. */ @InitiatingFlow + @StartableByRPC class IssuanceRequester(val amount: Amount, val issueToParty: Party, val issueToPartyRef: OpaqueBytes, val issuerBankParty: Party) : FlowLogic() { @Suspendable diff --git a/node/build.gradle b/node/build.gradle index 7520a85a70..f6ca57406d 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -150,6 +150,9 @@ dependencies { // Requery: object mapper for Kotlin compile "io.requery:requery-kotlin:$requery_version" + // FastClasspathScanner: classpath scanning + compile 'io.github.lukehutch:fast-classpath-scanner:2.0.19' + // Integration test helpers integrationTestCompile "junit:junit:$junit_version" } diff --git a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt index 6f67459650..0884300518 100644 --- a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt @@ -1,10 +1,11 @@ package net.corda.node +import co.paralleluniverse.fibers.Suspendable import net.corda.core.div import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC import net.corda.core.getOrThrow import net.corda.core.messaging.startFlow -import net.corda.core.node.CordaPluginRegistry import net.corda.core.utilities.ALICE import net.corda.node.driver.driver import net.corda.node.services.startFlowPermission @@ -48,18 +49,12 @@ class BootTests { } } +@StartableByRPC class ObjectInputStreamFlow : FlowLogic() { - + @Suspendable override fun call() { System.clearProperty("jdk.serialFilter") // This checks that the node has already consumed the property. val data = ByteArrayOutputStream().apply { ObjectOutputStream(this).use { it.writeObject(object : Serializable {}) } }.toByteArray() ObjectInputStream(data.inputStream()).use { it.readObject() } } - -} - -class BootTestsPlugin : CordaPluginRegistry() { - - override val requiredFlows: Map> = mapOf(ObjectInputStreamFlow::class.java.name to setOf()) - } diff --git a/node/src/integration-test/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry b/node/src/integration-test/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry deleted file mode 100644 index 4734e517b7..0000000000 --- a/node/src/integration-test/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry +++ /dev/null @@ -1 +0,0 @@ -net.corda.node.BootTestsPlugin diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index b6a12c91b4..fad9f93ec2 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -5,15 +5,16 @@ import com.google.common.annotations.VisibleForTesting import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.MoreExecutors import com.google.common.util.concurrent.SettableFuture +import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner +import io.github.lukehutch.fastclasspathscanner.scanner.ClassInfo import net.corda.core.* -import net.corda.core.contracts.Amount -import net.corda.core.contracts.PartyAndReference import net.corda.core.crypto.KeyStoreUtilities import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.replaceCommonName import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.RPCOps @@ -21,7 +22,6 @@ import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.* import net.corda.core.node.services.* import net.corda.core.node.services.NetworkMapCache.MapChange -import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize @@ -47,7 +47,6 @@ import net.corda.node.services.network.PersistentNetworkMapService import net.corda.node.services.persistence.* import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService -import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.statemachine.flowVersion @@ -64,8 +63,11 @@ import org.bouncycastle.asn1.x500.X500Name import org.jetbrains.exposed.sql.Database import org.slf4j.Logger import java.io.IOException +import java.lang.reflect.Modifier.* +import java.net.URL import java.nio.file.FileAlreadyExistsException import java.nio.file.Path +import java.nio.file.Paths import java.security.KeyPair import java.security.KeyStoreException import java.time.Clock @@ -73,6 +75,7 @@ import java.util.* import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ExecutorService import java.util.concurrent.TimeUnit.SECONDS +import kotlin.collections.ArrayList import kotlin.reflect.KClass import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair @@ -93,14 +96,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, companion object { val PRIVATE_KEY_FILE_NAME = "identity-private-key" val PUBLIC_IDENTITY_FILE_NAME = "identity-public" - - val defaultFlowWhiteList: Map>, Set>> = mapOf( - CashExitFlow::class.java to setOf(Amount::class.java, PartyAndReference::class.java), - CashIssueFlow::class.java to setOf(Amount::class.java, OpaqueBytes::class.java, Party::class.java), - CashPaymentFlow::class.java to setOf(Amount::class.java, Party::class.java), - FinalityFlow::class.java to setOf(LinkedHashSet::class.java), - ContractUpgradeFlow::class.java to emptySet() - ) } // TODO: Persist this, as well as whether the node is registered. @@ -133,10 +128,10 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, override val schemaService: SchemaService get() = schemas override val transactionVerifierService: TransactionVerifierService get() = txVerifierService override val auditService: AuditService get() = auditService + override val rpcFlows: List>> get() = this@AbstractNode.rpcFlows // Internal only override val monitoringService: MonitoringService = MonitoringService(MetricRegistry()) - override val flowLogicRefFactory: FlowLogicRefFactoryInternal get() = flowLogicFactory override fun startFlow(logic: FlowLogic, flowInitiator: FlowInitiator): FlowStateMachineImpl { return serverThread.fetchFrom { smm.add(logic, flowInitiator) } @@ -176,13 +171,13 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, lateinit var net: MessagingService lateinit var netMapCache: NetworkMapCacheInternal lateinit var scheduler: NodeSchedulerService - lateinit var flowLogicFactory: FlowLogicRefFactoryInternal lateinit var schemas: SchemaService lateinit var auditService: AuditService val customServices: ArrayList = ArrayList() protected val runOnStop: ArrayList = ArrayList() lateinit var database: Database protected var dbCloser: Runnable? = null + private lateinit var rpcFlows: List>> /** Locates and returns a service of the given type if loaded, or throws an exception if not found. */ inline fun findService() = customServices.filterIsInstance().single() @@ -250,6 +245,16 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } startMessagingService(rpcOps) installCoreFlows() + + fun Class>.isUserInvokable(): Boolean { + return isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || isStatic(modifiers)) + } + + val flows = scanForFlows() + rpcFlows = flows.filter { it.isUserInvokable() && it.isAnnotationPresent(StartableByRPC::class.java) } + + // Add any core flows here + listOf(ContractUpgradeFlow::class.java) + runOnStop += Runnable { net.stop() } _networkMapRegistrationFuture.setFuture(registerWithNetworkMapIfConfigured()) smm.start() @@ -305,8 +310,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, // the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with // the identity key. But the infrastructure to make that easy isn't here yet. keyManagement = makeKeyManagementService() - flowLogicFactory = initialiseFlowLogicFactory() - scheduler = NodeSchedulerService(services, database, flowLogicFactory, unfinishedSchedules = busyNodeLatch) + scheduler = NodeSchedulerService(services, database, unfinishedSchedules = busyNodeLatch) val tokenizableServices = mutableListOf(storage, net, vault, keyManagement, identity, platformClock, scheduler) makeAdvertisedServices(tokenizableServices) @@ -318,6 +322,53 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, return tokenizableServices } + private fun scanForFlows(): List>> { + val pluginsDir = configuration.baseDirectory / "plugins" + log.info("Scanning plugins in $pluginsDir ...") + if (!pluginsDir.exists()) return emptyList() + + val pluginJars = pluginsDir.list { + it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.toArray() + } + + val scanResult = FastClasspathScanner().overrideClasspath(*pluginJars).scan() // This will only scan the plugin jars and nothing else + + fun loadFlowClass(className: String): Class>? { + return try { + // TODO Make sure this is loaded by the correct class loader + @Suppress("UNCHECKED_CAST") + Class.forName(className, false, javaClass.classLoader) as Class> + } catch (e: Exception) { + log.warn("Unable to load flow class $className", e) + null + } + } + + val flowClasses = scanResult.getNamesOfSubclassesOf(FlowLogic::class.java) + .mapNotNull { loadFlowClass(it) } + .filterNot { isAbstract(it.modifiers) } + + fun URL.pluginName(): String { + return try { + Paths.get(toURI()).fileName.toString() + } catch (e: Exception) { + toString() + } + } + + val classpathURLsField = ClassInfo::class.java.getDeclaredField("classpathElementURLs").apply { isAccessible = true } + + flowClasses.groupBy { + val classInfo = scanResult.classNameToClassInfo[it.name] + @Suppress("UNCHECKED_CAST") + (classpathURLsField.get(classInfo) as Set).first() + }.forEach { url, classes -> + log.info("Found flows in plugin ${url.pluginName()}: ${classes.joinToString { it.name }}") + } + + return flowClasses + } + private fun initUploaders(storageServices: Pair) { val uploaders: List = listOf(storageServices.first.attachments as NodeAttachmentService) + customServices.filterIsInstance(AcceptsFileUpload::class.java) @@ -386,26 +437,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } } - private fun initialiseFlowLogicFactory(): FlowLogicRefFactoryInternal { - val flowWhitelist = HashMap>() - - for ((flowClass, extraArgumentTypes) in defaultFlowWhiteList) { - val argumentWhitelistClassNames = HashSet(extraArgumentTypes.map { it.name }) - flowClass.constructors.forEach { - it.parameters.mapTo(argumentWhitelistClassNames) { it.type.name } - } - flowWhitelist.merge(flowClass.name, argumentWhitelistClassNames, { x, y -> x + y }) - } - - for (plugin in pluginRegistries) { - for ((className, classWhitelist) in plugin.requiredFlows) { - flowWhitelist.merge(className, classWhitelist, { x, y -> x + y }) - } - } - - return FlowLogicRefFactoryImpl(flowWhitelist) - } - private fun makePluginServices(tokenizableServices: MutableList): List { val pluginServices = pluginRegistries.flatMap { it.servicePlugins }.map { it.apply(services) } tokenizableServices.addAll(pluginServices) diff --git a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt index 8f16f60028..acdc90af16 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -7,6 +7,7 @@ import net.corda.core.contracts.UpgradedContract import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC import net.corda.core.messaging.* import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache @@ -20,6 +21,7 @@ import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.messaging.getRpcContext import net.corda.node.services.messaging.requirePermission import net.corda.node.services.startFlowPermission +import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.utilities.transaction import org.bouncycastle.asn1.x500.X500Name @@ -39,8 +41,6 @@ class CordaRPCOpsImpl( private val smm: StateMachineManager, private val database: Database ) : CordaRPCOps { - override val protocolVersion: Int = 0 - override fun networkMapUpdates(): Pair, Observable> { return database.transaction { services.networkMapCache.track() @@ -115,12 +115,8 @@ class CordaRPCOpsImpl( } } - // TODO: Check that this flow is annotated as being intended for RPC invocation override fun startTrackedFlowDynamic(logicType: Class>, vararg args: Any?): FlowProgressHandle { - val rpcContext = getRpcContext() - rpcContext.requirePermission(startFlowPermission(logicType)) - val currentUser = FlowInitiator.RPC(rpcContext.currentUser.username) - val stateMachine = services.invokeFlowAsync(logicType, currentUser, *args) + val stateMachine = startFlow(logicType, args) return FlowProgressHandleImpl( id = stateMachine.id, returnValue = stateMachine.resultFuture, @@ -128,13 +124,17 @@ class CordaRPCOpsImpl( ) } - // TODO: Check that this flow is annotated as being intended for RPC invocation override fun startFlowDynamic(logicType: Class>, vararg args: Any?): FlowHandle { + val stateMachine = startFlow(logicType, args) + return FlowHandleImpl(id = stateMachine.id, returnValue = stateMachine.resultFuture) + } + + private fun startFlow(logicType: Class>, args: Array): FlowStateMachineImpl { + require(logicType.isAnnotationPresent(StartableByRPC::class.java)) { "${logicType.name} was not designed for RPC" } val rpcContext = getRpcContext() rpcContext.requirePermission(startFlowPermission(logicType)) val currentUser = FlowInitiator.RPC(rpcContext.currentUser.username) - val stateMachine = services.invokeFlowAsync(logicType, currentUser, *args) - return FlowHandleImpl(id = stateMachine.id, returnValue = stateMachine.resultFuture) + return services.invokeFlowAsync(logicType, currentUser, *args) } override fun attachmentExists(id: SecureHash): Boolean { @@ -171,11 +171,12 @@ class CordaRPCOpsImpl( override fun waitUntilRegisteredWithNetworkMap() = services.networkMapCache.mapServiceRegistered override fun partyFromKey(key: PublicKey) = services.identityService.partyFromKey(key) + @Suppress("DEPRECATION") @Deprecated("Use partyFromX500Name instead") override fun partyFromName(name: String) = services.identityService.partyFromName(name) override fun partyFromX500Name(x500Name: X500Name)= services.identityService.partyFromX500Name(x500Name) - override fun registeredFlows(): List = services.flowLogicRefFactory.flowWhitelist.keys.sorted() + override fun registeredFlows(): List = services.rpcFlows.map { it.name }.sorted() companion object { private fun stateMachineInfoFromFlowLogic(flowLogic: FlowLogic<*>): StateMachineInfo { diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index a67796711a..e1af4b173a 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -2,7 +2,9 @@ package net.corda.node.services.api import com.google.common.annotations.VisibleForTesting import com.google.common.util.concurrent.ListenableFuture -import net.corda.core.flows.* +import net.corda.core.flows.FlowInitiator +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowStateMachine import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.NodeInfo import net.corda.core.node.PluginServiceHub @@ -13,6 +15,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.loggerFor import net.corda.node.internal.ServiceFlowInfo import net.corda.node.services.messaging.MessagingService +import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.services.statemachine.FlowStateMachineImpl interface NetworkMapCacheInternal : NetworkMapCache { @@ -47,11 +50,6 @@ interface NetworkMapCacheInternal : NetworkMapCache { } -interface FlowLogicRefFactoryInternal : FlowLogicRefFactory { - val flowWhitelist: Map> - fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*> -} - @CordaSerializable sealed class NetworkCacheError : Exception() { /** Indicates a failure to deregister, because of a rejected request from the remote node */ @@ -64,12 +62,11 @@ abstract class ServiceHubInternal : PluginServiceHub { } abstract val monitoringService: MonitoringService - abstract val flowLogicRefFactory: FlowLogicRefFactoryInternal abstract val schemaService: SchemaService abstract override val networkMapCache: NetworkMapCacheInternal abstract val schedulerService: SchedulerService abstract val auditService: AuditService - + abstract val rpcFlows: List>> abstract val networkService: MessagingService /** @@ -119,9 +116,9 @@ abstract class ServiceHubInternal : PluginServiceHub { logicType: Class>, flowInitiator: FlowInitiator, vararg args: Any?): FlowStateMachineImpl { - val logicRef = flowLogicRefFactory.create(logicType, *args) + val logicRef = FlowLogicRefFactoryImpl.createForRPC(logicType, *args) @Suppress("UNCHECKED_CAST") - val logic = flowLogicRefFactory.toFlowLogic(logicRef) as FlowLogic + val logic = FlowLogicRefFactoryImpl.toFlowLogic(logicRef) as FlowLogic return startFlow(logic, flowInitiator) } diff --git a/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt b/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt index 88d8c3032d..6faa38d735 100644 --- a/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt +++ b/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt @@ -12,9 +12,9 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.then import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace -import net.corda.node.services.api.FlowLogicRefFactoryInternal import net.corda.node.services.api.SchedulerService import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.utilities.* import org.apache.activemq.artemis.utils.ReusableLatch import org.jetbrains.exposed.sql.Database @@ -38,14 +38,12 @@ import javax.annotation.concurrent.ThreadSafe * but that starts to sound a lot like off-ledger state. * * @param services Core node services. - * @param flowLogicRefFactory Factory for restoring [FlowLogic] instances from references. * @param schedulerTimerExecutor The executor the scheduler blocks on waiting for the clock to advance to the next * activity. Only replace this for unit testing purposes. This is not the executor the [FlowLogic] is launched on. */ @ThreadSafe class NodeSchedulerService(private val services: ServiceHubInternal, private val database: Database, - private val flowLogicRefFactory: FlowLogicRefFactoryInternal, private val schedulerTimerExecutor: Executor = Executors.newSingleThreadExecutor(), private val unfinishedSchedules: ReusableLatch = ReusableLatch()) : SchedulerService, SingletonSerializeAsToken() { @@ -192,7 +190,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal, ScheduledStateRef(scheduledState.ref, scheduledActivity.scheduledAt) } else { // TODO: FlowLogicRefFactory needs to sort out the class loader etc - val flowLogic = flowLogicRefFactory.toFlowLogic(scheduledActivity.logicRef) + val flowLogic = FlowLogicRefFactoryImpl.toFlowLogic(scheduledActivity.logicRef) log.trace { "Scheduler starting FlowLogic $flowLogic" } scheduledFlow = flowLogic null @@ -213,7 +211,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal, val state = txState.data as SchedulableState return try { // This can throw as running contract code. - state.nextScheduledActivity(scheduledState.ref, flowLogicRefFactory) + state.nextScheduledActivity(scheduledState.ref, FlowLogicRefFactoryImpl) } catch (e: Exception) { log.error("Attempt to run scheduled state $scheduledState resulted in error.", e) null diff --git a/node/src/main/kotlin/net/corda/node/services/events/ScheduledActivityObserver.kt b/node/src/main/kotlin/net/corda/node/services/events/ScheduledActivityObserver.kt index 8b8f7beec0..3509fa2d34 100644 --- a/node/src/main/kotlin/net/corda/node/services/events/ScheduledActivityObserver.kt +++ b/node/src/main/kotlin/net/corda/node/services/events/ScheduledActivityObserver.kt @@ -4,8 +4,8 @@ import net.corda.core.contracts.ContractState import net.corda.core.contracts.SchedulableState import net.corda.core.contracts.ScheduledStateRef import net.corda.core.contracts.StateAndRef -import net.corda.core.flows.FlowLogicRefFactory import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl /** * This observes the vault and schedules and unschedules activities appropriately based on state production and @@ -13,16 +13,16 @@ import net.corda.node.services.api.ServiceHubInternal */ class ScheduledActivityObserver(val services: ServiceHubInternal) { init { - services.vaultService.rawUpdates.subscribe { update -> - update.consumed.forEach { services.schedulerService.unscheduleStateActivity(it.ref) } - update.produced.forEach { scheduleStateActivity(it, services.flowLogicRefFactory) } + services.vaultService.rawUpdates.subscribe { (consumed, produced) -> + consumed.forEach { services.schedulerService.unscheduleStateActivity(it.ref) } + produced.forEach { scheduleStateActivity(it) } } } - private fun scheduleStateActivity(produced: StateAndRef, flowLogicRefFactory: FlowLogicRefFactory) { + private fun scheduleStateActivity(produced: StateAndRef) { val producedState = produced.state.data if (producedState is SchedulableState) { - val scheduledAt = sandbox { producedState.nextScheduledActivity(produced.ref, flowLogicRefFactory)?.scheduledAt } ?: return + val scheduledAt = sandbox { producedState.nextScheduledActivity(produced.ref, FlowLogicRefFactoryImpl)?.scheduledAt } ?: return services.schedulerService.scheduleStateActivity(ScheduledStateRef(produced.ref, scheduledAt)) } } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt index fd2a9b3e89..150143cc34 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt @@ -1,14 +1,10 @@ package net.corda.node.services.statemachine +import com.google.common.annotations.VisibleForTesting import com.google.common.primitives.Primitives -import net.corda.core.crypto.SecureHash -import net.corda.core.flows.AppContext -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowLogicRef -import net.corda.core.flows.IllegalFlowLogicException +import net.corda.core.flows.* import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.node.services.api.FlowLogicRefFactoryInternal import java.lang.reflect.ParameterizedType import java.lang.reflect.Type import java.util.* @@ -29,59 +25,26 @@ data class FlowLogicRefImpl internal constructor(val flowLogicClassName: String, * Validation of types is performed on the way in and way out in case this object is passed between JVMs which might have differing * whitelists. * - * TODO: Ways to populate whitelist of "blessed" flows per node/party - * TODO: Ways to populate argument types whitelist. Per node/party or global? * TODO: Align with API related logic for passing in FlowLogic references (FlowRef) * TODO: Actual support for AppContext / AttachmentsClassLoader + * TODO: at some point check whether there is permission, beyond the annotations, to start flows. For example, as a security + * measure we might want the ability for the node admin to blacklist a flow such that it moves immediately to the "Flow Hospital" + * in response to a potential malicious use or buggy update to an app etc. */ -class FlowLogicRefFactoryImpl(override val flowWhitelist: Map>) : SingletonSerializeAsToken(), FlowLogicRefFactoryInternal { - constructor() : this(mapOf()) - - // Pending real dependence on AppContext for class loading etc - @Suppress("UNUSED_PARAMETER") - private fun validateFlowClassName(className: String, appContext: AppContext) { - // TODO: make this specific to the attachments in the [AppContext] by including [SecureHash] in whitelist check - require(flowWhitelist.containsKey(className)) { "${FlowLogic::class.java.simpleName} of ${FlowLogicRef::class.java.simpleName} must have type on the whitelist: $className" } - } - - // Pending real dependence on AppContext for class loading etc - @Suppress("UNUSED_PARAMETER") - private fun validateArgClassName(className: String, argClassName: String, appContext: AppContext) { - // TODO: consider more carefully what to whitelist and how to secure flows - // For now automatically accept standard java.lang.* and kotlin.* types. - // All other types require manual specification at FlowLogicRefFactory construction time. - if (argClassName.startsWith("java.lang.") || argClassName.startsWith("kotlin.")) { - return +object FlowLogicRefFactoryImpl : SingletonSerializeAsToken(), FlowLogicRefFactory { + override fun create(flowClass: Class>, vararg args: Any?): FlowLogicRef { + if (!flowClass.isAnnotationPresent(SchedulableFlow::class.java)) { + throw IllegalFlowLogicException(flowClass, "because it's not a schedulable flow") } - // TODO: make this specific to the attachments in the [AppContext] by including [SecureHash] in whitelist check - require(flowWhitelist[className]!!.contains(argClassName)) { "Args to $className must have types on the args whitelist: $argClassName, but it has ${flowWhitelist[className]}" } + return createForRPC(flowClass, *args) } - /** - * Create a [FlowLogicRef] for the Kotlin primary constructor of a named [FlowLogic] - */ - fun createKotlin(flowLogicClassName: String, args: Map, attachments: List = emptyList()): FlowLogicRef { - val context = AppContext(attachments) - validateFlowClassName(flowLogicClassName, context) - for (arg in args.values.filterNotNull()) { - validateArgClassName(flowLogicClassName, arg.javaClass.name, context) - } - val clazz = Class.forName(flowLogicClassName) - require(FlowLogic::class.java.isAssignableFrom(clazz)) { "$flowLogicClassName is not a FlowLogic" } - @Suppress("UNCHECKED_CAST") - val logic = clazz as Class>> - return createKotlin(logic, args) - } - - /** - * Create a [FlowLogicRef] by assuming a single constructor and the given args. - */ - override fun create(type: Class>, vararg args: Any?): FlowLogicRef { + fun createForRPC(flowClass: Class>, vararg args: Any?): FlowLogicRef { // TODO: This is used via RPC but it's probably better if we pass in argument names and values explicitly // to avoid requiring only a single constructor. val argTypes = args.map { it?.javaClass } val constructor = try { - type.kotlin.constructors.single { ctor -> + flowClass.kotlin.constructors.single { ctor -> // Get the types of the arguments, always boxed (as that's what we get in the invocation). val ctorTypes = ctor.javaConstructor!!.parameterTypes.map { Primitives.wrap(it) } if (argTypes.size != ctorTypes.size) @@ -93,14 +56,14 @@ class FlowLogicRefFactoryImpl(override val flowWhitelist: Map>, args: Map): FlowLogicRef { + @VisibleForTesting + internal fun createKotlin(type: Class>, args: Map): FlowLogicRef { // TODO: we need to capture something about the class loader or "application context" into the ref, // perhaps as some sort of ThreadLocal style object. For now, just create an empty one. val appContext = AppContext(emptyList()) - validateFlowClassName(type.name, appContext) // Check we can find a constructor and populate the args to it, but don't call it - createConstructor(appContext, type, args) + createConstructor(type, args) return FlowLogicRefImpl(type.name, appContext, args) } - /** - * Create a [FlowLogicRef] by trying to find a Java constructor that matches the given args. - */ - private fun createJava(type: Class>, vararg args: Any?): FlowLogicRef { - // Build map for each - val argsMap = HashMap(args.size) - var index = 0 - args.forEach { argsMap["arg${index++}"] = it } - return createKotlin(type, argsMap) - } - - override fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*> { + fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*> { if (ref !is FlowLogicRefImpl) throw IllegalFlowLogicException(ref.javaClass, "FlowLogicRef was not created via correct FlowLogicRefFactory interface") - validateFlowClassName(ref.flowLogicClassName, ref.appContext) val klass = Class.forName(ref.flowLogicClassName, true, ref.appContext.classLoader).asSubclass(FlowLogic::class.java) - return createConstructor(ref.appContext, klass, ref.args)() + return createConstructor(klass, ref.args)() } - private fun createConstructor(appContext: AppContext, clazz: Class>, args: Map): () -> FlowLogic<*> { + private fun createConstructor(clazz: Class>, args: Map): () -> FlowLogic<*> { for (constructor in clazz.kotlin.constructors) { - val params = buildParams(appContext, clazz, constructor, args) ?: continue + val params = buildParams(constructor, args) ?: continue // If we get here then we matched every parameter return { constructor.callBy(params) } } throw IllegalFlowLogicException(clazz, "as could not find matching constructor for: $args") } - private fun buildParams(appContext: AppContext, clazz: Class>, constructor: KFunction>, args: Map): HashMap? { + private fun buildParams(constructor: KFunction>, args: Map): HashMap? { val params = hashMapOf() val usedKeys = hashSetOf() for (parameter in constructor.parameters) { @@ -159,7 +110,6 @@ class FlowLogicRefFactoryImpl(override val flowWhitelist: Map output.println("${i + 1}. $s", Color.yellow) } return } - val match = matches.single() - val clazz = Class.forName(match) - if (!FlowLogic::class.java.isAssignableFrom(clazz)) - throw IllegalStateException("Found a non-FlowLogic class in the whitelist? $clazz") + + @Suppress("UNCHECKED_CAST") + val clazz = matches.single() as Class> try { // TODO Flow invocation should use startFlowDynamic. - @Suppress("UNCHECKED_CAST") - val fsm = runFlowFromString({ node.services.startFlow(it, FlowInitiator.Shell) }, inputData, clazz as Class>) + val fsm = runFlowFromString({ node.services.startFlow(it, FlowInitiator.Shell) }, inputData, clazz) // Show the progress tracker on the console until the flow completes or is interrupted with a // Ctrl-C keypress. val latch = CountDownLatch(1) @@ -275,10 +273,7 @@ object InteractiveShell { var paramNamesFromConstructor: List? = null fun getPrototype(ctor: Constructor<*>): List { val argTypes = ctor.parameterTypes.map { it.simpleName } - val prototype = paramNamesFromConstructor!!.zip(argTypes).map { pair -> - val (name, type) = pair - "$name: $type" - } + val prototype = paramNamesFromConstructor!!.zip(argTypes).map { (name, type) -> "$name: $type" } return prototype } try { diff --git a/node/src/test/java/net/corda/node/services/events/FlowLogicRefFromJavaTest.java b/node/src/test/java/net/corda/node/services/events/FlowLogicRefFromJavaTest.java index 20eb7fa812..5e0a69cbad 100644 --- a/node/src/test/java/net/corda/node/services/events/FlowLogicRefFromJavaTest.java +++ b/node/src/test/java/net/corda/node/services/events/FlowLogicRefFromJavaTest.java @@ -1,15 +1,9 @@ package net.corda.node.services.events; import net.corda.core.flows.FlowLogic; -import net.corda.core.flows.FlowLogicRefFactory; import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl; import org.junit.Test; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - public class FlowLogicRefFromJavaTest { @SuppressWarnings("unused") @@ -56,20 +50,11 @@ public class FlowLogicRefFromJavaTest { @Test public void test() { - Map> whiteList = new HashMap<>(); - Set argsList = new HashSet<>(); - argsList.add(ParamType1.class.getName()); - argsList.add(ParamType2.class.getName()); - whiteList.put(JavaFlowLogic.class.getName(), argsList); - FlowLogicRefFactory factory = new FlowLogicRefFactoryImpl(whiteList); - factory.create(JavaFlowLogic.class, new ParamType1(1), new ParamType2("Hello Jack")); + FlowLogicRefFactoryImpl.INSTANCE.createForRPC(JavaFlowLogic.class, new ParamType1(1), new ParamType2("Hello Jack")); } @Test public void testNoArg() { - Map> whiteList = new HashMap<>(); - whiteList.put(JavaNoArgFlowLogic.class.getName(), new HashSet<>()); - FlowLogicRefFactory factory = new FlowLogicRefFactoryImpl(whiteList); - factory.create(JavaNoArgFlowLogic.class); + FlowLogicRefFactoryImpl.INSTANCE.createForRPC(JavaNoArgFlowLogic.class); } } diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index 36ed83a2ab..c5c8c16129 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -1,9 +1,11 @@ package net.corda.node +import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.Cash import net.corda.core.contracts.* import net.corda.core.crypto.isFulfilledBy import net.corda.core.crypto.keys +import net.corda.core.flows.FlowLogic import net.corda.core.flows.StateMachineRunId import net.corda.core.messaging.StateMachineUpdate import net.corda.core.messaging.startFlow @@ -35,7 +37,9 @@ import org.junit.Before import org.junit.Test import rx.Observable import java.io.ByteArrayOutputStream -import kotlin.test.* +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue class CordaRPCOpsImplTest { @@ -118,7 +122,6 @@ class CordaRPCOpsImplTest { @Test fun `issue and move`() { - rpc.startFlow(::CashIssueFlow, Amount(100, USD), OpaqueBytes(ByteArray(1, { 1 })), @@ -225,4 +228,19 @@ class CordaRPCOpsImplTest { assertArrayEquals(bufferFile.toByteArray(), bufferRpc.toByteArray()) } + + @Test + fun `attempt to start non-RPC flow`() { + CURRENT_RPC_CONTEXT.set(RpcContext(User("user", "pwd", permissions = setOf( + startFlowPermission() + )))) + assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy { + rpc.startFlow(::NonRPCFlow) + } + } + + class NonRPCFlow : FlowLogic() { + @Suspendable + override fun call() = Unit + } } diff --git a/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt b/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt index 7ac3f7038e..e79a83b653 100644 --- a/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt +++ b/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt @@ -12,7 +12,6 @@ import net.corda.node.serialization.NodeClock import net.corda.node.services.api.* import net.corda.node.services.messaging.MessagingService import net.corda.node.services.schema.NodeSchemaService -import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.transactions.InMemoryTransactionVerifierService @@ -30,7 +29,6 @@ open class MockServiceHubInternal( val mapCache: NetworkMapCacheInternal? = MockNetworkMapCache(), val scheduler: SchedulerService? = null, val overrideClock: Clock? = NodeClock(), - val flowFactory: FlowLogicRefFactoryInternal? = FlowLogicRefFactoryImpl(), val schemas: SchemaService? = NodeSchemaService(), val customTransactionVerifierService: TransactionVerifierService? = InMemoryTransactionVerifierService(2) ) : ServiceHubInternal() { @@ -56,8 +54,8 @@ open class MockServiceHubInternal( get() = throw UnsupportedOperationException() override val monitoringService: MonitoringService = MonitoringService(MetricRegistry()) - override val flowLogicRefFactory: FlowLogicRefFactoryInternal - get() = flowFactory ?: throw UnsupportedOperationException() + override val rpcFlows: List>> + get() = throw UnsupportedOperationException() override val schemaService: SchemaService get() = schemas ?: throw UnsupportedOperationException() override val auditService: AuditService = DummyAuditService() diff --git a/node/src/test/kotlin/net/corda/node/services/events/FlowLogicRefTest.kt b/node/src/test/kotlin/net/corda/node/services/events/FlowLogicRefTest.kt index 3baa1a7448..ae6a5b7680 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/FlowLogicRefTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/FlowLogicRefTest.kt @@ -1,9 +1,8 @@ package net.corda.node.services.events -import net.corda.core.days import net.corda.core.flows.FlowLogic +import net.corda.core.flows.IllegalFlowLogicException import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl -import org.junit.Before import org.junit.Test import java.time.Duration @@ -31,67 +30,51 @@ class FlowLogicRefTest { override fun call() = Unit } - @Suppress("UNUSED_PARAMETER") // We will never use A or b - class NotWhiteListedKotlinFlowLogic(A: Int, b: String) : FlowLogic() { + class NonSchedulableFlow : FlowLogic() { override fun call() = Unit } - lateinit var factory: FlowLogicRefFactoryImpl - - @Before - fun setup() { - // We have to allow Java boxed primitives but Kotlin warns we shouldn't be using them - factory = FlowLogicRefFactoryImpl(mapOf(Pair(KotlinFlowLogic::class.java.name, setOf(ParamType1::class.java.name, ParamType2::class.java.name)), - Pair(KotlinNoArgFlowLogic::class.java.name, setOf()))) + @Test + fun `create kotlin no arg`() { + FlowLogicRefFactoryImpl.createForRPC(KotlinNoArgFlowLogic::class.java) } @Test - fun testCreateKotlinNoArg() { - factory.create(KotlinNoArgFlowLogic::class.java) - } - - @Test - fun testCreateKotlin() { + fun `create kotlin`() { val args = mapOf(Pair("A", ParamType1(1)), Pair("b", ParamType2("Hello Jack"))) - factory.createKotlin(KotlinFlowLogic::class.java, args) + FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args) } @Test - fun testCreatePrimary() { - factory.create(KotlinFlowLogic::class.java, ParamType1(1), ParamType2("Hello Jack")) - } - - @Test(expected = IllegalArgumentException::class) - fun testCreateNotWhiteListed() { - factory.create(NotWhiteListedKotlinFlowLogic::class.java, ParamType1(1), ParamType2("Hello Jack")) + fun `create primary`() { + FlowLogicRefFactoryImpl.createForRPC(KotlinFlowLogic::class.java, ParamType1(1), ParamType2("Hello Jack")) } @Test - fun testCreateKotlinVoid() { - factory.createKotlin(KotlinFlowLogic::class.java, emptyMap()) + fun `create kotlin void`() { + FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, emptyMap()) } @Test - fun testCreateKotlinNonPrimary() { + fun `create kotlin non primary`() { val args = mapOf(Pair("C", ParamType2("Hello Jack"))) - factory.createKotlin(KotlinFlowLogic::class.java, args) - } - - @Test(expected = IllegalArgumentException::class) - fun testCreateArgNotWhiteListed() { - val args = mapOf(Pair("illegal", 1.days)) - factory.createKotlin(KotlinFlowLogic::class.java, args) + FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args) } @Test - fun testCreateJavaPrimitiveNoRegistrationRequired() { + fun `create java primitive no registration required`() { val args = mapOf(Pair("primitive", "A string")) - factory.createKotlin(KotlinFlowLogic::class.java, args) + FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args) } @Test - fun testCreateKotlinPrimitiveNoRegistrationRequired() { + fun `create kotlin primitive no registration required`() { val args = mapOf(Pair("kotlinType", 3)) - factory.createKotlin(KotlinFlowLogic::class.java, args) + FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args) + } + + @Test(expected = IllegalFlowLogicException::class) + fun `create for non-schedulable flow logic`() { + FlowLogicRefFactoryImpl.create(NonSchedulableFlow::class.java) } } diff --git a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt index eda373e2db..ee3055c3a8 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt @@ -44,10 +44,6 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { val schedulerGatedExecutor = AffinityExecutor.Gate(true) - // We have to allow Java boxed primitives but Kotlin warns we shouldn't be using them - @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") - val factory = FlowLogicRefFactoryImpl(mapOf(Pair(TestFlowLogic::class.java.name, setOf(NodeSchedulerServiceTest::class.java.name, Integer::class.java.name)))) - lateinit var services: MockServiceHubInternal lateinit var scheduler: NodeSchedulerService @@ -82,12 +78,16 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { database.transaction { val kms = MockKeyManagementService(ALICE_KEY) val nullIdentity = X500Name("cn=None") - val mockMessagingService = InMemoryMessagingNetwork(false).InMemoryMessaging(false, InMemoryMessagingNetwork.PeerHandle(0, nullIdentity), AffinityExecutor.ServiceAffinityExecutor("test", 1), database) + val mockMessagingService = InMemoryMessagingNetwork(false).InMemoryMessaging( + false, + InMemoryMessagingNetwork.PeerHandle(0, nullIdentity), + AffinityExecutor.ServiceAffinityExecutor("test", 1), + database) services = object : MockServiceHubInternal(overrideClock = testClock, keyManagement = kms, net = mockMessagingService), TestReference { override val vaultService: VaultService = NodeVaultService(this, dataSourceProps) override val testReference = this@NodeSchedulerServiceTest } - scheduler = NodeSchedulerService(services, database, factory, schedulerGatedExecutor) + scheduler = NodeSchedulerService(services, database, schedulerGatedExecutor) smmExecutor = AffinityExecutor.ServiceAffinityExecutor("test", 1) val mockSMM = StateMachineManager(services, listOf(services, scheduler), DBCheckpointStorage(), smmExecutor, database) mockSMM.changes.subscribe { change -> @@ -269,7 +269,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { database.transaction { apply { val freshKey = services.keyManagementService.freshKey() - val state = TestState(factory.create(TestFlowLogic::class.java, increment), instant) + val state = TestState(FlowLogicRefFactoryImpl.createForRPC(TestFlowLogic::class.java, increment), instant) val usefulTX = TransactionType.General.Builder(null).apply { addOutputState(state, DUMMY_NOTARY) addCommand(Command(), freshKey.public) diff --git a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt index fdbee20bdd..3d79ad6ed7 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt @@ -7,7 +7,7 @@ import net.corda.core.crypto.containsAny import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogicRefFactory -import net.corda.core.node.CordaPluginRegistry +import net.corda.core.flows.SchedulableFlow import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.linearHeadsOfType import net.corda.core.utilities.DUMMY_NOTARY @@ -15,7 +15,6 @@ import net.corda.flows.FinalityFlow import net.corda.node.services.network.NetworkMapService import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.transactions.ValidatingNotaryService -import net.corda.node.utilities.AddOrRemove import net.corda.node.utilities.transaction import net.corda.testing.node.MockNetwork import org.junit.After @@ -68,6 +67,7 @@ class ScheduledFlowTests { } } + @SchedulableFlow class ScheduledFlow(val stateRef: StateRef) : FlowLogic() { @Suspendable override fun call() { @@ -87,14 +87,6 @@ class ScheduledFlowTests { } } - object ScheduledFlowTestPlugin : CordaPluginRegistry() { - override val requiredFlows: Map> = mapOf( - InsertInitialStateFlow::class.java.name to setOf(Party::class.java.name), - ScheduledFlow::class.java.name to setOf(StateRef::class.java.name) - ) - } - - @Before fun setup() { net = MockNetwork(threadPerNode = true) @@ -103,8 +95,6 @@ class ScheduledFlowTests { advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type))) nodeA = net.createNode(notaryNode.info.address, start = false) nodeB = net.createNode(notaryNode.info.address, start = false) - nodeA.testPluginRegistries.add(ScheduledFlowTestPlugin) - nodeB.testPluginRegistries.add(ScheduledFlowTestPlugin) net.startNodes() } @@ -138,7 +128,7 @@ class ScheduledFlowTests { } @Test - fun `Run a whole batch of scheduled flows`() { + fun `run a whole batch of scheduled flows`() { val N = 100 for (i in 0..N - 1) { nodeA.services.startFlow(InsertInitialStateFlow(nodeB.info.legalIdentity)) diff --git a/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt b/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt index 6bcbb5f65c..0b9ca23c58 100644 --- a/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt +++ b/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt @@ -7,6 +7,7 @@ import net.corda.core.utilities.DUMMY_BANK_A import net.corda.core.utilities.DUMMY_BANK_B import net.corda.core.utilities.DUMMY_NOTARY import net.corda.node.driver.driver +import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User import org.junit.Test @@ -17,11 +18,11 @@ class AttachmentDemoTest { @Test fun `attachment demo using a 10MB zip file`() { val numOfExpectedBytes = 10_000_000 driver(dsl = { - val demoUser = listOf(User("demo", "demo", setOf("StartFlow.net.corda.flows.FinalityFlow"))) + val demoUser = listOf(User("demo", "demo", setOf(startFlowPermission()))) val (nodeA, nodeB) = Futures.allAsList( startNode(DUMMY_BANK_A.name, rpcUsers = demoUser), startNode(DUMMY_BANK_B.name, rpcUsers = demoUser), - startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.Companion.type))) + startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type))) ).getOrThrow() val senderThread = CompletableFuture.supplyAsync { diff --git a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt index ee24480e6d..4ca8f0e9ee 100644 --- a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt +++ b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt @@ -1,5 +1,6 @@ package net.corda.attachmentdemo +import co.paralleluniverse.fibers.Suspendable import com.google.common.net.HostAndPort import joptsimple.OptionParser import net.corda.client.rpc.CordaRPCClient @@ -9,14 +10,14 @@ import net.corda.core.contracts.TransactionForContract import net.corda.core.contracts.TransactionType import net.corda.core.identity.Party import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC import net.corda.core.getOrThrow import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startTrackedFlow import net.corda.core.sizedInputStreamAndHash -import net.corda.core.utilities.DUMMY_BANK_B -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_NOTARY_KEY -import net.corda.core.utilities.Emoji +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.* import net.corda.flows.FinalityFlow import net.corda.node.driver.poll import java.io.InputStream @@ -83,26 +84,40 @@ fun sender(rpc: CordaRPCOps, inputStream: InputStream, hash: SecureHash.SHA256) val id = rpc.uploadAttachment(it) require(hash == id) { "Id was '$id' instead of '$hash'" } } + require(rpc.attachmentExists(hash)) } - // Create a trivial transaction with an output that describes the attachment, and the attachment itself - val ptx = TransactionType.General.Builder(notary = DUMMY_NOTARY) - require(rpc.attachmentExists(hash)) - ptx.addOutputState(AttachmentContract.State(hash)) - ptx.addAttachment(hash) - - // Sign with the notary key - ptx.signWith(DUMMY_NOTARY_KEY) - - // Send the transaction to the other recipient - val stx = ptx.toSignedTransaction() - println("Sending ${stx.id}") - val flowHandle = rpc.startTrackedFlow(::FinalityFlow, stx, setOf(otherSide)) + val flowHandle = rpc.startTrackedFlow(::AttachmentDemoFlow, otherSide, hash) flowHandle.progress.subscribe(::println) - flowHandle.returnValue.getOrThrow() + val stx = flowHandle.returnValue.getOrThrow() println("Sent ${stx.id}") } +@StartableByRPC +class AttachmentDemoFlow(val otherSide: Party, val hash: SecureHash.SHA256) : FlowLogic() { + + object SIGNING : ProgressTracker.Step("Signing transaction") + + override val progressTracker: ProgressTracker = ProgressTracker(SIGNING) + + @Suspendable + override fun call(): SignedTransaction { + // Create a trivial transaction with an output that describes the attachment, and the attachment itself + val ptx = TransactionType.General.Builder(notary = DUMMY_NOTARY) + ptx.addOutputState(AttachmentContract.State(hash)) + ptx.addAttachment(hash) + + progressTracker.currentStep = SIGNING + // Sign with the notary key + ptx.signWith(DUMMY_NOTARY_KEY) + + // Send the transaction to the other recipient + val stx = ptx.toSignedTransaction() + + return subFlow(FinalityFlow(stx, setOf(otherSide))).single() + } +} + fun recipient(rpc: CordaRPCOps) { println("Waiting to receive transaction ...") val stx = rpc.verifiedTransactions().second.toBlocking().first() diff --git a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/plugin/AttachmentDemoPlugin.kt b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/plugin/AttachmentDemoPlugin.kt deleted file mode 100644 index 33c80473ed..0000000000 --- a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/plugin/AttachmentDemoPlugin.kt +++ /dev/null @@ -1,12 +0,0 @@ -package net.corda.attachmentdemo.plugin - -import net.corda.core.node.CordaPluginRegistry -import net.corda.core.transactions.SignedTransaction -import net.corda.flows.FinalityFlow - -class AttachmentDemoPlugin : CordaPluginRegistry() { - // A list of Flows that are required for this cordapp - override val requiredFlows: Map> = mapOf( - FinalityFlow::class.java.name to setOf(SignedTransaction::class.java.name, setOf(Unit).javaClass.name, setOf(Unit).javaClass.name) - ) -} diff --git a/samples/attachment-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry b/samples/attachment-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry deleted file mode 100644 index 2c117fcac4..0000000000 --- a/samples/attachment-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry +++ /dev/null @@ -1,2 +0,0 @@ -# Register a ServiceLoader service extending from net.corda.core.node.CordaPluginRegistry -net.corda.attachmentdemo.plugin.AttachmentDemoPlugin diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/plugin/BankOfCordaPlugin.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/plugin/BankOfCordaPlugin.kt index e0d12385c3..8fd4ce47bb 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/plugin/BankOfCordaPlugin.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/plugin/BankOfCordaPlugin.kt @@ -1,17 +1,13 @@ package net.corda.bank.plugin import net.corda.bank.api.BankOfCordaWebApi -import net.corda.core.contracts.Amount import net.corda.core.identity.Party import net.corda.core.node.CordaPluginRegistry -import net.corda.core.serialization.OpaqueBytes import net.corda.flows.IssuerFlow import java.util.function.Function class BankOfCordaPlugin : CordaPluginRegistry() { // A list of classes that expose web APIs. override val webApis = listOf(Function(::BankOfCordaWebApi)) - // A list of flow that are required for this cordapp - override val requiredFlows: Map> = mapOf(IssuerFlow.IssuanceRequester::class.java.name to setOf(Amount::class.java.name, Party::class.java.name, OpaqueBytes::class.java.name, Party::class.java.name)) override val servicePlugins = listOf(Function(IssuerFlow.Issuer::Service)) } diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt index 7ba75b4a64..99ec82e50b 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt @@ -19,7 +19,6 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.FilteredTransaction import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.unwrap -import net.corda.irs.flows.FixingFlow import net.corda.irs.flows.RatesFixFlow import net.corda.node.services.api.AcceptsFileUpload import net.corda.node.utilities.AbstractJDBCHashSet @@ -32,12 +31,14 @@ import java.io.InputStream import java.math.BigDecimal import java.security.KeyPair import java.time.Clock -import java.time.Duration import java.time.Instant import java.time.LocalDate import java.util.* import java.util.function.Function import javax.annotation.concurrent.ThreadSafe +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.set /** * An interest rates service is an oracle that signs transactions which contain embedded assertions about an interest @@ -55,7 +56,6 @@ object NodeInterestRates { * Register the flow that is used with the Fixing integration tests. */ class Plugin : CordaPluginRegistry() { - override val requiredFlows = mapOf(Pair(FixingFlow.FixingRoleDecider::class.java.name, setOf(Duration::class.java.name, StateRef::class.java.name))) override val servicePlugins = listOf(Function(::Service)) } diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/AutoOfferFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/AutoOfferFlow.kt index 402a4f89c8..e1f824049e 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/AutoOfferFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/AutoOfferFlow.kt @@ -5,6 +5,7 @@ import net.corda.core.contracts.DealState import net.corda.core.identity.AbstractParty import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC import net.corda.core.node.CordaPluginRegistry import net.corda.core.node.PluginServiceHub import net.corda.core.serialization.SingletonSerializeAsToken @@ -37,6 +38,7 @@ object AutoOfferFlow { } @InitiatingFlow + @StartableByRPC class Requester(val dealToBeOffered: DealState) : FlowLogic() { companion object { diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt index e962181798..bf65a760ad 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt @@ -8,6 +8,7 @@ import net.corda.core.crypto.keys import net.corda.core.crypto.toBase58String import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.SchedulableFlow import net.corda.core.node.NodeInfo import net.corda.core.node.PluginServiceHub import net.corda.core.node.services.ServiceType @@ -136,6 +137,7 @@ object FixingFlow { * Fixer role is chosen, then that will be initiated by the [FixingSession] message sent from the other party. */ @InitiatingFlow + @SchedulableFlow class FixingRoleDecider(val ref: StateRef, override val progressTracker: ProgressTracker) : FlowLogic() { @Suppress("unused") // Used via reflection. constructor(ref: StateRef) : this(ref, tracker()) diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt index 47812e4ba8..fd6d7c79cf 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt @@ -4,6 +4,7 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.identity.Party import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC import net.corda.core.node.CordaPluginRegistry import net.corda.core.node.NodeInfo import net.corda.core.node.PluginServiceHub @@ -44,6 +45,7 @@ object UpdateBusinessDayFlow { @InitiatingFlow + @StartableByRPC class Broadcast(val date: LocalDate, override val progressTracker: ProgressTracker) : FlowLogic() { constructor(date: LocalDate) : this(date, tracker()) diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/plugin/IRSPlugin.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/plugin/IRSPlugin.kt index fc607ef272..343ebccb79 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/plugin/IRSPlugin.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/plugin/IRSPlugin.kt @@ -1,15 +1,9 @@ package net.corda.irs.plugin -import net.corda.core.contracts.StateRef import net.corda.core.identity.Party import net.corda.core.node.CordaPluginRegistry import net.corda.irs.api.InterestRateSwapAPI -import net.corda.irs.contract.InterestRateSwap -import net.corda.irs.flows.AutoOfferFlow import net.corda.irs.flows.FixingFlow -import net.corda.irs.flows.UpdateBusinessDayFlow -import java.time.Duration -import java.time.LocalDate import java.util.function.Function class IRSPlugin : CordaPluginRegistry() { @@ -18,9 +12,4 @@ class IRSPlugin : CordaPluginRegistry() { "irsdemo" to javaClass.classLoader.getResource("irsweb").toExternalForm() ) override val servicePlugins = listOf(Function(FixingFlow::Service)) - override val requiredFlows: Map> = mapOf( - AutoOfferFlow.Requester::class.java.name to setOf(InterestRateSwap.State::class.java.name), - UpdateBusinessDayFlow.Broadcast::class.java.name to setOf(LocalDate::class.java.name), - FixingFlow.FixingRoleDecider::class.java.name to setOf(StateRef::class.java.name, Duration::class.java.name), - FixingFlow.Floater::class.java.name to setOf(Party::class.java.name, FixingFlow.FixingSession::class.java.name)) } diff --git a/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/plugin/NotaryDemoPlugin.kt b/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/plugin/NotaryDemoPlugin.kt deleted file mode 100644 index 816faa655a..0000000000 --- a/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/plugin/NotaryDemoPlugin.kt +++ /dev/null @@ -1,15 +0,0 @@ -package net.corda.notarydemo.plugin - -import net.corda.core.identity.Party -import net.corda.core.node.CordaPluginRegistry -import net.corda.core.transactions.SignedTransaction -import net.corda.flows.NotaryFlow -import net.corda.notarydemo.flows.DummyIssueAndMove - -class NotaryDemoPlugin : CordaPluginRegistry() { - // A list of protocols that are required for this cordapp - override val requiredFlows = mapOf( - NotaryFlow.Client::class.java.name to setOf(SignedTransaction::class.java.name, setOf(Unit).javaClass.name), - DummyIssueAndMove::class.java.name to setOf(Party::class.java.name) - ) -} diff --git a/samples/raft-notary-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry b/samples/raft-notary-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry deleted file mode 100644 index 2ea555bbb9..0000000000 --- a/samples/raft-notary-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry +++ /dev/null @@ -1,2 +0,0 @@ -# Register a ServiceLoader service extending from net.corda.node.CordaPluginRegistry -net.corda.notarydemo.plugin.NotaryDemoPlugin \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/IRSTradeFlow.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/IRSTradeFlow.kt index 3959dcb127..d46ff74a4b 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/IRSTradeFlow.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/IRSTradeFlow.kt @@ -4,6 +4,7 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.identity.Party import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC import net.corda.core.node.PluginServiceHub import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction @@ -24,6 +25,7 @@ object IRSTradeFlow { data class OfferMessage(val notary: Party, val dealBeingOffered: IRSState) @InitiatingFlow + @StartableByRPC class Requester(val swap: SwapData, val otherParty: Party) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt index c2c764013f..d4b4342b0a 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt @@ -14,6 +14,7 @@ import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC import net.corda.core.node.PluginServiceHub import net.corda.core.node.services.dealsWith import net.corda.core.serialization.CordaSerializable @@ -51,6 +52,7 @@ object SimmFlow { * margin using SIMM. If there is an existing state it will update and revalue the portfolio agreement. */ @InitiatingFlow + @StartableByRPC class Requester(val otherParty: Party, val valuationDate: LocalDate, val existing: StateAndRef?) diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmRevaluation.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmRevaluation.kt index 4580e568be..d6e932c6e5 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmRevaluation.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmRevaluation.kt @@ -3,6 +3,8 @@ package net.corda.vega.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.StateRef import net.corda.core.flows.FlowLogic +import net.corda.core.flows.SchedulableFlow +import net.corda.core.flows.StartableByRPC import net.corda.core.node.services.linearHeadsOfType import net.corda.vega.contracts.PortfolioState import java.time.LocalDate @@ -12,6 +14,8 @@ import java.time.LocalDate * requirements */ object SimmRevaluation { + @StartableByRPC + @SchedulableFlow class Initiator(val curStateRef: StateRef, val valuationDate: LocalDate) : FlowLogic() { @Suspendable override fun call(): Unit { diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/services/SimmService.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/services/SimmService.kt index d6d9aef072..06d14023d7 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/services/SimmService.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/services/SimmService.kt @@ -10,18 +10,14 @@ import com.opengamma.strata.market.curve.CurveName import com.opengamma.strata.market.param.CurrencyParameterSensitivities import com.opengamma.strata.market.param.CurrencyParameterSensitivity import com.opengamma.strata.market.param.TenorDateParameterMetadata -import net.corda.core.contracts.StateRef import net.corda.core.identity.Party import net.corda.core.node.CordaPluginRegistry import net.corda.core.serialization.SerializationCustomization import net.corda.vega.analytics.CordaMarketData import net.corda.vega.analytics.InitialMarginTriple import net.corda.vega.api.PortfolioApi -import net.corda.vega.contracts.SwapData import net.corda.vega.flows.IRSTradeFlow import net.corda.vega.flows.SimmFlow -import net.corda.vega.flows.SimmRevaluation -import java.time.LocalDate import java.util.function.Function /** @@ -32,10 +28,6 @@ import java.util.function.Function object SimmService { class Plugin : CordaPluginRegistry() { override val webApis = listOf(Function(::PortfolioApi)) - override val requiredFlows: Map> = mapOf( - SimmFlow.Requester::class.java.name to setOf(Party::class.java.name, LocalDate::class.java.name), - SimmRevaluation.Initiator::class.java.name to setOf(StateRef::class.java.name, LocalDate::class.java.name), - IRSTradeFlow.Requester::class.java.name to setOf(SwapData::class.java.name, Party::class.java.name)) override val staticServeDirs: Map = mapOf("simmvaluationdemo" to javaClass.classLoader.getResource("simmvaluationweb").toExternalForm()) override val servicePlugins = listOf(Function(SimmFlow::Service), Function(IRSTradeFlow::Service)) override fun customizeSerialization(custom: SerializationCustomization): Boolean { diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt index d2bee38d56..9222a27a9c 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt @@ -10,6 +10,7 @@ import net.corda.core.crypto.generateKeyPair import net.corda.core.days import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC import net.corda.core.node.NodeInfo import net.corda.core.seconds import net.corda.core.transactions.SignedTransaction @@ -22,6 +23,7 @@ import java.time.Instant import java.util.* @InitiatingFlow +@StartableByRPC class SellerFlow(val otherParty: Party, val amount: Amount, override val progressTracker: ProgressTracker) : FlowLogic() { diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/plugin/TraderDemoPlugin.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/plugin/TraderDemoPlugin.kt index 8fa30e3e10..cd45ed4b95 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/plugin/TraderDemoPlugin.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/plugin/TraderDemoPlugin.kt @@ -1,16 +1,10 @@ package net.corda.traderdemo.plugin -import net.corda.core.contracts.Amount import net.corda.core.identity.Party import net.corda.core.node.CordaPluginRegistry import net.corda.traderdemo.flow.BuyerFlow -import net.corda.traderdemo.flow.SellerFlow import java.util.function.Function class TraderDemoPlugin : CordaPluginRegistry() { - // A list of Flows that are required for this cordapp - override val requiredFlows: Map> = mapOf( - SellerFlow::class.java.name to setOf(Party::class.java.name, Amount::class.java.name) - ) override val servicePlugins = listOf(Function(BuyerFlow::Service)) } diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/plugin/ExplorerPlugin.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/plugin/ExplorerPlugin.kt index a956b864a4..0dbc6b5e4e 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/plugin/ExplorerPlugin.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/plugin/ExplorerPlugin.kt @@ -1,14 +1,10 @@ package net.corda.explorer.plugin -import net.corda.core.contracts.Amount import net.corda.core.identity.Party import net.corda.core.node.CordaPluginRegistry -import net.corda.core.serialization.OpaqueBytes import net.corda.flows.IssuerFlow import java.util.function.Function class ExplorerPlugin : CordaPluginRegistry() { - // A list of flow that are required for this cordapp - override val requiredFlows: Map> = mapOf(IssuerFlow.IssuanceRequester::class.java.name to setOf(Amount::class.java.name, Party::class.java.name, OpaqueBytes::class.java.name, Party::class.java.name)) override val servicePlugins = listOf(Function(IssuerFlow.Issuer::Service)) } diff --git a/webserver/src/main/kotlin/net/corda/webserver/servlets/CorDappInfoServlet.kt b/webserver/src/main/kotlin/net/corda/webserver/servlets/CorDappInfoServlet.kt index 38bcd8a427..8de361d8a1 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/servlets/CorDappInfoServlet.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/servlets/CorDappInfoServlet.kt @@ -30,14 +30,6 @@ class CorDappInfoServlet(val plugins: List, val rpc: CordaR } else { plugins.forEach { plugin -> h3 { +plugin::class.java.name } - if (plugin.requiredFlows.isNotEmpty()) { - div { - p { +"Whitelisted flows:" } - ul { - plugin.requiredFlows.map { it.key }.forEach { li { +it } } - } - } - } if (plugin.webApis.isNotEmpty()) { div { plugin.webApis.forEach { api -> @@ -56,7 +48,7 @@ class CorDappInfoServlet(val plugins: List, val rpc: CordaR div { p { +"Static web content:" } ul { - plugin.staticServeDirs.map { it.key }.forEach { + plugin.staticServeDirs.keys.forEach { li { a("web/$it") { +it } } } }