Added a distributed notary demo that runs a notary cluster and gets several transactions notarised

This commit is contained in:
Andrius Dagys 2016-11-22 18:09:56 +00:00
parent 758ab6d359
commit 946cfda03e
19 changed files with 414 additions and 12 deletions

View File

@ -0,0 +1,15 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Notary Demo: Run Nodes" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.notarydemo.MainKt" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="raft-notary-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -0,0 +1,15 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Notary Demo: Run Notarisation" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.notarydemo.NotaryDemoKt" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="raft-notary-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -36,10 +36,6 @@ open class TransactionBuilder(
val time: Timestamp? get() = timestamp
init {
notary?.let { signers.add(it.owningKey) }
}
/**
* Creates a copy of the builder.
*/
@ -72,6 +68,7 @@ open class TransactionBuilder(
fun setTime(newTimestamp: Timestamp) {
check(notary != null) { "Only notarised transactions can have a timestamp" }
signers.add(notary!!.owningKey)
check(currentSigs.isEmpty()) { "Cannot change timestamp after signing" }
this.timestamp = newTimestamp
}

View File

@ -12,6 +12,7 @@ so far. We have:
4. The attachment demo, which demonstrates uploading attachments to nodes.
5. The SIMM valuation demo, a large demo which shows two nodes agreeing on a portfolio and valuing the initial margin
using the Standard Initial Margin Model.
6. The distributed notary demo, which demonstrates a single node getting multiple transactions notarised by a distributed (Raft-based) notary.
.. note:: If any demos don't work please jump on our mailing list and let us know.
@ -116,3 +117,46 @@ To run the demo run:
Now open http://localhost:10005/web/simmvaluationdemo and http://localhost:10007/web/simmvaluationdemo to view the two nodes that this
will have started respectively. You can now use the demo by creating trades and agreeing the valuations.
Distributed Notary demo
-----------------------
This is a simple demonstration showing a party getting transactions notarised by a distributed `Raft <https://raft.github.io/>`_-based notary service.
The demo will start three distributed notary nodes, and two counterparty nodes. One of the parties will generate transactions
that move a self-issued asset to the other party, and submit them for notarisation.
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 services client requests, and one signature is sufficient to satisfy the notary composite key requirement.
You will notice that subsequent transactions get signed by different members of the cluster (usually allocated in a random order).
To run from IntelliJ:
1. Open the Corda samples project in IntelliJ and run the ``Notary Demo: Run Nodes`` configuration to start the nodes.
Once all nodes are started you will see several "Node started up and registered in ..." messages.
2. Run ``Notary Demo: Run Notarisation`` to make a call to the "Party" node to initiate notarisation requests.
In a few seconds you will see a message "Notarised 10 transactions" with a list of transaction ids and the signer public keys.
To run from the command line:
1. Run ``./gradlew samples:raft-notary-demo:deployNodes``, which will create node directories with configs under ``samples/raft-notary-demo/build/nodes``.
2. Run ``./samples/raft-notary-demo/build/nodes/runnodes``, which will start the nodes in separate terminal windows/tabs.
Wait until a "Node started up and registered in ..." appears on each of the terminals.
3. Run ``./gradlew samples:raft-notary-demo:notarise`` to make a call to the "Party" node to initiate notarisation requests.
In a few seconds you will see a message "Notarised 10 transactions" with a list of transaction ids and the signer public keys.
Notary nodes store consumed states in a replicated commit log, which is backed by a H2 database on each node.
To ascertain that the commit log is synchronised across the cluster you access and compare each of the nodes' backing stores
by using the H2 web console:
- Firstly, download `H2 web console <http://www.h2database.com/html/download.html>`_ (download the "platform-independent zip"),
and start it using a script in the extracted folder: ``h2/bin/h2.sh`` (or ``h2.bat`` for Windows)
- The H2 web console should start up in a web browser tab. To connect we first need to obtain a JDBC connection string.
Each node outputs its connection string in the terminal window as it starts up. In a terminal window where a node is running,
look for the following string:
``Database connection url is : jdbc:h2:tcp://10.18.0.150:56736/node``
You can use the string on the right to connect to the h2 database: just paste it in to the `JDBC URL` field and click *Connect*.
You will be presented with a web application that enumerates all the available tables and provides an interface for you to query them using SQL.
- The committed states are stored in the ``NOTARY_COMMITTED_STATES`` table. Note that the raw data is not human-readable,
but we're only interested in the row count for this demo.

View File

@ -20,6 +20,11 @@ class Node {
* A list of advertised services ID strings.
*/
protected List<String> advertisedServices = []
/**
* If running a distributed notary, a list of node addresses for joining the Raft cluster
*/
protected List<String> notaryClusterAddresses = []
/**
* Set the list of CorDapps to install to the plugins directory. Each cordapp is a fully qualified Maven
* dependency name, eg: com.example:product-name:0.1
@ -104,6 +109,14 @@ class Node {
ConfigValueFactory.fromAnyRef("$DEFAULT_HOST:$webPort".toString()))
}
/**
* Set the port which to bind the Copycat (Raft) node to
*/
void notaryNodePort(Integer notaryPort) {
config = config.withValue("notaryNodeAddress",
ConfigValueFactory.fromAnyRef("$DEFAULT_HOST:$notaryPort".toString()))
}
/**
* Set the network map address for this node.
*
@ -205,8 +218,10 @@ class Node {
*/
private void installConfig() {
// Adding required default values
config = config.withValue('extraAdvertisedServiceIds',
ConfigValueFactory.fromAnyRef(advertisedServices.join(',')))
config = config.withValue('extraAdvertisedServiceIds', ConfigValueFactory.fromAnyRef(advertisedServices.join(',')))
if (notaryClusterAddresses.size() > 0) {
config = config.withValue('notaryClusterAddresses', ConfigValueFactory.fromIterable(notaryClusterAddresses))
}
def configFileText = config.root().render(new ConfigRenderOptions(false, false, true, false)).split("\n").toList()
// Need to write a temporary file first to use the project.copy, which resolves directories correctly.

View File

@ -356,7 +356,7 @@ open class DriverDSL(
// All other nodes will join the cluster
nodeNames.drop(1).forEach {
val nodeAddress = portAllocation.nextHostAndPort()
val configOverride = mapOf("notaryNodeAddress" to nodeAddress.toString(), "notaryClusterAddress" to notaryClusterAddress.toString())
val configOverride = mapOf("notaryNodeAddress" to nodeAddress.toString(), "notaryClusterAddresses" to listOf(notaryClusterAddress.toString()))
startNode(it, advertisedService, emptyList(), configOverride)
}
}

View File

@ -21,10 +21,9 @@ import java.util.*
* to disk, and sharing them across the cluster. A new node joining the cluster will have to obtain and install a snapshot
* containing the entire JDBC table contents.
*/
class DistributedImmutableMap<K : Any, V : Any>(val db: Database, tableName: String = DEFAULT_TABLE_NAME) : StateMachine(), Snapshottable {
class DistributedImmutableMap<K : Any, V : Any>(val db: Database, tableName: String) : StateMachine(), Snapshottable {
companion object {
private val log = loggerFor<DistributedImmutableMap<*, *>>()
private val DEFAULT_TABLE_NAME = "committed_states"
}
object Commands {

View File

@ -44,6 +44,7 @@ class RaftUniquenessProvider(storagePath: Path, myAddress: HostAndPort, clusterA
db: Database, config: NodeSSLConfiguration) : UniquenessProvider, SingletonSerializeAsToken() {
companion object {
private val log = loggerFor<RaftUniquenessProvider>()
private val DB_TABLE_NAME = "notary_committed_states"
}
private val _clientFuture: CompletableFuture<CopycatClient>
@ -56,7 +57,7 @@ class RaftUniquenessProvider(storagePath: Path, myAddress: HostAndPort, clusterA
init {
log.info("Creating Copycat server, log stored in: ${storagePath.toFile()}")
val stateMachineFactory = { DistributedImmutableMap<String, ByteArray>(db) }
val stateMachineFactory = { DistributedImmutableMap<String, ByteArray>(db, DB_TABLE_NAME) }
val address = Address(myAddress.hostText, myAddress.port)
val storage = buildStorage(storagePath)
val transport = buildTransport(config)

View File

@ -8,6 +8,7 @@ import net.corda.core.serialization.serialize
import net.corda.core.utilities.loggerFor
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
object ServiceIdentityGenerator {
private val log = loggerFor<ServiceIdentityGenerator>()
@ -37,4 +38,13 @@ object ServiceIdentityGenerator {
keyPair.serialize().writeToFile(dir.resolve(privateKeyFile))
}
}
}
fun main(args: Array<String>) {
val dirs = args[0].split(",").map { Paths.get(it) }
val serviceId = args[1]
val serviceName = args[2]
println("Generating service identity for \"$serviceName\"")
ServiceIdentityGenerator.generateToDisk(dirs, serviceId, serviceName)
}

View File

@ -6,4 +6,5 @@ Please refer to `README.md` in the individual project folders. There are the fo
* **irs-demo** A demo showing two nodes agreeing to an interest rate swap and doing fixings using an oracle.
* **trader-demo** A simple driver for exercising the two party trading flow. In this scenario, a buyer wants to purchase some commercial paper by swapping his cash for commercial paper. The seller learns that the buyer exists, and sends them a message to kick off the trade. The seller, having obtained his CP, then quits and the buyer goes back to waiting. The buyer will sell as much CP as he can! **We recommend starting with this demo.**
* **Network-visualiser** A tool that uses a simulation to visualise the interaction and messages between nodes on the Corda network. Currently only works for the IRS demo.
* **simm-valudation-demo** A demo showing two nodes reaching agreement on the valuation of a derivatives portfolio.
* **simm-valudation-demo** A demo showing two nodes reaching agreement on the valuation of a derivatives portfolio.
* **raft-notary-demo** A simple demonstration of a node getting multiple transactions notarised by a distributed (Raft-based) notary.

View File

@ -0,0 +1,5 @@
# Distributed Notary (Raft) Demo
This program is a simple demonstration of a node getting multiple transactions notarised by a distributed (Raft-based) notary.
Please see docs/build/html/running-the-demos.html to learn how to use this demo.

View File

@ -0,0 +1,143 @@
apply plugin: 'java'
apply plugin: 'kotlin'
apply plugin: 'idea'
apply plugin: 'net.corda.plugins.quasar-utils'
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'net.corda.plugins.cordformation'
apply plugin: 'maven-publish'
ext {
deployTo = "./build/nodes"
notaryType = "corda.notary.validating.raft"
notaryName = "Raft"
advertisedNotary = "$notaryType|$notaryName"
}
repositories {
mavenLocal()
mavenCentral()
jcenter()
maven {
url 'https://dl.bintray.com/kotlin/exposed'
}
}
sourceSets {
main {
resources {
srcDir "../../config/dev"
}
}
test {
resources {
srcDir "../../config/test"
}
}
}
configurations {
integrationTestCompile.extendsFrom testCompile
integrationTestRuntime.extendsFrom testRuntime
}
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
testCompile group: 'junit', name: 'junit', version: '4.11'
// Corda integration dependencies
compile "net.corda:corda:$corda_version" // TODO
compile project(':core')
compile project(':client')
compile project(':node')
compile project(':test-utils')
// Javax is required for webapis
compile "org.glassfish.jersey.core:jersey-server:${jersey_version}"
}
idea {
module {
downloadJavadoc = true // defaults to false
downloadSources = true
}
}
publishing {
publications {
jarAndSources(MavenPublication) {
from components.java
artifactId 'raftnotarydemo'
artifact sourceJar
artifact javadocJar
}
}
}
task generateNotaryIdentity(type: JavaExec) {
classpath = sourceSets.main.runtimeClasspath
main = "net.corda.node.utilities.ServiceIdentityGeneratorKt"
def nodeDirs = ["$deployTo/notary1", "$deployTo/notary2", "$deployTo/notary3"].join(",")
args = [nodeDirs, notaryType, notaryName]
}
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: [':install', 'build', 'generateNotaryIdentity']) {
directory deployTo
networkMap "Notary 1"
node {
name "Notary 1"
dirName "notary1"
nearestCity "London"
advertisedServices = [advertisedNotary]
artemisPort 10002
webPort 10009
cordapps = []
notaryNodePort 11002
}
node {
name "Notary 2"
dirName "notary2"
nearestCity "London"
advertisedServices = [advertisedNotary]
artemisPort 10004
webPort 10005
cordapps = []
notaryNodePort 11004
notaryClusterAddresses = ["localhost:11002"]
}
node {
name "Notary 3"
dirName "notary3"
nearestCity "London"
advertisedServices = [advertisedNotary]
artemisPort 10006
webPort 10007
cordapps = []
notaryNodePort 11006
notaryClusterAddresses = ["localhost:11002"]
}
node {
name "Party"
dirName "party"
nearestCity "London"
advertisedServices = []
artemisPort 10008
webPort 10003
cordapps = []
}
node {
name "Counterparty"
dirName "counterparty"
nearestCity "New York"
advertisedServices = []
artemisPort 10010
webPort 10011
cordapps = []
}
}
task notarise(type: JavaExec) {
classpath = sourceSets.main.runtimeClasspath
main = 'net.corda.notarydemo.NotaryDemoKt'
}

View File

@ -0,0 +1,3 @@
name=Notary Demo
group=net.corda
kotlin.incremental=false

View File

@ -0,0 +1,14 @@
package net.corda.notarydemo
import net.corda.node.driver.driver
import net.corda.node.services.transactions.RaftValidatingNotaryService
/** Creates and starts all nodes required for the demo. */
fun main(args: Array<String>) {
driver(dsl = {
startNode("Party")
startNode("Counterparty")
startNotaryCluster("Raft notary", clusterSize = 3, type = RaftValidatingNotaryService.type)
waitForAllNodesToFinish()
}, isDebug = true)
}

View File

@ -0,0 +1,30 @@
package net.corda.notarydemo
import com.google.common.net.HostAndPort
import okhttp3.OkHttpClient
import okhttp3.Request
import java.util.concurrent.TimeUnit
fun main(args: Array<String>) {
val api = NotaryDemoClientApi(HostAndPort.fromString("localhost:10003"))
api.startNotarisation()
}
/** Interface for using the notary demo API from a client. */
private class NotaryDemoClientApi(val hostAndPort: HostAndPort) {
companion object {
private val API_ROOT = "api/notarydemo"
private val TRANSACTION_COUNT = 10
}
/** Makes a call to the demo api to start transaction notarisation. */
fun startNotarisation() {
val request = buildRequest()
val response = buildClient().newCall(request).execute()
println(response.body().string())
require(response.isSuccessful)
}
private fun buildRequest() = Request.Builder().url("http://$hostAndPort/$API_ROOT/notarise/$TRANSACTION_COUNT").build()
private fun buildClient() = OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS).readTimeout(60, TimeUnit.SECONDS).build()
}

View File

@ -0,0 +1,91 @@
package net.corda.notarydemo.api
import net.corda.core.contracts.DummyContract
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.toStringShort
import net.corda.core.node.ServiceHub
import net.corda.core.node.recordTransactions
import net.corda.core.transactions.SignedTransaction
import net.corda.flows.NotaryFlow
import java.util.*
import javax.ws.rs.GET
import javax.ws.rs.Path
import javax.ws.rs.PathParam
import javax.ws.rs.core.Response
@Path("notarydemo")
class NotaryDemoApi(val services: ServiceHub) {
private val notary by lazy {
services.networkMapCache.getAnyNotary() ?: throw IllegalStateException("No notary found on the network")
}
private val counterpartyNode by lazy {
services.networkMapCache.getNodeByLegalName("Counterparty") ?: throw IllegalStateException("Counterparty not found")
}
private val random = Random()
@GET
@Path("/notarise/{count}")
fun notarise(@PathParam("count") count: Int): Response {
val transactions = buildTransactions(count)
val signers = notariseTransactions(transactions)
val response = buildResponse(transactions, signers)
return Response.ok(response).build()
}
/**
* Builds a number of dummy transactions (as specified by [count]). The party first self-issues a state (asset),
* and builds a transaction to transfer the asset to the counterparty. The *move* transaction requires notarisation,
* as it consumes the original asset and creates a copy with the new owner as its output.
*/
private fun buildTransactions(count: Int): List<SignedTransaction> {
val myIdentity = services.myInfo.legalIdentity
val myKeyPair = services.legalIdentityKey
val moveTransactions = (1..count).map {
// Self issue an asset
val issueTx = DummyContract.generateInitial(myIdentity.ref(0), random.nextInt(), notary).apply {
signWith(myKeyPair)
}
services.recordTransactions(issueTx.toSignedTransaction())
// Move ownership of the asset to the counterparty
val counterPartyKey = counterpartyNode.legalIdentity.owningKey
val asset = issueTx.toWireTransaction().outRef<DummyContract.SingleOwnerState>(0)
val moveTx = DummyContract.move(asset, counterPartyKey).apply {
signWith(myKeyPair)
}
// We don't check signatures because we know that the notary's signature is missing
moveTx.toSignedTransaction(checkSufficientSignatures = false)
}
return moveTransactions
}
/**
* For every transactions invokes the notary flow and obtains a notary signature.
* The signer can be any of the nodes in the notary cluster.
*
* @return a list of encoded signer public keys one for every transaction
*/
private fun notariseTransactions(transactions: List<SignedTransaction>): List<String> {
val signatureFutures = transactions.map {
val protocol = NotaryFlow.Client::class.java
services.invokeFlowAsync<DigitalSignature.WithKey>(protocol, it).resultFuture
}
val signers = signatureFutures.map { it.get().by.toStringShort() }
return signers
}
/** Builds a response for the caller containing the list of transaction ids and corresponding signer keys. */
private fun buildResponse(transactions: List<SignedTransaction>, signers: List<String>): String {
val transactionSigners = transactions.zip(signers).map {
val (tx, signer) = it
"Tx [${tx.tx.id.prefixChars()}..] signed by $signer"
}.joinToString("\n")
val response = "Notary: \"${notary.name}\", with composite key: ${notary.owningKey}\n" +
"Notarised ${transactions.size} transactions:\n" + transactionSigners
return response
}
}

View File

@ -0,0 +1,16 @@
package net.corda.notarydemo.plugin
import net.corda.core.node.CordaPluginRegistry
import net.corda.core.transactions.SignedTransaction
import net.corda.flows.NotaryFlow
import net.corda.notarydemo.api.NotaryDemoApi
class NotaryDemoPlugin : CordaPluginRegistry() {
// A list of classes that expose web APIs.
override val webApis: List<Class<*>> = listOf(NotaryDemoApi::class.java)
// A list of protocols that are required for this cordapp
override val requiredFlows: Map<String, Set<String>> = mapOf(
NotaryFlow.Client::class.java.name to setOf(SignedTransaction::class.java.name, setOf(Unit).javaClass.name)
)
override val servicePlugins: List<Class<*>> = listOf()
}

View File

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

View File

@ -15,4 +15,5 @@ include 'samples:attachment-demo'
include 'samples:trader-demo'
include 'samples:irs-demo'
include 'samples:network-visualiser'
include 'samples:simm-valuation-demo'
include 'samples:simm-valuation-demo'
include 'samples:raft-notary-demo'