Fixed AbstractNode to load custom notary services properly (#1720)

* Fixed AbstractNode to load custom notary services properly.
Added a custom notary sample.

* Prevent multiple custom notaries from being loaded

* Throw if more than once custom notary service is loaded
This commit is contained in:
Andrius Dagys 2017-10-13 10:36:25 +01:00 committed by GitHub
parent 544b761682
commit 7b10e92819
11 changed files with 148 additions and 45 deletions

View File

@ -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()
}
}

View File

@ -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:

View File

@ -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 <https://raft.github.io/>`_ version of the demo will start three distributed notary nodes.
The `BFT SMaRt <https://bft-smart.github.io/library/>`_ version of the demo will start four distributed notary nodes.
* The `Raft <https://raft.github.io/>`_ version of the demo will start three distributed notary nodes.
* The `BFT SMaRt <https://bft-smart.github.io/library/>`_ 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

View File

@ -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
}

View File

@ -16,6 +16,7 @@ Tutorials
flow-testing
running-a-notary
oracles
tutorial-custom-notary
tutorial-tear-offs
tutorial-attachments
event-scheduling

View File

@ -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<Class<out SerializeAsToken>>): List<Class<out SerializeAsToken>> {
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 <T : SerializeAsToken> installCordaService(serviceClass: Class<T>): T {
serviceClass.requireAnnotation<CordaService>()
val service = try {
if (NotaryService::class.java.isAssignableFrom(serviceClass)) {
val serviceContext = AppServiceHubImpl<T>(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<T>(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))

View File

@ -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"
}
}
}
@ -48,7 +54,8 @@ data class RaftConfig(val nodeAddress: NetworkHostAndPort, val clusterAddresses:
data class BFTSMaRtConfiguration constructor(val replicaId: Int,
val clusterAddresses: List<NetworkHostAndPort>,
val debug: Boolean = false,
val exposeRaces: Boolean = false) {
val exposeRaces: Boolean = false
) {
init {
require(replicaId >= 0) { "replicaId cannot be negative" }
}

View File

@ -69,7 +69,7 @@ private class BankOfCordaDriver {
val bigCorpUser = User(BIGCORP_USERNAME, "test",
permissions = setOf(
startFlowPermission<CashPaymentFlow>()))
startNotaryNode(DUMMY_NOTARY.name, validating = false)
startNotaryNode(DUMMY_NOTARY.name, validating = true)
val bankOfCorda = startNode(
providedName = BOC.name,
rpcUsers = listOf(bankUser))

View File

@ -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'

View File

@ -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<String>) = 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) {}
}

View File

@ -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)