diff --git a/.idea/modules.xml b/.idea/modules.xml
index 5517a9dc4a..63596a0a6f 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -21,6 +21,9 @@
+
+
+
@@ -34,4 +37,4 @@
-
\ No newline at end of file
+
diff --git a/core/src/main/kotlin/com/r3corda/core/crypto/X509Utilities.kt b/core/src/main/kotlin/com/r3corda/core/crypto/X509Utilities.kt
index 3c7164800c..0bad95ea41 100644
--- a/core/src/main/kotlin/com/r3corda/core/crypto/X509Utilities.kt
+++ b/core/src/main/kotlin/com/r3corda/core/crypto/X509Utilities.kt
@@ -1,6 +1,7 @@
package com.r3corda.core.crypto
import com.r3corda.core.random63BitValue
+import com.r3corda.core.use
import org.bouncycastle.asn1.ASN1Encodable
import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.DERSequence
@@ -16,9 +17,14 @@ import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
+import org.bouncycastle.pkcs.PKCS10CertificationRequest
+import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder
import org.bouncycastle.util.IPAddress
import org.bouncycastle.util.io.pem.PemReader
-import java.io.*
+import java.io.ByteArrayInputStream
+import java.io.FileReader
+import java.io.FileWriter
+import java.io.InputStream
import java.math.BigInteger
import java.net.InetAddress
import java.nio.file.Files
@@ -41,10 +47,14 @@ object X509Utilities {
val ECDSA_CURVE = "secp256r1"
val KEYSTORE_TYPE = "JKS"
- val CA_CERT_ALIAS = "CA Cert"
- val CERT_PRIVATE_KEY_ALIAS = "Server Private Key"
- val ROOT_CA_CERT_PRIVATE_KEY_ALIAS = "Root CA Private Key"
- val INTERMEDIATE_CA_PRIVATE_KEY_ALIAS = "Intermediate CA Private Key"
+
+ // Aliases for private keys and certificates.
+ val CORDA_ROOT_CA_PRIVATE_KEY = "cordarootcaprivatekey"
+ val CORDA_ROOT_CA = "cordarootca"
+ val CORDA_INTERMEDIATE_CA_PRIVATE_KEY = "cordaintermediatecaprivatekey"
+ val CORDA_INTERMEDIATE_CA = "cordaintermediateca"
+ val CORDA_CLIENT_CA_PRIVATE_KEY = "cordaclientcaprivatekey"
+ val CORDA_CLIENT_CA = "cordaclientca"
init {
Security.addProvider(BouncyCastleProvider()) // register Bouncy Castle Crypto Provider required to sign certificates
@@ -108,8 +118,15 @@ object X509Utilities {
return nameBuilder.build()
}
+ fun getX509Name(myLegalName: String, nearestCity: String, email: String): X500Name {
+ return X500NameBuilder(BCStyle.INSTANCE)
+ .addRDN(BCStyle.CN, myLegalName)
+ .addRDN(BCStyle.L, nearestCity)
+ .addRDN(BCStyle.E, email).build()
+ }
+
/**
- * Helper method to either open an existing keystore for modification, or create a new blank keystore
+ * Helper method to either open an existing keystore for modification, or create a new blank keystore.
* @param keyStoreFilePath location of KeyStore file
* @param storePassword password to open the store. This does not have to be the same password as any keys stored,
* but for SSL purposes this is recommended.
@@ -119,13 +136,10 @@ object X509Utilities {
val pass = storePassword.toCharArray()
val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
if (Files.exists(keyStoreFilePath)) {
- val input = FileInputStream(keyStoreFilePath.toFile())
- input.use {
- keyStore.load(input, pass)
- }
+ keyStoreFilePath.use { keyStore.load(it, pass) }
} else {
keyStore.load(null, pass)
- val output = FileOutputStream(keyStoreFilePath.toFile())
+ val output = Files.newOutputStream(keyStoreFilePath)
output.use {
keyStore.store(output, pass)
}
@@ -143,10 +157,7 @@ object X509Utilities {
fun loadKeyStore(keyStoreFilePath: Path, storePassword: String): KeyStore {
val pass = storePassword.toCharArray()
val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
- val input = FileInputStream(keyStoreFilePath.toFile())
- input.use {
- keyStore.load(input, pass)
- }
+ keyStoreFilePath.use { keyStore.load(it, pass) }
return keyStore
}
@@ -175,7 +186,7 @@ object X509Utilities {
*/
fun saveKeyStore(keyStore: KeyStore, keyStoreFilePath: Path, storePassword: String) {
val pass = storePassword.toCharArray()
- val output = FileOutputStream(keyStoreFilePath.toFile())
+ val output = Files.newOutputStream(keyStoreFilePath)
output.use {
keyStore.store(output, pass)
}
@@ -189,7 +200,7 @@ object X509Utilities {
* but for SSL purposes this is recommended.
* @param chain the sequence of certificates starting with the public key certificate for this key and extending to the root CA cert
*/
- private fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain: Array) {
+ fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain: Array) {
try {
this.deleteEntry(alias)
} catch (kse: KeyStoreException) {
@@ -203,7 +214,7 @@ object X509Utilities {
* @param alias name to record the public certificate under
* @param cert certificate to store
*/
- private fun KeyStore.addOrReplaceCertificate(alias: String, cert: Certificate) {
+ fun KeyStore.addOrReplaceCertificate(alias: String, cert: Certificate) {
try {
this.deleteEntry(alias)
} catch (kse: KeyStoreException) {
@@ -224,6 +235,24 @@ object X509Utilities {
return keyGen.generateKeyPair()
}
+ /**
+ * Create certificate signing request using provided information.
+ *
+ * @param myLegalName The legal name of your organization. This should not be abbreviated and should include suffixes such as Inc, Corp, or LLC.
+ * @param nearestCity The city where your organization is located.
+ * @param email An email address used to contact your organization.
+ * @param keyPair Standard curve ECDSA KeyPair generated for TLS.
+ * @return The generated Certificate signing request.
+ */
+ fun createCertificateSigningRequest(myLegalName: String, nearestCity: String, email: String, keyPair: KeyPair): PKCS10CertificationRequest {
+ val subject = getX509Name(myLegalName, nearestCity, email)
+ val signer = JcaContentSignerBuilder(SIGNATURE_ALGORITHM)
+ .setProvider(BouncyCastleProvider.PROVIDER_NAME)
+ .build(keyPair.private)
+ return JcaPKCS10CertificationRequestBuilder(subject, keyPair.public).build(signer)
+ }
+
+
/**
* Helper data class to pass around public certificate and KeyPair entities when using CA certs
*/
@@ -236,10 +265,10 @@ object X509Utilities {
* @return A data class is returned containing the new root CA Cert and its KeyPair for signing downstream certificates.
* Note the generated certificate tree is capped at max depth of 2 to be in line with commercially available certificates
*/
- fun createSelfSignedCACert(domain: String): CACertAndKey {
+ fun createSelfSignedCACert(myLegalName: String): CACertAndKey {
val keyPair = generateECDSAKeyPairForSSL()
- val issuer = getDevX509Name(domain)
+ val issuer = getDevX509Name(myLegalName)
val serial = BigInteger.valueOf(random63BitValue())
val subject = issuer
val pubKey = keyPair.public
@@ -292,7 +321,7 @@ object X509Utilities {
// Ten year certificate validity
// TODO how do we manage certificate expiry, revocation and loss
- val window = getCertificateValidityWindow(0, 365*10, certificateAuthority.certificate.notBefore, certificateAuthority.certificate.notAfter)
+ val window = getCertificateValidityWindow(0, 365 * 10, certificateAuthority.certificate.notBefore, certificateAuthority.certificate.notAfter)
val builder = JcaX509v3CertificateBuilder(
issuer, serial, window.first, window.second, subject, pubKey)
@@ -341,7 +370,7 @@ object X509Utilities {
// Ten year certificate validity
// TODO how do we manage certificate expiry, revocation and loss
- val window = getCertificateValidityWindow(0, 365*10, certificateAuthority.certificate.notBefore, certificateAuthority.certificate.notAfter)
+ val window = getCertificateValidityWindow(0, 365 * 10, certificateAuthority.certificate.notBefore, certificateAuthority.certificate.notAfter)
val builder = JcaX509v3CertificateBuilder(issuer, serial, window.first, window.second, subject, publicKey)
builder.addExtension(Extension.subjectKeyIdentifier, false, createSubjectKeyIdentifier(publicKey))
@@ -420,7 +449,7 @@ object X509Utilities {
}
/**
- * Extract public and private keys from a KeyStore file assuming storage alias is know
+ * Extract public and private keys from a KeyStore file assuming storage alias is known.
* @param keyStoreFilePath Path to load KeyStore from
* @param storePassword Password to unlock the KeyStore
* @param keyPassword Password to unlock the private key entries
@@ -437,6 +466,32 @@ object X509Utilities {
return KeyPair(certificate.publicKey, keyEntry)
}
+ /**
+ * Extract public and private keys from a KeyStore file assuming storage alias is known, or
+ * create a new pair of keys using the provided function if the keys not exist.
+ * @param keyStoreFilePath Path to load KeyStore from
+ * @param storePassword Password to unlock the KeyStore
+ * @param keyPassword Password to unlock the private key entries
+ * @param alias The name to lookup the Key and Certificate chain from
+ * @param keyGenerator Function for generating new keys
+ * @return The KeyPair found in the KeyStore under the specified alias
+ */
+ fun loadOrCreateKeyPairFromKeyStore(keyStoreFilePath: Path, storePassword: String, keyPassword: String,
+ alias: String, keyGenerator: () -> CACertAndKey): KeyPair {
+ val keyStore = X509Utilities.loadKeyStore(keyStoreFilePath, storePassword)
+ if (!keyStore.containsAlias(alias)) {
+ val selfSignCert = keyGenerator()
+ // Save to the key store.
+ keyStore.addOrReplaceKey(alias, selfSignCert.keypair.private, keyPassword.toCharArray(), arrayOf(selfSignCert.certificate))
+ X509Utilities.saveKeyStore(keyStore, keyStoreFilePath, storePassword)
+ }
+
+ val certificate = keyStore.getCertificate(alias)
+ val keyEntry = keyStore.getKey(alias, keyPassword.toCharArray())
+
+ return KeyPair(certificate.publicKey, keyEntry as PrivateKey)
+ }
+
/**
* Extract public X509 certificate from a KeyStore file assuming storage alias is know
* @param keyStoreFilePath Path to load KeyStore from
@@ -475,9 +530,9 @@ object X509Utilities {
val keypass = keyPassword.toCharArray()
val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword)
- keyStore.addOrReplaceKey(ROOT_CA_CERT_PRIVATE_KEY_ALIAS, rootCA.keypair.private, keypass, arrayOf(rootCA.certificate))
+ keyStore.addOrReplaceKey(CORDA_ROOT_CA_PRIVATE_KEY, rootCA.keypair.private, keypass, arrayOf(rootCA.certificate))
- keyStore.addOrReplaceKey(INTERMEDIATE_CA_PRIVATE_KEY_ALIAS,
+ keyStore.addOrReplaceKey(CORDA_INTERMEDIATE_CA_PRIVATE_KEY,
intermediateCA.keypair.private,
keypass,
arrayOf(intermediateCA.certificate, rootCA.certificate))
@@ -486,7 +541,8 @@ object X509Utilities {
val trustStore = loadOrCreateKeyStore(trustStoreFilePath, trustStorePassword)
- trustStore.addOrReplaceCertificate(CA_CERT_ALIAS, rootCA.certificate)
+ trustStore.addOrReplaceCertificate(CORDA_ROOT_CA, rootCA.certificate)
+ trustStore.addOrReplaceCertificate(CORDA_INTERMEDIATE_CA, intermediateCA.certificate)
saveKeyStore(trustStore, trustStoreFilePath, trustStorePassword)
@@ -527,10 +583,10 @@ object X509Utilities {
caKeyPassword: String): KeyStore {
val rootCA = X509Utilities.loadCertificateAndKey(caKeyStore,
caKeyPassword,
- X509Utilities.ROOT_CA_CERT_PRIVATE_KEY_ALIAS)
+ X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY)
val intermediateCA = X509Utilities.loadCertificateAndKey(caKeyStore,
caKeyPassword,
- X509Utilities.INTERMEDIATE_CA_PRIVATE_KEY_ALIAS)
+ X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY)
val serverKey = X509Utilities.generateECDSAKeyPairForSSL()
val host = InetAddress.getLocalHost()
@@ -538,18 +594,18 @@ object X509Utilities {
val serverCert = X509Utilities.createServerCert(subject,
serverKey.public,
intermediateCA,
- if(host.canonicalHostName == host.hostName) listOf() else listOf(host.hostName),
+ if (host.canonicalHostName == host.hostName) listOf() else listOf(host.hostName),
listOf(host.hostAddress))
val keypass = keyPassword.toCharArray()
val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword)
- keyStore.addOrReplaceKey(CERT_PRIVATE_KEY_ALIAS,
+ keyStore.addOrReplaceKey(CORDA_CLIENT_CA_PRIVATE_KEY,
serverKey.private,
keypass,
arrayOf(serverCert, intermediateCA.certificate, rootCA.certificate))
- keyStore.addOrReplaceCertificate(CA_CERT_ALIAS, rootCA.certificate)
+ keyStore.addOrReplaceCertificate(CORDA_CLIENT_CA, serverCert)
saveKeyStore(keyStore, keyStoreFilePath, storePassword)
diff --git a/core/src/main/kotlin/com/r3corda/core/node/services/NetworkMapCache.kt b/core/src/main/kotlin/com/r3corda/core/node/services/NetworkMapCache.kt
index 3235eca87e..ce4b8239f3 100644
--- a/core/src/main/kotlin/com/r3corda/core/node/services/NetworkMapCache.kt
+++ b/core/src/main/kotlin/com/r3corda/core/node/services/NetworkMapCache.kt
@@ -1,5 +1,6 @@
package com.r3corda.core.node.services
+import com.google.common.annotations.VisibleForTesting
import com.google.common.util.concurrent.ListenableFuture
import com.r3corda.core.contracts.Contract
import com.r3corda.core.crypto.Party
@@ -32,6 +33,8 @@ interface NetworkMapCache {
val partyNodes: List
/** Tracks changes to the network map cache */
val changed: Observable
+ /** Future to track completion of the NetworkMapService registration. */
+ val mapServiceRegistered: ListenableFuture
/**
* A list of nodes that advertise a regulatory service. Identifying the correct regulator for a trade is outside
@@ -97,6 +100,12 @@ interface NetworkMapCache {
* @param service the network map service to fetch current state from.
*/
fun deregisterForUpdates(net: MessagingService, service: NodeInfo): ListenableFuture
+
+ /**
+ * For testing where the network map cache is manipulated marks the service as immediately ready.
+ */
+ @VisibleForTesting
+ fun runWithoutMapService()
}
sealed class NetworkCacheError : Exception() {
diff --git a/core/src/test/kotlin/com/r3corda/core/crypto/X509UtilitiesTest.kt b/core/src/test/kotlin/com/r3corda/core/crypto/X509UtilitiesTest.kt
index b95ffe0d60..e046f3743a 100644
--- a/core/src/test/kotlin/com/r3corda/core/crypto/X509UtilitiesTest.kt
+++ b/core/src/test/kotlin/com/r3corda/core/crypto/X509UtilitiesTest.kt
@@ -97,9 +97,9 @@ class X509UtilitiesTest {
// Load back generated root CA Cert and private key from keystore and check against copy in truststore
val keyStore = X509Utilities.loadKeyStore(tmpKeyStore, "keystorepass")
val trustStore = X509Utilities.loadKeyStore(tmpTrustStore, "trustpass")
- val rootCaCert = keyStore.getCertificate(X509Utilities.ROOT_CA_CERT_PRIVATE_KEY_ALIAS) as X509Certificate
- val rootCaPrivateKey = keyStore.getKey(X509Utilities.ROOT_CA_CERT_PRIVATE_KEY_ALIAS, "keypass".toCharArray()) as PrivateKey
- val rootCaFromTrustStore = trustStore.getCertificate(X509Utilities.CA_CERT_ALIAS) as X509Certificate
+ val rootCaCert = keyStore.getCertificate(X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY) as X509Certificate
+ val rootCaPrivateKey = keyStore.getKey(X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY, "keypass".toCharArray()) as PrivateKey
+ val rootCaFromTrustStore = trustStore.getCertificate(X509Utilities.CORDA_ROOT_CA) as X509Certificate
assertEquals(rootCaCert, rootCaFromTrustStore)
rootCaCert.checkValidity(Date())
rootCaCert.verify(rootCaCert.publicKey)
@@ -116,8 +116,8 @@ class X509UtilitiesTest {
assertTrue { caVerifier.verify(caSignature) }
// Load back generated intermediate CA Cert and private key
- val intermediateCaCert = keyStore.getCertificate(X509Utilities.INTERMEDIATE_CA_PRIVATE_KEY_ALIAS) as X509Certificate
- val intermediateCaCertPrivateKey = keyStore.getKey(X509Utilities.INTERMEDIATE_CA_PRIVATE_KEY_ALIAS, "keypass".toCharArray()) as PrivateKey
+ val intermediateCaCert = keyStore.getCertificate(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY) as X509Certificate
+ val intermediateCaCertPrivateKey = keyStore.getKey(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY, "keypass".toCharArray()) as PrivateKey
intermediateCaCert.checkValidity(Date())
intermediateCaCert.verify(rootCaCert.publicKey)
@@ -148,14 +148,14 @@ class X509UtilitiesTest {
// Load signing intermediate CA cert
val caKeyStore = X509Utilities.loadKeyStore(tmpCAKeyStore, "cakeystorepass")
- val caCertAndKey = X509Utilities.loadCertificateAndKey(caKeyStore, "cakeypass", X509Utilities.INTERMEDIATE_CA_PRIVATE_KEY_ALIAS)
+ val caCertAndKey = X509Utilities.loadCertificateAndKey(caKeyStore, "cakeypass", X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY)
// Generate server cert and private key and populate another keystore suitable for SSL
X509Utilities.createKeystoreForSSL(tmpServerKeyStore, "serverstorepass", "serverkeypass", caKeyStore, "cakeypass")
// Load back server certificate
val serverKeyStore = X509Utilities.loadKeyStore(tmpServerKeyStore, "serverstorepass")
- val serverCertAndKey = X509Utilities.loadCertificateAndKey(serverKeyStore, "serverkeypass", X509Utilities.CERT_PRIVATE_KEY_ALIAS)
+ val serverCertAndKey = X509Utilities.loadCertificateAndKey(serverKeyStore, "serverkeypass", X509Utilities.CORDA_CLIENT_CA_PRIVATE_KEY)
serverCertAndKey.certificate.checkValidity(Date())
serverCertAndKey.certificate.verify(caCertAndKey.certificate.publicKey)
diff --git a/docs/source/index.rst b/docs/source/index.rst
index c34068fa09..e1372461c4 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -71,7 +71,7 @@ Read on to learn:
release-process
release-notes
- visualiser
+ network-simulator
codestyle
building-the-docs
diff --git a/docs/source/network-simulator.png b/docs/source/network-simulator.png
new file mode 100644
index 0000000000..3706064338
Binary files /dev/null and b/docs/source/network-simulator.png differ
diff --git a/docs/source/network-simulator.rst b/docs/source/network-simulator.rst
new file mode 100644
index 0000000000..93dba16ccb
--- /dev/null
+++ b/docs/source/network-simulator.rst
@@ -0,0 +1,41 @@
+Network Simulator
+=================
+
+A network simulator is provided which shows traffic between nodes through the lifecycle of an interest rate swap
+contract. It can optionally also show network setup, during which nodes register themselves with the network
+map service and are notified of the changes to the map. The network simulator is run from the command line via Gradle:
+
+**Windows**::
+
+ gradlew.bat network-simulator:run
+
+**Other**::
+
+ ./gradlew network-simulator:run
+
+Interface
+---------
+
+.. image:: network-simulator.png
+
+The network simulator can be run automatically, or stepped manually through each step of the interest rate swap. The
+options on the simulator window are:
+
+Simulate initialisation
+ If checked, the nodes registering with the network map is shown. Normally this setup step
+ is not shown, but may be of interest to understand the details of node discovery.
+Run
+ Runs the network simulation in automatic mode, in which it progresses each step on a timed basis. Once running,
+ the simulation can be paused in order to manually progress it, or reset.
+Next
+ Manually progress the simulation to the next step.
+Reset
+ Reset the simulation (only available when paused).
+Map/Circle
+ How the nodes are shown, by default nodes are rendered on a world map, but alternatively they can rendered
+ in a circle layout.
+
+While the simulation runs, details of the steps currently being executed are shown in a sidebar on the left hand side
+of the window.
+
+.. TODO: Add documentation on how to use with different contracts for testing/debugging
diff --git a/docs/source/visualiser.png b/docs/source/visualiser.png
deleted file mode 100644
index 51a48000d0..0000000000
Binary files a/docs/source/visualiser.png and /dev/null differ
diff --git a/docs/source/visualiser.rst b/docs/source/visualiser.rst
deleted file mode 100644
index e5167cb025..0000000000
--- a/docs/source/visualiser.rst
+++ /dev/null
@@ -1,78 +0,0 @@
-.. highlight:: kotlin
-.. raw:: html
-
-
-
-
-Using the visualiser
-====================
-
-In order to assist with understanding of the state model, the repository includes a simple graph visualiser. The
-visualiser is integrated with the unit test framework and the same domain specific language. It is currently very
-early and the diagrams it produces are not especially beautiful. The intention is to improve it in future releases.
-
-.. image:: visualiser.png
-
-An example of how to use it can be seen in ``src/test/kotlin/contracts/CommercialPaperTests.kt``.
-
-Briefly, define a set of transactions in a group using the same DSL that is used in the unit tests. Here's an example
-of a trade lifecycle using the commercial paper contract
-
-.. container:: codeset
-
- .. sourcecode:: kotlin
-
- val group: TransactionGroupDSL = transactionGroupFor() {
- roots {
- transaction(900.DOLLARS.CASH `owned by` ALICE label "alice's $900")
- transaction(someProfits.CASH `owned by` MEGA_CORP_PUBKEY label "some profits")
- }
-
- // Some CP is issued onto the ledger by MegaCorp.
- transaction("Issuance") {
- output("paper") { PAPER_1 }
- arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
- }
-
- // The CP is sold to alice for her $900, $100 less than the face value. At 10% interest after only 7 days,
- // that sounds a bit too good to be true!
- transaction("Trade") {
- input("paper")
- input("alice's $900")
- output("borrowed $900") { 900.DOLLARS.CASH `owned by` MEGA_CORP_PUBKEY }
- output("alice's paper") { "paper".output `owned by` ALICE }
- arg(ALICE) { Cash.Commands.Move() }
- arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
- }
-
- // Time passes, and Alice redeem's her CP for $1000, netting a $100 profit. MegaCorp has received $1200
- // as a single payment from somewhere and uses it to pay Alice off, keeping the remaining $200 as change.
- transaction("Redemption", redemptionTime) {
- input("alice's paper")
- input("some profits")
-
- output("Alice's profit") { aliceGetsBack.CASH `owned by` ALICE }
- output("Change") { (someProfits - aliceGetsBack).CASH `owned by` MEGA_CORP_PUBKEY }
- if (!destroyPaperAtRedemption)
- output { "paper".output }
-
- arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
- arg(ALICE) { CommercialPaper.Commands.Redeem() }
- }
- }
-
-Now you can define a main method in your unit test class that takes the ``TransactionGroupDSL`` object and uses it:
-
-.. container:: codeset
-
- .. sourcecode:: kotlin
-
- CommercialPaperTests().trade().visualise()
-
-This will open up a window with the following features:
-
-* The nodes can be dragged around to try and obtain a better layout (an improved layout algorithm will be a future
- feature).
-* States are rendered as circles. Transactions are small blue squares. Commands are small diamonds.
-* Clicking a state will open up a window that shows its fields.
-
diff --git a/network-simulator/build.gradle b/network-simulator/build.gradle
new file mode 100644
index 0000000000..e147512b57
--- /dev/null
+++ b/network-simulator/build.gradle
@@ -0,0 +1,59 @@
+buildscript {
+ repositories {
+ mavenCentral()
+ maven {
+ url 'http://oss.sonatype.org/content/repositories/snapshots'
+ }
+ }
+ dependencies {
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+apply plugin: 'java'
+apply plugin: 'kotlin'
+apply plugin: 'application'
+apply plugin: 'us.kirchmeier.capsule'
+
+group 'com.r3cev.prototyping'
+version '1.0-SNAPSHOT'
+
+sourceCompatibility = 1.5
+
+repositories {
+ mavenLocal()
+ mavenCentral()
+ jcenter()
+ maven {
+ url 'http://oss.sonatype.org/content/repositories/snapshots'
+ }
+ maven {
+ url 'https://dl.bintray.com/kotlin/exposed'
+ }
+}
+
+applicationDefaultJvmArgs = ["-javaagent:${rootProject.configurations.quasar.singleFile}"]
+mainClassName = 'com.r3cev.corda.netmap.NetworkMapVisualiserKt'
+
+dependencies {
+ compile project(":core")
+ compile project(":node")
+ compile project(":contracts")
+ compile rootProject
+
+ // GraphStream: For visualisation
+ compile "org.graphstream:gs-core:1.3"
+ compile "org.graphstream:gs-ui:1.3"
+
+ compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+ testCompile group: 'junit', name: 'junit', version: '4.11'
+}
+
+task capsule(type: FatCapsule) {
+ applicationClass 'com.r3cev.corda.netmap.NetworkExplorerKt'
+ reallyExecutable
+ capsuleManifest {
+ minJavaVersion = '1.8.0'
+ javaAgents = [rootProject.configurations.quasar.singleFile.name]
+ }
+}
diff --git a/network-simulator/src/main/kotlin/com/r3cev/corda/netmap/NetworkMapVisualiser.kt b/network-simulator/src/main/kotlin/com/r3cev/corda/netmap/NetworkMapVisualiser.kt
new file mode 100644
index 0000000000..2cc862ab6c
--- /dev/null
+++ b/network-simulator/src/main/kotlin/com/r3cev/corda/netmap/NetworkMapVisualiser.kt
@@ -0,0 +1,358 @@
+/*
+ * Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
+ * pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
+ * set forth therein.
+ *
+ * All other rights reserved.
+ */
+
+package com.r3cev.corda.netmap
+
+import com.r3corda.core.messaging.SingleMessageRecipient
+import com.r3corda.core.then
+import com.r3corda.core.utilities.ProgressTracker
+import com.r3corda.testing.node.InMemoryMessagingNetwork
+import com.r3corda.testing.node.MockNetwork
+import com.r3corda.simulation.IRSSimulation
+import com.r3corda.simulation.Simulation
+import com.r3corda.node.services.network.NetworkMapService
+import javafx.animation.*
+import javafx.application.Application
+import javafx.application.Platform
+import javafx.beans.property.SimpleDoubleProperty
+import javafx.beans.value.WritableValue
+import javafx.geometry.Insets
+import javafx.geometry.Pos
+import javafx.scene.control.*
+import javafx.scene.input.KeyCode
+import javafx.scene.input.KeyCodeCombination
+import javafx.scene.layout.*
+import javafx.scene.paint.Color
+import javafx.scene.shape.Circle
+import javafx.scene.shape.Line
+import javafx.scene.shape.Polygon
+import javafx.stage.Stage
+import javafx.util.Duration
+import rx.Scheduler
+import rx.schedulers.Schedulers
+import java.nio.file.Files
+import java.nio.file.Paths
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
+import java.util.*
+import kotlin.concurrent.schedule
+import kotlin.concurrent.scheduleAtFixedRate
+import kotlin.system.exitProcess
+import com.r3cev.corda.netmap.VisualiserViewModel.Style
+
+fun WritableValue.keyValue(endValue: T, interpolator: Interpolator = Interpolator.EASE_OUT) = KeyValue(this, endValue, interpolator)
+
+// TODO: This code is all horribly ugly. Refactor to use TornadoFX to clean it up.
+
+class NetworkMapVisualiser : Application() {
+ enum class NodeType {
+ BANK, SERVICE
+ }
+
+ enum class RunPauseButtonLabel {
+ RUN, PAUSE;
+
+ override fun toString(): String {
+ return name.toLowerCase().capitalize()
+ }
+ }
+
+ sealed class RunningPausedState {
+ class Running(val tickTimer: TimerTask): RunningPausedState()
+ class Paused(): RunningPausedState()
+
+ val buttonLabel: RunPauseButtonLabel
+ get() {
+ return when (this) {
+ is RunningPausedState.Running -> RunPauseButtonLabel.PAUSE
+ is RunningPausedState.Paused -> RunPauseButtonLabel.RUN
+ }
+ }
+ }
+
+ private val view = VisualiserView()
+ private val viewModel = VisualiserViewModel()
+
+ val timer = Timer()
+ val uiThread: Scheduler = Schedulers.from { Platform.runLater(it) }
+
+ override fun start(stage: Stage) {
+ viewModel.view = view
+ viewModel.presentationMode = "--presentation-mode" in parameters.raw
+ buildScene(stage)
+ viewModel.displayStyle = if ("--circle" in parameters.raw) { Style.CIRCLE } else { viewModel.displayStyle }
+
+ val simulation = viewModel.simulation
+ // Update the white-backgrounded label indicating what protocol step it's up to.
+ simulation.allProtocolSteps.observeOn(uiThread).subscribe { step: Pair ->
+ val (node, change) = step
+ val label = viewModel.nodesToWidgets[node]!!.statusLabel
+ if (change is ProgressTracker.Change.Position) {
+ // Fade in the status label if it's our first step.
+ if (label.text == "") {
+ with(FadeTransition(Duration(150.0), label)) {
+ fromValue = 0.0
+ toValue = 1.0
+ play()
+ }
+ }
+ label.text = change.newStep.label
+ if (change.newStep == ProgressTracker.DONE && change.tracker == change.tracker.topLevelTracker) {
+ runLater(500, -1) {
+ // Fade out the status label.
+ with(FadeTransition(Duration(750.0), label)) {
+ fromValue = 1.0
+ toValue = 0.0
+ setOnFinished { label.text = "" }
+ play()
+ }
+ }
+ }
+ } else if (change is ProgressTracker.Change.Rendering) {
+ label.text = change.ofStep.label
+ }
+ }
+ // Fire the message bullets between nodes.
+ simulation.network.messagingNetwork.sentMessages.observeOn(uiThread).subscribe { msg: InMemoryMessagingNetwork.MessageTransfer ->
+ val senderNode: MockNetwork.MockNode = simulation.network.addressToNode(msg.sender.myAddress)
+ val destNode: MockNetwork.MockNode = simulation.network.addressToNode(msg.recipients as SingleMessageRecipient)
+
+ if (transferIsInteresting(msg)) {
+ viewModel.nodesToWidgets[senderNode]!!.pulseAnim.play()
+ viewModel.fireBulletBetweenNodes(senderNode, destNode, "bank", "bank")
+ }
+ }
+ // Pulse all parties in a trade when the trade completes
+ simulation.doneSteps.observeOn(uiThread).subscribe { nodes: Collection ->
+ nodes.forEach { viewModel.nodesToWidgets[it]!!.longPulseAnim.play() }
+ }
+
+ stage.setOnCloseRequest { exitProcess(0) }
+ //stage.isMaximized = true
+ stage.show()
+ }
+
+ fun runLater(startAfter: Int, delayBetween: Int, body: () -> Unit) {
+ if (delayBetween != -1) {
+ timer.scheduleAtFixedRate(startAfter.toLong(), delayBetween.toLong()) {
+ Platform.runLater {
+ body()
+ }
+ }
+ } else {
+ timer.schedule(startAfter.toLong()) {
+ Platform.runLater {
+ body()
+ }
+ }
+ }
+ }
+
+ private fun buildScene(stage: Stage) {
+ view.stage = stage
+ view.setup(viewModel.runningPausedState, viewModel.displayStyle, viewModel.presentationMode)
+ bindSidebar()
+ bindTopbar()
+ viewModel.createNodes()
+
+ // Spacebar advances simulation by one step.
+ stage.scene.accelerators[KeyCodeCombination(KeyCode.SPACE)] = Runnable { onNextInvoked() }
+
+ reloadStylesheet(stage)
+
+ stage.focusedProperty().addListener { value, old, new ->
+ if (new) {
+ reloadStylesheet(stage)
+ }
+ }
+ }
+
+ private fun bindTopbar() {
+ view.resetButton.setOnAction({reset()})
+ view.nextButton.setOnAction {
+ if (!view.simulateInitialisationCheckbox.isSelected && !viewModel.simulation.networkInitialisationFinished.isDone) {
+ skipNetworkInitialisation()
+ } else {
+ onNextInvoked()
+ }
+ }
+ viewModel.simulation.networkInitialisationFinished.then {
+ view.simulateInitialisationCheckbox.isVisible = false
+ }
+ view.runPauseButton.setOnAction {
+ val oldRunningPausedState = viewModel.runningPausedState
+ val newRunningPausedState = when (oldRunningPausedState) {
+ is NetworkMapVisualiser.RunningPausedState.Running -> {
+ oldRunningPausedState.tickTimer.cancel()
+
+ view.nextButton.isDisable = false
+ view.resetButton.isDisable = false
+
+ NetworkMapVisualiser.RunningPausedState.Paused()
+ }
+ is NetworkMapVisualiser.RunningPausedState.Paused -> {
+ val tickTimer = timer.scheduleAtFixedRate(viewModel.stepDuration.toMillis().toLong(), viewModel.stepDuration.toMillis().toLong()) {
+ Platform.runLater {
+ onNextInvoked()
+ }
+ }
+
+ view.nextButton.isDisable = true
+ view.resetButton.isDisable = true
+
+ if (!view.simulateInitialisationCheckbox.isSelected && !viewModel.simulation.networkInitialisationFinished.isDone) {
+ skipNetworkInitialisation()
+ }
+
+ NetworkMapVisualiser.RunningPausedState.Running(tickTimer)
+ }
+ }
+
+ view.runPauseButton.text = newRunningPausedState.buttonLabel.toString()
+ viewModel.runningPausedState = newRunningPausedState
+ }
+ view.styleChoice.selectionModel.selectedItemProperty()
+ .addListener { ov, value, newValue -> viewModel.displayStyle = newValue }
+ viewModel.simulation.dateChanges.observeOn(uiThread).subscribe { view.dateLabel.text = it.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)) }
+ }
+
+ private fun reloadStylesheet(stage: Stage) {
+ stage.scene.stylesheets.clear()
+ stage.scene.stylesheets.add(NetworkMapVisualiser::class.java.getResource("styles.css").toString())
+ }
+
+ private fun bindSidebar() {
+ viewModel.simulation.allProtocolSteps.observeOn(uiThread).subscribe { step: Pair ->
+ val (node, change) = step
+
+ if (change is ProgressTracker.Change.Position) {
+ val tracker = change.tracker.topLevelTracker
+ if (change.newStep == ProgressTracker.DONE) {
+ if (change.tracker == tracker) {
+ // Protocol done; schedule it for removal in a few seconds. We batch them up to make nicer
+ // animations.
+ println("Protocol done for ${node.info.identity.name}")
+ viewModel.doneTrackers += tracker
+ } else {
+ // Subprotocol is done; ignore it.
+ }
+ } else if (!viewModel.trackerBoxes.containsKey(tracker)) {
+ // New protocol started up; add.
+ val extraLabel = viewModel.simulation.extraNodeLabels[node]
+ val label = if (extraLabel != null) "${node.storage.myLegalIdentity.name}: $extraLabel" else node.storage.myLegalIdentity.name
+ val widget = view.buildProgressTrackerWidget(label, tracker.topLevelTracker)
+ bindProgressTracketWidget(tracker.topLevelTracker, widget)
+ println("Added: ${tracker}, ${widget}")
+ viewModel.trackerBoxes[tracker] = widget.vbox
+ view.sidebar.children += widget.vbox
+ }
+ }
+ }
+
+ Timer().scheduleAtFixedRate(0, 500) {
+ Platform.runLater {
+ for (tracker in viewModel.doneTrackers) {
+ val pane = viewModel.trackerBoxes[tracker]!!
+ // Slide the other tracker widgets up and over this one.
+ val slideProp = SimpleDoubleProperty(0.0)
+ slideProp.addListener { obv -> pane.padding = Insets(0.0, 0.0, slideProp.value, 0.0) }
+ val timeline = Timeline(
+ KeyFrame(Duration(250.0),
+ KeyValue(pane.opacityProperty(), 0.0),
+ KeyValue(slideProp, -pane.height - 50.0) // Subtract the bottom padding gap.
+ )
+ )
+ timeline.setOnFinished {
+ println("Removed: ${tracker}")
+ val vbox = viewModel.trackerBoxes.remove(tracker)
+ view.sidebar.children.remove(vbox)
+ }
+ timeline.play()
+ }
+ viewModel.doneTrackers.clear()
+ }
+ }
+ }
+
+ private fun bindProgressTracketWidget(tracker: ProgressTracker, widget: TrackerWidget) {
+ val allSteps: List> = tracker.allSteps
+ tracker.changes.observeOn(uiThread).subscribe { step: ProgressTracker.Change ->
+ val stepHeight = widget.cursorBox.height / allSteps.size
+ if (step is ProgressTracker.Change.Position) {
+ // Figure out the index of the new step.
+ val curStep = allSteps.indexOfFirst { it.second == step.newStep }
+ // Animate the cursor to the right place.
+ with(TranslateTransition(Duration(350.0), widget.cursor)) {
+ fromY = widget.cursor.translateY
+ toY = (curStep * stepHeight) + 22.5
+ play()
+ }
+ } else if (step is ProgressTracker.Change.Structural) {
+ val new = view.buildProgressTrackerWidget(widget.label.text, tracker)
+ val prevWidget = viewModel.trackerBoxes[step.tracker] ?: throw AssertionError("No previous widget for tracker: ${step.tracker}")
+ val i = (prevWidget.parent as VBox).children.indexOf(viewModel.trackerBoxes[step.tracker])
+ (prevWidget.parent as VBox).children[i] = new.vbox
+ viewModel.trackerBoxes[step.tracker] = new.vbox
+ }
+ }
+ }
+
+ var started = false
+ private fun startSimulation() {
+ if (!started) {
+ viewModel.simulation.start()
+ started = true
+ }
+ }
+
+ private fun reset() {
+ viewModel.simulation.stop()
+ viewModel.simulation = IRSSimulation(true, false, null)
+ started = false
+ start(view.stage)
+ }
+
+ private fun skipNetworkInitialisation() {
+ startSimulation()
+ while (!viewModel.simulation.networkInitialisationFinished.isDone) {
+ iterateSimulation()
+ }
+ }
+
+ private fun onNextInvoked() {
+ if (started) {
+ iterateSimulation()
+ } else {
+ startSimulation()
+ }
+ }
+
+ private fun iterateSimulation() {
+ // Loop until either we ran out of things to do, or we sent an interesting message.
+ while (true) {
+ val transfer: InMemoryMessagingNetwork.MessageTransfer = viewModel.simulation.iterate() ?: break
+ if (transferIsInteresting(transfer))
+ break
+ else
+ System.err.println("skipping boring $transfer")
+ }
+ }
+
+ private fun transferIsInteresting(transfer: InMemoryMessagingNetwork.MessageTransfer): Boolean {
+ // Loopback messages are boring.
+ if (transfer.sender.myAddress == transfer.recipients) return false
+ // Network map push acknowledgements are boring.
+ if (NetworkMapService.PUSH_ACK_PROTOCOL_TOPIC in transfer.message.topicSession.topic) return false
+
+ return true
+ }
+}
+
+fun main(args: Array) {
+ Application.launch(NetworkMapVisualiser::class.java, *args)
+}
diff --git a/network-simulator/src/main/kotlin/com/r3cev/corda/netmap/VisualiserUtils.kt b/network-simulator/src/main/kotlin/com/r3cev/corda/netmap/VisualiserUtils.kt
new file mode 100644
index 0000000000..590b4e139f
--- /dev/null
+++ b/network-simulator/src/main/kotlin/com/r3cev/corda/netmap/VisualiserUtils.kt
@@ -0,0 +1,18 @@
+package com.r3cev.corda.netmap
+
+import javafx.scene.paint.Color
+
+internal
+fun colorToRgb(color: Color): String {
+ val builder = StringBuilder()
+
+ builder.append("rgb(")
+ builder.append(Math.round(color.red * 256))
+ builder.append(",")
+ builder.append(Math.round(color.green * 256))
+ builder.append(",")
+ builder.append(Math.round(color.blue * 256))
+ builder.append(")")
+
+ return builder.toString()
+}
\ No newline at end of file
diff --git a/network-simulator/src/main/kotlin/com/r3cev/corda/netmap/VisualiserView.kt b/network-simulator/src/main/kotlin/com/r3cev/corda/netmap/VisualiserView.kt
new file mode 100644
index 0000000000..4dd4470aa1
--- /dev/null
+++ b/network-simulator/src/main/kotlin/com/r3cev/corda/netmap/VisualiserView.kt
@@ -0,0 +1,304 @@
+package com.r3cev.corda.netmap
+
+import com.r3corda.core.utilities.ProgressTracker
+import javafx.animation.KeyFrame
+import javafx.animation.Timeline
+import javafx.application.Platform
+import javafx.collections.FXCollections
+import javafx.event.EventHandler
+import javafx.geometry.Insets
+import javafx.geometry.Pos
+import javafx.scene.Group
+import javafx.scene.Node
+import javafx.scene.Scene
+import javafx.scene.control.*
+import javafx.scene.image.Image
+import javafx.scene.image.ImageView
+import javafx.scene.input.ZoomEvent
+import javafx.scene.layout.*
+import javafx.scene.paint.Color
+import javafx.scene.shape.Polygon
+import javafx.scene.text.Font
+import javafx.stage.Stage
+import javafx.util.Duration
+import com.r3cev.corda.netmap.VisualiserViewModel.Style
+
+data class TrackerWidget(val vbox: VBox, val cursorBox: Pane, val label: Label, val cursor: Polygon)
+
+internal class VisualiserView() {
+ lateinit var root: Pane
+ lateinit var stage: Stage
+ lateinit var splitter: SplitPane
+ lateinit var sidebar: VBox
+ lateinit var resetButton: Button
+ lateinit var nextButton: Button
+ lateinit var runPauseButton: Button
+ lateinit var simulateInitialisationCheckbox: CheckBox
+ lateinit var styleChoice: ChoiceBox