Merge pull request #720 from corda/shams-flow-registration-annotation

Introducing InitiatedBy annotation to be used on initiated flows to s…
This commit is contained in:
Shams Asari 2017-05-31 11:17:23 +01:00 committed by GitHub
commit 37e183652e
66 changed files with 892 additions and 760 deletions

View File

@ -147,6 +147,12 @@ operator fun String.div(other: String) = Paths.get(this) / other
fun Path.createDirectory(vararg attrs: FileAttribute<*>): Path = Files.createDirectory(this, *attrs) fun Path.createDirectory(vararg attrs: FileAttribute<*>): Path = Files.createDirectory(this, *attrs)
fun Path.createDirectories(vararg attrs: FileAttribute<*>): Path = Files.createDirectories(this, *attrs) fun Path.createDirectories(vararg attrs: FileAttribute<*>): Path = Files.createDirectories(this, *attrs)
fun Path.exists(vararg options: LinkOption): Boolean = Files.exists(this, *options) fun Path.exists(vararg options: LinkOption): Boolean = Files.exists(this, *options)
fun Path.copyToDirectory(targetDir: Path, vararg options: CopyOption): Path {
require(targetDir.isDirectory()) { "$targetDir is not a directory" }
val targetFile = targetDir.resolve(fileName)
Files.copy(this, targetFile, *options)
return targetFile
}
fun Path.moveTo(target: Path, vararg options: CopyOption): Path = Files.move(this, target, *options) fun Path.moveTo(target: Path, vararg options: CopyOption): Path = Files.move(this, target, *options)
fun Path.isRegularFile(vararg options: LinkOption): Boolean = Files.isRegularFile(this, *options) fun Path.isRegularFile(vararg options: LinkOption): Boolean = Files.isRegularFile(this, *options)
fun Path.isDirectory(vararg options: LinkOption): Boolean = Files.isDirectory(this, *options) fun Path.isDirectory(vararg options: LinkOption): Boolean = Files.isDirectory(this, *options)

View File

@ -0,0 +1,16 @@
package net.corda.core.flows
import kotlin.annotation.AnnotationTarget.CLASS
import kotlin.reflect.KClass
/**
* This annotation is required by any [FlowLogic] that is designed to be initiated by a counterparty flow. The flow that
* does the initiating is specified by the [value] property and itself must be annotated with [InitiatingFlow].
*
* The node on startup scans for [FlowLogic]s which are annotated with this and automatically registers the initiating
* to initiated flow mapping.
*
* @see InitiatingFlow
*/
@Target(CLASS)
annotation class InitiatedBy(val value: KClass<out FlowLogic<*>>)

View File

@ -5,8 +5,7 @@ import kotlin.annotation.AnnotationTarget.CLASS
/** /**
* This annotation is required by any [FlowLogic] which has been designated to initiate communication with a counterparty * This annotation is required by any [FlowLogic] which has been designated to initiate communication with a counterparty
* and request they start their side of the flow communication. To ensure that this is correctly applied * and request they start their side of the flow communication.
* [net.corda.core.node.PluginServiceHub.registerServiceFlow] checks the initiating flow class has this annotation.
* *
* There is also an optional [version] property, which defaults to 1, to specify the version of the flow protocol. This * There is also an optional [version] property, which defaults to 1, to specify the version of the flow protocol. This
* integer value should be incremented whenever there is a release of this flow which has changes that are not backwards * integer value should be incremented whenever there is a release of this flow which has changes that are not backwards
@ -19,6 +18,11 @@ import kotlin.annotation.AnnotationTarget.CLASS
* *
* The flow version number is similar in concept to Corda's platform version but they are not the same. A flow's version * The flow version number is similar in concept to Corda's platform version but they are not the same. A flow's version
* number can change independently of the platform version. * number can change independently of the platform version.
*
* If you are customising an existing initiating flow by sub-classing it then there's no need to specify this annotation
* again. In fact doing so is an error and checks are made to make sure this doesn't occur.
*
* @see InitiatedBy
*/ */
// TODO Add support for multiple versions once CorDapps are loaded in separate class loaders // TODO Add support for multiple versions once CorDapps are loaded in separate class loaders
@Target(CLASS) @Target(CLASS)

View File

@ -1,6 +1,5 @@
package net.corda.core.flows package net.corda.core.flows
import java.lang.annotation.Inherited
import kotlin.annotation.AnnotationTarget.CLASS import kotlin.annotation.AnnotationTarget.CLASS
/** /**
@ -9,7 +8,6 @@ import kotlin.annotation.AnnotationTarget.CLASS
* flow will not be allowed to start and an exception will be thrown. * flow will not be allowed to start and an exception will be thrown.
*/ */
@Target(CLASS) @Target(CLASS)
@Inherited
@MustBeDocumented @MustBeDocumented
// TODO Consider a different name, something along the lines of SchedulableFlow // TODO Consider a different name, something along the lines of SchedulableFlow
annotation class StartableByRPC annotation class StartableByRPC

View File

@ -15,7 +15,7 @@ import java.security.cert.X509Certificate
* This is intended for use as a subflow of another flow. * This is intended for use as a subflow of another flow.
*/ */
object TxKeyFlow { object TxKeyFlow {
abstract class AbstractIdentityFlow(val otherSide: Party, val revocationEnabled: Boolean): FlowLogic<Map<Party, AnonymousIdentity>>() { abstract class AbstractIdentityFlow<out T>(val otherSide: Party): FlowLogic<T>() {
fun validateIdentity(untrustedIdentity: Pair<X509CertificateHolder, CertPath>): AnonymousIdentity { fun validateIdentity(untrustedIdentity: Pair<X509CertificateHolder, CertPath>): AnonymousIdentity {
val (wellKnownCert, certPath) = untrustedIdentity val (wellKnownCert, certPath) = untrustedIdentity
val theirCert = certPath.certificates.last() val theirCert = certPath.certificates.last()
@ -26,20 +26,21 @@ object TxKeyFlow {
val anonymousParty = AnonymousParty(theirCert.publicKey) val anonymousParty = AnonymousParty(theirCert.publicKey)
serviceHub.identityService.registerPath(wellKnownCert, anonymousParty, certPath) serviceHub.identityService.registerPath(wellKnownCert, anonymousParty, certPath)
AnonymousIdentity(certPath, X509CertificateHolder(theirCert.encoded), anonymousParty) AnonymousIdentity(certPath, X509CertificateHolder(theirCert.encoded), anonymousParty)
} else } else {
throw IllegalStateException("Expected certificate subject to be ${otherSide.name} but found ${certName}") throw IllegalStateException("Expected certificate subject to be ${otherSide.name} but found $certName")
} else }
} else {
throw IllegalStateException("Expected an X.509 certificate but received ${theirCert.javaClass.name}") throw IllegalStateException("Expected an X.509 certificate but received ${theirCert.javaClass.name}")
} }
} }
}
@StartableByRPC @StartableByRPC
@InitiatingFlow @InitiatingFlow
class Requester(otherSide: Party, class Requester(otherSide: Party,
revocationEnabled: Boolean, val revocationEnabled: Boolean,
override val progressTracker: ProgressTracker) : AbstractIdentityFlow(otherSide, revocationEnabled) { override val progressTracker: ProgressTracker) : AbstractIdentityFlow<Map<Party, AnonymousIdentity>>(otherSide) {
constructor(otherSide: Party, constructor(otherSide: Party, revocationEnabled: Boolean) : this(otherSide, revocationEnabled, tracker())
revocationEnabled: Boolean) : this(otherSide, revocationEnabled, tracker())
companion object { companion object {
object AWAITING_KEY : ProgressTracker.Step("Awaiting key") object AWAITING_KEY : ProgressTracker.Step("Awaiting key")
@ -62,26 +63,21 @@ object TxKeyFlow {
* Flow which waits for a key request from a counterparty, generates a new key and then returns it to the * Flow which waits for a key request from a counterparty, generates a new key and then returns it to the
* counterparty and as the result from the flow. * counterparty and as the result from the flow.
*/ */
class Provider(otherSide: Party, @InitiatedBy(Requester::class)
revocationEnabled: Boolean, class Provider(otherSide: Party) : AbstractIdentityFlow<Unit>(otherSide) {
override val progressTracker: ProgressTracker) : AbstractIdentityFlow(otherSide,revocationEnabled) {
constructor(otherSide: Party,
revocationEnabled: Boolean = false) : this(otherSide, revocationEnabled, tracker())
companion object { companion object {
object SENDING_KEY : ProgressTracker.Step("Sending key") object SENDING_KEY : ProgressTracker.Step("Sending key")
fun tracker() = ProgressTracker(SENDING_KEY)
} }
override val progressTracker: ProgressTracker = ProgressTracker(SENDING_KEY)
@Suspendable @Suspendable
override fun call(): Map<Party, AnonymousIdentity> { override fun call() {
val revocationEnabled = false
progressTracker.currentStep = SENDING_KEY progressTracker.currentStep = SENDING_KEY
val myIdentityFragment = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentity, revocationEnabled) val myIdentityFragment = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentity, revocationEnabled)
send(otherSide, myIdentityFragment) send(otherSide, myIdentityFragment)
val theirIdentity = receive<Pair<X509CertificateHolder, CertPath>>(otherSide).unwrap { validateIdentity(it) } receive<Pair<X509CertificateHolder, CertPath>>(otherSide).unwrap { validateIdentity(it) }
return mapOf(Pair(otherSide, AnonymousIdentity(myIdentityFragment)),
Pair(serviceHub.myInfo.legalIdentity, theirIdentity))
} }
} }

View File

@ -33,6 +33,9 @@ abstract class CordaPluginRegistry {
* The [PluginServiceHub] will be fully constructed before the plugin service is created and will * 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. * allow access to the Flow factory and Flow initiation entry points there.
*/ */
@Suppress("unused")
@Deprecated("This is no longer used. If you need to create your own service, such as an oracle, then use the " +
"@CordaService annotation. For flow registrations use @InitiatedBy.", level = DeprecationLevel.ERROR)
open val servicePlugins: List<Function<PluginServiceHub, out Any>> get() = emptyList() open val servicePlugins: List<Function<PluginServiceHub, out Any>> get() = emptyList()
/** /**

View File

@ -7,22 +7,7 @@ import net.corda.core.identity.Party
* A service hub to be used by the [CordaPluginRegistry] * A service hub to be used by the [CordaPluginRegistry]
*/ */
interface PluginServiceHub : ServiceHub { interface PluginServiceHub : ServiceHub {
/** @Deprecated("This is no longer used. Instead annotate the flows produced by your factory with @InitiatedBy and have " +
* Register the service flow factory to use when an initiating party attempts to communicate with us. The registration "them point to the initiating flow class.", level = DeprecationLevel.ERROR)
* is done against the [Class] object of the client flow to the service flow. What this means is if a counterparty fun registerFlowInitiator(initiatingFlowClass: Class<out FlowLogic<*>>, serviceFlowFactory: (Party) -> FlowLogic<*>) = Unit
* starts a [FlowLogic] represented by [initiatingFlowClass] and starts communication with us, we will execute the service
* flow produced by [serviceFlowFactory]. This service flow has respond correctly to the sends and receives the client
* does.
* @param initiatingFlowClass [Class] of the client flow involved in this client-server communication.
* @param serviceFlowFactory Lambda which produces a new service flow for each new client flow communication. The
* [Party] parameter of the factory is the client's identity.
* @throws IllegalArgumentException If [initiatingFlowClass] is not annotated with [net.corda.core.flows.InitiatingFlow].
*/
fun registerServiceFlow(initiatingFlowClass: Class<out FlowLogic<*>>, serviceFlowFactory: (Party) -> FlowLogic<*>)
@Suppress("UNCHECKED_CAST")
@Deprecated("This is scheduled to be removed in a future release", ReplaceWith("registerServiceFlow"))
fun registerFlowInitiator(markerClass: Class<*>, flowFactory: (Party) -> FlowLogic<*>) {
registerServiceFlow(markerClass as Class<out FlowLogic<*>>, flowFactory)
}
} }

View File

@ -3,6 +3,7 @@ package net.corda.core.node
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.DigitalSignature
import net.corda.core.node.services.* import net.corda.core.node.services.*
import net.corda.core.serialization.SerializeAsToken
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import java.security.PublicKey import java.security.PublicKey
@ -44,6 +45,13 @@ interface ServiceHub : ServicesForResolution {
val clock: Clock val clock: Clock
val myInfo: NodeInfo val myInfo: NodeInfo
/**
* Return the singleton instance of the given Corda service type. This is a class that is annotated with
* [CordaService] and will have automatically been registered by the node.
* @throws IllegalArgumentException If [type] is not annotated with [CordaService] or if the instance is not found.
*/
fun <T : SerializeAsToken> cordaService(type: Class<T>): T
/** /**
* Given a [SignedTransaction], writes it to the local storage for validated transactions and then * Given a [SignedTransaction], writes it to the local storage for validated transactions and then
* sends them to the vault for further processing. Expects to be run within a database transaction. * sends them to the vault for further processing. Expects to be run within a database transaction.

View File

@ -0,0 +1,20 @@
package net.corda.core.node.services
import kotlin.annotation.AnnotationTarget.CLASS
/**
* Annotate any class that needs to be a long-lived service within the node, such as an oracle, with this annotation.
* Such a class needs to have a constructor with a single parameter of type [net.corda.core.node.PluginServiceHub]. This
* construtor will be invoked during node start to initialise the service. The service hub provided can be used to get
* information about the node that may be necessary for the service. Corda services are created as singletons within
* the node and are available to flows via [net.corda.core.node.ServiceHub.cordaService].
*
* The service class has to implement [net.corda.core.serialization.SerializeAsToken] to ensure correct usage within flows.
* (If possible extend [net.corda.core.serialization.SingletonSerializeAsToken] instead as it removes the boilerplate.)
*/
// TODO Handle the singleton serialisation of Corda services automatically, removing the need to implement SerializeAsToken
// TODO Currently all nodes which load the plugin will attempt to load the service even if it's not revelant to them. The
// underlying problem is that the entire CorDapp jar is used as a dependency, when in fact it's just the client-facing
// bit of the CorDapp that should be depended on (e.g. the initiating flows).
@Target(CLASS)
annotation class CordaService

View File

@ -2,21 +2,24 @@ package net.corda.core.utilities
import java.nio.file.Path import java.nio.file.Path
// TODO This doesn't belong in core and can be moved into node
object ProcessUtilities { object ProcessUtilities {
inline fun <reified C : Any> startJavaProcess( inline fun <reified C : Any> startJavaProcess(
arguments: List<String>, arguments: List<String>,
classpath: String = defaultClassPath,
jdwpPort: Int? = null, jdwpPort: Int? = null,
extraJvmArguments: List<String> = emptyList(), extraJvmArguments: List<String> = emptyList(),
inheritIO: Boolean = true, inheritIO: Boolean = true,
errorLogPath: Path? = null, errorLogPath: Path? = null,
workingDirectory: Path? = null workingDirectory: Path? = null
): Process { ): Process {
return startJavaProcess(C::class.java.name, arguments, jdwpPort, extraJvmArguments, inheritIO, errorLogPath, workingDirectory) return startJavaProcess(C::class.java.name, arguments, classpath, jdwpPort, extraJvmArguments, inheritIO, errorLogPath, workingDirectory)
} }
fun startJavaProcess( fun startJavaProcess(
className: String, className: String,
arguments: List<String>, arguments: List<String>,
classpath: String = defaultClassPath,
jdwpPort: Int? = null, jdwpPort: Int? = null,
extraJvmArguments: List<String> = emptyList(), extraJvmArguments: List<String> = emptyList(),
inheritIO: Boolean = true, inheritIO: Boolean = true,
@ -24,7 +27,6 @@ object ProcessUtilities {
workingDirectory: Path? = null workingDirectory: Path? = null
): Process { ): Process {
val separator = System.getProperty("file.separator") val separator = System.getProperty("file.separator")
val classpath = System.getProperty("java.class.path")
val javaPath = System.getProperty("java.home") + separator + "bin" + separator + "java" val javaPath = System.getProperty("java.home") + separator + "bin" + separator + "java"
val debugPortArgument = if (jdwpPort != null) { val debugPortArgument = if (jdwpPort != null) {
listOf("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$jdwpPort") listOf("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$jdwpPort")
@ -44,4 +46,6 @@ object ProcessUtilities {
if (workingDirectory != null) directory(workingDirectory.toFile()) if (workingDirectory != null) directory(workingDirectory.toFile())
}.start() }.start()
} }
val defaultClassPath: String get() = System.getProperty("java.class.path")
} }

View File

@ -1,13 +1,15 @@
package net.corda.core.flows; package net.corda.core.flows;
import co.paralleluniverse.fibers.*; import co.paralleluniverse.fibers.Suspendable;
import net.corda.core.identity.Party; import net.corda.core.identity.Party;
import net.corda.testing.node.*; import net.corda.testing.node.MockNetwork;
import org.junit.*; import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.concurrent.*; import java.util.concurrent.Future;
import static org.assertj.core.api.AssertionsForClassTypes.*; import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
public class FlowsInJavaTest { public class FlowsInJavaTest {
@ -30,13 +32,12 @@ public class FlowsInJavaTest {
@Test @Test
public void suspendableActionInsideUnwrap() throws Exception { public void suspendableActionInsideUnwrap() throws Exception {
node2.getServices().registerServiceFlow(SendInUnwrapFlow.class, (otherParty) -> new OtherFlow(otherParty, "Hello")); node2.registerInitiatedFlow(SendHelloAndThenReceive.class);
Future<String> result = node1.getServices().startFlow(new SendInUnwrapFlow(node2.getInfo().getLegalIdentity())).getResultFuture(); Future<String> result = node1.getServices().startFlow(new SendInUnwrapFlow(node2.getInfo().getLegalIdentity())).getResultFuture();
net.runNetwork(); net.runNetwork();
assertThat(result.get()).isEqualTo("Hello"); assertThat(result.get()).isEqualTo("Hello");
} }
@SuppressWarnings("unused")
@InitiatingFlow @InitiatingFlow
private static class SendInUnwrapFlow extends FlowLogic<String> { private static class SendInUnwrapFlow extends FlowLogic<String> {
private final Party otherParty; private final Party otherParty;
@ -55,19 +56,18 @@ public class FlowsInJavaTest {
} }
} }
private static class OtherFlow extends FlowLogic<String> { @InitiatedBy(SendInUnwrapFlow.class)
private static class SendHelloAndThenReceive extends FlowLogic<String> {
private final Party otherParty; private final Party otherParty;
private final String payload;
private OtherFlow(Party otherParty, String payload) { private SendHelloAndThenReceive(Party otherParty) {
this.otherParty = otherParty; this.otherParty = otherParty;
this.payload = payload;
} }
@Suspendable @Suspendable
@Override @Override
public String call() throws FlowException { public String call() throws FlowException {
return sendAndReceive(String.class, otherParty, payload).unwrap(data -> data); return sendAndReceive(String.class, otherParty, "Hello").unwrap(data -> data);
} }
} }

View File

@ -7,7 +7,6 @@ import net.corda.core.contracts.TransactionType
import net.corda.core.contracts.requireThat import net.corda.core.contracts.requireThat
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.PluginServiceHub
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.unwrap import net.corda.core.utilities.unwrap
import net.corda.flows.CollectSignaturesFlow import net.corda.flows.CollectSignaturesFlow
@ -18,7 +17,7 @@ import net.corda.testing.node.MockNetwork
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import java.util.concurrent.ExecutionException import kotlin.reflect.KClass
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
class CollectSignaturesFlowTests { class CollectSignaturesFlowTests {
@ -37,9 +36,6 @@ class CollectSignaturesFlowTests {
c = nodes.partyNodes[2] c = nodes.partyNodes[2]
notary = nodes.notaryNode.info.notaryIdentity notary = nodes.notaryNode.info.notaryIdentity
mockNet.runNetwork() mockNet.runNetwork()
CollectSigsTestCorDapp.registerFlows(a.services)
CollectSigsTestCorDapp.registerFlows(b.services)
CollectSigsTestCorDapp.registerFlows(c.services)
} }
@After @After
@ -47,11 +43,9 @@ class CollectSignaturesFlowTests {
mockNet.stopNodes() mockNet.stopNodes()
} }
object CollectSigsTestCorDapp { private fun registerFlowOnAllNodes(flowClass: KClass<out FlowLogic<*>>) {
// Would normally be called by custom service init in a CorDapp. listOf(a, b, c).forEach {
fun registerFlows(pluginHub: PluginServiceHub) { it.registerInitiatedFlow(flowClass.java)
pluginHub.registerFlowInitiator(TestFlow.Initiator::class.java) { TestFlow.Responder(it) }
pluginHub.registerFlowInitiator(TestFlowTwo.Initiator::class.java) { TestFlowTwo.Responder(it) }
} }
} }
@ -82,6 +76,7 @@ class CollectSignaturesFlowTests {
} }
} }
@InitiatedBy(TestFlow.Initiator::class)
class Responder(val otherParty: Party) : FlowLogic<SignedTransaction>() { class Responder(val otherParty: Party) : FlowLogic<SignedTransaction>() {
@Suspendable @Suspendable
override fun call(): SignedTransaction { override fun call(): SignedTransaction {
@ -104,7 +99,7 @@ class CollectSignaturesFlowTests {
// receiving off the wire. // receiving off the wire.
object TestFlowTwo { object TestFlowTwo {
@InitiatingFlow @InitiatingFlow
class Initiator(val state: DummyContract.MultiOwnerState, val otherParty: Party) : FlowLogic<SignedTransaction>() { class Initiator(val state: DummyContract.MultiOwnerState) : FlowLogic<SignedTransaction>() {
@Suspendable @Suspendable
override fun call(): SignedTransaction { override fun call(): SignedTransaction {
val notary = serviceHub.networkMapCache.notaryNodes.single().notaryIdentity val notary = serviceHub.networkMapCache.notaryNodes.single().notaryIdentity
@ -118,6 +113,7 @@ class CollectSignaturesFlowTests {
} }
} }
@InitiatedBy(TestFlowTwo.Initiator::class)
class Responder(val otherParty: Party) : FlowLogic<SignedTransaction>() { class Responder(val otherParty: Party) : FlowLogic<SignedTransaction>() {
@Suspendable override fun call(): SignedTransaction { @Suspendable override fun call(): SignedTransaction {
val flow = object : SignTransactionFlow(otherParty) { val flow = object : SignTransactionFlow(otherParty) {
@ -137,13 +133,13 @@ class CollectSignaturesFlowTests {
} }
} }
@Test @Test
fun `successfully collects two signatures`() { fun `successfully collects two signatures`() {
registerFlowOnAllNodes(TestFlowTwo.Responder::class)
val magicNumber = 1337 val magicNumber = 1337
val parties = listOf(a.info.legalIdentity, b.info.legalIdentity, c.info.legalIdentity) val parties = listOf(a.info.legalIdentity, b.info.legalIdentity, c.info.legalIdentity)
val state = DummyContract.MultiOwnerState(magicNumber, parties) val state = DummyContract.MultiOwnerState(magicNumber, parties)
val flow = a.services.startFlow(TestFlowTwo.Initiator(state, b.info.legalIdentity)) val flow = a.services.startFlow(TestFlowTwo.Initiator(state))
mockNet.runNetwork() mockNet.runNetwork()
val result = flow.resultFuture.getOrThrow() val result = flow.resultFuture.getOrThrow()
result.verifySignatures() result.verifySignatures()
@ -169,8 +165,8 @@ class CollectSignaturesFlowTests {
val ptx = onePartyDummyContract.signWith(MINI_CORP_KEY).toSignedTransaction(false) val ptx = onePartyDummyContract.signWith(MINI_CORP_KEY).toSignedTransaction(false)
val flow = a.services.startFlow(CollectSignaturesFlow(ptx)) val flow = a.services.startFlow(CollectSignaturesFlow(ptx))
mockNet.runNetwork() mockNet.runNetwork()
assertFailsWith<ExecutionException>("The Initiator of CollectSignaturesFlow must have signed the transaction.") { assertFailsWith<IllegalArgumentException>("The Initiator of CollectSignaturesFlow must have signed the transaction.") {
flow.resultFuture.get() flow.resultFuture.getOrThrow()
} }
} }

View File

@ -38,7 +38,7 @@ class TxKeyFlowTests {
bobNode.services.identityService.registerIdentity(notaryNode.info.legalIdentity) bobNode.services.identityService.registerIdentity(notaryNode.info.legalIdentity)
// Run the flows // Run the flows
bobNode.registerServiceFlow(TxKeyFlow.Requester::class) { TxKeyFlow.Provider(it) } bobNode.registerInitiatedFlow(TxKeyFlow.Provider::class.java)
val requesterFlow = aliceNode.services.startFlow(TxKeyFlow.Requester(bob, revocationEnabled)) val requesterFlow = aliceNode.services.startFlow(TxKeyFlow.Requester(bob, revocationEnabled))
// Get the results // Get the results

View File

@ -12,10 +12,12 @@ import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.core.utilities.unwrap import net.corda.core.utilities.unwrap
import net.corda.flows.FetchAttachmentsFlow import net.corda.flows.FetchAttachmentsFlow
import net.corda.node.internal.InitiatedFlowFactory
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.network.NetworkMapService import net.corda.node.services.network.NetworkMapService
import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.services.persistence.NodeAttachmentService
import net.corda.node.services.persistence.schemas.AttachmentEntity import net.corda.node.services.persistence.schemas.AttachmentEntity
import net.corda.node.services.statemachine.SessionInit
import net.corda.node.utilities.transaction import net.corda.node.utilities.transaction
import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork
import org.junit.After import org.junit.After
@ -136,7 +138,11 @@ class AttachmentSerializationTest {
} }
private fun launchFlow(clientLogic: ClientLogic, rounds: Int) { private fun launchFlow(clientLogic: ClientLogic, rounds: Int) {
server.services.registerServiceFlow(clientLogic.javaClass, ::ServerLogic) server.registerFlowFactory(ClientLogic::class.java, object : InitiatedFlowFactory<ServerLogic> {
override fun createFlow(platformVersion: Int, otherParty: Party, sessionInit: SessionInit): ServerLogic {
return ServerLogic(otherParty)
}
}, ServerLogic::class.java, track = false)
client.services.startFlow(clientLogic) client.services.startFlow(clientLogic)
network.runNetwork(rounds) network.runNetwork(rounds)
} }

View File

@ -7,30 +7,26 @@ from the previous milestone release.
UNRELEASED UNRELEASED
---------- ----------
* API changes: * Quite a few changes have been made to the flow API which should make things simpler when writing CorDapps:
* ``Timestamp`` used for validation/notarization time-range has been renamed to ``TimeWindow``.
There are now 4 factory methods ``TimeWindow.fromOnly(fromTime: Instant)``,
``TimeWindow.untilOnly(untilTime: Instant)``, ``TimeWindow.between(fromTime: Instant, untilTime: Instant)`` and
``TimeWindow.withTolerance(time: Instant, tolerance: Duration)``.
Previous constructors ``TimeWindow(fromTime: Instant, untilTime: Instant)`` and
``TimeWindow(time: Instant, tolerance: Duration)`` have been removed.
* ``CordaPluginRegistry.requiredFlows`` is no longer needed. Instead annotate any flows you wish to start via RPC with * ``CordaPluginRegistry.requiredFlows`` is no longer needed. Instead annotate any flows you wish to start via RPC with
``@StartableByRPC`` and any scheduled flows with ``@SchedulableFlow``. ``@StartableByRPC`` and any scheduled flows with ``@SchedulableFlow``.
* Flows which initiate flows in their counterparties (an example of which is the ``NotaryFlow.Client``) are now * ``CordaPluginRegistry.servicePlugins`` is also no longer used, along with ``PluginServiceHub.registerFlowInitiator``.
required to be annotated with ``@InitiatingFlow``. Instead annotate your initiated flows with ``@InitiatedBy``. This annotation takes a single parameter which is the
initiating flow. This initiating flow further has to be annotated with ``@InitiatingFlow``. For any services you
may have, such as oracles, annotate them with ``@CordaService``.
* ``PluginServiceHub.registerFlowInitiator`` has been deprecated and replaced by ``registerServiceFlow`` with the * Related to ``InitiatingFlow``, the ``shareParentSessions`` boolean parameter of ``FlowLogic.subFlow`` has been
marker Class restricted to ``FlowLogic``. In line with the introduction of ``InitiatingFlow``, it throws an removed. This was an unfortunate parameter that unnecessarily exposed the inner workings of flow sessions. Now, if
``IllegalArgumentException`` if the initiating flow class is not annotated with it. your sub-flow can be started outside the context of the parent flow then annotate it with ``@InitiatingFlow``. If
it's meant to be used as a continuation of the existing parent flow, such as ``CollectSignaturesFlow``, then it
doesn't need any annotation.
* Also related to ``InitiatingFlow``, the ``shareParentSessions`` boolean parameter of ``FlowLogic.subFlow`` has been * The ``InitiatingFlow`` annotation also has an integer ``version`` property which assigns the initiating flow a version
removed. Its purpose was to allow subflows to be inlined with the parent flow - i.e. the subflow does not initiate number, defaulting to 1 if it's not specified. This enables versioning of flows with nodes only accepting communication
new sessions with parties the parent flow has already started. This allowed flows to be used as building blocks. To if the version number matches. At some point we will support the ability for a node to have multiple versions of the
achieve the same effect now simply requires the subflow to be *not* annotated wth ``InitiatingFlow`` (i.e. we've made same flow registered, enabling backwards compatibility of flows.
this the default behaviour). If the subflow is not meant to be inlined, and is supposed to initiate flows on the
other side, the annotation is required.
* ``ContractUpgradeFlow.Instigator`` has been renamed to just ``ContractUpgradeFlow``. * ``ContractUpgradeFlow.Instigator`` has been renamed to just ``ContractUpgradeFlow``.
@ -50,10 +46,6 @@ UNRELEASED
* Names of parties are now stored as a ``X500Name`` rather than a ``String``, to correctly enforce basic structure of the * Names of parties are now stored as a ``X500Name`` rather than a ``String``, to correctly enforce basic structure of the
name. As a result all node legal names must now be structured as X.500 distinguished names. name. As a result all node legal names must now be structured as X.500 distinguished names.
* The Bouncy Castle library ``X509CertificateHolder`` class is now used in place of ``X509Certificate`` in order to
have a consistent class used internally. Conversions to/from ``X509Certificate`` are done as required, but should
be avoided where possible.
* There are major changes to transaction signing in flows: * There are major changes to transaction signing in flows:
* You should use the new ``CollectSignaturesFlow`` and corresponding ``SignTransactionFlow`` which handle most * You should use the new ``CollectSignaturesFlow`` and corresponding ``SignTransactionFlow`` which handle most
@ -78,11 +70,16 @@ UNRELEASED
* The original ``KeyPair`` signing methods have been left on the ``TransactionBuilder`` and ``SignedTransaction``, but * The original ``KeyPair`` signing methods have been left on the ``TransactionBuilder`` and ``SignedTransaction``, but
should only be used as part of unit testing. should only be used as part of unit testing.
* The ``InitiatingFlow`` annotation also has an integer ``version`` property which assigns the initiating flow a version * ``Timestamp`` used for validation/notarization time-range has been renamed to ``TimeWindow``.
number, defaulting to 1 if it's specified. The flow version is included in the flow session request and the counterparty There are now 4 factory methods ``TimeWindow.fromOnly(fromTime: Instant)``,
will only respond and start their own flow if the version number matches to the one they've registered with. At some ``TimeWindow.untilOnly(untilTime: Instant)``, ``TimeWindow.between(fromTime: Instant, untilTime: Instant)`` and
point we will support the ability for a node to have multiple versions of the same flow registered, enabling backwards ``TimeWindow.withTolerance(time: Instant, tolerance: Duration)``.
compatibility of CorDapp flows. Previous constructors ``TimeWindow(fromTime: Instant, untilTime: Instant)`` and
``TimeWindow(time: Instant, tolerance: Duration)`` have been removed.
* The Bouncy Castle library ``X509CertificateHolder`` class is now used in place of ``X509Certificate`` in order to
have a consistent class used internally. Conversions to/from ``X509Certificate`` are done as required, but should
be avoided where possible.
* The certificate hierarchy has been changed in order to allow corda node to sign keys with proper certificate chain. * The certificate hierarchy has been changed in order to allow corda node to sign keys with proper certificate chain.
* The corda node will now be issued a restricted client CA for identity/transaction key signing. * The corda node will now be issued a restricted client CA for identity/transaction key signing.

View File

@ -45,40 +45,7 @@ extensions to be created, or registered at startup. In particular:
jars. These static serving directories will not be available if the jars. These static serving directories will not be available if the
bundled web server is not started. bundled web server is not started.
c. The ``servicePlugins`` property returns a list of classes which will c. The ``customizeSerialization`` function allows classes to be whitelisted
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
``SingletonSerializeAsToken`` which ensures that if any reference to your
service is captured in a flow checkpoint (i.e. serialized by Kryo as
part of Quasar checkpoints, either on the stack or by reference within
your flows) it is stored as a simple token representing your service.
When checkpoints are restored, after a node restart for example,
the latest instance of the service will be substituted back in place of
the token stored in the checkpoint.
i. Firstly, they can call ``PluginServiceHub.registerServiceFlow`` and
register flows that will be initiated locally in response to remote flow
requests.
ii. Second, the service can hold a long lived reference to the
PluginServiceHub and to other private data, so the service can be used
to provide Oracle functionality. This Oracle functionality would
typically be exposed to other nodes by flows which are given a reference
to the service plugin when initiated (as defined by the
``registerServiceFlow`` call). The flow can then call into functions
on the plugin service singleton. Note, care should be taken to not allow
flows to hold references to fields which are not
also ``SingletonSerializeAsToken``, otherwise Quasar suspension in the
``StateMachineManager`` will fail with exceptions. An example oracle can
be seen in ``NodeInterestRates.kt`` in the irs-demo sample.
iii. The final use case for service plugins is that they can spawn threads, or register
to monitor vault updates. This allows them to provide long lived active
functions inside the node, for instance to initiate workflows when
certain conditions are met.
d. The ``customizeSerialization`` function allows classes to be whitelisted
for object serialisation, over and above those tagged with the ``@CordaSerializable`` for object serialisation, over and above those tagged with the ``@CordaSerializable``
annotation. In general the annotation should be preferred. For annotation. In general the annotation should be preferred. For
instance new state types will need to be explicitly registered. This will be called at instance new state types will need to be explicitly registered. This will be called at

View File

@ -12,20 +12,33 @@ App plugins
To create an app plugin you must extend from `CordaPluginRegistry`_. The JavaDoc contains 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: specific details of the implementation, but you can extend the server in the following ways:
1. Service plugins: Register your services (see below). 1. Register your flows and services (see below).
2. Web APIs: You may register your own endpoints under /api/ of the bundled web server. 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. 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 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. of a persisted state, that is used in messaging between flows or in RPC needs to be whitelisted.
Services Flows and services
-------- ------------------
Services are classes which are constructed after the node has started. It is provided a `PluginServiceHub`_ which Flows are of two types: initiating and initiated. Initiating flows need to be annotated with ``@InitiatingFlow`` and can
allows a richer API than the `ServiceHub`_ exposed to contracts. It enables adding flows, registering be started in one of three ways:
message handlers and more. The service does not run in a separate thread, so the only entry point to the service is during
construction, where message handlers should be registered and threads started.
1. By a user of your CorDapp via RPC in which the flow also needs to be annotated with ``@StartableByRPC``.
2. By another CorDapp executing it as a sub-flow in their own flow.
3. By a ``SchedulableState`` activity event, in which the flow also needs to be annotated with ``@SchedulableFlow``
``InitiatingFlow`` also has a ``version`` property to enable you to version your flows. A node will only accept communication
from an initiating party if the version numbers match up.
Initiated flows are typically private to your CorDapp and need to be annotated with ``@InitiatedBy`` which point to
initiating flow Class. The node scans your CorDapps for these annotations and automatically registers the initiating to
initiated mapping for you.
If your CorDapp also needs to have additional services running in the node, such as oracles, then annotate your service
class with ``@CordaService``. As with the flows, the node will automatically register it and make it available for use by
your flows. The service class has to implement ``SerializeAsToken`` to ensure they work correctly within flows. If possible
extend ``SingletonSerializeAsToken`` instead to avoid the boilerplate.
Starting nodes Starting nodes
-------------- --------------

View File

@ -9,9 +9,9 @@ import net.corda.core.contracts.TransactionType
import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.PluginServiceHub
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
import net.corda.core.node.services.unconsumedStates import net.corda.core.node.services.unconsumedStates
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
@ -21,13 +21,6 @@ import net.corda.flows.FinalityFlow
import net.corda.flows.ResolveTransactionsFlow import net.corda.flows.ResolveTransactionsFlow
import java.util.* import java.util.*
object FxTransactionDemoTutorial {
// Would normally be called by custom service init in a CorDapp
fun registerFxProtocols(pluginHub: PluginServiceHub) {
pluginHub.registerServiceFlow(ForeignExchangeFlow::class.java, ::ForeignExchangeRemoteFlow)
}
}
@CordaSerializable @CordaSerializable
private data class FxRequest(val tradeId: String, private data class FxRequest(val tradeId: String,
val amount: Amount<Issued<Currency>>, val amount: Amount<Issued<Currency>>,
@ -212,6 +205,7 @@ class ForeignExchangeFlow(val tradeId: String,
// DOCEND 3 // DOCEND 3
} }
@InitiatedBy(ForeignExchangeFlow::class)
class ForeignExchangeRemoteFlow(val source: Party) : FlowLogic<Unit>() { class ForeignExchangeRemoteFlow(val source: Party) : FlowLogic<Unit>() {
@Suspendable @Suspendable
override fun call() { override fun call() {

View File

@ -6,10 +6,10 @@ import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.containsAny import net.corda.core.crypto.containsAny
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.PluginServiceHub
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
import net.corda.core.node.services.linearHeadsOfType import net.corda.core.node.services.linearHeadsOfType
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
@ -19,22 +19,13 @@ import net.corda.flows.FinalityFlow
import java.security.PublicKey import java.security.PublicKey
import java.time.Duration import java.time.Duration
object WorkflowTransactionBuildTutorial {
// Would normally be called by custom service init in a CorDapp
fun registerWorkflowProtocols(pluginHub: PluginServiceHub) {
pluginHub.registerServiceFlow(SubmitCompletionFlow::class.java, ::RecordCompletionFlow)
}
}
// DOCSTART 1 // DOCSTART 1
// Helper method to locate the latest Vault version of a LinearState from a possibly out of date StateRef // Helper method to locate the latest Vault version of a LinearState from a possibly out of date StateRef
inline fun <reified T : LinearState> ServiceHub.latest(ref: StateRef): StateAndRef<T> { inline fun <reified T : LinearState> ServiceHub.latest(ref: StateRef): StateAndRef<T> {
val linearHeads = vaultService.linearHeadsOfType<T>() val linearHeads = vaultService.linearHeadsOfType<T>()
val original = toStateAndRef<T>(ref) val original = toStateAndRef<T>(ref)
return linearHeads.get(original.state.data.linearId)!! return linearHeads[original.state.data.linearId]!!
} }
// DOCEND 1 // DOCEND 1
// Minimal state model of a manual approval process // Minimal state model of a manual approval process
@ -87,7 +78,7 @@ data class TradeApprovalContract(override val legalContractReference: SecureHash
"Issue of new WorkflowContract must not include any inputs" using (tx.inputs.isEmpty()) "Issue of new WorkflowContract must not include any inputs" using (tx.inputs.isEmpty())
"Issue of new WorkflowContract must be in a unique transaction" using (tx.outputs.size == 1) "Issue of new WorkflowContract must be in a unique transaction" using (tx.outputs.size == 1)
} }
val issued = tx.outputs.get(0) as TradeApprovalContract.State val issued = tx.outputs[0] as TradeApprovalContract.State
requireThat { requireThat {
"Issue requires the source Party as signer" using (command.signers.contains(issued.source.owningKey)) "Issue requires the source Party as signer" using (command.signers.contains(issued.source.owningKey))
"Initial Issue state must be NEW" using (issued.state == WorkflowState.NEW) "Initial Issue state must be NEW" using (issued.state == WorkflowState.NEW)
@ -96,9 +87,9 @@ data class TradeApprovalContract(override val legalContractReference: SecureHash
is Commands.Completed -> { is Commands.Completed -> {
val stateGroups = tx.groupStates(TradeApprovalContract.State::class.java) { it.linearId } val stateGroups = tx.groupStates(TradeApprovalContract.State::class.java) { it.linearId }
require(stateGroups.size == 1) { "Must be only a single proposal in transaction" } require(stateGroups.size == 1) { "Must be only a single proposal in transaction" }
for (group in stateGroups) { for ((inputs, outputs) in stateGroups) {
val before = group.inputs.single() val before = inputs.single()
val after = group.outputs.single() val after = outputs.single()
requireThat { requireThat {
"Only a non-final trade can be modified" using (before.state == WorkflowState.NEW) "Only a non-final trade can be modified" using (before.state == WorkflowState.NEW)
"Output must be a final state" using (after.state in setOf(WorkflowState.APPROVED, WorkflowState.REJECTED)) "Output must be a final state" using (after.state in setOf(WorkflowState.APPROVED, WorkflowState.REJECTED))
@ -227,6 +218,7 @@ class SubmitCompletionFlow(val ref: StateRef, val verdict: WorkflowState) : Flow
* Then after checking to sign it and eventually store the fully notarised * Then after checking to sign it and eventually store the fully notarised
* transaction to the ledger. * transaction to the ledger.
*/ */
@InitiatedBy(SubmitCompletionFlow::class)
class RecordCompletionFlow(val source: Party) : FlowLogic<Unit>() { class RecordCompletionFlow(val source: Party) : FlowLogic<Unit>() {
@Suspendable @Suspendable
override fun call(): Unit { override fun call(): Unit {

View File

@ -29,14 +29,11 @@ class FxTransactionBuildTutorialTest {
val notaryService = ServiceInfo(ValidatingNotaryService.type) val notaryService = ServiceInfo(ValidatingNotaryService.type)
notaryNode = net.createNode( notaryNode = net.createNode(
legalName = DUMMY_NOTARY.name, legalName = DUMMY_NOTARY.name,
overrideServices = mapOf(Pair(notaryService, DUMMY_NOTARY_KEY)), overrideServices = mapOf(notaryService to DUMMY_NOTARY_KEY),
advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), notaryService)) advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), notaryService))
nodeA = net.createPartyNode(notaryNode.info.address) nodeA = net.createPartyNode(notaryNode.info.address)
nodeB = net.createPartyNode(notaryNode.info.address) nodeB = net.createPartyNode(notaryNode.info.address)
FxTransactionDemoTutorial.registerFxProtocols(nodeA.services) nodeB.registerInitiatedFlow(ForeignExchangeRemoteFlow::class.java)
FxTransactionDemoTutorial.registerFxProtocols(nodeB.services)
WorkflowTransactionBuildTutorial.registerWorkflowProtocols(nodeA.services)
WorkflowTransactionBuildTutorial.registerWorkflowProtocols(nodeB.services)
} }
@After @After

View File

@ -4,7 +4,6 @@ import net.corda.core.contracts.LinearState
import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef import net.corda.core.contracts.StateRef
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
import net.corda.core.node.ServiceEntry
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.core.node.services.linearHeadsOfType import net.corda.core.node.services.linearHeadsOfType
@ -30,7 +29,7 @@ class WorkflowTransactionBuildTutorialTest {
private inline fun <reified T : LinearState> ServiceHub.latest(ref: StateRef): StateAndRef<T> { private inline fun <reified T : LinearState> ServiceHub.latest(ref: StateRef): StateAndRef<T> {
val linearHeads = vaultService.linearHeadsOfType<T>() val linearHeads = vaultService.linearHeadsOfType<T>()
val original = storageService.validatedTransactions.getTransaction(ref.txhash)!!.tx.outRef<T>(ref.index) val original = storageService.validatedTransactions.getTransaction(ref.txhash)!!.tx.outRef<T>(ref.index)
return linearHeads.get(original.state.data.linearId)!! return linearHeads[original.state.data.linearId]!!
} }
@Before @Before
@ -43,10 +42,7 @@ class WorkflowTransactionBuildTutorialTest {
advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), notaryService)) advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), notaryService))
nodeA = net.createPartyNode(notaryNode.info.address) nodeA = net.createPartyNode(notaryNode.info.address)
nodeB = net.createPartyNode(notaryNode.info.address) nodeB = net.createPartyNode(notaryNode.info.address)
FxTransactionDemoTutorial.registerFxProtocols(nodeA.services) nodeA.registerInitiatedFlow(RecordCompletionFlow::class.java)
FxTransactionDemoTutorial.registerFxProtocols(nodeB.services)
WorkflowTransactionBuildTutorial.registerWorkflowProtocols(nodeA.services)
WorkflowTransactionBuildTutorial.registerWorkflowProtocols(nodeB.services)
} }
@After @After

View File

@ -419,48 +419,56 @@ sequence of message transfers. Flows end pre-maturely due to exceptions, and as
Taking a step back, we mentioned that the other side has to accept the session request for there to be a communication 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 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 making the request - each session initiation includes the initiating flow type. This registration is done automatically
which has made available the particular flow communication, using ``PluginServiceHub.registerServiceFlow``. This method by the node at startup by searching for flows which are annotated with ``@InitiatedBy``. This annotation points to the
specifies a flow factory for generating the counter-flow to any given initiating flow. If this registration doesn't exist flow that is doing the initiating, and this flow must be annotated with ``@InitiatingFlow``. The ``InitiatedBy`` flow
then no further communication takes place and the initiating flow ends with an exception. The initiating flow has to be must have a constructor which takes in a single parameter of type ``Party`` - this is the initiating party.
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 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. with one side started manually using the ``startFlowDynamic`` RPC and this initiates the flow on the other side. In our
In this case it doesn't matter which flow is the initiator and which is the initiated, which is why neither ``Buyer`` nor case it doesn't matter which flow is the initiator and which is the initiated, which is why neither ``Buyer`` nor ``Seller``
``Seller`` are annotated with ``InitiatingFlow``. For example, if we choose the seller side as the initiator then we need are annotated with ``InitiatedBy`` or ``InitiatingFlow``. If we, for example, choose the seller side as the initiator then
to create a simple seller starter flow that has the annotation we need: we need to create a simple seller starter flow that has the annotation we need:
.. container:: codeset .. container:: codeset
.. sourcecode:: kotlin .. sourcecode:: kotlin
@InitiatingFlow @InitiatingFlow
class SellerStarter(val otherParty: Party, val assetToSell: StateAndRef<OwnableState>, val price: Amount<Currency>) : FlowLogic<SignedTransaction>() { class SellerInitiator(val buyer: Party,
val notary: NodeInfo,
val assetToSell: StateAndRef<OwnableState>,
val price: Amount<Currency>) : FlowLogic<SignedTransaction>() {
@Suspendable @Suspendable
override fun call(): SignedTransaction { override fun call(): SignedTransaction {
val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0] send(buyer, Pair(notary.notaryIdentity, price))
val cpOwnerKey: PublicKey = serviceHub.legalIdentityKey return subFlow(Seller(
return subFlow(TwoPartyTradeFlow.Seller(otherParty, notary, assetToSell, price, cpOwnerKey)) buyer,
notary,
assetToSell,
price,
serviceHub.legalIdentityKey))
} }
} }
The buyer side would then need to register their flow, perhaps with something like: The buyer side would look something like this. Notice the constructor takes in a single ``Party`` object which represents
the seller.
.. container:: codeset .. container:: codeset
.. sourcecode:: kotlin .. sourcecode:: kotlin
val services: PluginServiceHub = TODO() @InitiatedBy(SellerInitiator::class)
services.registerServiceFlow(SellerStarter::class.java) { otherParty -> class BuyerAcceptor(val seller: Party) : FlowLogic<Unit>() {
val notary = services.networkMapCache.notaryNodes[0] @Suspendable
val acceptablePrice = TODO() override fun call() {
val typeToBuy = TODO() val (notary, price) = receive<Pair<Party, Amount<Currency>>>(seller).unwrap {
Buyer(otherParty, notary, acceptablePrice, typeToBuy) require(serviceHub.networkMapCache.isNotary(it.first)) { "${it.first} is not a notary" }
it
}
subFlow(Buyer(seller, notary, price, CommercialPaper.State::class.java))
}
} }
This is telling the buyer node to fire up an instance of ``Buyer`` (the code in the lambda) when the initiating flow
is a seller (``SellerStarter::class.java``).
.. _progress-tracking: .. _progress-tracking:

View File

@ -195,41 +195,37 @@ Here we can see that there are several steps:
exactly our data source. The final step, assuming we have got this far, is to generate a signature for the exactly our data source. The final step, assuming we have got this far, is to generate a signature for the
transaction and return it. transaction and return it.
Binding to the network via a CorDapp plugin Binding to the network
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
.. note:: Before reading any further, we advise that you understand the concept of flows and how to write them and use .. note:: Before reading any further, we advise that you understand the concept of flows and how to write them and use
them. See :doc:`flow-state-machines`. Likewise some understanding of Cordapps, plugins and services will be helpful. them. See :doc:`flow-state-machines`. Likewise some understanding of Cordapps, plugins and services will be helpful.
See :doc:`creating-a-cordapp`. See :doc:`creating-a-cordapp`.
The first step is to create a service to host the oracle on the network. Let's see how that's implemented: The first step is to create the oracle as a service by annotating its class with ``@CordaService``. Let's see how that's
done:
.. literalinclude:: ../../samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt
:language: kotlin
:start-after: DOCSTART 3
:end-before: DOCEND 3
The Corda node scans for any class with this annotation and initialises them. The only requirement is that the class provide
a constructor with a single parameter of type ``PluginServiceHub```. In our example the oracle class has two constructors.
The second is used for testing.
.. literalinclude:: ../../samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt .. literalinclude:: ../../samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt
:language: kotlin :language: kotlin
:start-after: DOCSTART 2 :start-after: DOCSTART 2
:end-before: DOCEND 2 :end-before: DOCEND 2
This may look complicated, but really it's made up of some relatively simple elements (in the order they appear in the code): These two flows leverage the oracle to provide the querying and signing operations. They get reference to the oracle,
which will have already been initialised by the node, using ``ServiceHub.cordappService``. Both flows are annotated with
``@InitiatedBy``. This tells the node which initiating flow (which are discussed in the next section) they are meant to
be executed with.
1. Accept a ``PluginServiceHub`` in the constructor. This is your interface to the Corda node. Providing sub-flows for querying and signing
2. Ensure you extend the abstract class ``SingletonSerializeAsToken`` (see :doc:`corda-plugins`). ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3. Create an instance of your core oracle class that has the ``query`` and ``sign`` methods as discussed above.
4. Register your client sub-flows (in this case both in ``RatesFixFlow``. See the next section) for querying and
signing as initiating your service flows that actually do the querying and signing using your core oracle class instance.
5. Implement your service flows that call your core oracle class instance.
The final step is to register your service with the node via the plugin mechanism. Do this by
implementing a plugin. Don't forget the resources file to register it with the ``ServiceLoader`` framework
(see :doc:`corda-plugins`).
.. sourcecode:: kotlin
class Plugin : CordaPluginRegistry() {
override val servicePlugins: List<Class<*>> = listOf(Service::class.java)
}
Providing client sub-flows for querying and signing
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
We mentioned the client sub-flow briefly above. They are the mechanism that clients, in the form of other flows, will We mentioned the client sub-flow briefly above. They are the mechanism that clients, in the form of other flows, will
interact with your oracle. Typically there will be one for querying and one for signing. Let's take a look at interact with your oracle. Typically there will be one for querying and one for signing. Let's take a look at

View File

@ -6,13 +6,15 @@ Here are release notes for each snapshot release from M9 onwards.
Unreleased Unreleased
---------- ----------
We've added the ability for flows to be versioned by their CorDapp developers. This enables a node to support a particular Writing CorDapps has been made simpler by removing boiler-plate code that was previously required when registering flows.
version of a flow and allows it to reject flow communication with a node which isn't using the same fact. In a future Instead we now make use of classpath scanning to automatically wire-up flows.
release we allow a node to have multiple versions of the same flow running to enable backwards compatibility.
There are major changes to the ``Party`` class as part of confidential identities, and how parties and keys are stored There are major changes to the ``Party`` class as part of confidential identities, and how parties and keys are stored
in transaction state objects. See :doc:`changelog` for full details. in transaction state objects. See :doc:`changelog` for full details.
We've added the ability for flows to be versioned by their CorDapp developers. This enables a node to support a particular
version of a flow and allows it to reject flow communication with a node which isn't using the same fact. In a future
release we allow a node to have multiple versions of the same flow running to enable backwards compatibility.
Milestone 11 Milestone 11
------------ ------------

View File

@ -2,12 +2,8 @@ package net.corda.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.flows.FlowException import net.corda.core.flows.*
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow
import net.corda.core.identity.Party 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.CordaSerializable
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
@ -46,6 +42,7 @@ object IssuerFlow {
* Issuer refers to a Node acting as a Bank Issuer of [FungibleAsset], and processes requests from a [IssuanceRequester] client. * Issuer refers to a Node acting as a Bank Issuer of [FungibleAsset], and processes requests from a [IssuanceRequester] client.
* Returns the generated transaction representing the transfer of the [Issued] [FungibleAsset] to the issue requester. * Returns the generated transaction representing the transfer of the [Issued] [FungibleAsset] to the issue requester.
*/ */
@InitiatedBy(IssuanceRequester::class)
class Issuer(val otherParty: Party) : FlowLogic<SignedTransaction>() { class Issuer(val otherParty: Party) : FlowLogic<SignedTransaction>() {
companion object { companion object {
object AWAITING_REQUEST : ProgressTracker.Step("Awaiting issuance request") object AWAITING_REQUEST : ProgressTracker.Step("Awaiting issuance request")
@ -97,11 +94,5 @@ object IssuerFlow {
// NOTE: CashFlow PayCash calls FinalityFlow which performs a Broadcast (which stores a local copy of the txn to the ledger) // NOTE: CashFlow PayCash calls FinalityFlow which performs a Broadcast (which stores a local copy of the txn to the ledger)
return moveTx return moveTx
} }
class Service(services: PluginServiceHub) {
init {
services.registerServiceFlow(IssuanceRequester::class.java, ::Issuer)
}
}
} }
} }

View File

@ -11,16 +11,17 @@ import net.corda.core.getOrThrow
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.map import net.corda.core.map
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
import net.corda.core.toFuture
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.flows.IssuerFlow.IssuanceRequester import net.corda.flows.IssuerFlow.IssuanceRequester
import net.corda.testing.BOC import net.corda.testing.BOC
import net.corda.testing.MEGA_CORP import net.corda.testing.MEGA_CORP
import net.corda.testing.initiateSingleShotFlow
import net.corda.testing.ledger import net.corda.testing.ledger
import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockNetwork.MockNode import net.corda.testing.node.MockNetwork.MockNode
import org.junit.Test import org.junit.Test
import rx.Observable
import java.util.* import java.util.*
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
@ -73,7 +74,6 @@ class IssuerFlowTest {
@Test @Test
fun `test concurrent issuer flow`() { fun `test concurrent issuer flow`() {
net = MockNetwork(false, true) net = MockNetwork(false, true)
ledger { ledger {
notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name) notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name)
@ -96,18 +96,19 @@ class IssuerFlowTest {
} }
} }
private fun runIssuerAndIssueRequester(issuerNode: MockNode, issueToNode: MockNode, private fun runIssuerAndIssueRequester(issuerNode: MockNode,
issueToNode: MockNode,
amount: Amount<Currency>, amount: Amount<Currency>,
party: Party, ref: OpaqueBytes): RunResult { party: Party,
ref: OpaqueBytes): RunResult {
val issueToPartyAndRef = party.ref(ref) val issueToPartyAndRef = party.ref(ref)
val issuerFuture = issuerNode.initiateSingleShotFlow(IssuerFlow.IssuanceRequester::class) { _ -> val issuerFlows: Observable<IssuerFlow.Issuer> = issuerNode.registerInitiatedFlow(IssuerFlow.Issuer::class.java)
IssuerFlow.Issuer(party) val firstIssuerFiber = issuerFlows.toFuture().map { it.stateMachine }
}.map { it.stateMachine }
val issueRequest = IssuanceRequester(amount, party, issueToPartyAndRef.reference, issuerNode.info.legalIdentity) val issueRequest = IssuanceRequester(amount, party, issueToPartyAndRef.reference, issuerNode.info.legalIdentity)
val issueRequestResultFuture = issueToNode.services.startFlow(issueRequest).resultFuture val issueRequestResultFuture = issueToNode.services.startFlow(issueRequest).resultFuture
return IssuerFlowTest.RunResult(issuerFuture, issueRequestResultFuture) return IssuerFlowTest.RunResult(firstIssuerFiber, issueRequestResultFuture)
} }
private data class RunResult( private data class RunResult(

View File

@ -46,6 +46,14 @@ processResources {
from file("$rootDir/config/dev/log4j2.xml") from file("$rootDir/config/dev/log4j2.xml")
} }
processIntegrationTestResources {
// Build one of the demos so that we can test CorDapp scanning in CordappScanningTest. It doesn't matter which demo
// we use, just make sure the test is updated accordingly.
from(project(':samples:trader-demo').tasks.jar) {
rename 'trader-demo-(.*)', 'trader-demo.jar'
}
}
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in // To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
// build/reports/project/dependencies/index.html for green highlighted parts of the tree. // build/reports/project/dependencies/index.html for green highlighted parts of the tree.
@ -153,7 +161,7 @@ dependencies {
compile "io.requery:requery-kotlin:$requery_version" compile "io.requery:requery-kotlin:$requery_version"
// FastClasspathScanner: classpath scanning // FastClasspathScanner: classpath scanning
compile 'io.github.lukehutch:fast-classpath-scanner:2.0.20' compile 'io.github.lukehutch:fast-classpath-scanner:2.0.21'
// Integration test helpers // Integration test helpers
integrationTestCompile "junit:junit:$junit_version" integrationTestCompile "junit:junit:$junit_version"

View File

@ -0,0 +1,82 @@
package net.corda.node
import co.paralleluniverse.fibers.Suspendable
import com.google.common.util.concurrent.Futures
import net.corda.core.copyToDirectory
import net.corda.core.createDirectories
import net.corda.core.div
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.getOrThrow
import net.corda.core.identity.Party
import net.corda.core.messaging.startFlow
import net.corda.core.utilities.ALICE
import net.corda.core.utilities.BOB
import net.corda.core.utilities.unwrap
import net.corda.node.driver.driver
import net.corda.node.services.startFlowPermission
import net.corda.nodeapi.User
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import java.nio.file.Paths
class CordappScanningTest {
@Test
fun `CorDapp jar in plugins directory is scanned`() {
// If the CorDapp jar does't exist then run the integrationTestClasses gradle task
val cordappJar = Paths.get(javaClass.getResource("/trader-demo.jar").toURI())
driver {
val pluginsDir = (baseDirectory(ALICE.name) / "plugins").createDirectories()
cordappJar.copyToDirectory(pluginsDir)
val user = User("u", "p", emptySet())
val alice = startNode(ALICE.name, rpcUsers = listOf(user)).getOrThrow()
val rpc = alice.rpcClientToNode().start(user.username, user.password)
// If the CorDapp wasn't scanned then SellerFlow won't have been picked up as an RPC flow
assertThat(rpc.proxy.registeredFlows()).contains("net.corda.traderdemo.flow.SellerFlow")
}
}
@Test
fun `empty plugins directory`() {
driver {
val baseDirectory = baseDirectory(ALICE.name)
(baseDirectory / "plugins").createDirectories()
startNode(ALICE.name).getOrThrow()
}
}
@Test
fun `sub-classed initiated flow pointing to the same initiating flow as its super-class`() {
val user = User("u", "p", setOf(startFlowPermission<ReceiveFlow>()))
driver(systemProperties = mapOf("net.corda.node.cordapp.scan.package" to "net.corda.node")) {
val (alice, bob) = Futures.allAsList(
startNode(ALICE.name, rpcUsers = listOf(user)),
startNode(BOB.name)).getOrThrow()
val initiatedFlowClass = alice.rpcClientToNode()
.start(user.username, user.password)
.proxy
.startFlow(::ReceiveFlow, bob.nodeInfo.legalIdentity)
.returnValue
assertThat(initiatedFlowClass.getOrThrow()).isEqualTo(SendSubClassFlow::class.java.name)
}
}
@StartableByRPC
@InitiatingFlow
class ReceiveFlow(val otherParty: Party) : FlowLogic<String>() {
@Suspendable
override fun call(): String = receive<String>(otherParty).unwrap { it }
}
@InitiatedBy(ReceiveFlow::class)
open class SendClassFlow(val otherParty: Party) : FlowLogic<Unit>() {
@Suspendable
override fun call() = send(otherParty, javaClass.name)
}
@InitiatedBy(ReceiveFlow::class)
class SendSubClassFlow(otherParty: Party) : SendClassFlow(otherParty)
}

View File

@ -6,6 +6,7 @@ import net.corda.client.rpc.CordaRPCClient
import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.generateKeyPair
import net.corda.core.crypto.toBase58String import net.corda.core.crypto.toBase58String
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
import net.corda.core.identity.Party import net.corda.core.identity.Party
@ -216,7 +217,7 @@ abstract class MQSecurityTest : NodeBasedTest() {
private fun startBobAndCommunicateWithAlice(): Party { private fun startBobAndCommunicateWithAlice(): Party {
val bob = startNode(BOB.name).getOrThrow() val bob = startNode(BOB.name).getOrThrow()
bob.services.registerServiceFlow(SendFlow::class.java, ::ReceiveFlow) bob.registerInitiatedFlow(ReceiveFlow::class.java)
val bobParty = bob.info.legalIdentity val bobParty = bob.info.legalIdentity
// Perform a protocol exchange to force the peer queue to be created // Perform a protocol exchange to force the peer queue to be created
alice.services.startFlow(SendFlow(bobParty, 0)).resultFuture.getOrThrow() alice.services.startFlow(SendFlow(bobParty, 0)).resultFuture.getOrThrow()
@ -229,6 +230,7 @@ abstract class MQSecurityTest : NodeBasedTest() {
override fun call() = send(otherParty, payload) override fun call() = send(otherParty, payload)
} }
@InitiatedBy(SendFlow::class)
private class ReceiveFlow(val otherParty: Party) : FlowLogic<Any>() { private class ReceiveFlow(val otherParty: Party) : FlowLogic<Any>() {
@Suspendable @Suspendable
override fun call() = receive<Any>(otherParty).unwrap { it } override fun call() = receive<Any>(otherParty).unwrap { it }

View File

@ -7,6 +7,8 @@ import com.google.common.util.concurrent.*
import com.typesafe.config.Config import com.typesafe.config.Config
import com.typesafe.config.ConfigRenderOptions import com.typesafe.config.ConfigRenderOptions
import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCClient
import net.corda.cordform.CordformContext
import net.corda.cordform.CordformNode
import net.corda.core.* import net.corda.core.*
import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.X509Utilities
import net.corda.core.crypto.appendToCommonName import net.corda.core.crypto.appendToCommonName
@ -19,10 +21,6 @@ import net.corda.core.node.services.ServiceType
import net.corda.core.utilities.* import net.corda.core.utilities.*
import net.corda.node.LOGS_DIRECTORY_NAME import net.corda.node.LOGS_DIRECTORY_NAME
import net.corda.node.services.config.* import net.corda.node.services.config.*
import net.corda.node.services.config.ConfigHelper
import net.corda.node.services.config.FullNodeConfiguration
import net.corda.node.services.config.VerifierType
import net.corda.node.services.config.configOf
import net.corda.node.services.network.NetworkMapService import net.corda.node.services.network.NetworkMapService
import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.services.transactions.RaftValidatingNotaryService
import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.node.utilities.ServiceIdentityGenerator
@ -30,8 +28,6 @@ import net.corda.nodeapi.ArtemisMessagingComponent
import net.corda.nodeapi.User import net.corda.nodeapi.User
import net.corda.nodeapi.config.SSLConfiguration import net.corda.nodeapi.config.SSLConfiguration
import net.corda.nodeapi.config.parseAs import net.corda.nodeapi.config.parseAs
import net.corda.cordform.CordformNode
import net.corda.cordform.CordformContext
import net.corda.core.internal.ShutdownHook import net.corda.core.internal.ShutdownHook
import net.corda.core.internal.addShutdownHook import net.corda.core.internal.addShutdownHook
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -39,6 +35,7 @@ import okhttp3.Request
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import org.slf4j.Logger import org.slf4j.Logger
import java.io.File import java.io.File
import java.io.File.pathSeparator
import java.net.* import java.net.*
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
@ -614,7 +611,7 @@ class DriverDSL(
} }
} }
override fun baseDirectory(nodeName: X500Name) = driverDirectory / nodeName.commonName.replace(WHITESPACE, "") override fun baseDirectory(nodeName: X500Name): Path = driverDirectory / nodeName.commonName.replace(WHITESPACE, "")
override fun startDedicatedNetworkMapService(): ListenableFuture<Unit> { override fun startDedicatedNetworkMapService(): ListenableFuture<Unit> {
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
@ -679,6 +676,8 @@ class DriverDSL(
"-javaagent:$quasarJarPath" "-javaagent:$quasarJarPath"
val loggingLevel = if (debugPort == null) "INFO" else "DEBUG" val loggingLevel = if (debugPort == null) "INFO" else "DEBUG"
val pluginsDirectory = nodeConf.baseDirectory / "plugins"
ProcessUtilities.startJavaProcess( ProcessUtilities.startJavaProcess(
className = "net.corda.node.Corda", // cannot directly get class for this, so just use string className = "net.corda.node.Corda", // cannot directly get class for this, so just use string
arguments = listOf( arguments = listOf(
@ -686,6 +685,8 @@ class DriverDSL(
"--logging-level=$loggingLevel", "--logging-level=$loggingLevel",
"--no-local-shell" "--no-local-shell"
), ),
// Like the capsule, include the node's plugin directory
classpath = "${ProcessUtilities.defaultClassPath}$pathSeparator$pluginsDirectory/*",
jdwpPort = debugPort, jdwpPort = debugPort,
extraJvmArguments = extraJvmArguments, extraJvmArguments = extraJvmArguments,
errorLogPath = nodeConf.baseDirectory / LOGS_DIRECTORY_NAME / "error.log", errorLogPath = nodeConf.baseDirectory / LOGS_DIRECTORY_NAME / "error.log",

View File

@ -2,16 +2,15 @@ package net.corda.node.internal
import com.codahale.metrics.MetricRegistry import com.codahale.metrics.MetricRegistry
import com.google.common.annotations.VisibleForTesting import com.google.common.annotations.VisibleForTesting
import com.google.common.collect.MutableClassToInstanceMap
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.MoreExecutors import com.google.common.util.concurrent.MoreExecutors
import com.google.common.util.concurrent.SettableFuture import com.google.common.util.concurrent.SettableFuture
import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner
import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult
import net.corda.core.* import net.corda.core.*
import net.corda.core.crypto.* import net.corda.core.crypto.*
import net.corda.core.flows.FlowInitiator import net.corda.core.flows.*
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.identity.Party
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.RPCOps import net.corda.core.messaging.RPCOps
@ -19,6 +18,7 @@ import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.node.* import net.corda.core.node.*
import net.corda.core.node.services.* import net.corda.core.node.services.*
import net.corda.core.node.services.NetworkMapCache.MapChange import net.corda.core.node.services.NetworkMapCache.MapChange
import net.corda.core.serialization.SerializeAsToken
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
@ -45,7 +45,7 @@ import net.corda.node.services.schema.HibernateObserver
import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.schema.NodeSchemaService
import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.FlowStateMachineImpl
import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.statemachine.StateMachineManager
import net.corda.node.services.statemachine.flowVersion import net.corda.node.services.statemachine.flowVersionAndInitiatingClass
import net.corda.node.services.transactions.* import net.corda.node.services.transactions.*
import net.corda.node.services.vault.CashBalanceAsMetricsObserver import net.corda.node.services.vault.CashBalanceAsMetricsObserver
import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.NodeVaultService
@ -60,9 +60,11 @@ import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.slf4j.Logger import org.slf4j.Logger
import rx.Observable
import java.io.IOException import java.io.IOException
import java.lang.reflect.Modifier.* import java.lang.reflect.Modifier.*
import java.net.URL import java.net.JarURLConnection
import java.net.URI
import java.nio.file.FileAlreadyExistsException import java.nio.file.FileAlreadyExistsException
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
@ -73,6 +75,7 @@ import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.TimeUnit.SECONDS import java.util.concurrent.TimeUnit.SECONDS
import java.util.stream.Collectors.toList
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.reflect.KClass import kotlin.reflect.KClass
import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair
@ -106,7 +109,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
// low-performance prototyping period. // low-performance prototyping period.
protected abstract val serverThread: AffinityExecutor protected abstract val serverThread: AffinityExecutor
protected val serviceFlowFactories = ConcurrentHashMap<Class<*>, ServiceFlowInfo>() private val cordappServices = MutableClassToInstanceMap.create<SerializeAsToken>()
private val flowFactories = ConcurrentHashMap<Class<out FlowLogic<*>>, InitiatedFlowFactory<*>>()
protected val partyKeys = mutableSetOf<KeyPair>() protected val partyKeys = mutableSetOf<KeyPair>()
val services = object : ServiceHubInternal() { val services = object : ServiceHubInternal() {
@ -122,6 +126,12 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
override val schemaService: SchemaService get() = schemas override val schemaService: SchemaService get() = schemas
override val transactionVerifierService: TransactionVerifierService get() = txVerifierService override val transactionVerifierService: TransactionVerifierService get() = txVerifierService
override val auditService: AuditService get() = auditService override val auditService: AuditService get() = auditService
override fun <T : SerializeAsToken> cordaService(type: Class<T>): T {
require(type.isAnnotationPresent(CordaService::class.java)) { "${type.name} is not a Corda service" }
return cordappServices.getInstance(type) ?: throw IllegalArgumentException("Corda service ${type.name} does not exist")
}
override val rpcFlows: List<Class<out FlowLogic<*>>> get() = this@AbstractNode.rpcFlows override val rpcFlows: List<Class<out FlowLogic<*>>> get() = this@AbstractNode.rpcFlows
// Internal only // Internal only
@ -131,17 +141,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
return serverThread.fetchFrom { smm.add(logic, flowInitiator) } return serverThread.fetchFrom { smm.add(logic, flowInitiator) }
} }
override fun registerServiceFlow(initiatingFlowClass: Class<out FlowLogic<*>>, serviceFlowFactory: (Party) -> FlowLogic<*>) { override fun getFlowFactory(initiatingFlowClass: Class<out FlowLogic<*>>): InitiatedFlowFactory<*>? {
require(initiatingFlowClass !in serviceFlowFactories) { return flowFactories[initiatingFlowClass]
"${initiatingFlowClass.name} has already been used to register a service flow"
}
val info = ServiceFlowInfo.CorDapp(initiatingFlowClass.flowVersion, serviceFlowFactory)
log.info("Registering service flow for ${initiatingFlowClass.name}: $info")
serviceFlowFactories[initiatingFlowClass] = info
}
override fun getServiceFlowFactory(clientFlowClass: Class<out FlowLogic<*>>): ServiceFlowInfo? {
return serviceFlowFactories[clientFlowClass]
} }
override fun recordTransactions(txs: Iterable<SignedTransaction>) { override fun recordTransactions(txs: Iterable<SignedTransaction>) {
@ -167,15 +168,11 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
lateinit var scheduler: NodeSchedulerService lateinit var scheduler: NodeSchedulerService
lateinit var schemas: SchemaService lateinit var schemas: SchemaService
lateinit var auditService: AuditService lateinit var auditService: AuditService
val customServices: ArrayList<Any> = ArrayList()
protected val runOnStop: ArrayList<Runnable> = ArrayList() protected val runOnStop: ArrayList<Runnable> = ArrayList()
lateinit var database: Database lateinit var database: Database
protected var dbCloser: Runnable? = null protected var dbCloser: Runnable? = null
private lateinit var rpcFlows: List<Class<out FlowLogic<*>>> private lateinit var rpcFlows: List<Class<out FlowLogic<*>>>
/** Locates and returns a service of the given type if loaded, or throws an exception if not found. */
inline fun <reified T : Any> findService() = customServices.filterIsInstance<T>().single()
var isPreviousCheckpointsPresent = false var isPreviousCheckpointsPresent = false
private set private set
@ -217,7 +214,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
val tokenizableServices = makeServices() val tokenizableServices = makeServices()
smm = StateMachineManager(services, smm = StateMachineManager(services,
listOf(tokenizableServices),
checkpointStorage, checkpointStorage,
serverThread, serverThread,
database, database,
@ -240,22 +236,24 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
startMessagingService(rpcOps) startMessagingService(rpcOps)
installCoreFlows() installCoreFlows()
fun Class<out FlowLogic<*>>.isUserInvokable(): Boolean { val scanResult = scanCorDapps()
return isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || isStatic(modifiers)) if (scanResult != null) {
val cordappServices = installCordaServices(scanResult)
tokenizableServices.addAll(cordappServices)
registerInitiatedFlows(scanResult)
rpcFlows = findRPCFlows(scanResult)
} else {
rpcFlows = emptyList()
} }
val flows = scanForFlows() // TODO Remove this once the cash stuff is in its own CorDapp
rpcFlows = flows.filter { it.isUserInvokable() && it.isAnnotationPresent(StartableByRPC::class.java) } + registerInitiatedFlow(IssuerFlow.Issuer::class.java)
// Add any core flows here
listOf(ContractUpgradeFlow::class.java, initUploaders()
// TODO Remove all Cash flows from default list once they are split into separate CorDapp.
CashIssueFlow::class.java,
CashExitFlow::class.java,
CashPaymentFlow::class.java)
runOnStop += Runnable { net.stop() } runOnStop += Runnable { net.stop() }
_networkMapRegistrationFuture.setFuture(registerWithNetworkMapIfConfigured()) _networkMapRegistrationFuture.setFuture(registerWithNetworkMapIfConfigured())
smm.start() smm.start(tokenizableServices)
// Shut down the SMM so no Fibers are scheduled. // Shut down the SMM so no Fibers are scheduled.
runOnStop += Runnable { smm.stop(acceptableLiveFiberCountOnStop()) } runOnStop += Runnable { smm.stop(acceptableLiveFiberCountOnStop()) }
scheduler.start() scheduler.start()
@ -264,18 +262,142 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
return this return this
} }
private fun installCordaServices(scanResult: ScanResult): List<SerializeAsToken> {
return scanResult.getClassesWithAnnotation(SerializeAsToken::class, CordaService::class).mapNotNull {
try {
installCordaService(it)
} catch (e: NoSuchMethodException) {
log.error("${it.name}, as a Corda service, must have a constructor with a single parameter " +
"of type ${PluginServiceHub::class.java.name}")
null
} catch (e: Exception) {
log.error("Unable to install Corda service ${it.name}", e)
null
}
}
}
/**
* Use this method to install your Corda services in your tests. This is automatically done by the node when it
* starts up for all classes it finds which are annotated with [CordaService].
*/
fun <T : SerializeAsToken> installCordaService(clazz: Class<T>): T {
clazz.requireAnnotation<CordaService>()
val ctor = clazz.getDeclaredConstructor(PluginServiceHub::class.java).apply { isAccessible = true }
val service = ctor.newInstance(services)
cordappServices.putInstance(clazz, service)
log.info("Installed ${clazz.name} Corda service")
return service
}
private inline fun <reified A : Annotation> Class<*>.requireAnnotation(): A {
return requireNotNull(getDeclaredAnnotation(A::class.java)) { "$name needs to be annotated with ${A::class.java.name}" }
}
private fun registerInitiatedFlows(scanResult: ScanResult) {
scanResult
.getClassesWithAnnotation(FlowLogic::class, InitiatedBy::class)
// First group by the initiating flow class in case there are multiple mappings
.groupBy { it.requireAnnotation<InitiatedBy>().value.java }
.map { (initiatingFlow, initiatedFlows) ->
val sorted = initiatedFlows.sortedWith(FlowTypeHierarchyComparator(initiatingFlow))
if (sorted.size > 1) {
log.warn("${initiatingFlow.name} has been specified as the inititating flow by multiple flows " +
"in the same type hierarchy: ${sorted.joinToString { it.name }}. Choosing the most " +
"specific sub-type for registration: ${sorted[0].name}.")
}
sorted[0]
}
.forEach {
try {
registerInitiatedFlowInternal(it, track = false)
} catch (e: NoSuchMethodException) {
log.error("${it.name}, as an initiated flow, must have a constructor with a single parameter " +
"of type ${Party::class.java.name}")
} catch (e: Exception) {
log.error("Unable to register initiated flow ${it.name}", e)
}
}
}
private class FlowTypeHierarchyComparator(val initiatingFlow: Class<out FlowLogic<*>>) : Comparator<Class<out FlowLogic<*>>> {
override fun compare(o1: Class<out FlowLogic<*>>, o2: Class<out FlowLogic<*>>): Int {
return if (o1 == o2) {
0
} else if (o1.isAssignableFrom(o2)) {
1
} else if (o2.isAssignableFrom(o1)) {
-1
} else {
throw IllegalArgumentException("${initiatingFlow.name} has been specified as the initiating flow by " +
"both ${o1.name} and ${o2.name}")
}
}
}
/**
* Use this method to register your initiated flows in your tests. This is automatically done by the node when it
* starts up for all [FlowLogic] classes it finds which are annotated with [InitiatedBy].
* @return An [Observable] of the initiated flows started by counter-parties.
*/
fun <T : FlowLogic<*>> registerInitiatedFlow(initiatedFlowClass: Class<T>): Observable<T> {
return registerInitiatedFlowInternal(initiatedFlowClass, track = true)
}
private fun <F : FlowLogic<*>> registerInitiatedFlowInternal(initiatedFlow: Class<F>, track: Boolean): Observable<F> {
val ctor = initiatedFlow.getDeclaredConstructor(Party::class.java).apply { isAccessible = true }
val initiatingFlow = initiatedFlow.requireAnnotation<InitiatedBy>().value.java
val (version, classWithAnnotation) = initiatingFlow.flowVersionAndInitiatingClass
require(classWithAnnotation == initiatingFlow) {
"${InitiatingFlow::class.java.name} must be annotated on ${initiatingFlow.name} and not on a super-type"
}
val flowFactory = InitiatedFlowFactory.CorDapp(version, { ctor.newInstance(it) })
val observable = registerFlowFactory(initiatingFlow, flowFactory, initiatedFlow, track)
log.info("Registered ${initiatingFlow.name} to initiate ${initiatedFlow.name} (version $version)")
return observable
}
@VisibleForTesting
fun <F : FlowLogic<*>> registerFlowFactory(initiatingFlowClass: Class<out FlowLogic<*>>,
flowFactory: InitiatedFlowFactory<F>,
initiatedFlowClass: Class<F>,
track: Boolean): Observable<F> {
val observable = if (track) {
smm.changes.filter { it is StateMachineManager.Change.Add }.map { it.logic }.ofType(initiatedFlowClass)
} else {
Observable.empty()
}
flowFactories[initiatingFlowClass] = flowFactory
return observable
}
private fun findRPCFlows(scanResult: ScanResult): List<Class<out FlowLogic<*>>> {
fun Class<out FlowLogic<*>>.isUserInvokable(): Boolean {
return isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || isStatic(modifiers))
}
return scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class).filter { it.isUserInvokable() } +
// Add any core flows here
listOf(
ContractUpgradeFlow::class.java,
// TODO Remove all Cash flows from default list once they are split into separate CorDapp.
CashIssueFlow::class.java,
CashExitFlow::class.java,
CashPaymentFlow::class.java)
}
/** /**
* Installs a flow that's core to the Corda platform. Unlike CorDapp flows which are versioned individually using * Installs a flow that's core to the Corda platform. Unlike CorDapp flows which are versioned individually using
* [InitiatingFlow.version], core flows have the same version as the node's platform version. To cater for backwards * [InitiatingFlow.version], core flows have the same version as the node's platform version. To cater for backwards
* compatibility [serviceFlowFactory] provides a second parameter which is the platform version of the initiating party. * compatibility [flowFactory] provides a second parameter which is the platform version of the initiating party.
* @suppress * @suppress
*/ */
@VisibleForTesting @VisibleForTesting
fun installCoreFlow(clientFlowClass: KClass<out FlowLogic<*>>, serviceFlowFactory: (Party, Int) -> FlowLogic<*>) { fun installCoreFlow(clientFlowClass: KClass<out FlowLogic<*>>, flowFactory: (Party, Int) -> FlowLogic<*>) {
require(clientFlowClass.java.flowVersion == 1) { require(clientFlowClass.java.flowVersionAndInitiatingClass.first == 1) {
"${InitiatingFlow::class.java.name}.version not applicable for core flows; their version is the node's platform version" "${InitiatingFlow::class.java.name}.version not applicable for core flows; their version is the node's platform version"
} }
serviceFlowFactories[clientFlowClass.java] = ServiceFlowInfo.Core(serviceFlowFactory) flowFactories[clientFlowClass.java] = InitiatedFlowFactory.Core(flowFactory)
log.debug { "Installed core flow ${clientFlowClass.java.name}" } log.debug { "Installed core flow ${clientFlowClass.java.name}" }
} }
@ -313,61 +435,62 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
val tokenizableServices = mutableListOf(storage, net, vault, keyManagement, identity, platformClock, scheduler) val tokenizableServices = mutableListOf(storage, net, vault, keyManagement, identity, platformClock, scheduler)
makeAdvertisedServices(tokenizableServices) makeAdvertisedServices(tokenizableServices)
customServices.clear()
customServices.addAll(makePluginServices(tokenizableServices))
initUploaders(storageServices)
return tokenizableServices return tokenizableServices
} }
private fun scanForFlows(): List<Class<out FlowLogic<*>>> { private fun scanCorDapps(): ScanResult? {
val scanPackage = System.getProperty("net.corda.node.cordapp.scan.package")
val paths = if (scanPackage != null) {
// This is purely for integration tests so that classes defined in the test can automatically be picked up
check(configuration.devMode) { "Package scanning can only occur in dev mode" }
val resource = scanPackage.replace('.', '/')
javaClass.classLoader.getResources(resource)
.asSequence()
.map {
val uri = if (it.protocol == "jar") {
(it.openConnection() as JarURLConnection).jarFileURL.toURI()
} else {
URI(it.toExternalForm().removeSuffix(resource))
}
Paths.get(uri)
}
.toList()
} else {
val pluginsDir = configuration.baseDirectory / "plugins" val pluginsDir = configuration.baseDirectory / "plugins"
log.info("Scanning plugins in $pluginsDir ...") if (!pluginsDir.exists()) return null
if (!pluginsDir.exists()) return emptyList() pluginsDir.list {
it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.collect(toList())
val pluginJars = pluginsDir.list { }
it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.toArray()
} }
if (pluginJars.isEmpty()) return emptyList() log.info("Scanning CorDapps in $paths")
val scanResult = FastClasspathScanner().overrideClasspath(*pluginJars).scan() // This will only scan the plugin jars and nothing else // This will only scan the plugin jars and nothing else
return if (paths.isNotEmpty()) FastClasspathScanner().overrideClasspath(paths).scan() else null
}
fun loadFlowClass(className: String): Class<out FlowLogic<*>>? { private fun <T : Any> ScanResult.getClassesWithAnnotation(type: KClass<T>, annotation: KClass<out Annotation>): List<Class<out T>> {
fun loadClass(className: String): Class<out T>? {
return try { return try {
// TODO Make sure this is loaded by the correct class loader // TODO Make sure this is loaded by the correct class loader
@Suppress("UNCHECKED_CAST") Class.forName(className, false, javaClass.classLoader).asSubclass(type.java)
Class.forName(className, false, javaClass.classLoader) as Class<out FlowLogic<*>> } catch (e: ClassCastException) {
log.warn("As $className is annotated with ${annotation.qualifiedName} it must be a sub-type of ${type.java.name}")
null
} catch (e: Exception) { } catch (e: Exception) {
log.warn("Unable to load flow class $className", e) log.warn("Unable to load class $className", e)
null null
} }
} }
val flowClasses = scanResult.getNamesOfSubclassesOf(FlowLogic::class.java) return getNamesOfClassesWithAnnotation(annotation.java)
.mapNotNull { loadFlowClass(it) } .mapNotNull { loadClass(it) }
.filterNot { isAbstract(it.modifiers) } .filterNot { isAbstract(it.modifiers) }
fun URL.pluginName(): String {
return try {
Paths.get(toURI()).fileName.toString()
} catch (e: Exception) {
toString()
}
} }
flowClasses.groupBy { private fun initUploaders() {
scanResult.classNameToClassInfo[it.name]!!.classpathElementURLs.first() val uploaders: List<FileUploader> = listOf(storage.attachments as NodeAttachmentService) +
}.forEach { url, classes -> cordappServices.values.filterIsInstance(AcceptsFileUpload::class.java)
log.info("Found flows in plugin ${url.pluginName()}: ${classes.joinToString { it.name }}")
}
return flowClasses
}
private fun initUploaders(storageServices: Pair<TxWritableStorageService, CheckpointStorage>) {
val uploaders: List<FileUploader> = listOf(storageServices.first.attachments as NodeAttachmentService) +
customServices.filterIsInstance(AcceptsFileUpload::class.java)
(storage as StorageServiceImpl).initUploaders(uploaders) (storage as StorageServiceImpl).initUploaders(uploaders)
} }
@ -433,12 +556,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
} }
} }
private fun makePluginServices(tokenizableServices: MutableList<Any>): List<Any> {
val pluginServices = pluginRegistries.flatMap { it.servicePlugins }.map { it.apply(services) }
tokenizableServices.addAll(pluginServices)
return pluginServices
}
/** /**
* Run any tasks that are needed to ensure the node is in a correct state before running start(). * Run any tasks that are needed to ensure the node is in a correct state before running start().
*/ */
@ -658,11 +775,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
} }
} }
sealed class ServiceFlowInfo {
data class Core(val factory: (Party, Int) -> FlowLogic<*>) : ServiceFlowInfo()
data class CorDapp(val version: Int, val factory: (Party) -> FlowLogic<*>) : ServiceFlowInfo()
}
private class KeyStoreWrapper(private val storePath: Path, private val storePassword: String) { private class KeyStoreWrapper(private val storePath: Path, private val storePassword: String) {
private val keyStore = KeyStoreUtilities.loadKeyStore(storePath, storePassword) private val keyStore = KeyStoreUtilities.loadKeyStore(storePath, storePassword)

View File

@ -0,0 +1,27 @@
package net.corda.node.internal
import net.corda.core.flows.FlowLogic
import net.corda.core.identity.Party
import net.corda.node.services.statemachine.SessionInit
interface InitiatedFlowFactory<out F : FlowLogic<*>> {
fun createFlow(platformVersion: Int, otherParty: Party, sessionInit: SessionInit): F
data class Core<out F : FlowLogic<*>>(val factory: (Party, Int) -> F) : InitiatedFlowFactory<F> {
override fun createFlow(platformVersion: Int, otherParty: Party, sessionInit: SessionInit): F {
return factory(otherParty, platformVersion)
}
}
data class CorDapp<out F : FlowLogic<*>>(val version: Int, val factory: (Party) -> F) : InitiatedFlowFactory<F> {
override fun createFlow(platformVersion: Int, otherParty: Party, sessionInit: SessionInit): F {
// TODO Add support for multiple versions of the same flow when CorDapps are loaded in separate class loaders
if (sessionInit.flowVerison == version) return factory(otherParty)
throw SessionRejectException(
"Version not supported",
"Version mismatch - ${sessionInit.initiatingFlowClass} is only registered for version $version")
}
}
}
class SessionRejectException(val rejectMessage: String, val logMessage: String) : Exception()

View File

@ -13,7 +13,7 @@ import net.corda.core.node.services.TxWritableStorageService
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.node.internal.ServiceFlowInfo import net.corda.node.internal.InitiatedFlowFactory
import net.corda.node.services.messaging.MessagingService import net.corda.node.services.messaging.MessagingService
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.FlowStateMachineImpl
@ -47,7 +47,6 @@ interface NetworkMapCacheInternal : NetworkMapCache {
/** For testing where the network map cache is manipulated marks the service as immediately ready. */ /** For testing where the network map cache is manipulated marks the service as immediately ready. */
@VisibleForTesting @VisibleForTesting
fun runWithoutMapService() fun runWithoutMapService()
} }
@CordaSerializable @CordaSerializable
@ -93,7 +92,6 @@ abstract class ServiceHubInternal : PluginServiceHub {
* Starts an already constructed flow. Note that you must be on the server thread to call this method. [FlowInitiator] * Starts an already constructed flow. Note that you must be on the server thread to call this method. [FlowInitiator]
* defaults to [FlowInitiator.RPC] with username "Only For Testing". * defaults to [FlowInitiator.RPC] with username "Only For Testing".
*/ */
// TODO Move it to test utils.
@VisibleForTesting @VisibleForTesting
fun <T> startFlow(logic: FlowLogic<T>): FlowStateMachine<T> = startFlow(logic, FlowInitiator.RPC("Only For Testing")) fun <T> startFlow(logic: FlowLogic<T>): FlowStateMachine<T> = startFlow(logic, FlowInitiator.RPC("Only For Testing"))
@ -103,7 +101,6 @@ abstract class ServiceHubInternal : PluginServiceHub {
*/ */
abstract fun <T> startFlow(logic: FlowLogic<T>, flowInitiator: FlowInitiator): FlowStateMachineImpl<T> abstract fun <T> startFlow(logic: FlowLogic<T>, flowInitiator: FlowInitiator): FlowStateMachineImpl<T>
/** /**
* Will check [logicType] and [args] against a whitelist and if acceptable then construct and initiate the flow. * Will check [logicType] and [args] against a whitelist and if acceptable then construct and initiate the flow.
* Note that you must be on the server thread to call this method. [flowInitiator] points how flow was started, * Note that you must be on the server thread to call this method. [flowInitiator] points how flow was started,
@ -122,5 +119,5 @@ abstract class ServiceHubInternal : PluginServiceHub {
return startFlow(logic, flowInitiator) return startFlow(logic, flowInitiator)
} }
abstract fun getServiceFlowFactory(clientFlowClass: Class<out FlowLogic<*>>): ServiceFlowInfo? abstract fun getFlowFactory(initiatingFlowClass: Class<out FlowLogic<*>>): InitiatedFlowFactory<*>?
} }

View File

@ -8,9 +8,9 @@ import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture import com.google.common.util.concurrent.SettableFuture
import net.corda.core.ErrorOr import net.corda.core.ErrorOr
import net.corda.core.abbreviate import net.corda.core.abbreviate
import net.corda.core.identity.Party
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.flows.* import net.corda.core.flows.*
import net.corda.core.identity.Party
import net.corda.core.random63BitValue import net.corda.core.random63BitValue
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
@ -27,7 +27,6 @@ import org.jetbrains.exposed.sql.Transaction
import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.lang.reflect.Modifier
import java.sql.Connection import java.sql.Connection
import java.sql.SQLException import java.sql.SQLException
import java.util.* import java.util.*
@ -322,9 +321,8 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
logger.trace { "Initiating a new session with $otherParty" } logger.trace { "Initiating a new session with $otherParty" }
val session = FlowSession(sessionFlow, random63BitValue(), null, FlowSessionState.Initiating(otherParty), retryable) val session = FlowSession(sessionFlow, random63BitValue(), null, FlowSessionState.Initiating(otherParty), retryable)
openSessions[Pair(sessionFlow, otherParty)] = session openSessions[Pair(sessionFlow, otherParty)] = session
// We get the top-most concrete class object to cater for the case where the client flow is customised via a sub-class val (version, initiatingFlowClass) = sessionFlow.javaClass.flowVersionAndInitiatingClass
val clientFlowClass = sessionFlow.topConcreteFlowClass val sessionInit = SessionInit(session.ourSessionId, initiatingFlowClass, version, firstPayload)
val sessionInit = SessionInit(session.ourSessionId, clientFlowClass, clientFlowClass.flowVersion, firstPayload)
sendInternal(session, sessionInit) sendInternal(session, sessionInit)
if (waitForConfirmation) { if (waitForConfirmation) {
session.waitForConfirmation() session.waitForConfirmation()
@ -332,15 +330,6 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
return session return session
} }
@Suppress("UNCHECKED_CAST")
private val FlowLogic<*>.topConcreteFlowClass: Class<out FlowLogic<*>> get() {
var current: Class<out FlowLogic<*>> = javaClass
while (!Modifier.isAbstract(current.superclass.modifiers)) {
current = current.superclass as Class<out FlowLogic<*>>
}
return current
}
@Suspendable @Suspendable
private fun <M : ExistingSessionMessage> waitForMessage(receiveRequest: ReceiveRequest<M>): ReceivedSessionMessage<M> { private fun <M : ExistingSessionMessage> waitForMessage(receiveRequest: ReceiveRequest<M>): ReceivedSessionMessage<M> {
return receiveRequest.suspendAndExpectReceive().confirmReceiveType(receiveRequest) return receiveRequest.suspendAndExpectReceive().confirmReceiveType(receiveRequest)
@ -460,10 +449,19 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
} }
} }
val Class<out FlowLogic<*>>.flowVersion: Int get() { @Suppress("UNCHECKED_CAST")
val annotation = requireNotNull(getAnnotation(InitiatingFlow::class.java)) { val Class<out FlowLogic<*>>.flowVersionAndInitiatingClass: Pair<Int, Class<out FlowLogic<*>>> get() {
"$name as the initiating flow must be annotated with ${InitiatingFlow::class.java.name}" var current: Class<*> = this
} var found: Pair<Int, Class<out FlowLogic<*>>>? = null
while (true) {
val annotation = current.getDeclaredAnnotation(InitiatingFlow::class.java)
if (annotation != null) {
if (found != null) throw IllegalArgumentException("${InitiatingFlow::class.java.name} can only be annotated once")
require(annotation.version > 0) { "Flow versions have to be greater or equal to 1" } require(annotation.version > 0) { "Flow versions have to be greater or equal to 1" }
return annotation.version found = annotation.version to (current as Class<out FlowLogic<*>>)
}
current = current.superclass
?: return found
?: throw IllegalArgumentException("$name as an initiating flow must be annotated with ${InitiatingFlow::class.java.name}")
}
} }

View File

@ -15,7 +15,7 @@ import net.corda.core.utilities.UntrustworthyData
interface SessionMessage interface SessionMessage
data class SessionInit(val initiatorSessionId: Long, data class SessionInit(val initiatorSessionId: Long,
val clientFlowClass: Class<out FlowLogic<*>>, val initiatingFlowClass: Class<out FlowLogic<*>>,
val flowVerison: Int, val flowVerison: Int,
val firstPayload: Any?) : SessionMessage val firstPayload: Any?) : SessionMessage

View File

@ -15,14 +15,14 @@ import com.google.common.collect.HashMultimap
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import io.requery.util.CloseableIterator import io.requery.util.CloseableIterator
import net.corda.core.* import net.corda.core.*
import net.corda.core.identity.Party
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.flows.* import net.corda.core.flows.*
import net.corda.core.identity.Party
import net.corda.core.serialization.* import net.corda.core.serialization.*
import net.corda.core.utilities.debug import net.corda.core.utilities.debug
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.trace import net.corda.core.utilities.trace
import net.corda.node.internal.ServiceFlowInfo import net.corda.node.internal.SessionRejectException
import net.corda.node.services.api.Checkpoint import net.corda.node.services.api.Checkpoint
import net.corda.node.services.api.CheckpointStorage import net.corda.node.services.api.CheckpointStorage
import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.api.ServiceHubInternal
@ -61,7 +61,6 @@ import javax.annotation.concurrent.ThreadSafe
*/ */
@ThreadSafe @ThreadSafe
class StateMachineManager(val serviceHub: ServiceHubInternal, class StateMachineManager(val serviceHub: ServiceHubInternal,
tokenizableServices: List<Any>,
val checkpointStorage: CheckpointStorage, val checkpointStorage: CheckpointStorage,
val executor: AffinityExecutor, val executor: AffinityExecutor,
val database: Database, val database: Database,
@ -147,7 +146,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
private val recentlyClosedSessions = ConcurrentHashMap<Long, Party>() private val recentlyClosedSessions = ConcurrentHashMap<Long, Party>()
// Context for tokenized services in checkpoints // Context for tokenized services in checkpoints
private val serializationContext = SerializeAsTokenContext(tokenizableServices, quasarKryoPool, serviceHub) private lateinit var serializationContext: SerializeAsTokenContext
/** Returns a list of all state machines executing the given flow logic at the top level (subflows do not count) */ /** Returns a list of all state machines executing the given flow logic at the top level (subflows do not count) */
fun <P : FlowLogic<T>, T> findStateMachines(flowClass: Class<P>): List<Pair<P, ListenableFuture<T>>> { fun <P : FlowLogic<T>, T> findStateMachines(flowClass: Class<P>): List<Pair<P, ListenableFuture<T>>> {
@ -171,7 +170,8 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
*/ */
val changes: Observable<Change> = mutex.content.changesPublisher.wrapWithDatabaseTransaction() val changes: Observable<Change> = mutex.content.changesPublisher.wrapWithDatabaseTransaction()
fun start() { fun start(tokenizableServices: List<Any>) {
serializationContext = SerializeAsTokenContext(tokenizableServices, quasarKryoPool, serviceHub)
restoreFibersFromCheckpoints() restoreFibersFromCheckpoints()
listenToLedgerTransactions() listenToLedgerTransactions()
serviceHub.networkMapCache.mapServiceRegistered.then(executor) { resumeRestoredFibers() } serviceHub.networkMapCache.mapServiceRegistered.then(executor) { resumeRestoredFibers() }
@ -345,28 +345,15 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
fun sendSessionReject(message: String) = sendSessionMessage(sender, SessionReject(otherPartySessionId, message)) fun sendSessionReject(message: String) = sendSessionMessage(sender, SessionReject(otherPartySessionId, message))
val serviceFlowInfo = serviceHub.getServiceFlowFactory(sessionInit.clientFlowClass) val initiatedFlowFactory = serviceHub.getFlowFactory(sessionInit.initiatingFlowClass)
if (serviceFlowInfo == null) { if (initiatedFlowFactory == null) {
logger.warn("${sessionInit.clientFlowClass} has not been registered with a service flow: $sessionInit") logger.warn("${sessionInit.initiatingFlowClass} has not been registered: $sessionInit")
sendSessionReject("${sessionInit.clientFlowClass.name} has not been registered with a service flow") sendSessionReject("${sessionInit.initiatingFlowClass.name} has not been registered with a service flow")
return return
} }
val session = try { val session = try {
val flow = when (serviceFlowInfo) { val flow = initiatedFlowFactory.createFlow(receivedMessage.platformVersion, sender, sessionInit)
is ServiceFlowInfo.CorDapp -> {
// TODO Add support for multiple versions of the same flow when CorDapps are loaded in separate class loaders
if (sessionInit.flowVerison != serviceFlowInfo.version) {
logger.warn("Version mismatch - ${sessionInit.clientFlowClass} is only registered for version " +
"${serviceFlowInfo.version}: $sessionInit")
sendSessionReject("Version not supported")
return
}
serviceFlowInfo.factory(sender)
}
is ServiceFlowInfo.Core -> serviceFlowInfo.factory(sender, receivedMessage.platformVersion)
}
val fiber = createFiber(flow, FlowInitiator.Peer(sender)) val fiber = createFiber(flow, FlowInitiator.Peer(sender))
val session = FlowSession(flow, random63BitValue(), sender, FlowSessionState.Initiated(sender, otherPartySessionId)) val session = FlowSession(flow, random63BitValue(), sender, FlowSessionState.Initiated(sender, otherPartySessionId))
if (sessionInit.firstPayload != null) { if (sessionInit.firstPayload != null) {
@ -376,6 +363,10 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
fiber.openSessions[Pair(flow, sender)] = session fiber.openSessions[Pair(flow, sender)] = session
updateCheckpoint(fiber) updateCheckpoint(fiber)
session session
} catch (e: SessionRejectException) {
logger.warn("${e.logMessage}: $sessionInit")
sendSessionReject(e.rejectMessage)
return
} catch (e: Exception) { } catch (e: Exception) {
logger.warn("Couldn't start flow session from $sessionInit", e) logger.warn("Couldn't start flow session from $sessionInit", e)
sendSessionReject("Unable to establish session") sendSessionReject("Unable to establish session")
@ -383,7 +374,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
} }
sendSessionMessage(sender, SessionConfirm(otherPartySessionId, session.ourSessionId), session.fiber) sendSessionMessage(sender, SessionConfirm(otherPartySessionId, session.ourSessionId), session.fiber)
session.fiber.logger.debug { "Initiated by $sender using ${sessionInit.clientFlowClass.name}" } session.fiber.logger.debug { "Initiated by $sender using ${sessionInit.initiatingFlowClass.name}" }
session.fiber.logger.trace { "Initiated from $sessionInit on $session" } session.fiber.logger.trace { "Initiated from $sessionInit on $session" }
resumeFiber(session.fiber) resumeFiber(session.fiber)
} }

View File

@ -1,20 +0,0 @@
package net.corda.node.internal
import net.corda.core.createDirectories
import net.corda.core.crypto.commonName
import net.corda.core.div
import net.corda.core.getOrThrow
import net.corda.core.utilities.ALICE
import net.corda.core.utilities.WHITESPACE
import net.corda.testing.node.NodeBasedTest
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
class NodeTest : NodeBasedTest() {
@Test
fun `empty plugins directory`() {
val baseDirectory = baseDirectory(ALICE.name)
(baseDirectory / "plugins").createDirectories()
startNode(ALICE.name).getOrThrow()
}
}

View File

@ -4,24 +4,18 @@ import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.CommercialPaper import net.corda.contracts.CommercialPaper
import net.corda.contracts.asset.* import net.corda.contracts.asset.*
import net.corda.contracts.testing.fillWithSomeTestCash import net.corda.contracts.testing.fillWithSomeTestCash
import net.corda.core.*
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sign import net.corda.core.crypto.sign
import net.corda.core.days import net.corda.core.flows.*
import net.corda.core.flows.FlowLogic import net.corda.core.identity.AbstractParty
import net.corda.core.flows.FlowStateMachine
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StateMachineRunId
import net.corda.core.getOrThrow
import net.corda.core.identity.AnonymousParty import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.identity.AbstractParty
import net.corda.core.map
import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.services.* import net.corda.core.node.services.*
import net.corda.core.rootCause
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
@ -86,9 +80,10 @@ class TwoPartyTradeFlowTests {
net = MockNetwork(false, true) net = MockNetwork(false, true)
ledger { ledger {
val notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name) val basketOfNodes = net.createSomeNodes(2)
val aliceNode = net.createPartyNode(notaryNode.info.address, ALICE.name) val notaryNode = basketOfNodes.notaryNode
val bobNode = net.createPartyNode(notaryNode.info.address, BOB.name) val aliceNode = basketOfNodes.partyNodes[0]
val bobNode = basketOfNodes.partyNodes[1]
aliceNode.disableDBCloseOnStop() aliceNode.disableDBCloseOnStop()
bobNode.disableDBCloseOnStop() bobNode.disableDBCloseOnStop()
@ -137,8 +132,7 @@ class TwoPartyTradeFlowTests {
aliceNode.disableDBCloseOnStop() aliceNode.disableDBCloseOnStop()
bobNode.disableDBCloseOnStop() bobNode.disableDBCloseOnStop()
val cashStates = val cashStates = bobNode.database.transaction {
bobNode.database.transaction {
bobNode.services.fillWithSomeTestCash(2000.DOLLARS, notaryNode.info.notaryIdentity, 3, 3) bobNode.services.fillWithSomeTestCash(2000.DOLLARS, notaryNode.info.notaryIdentity, 3, 3)
} }
@ -239,7 +233,7 @@ class TwoPartyTradeFlowTests {
}, true, BOB.name) }, true, BOB.name)
// Find the future representing the result of this state machine again. // Find the future representing the result of this state machine again.
val bobFuture = bobNode.smm.findStateMachines(Buyer::class.java).single().second val bobFuture = bobNode.smm.findStateMachines(BuyerAcceptor::class.java).single().second
// And off we go again. // And off we go again.
net.runNetwork() net.runNetwork()
@ -489,25 +483,42 @@ class TwoPartyTradeFlowTests {
sellerNode: MockNetwork.MockNode, sellerNode: MockNetwork.MockNode,
buyerNode: MockNetwork.MockNode, buyerNode: MockNetwork.MockNode,
assetToSell: StateAndRef<OwnableState>): RunResult { assetToSell: StateAndRef<OwnableState>): RunResult {
sellerNode.services.identityService.registerIdentity(buyerNode.info.legalIdentity)
buyerNode.services.identityService.registerIdentity(sellerNode.info.legalIdentity)
val buyerFlows: Observable<BuyerAcceptor> = buyerNode.registerInitiatedFlow(BuyerAcceptor::class.java)
val firstBuyerFiber = buyerFlows.toFuture().map { it.stateMachine }
val seller = SellerInitiator(buyerNode.info.legalIdentity, notaryNode.info, assetToSell, 1000.DOLLARS)
val sellerResult = sellerNode.services.startFlow(seller).resultFuture
return RunResult(firstBuyerFiber, sellerResult, seller.stateMachine.id)
}
@InitiatingFlow @InitiatingFlow
class SellerRunnerFlow(val buyer: Party, val notary: NodeInfo) : FlowLogic<SignedTransaction>() { class SellerInitiator(val buyer: Party,
val notary: NodeInfo,
val assetToSell: StateAndRef<OwnableState>,
val price: Amount<Currency>) : FlowLogic<SignedTransaction>() {
@Suspendable @Suspendable
override fun call(): SignedTransaction = subFlow(Seller( override fun call(): SignedTransaction {
send(buyer, Pair(notary.notaryIdentity, price))
return subFlow(Seller(
buyer, buyer,
notary, notary,
assetToSell, assetToSell,
1000.DOLLARS, price,
serviceHub.legalIdentityKey)) serviceHub.legalIdentityKey))
} }
}
sellerNode.services.identityService.registerIdentity(buyerNode.info.legalIdentity) @InitiatedBy(SellerInitiator::class)
buyerNode.services.identityService.registerIdentity(sellerNode.info.legalIdentity) class BuyerAcceptor(val seller: Party) : FlowLogic<SignedTransaction>() {
val buyerFuture = buyerNode.initiateSingleShotFlow(SellerRunnerFlow::class) { otherParty -> @Suspendable
Buyer(otherParty, notaryNode.info.notaryIdentity, 1000.DOLLARS, CommercialPaper.State::class.java) override fun call(): SignedTransaction {
}.map { it.stateMachine } val (notary, price) = receive<Pair<Party, Amount<Currency>>>(seller).unwrap {
val seller = SellerRunnerFlow(buyerNode.info.legalIdentity, notaryNode.info) require(serviceHub.networkMapCache.isNotary(it.first)) { "${it.first} is not a notary" }
val sellerResultFuture = sellerNode.services.startFlow(seller).resultFuture it
return RunResult(buyerFuture, sellerResultFuture, seller.stateMachine.id) }
return subFlow(Buyer(seller, notary, price, CommercialPaper.State::class.java))
}
} }
private fun LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.runWithError( private fun LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.runWithError(

View File

@ -3,11 +3,11 @@ package net.corda.node.services
import com.codahale.metrics.MetricRegistry import com.codahale.metrics.MetricRegistry
import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowInitiator
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.identity.Party
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.services.* import net.corda.core.node.services.*
import net.corda.core.serialization.SerializeAsToken
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.node.internal.ServiceFlowInfo import net.corda.node.internal.InitiatedFlowFactory
import net.corda.node.serialization.NodeClock import net.corda.node.serialization.NodeClock
import net.corda.node.services.api.* import net.corda.node.services.api.*
import net.corda.node.services.messaging.MessagingService import net.corda.node.services.messaging.MessagingService
@ -67,11 +67,11 @@ open class MockServiceHubInternal(
override fun recordTransactions(txs: Iterable<SignedTransaction>) = recordTransactionsInternal(txStorageService, txs) override fun recordTransactions(txs: Iterable<SignedTransaction>) = recordTransactionsInternal(txStorageService, txs)
override fun <T : SerializeAsToken> cordaService(type: Class<T>): T = throw UnsupportedOperationException()
override fun <T> startFlow(logic: FlowLogic<T>, flowInitiator: FlowInitiator): FlowStateMachineImpl<T> { override fun <T> startFlow(logic: FlowLogic<T>, flowInitiator: FlowInitiator): FlowStateMachineImpl<T> {
return smm.executor.fetchFrom { smm.add(logic, flowInitiator) } return smm.executor.fetchFrom { smm.add(logic, flowInitiator) }
} }
override fun registerServiceFlow(initiatingFlowClass: Class<out FlowLogic<*>>, serviceFlowFactory: (Party) -> FlowLogic<*>) = Unit override fun getFlowFactory(initiatingFlowClass: Class<out FlowLogic<*>>): InitiatedFlowFactory<*>? = null
override fun getServiceFlowFactory(clientFlowClass: Class<out FlowLogic<*>>): ServiceFlowInfo? = null
} }

View File

@ -92,13 +92,13 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
} }
scheduler = NodeSchedulerService(services, database, schedulerGatedExecutor) scheduler = NodeSchedulerService(services, database, schedulerGatedExecutor)
smmExecutor = AffinityExecutor.ServiceAffinityExecutor("test", 1) smmExecutor = AffinityExecutor.ServiceAffinityExecutor("test", 1)
val mockSMM = StateMachineManager(services, listOf(services, scheduler), DBCheckpointStorage(), smmExecutor, database) val mockSMM = StateMachineManager(services, DBCheckpointStorage(), smmExecutor, database)
mockSMM.changes.subscribe { change -> mockSMM.changes.subscribe { change ->
if (change is StateMachineManager.Change.Removed && mockSMM.allStateMachines.isEmpty()) { if (change is StateMachineManager.Change.Removed && mockSMM.allStateMachines.isEmpty()) {
smmHasRemovedAllFlows.countDown() smmHasRemovedAllFlows.countDown()
} }
} }
mockSMM.start() mockSMM.start(listOf(services, scheduler))
services.smm = mockSMM services.smm = mockSMM
scheduler.start() scheduler.start()
} }

View File

@ -6,9 +6,10 @@ import net.corda.core.contracts.Amount
import net.corda.core.contracts.Issued import net.corda.core.contracts.Issued
import net.corda.core.contracts.TransactionType import net.corda.core.contracts.TransactionType
import net.corda.core.contracts.USD import net.corda.core.contracts.USD
import net.corda.core.identity.Party
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.identity.Party
import net.corda.core.node.services.unconsumedStates import net.corda.core.node.services.unconsumedStates
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_NOTARY
@ -86,16 +87,20 @@ class DataVendingServiceTests {
} }
private fun MockNode.sendNotifyTx(tx: SignedTransaction, walletServiceNode: MockNode) { private fun MockNode.sendNotifyTx(tx: SignedTransaction, walletServiceNode: MockNode) {
walletServiceNode.registerServiceFlow(clientFlowClass = NotifyTxFlow::class, serviceFlowFactory = ::NotifyTransactionHandler) walletServiceNode.registerInitiatedFlow(InitiateNotifyTxFlow::class.java)
services.startFlow(NotifyTxFlow(walletServiceNode.info.legalIdentity, tx)) services.startFlow(NotifyTxFlow(walletServiceNode.info.legalIdentity, tx))
network.runNetwork() network.runNetwork()
} }
@InitiatingFlow @InitiatingFlow
private class NotifyTxFlow(val otherParty: Party, val stx: SignedTransaction) : FlowLogic<Unit>() { private class NotifyTxFlow(val otherParty: Party, val stx: SignedTransaction) : FlowLogic<Unit>() {
@Suspendable @Suspendable
override fun call() = send(otherParty, NotifyTxRequest(stx)) override fun call() = send(otherParty, NotifyTxRequest(stx))
} }
@InitiatedBy(NotifyTxFlow::class)
private class InitiateNotifyTxFlow(val otherParty: Party) : FlowLogic<Unit>() {
@Suspendable
override fun call() = subFlow(NotifyTransactionHandler(otherParty))
}
} }

View File

@ -7,13 +7,13 @@ import net.corda.contracts.asset.Cash
import net.corda.core.* import net.corda.core.*
import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.DOLLARS
import net.corda.core.contracts.DummyState import net.corda.core.contracts.DummyState
import net.corda.core.identity.Party
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.generateKeyPair
import net.corda.core.flows.FlowException import net.corda.core.flows.FlowException
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSessionException import net.corda.core.flows.FlowSessionException
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.identity.Party
import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.MessageRecipients
import net.corda.core.node.services.PartyInfo import net.corda.core.node.services.PartyInfo
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
@ -30,15 +30,19 @@ import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow import net.corda.flows.CashPaymentFlow
import net.corda.flows.FinalityFlow import net.corda.flows.FinalityFlow
import net.corda.flows.NotaryFlow import net.corda.flows.NotaryFlow
import net.corda.node.internal.InitiatedFlowFactory
import net.corda.node.services.persistence.checkpoints import net.corda.node.services.persistence.checkpoints
import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.node.services.transactions.ValidatingNotaryService
import net.corda.node.utilities.transaction import net.corda.node.utilities.transaction
import net.corda.testing.* import net.corda.testing.expect
import net.corda.testing.expectEvents
import net.corda.testing.getTestX509Name
import net.corda.testing.node.InMemoryMessagingNetwork import net.corda.testing.node.InMemoryMessagingNetwork
import net.corda.testing.node.InMemoryMessagingNetwork.MessageTransfer import net.corda.testing.node.InMemoryMessagingNetwork.MessageTransfer
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockNetwork.MockNode import net.corda.testing.node.MockNetwork.MockNode
import net.corda.testing.sequence
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.Assertions.assertThatThrownBy
import org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType import org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType
@ -110,7 +114,7 @@ class FlowFrameworkTests {
@Test @Test
fun `exception while fiber suspended`() { fun `exception while fiber suspended`() {
node2.registerServiceFlow(ReceiveFlow::class) { SendFlow("Hello", it) } node2.registerFlowFactory(ReceiveFlow::class) { SendFlow("Hello", it) }
val flow = ReceiveFlow(node2.info.legalIdentity) val flow = ReceiveFlow(node2.info.legalIdentity)
val fiber = node1.services.startFlow(flow) as FlowStateMachineImpl val fiber = node1.services.startFlow(flow) as FlowStateMachineImpl
// Before the flow runs change the suspend action to throw an exception // Before the flow runs change the suspend action to throw an exception
@ -129,7 +133,7 @@ class FlowFrameworkTests {
@Test @Test
fun `flow restarted just after receiving payload`() { fun `flow restarted just after receiving payload`() {
node2.registerServiceFlow(SendFlow::class) { ReceiveFlow(it).nonTerminating() } node2.registerFlowFactory(SendFlow::class) { ReceiveFlow(it).nonTerminating() }
node1.services.startFlow(SendFlow("Hello", node2.info.legalIdentity)) node1.services.startFlow(SendFlow("Hello", node2.info.legalIdentity))
// We push through just enough messages to get only the payload sent // We push through just enough messages to get only the payload sent
@ -179,7 +183,7 @@ class FlowFrameworkTests {
@Test @Test
fun `flow loaded from checkpoint will respond to messages from before start`() { fun `flow loaded from checkpoint will respond to messages from before start`() {
node1.registerServiceFlow(ReceiveFlow::class) { SendFlow("Hello", it) } node1.registerFlowFactory(ReceiveFlow::class) { SendFlow("Hello", it) }
node2.services.startFlow(ReceiveFlow(node1.info.legalIdentity).nonTerminating()) // Prepare checkpointed receive flow node2.services.startFlow(ReceiveFlow(node1.info.legalIdentity).nonTerminating()) // Prepare checkpointed receive flow
// Make sure the add() has finished initial processing. // Make sure the add() has finished initial processing.
node2.smm.executor.flush() node2.smm.executor.flush()
@ -198,7 +202,7 @@ class FlowFrameworkTests {
net.messagingNetwork.sentMessages.toSessionTransfers().filter { it.isPayloadTransfer }.forEach { sentCount++ } net.messagingNetwork.sentMessages.toSessionTransfers().filter { it.isPayloadTransfer }.forEach { sentCount++ }
val node3 = net.createNode(node1.info.address) val node3 = net.createNode(node1.info.address)
val secondFlow = node3.initiateSingleShotFlow(PingPongFlow::class) { PingPongFlow(it, payload2) } val secondFlow = node3.registerFlowFactory(PingPongFlow::class) { PingPongFlow(it, payload2) }
net.runNetwork() net.runNetwork()
// Kick off first send and receive // Kick off first send and receive
@ -243,8 +247,8 @@ class FlowFrameworkTests {
fun `sending to multiple parties`() { fun `sending to multiple parties`() {
val node3 = net.createNode(node1.info.address) val node3 = net.createNode(node1.info.address)
net.runNetwork() net.runNetwork()
node2.registerServiceFlow(SendFlow::class) { ReceiveFlow(it).nonTerminating() } node2.registerFlowFactory(SendFlow::class) { ReceiveFlow(it).nonTerminating() }
node3.registerServiceFlow(SendFlow::class) { ReceiveFlow(it).nonTerminating() } node3.registerFlowFactory(SendFlow::class) { ReceiveFlow(it).nonTerminating() }
val payload = "Hello World" val payload = "Hello World"
node1.services.startFlow(SendFlow(payload, node2.info.legalIdentity, node3.info.legalIdentity)) node1.services.startFlow(SendFlow(payload, node2.info.legalIdentity, node3.info.legalIdentity))
net.runNetwork() net.runNetwork()
@ -277,8 +281,8 @@ class FlowFrameworkTests {
net.runNetwork() net.runNetwork()
val node2Payload = "Test 1" val node2Payload = "Test 1"
val node3Payload = "Test 2" val node3Payload = "Test 2"
node2.registerServiceFlow(ReceiveFlow::class) { SendFlow(node2Payload, it) } node2.registerFlowFactory(ReceiveFlow::class) { SendFlow(node2Payload, it) }
node3.registerServiceFlow(ReceiveFlow::class) { SendFlow(node3Payload, it) } node3.registerFlowFactory(ReceiveFlow::class) { SendFlow(node3Payload, it) }
val multiReceiveFlow = ReceiveFlow(node2.info.legalIdentity, node3.info.legalIdentity).nonTerminating() val multiReceiveFlow = ReceiveFlow(node2.info.legalIdentity, node3.info.legalIdentity).nonTerminating()
node1.services.startFlow(multiReceiveFlow) node1.services.startFlow(multiReceiveFlow)
node1.acceptableLiveFiberCountOnStop = 1 node1.acceptableLiveFiberCountOnStop = 1
@ -303,7 +307,7 @@ class FlowFrameworkTests {
@Test @Test
fun `both sides do a send as their first IO request`() { fun `both sides do a send as their first IO request`() {
node2.registerServiceFlow(PingPongFlow::class) { PingPongFlow(it, 20L) } node2.registerFlowFactory(PingPongFlow::class) { PingPongFlow(it, 20L) }
node1.services.startFlow(PingPongFlow(node2.info.legalIdentity, 10L)) node1.services.startFlow(PingPongFlow(node2.info.legalIdentity, 10L))
net.runNetwork() net.runNetwork()
@ -339,7 +343,7 @@ class FlowFrameworkTests {
sessionTransfers.expectEvents(isStrict = false) { sessionTransfers.expectEvents(isStrict = false) {
sequence( sequence(
// First Pay // First Pay
expect(match = { it.message is SessionInit && it.message.clientFlowClass == NotaryFlow.Client::class.java }) { expect(match = { it.message is SessionInit && it.message.initiatingFlowClass == NotaryFlow.Client::class.java }) {
it.message as SessionInit it.message as SessionInit
assertEquals(node1.id, it.from) assertEquals(node1.id, it.from)
assertEquals(notary1Address, it.to) assertEquals(notary1Address, it.to)
@ -349,7 +353,7 @@ class FlowFrameworkTests {
assertEquals(notary1.id, it.from) assertEquals(notary1.id, it.from)
}, },
// Second pay // Second pay
expect(match = { it.message is SessionInit && it.message.clientFlowClass == NotaryFlow.Client::class.java }) { expect(match = { it.message is SessionInit && it.message.initiatingFlowClass == NotaryFlow.Client::class.java }) {
it.message as SessionInit it.message as SessionInit
assertEquals(node1.id, it.from) assertEquals(node1.id, it.from)
assertEquals(notary1Address, it.to) assertEquals(notary1Address, it.to)
@ -359,7 +363,7 @@ class FlowFrameworkTests {
assertEquals(notary2.id, it.from) assertEquals(notary2.id, it.from)
}, },
// Third pay // Third pay
expect(match = { it.message is SessionInit && it.message.clientFlowClass == NotaryFlow.Client::class.java }) { expect(match = { it.message is SessionInit && it.message.initiatingFlowClass == NotaryFlow.Client::class.java }) {
it.message as SessionInit it.message as SessionInit
assertEquals(node1.id, it.from) assertEquals(node1.id, it.from)
assertEquals(notary1Address, it.to) assertEquals(notary1Address, it.to)
@ -374,7 +378,7 @@ class FlowFrameworkTests {
@Test @Test
fun `other side ends before doing expected send`() { fun `other side ends before doing expected send`() {
node2.registerServiceFlow(ReceiveFlow::class) { NoOpFlow() } node2.registerFlowFactory(ReceiveFlow::class) { NoOpFlow() }
val resultFuture = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)).resultFuture val resultFuture = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)).resultFuture
net.runNetwork() net.runNetwork()
assertThatExceptionOfType(FlowSessionException::class.java).isThrownBy { assertThatExceptionOfType(FlowSessionException::class.java).isThrownBy {
@ -384,7 +388,7 @@ class FlowFrameworkTests {
@Test @Test
fun `non-FlowException thrown on other side`() { fun `non-FlowException thrown on other side`() {
val erroringFlowFuture = node2.initiateSingleShotFlow(ReceiveFlow::class) { val erroringFlowFuture = node2.registerFlowFactory(ReceiveFlow::class) {
ExceptionFlow { Exception("evil bug!") } ExceptionFlow { Exception("evil bug!") }
} }
val erroringFlowSteps = erroringFlowFuture.flatMap { it.progressSteps } val erroringFlowSteps = erroringFlowFuture.flatMap { it.progressSteps }
@ -418,7 +422,7 @@ class FlowFrameworkTests {
@Test @Test
fun `FlowException thrown on other side`() { fun `FlowException thrown on other side`() {
val erroringFlow = node2.initiateSingleShotFlow(ReceiveFlow::class) { val erroringFlow = node2.registerFlowFactory(ReceiveFlow::class) {
ExceptionFlow { MyFlowException("Nothing useful") } ExceptionFlow { MyFlowException("Nothing useful") }
} }
val erroringFlowSteps = erroringFlow.flatMap { it.progressSteps } val erroringFlowSteps = erroringFlow.flatMap { it.progressSteps }
@ -456,8 +460,8 @@ class FlowFrameworkTests {
val node3 = net.createNode(node1.info.address) val node3 = net.createNode(node1.info.address)
net.runNetwork() net.runNetwork()
node3.initiateSingleShotFlow(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } } node3.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } }
node2.initiateSingleShotFlow(ReceiveFlow::class) { ReceiveFlow(node3.info.legalIdentity) } node2.registerFlowFactory(ReceiveFlow::class) { ReceiveFlow(node3.info.legalIdentity) }
val receivingFiber = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)) val receivingFiber = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity))
net.runNetwork() net.runNetwork()
assertThatExceptionOfType(MyFlowException::class.java) assertThatExceptionOfType(MyFlowException::class.java)
@ -473,9 +477,9 @@ class FlowFrameworkTests {
// Node 2 will send its payload and then block waiting for the receive from node 1. Meanwhile node 1 will move // Node 2 will send its payload and then block waiting for the receive from node 1. Meanwhile node 1 will move
// onto node 3 which will throw the exception // onto node 3 which will throw the exception
val node2Fiber = node2 val node2Fiber = node2
.initiateSingleShotFlow(ReceiveFlow::class) { SendAndReceiveFlow(it, "Hello") } .registerFlowFactory(ReceiveFlow::class) { SendAndReceiveFlow(it, "Hello") }
.map { it.stateMachine } .map { it.stateMachine }
node3.initiateSingleShotFlow(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } } node3.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } }
val node1Fiber = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity, node3.info.legalIdentity)) as FlowStateMachineImpl val node1Fiber = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity, node3.info.legalIdentity)) as FlowStateMachineImpl
net.runNetwork() net.runNetwork()
@ -528,7 +532,7 @@ class FlowFrameworkTests {
} }
} }
node2.registerServiceFlow(AskForExceptionFlow::class) { ConditionalExceptionFlow(it, "Hello") } node2.registerFlowFactory(AskForExceptionFlow::class) { ConditionalExceptionFlow(it, "Hello") }
val resultFuture = node1.services.startFlow(RetryOnExceptionFlow(node2.info.legalIdentity)).resultFuture val resultFuture = node1.services.startFlow(RetryOnExceptionFlow(node2.info.legalIdentity)).resultFuture
net.runNetwork() net.runNetwork()
assertThat(resultFuture.getOrThrow()).isEqualTo("Hello") assertThat(resultFuture.getOrThrow()).isEqualTo("Hello")
@ -536,7 +540,7 @@ class FlowFrameworkTests {
@Test @Test
fun `serialisation issue in counterparty`() { fun `serialisation issue in counterparty`() {
node2.registerServiceFlow(ReceiveFlow::class) { SendFlow(NonSerialisableData(1), it) } node2.registerFlowFactory(ReceiveFlow::class) { SendFlow(NonSerialisableData(1), it) }
val result = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)).resultFuture val result = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)).resultFuture
net.runNetwork() net.runNetwork()
assertThatExceptionOfType(FlowSessionException::class.java).isThrownBy { assertThatExceptionOfType(FlowSessionException::class.java).isThrownBy {
@ -546,7 +550,7 @@ class FlowFrameworkTests {
@Test @Test
fun `FlowException has non-serialisable object`() { fun `FlowException has non-serialisable object`() {
node2.initiateSingleShotFlow(ReceiveFlow::class) { node2.registerFlowFactory(ReceiveFlow::class) {
ExceptionFlow { NonSerialisableFlowException(NonSerialisableData(1)) } ExceptionFlow { NonSerialisableFlowException(NonSerialisableData(1)) }
} }
val result = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)).resultFuture val result = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)).resultFuture
@ -562,9 +566,9 @@ class FlowFrameworkTests {
ptx.addOutputState(DummyState()) ptx.addOutputState(DummyState())
val stx = node1.services.signInitialTransaction(ptx) val stx = node1.services.signInitialTransaction(ptx)
val committerFiber = node1 val committerFiber = node1.registerFlowFactory(WaitingFlows.Waiter::class) {
.initiateSingleShotFlow(WaitingFlows.Waiter::class) { WaitingFlows.Committer(it) } WaitingFlows.Committer(it)
.map { it.stateMachine } }.map { it.stateMachine }
val waiterStx = node2.services.startFlow(WaitingFlows.Waiter(stx, node1.info.legalIdentity)).resultFuture val waiterStx = node2.services.startFlow(WaitingFlows.Waiter(stx, node1.info.legalIdentity)).resultFuture
net.runNetwork() net.runNetwork()
assertThat(waiterStx.getOrThrow()).isEqualTo(committerFiber.getOrThrow().resultFuture.getOrThrow()) assertThat(waiterStx.getOrThrow()).isEqualTo(committerFiber.getOrThrow().resultFuture.getOrThrow())
@ -576,7 +580,7 @@ class FlowFrameworkTests {
ptx.addOutputState(DummyState()) ptx.addOutputState(DummyState())
val stx = node1.services.signInitialTransaction(ptx) val stx = node1.services.signInitialTransaction(ptx)
node1.registerServiceFlow(WaitingFlows.Waiter::class) { node1.registerFlowFactory(WaitingFlows.Waiter::class) {
WaitingFlows.Committer(it) { throw Exception("Error") } WaitingFlows.Committer(it) { throw Exception("Error") }
} }
val waiter = node2.services.startFlow(WaitingFlows.Waiter(stx, node1.info.legalIdentity)).resultFuture val waiter = node2.services.startFlow(WaitingFlows.Waiter(stx, node1.info.legalIdentity)).resultFuture
@ -594,13 +598,22 @@ class FlowFrameworkTests {
} }
@Test @Test
fun `custom client flow`() { fun `customised client flow`() {
val receiveFlowFuture = node2.initiateSingleShotFlow(SendFlow::class) { ReceiveFlow(it) } val receiveFlowFuture = node2.registerFlowFactory(SendFlow::class) { ReceiveFlow(it) }
node1.services.startFlow(CustomSendFlow("Hello", node2.info.legalIdentity)).resultFuture node1.services.startFlow(CustomSendFlow("Hello", node2.info.legalIdentity)).resultFuture
net.runNetwork() net.runNetwork()
assertThat(receiveFlowFuture.getOrThrow().receivedPayloads).containsOnly("Hello") assertThat(receiveFlowFuture.getOrThrow().receivedPayloads).containsOnly("Hello")
} }
@Test
fun `customised client flow which has annotated @InitiatingFlow again`() {
val result = node1.services.startFlow(IncorrectCustomSendFlow("Hello", node2.info.legalIdentity)).resultFuture
net.runNetwork()
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
result.getOrThrow()
}.withMessageContaining(InitiatingFlow::class.java.simpleName)
}
@Test @Test
fun `upgraded flow`() { fun `upgraded flow`() {
node1.services.startFlow(UpgradedFlow(node2.info.legalIdentity)) node1.services.startFlow(UpgradedFlow(node2.info.legalIdentity))
@ -612,7 +625,11 @@ class FlowFrameworkTests {
@Test @Test
fun `unsupported new flow version`() { fun `unsupported new flow version`() {
node2.registerServiceFlow(UpgradedFlow::class, flowVersion = 1) { SendFlow("Hello", it) } node2.registerFlowFactory(
UpgradedFlow::class.java,
InitiatedFlowFactory.CorDapp(version = 1, factory = ::DoubleInlinedSubFlow),
DoubleInlinedSubFlow::class.java,
track = false)
val result = node1.services.startFlow(UpgradedFlow(node2.info.legalIdentity)).resultFuture val result = node1.services.startFlow(UpgradedFlow(node2.info.legalIdentity)).resultFuture
net.runNetwork() net.runNetwork()
assertThatExceptionOfType(FlowSessionException::class.java).isThrownBy { assertThatExceptionOfType(FlowSessionException::class.java).isThrownBy {
@ -622,7 +639,7 @@ class FlowFrameworkTests {
@Test @Test
fun `single inlined sub-flow`() { fun `single inlined sub-flow`() {
node2.registerServiceFlow(SendAndReceiveFlow::class) { SingleInlinedSubFlow(it) } node2.registerFlowFactory(SendAndReceiveFlow::class, ::SingleInlinedSubFlow)
val result = node1.services.startFlow(SendAndReceiveFlow(node2.info.legalIdentity, "Hello")).resultFuture val result = node1.services.startFlow(SendAndReceiveFlow(node2.info.legalIdentity, "Hello")).resultFuture
net.runNetwork() net.runNetwork()
assertThat(result.getOrThrow()).isEqualTo("HelloHello") assertThat(result.getOrThrow()).isEqualTo("HelloHello")
@ -630,7 +647,7 @@ class FlowFrameworkTests {
@Test @Test
fun `double inlined sub-flow`() { fun `double inlined sub-flow`() {
node2.registerServiceFlow(SendAndReceiveFlow::class) { DoubleInlinedSubFlow(it) } node2.registerFlowFactory(SendAndReceiveFlow::class, ::DoubleInlinedSubFlow)
val result = node1.services.startFlow(SendAndReceiveFlow(node2.info.legalIdentity, "Hello")).resultFuture val result = node1.services.startFlow(SendAndReceiveFlow(node2.info.legalIdentity, "Hello")).resultFuture
net.runNetwork() net.runNetwork()
assertThat(result.getOrThrow()).isEqualTo("HelloHello") assertThat(result.getOrThrow()).isEqualTo("HelloHello")
@ -654,6 +671,18 @@ class FlowFrameworkTests {
return smm.findStateMachines(P::class.java).single() return smm.findStateMachines(P::class.java).single()
} }
private inline fun <reified P : FlowLogic<*>> MockNode.registerFlowFactory(
initiatingFlowClass: KClass<out FlowLogic<*>>,
noinline flowFactory: (Party) -> P): ListenableFuture<P>
{
val observable = registerFlowFactory(initiatingFlowClass.java, object : InitiatedFlowFactory<P> {
override fun createFlow(platformVersion: Int, otherParty: Party, sessionInit: SessionInit): P {
return flowFactory(otherParty)
}
}, P::class.java, track = true)
return observable.toFuture()
}
private fun sessionInit(clientFlowClass: KClass<out FlowLogic<*>>, flowVersion: Int = 1, payload: Any? = null): SessionInit { private fun sessionInit(clientFlowClass: KClass<out FlowLogic<*>>, flowVersion: Int = 1, payload: Any? = null): SessionInit {
return SessionInit(0, clientFlowClass.java, flowVersion, payload) return SessionInit(0, clientFlowClass.java, flowVersion, payload)
} }
@ -730,8 +759,12 @@ class FlowFrameworkTests {
} }
private interface CustomInterface private interface CustomInterface
private class CustomSendFlow(payload: String, otherParty: Party) : CustomInterface, SendFlow(payload, otherParty) private class CustomSendFlow(payload: String, otherParty: Party) : CustomInterface, SendFlow(payload, otherParty)
@InitiatingFlow
private class IncorrectCustomSendFlow(payload: String, otherParty: Party) : CustomInterface, SendFlow(payload, otherParty)
@InitiatingFlow @InitiatingFlow
private class ReceiveFlow(vararg val otherParties: Party) : FlowLogic<Unit>() { private class ReceiveFlow(vararg val otherParties: Party) : FlowLogic<Unit>() {
object START_STEP : ProgressTracker.Step("Starting") object START_STEP : ProgressTracker.Step("Starting")
@ -852,7 +885,7 @@ class FlowFrameworkTests {
} }
private data class NonSerialisableData(val a: Int) private data class NonSerialisableData(val a: Int)
private class NonSerialisableFlowException(val data: NonSerialisableData) : FlowException() private class NonSerialisableFlowException(@Suppress("unused") val data: NonSerialisableData) : FlowException()
//endregion Helpers //endregion Helpers
} }

View File

@ -1,13 +1,10 @@
package net.corda.bank.plugin package net.corda.bank.plugin
import net.corda.bank.api.BankOfCordaWebApi import net.corda.bank.api.BankOfCordaWebApi
import net.corda.core.identity.Party
import net.corda.core.node.CordaPluginRegistry import net.corda.core.node.CordaPluginRegistry
import net.corda.flows.IssuerFlow
import java.util.function.Function import java.util.function.Function
class BankOfCordaPlugin : CordaPluginRegistry() { class BankOfCordaPlugin : CordaPluginRegistry() {
// A list of classes that expose web APIs. // A list of classes that expose web APIs.
override val webApis = listOf(Function(::BankOfCordaWebApi)) override val webApis = listOf(Function(::BankOfCordaWebApi))
override val servicePlugins = listOf(Function(IssuerFlow.Issuer::Service))
} }

View File

@ -33,7 +33,11 @@ class IRSDemoTest : IntegrationTestCategory {
@Test @Test
fun `runs IRS demo`() { fun `runs IRS demo`() {
driver(useTestClock = true, isDebug = true) { driver(
useTestClock = true,
isDebug = true,
systemProperties = mapOf("net.corda.node.cordapp.scan.package" to "net.corda.irs"))
{
val (controller, nodeA, nodeB) = Futures.allAsList( val (controller, nodeA, nodeB) = Futures.allAsList(
startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type), ServiceInfo(NodeInterestRates.type))), startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type), ServiceInfo(NodeInterestRates.type))),
startNode(DUMMY_BANK_A.name, rpcUsers = listOf(rpcUser)), startNode(DUMMY_BANK_A.name, rpcUsers = listOf(rpcUser)),
@ -83,18 +87,22 @@ class IRSDemoTest : IntegrationTestCategory {
} }
private fun runTrade(nodeAddr: HostAndPort) { private fun runTrade(nodeAddr: HostAndPort) {
val fileContents = IOUtils.toString(Thread.currentThread().contextClassLoader.getResourceAsStream("net/corda/irs/simulation/example-irs-trade.json"), Charsets.UTF_8.name()) val fileContents = loadResourceFile("net/corda/irs/simulation/example-irs-trade.json")
val tradeFile = fileContents.replace("tradeXXX", "trade1") val tradeFile = fileContents.replace("tradeXXX", "trade1")
val url = URL("http://$nodeAddr/api/irs/deals") val url = URL("http://$nodeAddr/api/irs/deals")
assertThat(postJson(url, tradeFile)).isTrue() assertThat(postJson(url, tradeFile)).isTrue()
} }
private fun runUploadRates(host: HostAndPort) { private fun runUploadRates(host: HostAndPort) {
val fileContents = IOUtils.toString(Thread.currentThread().contextClassLoader.getResourceAsStream("net/corda/irs/simulation/example.rates.txt"), Charsets.UTF_8.name()) val fileContents = loadResourceFile("net/corda/irs/simulation/example.rates.txt")
val url = URL("http://$host/upload/interest-rates") val url = URL("http://$host/upload/interest-rates")
assertThat(uploadFile(url, fileContents)).isTrue() assertThat(uploadFile(url, fileContents)).isTrue()
} }
private fun loadResourceFile(filename: String): String {
return IOUtils.toString(Thread.currentThread().contextClassLoader.getResourceAsStream(filename), Charsets.UTF_8.name())
}
private fun getTradeCount(nodeAddr: HostAndPort): Int { private fun getTradeCount(nodeAddr: HostAndPort): Int {
val api = HttpApi.fromHostAndPort(nodeAddr, "api/irs") val api = HttpApi.fromHostAndPort(nodeAddr, "api/irs")
val deals = api.getJson<Array<*>>("deals") val deals = api.getJson<Array<*>>("deals")

View File

@ -6,15 +6,15 @@ import net.corda.core.contracts.*
import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.MerkleTreeException import net.corda.core.crypto.MerkleTreeException
import net.corda.core.crypto.keys import net.corda.core.crypto.keys
import net.corda.core.crypto.sign
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.math.CubicSplineInterpolator import net.corda.core.math.CubicSplineInterpolator
import net.corda.core.math.Interpolator import net.corda.core.math.Interpolator
import net.corda.core.math.InterpolatorFactory import net.corda.core.math.InterpolatorFactory
import net.corda.core.node.CordaPluginRegistry
import net.corda.core.node.PluginServiceHub import net.corda.core.node.PluginServiceHub
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
import net.corda.core.node.services.CordaService
import net.corda.core.node.services.ServiceType import net.corda.core.node.services.ServiceType
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.transactions.FilteredTransaction import net.corda.core.transactions.FilteredTransaction
@ -30,14 +30,10 @@ import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.statements.InsertStatement import org.jetbrains.exposed.sql.statements.InsertStatement
import java.io.InputStream import java.io.InputStream
import java.math.BigDecimal import java.math.BigDecimal
import java.security.KeyPair
import java.time.Clock
import java.security.PublicKey import java.security.PublicKey
import java.time.Duration
import java.time.Instant import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
import java.util.* import java.util.*
import java.util.function.Function
import javax.annotation.concurrent.ThreadSafe import javax.annotation.concurrent.ThreadSafe
import kotlin.collections.component1 import kotlin.collections.component1
import kotlin.collections.component2 import kotlin.collections.component2
@ -55,83 +51,50 @@ import kotlin.collections.set
object NodeInterestRates { object NodeInterestRates {
val type = ServiceType.corda.getSubType("interest_rates") val type = ServiceType.corda.getSubType("interest_rates")
/**
* Register the flow that is used with the Fixing integration tests.
*/
class Plugin : CordaPluginRegistry() {
override val servicePlugins = listOf(Function(::Service))
}
/**
* The Service that wraps [Oracle] and handles messages/network interaction/request scrubbing.
*/
// DOCSTART 2 // DOCSTART 2
class Service(val services: PluginServiceHub) : AcceptsFileUpload, SingletonSerializeAsToken() { @InitiatedBy(RatesFixFlow.FixSignFlow::class)
val oracle: Oracle by lazy { class FixSignHandler(val otherParty: Party) : FlowLogic<Unit>() {
val myNodeInfo = services.myInfo
val myIdentity = myNodeInfo.serviceIdentities(type).first()
val mySigningKey = myIdentity.owningKey.keys.first { services.keyManagementService.keys.contains(it) }
Oracle(myIdentity, mySigningKey, services)
}
init {
// Note: access to the singleton oracle property is via the registered SingletonSerializeAsToken Service.
// Otherwise the Kryo serialisation of the call stack in the Quasar Fiber extends to include
// the framework Oracle and the flow will crash.
services.registerServiceFlow(RatesFixFlow.FixSignFlow::class.java) { FixSignHandler(it, this) }
services.registerServiceFlow(RatesFixFlow.FixQueryFlow::class.java) { FixQueryHandler(it, this) }
}
private class FixSignHandler(val otherParty: Party, val service: Service) : FlowLogic<Unit>() {
@Suspendable @Suspendable
override fun call() { override fun call() {
val request = receive<RatesFixFlow.SignRequest>(otherParty).unwrap { it } val request = receive<RatesFixFlow.SignRequest>(otherParty).unwrap { it }
send(otherParty, service.oracle.sign(request.ftx)) send(otherParty, serviceHub.cordaService(Oracle::class.java).sign(request.ftx))
} }
} }
private class FixQueryHandler(val otherParty: Party, val service: Service) : FlowLogic<Unit>() { @InitiatedBy(RatesFixFlow.FixQueryFlow::class)
companion object { class FixQueryHandler(val otherParty: Party) : FlowLogic<Unit>() {
object RECEIVED : ProgressTracker.Step("Received fix request") object RECEIVED : ProgressTracker.Step("Received fix request")
object SENDING : ProgressTracker.Step("Sending fix response") object SENDING : ProgressTracker.Step("Sending fix response")
}
override val progressTracker = ProgressTracker(RECEIVED, SENDING) override val progressTracker = ProgressTracker(RECEIVED, SENDING)
init {
progressTracker.currentStep = RECEIVED
}
@Suspendable @Suspendable
override fun call(): Unit { override fun call(): Unit {
val request = receive<RatesFixFlow.QueryRequest>(otherParty).unwrap { it } val request = receive<RatesFixFlow.QueryRequest>(otherParty).unwrap { it }
val answers = service.oracle.query(request.queries, request.deadline) progressTracker.currentStep = RECEIVED
val answers = serviceHub.cordaService(Oracle::class.java).query(request.queries, request.deadline)
progressTracker.currentStep = SENDING progressTracker.currentStep = SENDING
send(otherParty, answers) send(otherParty, answers)
} }
} }
// DOCEND 2 // DOCEND 2
// File upload support
override val dataTypePrefix = "interest-rates"
override val acceptableFileExtensions = listOf(".rates", ".txt")
override fun upload(file: InputStream): String {
val fixes = parseFile(file.bufferedReader().readText())
oracle.knownFixes = fixes
val msg = "Interest rates oracle accepted ${fixes.size} new interest rate fixes"
println(msg)
return msg
}
}
/** /**
* An implementation of an interest rate fix oracle which is given data in a simple string format. * An implementation of an interest rate fix oracle which is given data in a simple string format.
* *
* The oracle will try to interpolate the missing value of a tenor for the given fix name and date. * The oracle will try to interpolate the missing value of a tenor for the given fix name and date.
*/ */
@ThreadSafe @ThreadSafe
class Oracle(val identity: Party, private val signingKey: PublicKey, val services: ServiceHub) { // DOCSTART 3
@CordaService
class Oracle(val identity: Party, private val signingKey: PublicKey, val services: ServiceHub) : AcceptsFileUpload, SingletonSerializeAsToken() {
constructor(services: PluginServiceHub) : this(
services.myInfo.serviceIdentities(type).first(),
services.myInfo.serviceIdentities(type).first().owningKey.keys.first { services.keyManagementService.keys.contains(it) },
services
)
// DOCEND 3
private object Table : JDBCHashedTable("demo_interest_rate_fixes") { private object Table : JDBCHashedTable("demo_interest_rate_fixes") {
val name = varchar("index_name", length = 255) val name = varchar("index_name", length = 255)
val forDay = localDate("for_day") val forDay = localDate("for_day")
@ -175,7 +138,7 @@ object NodeInterestRates {
/** /**
* This method will now wait until the given deadline if the fix for the given [FixOf] is not immediately * This method will now wait until the given deadline if the fix for the given [FixOf] is not immediately
* available. To implement this, [readWithDeadline] will loop if the deadline is not reached and we throw * available. To implement this, [FiberBox.readWithDeadline] will loop if the deadline is not reached and we throw
* [UnknownFix] as it implements [RetryableException] which has special meaning to this function. * [UnknownFix] as it implements [RetryableException] which has special meaning to this function.
*/ */
@Suspendable @Suspendable
@ -231,6 +194,16 @@ object NodeInterestRates {
return DigitalSignature.LegallyIdentifiable(identity, signature.bytes) return DigitalSignature.LegallyIdentifiable(identity, signature.bytes)
} }
// DOCEND 1 // DOCEND 1
// File upload support
override val dataTypePrefix = "interest-rates"
override val acceptableFileExtensions = listOf(".rates", ".txt")
override fun upload(file: InputStream): String {
val fixes = parseFile(file.bufferedReader().readText())
knownFixes = fixes
return "Interest rates oracle accepted ${fixes.size} new interest rate fixes"
}
} }
// TODO: can we split into two? Fix not available (retryable/transient) and unknown (permanent) // TODO: can we split into two? Fix not available (retryable/transient) and unknown (permanent)

View File

@ -3,19 +3,17 @@ package net.corda.irs.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.DealState import net.corda.core.contracts.DealState
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.node.CordaPluginRegistry import net.corda.core.identity.Party
import net.corda.core.node.PluginServiceHub
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import net.corda.flows.TwoPartyDealFlow import net.corda.flows.TwoPartyDealFlow
import net.corda.flows.TwoPartyDealFlow.Acceptor import net.corda.flows.TwoPartyDealFlow.Acceptor
import net.corda.flows.TwoPartyDealFlow.AutoOffer import net.corda.flows.TwoPartyDealFlow.AutoOffer
import net.corda.flows.TwoPartyDealFlow.Instigator import net.corda.flows.TwoPartyDealFlow.Instigator
import java.util.function.Function
/** /**
* This whole class is really part of a demo just to initiate the agreement of a deal with a simple * This whole class is really part of a demo just to initiate the agreement of a deal with a simple
@ -25,18 +23,6 @@ import java.util.function.Function
* or the flow would have to reach out to external systems (or users) to verify the deals. * or the flow would have to reach out to external systems (or users) to verify the deals.
*/ */
object AutoOfferFlow { object AutoOfferFlow {
class Plugin : CordaPluginRegistry() {
override val servicePlugins = listOf(Function(::Service))
}
class Service(services: PluginServiceHub) : SingletonSerializeAsToken() {
init {
services.registerServiceFlow(Requester::class.java) { Acceptor(it) }
}
}
@InitiatingFlow @InitiatingFlow
@StartableByRPC @StartableByRPC
class Requester(val dealToBeOffered: DealState) : FlowLogic<SignedTransaction>() { class Requester(val dealToBeOffered: DealState) : FlowLogic<SignedTransaction>() {
@ -81,4 +67,7 @@ object AutoOfferFlow {
return parties.filter { serviceHub.myInfo.legalIdentity != it } return parties.filter { serviceHub.myInfo.legalIdentity != it }
} }
} }
@InitiatedBy(Requester::class)
class AutoOfferAcceptor(otherParty: Party) : Acceptor(otherParty)
} }

View File

@ -5,11 +5,11 @@ import net.corda.core.TransientProperty
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.toBase58String import net.corda.core.crypto.toBase58String
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.SchedulableFlow import net.corda.core.flows.SchedulableFlow
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.PluginServiceHub
import net.corda.core.node.services.ServiceType import net.corda.core.node.services.ServiceType
import net.corda.core.seconds import net.corda.core.seconds
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
@ -22,13 +22,6 @@ import java.math.BigDecimal
import java.security.PublicKey import java.security.PublicKey
object FixingFlow { object FixingFlow {
class Service(services: PluginServiceHub) {
init {
services.registerServiceFlow(FixingRoleDecider::class.java) { Fixer(it) }
}
}
/** /**
* One side of the fixing flow for an interest rate swap, but could easily be generalised further. * One side of the fixing flow for an interest rate swap, but could easily be generalised further.
* *
@ -36,8 +29,9 @@ object FixingFlow {
* of the flow that is run by the party with the fixed leg of swap deal, which is the basis for deciding * of the flow that is run by the party with the fixed leg of swap deal, which is the basis for deciding
* who does what in the flow. * who does what in the flow.
*/ */
class Fixer(override val otherParty: Party, @InitiatedBy(FixingRoleDecider::class)
override val progressTracker: ProgressTracker = TwoPartyDealFlow.Secondary.tracker()) : TwoPartyDealFlow.Secondary<FixingSession>() { class Fixer(override val otherParty: Party) : TwoPartyDealFlow.Secondary<FixingSession>() {
override val progressTracker: ProgressTracker = TwoPartyDealFlow.Secondary.tracker()
private lateinit var txState: TransactionState<*> private lateinit var txState: TransactionState<*>
private lateinit var deal: FixableDealState private lateinit var deal: FixableDealState

View File

@ -2,19 +2,17 @@ package net.corda.irs.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.CordaPluginRegistry
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.PluginServiceHub
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.unwrap import net.corda.core.utilities.unwrap
import net.corda.node.utilities.TestClock import net.corda.node.utilities.TestClock
import net.corda.testing.node.MockNetworkMapCache import net.corda.testing.node.MockNetworkMapCache
import java.time.LocalDate import java.time.LocalDate
import java.util.function.Function
/** /**
* This is a less temporary, demo-oriented way of initiating processing of temporal events. * This is a less temporary, demo-oriented way of initiating processing of temporal events.
@ -26,16 +24,7 @@ object UpdateBusinessDayFlow {
@CordaSerializable @CordaSerializable
data class UpdateBusinessDayMessage(val date: LocalDate) data class UpdateBusinessDayMessage(val date: LocalDate)
class Plugin : CordaPluginRegistry() { @InitiatedBy(Broadcast::class)
override val servicePlugins = listOf(Function(::Service))
}
class Service(services: PluginServiceHub) {
init {
services.registerServiceFlow(Broadcast::class.java, ::UpdateBusinessDayHandler)
}
}
private class UpdateBusinessDayHandler(val otherParty: Party) : FlowLogic<Unit>() { private class UpdateBusinessDayHandler(val otherParty: Party) : FlowLogic<Unit>() {
override fun call() { override fun call() {
val message = receive<UpdateBusinessDayMessage>(otherParty).unwrap { it } val message = receive<UpdateBusinessDayMessage>(otherParty).unwrap { it }

View File

@ -2,7 +2,6 @@ package net.corda.irs.plugin
import net.corda.core.node.CordaPluginRegistry import net.corda.core.node.CordaPluginRegistry
import net.corda.irs.api.InterestRateSwapAPI import net.corda.irs.api.InterestRateSwapAPI
import net.corda.irs.flows.FixingFlow
import java.util.function.Function import java.util.function.Function
class IRSPlugin : CordaPluginRegistry() { class IRSPlugin : CordaPluginRegistry() {
@ -10,5 +9,4 @@ class IRSPlugin : CordaPluginRegistry() {
override val staticServeDirs: Map<String, String> = mapOf( override val staticServeDirs: Map<String, String> = mapOf(
"irsdemo" to javaClass.classLoader.getResource("irsweb").toExternalForm() "irsdemo" to javaClass.classLoader.getResource("irsweb").toExternalForm()
) )
override val servicePlugins = listOf(Function(FixingFlow::Service))
} }

View File

@ -7,27 +7,26 @@ import com.google.common.util.concurrent.FutureCallback
import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture import com.google.common.util.concurrent.SettableFuture
import net.corda.core.RunOnCallerThread import net.corda.core.*
import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.UniqueIdentifier import net.corda.core.contracts.UniqueIdentifier
import net.corda.core.flatMap
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowStateMachine import net.corda.core.flows.FlowStateMachine
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.map
import net.corda.core.node.services.linearHeadsOfType import net.corda.core.node.services.linearHeadsOfType
import net.corda.core.success
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.flows.TwoPartyDealFlow.Acceptor import net.corda.flows.TwoPartyDealFlow.Acceptor
import net.corda.flows.TwoPartyDealFlow.AutoOffer import net.corda.flows.TwoPartyDealFlow.AutoOffer
import net.corda.flows.TwoPartyDealFlow.Instigator import net.corda.flows.TwoPartyDealFlow.Instigator
import net.corda.irs.contract.InterestRateSwap import net.corda.irs.contract.InterestRateSwap
import net.corda.irs.flows.FixingFlow
import net.corda.jackson.JacksonSupport import net.corda.jackson.JacksonSupport
import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.identity.InMemoryIdentityService
import net.corda.node.utilities.transaction import net.corda.node.utilities.transaction
import net.corda.testing.initiateSingleShotFlow
import net.corda.testing.node.InMemoryMessagingNetwork import net.corda.testing.node.InMemoryMessagingNetwork
import rx.Observable
import java.security.PublicKey import java.security.PublicKey
import java.time.LocalDate import java.time.LocalDate
import java.util.* import java.util.*
@ -126,6 +125,9 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten
irs.fixedLeg.fixedRatePayer = node1.info.legalIdentity irs.fixedLeg.fixedRatePayer = node1.info.legalIdentity
irs.floatingLeg.floatingRatePayer = node2.info.legalIdentity irs.floatingLeg.floatingRatePayer = node2.info.legalIdentity
node1.registerInitiatedFlow(FixingFlow.Fixer::class.java)
node2.registerInitiatedFlow(FixingFlow.Fixer::class.java)
@InitiatingFlow @InitiatingFlow
class StartDealFlow(val otherParty: Party, class StartDealFlow(val otherParty: Party,
val payload: AutoOffer, val payload: AutoOffer,
@ -134,8 +136,13 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten
override fun call(): SignedTransaction = subFlow(Instigator(otherParty, payload, myKey)) override fun call(): SignedTransaction = subFlow(Instigator(otherParty, payload, myKey))
} }
@InitiatedBy(StartDealFlow::class)
class AcceptDealFlow(otherParty: Party) : Acceptor(otherParty)
val acceptDealFlows: Observable<AcceptDealFlow> = node2.registerInitiatedFlow(AcceptDealFlow::class.java)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val acceptorTx = node2.initiateSingleShotFlow(StartDealFlow::class) { Acceptor(it) }.flatMap { val acceptorTxFuture = acceptDealFlows.toFuture().flatMap {
(it.stateMachine as FlowStateMachine<SignedTransaction>).resultFuture (it.stateMachine as FlowStateMachine<SignedTransaction>).resultFuture
} }
@ -146,9 +153,9 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten
node2.info.legalIdentity, node2.info.legalIdentity,
AutoOffer(notary.info.notaryIdentity, irs), AutoOffer(notary.info.notaryIdentity, irs),
node1.services.legalIdentityKey) node1.services.legalIdentityKey)
val instigatorTx = node1.services.startFlow(instigator).resultFuture val instigatorTxFuture = node1.services.startFlow(instigator).resultFuture
return Futures.allAsList(instigatorTx, acceptorTx).flatMap { instigatorTx } return Futures.allAsList(instigatorTxFuture, acceptorTxFuture).flatMap { instigatorTxFuture }
} }
override fun iterate(): InMemoryMessagingNetwork.MessageTransfer? { override fun iterate(): InMemoryMessagingNetwork.MessageTransfer? {

View File

@ -126,9 +126,11 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean,
return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) {
override fun start(): MockNetwork.MockNode { override fun start(): MockNetwork.MockNode {
super.start() super.start()
registerInitiatedFlow(NodeInterestRates.FixQueryHandler::class.java)
registerInitiatedFlow(NodeInterestRates.FixSignHandler::class.java)
javaClass.classLoader.getResourceAsStream("net/corda/irs/simulation/example.rates.txt").use { javaClass.classLoader.getResourceAsStream("net/corda/irs/simulation/example.rates.txt").use {
database.transaction { database.transaction {
findService<NodeInterestRates.Service>().upload(it) installCordaService(NodeInterestRates.Oracle::class.java).upload(it)
} }
} }
return this return this

View File

@ -1,5 +1,2 @@
# Register a ServiceLoader service extending from net.corda.core.node.CordaPluginRegistry # Register a ServiceLoader service extending from net.corda.core.node.CordaPluginRegistry
net.corda.irs.plugin.IRSPlugin net.corda.irs.plugin.IRSPlugin
net.corda.irs.api.NodeInterestRates$Plugin
net.corda.irs.flows.AutoOfferFlow$Plugin
net.corda.irs.flows.UpdateBusinessDayFlow$Plugin

View File

@ -210,8 +210,10 @@ class NodeInterestRatesTest {
val net = MockNetwork() val net = MockNetwork()
val n1 = net.createNotaryNode() val n1 = net.createNotaryNode()
val n2 = net.createNode(n1.info.address, advertisedServices = ServiceInfo(NodeInterestRates.type)) val n2 = net.createNode(n1.info.address, advertisedServices = ServiceInfo(NodeInterestRates.type))
n2.registerInitiatedFlow(NodeInterestRates.FixQueryHandler::class.java)
n2.registerInitiatedFlow(NodeInterestRates.FixSignHandler::class.java)
n2.database.transaction { n2.database.transaction {
n2.findService<NodeInterestRates.Service>().oracle.knownFixes = TEST_DATA n2.installCordaService(NodeInterestRates.Oracle::class.java).knownFixes = TEST_DATA
} }
val tx = TransactionType.General.Builder(null) val tx = TransactionType.General.Builder(null)
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M") val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
@ -233,7 +235,8 @@ class NodeInterestRatesTest {
fixOf: FixOf, fixOf: FixOf,
expectedRate: BigDecimal, expectedRate: BigDecimal,
rateTolerance: BigDecimal, rateTolerance: BigDecimal,
progressTracker: ProgressTracker = RatesFixFlow.tracker(fixOf.name)) : RatesFixFlow(tx, oracle, fixOf, expectedRate, rateTolerance, progressTracker) { progressTracker: ProgressTracker = RatesFixFlow.tracker(fixOf.name))
: RatesFixFlow(tx, oracle, fixOf, expectedRate, rateTolerance, progressTracker) {
override fun filtering(elem: Any): Boolean { override fun filtering(elem: Any): Boolean {
return when (elem) { return when (elem) {
is Command -> oracle.owningKey in elem.signers && elem.value is Fix is Command -> oracle.owningKey in elem.signers && elem.value is Fix
@ -242,5 +245,6 @@ class NodeInterestRatesTest {
} }
} }
private fun makeTX() = TransactionType.General.Builder(DUMMY_NOTARY).withItems(1000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE `with notary` DUMMY_NOTARY) private fun makeTX() = TransactionType.General.Builder(DUMMY_NOTARY).withItems(
1000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE `with notary` DUMMY_NOTARY)
} }

View File

@ -32,7 +32,7 @@ class SimmValuationTest : IntegrationTestCategory {
@Test @Test
fun `runs SIMM valuation demo`() { fun `runs SIMM valuation demo`() {
driver(isDebug = true) { driver(isDebug = true, systemProperties = mapOf("net.corda.node.cordapp.scan.package" to "net.corda.vega")) {
startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type))).getOrThrow() startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type))).getOrThrow()
val (nodeA, nodeB) = Futures.allAsList(startNode(nodeALegalName), startNode(nodeBLegalName)).getOrThrow() val (nodeA, nodeB) = Futures.allAsList(startNode(nodeALegalName), startNode(nodeBLegalName)).getOrThrow()
val (nodeAApi, nodeBApi) = Futures.allAsList(startWebserver(nodeA), startWebserver(nodeB)) val (nodeAApi, nodeBApi) = Futures.allAsList(startWebserver(nodeA), startWebserver(nodeB))
@ -50,8 +50,9 @@ class SimmValuationTest : IntegrationTestCategory {
} }
} }
private fun getPartyWithName(partyApi: HttpApi, counterparty: X500Name): PortfolioApi.ApiParty = private fun getPartyWithName(partyApi: HttpApi, counterparty: X500Name): PortfolioApi.ApiParty {
getAvailablePartiesFor(partyApi).counterparties.single { it.text == counterparty } return getAvailablePartiesFor(partyApi).counterparties.single { it.text == counterparty }
}
private fun getAvailablePartiesFor(partyApi: HttpApi): PortfolioApi.AvailableParties { private fun getAvailablePartiesFor(partyApi: HttpApi): PortfolioApi.AvailableParties {
return partyApi.getJson<PortfolioApi.AvailableParties>("whoami") return partyApi.getJson<PortfolioApi.AvailableParties>("whoami")

View File

@ -2,10 +2,10 @@ package net.corda.vega.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.PluginServiceHub
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.unwrap import net.corda.core.utilities.unwrap
@ -15,12 +15,6 @@ import net.corda.vega.contracts.OGTrade
import net.corda.vega.contracts.SwapData import net.corda.vega.contracts.SwapData
object IRSTradeFlow { object IRSTradeFlow {
class Service(services: PluginServiceHub) {
init {
services.registerServiceFlow(Requester::class.java, ::Receiver)
}
}
@CordaSerializable @CordaSerializable
data class OfferMessage(val notary: Party, val dealBeingOffered: IRSState) data class OfferMessage(val notary: Party, val dealBeingOffered: IRSState)
@ -52,6 +46,7 @@ object IRSTradeFlow {
} }
@InitiatedBy(Requester::class)
class Receiver(private val replyToParty: Party) : FlowLogic<Unit>() { class Receiver(private val replyToParty: Party) : FlowLogic<Unit>() {
@Suspendable @Suspendable
override fun call() { override fun call() {

View File

@ -11,6 +11,7 @@ import com.opengamma.strata.pricer.swap.DiscountingSwapProductPricer
import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef import net.corda.core.contracts.StateRef
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party import net.corda.core.identity.Party
@ -180,18 +181,10 @@ object SimmFlow {
} }
} }
/**
* Service plugin for listening for incoming Simm flow communication
*/
class Service(services: PluginServiceHub) {
init {
services.registerServiceFlow(Requester::class.java, ::Receiver)
}
}
/** /**
* Receives and validates a portfolio and comes to consensus over the portfolio initial margin using SIMM. * Receives and validates a portfolio and comes to consensus over the portfolio initial margin using SIMM.
*/ */
@InitiatedBy(Requester::class)
class Receiver(val replyToParty: Party) : FlowLogic<Unit>() { class Receiver(val replyToParty: Party) : FlowLogic<Unit>() {
lateinit var ownParty: Party lateinit var ownParty: Party
lateinit var offer: OfferMessage lateinit var offer: OfferMessage

View File

@ -15,8 +15,6 @@ import net.corda.core.serialization.SerializationCustomization
import net.corda.vega.analytics.CordaMarketData import net.corda.vega.analytics.CordaMarketData
import net.corda.vega.analytics.InitialMarginTriple import net.corda.vega.analytics.InitialMarginTriple
import net.corda.vega.api.PortfolioApi import net.corda.vega.api.PortfolioApi
import net.corda.vega.flows.IRSTradeFlow
import net.corda.vega.flows.SimmFlow
import java.util.function.Function import java.util.function.Function
/** /**
@ -28,7 +26,6 @@ object SimmService {
class Plugin : CordaPluginRegistry() { class Plugin : CordaPluginRegistry() {
override val webApis = listOf(Function(::PortfolioApi)) override val webApis = listOf(Function(::PortfolioApi))
override val staticServeDirs: Map<String, String> = mapOf("simmvaluationdemo" to javaClass.classLoader.getResource("simmvaluationweb").toExternalForm()) override val staticServeDirs: Map<String, String> = mapOf("simmvaluationdemo" to javaClass.classLoader.getResource("simmvaluationweb").toExternalForm())
override val servicePlugins = listOf(Function(SimmFlow::Service), Function(IRSTradeFlow::Service))
override fun customizeSerialization(custom: SerializationCustomization): Boolean { override fun customizeSerialization(custom: SerializationCustomization): Boolean {
custom.apply { custom.apply {
// OpenGamma classes. // OpenGamma classes.

View File

@ -16,6 +16,7 @@ import net.corda.node.services.transactions.SimpleNotaryService
import net.corda.nodeapi.User import net.corda.nodeapi.User
import net.corda.testing.BOC import net.corda.testing.BOC
import net.corda.testing.node.NodeBasedTest import net.corda.testing.node.NodeBasedTest
import net.corda.traderdemo.flow.BuyerFlow
import net.corda.traderdemo.flow.SellerFlow import net.corda.traderdemo.flow.SellerFlow
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.Test import org.junit.Test
@ -36,6 +37,8 @@ class TraderDemoTest : NodeBasedTest() {
startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
).getOrThrow() ).getOrThrow()
nodeA.registerInitiatedFlow(BuyerFlow::class.java)
val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map { val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map {
val client = CordaRPCClient(it.configuration.rpcAddress!!) val client = CordaRPCClient(it.configuration.rpcAddress!!)
client.start(demoUser[0].username, demoUser[0].password).proxy client.start(demoUser[0].username, demoUser[0].password).proxy
@ -57,12 +60,8 @@ class TraderDemoTest : NodeBasedTest() {
val executor = Executors.newScheduledThreadPool(1) val executor = Executors.newScheduledThreadPool(1)
poll(executor, "A to be notified of the commercial paper", pollInterval = 100.millis) { poll(executor, "A to be notified of the commercial paper", pollInterval = 100.millis) {
val actualPaper = listOf(clientA.commercialPaperCount, clientB.commercialPaperCount) val actualPaper = listOf(clientA.commercialPaperCount, clientB.commercialPaperCount)
if (actualPaper == expectedPaper) { if (actualPaper == expectedPaper) Unit else null
Unit }.getOrThrow()
} else {
null
}
}.get()
executor.shutdown() executor.shutdown()
assertThat(clientA.dollarCashBalance).isEqualTo(95.DOLLARS) assertThat(clientA.dollarCashBalance).isEqualTo(95.DOLLARS)
assertThat(clientB.dollarCashBalance).isEqualTo(5.DOLLARS) assertThat(clientB.dollarCashBalance).isEqualTo(5.DOLLARS)

View File

@ -4,36 +4,24 @@ import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.CommercialPaper import net.corda.contracts.CommercialPaper
import net.corda.core.contracts.Amount import net.corda.core.contracts.Amount
import net.corda.core.contracts.TransactionGraphSearch import net.corda.core.contracts.TransactionGraphSearch
import net.corda.core.identity.Party import net.corda.core.div
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy
import net.corda.core.identity.Party
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.PluginServiceHub
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.Emoji import net.corda.core.utilities.Emoji
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.unwrap import net.corda.core.utilities.unwrap
import net.corda.flows.TwoPartyTradeFlow import net.corda.flows.TwoPartyTradeFlow
import java.nio.file.Paths
import java.util.* import java.util.*
class BuyerFlow(val otherParty: Party, @InitiatedBy(SellerFlow::class)
private val attachmentsDirectory: String, class BuyerFlow(val otherParty: Party) : FlowLogic<Unit>() {
override val progressTracker: ProgressTracker = ProgressTracker(STARTING_BUY)) : FlowLogic<Unit>() {
object STARTING_BUY : ProgressTracker.Step("Seller connected, purchasing commercial paper asset") object STARTING_BUY : ProgressTracker.Step("Seller connected, purchasing commercial paper asset")
class Service(services: PluginServiceHub) : SingletonSerializeAsToken() { override val progressTracker: ProgressTracker = ProgressTracker(STARTING_BUY)
init {
// Buyer will fetch the attachment from the seller automatically when it resolves the transaction.
// For demo purposes just extract attachment jars when saved to disk, so the user can explore them.
val attachmentsPath = (services.storageService.attachments).let {
it.automaticallyExtractAttachments = true
it.storePath
}
services.registerServiceFlow(SellerFlow::class.java) { BuyerFlow(it, attachmentsPath.toString()) }
}
}
@Suspendable @Suspendable
override fun call() { override fun call() {
@ -72,8 +60,15 @@ class BuyerFlow(val otherParty: Party,
followInputsOfType = CommercialPaper.State::class.java) followInputsOfType = CommercialPaper.State::class.java)
val cpIssuance = search.call().single() val cpIssuance = search.call().single()
// Buyer will fetch the attachment from the seller automatically when it resolves the transaction.
// For demo purposes just extract attachment jars when saved to disk, so the user can explore them.
val attachmentsPath = (serviceHub.storageService.attachments).let {
it.automaticallyExtractAttachments = true
it.storePath
}
cpIssuance.attachments.first().let { cpIssuance.attachments.first().let {
val p = Paths.get(attachmentsDirectory, "$it.jar") val p = attachmentsPath / "$it.jar"
println(""" println("""
The issuance of the commercial paper came with an attachment. You can find it expanded in this directory: The issuance of the commercial paper came with an attachment. You can find it expanded in this directory:

View File

@ -1,10 +0,0 @@
package net.corda.traderdemo.plugin
import net.corda.core.identity.Party
import net.corda.core.node.CordaPluginRegistry
import net.corda.traderdemo.flow.BuyerFlow
import java.util.function.Function
class TraderDemoPlugin : CordaPluginRegistry() {
override val servicePlugins = listOf(Function(BuyerFlow::Service))
}

View File

@ -1,2 +0,0 @@
# Register a ServiceLoader service extending from net.corda.core.node.CordaPluginRegistry
net.corda.traderdemo.plugin.TraderDemoPlugin

View File

@ -4,24 +4,21 @@
package net.corda.testing package net.corda.testing
import com.google.common.net.HostAndPort import com.google.common.net.HostAndPort
import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.contracts.StateRef import net.corda.core.contracts.StateRef
import net.corda.core.crypto.* import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic import net.corda.core.crypto.X509Utilities
import net.corda.core.crypto.commonName
import net.corda.core.crypto.generateKeyPair
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
import net.corda.core.node.VersionInfo import net.corda.core.node.VersionInfo
import net.corda.core.node.services.IdentityService import net.corda.core.node.services.IdentityService
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
import net.corda.core.toFuture
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.* import net.corda.core.utilities.*
import net.corda.node.internal.AbstractNode
import net.corda.node.internal.NetworkMapInfo import net.corda.node.internal.NetworkMapInfo
import net.corda.node.services.config.* import net.corda.node.services.config.*
import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.identity.InMemoryIdentityService
import net.corda.node.services.statemachine.FlowStateMachineImpl
import net.corda.node.services.statemachine.StateMachineManager
import net.corda.nodeapi.User import net.corda.nodeapi.User
import net.corda.nodeapi.config.SSLConfiguration import net.corda.nodeapi.config.SSLConfiguration
import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices
@ -36,7 +33,6 @@ import java.security.KeyPair
import java.security.PublicKey import java.security.PublicKey
import java.util.* import java.util.*
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import kotlin.reflect.KClass
/** /**
* JAVA INTEROP * JAVA INTEROP
@ -138,19 +134,6 @@ fun getFreeLocalPorts(hostName: String, numberToAlloc: Int): List<HostAndPort> {
dsl: TransactionDSL<TransactionDSLInterpreter>.() -> EnforceVerifyOrFail dsl: TransactionDSL<TransactionDSLInterpreter>.() -> EnforceVerifyOrFail
) = ledger { this.transaction(transactionLabel, transactionBuilder, dsl) } ) = ledger { this.transaction(transactionLabel, transactionBuilder, dsl) }
/**
* The given flow factory will be used to initiate just one instance of a flow of type [P] when a counterparty
* flow requests for it using [clientFlowClass].
* @return Returns a [ListenableFuture] holding the single [FlowStateMachineImpl] created by the request.
*/
inline fun <reified P : FlowLogic<*>> AbstractNode.initiateSingleShotFlow(
clientFlowClass: KClass<out FlowLogic<*>>,
noinline serviceFlowFactory: (Party) -> P): ListenableFuture<P> {
val future = smm.changes.filter { it is StateMachineManager.Change.Add && it.logic is P }.map { it.logic as P }.toFuture()
services.registerServiceFlow(clientFlowClass.java, serviceFlowFactory)
return future
}
// TODO Replace this with testConfiguration // TODO Replace this with testConfiguration
data class TestNodeConfiguration( data class TestNodeConfiguration(
override val baseDirectory: Path, override val baseDirectory: Path,

View File

@ -1,13 +1,11 @@
package net.corda.testing.node package net.corda.testing.node
import com.google.common.annotations.VisibleForTesting
import com.google.common.jimfs.Configuration.unix import com.google.common.jimfs.Configuration.unix
import com.google.common.jimfs.Jimfs import com.google.common.jimfs.Jimfs
import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.* import net.corda.core.*
import net.corda.core.crypto.entropyToKeyPair import net.corda.core.crypto.entropyToKeyPair
import net.corda.core.flows.FlowLogic
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.messaging.RPCOps import net.corda.core.messaging.RPCOps
import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.messaging.SingleMessageRecipient
@ -18,14 +16,12 @@ import net.corda.core.node.services.*
import net.corda.core.utilities.DUMMY_NOTARY_KEY import net.corda.core.utilities.DUMMY_NOTARY_KEY
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.node.internal.AbstractNode import net.corda.node.internal.AbstractNode
import net.corda.node.internal.ServiceFlowInfo
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.identity.InMemoryIdentityService
import net.corda.node.services.keys.E2ETestKeyManagementService import net.corda.node.services.keys.E2ETestKeyManagementService
import net.corda.node.services.messaging.MessagingService import net.corda.node.services.messaging.MessagingService
import net.corda.node.services.network.InMemoryNetworkMapService import net.corda.node.services.network.InMemoryNetworkMapService
import net.corda.node.services.network.NetworkMapService import net.corda.node.services.network.NetworkMapService
import net.corda.node.services.statemachine.flowVersion
import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.services.transactions.InMemoryTransactionVerifierService
import net.corda.node.services.transactions.InMemoryUniquenessProvider import net.corda.node.services.transactions.InMemoryUniquenessProvider
import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.services.transactions.SimpleNotaryService
@ -45,7 +41,6 @@ import java.security.KeyPair
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import kotlin.reflect.KClass
/** /**
* A mock node brings up a suite of in-memory services in a fast manner suitable for unit testing. * A mock node brings up a suite of in-memory services in a fast manner suitable for unit testing.
@ -232,13 +227,6 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false,
// It is used from the network visualiser tool. // It is used from the network visualiser tool.
@Suppress("unused") val place: PhysicalLocation get() = findMyLocation()!! @Suppress("unused") val place: PhysicalLocation get() = findMyLocation()!!
@VisibleForTesting
fun registerServiceFlow(clientFlowClass: KClass<out FlowLogic<*>>,
flowVersion: Int = clientFlowClass.java.flowVersion,
serviceFlowFactory: (Party) -> FlowLogic<*>) {
serviceFlowFactories[clientFlowClass.java] = ServiceFlowInfo.CorDapp(flowVersion, serviceFlowFactory)
}
fun pumpReceive(block: Boolean = false): InMemoryMessagingNetwork.MessageTransfer? { fun pumpReceive(block: Boolean = false): InMemoryMessagingNetwork.MessageTransfer? {
return (net as InMemoryMessagingNetwork.InMemoryMessaging).pumpReceive(block) return (net as InMemoryMessagingNetwork.InMemoryMessaging).pumpReceive(block)
} }
@ -262,8 +250,6 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false,
* Returns a node, optionally created by the passed factory method. * Returns a node, optionally created by the passed factory method.
* @param overrideServices a set of service entries to use in place of the node's default service entries, * @param overrideServices a set of service entries to use in place of the node's default service entries,
* for example where a node's service is part of a cluster. * for example where a node's service is part of a cluster.
* @param entropyRoot the initial entropy value to use when generating keys. Defaults to an (insecure) random value,
* but can be overriden to cause nodes to have stable or colliding identity/service keys.
*/ */
fun createNode(networkMapAddress: SingleMessageRecipient? = null, forcedID: Int = -1, nodeFactory: Factory = defaultFactory, fun createNode(networkMapAddress: SingleMessageRecipient? = null, forcedID: Int = -1, nodeFactory: Factory = defaultFactory,
start: Boolean = true, legalName: X500Name? = null, overrideServices: Map<ServiceInfo, KeyPair>? = null, start: Boolean = true, legalName: X500Name? = null, overrideServices: Map<ServiceInfo, KeyPair>? = null,

View File

@ -8,6 +8,7 @@ import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
import net.corda.core.node.services.* import net.corda.core.node.services.*
import net.corda.core.serialization.SerializeAsToken
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_NOTARY
@ -28,6 +29,7 @@ import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
import java.security.KeyPair import java.security.KeyPair
import java.security.PrivateKey import java.security.PrivateKey
@ -75,6 +77,8 @@ open class MockServices(vararg val keys: KeyPair) : ServiceHub {
HibernateObserver(vaultService.rawUpdates, NodeSchemaService()) HibernateObserver(vaultService.rawUpdates, NodeSchemaService())
return vaultService return vaultService
} }
override fun <T : SerializeAsToken> cordaService(type: Class<T>): T = throw IllegalArgumentException("${type.name} not found")
} }
class MockKeyManagementService(val identityService: IdentityService, class MockKeyManagementService(val identityService: IdentityService,
@ -91,7 +95,9 @@ class MockKeyManagementService(val identityService: IdentityService,
return k.public return k.public
} }
override fun freshKeyAndCert(identity: Party, revocationEnabled: Boolean): Pair<X509CertificateHolder, CertPath> = freshKeyAndCert(this, identityService, identity, revocationEnabled) override fun freshKeyAndCert(identity: Party, revocationEnabled: Boolean): Pair<X509CertificateHolder, CertPath> {
return freshKeyAndCert(this, identityService, identity, revocationEnabled)
}
private fun getSigningKeyPair(publicKey: PublicKey): KeyPair { private fun getSigningKeyPair(publicKey: PublicKey): KeyPair {
val pk = publicKey.keys.first { keyStore.containsKey(it) } val pk = publicKey.keys.first { keyStore.containsKey(it) }
@ -108,7 +114,7 @@ class MockKeyManagementService(val identityService: IdentityService,
class MockAttachmentStorage : AttachmentStorage { class MockAttachmentStorage : AttachmentStorage {
val files = HashMap<SecureHash, ByteArray>() val files = HashMap<SecureHash, ByteArray>()
override var automaticallyExtractAttachments = false override var automaticallyExtractAttachments = false
override var storePath = Paths.get("") override var storePath: Path = Paths.get("")
override fun openAttachment(id: SecureHash): Attachment? { override fun openAttachment(id: SecureHash): Attachment? {
val f = files[id] ?: return null val f = files[id] ?: return null

View File

@ -1,10 +0,0 @@
package net.corda.explorer.plugin
import net.corda.core.identity.Party
import net.corda.core.node.CordaPluginRegistry
import net.corda.flows.IssuerFlow
import java.util.function.Function
class ExplorerPlugin : CordaPluginRegistry() {
override val servicePlugins = listOf(Function(IssuerFlow.Issuer::Service))
}

View File

@ -1,2 +0,0 @@
# Register a ServiceLoader service extending from net.corda.node.CordaPluginRegistry
net.corda.explorer.plugin.ExplorerPlugin