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)