diff --git a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt index 574008d6d8..8241fcd13c 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt @@ -1,5 +1,6 @@ package net.corda.core.node.services +import com.google.common.primitives.Booleans import net.corda.core.contracts.StateRef import net.corda.core.contracts.TimeWindow import net.corda.core.crypto.* @@ -15,12 +16,13 @@ import java.security.PublicKey abstract class NotaryService : SingletonSerializeAsToken() { companion object { const val ID_PREFIX = "corda.notary." - fun constructId(validating: Boolean, raft: Boolean = false, bft: Boolean = false): String { - require(!raft || !bft) + fun constructId(validating: Boolean, raft: Boolean = false, bft: Boolean = false, custom: Boolean = false): String { + require(Booleans.countTrue(raft, bft, custom) <= 1) { "At most one of raft, bft or custom may be true" } return StringBuffer(ID_PREFIX).apply { append(if (validating) "validating" else "simple") if (raft) append(".raft") if (bft) append(".bft") + if (custom) append(".custom") }.toString() } } diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index 8511b2d486..2b2627f56e 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -94,7 +94,7 @@ path to the node's base directory. .. note:: The driver will not automatically create a webserver instance, but the Cordformation will. If this field is present the web server will start. -:notary: Optional config object which if present configures the node to run as a notary. If part of a Raft or BFT SMaRt +:notary: Optional configuration object which if present configures the node to run as a notary. If part of a Raft or BFT SMaRt cluster then specify ``raft`` or ``bftSMaRt`` respectively as described below. If a single node notary then omit both. :validating: Boolean to determine whether the notary is a validating or non-validating one. @@ -108,11 +108,15 @@ path to the node's base directory. members must be active and be able to communicate with the cluster leader for joining. If empty, a new cluster will be bootstrapped. - :bftSMaRt: If part of a distributed BFT SMaRt cluster specify this config object, with the following settings: + :bftSMaRt: If part of a distributed BFT-SMaRt cluster specify this config object, with the following settings: - :replicaId: + :replicaId: The zero-based index of the current replica. All replicas must specify a unique replica id. - :clusterAddresses: + :clusterAddresses: List of all BFT-SMaRt cluster member addresses. + + :custom: If `true`, will load and install a notary service from a CorDapp. See :doc:`tutorial-custom-notary`. + + Only one of ``raft``, ``bftSMaRt`` or ``custom`` configuration values may be specified. :networkMapService: If `null`, or missing the node is declaring itself as the NetworkMapService host. Otherwise this is a config object with the details of the network map service: diff --git a/docs/source/running-the-demos.rst b/docs/source/running-the-demos.rst index f9e78737d1..11ebb794c2 100644 --- a/docs/source/running-the-demos.rst +++ b/docs/source/running-the-demos.rst @@ -35,7 +35,7 @@ To run from the command line in Unix: 2. Run ``./samples/trader-demo/build/nodes/runnodes`` to open up four new terminals with the four nodes 3. Run ``./gradlew samples:trader-demo:runBank`` to instruct the bank node to issue cash and commercial paper to the buyer and seller nodes respectively. 4. Run ``./gradlew samples:trader-demo:runSeller`` to trigger the transaction. If you entered ``flow watch`` - + you can see flows running on both sides of transaction. Additionally you should see final trade information displayed to your terminal. @@ -45,7 +45,7 @@ To run from the command line in Windows: 2. Run ``samples\trader-demo\build\nodes\runnodes`` to open up four new terminals with the four nodes 3. Run ``gradlew samples:trader-demo:runBank`` to instruct the buyer node to request issuance of some cash from the Bank of Corda node 4. Run ``gradlew samples:trader-demo:runSeller`` to trigger the transaction. If you entered ``flow watch`` - + you can see flows running on both sides of transaction. Additionally you should see final trade information displayed to your terminal. @@ -112,8 +112,11 @@ Notary demo This demo shows a party getting transactions notarised by either a single-node or a distributed notary service. All versions of the demo start two counterparty nodes. One of the counterparties will generate transactions that transfer a self-issued asset to the other party and submit them for notarisation. -The `Raft `_ version of the demo will start three distributed notary nodes. -The `BFT SMaRt `_ version of the demo will start four distributed notary nodes. + +* The `Raft `_ version of the demo will start three distributed notary nodes. +* The `BFT SMaRt `_ version of the demo will start four distributed notary nodes. +* The Single version of the demo will start a single-node validating notary service. +* The Custom version of the demo will load and start a custom single-node notary service that is defined the demo CorDapp. The output will display a list of notarised transaction IDs and corresponding signer public keys. In the Raft distributed notary, every node in the cluster can service client requests, and one signature is sufficient to satisfy the notary composite key requirement. @@ -122,9 +125,9 @@ You will notice that successive transactions get signed by different members of To run the Raft version of the demo from the command line in Unix: -1. Run ``./gradlew samples:notary-demo:deployNodes``, which will create all three types of notaries' node directories - with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT`` and ``nodesSingle`` for BFT and - Single notaries). +1. Run ``./gradlew samples:notary-demo:deployNodes``, which will create node directories for all versions of the demo, + with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT``, ``nodesSingle``, and ``nodesCustom`` for + BFT, Single and Custom notaries respectively). 2. Run ``./samples/notary-demo/build/nodes/nodesRaft/runnodes``, which will start the nodes in separate terminal windows/tabs. Wait until a "Node started up and registered in ..." message appears on each of the terminals 3. Run ``./gradlew samples:notary-demo:notarise`` to make a call to the "Party" node to initiate notarisation requests @@ -133,8 +136,8 @@ To run the Raft version of the demo from the command line in Unix: To run from the command line in Windows: 1. Run ``gradlew samples:notary-demo:deployNodes``, which will create all three types of notaries' node directories - with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT`` and ``nodesSingle`` for BFT and - Single notaries). + with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT``, ``nodesSingle``, and ``nodesCustom`` for + BFT, Single and Custom notaries respectively). 2. Run ``samples\notary-demo\build\nodes\nodesRaft\runnodes``, which will start the nodes in separate terminal windows/tabs. Wait until a "Node started up and registered in ..." message appears on each of the terminals 3. Run ``gradlew samples:notary-demo:notarise`` to make a call to the "Party" node to initiate notarisation requests @@ -142,6 +145,7 @@ To run from the command line in Windows: To run the BFT SMaRt notary demo, use ``nodesBFT`` instead of ``nodesRaft`` in the path (you will see messages from notary nodes trying to communicate each other sometime with connection errors, that's normal). For a single notary node, use ``nodesSingle``. +For the custom notary service use ``nodesCustom`. Distributed notary nodes store consumed states in a replicated commit log, which is backed by a H2 database on each node. You can ascertain that the commit log is synchronised across the cluster by accessing and comparing each of the nodes' backing stores diff --git a/docs/source/tutorial-custom-notary.rst b/docs/source/tutorial-custom-notary.rst index cabefdd203..28d5dc1158 100644 --- a/docs/source/tutorial-custom-notary.rst +++ b/docs/source/tutorial-custom-notary.rst @@ -1,17 +1,17 @@ .. highlight:: kotlin -Writing a custom notary service -=============================== +Writing a custom notary service (experimental) +============================================== -.. warning:: Customising a notary service is an advanced feature and not recommended for most use-cases. Currently, +.. warning:: Customising a notary service is still an experimental feature and not recommended for most use-cases. Currently, customising Raft or BFT notaries is not yet fully supported. If you want to write your own Raft notary you will have to implement a custom database connector (or use a separate database for the notary), and use a custom configuration file. Similarly to writing an oracle service, the first step is to create a service class in your CorDapp and annotate it -with ``@CordaService``. 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 ``AppServiceHub``. +with ``@CordaService``. The Corda node scans for any class with this annotation and initialises them. The custom notary +service class should provide a constructor with two parameters of types ``AppServiceHub`` and ``PublicKey``. -.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt +.. literalinclude:: ../../samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt :language: kotlin :start-after: START 1 :end-before: END 1 @@ -20,7 +20,16 @@ The next step is to write a notary service flow. You are free to copy and modify as ``ValidatingNotaryFlow``, ``NonValidatingNotaryFlow``, or implement your own from scratch (following the ``NotaryFlow.Service`` template). Below is an example of a custom flow for a *validating* notary service: -.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt +.. literalinclude:: ../../samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt :language: kotlin :start-after: START 2 :end-before: END 2 + +To enable the service, add the following to the node configuration: + +.. parsed-literal:: + + notary : { + validating : true # Set to false if your service is non-validating + custom : true + } \ No newline at end of file diff --git a/docs/source/tutorials-index.rst b/docs/source/tutorials-index.rst index c0b1a5af14..f14be4385f 100644 --- a/docs/source/tutorials-index.rst +++ b/docs/source/tutorials-index.rst @@ -16,6 +16,7 @@ Tutorials flow-testing running-a-notary oracles + tutorial-custom-notary tutorial-tear-offs tutorial-attachments event-scheduling \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index e08b457c37..9a7e5cbbeb 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -19,14 +19,11 @@ import net.corda.core.internal.concurrent.flatMap import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.toX509CertHolder import net.corda.core.internal.uncheckedCast -import net.corda.core.messaging.CordaRPCOps -import net.corda.core.messaging.RPCOps -import net.corda.core.messaging.SingleMessageRecipient -import net.corda.core.node.* import net.corda.core.messaging.* import net.corda.core.node.AppServiceHub import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub +import net.corda.core.node.StateLoader import net.corda.core.node.services.* import net.corda.core.node.services.NetworkMapCache.MapChange import net.corda.core.schemas.MappedSchema @@ -257,7 +254,8 @@ abstract class AbstractNode(config: NodeConfiguration, private class ServiceInstantiationException(cause: Throwable?) : CordaException("Service Instantiation Error", cause) private fun installCordaServices() { - cordappProvider.cordapps.flatMap { it.services }.forEach { + val loadedServices = cordappProvider.cordapps.flatMap { it.services } + filterServicesToInstall(loadedServices).forEach { try { installCordaService(it) } catch (e: NoSuchMethodException) { @@ -271,6 +269,25 @@ abstract class AbstractNode(config: NodeConfiguration, } } + private fun filterServicesToInstall(loadedServices: List>): List> { + val customNotaryServiceList = loadedServices.filter { isNotaryService(it) } + if (customNotaryServiceList.isNotEmpty()) { + if (configuration.notary?.custom == true) { + require(customNotaryServiceList.size == 1) { + "Attempting to install more than one notary service: ${customNotaryServiceList.joinToString()}" + } + } + else return loadedServices - customNotaryServiceList + } + return loadedServices + } + + /** + * If the [serviceClass] is a notary service, it will only be enable if the "custom" flag is set in + * the notary configuration. + */ + private fun isNotaryService(serviceClass: Class<*>) = NotaryService::class.java.isAssignableFrom(serviceClass) + /** * This customizes the ServiceHub for each CordaService that is initiating flows */ @@ -321,14 +338,15 @@ abstract class AbstractNode(config: NodeConfiguration, fun installCordaService(serviceClass: Class): T { serviceClass.requireAnnotation() val service = try { - if (NotaryService::class.java.isAssignableFrom(serviceClass)) { + val serviceContext = AppServiceHubImpl(services) + if (isNotaryService(serviceClass)) { check(myNotaryIdentity != null) { "Trying to install a notary service but no notary identity specified" } - val constructor = serviceClass.getDeclaredConstructor(ServiceHub::class.java, PublicKey::class.java).apply { isAccessible = true } - constructor.newInstance(services, myNotaryIdentity!!.owningKey) + val constructor = serviceClass.getDeclaredConstructor(AppServiceHub::class.java, PublicKey::class.java).apply { isAccessible = true } + serviceContext.serviceInstance = constructor.newInstance(serviceContext, myNotaryIdentity!!.owningKey) + serviceContext.serviceInstance } else { try { val extendedServiceConstructor = serviceClass.getDeclaredConstructor(AppServiceHub::class.java).apply { isAccessible = true } - val serviceContext = AppServiceHubImpl(services) serviceContext.serviceInstance = extendedServiceConstructor.newInstance(serviceContext) serviceContext.serviceInstance } catch (ex: NoSuchMethodException) { @@ -688,7 +706,9 @@ abstract class AbstractNode(config: NodeConfiguration, // Node's main identity Pair("identity", myLegalName) } else { - val notaryId = notaryConfig.run { NotaryService.constructId(validating, raft != null, bftSMaRt != null) } + val notaryId = notaryConfig.run { + NotaryService.constructId(validating, raft != null, bftSMaRt != null, custom) + } if (notaryConfig.bftSMaRt == null && notaryConfig.raft == null) { // Node's notary identity Pair(notaryId, myLegalName.copy(commonName = notaryId)) diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index a72465ccee..782064bf76 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -36,9 +36,15 @@ interface NodeConfiguration : NodeSSLConfiguration { val additionalNodeInfoPollingFrequencyMsec: Long } -data class NotaryConfig(val validating: Boolean, val raft: RaftConfig? = null, val bftSMaRt: BFTSMaRtConfiguration? = null) { +data class NotaryConfig(val validating: Boolean, + val raft: RaftConfig? = null, + val bftSMaRt: BFTSMaRtConfiguration? = null, + val custom: Boolean = false +) { init { - require(raft == null || bftSMaRt == null) { "raft and bftSMaRt configs cannot be specified together" } + require(raft == null || bftSMaRt == null || !custom) { + "raft, bftSMaRt, and custom configs cannot be specified together" + } } } @@ -46,9 +52,10 @@ data class RaftConfig(val nodeAddress: NetworkHostAndPort, val clusterAddresses: /** @param exposeRaces for testing only, so its default is not in reference.conf but here. */ data class BFTSMaRtConfiguration constructor(val replicaId: Int, - val clusterAddresses: List, - val debug: Boolean = false, - val exposeRaces: Boolean = false) { + val clusterAddresses: List, + val debug: Boolean = false, + val exposeRaces: Boolean = false +) { init { require(replicaId >= 0) { "replicaId cannot be negative" } } diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt index ac2e890f09..81dbf12166 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt @@ -69,7 +69,7 @@ private class BankOfCordaDriver { val bigCorpUser = User(BIGCORP_USERNAME, "test", permissions = setOf( startFlowPermission())) - startNotaryNode(DUMMY_NOTARY.name, validating = false) + startNotaryNode(DUMMY_NOTARY.name, validating = true) val bankOfCorda = startNode( providedName = BOC.name, rpcUsers = listOf(bankUser)) diff --git a/samples/notary-demo/build.gradle b/samples/notary-demo/build.gradle index a13ab70ad8..4d3f97f065 100644 --- a/samples/notary-demo/build.gradle +++ b/samples/notary-demo/build.gradle @@ -46,13 +46,18 @@ publishing { } } -task deployNodes(dependsOn: ['deployNodesSingle', 'deployNodesRaft', 'deployNodesBFT']) +task deployNodes(dependsOn: ['deployNodesSingle', 'deployNodesRaft', 'deployNodesBFT', 'deployNodesCustom']) task deployNodesSingle(type: Cordform, dependsOn: 'jar') { directory "./build/nodes/nodesSingle" definitionClass = 'net.corda.notarydemo.SingleNotaryCordform' } +task deployNodesCustom(type: Cordform, dependsOn: 'jar') { + directory "./build/nodes/nodesCustom" + definitionClass = 'net.corda.notarydemo.CustomNotaryCordform' +} + task deployNodesRaft(type: Cordform, dependsOn: 'jar') { directory "./build/nodes/nodesRaft" definitionClass = 'net.corda.notarydemo.RaftNotaryCordform' diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt new file mode 100644 index 0000000000..900150da69 --- /dev/null +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt @@ -0,0 +1,36 @@ +package net.corda.notarydemo + +import net.corda.cordform.CordformContext +import net.corda.cordform.CordformDefinition +import net.corda.core.internal.div +import net.corda.node.services.config.NotaryConfig +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.internal.demorun.* + +fun main(args: Array) = CustomNotaryCordform.runNodes() + +object CustomNotaryCordform : CordformDefinition("build" / "notary-demo-nodes") { + init { + node { + name(ALICE.name) + p2pPort(10002) + rpcPort(10003) + rpcUsers(notaryDemoUser) + } + node { + name(BOB.name) + p2pPort(10005) + rpcPort(10006) + } + node { + name(DUMMY_NOTARY.name) + p2pPort(10009) + rpcPort(10010) + notary(NotaryConfig(validating = true, custom = true)) + } + } + + override fun setup(context: CordformContext) {} +} \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt similarity index 74% rename from docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt rename to samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt index d7331d146c..55adae9ee2 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomNotaryTutorial.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt @@ -1,4 +1,4 @@ -package net.corda.docs +package net.corda.notarydemo import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.TimeWindow @@ -8,11 +8,17 @@ import net.corda.core.node.AppServiceHub import net.corda.core.node.services.CordaService import net.corda.core.node.services.TimeWindowChecker import net.corda.core.node.services.TrustedAuthorityNotaryService +import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionWithSignatures import net.corda.node.services.transactions.PersistentUniquenessProvider import java.security.PublicKey import java.security.SignatureException +/** + * A custom notary service should provide a constructor that accepts two parameters of types [AppServiceHub] and [PublicKey]. + * + * Note that at present only a single-node notary service can be customised. + */ // START 1 @CordaService class MyCustomValidatingNotaryService(override val services: AppServiceHub, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { @@ -26,6 +32,7 @@ class MyCustomValidatingNotaryService(override val services: AppServiceHub, over } // END 1 +@Suppress("UNUSED_PARAMETER") // START 2 class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidatingNotaryService) : NotaryFlow.Service(otherSide, service) { /** @@ -38,11 +45,15 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false)) val notary = stx.notary checkNotary(notary) - val timeWindow: TimeWindow? = if (stx.isNotaryChangeTransaction()) - null - else - stx.tx.timeWindow - val transactionWithSignatures = stx.resolveTransactionWithSignatures(serviceHub) + var timeWindow: TimeWindow? = null + val transactionWithSignatures = if (stx.isNotaryChangeTransaction()) { + stx.resolveNotaryChangeTransaction(serviceHub) + } else { + val wtx = stx.tx + customVerify(wtx.toLedgerTransaction(serviceHub)) + timeWindow = wtx.timeWindow + stx + } checkSignatures(transactionWithSignatures) return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!) } catch (e: Exception) { @@ -54,6 +65,10 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating } } + private fun customVerify(transaction: LedgerTransaction) { + // Add custom verification logic + } + private fun checkSignatures(tx: TransactionWithSignatures) { try { tx.verifySignaturesExcept(service.notaryIdentityKey)