Merge remote-tracking branch 'remotes/open/master' into merges/march-1-15-20

# Conflicts:
#	docs/source/corda-configuration-file.rst
#	docs/source/release-notes.rst
#	node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt
#	node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt
#	tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt
#	tools/explorer/README.md
#	tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt
This commit is contained in:
sollecitom 2018-03-01 15:36:40 +00:00
commit 36cd34bff5
37 changed files with 386 additions and 181 deletions

View File

@ -1,4 +1,4 @@
gradlePluginsVersion=4.0.2
gradlePluginsVersion=4.0.3
kotlinVersion=1.2.20
platformVersion=2
guavaVersion=21.0

View File

@ -0,0 +1,3 @@
package net.corda.core.internal
val STRUCTURAL_STEP_PREFIX = "Structural step change in child of "

View File

@ -1,5 +1,6 @@
package net.corda.core.utilities
import net.corda.core.internal.STRUCTURAL_STEP_PREFIX
import net.corda.core.serialization.CordaSerializable
import rx.Observable
import rx.Subscription
@ -41,7 +42,7 @@ class ProgressTracker(vararg steps: Step) {
}
data class Structural(val tracker: ProgressTracker, val parent: Step) : Change(tracker) {
override fun toString() = "Structural step change in child of ${parent.label}"
override fun toString() = STRUCTURAL_STEP_PREFIX + parent.label
}
}

View File

@ -7,6 +7,12 @@ from previous releases. Please refer to :doc:`upgrade-notes` for detailed instru
UNRELEASED
----------
* Parsing of ``NodeConfiguration`` will now fail if unknown configuration keys are found.
* The web server now has its own ``web-server.conf`` file, separate from ``node.conf``.
* Introduced a placeholder for custom properties within ``node.conf``; the property key is "custom".
* Added ``NetworkMapCache.getNodesByLegalName`` for querying nodes belonging to a distributed service such as a notary cluster
where they all share a common identity. ``NetworkMapCache.getNodeByLegalName`` has been tightened to throw if more than
one node with the legal name is found.
@ -57,7 +63,7 @@ R3 Corda 3.0 Developer Preview
only ever be issued by network services and therefore issuance constraints are not relevant to end users.
The ``TLS`` and ``WELL_KNOWN_LEGAL_IDENTITY`` roles must be issued by the ``NODE_CA`` certificate issued by the
Doorman, and ``CONFIDENTIAL_IDENTITY`` certificates must be issued from a ``WELL_KNOWN_LEGAL_IDENTITY`` certificate.
For a detailed specification of the extension please see :doc:`permissioning-certificate-specification`.
For a detailed specification of the extension please see :doc:`permissioning`.
* The network map service concept has been re-designed. More information can be found in :doc:`network-map`.

View File

@ -101,16 +101,6 @@ absolute path to the node's base directory.
:security: Contains various nested fields controlling user authentication/authorization, in particular for RPC accesses. See
:doc:`clientrpc` for details.
:webAddress: The host and port on which the webserver will listen if it is started. This is not used by the node itself.
.. note:: If HTTPS is enabled then the browser security checks will require that the accessing url host name is one
of either the machine name, fully qualified machine name, or server IP address to line up with the Subject Alternative
Names contained within the development certificates. This is addition to requiring the ``/config/dev/corda_dev_ca.cer``
root certificate be installed as a Trusted CA.
.. 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 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.
@ -228,9 +218,72 @@ Simple notary configuration file:
address : "my-corda-node:10003"
adminAddress : "my-corda-node:10004"
}
webAddress : "localhost:12347"
notary : {
validating : false
}
devMode : true
compatibilityZoneURL : "https://cz.corda.net"
An example ``web-server.conf`` file is as follow:
.. parsed-literal::
myLegalName : "O=Notary Service,OU=corda,L=London,C=GB"
keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass"
rpcSettings = {
useSsl = false
standAloneBroker = false
address : "my-corda-node:10003"
adminAddress : "my-corda-node:10004"
}
webAddress : "localhost:12347",
rpcUsers : [{ username=user1, password=letmein, permissions=[ StartFlow.net.corda.protocols.CashProtocol ] }]
Fields
------
The available config fields are listed below. ``baseDirectory`` is available as a substitution value, containing the absolute
path to the node's base directory.
:myLegalName: The legal identity of the node acts as a human readable alias to the node's public key and several demos use
this to lookup the NodeInfo.
:keyStorePassword: The password to unlock the KeyStore file (``<workspace>/certificates/sslkeystore.jks``) containing the
node certificate and private key.
.. note:: This is the non-secret value for the development certificates automatically generated during the first node run.
Longer term these keys will be managed in secure hardware devices.
:trustStorePassword: The password to unlock the Trust store file (``<workspace>/certificates/truststore.jks``) containing
the Corda network root certificate. This is the non-secret value for the development certificates automatically
generated during the first node run.
.. note:: Longer term these keys will be managed in secure hardware devices.
:rpcSettings: Options for the RPC server.
:useSsl: (optional) boolean, indicates whether the node should require clients to use SSL for RPC connections, defaulted to ``false``.
:standAloneBroker: (optional) boolean, indicates whether the node will connect to a standalone broker for RPC, defaulted to ``false``.
:address: (optional) host and port for the RPC server binding, if any.
:adminAddress: (optional) host and port for the RPC admin binding (only required when ``useSsl`` is ``false``, because the node connects to Artemis using SSL to ensure admin privileges are not accessible outside the node).
:ssl: (optional) SSL settings for the RPC client.
:keyStorePassword: password for the key store.
:trustStorePassword: password for the trust store.
:certificatesDirectory: directory in which the stores will be searched, unless absolute paths are provided.
:sslKeystore: absolute path to the ssl key store, defaulted to ``certificatesDirectory / "sslkeystore.jks"``.
:trustStoreFile: absolute path to the trust store, defaulted to ``certificatesDirectory / "truststore.jks"``.
:trustStoreFile: absolute path to the trust store, defaulted to ``certificatesDirectory / "truststore.jks"``.
:webAddress: The host and port on which the webserver will listen if it is started. This is not used by the node itself.
:rpcUsers: A list of users who are authorised to access the RPC system. Each user in the list is a config object with the
following fields:
:username: Username consisting only of word characters (a-z, A-Z, 0-9 and _)
:password: The password
:permissions: A list of permissions for starting flows via RPC. To give the user the permission to start the flow
``foo.bar.FlowClass``, add the string ``StartFlow.foo.bar.FlowClass`` to the list. If the list
contains the string ``ALL``, the user can start any flow via RPC. This value is intended for administrator
users and for development.

View File

@ -8,4 +8,3 @@ Corda networks
permissioning
network-map
versioning
permissioning-certificate-spec

View File

@ -2,5 +2,3 @@ myLegalName : "O=Notary Service,OU=corda,L=London,C=GB"
keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass"
p2pAddress : "my-network-map:10000"
webAddress : "localhost:10001"
sshdAddress : "localhost:10002"

View File

@ -14,7 +14,6 @@ rpcSettings = {
address : "my-corda-node:10003"
adminAddress : "my-corda-node:10004"
}
webAddress : "localhost:10004"
rpcUsers : [
{ username=user1, password=letmein, permissions=[ StartFlow.net.corda.protocols.CashProtocol ] }
]

View File

@ -1,4 +1,3 @@
myLegalName : "O=Bank A,L=London,C=GB"
p2pAddress : "my-corda-node:10002"
webAddress : "localhost:10003"
verifierType: "OutOfProcess"

View File

@ -21,24 +21,38 @@ Each Corda node has the following structure:
The node is configured by editing its ``node.conf`` file. You install CorDapps on the node by dropping the CorDapp JARs
into the ``cordapps`` folder.
In development mode (i.e. when ``devMode = true``, see :doc:`corda-configuration-file` for more information), the ``certificates``
directory is filled with pre-configured keystores if the required keystores do not exist. This ensures that developers
can get the nodes working as quickly as possible. However, these pre-configured keystores are not secure, to learn more see :doc:`permissioning`.
Node naming
-----------
A node's name must be a valid X.500 distinguished name. In order to be compatible with other implementations
(particularly TLS implementations), we constrain the allowed X.500 attribute types to a subset of the minimum supported
set for X.509 certificates (specified in RFC 3280), plus the locality attribute:
(particularly TLS implementations), we constrain the allowed X.500 name attribute types to a subset of the minimum
supported set for X.509 certificates (specified in RFC 3280), plus the locality attribute:
* Organization (O)
* State (ST)
* Locality (L)
* Country (C)
* Organizational-unit (OU)
* Common name (CN) (only used for service identities)
* Common name (CN)
Note that the serial number is intentionally excluded in order to minimise scope for uncertainty in the distinguished name format.
The distinguished name qualifier has been removed due to technical issues; consideration was given to "Corda" as qualifier,
however the qualifier needs to reflect the compatibility zone, not the technology involved. There may be many Corda namespaces,
but only one R3 namespace on Corda. The ordering of attributes is important.
``State`` should be avoided unless required to differentiate from other ``localities`` with the same or similar names at the
country level. For example, London (GB) would not need a ``state``, but St Ives would (there are two, one in Cornwall, one
in Cambridgeshire). As legal entities in Corda are likely to be located in major cities, this attribute is not expected to be
present in the majority of names, but is an option for the cases which require it.
The name must also obey the following constraints:
* The organisation, locality and country attributes are present
* The ``organisation``, ``locality`` and ``country`` attributes are present
* The state, organisational-unit and common name attributes are optional
* The ``state``, ``organisational-unit`` and ``common name`` attributes are optional
* The fields of the name have the following maximum character lengths:
@ -48,7 +62,7 @@ The name must also obey the following constraints:
* Locality: 64
* State: 64
* The country attribute is a valid ISO 3166-1 two letter code in upper-case
* The ``country`` attribute is a valid ISO 3166-1 two letter code in upper-case
* All attributes must obey the following constraints:
@ -60,13 +74,20 @@ The name must also obey the following constraints:
* Does not contain the null character
* Only the latin, common and inherited unicode scripts are supported
* The organisation field of the name also obeys the following constraints:
* The ``organisation`` field of the name also obeys the following constraints:
* No double-spacing
* This is to avoid right-to-left issues, debugging issues when we can't pronounce names over the phone, and
character confusability attacks
External identifiers
^^^^^^^^^^^^^^^^^^^^
Mappings to external identifiers such as Companies House nos., LEI, BIC, etc. should be stored in custom X.509
certificate extensions. These values may change for operational reasons, without the identity they're associated with
necessarily changing, and their inclusion in the distinguished name would cause significant logistical complications.
The OID and format for these extensions will be described in a further specification.
The Cordform task
-----------------
Corda provides a gradle plugin called ``Cordform`` that allows you to automatically generate and configure a set of
@ -77,9 +98,8 @@ nodes. Here is an example ``Cordform`` task called ``deployNodes`` that creates
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
directory "./build/nodes"
networkMap "O=NetworkMapAndNotary,L=London,C=GB"
node {
name "O=NetworkMapAndNotary,L=London,C=GB"
name "O=Notary,L=London,C=GB"
// The notary will offer a validating notary service.
notary = [validating : true]
p2pPort 10002
@ -113,16 +133,14 @@ nodes. Here is an example ``Cordform`` task called ``deployNodes`` that creates
Running this task will create three nodes in the ``build/nodes`` folder:
* A ``NetworkMapAndNotary`` node that:
* A ``Notary`` node that:
* Serves as the network map
* Offers a validating notary service
* Will not have a webserver (since ``webPort`` is not defined)
* Is running the ``corda-finance`` CorDapp
* ``PartyA`` and ``PartyB`` nodes that:
* Are pointing at the ``NetworkMapAndNotary`` as the network map service
* Are not offering any services
* Will have a webserver (since ``webPort`` is defined)
* Are running the ``corda-finance`` CorDapp
@ -132,8 +150,7 @@ Additionally, all three nodes will include any CorDapps defined in the project's
CorDapps are not listed in each node's ``cordapps`` entry. This means that running the ``deployNodes`` task from the
template CorDapp, for example, would automatically build and add the template CorDapp to each node.
You can extend ``deployNodes`` to generate additional nodes. The only requirement is that you must specify
a single node to run the network map service, by putting its name in the ``networkMap`` field.
You can extend ``deployNodes`` to generate additional nodes.
.. warning:: When adding nodes, make sure that there are no port clashes!

View File

@ -1,43 +0,0 @@
Network Permissioning - Certificate Specification
=================================================
Certificates used by Corda have additional constraints in their contents and hierarchical structure. In a typical
installation node administrators should not need to be aware of these, however in some cases node certificates may
be managed by external tools (such as an existing PKI solution deployed within an organisation), in which case it is
important to understand these constraints.
There are a number of roles in Corda that certificates are used for:
* Doorman (Intermediate CA)
* Well known service identity (network map and notary)
* Node CA
* TLS
* Well known legal identity
* Confidential legal identity
Extension
---------
The Corda role that a certificate relates to is specified by custom X.509 v3 extension. This extension has OID 1.3.6.1.4.1.50530.1.1
and is non-critical, as it is safe for implementations outside of Corda nodes to ignore the extension. The extension
contains a single ASN.1 integer identifying the type of identity the certificate is for:
1. Doorman
2. Well known service identity
3. Node CA
4. TLS
5. Well known legal identity
6. Confidential legal identity
Hierarchy
---------
Certificate path validation is extended to enforce that the extension must be present where its issuer's certificate included the extension, and that:
* Doorman certificates are issued by a certificate without the extension present
* Well known service identity certificates are issued by a certificate marked as Doorman
* Node CA certificates are issued by a certificate marked as Doorman
* Well known legal identity/TLS certificates are issued by a certificate marked as node CA
* Confidential legal identity certificates are issued by a certificate marked as well known legal identity
* Party certificates are marked as either a well known identity or a confidential identity
* The structure of certificates above Doorman/Network map is intentionally left untouched, as they are not relevant to the identity service and therefore there is no advantage in enforcing a specific structure on those certificates. The certificate hierarchy consistency checks are required because nodes can issue their own certificates and can set their own role flags on certificates, and it's important to verify that these are set consistently with the certificate hierarchy design. As as side-effect this also acts as a secondary depth restriction on issued certificates.

View File

@ -1,4 +1,4 @@
Network Permissioning
Network permissioning
=====================
.. contents::
@ -10,23 +10,19 @@ Corda networks are *permissioned*. To connect to a network, a node needs three k
* ``nodekeystore.jks``, which stores the nodes identity keypairs and certificates
* ``sslkeystore.jks``, which stores the nodes TLS keypairs and certificates
In development mode (i.e. when ``devMode = true``, see :doc:`corda-configuration-file` for more information),
pre-configured keystores are used if the required keystores do not exist. This ensures that developers can get the
nodes working as quickly as possible.
However, these pre-configured keystores are not secure. Production deployments require a secure certificate authority.
Production deployments require a secure certificate authority.
Most production deployments will use an existing certificate authority or construct one using software that will be
made available in the coming months. Until then, the documentation below can be used to create your own certificate
authority.
Network structure
-----------------
A Corda network has three types of certificate authorities (CAs):
Certificate hierarchy
---------------------
A Corda network has four types of certificate authorities (CAs):
* The **root network CA**
* The **intermediate network CA**
* The **doorman CA**
* The intermediate network CA is used instead of the root network CA for day-to-day
* The doorman CA is used instead of the root network CA for day-to-day
key signing to reduce the risk of the root network CA's private key being compromised
* The **node CAs**
@ -34,6 +30,27 @@ A Corda network has three types of certificate authorities (CAs):
* Each node serves as its own CA in issuing the child certificates that it uses to sign its identity
keys and TLS certificates
* The **legal identity CAs**
* Node's well-known legal identity, apart from signing transactions, can also issue certificates for confidential legal identities
The following constraints are also imposed:
* Doorman certificates are issued by a network root which certificate doesn't contain the extension
* Well-known service identity certificates are issued by an entity with a Doorman certificate
* Node CA certificates are issued by an entity with a Doorman certificate
* Well known legal identity/TLS certificates are issued by a certificate marked as node CA
* Confidential legal identity certificates are issued by a certificate marked as well known legal identity
* Party certificates are marked as either a well known identity or a confidential identity
* The structure of certificates above Doorman/Network map is intentionally left untouched, as they are not relevant to
the identity service and therefore there is no advantage in enforcing a specific structure on those certificates. The
certificate hierarchy consistency checks are required because nodes can issue their own certificates and can set
their own role flags on certificates, and it's important to verify that these are set consistently with the
certificate hierarchy design. As as side-effect this also acts as a secondary depth restriction on issued
certificates
All the certificates must be issued with the custom role extension (see below).
We can visualise the permissioning structure as follows:
.. image:: resources/certificate_structure.png
@ -51,15 +68,36 @@ public/private keypairs and certificates. The keypairs and certificates should o
* The TLS certificates must follow the `TLS v1.2 standard <https://tools.ietf.org/html/rfc5246>`_
* The root network CA, intermediate network CA and node CA keys, as well as the node TLS
* The root network CA, doorman CA and node CA keys, as well as the node TLS
keys, must follow one of the following schemes:
* ECDSA using the NIST P-256 curve (secp256r1)
* RSA with 3072-bit key size
Creating the root and intermediate network CAs
----------------------------------------------
Certificate role extension
--------------------------
Corda certificates have a custom X.509 v3 extension that specifies the role the certificate relates to. This extension
has the OID 1.3.6.1.4.1.50530.1.1 and is non-critical, so implementations outside of Corda nodes can safely ignore it.
The extension contains a single ASN.1 integer identifying the identity type the certificate is for:
1. Doorman
2. Network map
3. Service identity (such as a notary or oracle)
3. Node certificate authority (from which the TLS and well-known identity certificates are issued)
4. Transport layer security
5. Well-known legal identity
6. Confidential legal identity
In a typical installation, node administrators needn't be aware of these. However, when node certificates are managed
by external tools (such as an existing PKI solution deployed within an organisation), it is important to understand
these constraints.
Certificate path validation is extended so that a certificate must contain the extension if the extension was present
in the certificate of the issuer.
Creating the root and doorman CAs
---------------------------------
Creating the root network CA's keystore and truststore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -74,31 +112,31 @@ Creating the root network CA's keystore and truststore
3. Create a new keystore and store the root network CA's keypair and certificate in it for later use
* This keystore will be used by the root network CA to sign the intermediate network CA's certificate
* This keystore will be used by the root network CA to sign the doorman CA's certificate
4. Create a new Java keystore named ``truststore.jks`` and store the root network CA's certificate in it using the
alias ``cordarootca``
* This keystore will be provisioned to the individual nodes later
* This keystore must then be provisioned to the individual nodes later so they can store it in their ``certificates`` folder
.. warning:: The root network CA's private key should be protected and kept safe.
Creating the intermediate network CA's keystore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Creating the doorman CA's keystore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1. Create a new keypair
* This will be used as the intermediate network CA's keypair
* This will be used as the doorman CA's keypair
2. Obtain a certificate for the keypair signed with the root network CA key. The basic constraints extension must be
set to ``true``
* This will be used as the intermediate network CA's certificate
* This will be used as the doorman CA's certificate
3. Create a new keystore and store the intermediate network CA's keypair and certificate chain
(i.e. the intermediate network CA certificate *and* the root network CA certificate) in it for later use
3. Create a new keystore and store the doorman CA's keypair and certificate chain
(i.e. the doorman CA certificate *and* the root network CA certificate) in it for later use
* This keystore will be used by the intermediate network CA to sign the nodes' identity certificates
* This keystore will be used by the doorman CA to sign the nodes' identity certificates
Creating the node CA keystores and TLS keystores
------------------------------------------------
@ -108,7 +146,7 @@ Creating the node CA keystores
1. For each node, create a new keypair
2. Obtain a certificate for the keypair signed with the intermediate network CA key. The basic constraints extension must be
2. Obtain a certificate for the keypair signed with the doorman CA key. The basic constraints extension must be
set to ``true``
3. Create a new Java keystore named ``nodekeystore.jks`` and store the keypair in it using the alias ``cordaclientca``

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 364 KiB

View File

@ -329,12 +329,10 @@ Assuming all went well, you should see some activity in PartyA's web-server term
>> Signing transaction with our private key.
>> Gathering the counterparty's signature.
>> Structural step change in child of Gathering the counterparty's signature.
>> Collecting signatures from counter-parties.
>> Verifying collected signatures.
>> Done
>> Obtaining notary signature and recording transaction.
>> Structural step change in child of Obtaining notary signature and recording transaction.
>> Requesting signature by notary service
>> Broadcasting transaction to participants
>> Done

View File

@ -24,7 +24,7 @@ class CashConfigDataFlowTest : IntegrationTest() {
@Test
fun `issuable currencies are read in from node config`() {
driver {
val node = startNode(customOverrides = mapOf("issuableCurrencies" to listOf("EUR", "USD"))).getOrThrow()
val node = startNode(customOverrides = mapOf("custom" to mapOf("issuableCurrencies" to listOf("EUR", "USD")))).getOrThrow()
val config = node.rpc.startFlow(::CashConfigDataFlow).returnValue.getOrThrow()
assertThat(config.issuableCurrencies).containsExactly(EUR, USD)
}

View File

@ -16,6 +16,7 @@ import net.corda.finance.EUR
import net.corda.finance.GBP
import net.corda.finance.USD
import net.corda.finance.flows.ConfigHolder.Companion.supportedCurrencies
import java.io.IOException
import java.nio.file.Path
import java.util.*
@ -35,13 +36,20 @@ class ConfigHolder(services: AppServiceHub) : SingletonSerializeAsToken() {
.let { it.javaClass.getMethod("getConfiguration").apply { isAccessible = true }.invoke(it) }
.let { it.javaClass.getMethod("getBaseDirectory").apply { isAccessible = true }.invoke(it)}
.let { it.javaClass.getMethod("toString").apply { isAccessible = true }.invoke(it) as String }
var issuableCurrenciesValue: List<Currency>
try {
val config = (baseDirectory / "node.conf").read { ConfigFactory.parseReader(it.reader()) }
if (config.hasPath("issuableCurrencies")) {
issuableCurrencies = config.getStringList("issuableCurrencies").map { Currency.getInstance(it) }
require(supportedCurrencies.containsAll(issuableCurrencies))
if (config.hasPath("custom.issuableCurrencies")) {
issuableCurrenciesValue = config.getStringList("custom.issuableCurrencies").map { Currency.getInstance(it) }
require(supportedCurrencies.containsAll(issuableCurrenciesValue))
} else {
issuableCurrencies = emptyList()
issuableCurrenciesValue = emptyList()
}
} catch (e: IOException) {
issuableCurrenciesValue = emptyList()
}
issuableCurrencies = issuableCurrenciesValue
}
}

View File

@ -29,6 +29,8 @@ import kotlin.reflect.jvm.jvmErasure
@Target(AnnotationTarget.PROPERTY)
annotation class OldConfig(val value: String)
const val CUSTOM_NODE_PROPERTIES_ROOT = "custom"
// TODO Move other config parsing to use parseAs and remove this
operator fun <T : Any> Config.getValue(receiver: Any, metadata: KProperty<*>): T {
return getValueInternal(metadata.name, metadata.returnType)
@ -37,9 +39,24 @@ operator fun <T : Any> Config.getValue(receiver: Any, metadata: KProperty<*>): T
fun <T : Any> Config.parseAs(clazz: KClass<T>): T {
require(clazz.isData) { "Only Kotlin data classes can be parsed. Offending: ${clazz.qualifiedName}" }
val constructor = clazz.primaryConstructor!!
val args = constructor.parameters
.filterNot { it.isOptional && !hasPath(it.name!!) }
.associateBy({ it }) { param ->
val parameters = constructor.parameters
val parameterNames = parameters.flatMap { param ->
mutableSetOf<String>().apply {
param.name?.let(this::add)
clazz.memberProperties.singleOrNull { it.name == param.name }?.let { matchingProperty ->
matchingProperty.annotations.filterIsInstance<OldConfig>().map { it.value }.forEach { this.add(it) }
}
}
}
val unknownConfigurationKeys = this.entrySet()
.mapNotNull { it.key.split(".").firstOrNull() }
.filterNot { it == CUSTOM_NODE_PROPERTIES_ROOT }
.filterNot(parameterNames::contains)
.toSortedSet()
if (unknownConfigurationKeys.isNotEmpty()) {
throw UnknownConfigurationKeysException.of(unknownConfigurationKeys)
}
val args = parameters.filterNot { it.isOptional && !hasPath(it.name!!) }.associateBy({ it }) { param ->
// Get the matching property for this parameter
val property = clazz.memberProperties.first { it.name == param.name }
val path = defaultToOldPath(property)
@ -48,6 +65,20 @@ fun <T : Any> Config.parseAs(clazz: KClass<T>): T {
return constructor.callBy(args)
}
class UnknownConfigurationKeysException private constructor(val unknownKeys: Set<String>) : IllegalArgumentException(message(unknownKeys)) {
init {
require(unknownKeys.isNotEmpty()) { "Absence of unknown keys should not raise UnknownConfigurationKeysException." }
}
companion object {
fun of(offendingKeys: Set<String>): UnknownConfigurationKeysException = UnknownConfigurationKeysException(offendingKeys)
private fun message(offendingKeys: Set<String>) = "Unknown configuration keys: ${offendingKeys.joinToString(", ", "[", "]")}."
}
}
inline fun <reified T : Any> Config.parseAs(): T = parseAs(T::class)
fun Config.toProperties(): Properties {

View File

@ -90,15 +90,17 @@ class NetworkBootstrapper {
}
private fun generateDirectoriesIfNeeded(directory: Path) {
val confFiles = directory.list { it.filter { it.toString().endsWith(".conf") }.toList() }
val confFiles = directory.list { it.filter { it.toString().endsWith("_node.conf") }.toList() }
val webServerConfFiles = directory.list { it.filter { it.toString().endsWith("_web-server.conf") }.toList() }
if (confFiles.isEmpty()) return
println("Node config files found in the root directory - generating node directories")
val cordaJar = extractCordaJarTo(directory)
for (confFile in confFiles) {
val nodeName = confFile.fileName.toString().removeSuffix(".conf")
val nodeName = confFile.fileName.toString().removeSuffix("_node.conf")
println("Generating directory for $nodeName")
val nodeDir = (directory / nodeName).createDirectories()
confFile.moveTo(nodeDir / "node.conf", StandardCopyOption.REPLACE_EXISTING)
webServerConfFiles.firstOrNull { directory.relativize(it).toString().removeSuffix("_web-server.conf") == nodeName }?.moveTo(nodeDir / "web-server.conf", StandardCopyOption.REPLACE_EXISTING)
Files.copy(cordaJar, (nodeDir / "corda.jar"), StandardCopyOption.REPLACE_EXISTING)
}
Files.delete(cordaJar)

View File

@ -200,6 +200,21 @@ class ConfigParsingTest {
assertThat(DataWithCompanion(3).toConfig()).isEqualTo(config("value" to 3))
}
@Test
fun `unknown configuration keys raise exception`() {
// intentional typo here, parsing should throw rather than sneakily return default value
val knownKey = "mandatory"
val unknownKey = "optioal"
val configuration = config(knownKey to "hello", unknownKey to "world")
assertThatThrownBy { configuration.parseAs<TypedConfiguration>() }.isInstanceOfSatisfying(UnknownConfigurationKeysException::class.java) { exception ->
assertThat(exception.unknownKeys).contains(unknownKey)
assertThat(exception.unknownKeys).doesNotContain(knownKey)
}
}
private inline fun <reified S : SingleData<V>, reified L : ListData<V>, V : Any> testPropertyType(
value1: V,
value2: V,
@ -243,6 +258,7 @@ class ConfigParsingTest {
val values: List<T>
}
data class TypedConfiguration(private val mandatory: String, private val optional: String = "optional")
data class StringData(override val value: String) : SingleData<String>
data class StringListData(override val values: List<String>) : ListData<String>
data class StringSetData(val values: Set<String>)

View File

@ -87,7 +87,6 @@ class AuthDBTests : NodeBasedTest() {
"password" to "",
"driverClassName" to "org.h2.Driver"
)
)
),
"options" to mapOf(
"cache" to mapOf(
@ -97,6 +96,7 @@ class AuthDBTests : NodeBasedTest() {
)
)
)
)
node = startNode(ALICE_NAME, rpcUsers = emptyList(), configOverrides = securityConfig)
client = CordaRPCClient(node.internals.configuration.rpcOptions.address!!)

View File

@ -100,7 +100,7 @@ public class CordaCaplet extends Capsule {
// Read JVM args from the config if specified, else leave alone.
List<String> jvmArgs = new ArrayList<>((List<String>) super.attribute(attr));
try {
List<String> configJvmArgs = nodeConfig.getStringList("jvmArgs");
List<String> configJvmArgs = nodeConfig.getStringList("custom.jvmArgs");
jvmArgs.clear();
jvmArgs.addAll(configJvmArgs);
log(LOG_VERBOSE, "Configured JVM args = " + jvmArgs);

View File

@ -15,6 +15,7 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.RPC_UPLOADER
import net.corda.core.internal.STRUCTURAL_STEP_PREFIX
import net.corda.core.internal.sign
import net.corda.core.messaging.*
import net.corda.core.node.NodeInfo
@ -157,7 +158,7 @@ internal class CordaRPCOpsImpl(
return FlowProgressHandleImpl(
id = stateMachine.id,
returnValue = stateMachine.resultFuture,
progress = stateMachine.logic.track()?.updates ?: Observable.empty(),
progress = stateMachine.logic.track()?.updates?.filter { !it.startsWith(STRUCTURAL_STEP_PREFIX) } ?: Observable.empty(),
stepsTreeIndexFeed = stateMachine.logic.trackStepsTreeIndex(),
stepsTreeFeed = stateMachine.logic.trackStepsTree()
)

View File

@ -17,6 +17,7 @@ import net.corda.node.shell.InteractiveShell
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
import net.corda.node.utilities.registration.NetworkRegistrationHelper
import net.corda.nodeapi.internal.addShutdownHook
import net.corda.nodeapi.internal.config.UnknownConfigurationKeysException
import net.corda.nodeapi.internal.persistence.oracleJdbcDriverSerialFilter
import org.fusesource.jansi.Ansi
import org.fusesource.jansi.AnsiConsole
@ -87,6 +88,9 @@ open class NodeStartup(val args: Array<String>) {
} else {
conf0
}
} catch (e: UnknownConfigurationKeysException) {
logger.error(e.message)
return false
} catch (e: Exception) {
logger.error("Exception during node configuration", e)
return false

View File

@ -175,7 +175,9 @@ data class NodeConfigurationImpl(
private val transactionCacheSizeMegaBytes: Int? = null,
private val attachmentContentCacheSizeMegaBytes: Int? = null,
override val attachmentCacheBound: Long = NodeConfiguration.defaultAttachmentCacheBound,
override val graphiteOptions: GraphiteOptions? = null
override val graphiteOptions: GraphiteOptions? = null,
// do not use or remove (breaks DemoBench together with rejection of unknown configuration keys during parsing)
private val h2port: Int = 0
) : NodeConfiguration {
companion object {
private val logger = loggerFor<NodeConfigurationImpl>()

View File

@ -38,7 +38,7 @@ class BankOfCordaCordform : CordformDefinition() {
}
node {
name(BOC_NAME)
extraConfig = mapOf("issuableCurrencies" to listOf("USD"))
extraConfig = mapOf("custom" to mapOf("issuableCurrencies" to listOf("USD")))
p2pPort(10005)
rpcSettings {
address("localhost:$BOC_RPC_PORT")

View File

@ -64,8 +64,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
notary = [validating : true]
p2pPort 10002
rpcSettings {
port 10003
adminPort 10023
address("localhost:10003")
adminAddress("localhost:10023")
}
cordapps = ["${project(":finance").group}:finance:$corda_release_version"]
rpcUsers = rpcUsersList
@ -75,8 +75,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
name "O=Bank A,L=London,C=GB"
p2pPort 10005
rpcSettings {
port 10006
adminPort 10026
address("localhost:10006")
adminAddress("localhost:10026")
}
cordapps = ["${project(":finance").group}:finance:$corda_release_version"]
rpcUsers = rpcUsersList
@ -86,8 +86,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
name "O=Bank B,L=New York,C=US"
p2pPort 10008
rpcSettings {
port 10009
adminPort 10029
address("localhost:10009")
adminAddress("localhost:10029")
}
cordapps = ["${project.group}:finance:$corda_release_version"]
rpcUsers = rpcUsersList
@ -97,8 +97,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
name "O=Regulator,L=Moscow,C=RU"
p2pPort 10011
rpcSettings {
port 10012
adminPort 10032
address("localhost:10012")
adminAddress("localhost:10032")
}
cordapps = ["${project.group}:finance:$corda_release_version"]
cordapps = ["${project(":finance").group}:finance:$corda_release_version"]

View File

@ -73,8 +73,10 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
p2pPort 10002
cordapp project(':finance')
extraConfig = [
custom: [
jvmArgs: ["-Xmx1g"]
]
]
}
node {
name "O=Bank A,L=London,C=GB"
@ -87,8 +89,10 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
cordapp project(':finance')
rpcUsers = ext.rpcUsers
extraConfig = [
custom: [
jvmArgs: ["-Xmx1g"]
]
]
}
node {
name "O=Bank B,L=New York,C=US"
@ -101,8 +105,10 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
cordapp project(':finance')
rpcUsers = ext.rpcUsers
extraConfig = [
custom: [
jvmArgs: ["-Xmx1g"]
]
]
}
node {
name "O=Bank C,L=Tokyo,C=JP"
@ -115,8 +121,10 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
cordapp project(':finance')
rpcUsers = ext.rpcUsers
extraConfig = [
custom: [
jvmArgs: ["-Xmx1g"]
]
]
}
}

View File

@ -5,6 +5,7 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigRenderOptions
import com.typesafe.config.ConfigValueFactory
import net.corda.client.rpc.CordaRPCClient
import net.corda.client.rpc.internal.createCordaRPCClientWithSsl
import net.corda.cordform.CordformContext
@ -229,7 +230,6 @@ class DriverDSLImpl(
"p2pAddress" to p2pAddress.toString(),
"rpcSettings.address" to rpcAddress.toString(),
"rpcSettings.adminAddress" to rpcAdminAddress.toString(),
"webAddress" to webAddress.toString(),
"useTestClock" to useTestClock,
"rpcUsers" to if (users.isEmpty()) defaultRpcUserList else users.map { it.toConfig().root().unwrapped() },
"verifierType" to verifierType.name
@ -355,13 +355,17 @@ class DriverDSLImpl(
val webAddress = cordform.webAddress?.let { NetworkHostAndPort.parse(it) } ?: portAllocation.nextHostAndPort()
val notary = if (cordform.notary != null) mapOf("notary" to cordform.notary) else emptyMap()
val rpcUsers = cordform.rpcUsers
val config = NodeConfig(ConfigHelper.loadConfig(
baseDirectory = baseDirectory(name),
allowMissingConfig = true,
configOverrides = cordform.config + rpcAddress + notary + mapOf(
val rawConfig = cordform.config + rpcAddress + notary + mapOf(
"rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers
)
))
val typesafe = ConfigHelper.loadConfig(
baseDirectory = baseDirectory(name),
allowMissingConfig = true,
configOverrides = rawConfig.toNodeOnly()
)
val cordaConfig = typesafe.parseAsNodeConfiguration()
val config = NodeConfig(rawConfig, cordaConfig)
return startNodeInternal(config, webAddress, null, "200m", localNetworkMap)
}
@ -384,9 +388,9 @@ class DriverDSLImpl(
override fun startWebserver(handle: NodeHandle, maximumHeapSize: String): CordaFuture<WebserverHandle> {
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
val process = startWebserver(handle, debugPort, maximumHeapSize)
val process = startWebserver(handle as NodeHandleInternal, debugPort, maximumHeapSize)
shutdownManager.registerProcessShutdown(process)
val webReadyFuture = addressMustBeBoundFuture(executorService, (handle as NodeHandleInternal).webAddress, process)
val webReadyFuture = addressMustBeBoundFuture(executorService, handle.webAddress, process)
return webReadyFuture.map { queryWebserver(handle, process) }
}
@ -726,14 +730,12 @@ class DriverDSLImpl(
* Simple holder class to capture the node configuration both as the raw [Config] object and the parsed [NodeConfiguration].
* Keeping [Config] around is needed as the user may specify extra config options not specified in [NodeConfiguration].
*/
private class NodeConfig(val typesafe: Config) {
val corda: NodeConfiguration = typesafe.parseAsNodeConfiguration().also { nodeConfiguration ->
private class NodeConfig(val typesafe: Config, val corda: NodeConfiguration = typesafe.parseAsNodeConfiguration().also { nodeConfiguration ->
val errors = nodeConfiguration.validate()
if (errors.isNotEmpty()) {
throw IllegalStateException("Invalid node configuration. Errors where:${System.lineSeparator()}${errors.joinToString(System.lineSeparator())}")
}
}
}
})
companion object {
internal val log = contextLogger()
@ -771,7 +773,7 @@ class DriverDSLImpl(
throw IllegalStateException("No quasar agent: -javaagent:lib/quasar.jar and working directory project root might fix")
}
// Write node.conf
writeConfig(config.corda.baseDirectory, "node.conf", config.typesafe)
writeConfig(config.corda.baseDirectory, "node.conf", config.typesafe.toNodeOnly())
// TODO pass the version in?
val node = InProcessNode(config.corda, MOCK_VERSION_INFO, cordappPackages).start()
val nodeThread = thread(name = config.corda.myLegalName.organisation) {
@ -798,7 +800,7 @@ class DriverDSLImpl(
"debug port is " + (debugPort ?: "not enabled") + ", " +
"jolokia monitoring port is " + (monitorPort ?: "not enabled"))
// Write node.conf
writeConfig(config.corda.baseDirectory, "node.conf", config.typesafe)
writeConfig(config.corda.baseDirectory, "node.conf", config.typesafe.toNodeOnly())
val systemProperties = mutableMapOf(
"name" to config.corda.myLegalName,
@ -844,8 +846,9 @@ class DriverDSLImpl(
)
}
private fun startWebserver(handle: NodeHandle, debugPort: Int?, maximumHeapSize: String): Process {
private fun startWebserver(handle: NodeHandleInternal, debugPort: Int?, maximumHeapSize: String): Process {
val className = "net.corda.webserver.WebServer"
writeConfig(handle.baseDirectory, "web-server.conf", handle.toWebServerConfig())
return ProcessUtilities.startCordaProcess(
className = className, // cannot directly get class for this, so just use string
arguments = listOf("--base-directory", handle.baseDirectory.toString()),
@ -860,6 +863,22 @@ class DriverDSLImpl(
)
}
private fun NodeHandleInternal.toWebServerConfig(): Config {
var config = ConfigFactory.empty()
config += "webAddress" to webAddress.toString()
config += "myLegalName" to configuration.myLegalName.toString()
config += "rpcAddress" to configuration.rpcOptions.address!!.toString()
config += "rpcUsers" to configuration.toConfig().getValue("rpcUsers")
config += "useHTTPS" to useHTTPS
config += "baseDirectory" to configuration.baseDirectory.toAbsolutePath().toString()
config += "keyStorePassword" to configuration.keyStorePassword
config += "trustStorePassword" to configuration.trustStorePassword
return config
}
private operator fun Config.plus(property: Pair<String, Any>) = withValue(property.first, ConfigValueFactory.fromAnyRef(property.second))
/**
* Get the package of the caller to the driver so that it can be added to the list of packages the nodes will scan.
* This makes the driver automatically pick the CorDapp module that it's run from.
@ -1053,3 +1072,14 @@ fun writeConfig(path: Path, filename: String, config: Config) {
val configString = config.root().render(ConfigRenderOptions.defaults())
configString.byteInputStream().copyTo(path / filename, StandardCopyOption.REPLACE_EXISTING)
}
private fun Config.toNodeOnly(): Config {
return if (hasPath("webAddress")) {
withoutPath("webAddress").withoutPath("useHTTPS")
} else {
this
}
}
private operator fun Config.plus(property: Pair<String, Any>) = withValue(property.first, ConfigValueFactory.fromAnyRef(property.second))

View File

@ -46,17 +46,49 @@ data class NodeConfig(
@Suppress("unused")
private val useTestClock = true
private fun asConfig(): Config {
fun nodeConf(): Config {
val config = toConfig()
val basic = NodeConfigurationData(myLegalName, p2pAddress, rpcAddress, notary, h2port, rpcUsers, useTestClock, detectPublicIp).toConfig()
val rpcSettings = empty()
.withValue("address", ConfigValueFactory.fromAnyRef(rpcAddress.toString()))
.withValue("adminAddress", ConfigValueFactory.fromAnyRef(rpcAdminAddress.toString()))
.root()
return config.withoutPath("rpcAddress").withoutPath("rpcAdminAddress").withValue("rpcSettings", rpcSettings)
return basic.withoutPath("rpcAddress").withoutPath("rpcAdminAddress").withValue("rpcSettings", rpcSettings)
}
fun toText(): String = asConfig().root().render(renderOptions)
fun webServerConf() = WebServerConfigurationData(myLegalName, rpcAddress, webAddress, rpcUsers).asConfig()
fun toNodeConfText() = nodeConf().render()
fun toWebServerConfText() = webServerConf().render()
fun serialiseAsString(): String {
return toConfig().render()
}
private fun Config.render(): String = root().render(renderOptions)
}
private data class NodeConfigurationData(
val myLegalName: CordaX500Name,
val p2pAddress: NetworkHostAndPort,
val rpcAddress: NetworkHostAndPort,
val notary: NotaryService?,
val h2port: Int,
val rpcUsers: List<User> = listOf(NodeConfig.defaultUser),
val useTestClock: Boolean,
val detectPublicIp: Boolean
)
private data class WebServerConfigurationData(
val myLegalName: CordaX500Name,
val rpcAddress: NetworkHostAndPort,
val webAddress: NetworkHostAndPort,
val rpcUsers: List<User>
) {
fun asConfig() = toConfig()
}
/**

View File

@ -121,7 +121,11 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() {
// Write this node's configuration file into its working directory.
val confFile = config.nodeDir / "node.conf"
Files.write(confFile, config.nodeConfig.toText().toByteArray())
Files.write(confFile, config.nodeConfig.toNodeConfText().toByteArray())
// Write this node's configuration file into its working directory.
val webConfFile = config.nodeDir / "web-server.conf"
Files.write(webConfFile, config.nodeConfig.toWebServerConfText().toByteArray())
// Execute the Corda node
val cordaEnv = System.getenv().toMutableMap().apply {

View File

@ -58,7 +58,7 @@ class ProfileController : Controller() {
configs.forEach { config ->
// Write the configuration file.
val nodeDir = fs.getPath(config.key).createDirectories()
val file = Files.write(nodeDir / "node.conf", config.nodeConfig.toText().toByteArray(UTF_8))
val file = Files.write(nodeDir / "node.conf", config.nodeConfig.serialiseAsString().toByteArray(UTF_8))
log.info("Wrote: $file")
// Write all of the non-built-in cordapps.

View File

@ -11,7 +11,7 @@ class WebServerController : Controller() {
log.info("Web Server JAR: $webserverPath")
}
internal fun process() = jvm.processFor(webserverPath)
internal fun process() = jvm.processFor(webserverPath, "--config-file", "web-server.conf")
fun webServer() = WebServer(this)
}

View File

@ -6,7 +6,6 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.services.config.parseAsNodeConfiguration
import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.config.toConfig
import net.corda.nodeapi.internal.persistence.CordaPersistence.DataSourceConfigTag
import net.corda.webserver.WebServerConfig
import org.assertj.core.api.Assertions.assertThat
@ -29,14 +28,14 @@ class NodeConfigTest {
legalName = myLegalName,
p2pPort = 10001,
rpcPort = 40002,
rpcAdminPort = 40003,
rpcAdminPort = 40005,
webPort = 20001,
h2port = 30001,
notary = NotaryService(validating = false),
users = listOf(user("jenny"))
)
val nodeConfig = config.toConfig()
val nodeConfig = config.nodeConf()
.withValue("baseDirectory", ConfigValueFactory.fromAnyRef(baseDir.toString()))
.withFallback(ConfigFactory.parseResources("reference.conf"))
.resolve()
@ -64,7 +63,7 @@ class NodeConfigTest {
users = listOf(user("jenny"))
)
val nodeConfig = config.toConfig()
val nodeConfig = config.webServerConf()
.withValue("baseDirectory", ConfigValueFactory.fromAnyRef(baseDir.toString()))
.withFallback(ConfigFactory.parseResources("web-reference.conf"))
.resolve()

View File

@ -34,11 +34,11 @@ The Participant nodes are only able to spend cash (eg. move cash).
**These Corda nodes will be created on the following port on localhost.**
* Notary -> 20001 (Does not accept logins)
* Alice -> 20004
* Bob -> 20007
* UK Bank Plc -> 20010 (*Issuer node*)
* USA Bank Corp -> 20013 (*Issuer node*)
* Notary -> 20005 (Does not accept logins)
* UK Bank Plc -> 20011 (*Issuer node*)
* USA Bank Corp -> 20008 (*Issuer node*)
* Alice -> 20017
* Bob -> 20014
Explorer login credentials to the Issuer nodes are defaulted to ``manager`` and ``test``.
Explorer login credentials to the Participant nodes are defaulted to ``user1`` and ``test``.

View File

@ -82,9 +82,9 @@ class ExplorerSimulation(private val options: OptionSet) {
val ukBankName = CordaX500Name(organisation = "UK Bank Plc", locality = "London", country = "GB")
val usaBankName = CordaX500Name(organisation = "USA Bank Corp", locality = "New York", country = "US")
val issuerGBP = startNode(providedName = ukBankName, rpcUsers = listOf(manager),
customOverrides = mapOf("issuableCurrencies" to listOf("GBP")))
customOverrides = mapOf("custom" to mapOf("issuableCurrencies" to listOf("GBP"))))
val issuerUSD = startNode(providedName = usaBankName, rpcUsers = listOf(manager),
customOverrides = mapOf("issuableCurrencies" to listOf("USD")))
customOverrides = mapOf("custom" to mapOf("issuableCurrencies" to listOf("USD"))))
val bno = startNode(providedName = IOUFlow.allowedMembershipName, rpcUsers = listOf(user))
notaryNode = defaultNotaryNode.get()

View File

@ -131,8 +131,8 @@ class Main : App(MainView::class) {
}
/**
* This main method will starts 5 nodes (Notary, Alice, Bob, UK Bank and USA Bank) locally for UI testing,
* they will be on localhost ports 20003, 20006, 20009, 20012 and 20015 respectively.
* This main method will starts 5 nodes (Notary, USA Bank, UK Bank, Bob and Alice) locally for UI testing,
* they will be on localhost ports 20005, 20008, 20011, 20014 and 20017 respectively.
*
* The simulation start with pre-allocating chunks of cash to each of the party in 2 currencies (USD, GBP), then it enter a loop to generate random events.
* On each iteration, the issuers will execute a Cash Issue or Cash Exit flow (at a 9:1 ratio) and a random party will execute a move of cash to another random party.

View File

@ -25,7 +25,7 @@ class ArgsParser {
private val configFileArg = optionParser
.accepts("config-file", "The path to the config file")
.withRequiredArg()
.defaultsTo("node.conf")
.defaultsTo("web-server.conf")
private val loggerLevel = optionParser
.accepts("logging-level", "Enable logging at this level and higher")
.withRequiredArg()