Merge pull request #424 from corda/pat/os-enterprise-merge

os to enterprise merge
This commit is contained in:
Patrick Kuo 2018-01-30 18:21:55 +00:00 committed by GitHub
commit 4d78d51663
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 364 additions and 246 deletions

View File

@ -60,7 +60,7 @@ buildscript {
ext.rxjava_version = '1.2.4' ext.rxjava_version = '1.2.4'
ext.dokka_version = '0.9.16-eap-2' ext.dokka_version = '0.9.16-eap-2'
ext.eddsa_version = '0.2.0' ext.eddsa_version = '0.2.0'
ext.dependency_checker_version = '3.0.1' ext.dependency_checker_version = '3.1.0'
ext.commons_collections_version = '4.1' ext.commons_collections_version = '4.1'
ext.beanutils_version = '1.9.3' ext.beanutils_version = '1.9.3'
ext.crash_version = 'cce5a00f114343c1145c1d7756e1dd6df3ea984e' ext.crash_version = 'cce5a00f114343c1145c1d7756e1dd6df3ea984e'

View File

@ -11,6 +11,7 @@ CorDapps
cordapp-build-systems cordapp-build-systems
building-against-master building-against-master
corda-api corda-api
serialization
secure-coding-guidelines secure-coding-guidelines
flow-cookbook flow-cookbook
cheat-sheet cheat-sheet

View File

@ -51,6 +51,10 @@ The ``cordformation`` plugin adds two new gradle configurations:
* ``cordaCompile``, which extends ``compile`` * ``cordaCompile``, which extends ``compile``
* ``cordaRuntime``, which extends ``runtime`` * ``cordaRuntime``, which extends ``runtime``
``cordaCompile`` and ``cordaRuntime`` indicate dependencies that should not be included in the CorDapp JAR. These
configurations should be used for any Corda dependency (e.g. ``corda-core``, ``corda-node``) in order to prevent a
dependency from being included twice (once in the CorDapp JAR and once in the Corda JARs).
To build against Corda, you must add the following to your ``build.gradle`` file: To build against Corda, you must add the following to your ``build.gradle`` file:
* ``net.corda:corda:$corda_release_version`` as a ``cordaRuntime`` dependency * ``net.corda:corda:$corda_release_version`` as a ``cordaRuntime`` dependency
@ -73,6 +77,15 @@ ways to add another CorDapp as a dependency in your CorDapp's ``build.gradle`` f
* ``cordapp project(":another-cordapp")`` (use this if the other CorDapp is defined in a module in the same project) * ``cordapp project(":another-cordapp")`` (use this if the other CorDapp is defined in a module in the same project)
* ``cordapp "net.corda:another-cordapp:1.0"`` (use this otherwise) * ``cordapp "net.corda:another-cordapp:1.0"`` (use this otherwise)
The ``cordapp`` gradle configuration serves two purposes:
* When using the ``cordformation`` Gradle plugin, the ``cordapp`` configuration indicates that this JAR should be
included on your node as a CorDapp
* When using the ``cordapp`` Gradle plugin, the ``cordapp`` configuration prevents the dependency from being included
in the CorDapp JAR
Note that the ``cordformation`` and ``cordapp`` Gradle plugins can be used together.
Other dependencies Other dependencies
^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
If your CorDapps have any additional external dependencies, they can be specified like normal Kotlin/Java dependencies If your CorDapps have any additional external dependencies, they can be specified like normal Kotlin/Java dependencies
@ -143,4 +156,4 @@ Installing the CorDapp JAR
At runtime, nodes will load any CorDapps present in their ``cordapps`` folder. Therefore in order to install a CorDapp on At runtime, nodes will load any CorDapps present in their ``cordapps`` folder. Therefore in order to install a CorDapp on
a node, the CorDapp JAR must be added to the ``<node_dir>/cordapps/`` folder, where ``node_dir`` is the folder in which a node, the CorDapp JAR must be added to the ``<node_dir>/cordapps/`` folder, where ``node_dir`` is the folder in which
the node's JAR and configuration files are stored. the node's JAR and configuration files are stored.

View File

@ -6,5 +6,4 @@ Node internals
node-services node-services
vault vault
serialization
messaging messaging

View File

@ -1,15 +1,41 @@
.. highlight:: kotlin
.. raw:: html
<script type="text/javascript" src="_static/jquery.js"></script>
<script type="text/javascript" src="_static/codesets.js"></script>
Object serialization Object serialization
==================== ====================
.. contents:: .. contents::
What is serialization (and deserialization)? Introduction
-------------------------------------------- ------------
Object serialization is the process of converting objects into a stream of bytes and, deserialization, the reverse Object serialization is the process of converting objects into a stream of bytes and, deserialization, the reverse
process of creating objects from a stream of bytes. It takes place every time nodes pass objects to each other as process of creating objects from a stream of bytes. It takes place every time nodes pass objects to each other as
messages, when objects are sent to or from RPC clients from the node, and when we store transactions in the database. messages, when objects are sent to or from RPC clients from the node, and when we store transactions in the database.
Corda pervasively uses a custom form of type safe binary serialisation. This stands in contrast to some other systems that use
weakly or untyped string-based serialisation schemes like JSON or XML. The primary drivers for this were:
* A desire to have a schema describing what has been serialized alongside the actual data:
#. To assist with versioning, both in terms of being able to interpret data archived long ago (e.g. trades from
a decade ago, long after the code has changed) and between differing code versions.
#. To make it easier to write generic code e.g. user interfaces that can navigate the serialized form of data.
#. To support cross platform (non-JVM) interaction, where the format of a class file is not so easily interpreted.
* A desire to use a documented and static wire format that is platform independent, and is not subject to change with
3rd party library upgrades, etc.
* A desire to support open-ended polymorphism, where the number of subclasses of a superclass can expand over time
and the subclasses do not need to be defined in the schema *upfront*. This is key to many Corda concepts, such as states.
* Increased security by constructing deserialized objects through supported constructors, rather than having
data inserted directly into their fields without an opportunity to validate consistency or intercept attempts to manipulate
supposed invariants.
* Binary formats work better with digital signatures than text based formats, as there's much less scope for
changes that modify syntax but not semantics.
Whitelisting Whitelisting
------------ ------------
@ -49,46 +75,28 @@ It's reproduced here as an example of both ways you can do this for a couple of
.. _amqp_ref: .. _amqp_ref:
AMQP AMQP
==== ----
Originally Corda used a ``Kryo``-based serialization scheme throughout for all serialization contexts. However, it was realised there Corda uses an extended form of AMQP 1.0 as its binary wire protocol.
was a compelling use case for the definition and development of a custom format based upon AMQP 1.0. The primary drivers for this were:
#. A desire to have a schema describing what has been serialized alongside the actual data: Corda serialisation is currently used for:
#. To assist with versioning, both in terms of being able to interpret data archived long ago (e.g. trades from #. Peer-to-peer networking.
a decade ago, long after the code has changed) and between differing code versions #. Persisted messages, like signed transactions and states.
#. To make it easier to write user interfaces that can navigate the serialized form of data
#. To support cross platform (non-JVM) interaction, where the format of a class file is not so easily interpreted
#. A desire to use a documented and static wire format that is platform independent, and is not subject to change with
3rd party library upgrades, etc.
#. A desire to support open-ended polymorphism, where the number of subclasses of a superclass can expand over time
and the subclasses do not need to be defined in the schema *upfront*. This is key to many Corda concepts, such as states.
#. Increased security by constructing deserialized objects through supported constructors, rather than having
data inserted directly into their fields without an opportunity to validate consistency or intercept attempts to manipulate
supposed invariants
Delivering this is an ongoing effort by the Corda development team. At present, the ``Kryo``-based format is still used by the RPC framework on .. note:: At present, the Kryo-based format is still used by the RPC framework on both the client and server side. However, it is planned that the RPC framework will move to the AMQP framework soon.
both the client and server side. However, it is planned that the RPC framework will move to the AMQP framework when ready.
The AMQP framework is currently used for: For the checkpointing of flows Corda uses a private scheme that is subject to change. It is currently based on the Kryo
framework, but this may not be true in future.
#. The peer-to-peer context, representing inter-node communication
#. The persistence layer, representing contract states persisted into the vault
Finally, for the checkpointing of flows, Corda will continue to use the existing ``Kryo`` scheme.
This separation of serialization schemes into different contexts allows us to use the most suitable framework for that context rather than This separation of serialization schemes into different contexts allows us to use the most suitable framework for that context rather than
attempting to force a one-size-fits-all approach. ``Kryo`` is more suited to the serialization of a program's stack frames, as it is more flexible attempting to force a one-size-fits-all approach. Kryo is more suited to the serialization of a program's stack frames, as it is more flexible
than our AMQP framework in what it can construct and serialize. However, that flexibility makes it exceptionally difficult to make secure. Conversely, than our AMQP framework in what it can construct and serialize. However, that flexibility makes it exceptionally difficult to make secure. Conversely,
our AMQP framework allows us to concentrate on a secure framework that can be reasoned about and thus made safer, with far fewer our AMQP framework allows us to concentrate on a secure framework that can be reasoned about and thus made safer, with far fewer
security holes. security holes.
.. note:: Selection of serialization context should, for the most part, be opaque to CorDapp developers, the Corda framework selecting Selection of serialization context should, for the most part, be opaque to CorDapp developers, the Corda framework selecting
the correct context as configured. the correct context as configured.
.. note:: For information on our choice of AMQP 1.0, see :doc:`amqp-choice`. For detail on how we utilise AMQP 1.0 and represent
objects in AMQP types, see :doc:`amqp-format`.
This document describes what is currently and what will be supported in the Corda AMQP format from the perspective This document describes what is currently and what will be supported in the Corda AMQP format from the perspective
of CorDapp developers, to allow CorDapps to take into consideration the future state. The AMQP serialization format will of CorDapp developers, to allow CorDapps to take into consideration the future state. The AMQP serialization format will
@ -102,8 +110,9 @@ This section describes the classes and interfaces that the AMQP serialization fo
Collection Types Collection Types
```````````````` ````````````````
The following collection types are supported. Any implementation of the following will be mapped to *an* implementation of the interface or class on the other end. The following collection types are supported. Any implementation of the following will be mapped to *an* implementation
For example, if you use a Guava implementation of a collection, it will deserialize as the primitive collection type. of the interface or class on the other end. For example, if you use a Guava implementation of a collection, it will
deserialize as the primitive collection type.
The declared types of properties should only use these types, and not any concrete implementation types (e.g. The declared types of properties should only use these types, and not any concrete implementation types (e.g.
Guava implementations). Collections must specify their generic type, the generic type parameters will be included in Guava implementations). Collections must specify their generic type, the generic type parameters will be included in
@ -240,7 +249,7 @@ General Rules
.. note:: In circumstances where classes cannot be recompiled, such as when using a third-party library, a .. note:: In circumstances where classes cannot be recompiled, such as when using a third-party library, a
proxy serializer can be used to avoid this problem. Details on creating such an object can be found on the proxy serializer can be used to avoid this problem. Details on creating such an object can be found on the
:doc:`cordapp-custom-serializers` page. :doc:`cordapp-custom-serializers` page.
#. The class must be annotated with ``@CordaSerializable`` #. The class must be annotated with ``@CordaSerializable``
#. The declared types of constructor arguments, getters, and setters must be supported, and where generics are used, the #. The declared types of constructor arguments, getters, and setters must be supported, and where generics are used, the
@ -311,21 +320,28 @@ For example:
.. container:: codeset .. container:: codeset
.. sourcecode:: Java .. sourcecode:: kotlin
class Example { class Example(var a: Int, var b: Int, var c: Int)
private int a;
private int b;
private int c;
public int getA() { return a; } .. sourcecode:: java
public int getB() { return b; }
public int getC() { return c; }
public void setA(int a) { this.a = a; } class Example {
public void setB(int b) { this.b = b; } private int a;
public void setC(int c) { this.c = c; } private int b;
} private int c;
public int getA() { return a; }
public int getB() { return b; }
public int getC() { return c; }
public void setA(int a) { this.a = a; }
public void setB(int b) { this.b = b; }
public void setC(int c) { this.c = c; }
}
.. warning:: We do not recommend this pattern! Corda tries to use immutable data structures throughout, and if you
rely heavily on mutable JavaBean style objects then you may sometimes find the API behaves in unintuitive ways.
Inaccessible Private Properties Inaccessible Private Properties
``````````````````````````````` ```````````````````````````````
@ -335,30 +351,26 @@ accessible getter methods, this development idiom is strongly discouraged.
For example. For example.
.. container:: codeset .. container:: codeset
Kotlin: .. sourcecode:: kotlin
.. sourcecode:: kotlin class C(val a: Int, private val b: Int)
data class C(val a: Int, private val b: Int) .. sourcecode:: java
Java: class C {
public Integer a;
private Integer b;
.. sourcecode:: Java public C(Integer a, Integer b) {
this.a = a;
this.b = b;
}
}
class C { When designing Corda states, it should be remembered that they are not, despite appearances, traditional
public Integer a; OOP style objects. They are signed over, transformed, serialised, and relationally mapped. As such,
private Integer b;
C(Integer a, Integer b) {
this.a = a;
this.b = b;
}
}
When designing stateful objects, is should be remembered that they are not, despite appearances, traditional
programmatic constructs. They are signed over, transformed, serialised, and relationally mapped. As such,
all elements should be publicly accessible by design. all elements should be publicly accessible by design.
.. warning:: IDEs will indicate erroneously that properties can be given something other than public visibility. Ignore .. warning:: IDEs will indicate erroneously that properties can be given something other than public visibility. Ignore
@ -366,40 +378,38 @@ all elements should be publicly accessible by design.
Providing a public getter, as per the following example, is acceptable: Providing a public getter, as per the following example, is acceptable:
.. container:: codeset .. container:: codeset
Kotlin: .. sourcecode:: kotlin
.. sourcecode:: kotlin class C(val a: Int, b: Int) {
var b: Int = b
private set
}
data class C(val a: Int, private val b: Int) { .. sourcecode:: java
public fun getB() = b
}
Java: class C {
public Integer a;
private Integer b;
.. sourcecode:: Java C(Integer a, Integer b) {
this.a = a;
this.b = b;
}
class C { public Integer getB() {
public Integer a; return b;
private Integer b; }
}
C(Integer a, Integer b) {
this.a = a;
this.b = b;
}
public Integer getB() {
return b;
}
}
Enums Enums
````` `````
#. All enums are supported, provided they are annotated with ``@CordaSerializable`` All enums are supported, provided they are annotated with ``@CordaSerializable``. Corda supports interoperability of
enumerated type versions. This allows such types to be changed over time without breaking backward (or forward)
compatibility. The rules and mechanisms for doing this are discussed in :doc:`serialization-enum-evolution`.
Exceptions Exceptions
`````````` ``````````
@ -414,24 +424,23 @@ The following rules apply to supported ``Throwable`` implementations.
Kotlin Objects Kotlin Objects
`````````````` ``````````````
#. Kotlin's non-anonymous ``object`` s (i.e. constructs like ``object foo : Contract {...}``) are singletons and Kotlin's non-anonymous ``object`` s (i.e. constructs like ``object foo : Contract {...}``) are singletons and
treated differently. They are recorded into the stream with no properties, and deserialize back to the treated differently. They are recorded into the stream with no properties, and deserialize back to the
singleton instance. Currently, the same is not true of Java singletons, which will deserialize to new instances singleton instance. Currently, the same is not true of Java singletons, which will deserialize to new instances
of the class of the class. This is hard to fix because there's no perfectly standard idiom for Java singletons.
#. Kotlin's anonymous ``object`` s (i.e. constructs like ``object : Contract {...}``) are not currently supported
and will not serialize correctly. They need to be re-written as an explicit class declaration
The Carpenter Kotlin's anonymous ``object`` s (i.e. constructs like ``object : Contract {...}``) are not currently supported
````````````` and will not serialize correctly. They need to be re-written as an explicit class declaration.
We support a class carpenter that can dynamically manufacture classes from the supplied schema when deserializing, Class synthesis
without the supporting classes being present on the classpath. This can be useful where other components might expect to ---------------
be able to use reflection over the deserialized data, and also for ensuring classes not on the classpath can be
deserialized without loading potentially malicious code dynamically without security review outside of a fully sandboxed
environment. A more detailed discussion of the carpenter will be provided in a future update to the documentation.
Future Enhancements Corda serialization supports dynamically synthesising classes from the supplied schema when deserializing,
``````````````````` without the supporting classes being present on the classpath. This can be useful where generic code might expect to
be able to use reflection over the deserialized data, for scripting languages that run on the JVM, and also for
ensuring classes not on the classpath can be deserialized without loading potentially malicious code.
Possible future enhancements include:
#. Java singleton support. We will add support for identifying classes which are singletons and identifying the #. Java singleton support. We will add support for identifying classes which are singletons and identifying the
static method responsible for returning the singleton instance static method responsible for returning the singleton instance
@ -449,10 +458,5 @@ and a version of the current state of the class instantiated.
More detail can be found in :doc:`serialization-default-evolution`. More detail can be found in :doc:`serialization-default-evolution`.
Enum Evolution
``````````````
Corda supports interoperability of enumerated type versions. This allows such types to be changed over time without breaking
backward (or forward) compatibility. The rules and mechanisms for doing this are discussed in :doc:`serialization-enum-evolution``.

View File

@ -126,10 +126,13 @@ class SigningServiceIntegrationTest {
} }
} }
config.certificatesDirectory.createDirectories() config.certificatesDirectory.createDirectories()
val trustStore = X509KeyStore.fromFile(config.trustStoreFile, config.trustStorePassword, createNew = true) val networkTrustStorePath = config.certificatesDirectory / "network-root-truststore.jks"
trustStore.update { val networkTrustStorePassword = "network-trust-password"
val networkTrustStore = X509KeyStore.fromFile(networkTrustStorePath, networkTrustStorePassword, createNew = true)
networkTrustStore.update {
setCertificate(X509Utilities.CORDA_ROOT_CA, rootCaCert) setCertificate(X509Utilities.CORDA_ROOT_CA, rootCaCert)
} }
val trustStore = X509KeyStore.fromFile(config.trustStoreFile, config.trustStorePassword, createNew = true)
val nodeKeyStore = X509KeyStore.fromFile(config.nodeKeystore, config.keyStorePassword, createNew = true) val nodeKeyStore = X509KeyStore.fromFile(config.nodeKeystore, config.keyStorePassword, createNew = true)
val sslKeyStore = X509KeyStore.fromFile(config.sslKeystore, config.keyStorePassword, createNew = true) val sslKeyStore = X509KeyStore.fromFile(config.sslKeystore, config.keyStorePassword, createNew = true)
config.also { config.also {
@ -137,7 +140,7 @@ class SigningServiceIntegrationTest {
doReturn(nodeKeyStore).whenever(it).loadNodeKeyStore(any()) doReturn(nodeKeyStore).whenever(it).loadNodeKeyStore(any())
doReturn(sslKeyStore).whenever(it).loadSslKeyStore(any()) doReturn(sslKeyStore).whenever(it).loadSslKeyStore(any())
} }
NetworkRegistrationHelper(config, HTTPNetworkRegistrationService(config.compatibilityZoneURL!!)).buildKeystore() NetworkRegistrationHelper(config, HTTPNetworkRegistrationService(config.compatibilityZoneURL!!), networkTrustStorePath, networkTrustStorePassword).buildKeystore()
verify(hsmSigner).sign(any()) verify(hsmSigner).sign(any())
} }
} }

View File

@ -1,6 +1,5 @@
package net.corda.node.utilities.registration package net.corda.node.utilities.registration
import net.corda.core.crypto.Crypto
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.concurrent.transpose import net.corda.core.internal.concurrent.transpose
import net.corda.core.messaging.startFlow import net.corda.core.messaging.startFlow
@ -28,7 +27,6 @@ import net.corda.testing.node.internal.CompatibilityZoneParams
import net.corda.testing.node.internal.internalDriver import net.corda.testing.node.internal.internalDriver
import net.corda.testing.node.internal.network.NetworkMapServer import net.corda.testing.node.internal.network.NetworkMapServer
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.PKCS10CertificationRequest
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import org.junit.* import org.junit.*
@ -37,12 +35,10 @@ import java.io.InputStream
import java.net.URL import java.net.URL
import java.security.KeyPair import java.security.KeyPair
import java.security.cert.CertPath import java.security.cert.CertPath
import java.security.cert.CertPathValidatorException
import java.security.cert.Certificate import java.security.cert.Certificate
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
import javax.security.auth.x500.X500Principal
import javax.ws.rs.* import javax.ws.rs.*
import javax.ws.rs.core.MediaType import javax.ws.rs.core.MediaType
import javax.ws.rs.core.Response import javax.ws.rs.core.Response
@ -118,28 +114,6 @@ class NodeRegistrationTest : IntegrationTest() {
).returnValue.getOrThrow() ).returnValue.getOrThrow()
} }
} }
@Test
fun `node registration wrong root cert`() {
val someRootCert = X509Utilities.createSelfSignedCACertificate(
X500Principal("CN=Integration Test Corda Node Root CA,O=R3 Ltd,L=London,C=GB"),
Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME))
val compatibilityZone = CompatibilityZoneParams(
URL("http://$serverHostAndPort"),
publishNotaries = { server.networkParameters = testNetworkParameters(it) },
rootCert = someRootCert)
internalDriver(
portAllocation = portAllocation,
compatibilityZone = compatibilityZone,
initialiseSerialization = false,
notarySpecs = listOf(NotarySpec(notaryName)),
startNodesInProcess = true // We need to run the nodes in the same process so that we can capture the correct exception
) {
assertThatThrownBy {
defaultNotaryNode.getOrThrow()
}.isInstanceOf(CertPathValidatorException::class.java)
}
}
} }
@Path("certificate") @Path("certificate")

View File

@ -1,6 +1,5 @@
package net.corda.node package net.corda.node
import com.typesafe.config.ConfigException
import joptsimple.OptionParser import joptsimple.OptionParser
import joptsimple.util.EnumConverter import joptsimple.util.EnumConverter
import net.corda.core.internal.div import net.corda.core.internal.div
@ -36,6 +35,10 @@ class ArgsParser {
private val sshdServerArg = optionParser.accepts("sshd", "Enables SSHD server for node administration.") private val sshdServerArg = optionParser.accepts("sshd", "Enables SSHD server for node administration.")
private val noLocalShellArg = optionParser.accepts("no-local-shell", "Do not start the embedded shell locally.") private val noLocalShellArg = optionParser.accepts("no-local-shell", "Do not start the embedded shell locally.")
private val isRegistrationArg = optionParser.accepts("initial-registration", "Start initial node registration with Corda network to obtain certificate from the permissioning server.") private val isRegistrationArg = optionParser.accepts("initial-registration", "Start initial node registration with Corda network to obtain certificate from the permissioning server.")
private val networkRootTruststorePathArg = optionParser.accepts("network-root-truststore", "Network root trust store obtained from network operator.")
.withRequiredArg()
private val networkRootTruststorePasswordArg = optionParser.accepts("network-root-truststore-password", "Network root trust store password obtained from network operator.")
.withRequiredArg()
private val isVersionArg = optionParser.accepts("version", "Print the version and exit") private val isVersionArg = optionParser.accepts("version", "Print the version and exit")
private val justGenerateNodeInfoArg = optionParser.accepts("just-generate-node-info", private val justGenerateNodeInfoArg = optionParser.accepts("just-generate-node-info",
"Perform the node start-up task necessary to generate its nodeInfo, save it to disk, then quit") "Perform the node start-up task necessary to generate its nodeInfo, save it to disk, then quit")
@ -58,8 +61,21 @@ class ArgsParser {
val sshdServer = optionSet.has(sshdServerArg) val sshdServer = optionSet.has(sshdServerArg)
val justGenerateNodeInfo = optionSet.has(justGenerateNodeInfoArg) val justGenerateNodeInfo = optionSet.has(justGenerateNodeInfoArg)
val bootstrapRaftCluster = optionSet.has(bootstrapRaftClusterArg) val bootstrapRaftCluster = optionSet.has(bootstrapRaftClusterArg)
return CmdLineOptions(baseDirectory, configFile, help, loggingLevel, logToConsole, isRegistration, isVersion, val networkRootTruststorePath = optionSet.valueOf(networkRootTruststorePathArg)?.let { Paths.get(it).normalize().toAbsolutePath() }
noLocalShell, sshdServer, justGenerateNodeInfo, bootstrapRaftCluster) val networkRootTruststorePassword = optionSet.valueOf(networkRootTruststorePasswordArg)
return CmdLineOptions(baseDirectory,
configFile,
help,
loggingLevel,
logToConsole,
isRegistration,
networkRootTruststorePath,
networkRootTruststorePassword,
isVersion,
noLocalShell,
sshdServer,
justGenerateNodeInfo,
bootstrapRaftCluster)
} }
fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink) fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink)
@ -71,6 +87,8 @@ data class CmdLineOptions(val baseDirectory: Path,
val loggingLevel: Level, val loggingLevel: Level,
val logToConsole: Boolean, val logToConsole: Boolean,
val isRegistration: Boolean, val isRegistration: Boolean,
val networkRootTruststorePath: Path?,
val networkRootTruststorePassword: String?,
val isVersion: Boolean, val isVersion: Boolean,
val noLocalShell: Boolean, val noLocalShell: Boolean,
val sshdServer: Boolean, val sshdServer: Boolean,
@ -80,6 +98,8 @@ data class CmdLineOptions(val baseDirectory: Path,
val config = ConfigHelper.loadConfig(baseDirectory, configFile).parseAsNodeConfiguration() val config = ConfigHelper.loadConfig(baseDirectory, configFile).parseAsNodeConfiguration()
if (isRegistration) { if (isRegistration) {
requireNotNull(config.compatibilityZoneURL) { "Compatibility Zone Url must be provided in registration mode." } requireNotNull(config.compatibilityZoneURL) { "Compatibility Zone Url must be provided in registration mode." }
requireNotNull(networkRootTruststorePath) { "Network root trust store path must be provided in registration mode." }
requireNotNull(networkRootTruststorePassword) { "Network root trust store password must be provided in registration mode." }
} }
return config return config
} }

View File

@ -95,7 +95,8 @@ open class NodeStartup(val args: Array<String>) {
banJavaSerialisation(conf) banJavaSerialisation(conf)
preNetworkRegistration(conf) preNetworkRegistration(conf)
if (shouldRegisterWithNetwork(cmdlineOptions, conf)) { if (shouldRegisterWithNetwork(cmdlineOptions, conf)) {
registerWithNetwork(cmdlineOptions, conf) // Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig]
registerWithNetwork(conf, cmdlineOptions.networkRootTruststorePath!!, cmdlineOptions.networkRootTruststorePassword!!)
return true return true
} }
logStartupInfo(versionInfo, cmdlineOptions, conf) logStartupInfo(versionInfo, cmdlineOptions, conf)
@ -184,7 +185,7 @@ open class NodeStartup(val args: Array<String>) {
return !(!cmdlineOptions.isRegistration || compatibilityZoneURL == null) return !(!cmdlineOptions.isRegistration || compatibilityZoneURL == null)
} }
open protected fun registerWithNetwork(cmdlineOptions: CmdLineOptions, conf: NodeConfiguration) { open protected fun registerWithNetwork(conf: NodeConfiguration, networkRootTruststorePath: Path, networkRootTruststorePassword: String) {
val compatibilityZoneURL = conf.compatibilityZoneURL!! val compatibilityZoneURL = conf.compatibilityZoneURL!!
println() println()
println("******************************************************************") println("******************************************************************")
@ -192,7 +193,7 @@ open class NodeStartup(val args: Array<String>) {
println("* Registering as a new participant with Corda network *") println("* Registering as a new participant with Corda network *")
println("* *") println("* *")
println("******************************************************************") println("******************************************************************")
NetworkRegistrationHelper(conf, HTTPNetworkRegistrationService(compatibilityZoneURL)).buildKeystore() NetworkRegistrationHelper(conf, HTTPNetworkRegistrationService(compatibilityZoneURL), networkRootTruststorePath, networkRootTruststorePassword).buildKeystore()
} }
open protected fun loadConfigFile(cmdlineOptions: CmdLineOptions): NodeConfiguration = cmdlineOptions.loadConfig() open protected fun loadConfigFile(cmdlineOptions: CmdLineOptions): NodeConfiguration = cmdlineOptions.loadConfig()

View File

@ -61,7 +61,7 @@ class NodeSchedulerService(private val clock: CordaClock,
private val serverThread: Executor, private val serverThread: Executor,
private val flowLogicRefFactory: FlowLogicRefFactory, private val flowLogicRefFactory: FlowLogicRefFactory,
private val log: Logger = staticLog, private val log: Logger = staticLog,
scheduledStates: MutableMap<StateRef, ScheduledStateRef> = createMap()) private val scheduledStates: MutableMap<StateRef, ScheduledStateRef> = createMap())
: SchedulerService, SingletonSerializeAsToken() { : SchedulerService, SingletonSerializeAsToken() {
companion object { companion object {
@ -153,13 +153,13 @@ class NodeSchedulerService(private val clock: CordaClock,
var scheduledAt: Instant = Instant.now() var scheduledAt: Instant = Instant.now()
) )
private class InnerState(var scheduledStates: MutableMap<StateRef, ScheduledStateRef>) { private class InnerState {
var scheduledStatesQueue: PriorityQueue<ScheduledStateRef> = PriorityQueue({ a, b -> a.scheduledAt.compareTo(b.scheduledAt) }) var scheduledStatesQueue: PriorityQueue<ScheduledStateRef> = PriorityQueue({ a, b -> a.scheduledAt.compareTo(b.scheduledAt) })
var rescheduled: GuavaSettableFuture<Boolean>? = null var rescheduled: GuavaSettableFuture<Boolean>? = null
} }
private val mutex = ThreadBox(InnerState(scheduledStates)) private val mutex = ThreadBox(InnerState())
// We need the [StateMachineManager] to be constructed before this is called in case it schedules a flow. // We need the [StateMachineManager] to be constructed before this is called in case it schedules a flow.
fun start() { fun start() {
mutex.locked { mutex.locked {
@ -170,9 +170,9 @@ class NodeSchedulerService(private val clock: CordaClock,
override fun scheduleStateActivity(action: ScheduledStateRef) { override fun scheduleStateActivity(action: ScheduledStateRef) {
log.trace { "Schedule $action" } log.trace { "Schedule $action" }
val previousState = scheduledStates[action.ref]
scheduledStates[action.ref] = action
mutex.locked { mutex.locked {
val previousState = scheduledStates[action.ref]
scheduledStates[action.ref] = action
val previousEarliest = scheduledStatesQueue.peek() val previousEarliest = scheduledStatesQueue.peek()
scheduledStatesQueue.remove(previousState) scheduledStatesQueue.remove(previousState)
scheduledStatesQueue.add(action) scheduledStatesQueue.add(action)
@ -192,12 +192,15 @@ class NodeSchedulerService(private val clock: CordaClock,
override fun unscheduleStateActivity(ref: StateRef) { override fun unscheduleStateActivity(ref: StateRef) {
log.trace { "Unschedule $ref" } log.trace { "Unschedule $ref" }
val removedAction = scheduledStates.remove(ref)
mutex.locked { mutex.locked {
val removedAction = scheduledStates.remove(ref)
if (removedAction != null) { if (removedAction != null) {
scheduledStatesQueue.remove(removedAction) val wasNext = (removedAction == scheduledStatesQueue.peek())
unfinishedSchedules.countDown() val wasRemoved = scheduledStatesQueue.remove(removedAction)
if (removedAction == scheduledStatesQueue.peek()) { if (wasRemoved) {
unfinishedSchedules.countDown()
}
if (wasNext) {
rescheduleWakeUp() rescheduleWakeUp()
} }
} }

View File

@ -18,7 +18,7 @@ class ScheduledActivityObserver private constructor(private val schedulerService
fun install(vaultService: VaultService, schedulerService: SchedulerService, flowLogicRefFactory: FlowLogicRefFactory) { fun install(vaultService: VaultService, schedulerService: SchedulerService, flowLogicRefFactory: FlowLogicRefFactory) {
val observer = ScheduledActivityObserver(schedulerService, flowLogicRefFactory) val observer = ScheduledActivityObserver(schedulerService, flowLogicRefFactory)
vaultService.rawUpdates.subscribe { (consumed, produced) -> vaultService.rawUpdates.subscribe { (consumed, produced) ->
consumed.forEach { schedulerService.unscheduleStateActivity(it.ref) } consumed.forEach { if (it.state.data is SchedulableState) schedulerService.unscheduleStateActivity(it.ref) }
produced.forEach { observer.scheduleStateActivity(it) } produced.forEach { observer.scheduleStateActivity(it) }
} }
} }

View File

@ -11,6 +11,7 @@ import net.corda.core.serialization.serialize
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.minutes import net.corda.core.utilities.minutes
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
import net.corda.core.utilities.trace
import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.node.utilities.NamedThreadFactory import net.corda.node.utilities.NamedThreadFactory
import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.SignedNodeInfo
@ -29,10 +30,15 @@ import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class NetworkMapClient(compatibilityZoneURL: URL, private val trustedRoot: X509Certificate) { class NetworkMapClient(compatibilityZoneURL: URL, private val trustedRoot: X509Certificate) {
companion object {
private val logger = contextLogger()
}
private val networkMapUrl = URL("$compatibilityZoneURL/network-map") private val networkMapUrl = URL("$compatibilityZoneURL/network-map")
fun publish(signedNodeInfo: SignedNodeInfo) { fun publish(signedNodeInfo: SignedNodeInfo) {
val publishURL = URL("$networkMapUrl/publish") val publishURL = URL("$networkMapUrl/publish")
logger.trace { "Publishing NodeInfo to $publishURL." }
publishURL.openHttpConnection().apply { publishURL.openHttpConnection().apply {
doOutput = true doOutput = true
requestMethod = "POST" requestMethod = "POST"
@ -40,27 +46,41 @@ class NetworkMapClient(compatibilityZoneURL: URL, private val trustedRoot: X509C
outputStream.use { signedNodeInfo.serialize().open().copyTo(it) } outputStream.use { signedNodeInfo.serialize().open().copyTo(it) }
checkOkResponse() checkOkResponse()
} }
logger.trace { "Published NodeInfo to $publishURL successfully." }
} }
fun getNetworkMap(): NetworkMapResponse { fun getNetworkMap(): NetworkMapResponse {
logger.trace { "Fetching network map update from $networkMapUrl." }
val connection = networkMapUrl.openHttpConnection() val connection = networkMapUrl.openHttpConnection()
val signedNetworkMap = connection.responseAs<SignedDataWithCert<NetworkMap>>() val signedNetworkMap = connection.responseAs<SignedDataWithCert<NetworkMap>>()
val networkMap = signedNetworkMap.verifiedNetworkMapCert(trustedRoot) val networkMap = signedNetworkMap.verifiedNetworkMapCert(trustedRoot)
val timeout = CacheControl.parse(Headers.of(connection.headerFields.filterKeys { it != null }.mapValues { it.value[0] })).maxAgeSeconds().seconds val timeout = CacheControl.parse(Headers.of(connection.headerFields.filterKeys { it != null }.mapValues { it.value[0] })).maxAgeSeconds().seconds
logger.trace { "Fetched network map update from $networkMapUrl successfully, retrieved ${networkMap.nodeInfoHashes.size} node info hashes. Node Info hashes: ${networkMap.nodeInfoHashes.joinToString("\n")}" }
return NetworkMapResponse(networkMap, timeout) return NetworkMapResponse(networkMap, timeout)
} }
fun getNodeInfo(nodeInfoHash: SecureHash): NodeInfo { fun getNodeInfo(nodeInfoHash: SecureHash): NodeInfo {
return URL("$networkMapUrl/node-info/$nodeInfoHash").openHttpConnection().responseAs<SignedNodeInfo>().verified() val url = URL("$networkMapUrl/node-info/$nodeInfoHash")
logger.trace { "Fetching node info: '$nodeInfoHash' from $url." }
val verifiedNodeInfo = url.openHttpConnection().responseAs<SignedNodeInfo>().verified()
logger.trace { "Fetched node info: '$nodeInfoHash' successfully. Node Info: $verifiedNodeInfo" }
return verifiedNodeInfo
} }
fun getNetworkParameters(networkParameterHash: SecureHash): SignedDataWithCert<NetworkParameters> { fun getNetworkParameters(networkParameterHash: SecureHash): SignedDataWithCert<NetworkParameters> {
return URL("$networkMapUrl/network-parameters/$networkParameterHash").openHttpConnection().responseAs() val url = URL("$networkMapUrl/network-parameters/$networkParameterHash")
logger.trace { "Fetching network parameters: '$networkParameterHash' from $url." }
val networkParameter = url.openHttpConnection().responseAs<SignedDataWithCert<NetworkParameters>>()
logger.trace { "Fetched network parameters: '$networkParameterHash' successfully. Network Parameters: $networkParameter" }
return networkParameter
} }
fun myPublicHostname(): String { fun myPublicHostname(): String {
val connection = URL("$networkMapUrl/my-hostname").openHttpConnection() val url = URL("$networkMapUrl/my-hostname")
return connection.inputStream.bufferedReader().use(BufferedReader::readLine) logger.trace { "Resolving public hostname from '$url'." }
val hostName = url.openHttpConnection().inputStream.bufferedReader().use(BufferedReader::readLine)
logger.trace { "My public hostname is $hostName." }
return hostName
} }
} }

View File

@ -3,7 +3,10 @@ package net.corda.node.services.transactions
import io.atomix.copycat.Command import io.atomix.copycat.Command
import io.atomix.copycat.Query import io.atomix.copycat.Query
import io.atomix.copycat.server.Commit import io.atomix.copycat.server.Commit
import io.atomix.copycat.server.Snapshottable
import io.atomix.copycat.server.StateMachine import io.atomix.copycat.server.StateMachine
import io.atomix.copycat.server.storage.snapshot.SnapshotReader
import io.atomix.copycat.server.storage.snapshot.SnapshotWriter
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.node.utilities.AppendOnlyPersistentMap
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
@ -16,7 +19,7 @@ import java.util.*
* The map contents are backed by a JDBC table. State re-synchronisation is achieved by replaying the command log to the * The map contents are backed by a JDBC table. State re-synchronisation is achieved by replaying the command log to the
* new (or re-joining) cluster member. * new (or re-joining) cluster member.
*/ */
class DistributedImmutableMap<K : Any, V : Any, E, EK>(val db: CordaPersistence, createMap: () -> AppendOnlyPersistentMap<K, Pair<Long, V>, E, EK>) : StateMachine() { class DistributedImmutableMap<K : Any, V : Any, E, EK>(val db: CordaPersistence, createMap: () -> AppendOnlyPersistentMap<K, Pair<Long, V>, E, EK>) : StateMachine(), Snapshottable {
companion object { companion object {
private val log = contextLogger() private val log = contextLogger()
} }
@ -75,4 +78,29 @@ class DistributedImmutableMap<K : Any, V : Any, E, EK>(val db: CordaPersistence,
return db.transaction { map.size } return db.transaction { map.size }
} }
} }
/**
* Writes out all [map] entries to disk. Note that this operation does not load all entries into memory, as the
* [SnapshotWriter] is using a disk-backed buffer internally, and iterating map entries results in only a
* fixed number of recently accessed entries to ever be kept in memory.
*/
override fun snapshot(writer: SnapshotWriter) {
db.transaction {
writer.writeInt(map.size)
map.allPersisted().forEach { writer.writeObject(it.first to it.second) }
}
}
/** Reads entries from disk and adds them to [map]. */
override fun install(reader: SnapshotReader) {
val size = reader.readInt()
db.transaction {
map.clear()
// TODO: read & put entries in batches
for (i in 1..size) {
val (key, value) = reader.readObject<Pair<K, Pair<Long, V>>>()
map[key] = value
}
}
}
} }

View File

@ -6,14 +6,15 @@ import net.corda.core.internal.*
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509KeyStore
import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
import net.corda.nodeapi.internal.crypto.x509
import org.bouncycastle.openssl.jcajce.JcaPEMWriter import org.bouncycastle.openssl.jcajce.JcaPEMWriter
import org.bouncycastle.util.io.pem.PemObject import org.bouncycastle.util.io.pem.PemObject
import java.io.StringWriter import java.io.StringWriter
import java.nio.file.Path
import java.security.KeyPair import java.security.KeyPair
import java.security.KeyStore import java.security.KeyStore
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
@ -22,7 +23,10 @@ import java.security.cert.X509Certificate
* Helper for managing the node registration process, which checks for any existing certificates and requests them if * Helper for managing the node registration process, which checks for any existing certificates and requests them if
* needed. * needed.
*/ */
class NetworkRegistrationHelper(private val config: NodeConfiguration, private val certService: NetworkRegistrationService) { class NetworkRegistrationHelper(private val config: NodeConfiguration,
private val certService: NetworkRegistrationService,
networkRootTrustStorePath: Path,
networkRootTruststorePassword: String) {
private companion object { private companion object {
val pollInterval = 10.seconds val pollInterval = 10.seconds
const val SELF_SIGNED_PRIVATE_KEY = "Self Signed Private Key" const val SELF_SIGNED_PRIVATE_KEY = "Self Signed Private Key"
@ -31,20 +35,16 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v
private val requestIdStore = config.certificatesDirectory / "certificate-request-id.txt" private val requestIdStore = config.certificatesDirectory / "certificate-request-id.txt"
// TODO: Use different password for private key. // TODO: Use different password for private key.
private val privateKeyPassword = config.keyStorePassword private val privateKeyPassword = config.keyStorePassword
private val rootTrustStore: X509KeyStore
private val rootCert: X509Certificate private val rootCert: X509Certificate
init { init {
require(config.trustStoreFile.exists()) { require(networkRootTrustStorePath.exists()) {
"${config.trustStoreFile} does not exist. This file must contain the root CA cert of your compatibility zone. " + "$networkRootTrustStorePath does not exist. This file must contain the root CA cert of your compatibility zone. " +
"Please contact your CZ operator." "Please contact your CZ operator."
} }
val rootCert = config.loadTrustStore().internal.getCertificate(CORDA_ROOT_CA) rootTrustStore = X509KeyStore.fromFile(networkRootTrustStorePath, networkRootTruststorePassword)
require(rootCert != null) { rootCert = rootTrustStore.getCertificate(CORDA_ROOT_CA)
"${config.trustStoreFile} does not contain a certificate with the key $CORDA_ROOT_CA." +
"This file must contain the root CA cert of your compatibility zone. " +
"Please contact your CZ operator."
}
this.rootCert = rootCert.x509
} }
/** /**
@ -109,7 +109,7 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v
throw CertificateRequestException("Received node CA cert has invalid role: $nodeCaCertRole") throw CertificateRequestException("Received node CA cert has invalid role: $nodeCaCertRole")
} }
println("Checking root of the certificate path is what we expect.") // Validate certificate chain returned from the doorman with the root cert obtained via out-of-band process, to prevent MITM attack on doorman server.
X509Utilities.validateCertificateChain(rootCert, certificates) X509Utilities.validateCertificateChain(rootCert, certificates)
println("Certificate signing request approved, storing private key with the certificate chain.") println("Certificate signing request approved, storing private key with the certificate chain.")
@ -119,6 +119,14 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, private v
nodeKeyStore.save() nodeKeyStore.save()
println("Node private key and certificate stored in ${config.nodeKeystore}.") println("Node private key and certificate stored in ${config.nodeKeystore}.")
// Save root certificates to trust store.
config.loadTrustStore(createNew = true).update {
println("Generating trust store for corda node.")
// Assumes certificate chain always starts with client certificate and end with root certificate.
setCertificate(CORDA_ROOT_CA, certificates.last())
}
println("Node trust store stored in ${config.trustStoreFile}.")
config.loadSslKeyStore(createNew = true).update { config.loadSslKeyStore(createNew = true).update {
println("Generating SSL certificate for node messaging service.") println("Generating SSL certificate for node messaging service.")
val sslKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val sslKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)

View File

@ -7,6 +7,7 @@ import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.Test import org.junit.Test
import org.slf4j.event.Level import org.slf4j.event.Level
import java.nio.file.Paths import java.nio.file.Paths
import kotlin.test.assertEquals
class ArgsParserTest { class ArgsParserTest {
private val parser = ArgsParser() private val parser = ArgsParser()
@ -25,8 +26,9 @@ class ArgsParserTest {
noLocalShell = false, noLocalShell = false,
sshdServer = false, sshdServer = false,
justGenerateNodeInfo = false, justGenerateNodeInfo = false,
bootstrapRaftCluster = false bootstrapRaftCluster = false,
)) networkRootTruststorePassword = null,
networkRootTruststorePath = null))
} }
@Test @Test
@ -111,8 +113,11 @@ class ArgsParserTest {
@Test @Test
fun `initial-registration`() { fun `initial-registration`() {
val cmdLineOptions = parser.parse("--initial-registration") val truststorePath = Paths.get("truststore") / "file.jks"
val cmdLineOptions = parser.parse("--initial-registration", "--network-root-truststore", "$truststorePath", "--network-root-truststore-password", "password-test")
assertThat(cmdLineOptions.isRegistration).isTrue() assertThat(cmdLineOptions.isRegistration).isTrue()
assertEquals(truststorePath.toAbsolutePath(), cmdLineOptions.networkRootTruststorePath)
assertEquals("password-test", cmdLineOptions.networkRootTruststorePassword)
} }
@Test @Test

View File

@ -8,16 +8,16 @@ import net.corda.core.crypto.newSecureRandom
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowLogicRef import net.corda.core.flows.FlowLogicRef
import net.corda.core.flows.FlowLogicRefFactory import net.corda.core.flows.FlowLogicRefFactory
import net.corda.core.utilities.days
import net.corda.testing.internal.rigorousMock
import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.concurrent.openFuture
import net.corda.core.internal.uncheckedCast import net.corda.core.internal.uncheckedCast
import net.corda.core.node.StateLoader import net.corda.core.node.StateLoader
import net.corda.core.utilities.days
import net.corda.node.services.api.FlowStarter import net.corda.node.services.api.FlowStarter
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseTransaction import net.corda.nodeapi.internal.persistence.DatabaseTransaction
import net.corda.testing.internal.doLookup import net.corda.testing.internal.doLookup
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.TestClock import net.corda.testing.node.TestClock
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -165,7 +165,7 @@ class NodeSchedulerServiceTest {
val eventA = schedule(mark + 1.days) val eventA = schedule(mark + 1.days)
val eventB = schedule(mark + 1.days) val eventB = schedule(mark + 1.days)
scheduler.unscheduleStateActivity(eventA.stateRef) scheduler.unscheduleStateActivity(eventA.stateRef)
assertWaitingFor(eventA) // XXX: Shouldn't it be waiting for eventB now? assertWaitingFor(eventB)
testClock.advanceBy(1.days) testClock.advanceBy(1.days)
assertStarted(eventB) assertStarted(eventB)
} }

View File

@ -10,9 +10,11 @@ import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.createDirectories import net.corda.core.internal.createDirectories
import net.corda.core.internal.div
import net.corda.core.internal.x500Name import net.corda.core.internal.x500Name
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509KeyStore
import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
import net.corda.testing.internal.createDevIntermediateCaCertPath import net.corda.testing.internal.createDevIntermediateCaCertPath
@ -35,10 +37,13 @@ class NetworkRegistrationHelperTest {
private val nodeLegalName = ALICE_NAME private val nodeLegalName = ALICE_NAME
private lateinit var config: NodeConfiguration private lateinit var config: NodeConfiguration
private val networkRootTrustStoreFileName = "network-root-truststore.jks"
private val networkRootTrustStorePassword = "network-root-truststore-password"
@Before @Before
fun init() { fun init() {
val baseDirectory = fs.getPath("/baseDir").createDirectories() val baseDirectory = fs.getPath("/baseDir").createDirectories()
abstract class AbstractNodeConfiguration : NodeConfiguration abstract class AbstractNodeConfiguration : NodeConfiguration
config = rigorousMock<AbstractNodeConfiguration>().also { config = rigorousMock<AbstractNodeConfiguration>().also {
doReturn(baseDirectory).whenever(it).baseDirectory doReturn(baseDirectory).whenever(it).baseDirectory
@ -62,7 +67,7 @@ class NetworkRegistrationHelperTest {
val nodeCaCertPath = createNodeCaCertPath() val nodeCaCertPath = createNodeCaCertPath()
saveTrustStoreWithRootCa(nodeCaCertPath.last()) saveNetworkTrustStore(nodeCaCertPath.last())
createRegistrationHelper(nodeCaCertPath).buildKeystore() createRegistrationHelper(nodeCaCertPath).buildKeystore()
val nodeKeystore = config.loadNodeKeyStore() val nodeKeystore = config.loadNodeKeyStore()
@ -105,7 +110,7 @@ class NetworkRegistrationHelperTest {
@Test @Test
fun `node CA with incorrect cert role`() { fun `node CA with incorrect cert role`() {
val nodeCaCertPath = createNodeCaCertPath(type = CertificateType.TLS) val nodeCaCertPath = createNodeCaCertPath(type = CertificateType.TLS)
saveTrustStoreWithRootCa(nodeCaCertPath.last()) saveNetworkTrustStore(nodeCaCertPath.last())
val registrationHelper = createRegistrationHelper(nodeCaCertPath) val registrationHelper = createRegistrationHelper(nodeCaCertPath)
assertThatExceptionOfType(CertificateRequestException::class.java) assertThatExceptionOfType(CertificateRequestException::class.java)
.isThrownBy { registrationHelper.buildKeystore() } .isThrownBy { registrationHelper.buildKeystore() }
@ -116,7 +121,7 @@ class NetworkRegistrationHelperTest {
fun `node CA with incorrect subject`() { fun `node CA with incorrect subject`() {
val invalidName = CordaX500Name("Foo", "MU", "GB") val invalidName = CordaX500Name("Foo", "MU", "GB")
val nodeCaCertPath = createNodeCaCertPath(legalName = invalidName) val nodeCaCertPath = createNodeCaCertPath(legalName = invalidName)
saveTrustStoreWithRootCa(nodeCaCertPath.last()) saveNetworkTrustStore(nodeCaCertPath.last())
val registrationHelper = createRegistrationHelper(nodeCaCertPath) val registrationHelper = createRegistrationHelper(nodeCaCertPath)
assertThatExceptionOfType(CertificateRequestException::class.java) assertThatExceptionOfType(CertificateRequestException::class.java)
.isThrownBy { registrationHelper.buildKeystore() } .isThrownBy { registrationHelper.buildKeystore() }
@ -128,7 +133,7 @@ class NetworkRegistrationHelperTest {
val wrongRootCert = X509Utilities.createSelfSignedCACertificate( val wrongRootCert = X509Utilities.createSelfSignedCACertificate(
X500Principal("O=Foo,L=MU,C=GB"), X500Principal("O=Foo,L=MU,C=GB"),
Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME))
saveTrustStoreWithRootCa(wrongRootCert) saveNetworkTrustStore(wrongRootCert)
val registrationHelper = createRegistrationHelper(createNodeCaCertPath()) val registrationHelper = createRegistrationHelper(createNodeCaCertPath())
assertThatThrownBy { assertThatThrownBy {
registrationHelper.buildKeystore() registrationHelper.buildKeystore()
@ -155,12 +160,13 @@ class NetworkRegistrationHelperTest {
doReturn(requestId).whenever(it).submitRequest(any()) doReturn(requestId).whenever(it).submitRequest(any())
doReturn(response).whenever(it).retrieveCertificates(eq(requestId)) doReturn(response).whenever(it).retrieveCertificates(eq(requestId))
} }
return NetworkRegistrationHelper(config, certService) return NetworkRegistrationHelper(config, certService, config.certificatesDirectory / networkRootTrustStoreFileName, networkRootTrustStorePassword)
} }
private fun saveTrustStoreWithRootCa(rootCert: X509Certificate) { private fun saveNetworkTrustStore(rootCert: X509Certificate) {
config.certificatesDirectory.createDirectories() config.certificatesDirectory.createDirectories()
config.loadTrustStore(createNew = true).update { val rootTruststorePath = config.certificatesDirectory / networkRootTrustStoreFileName
X509KeyStore.fromFile(rootTruststorePath, networkRootTrustStorePassword, createNew = true).update {
setCertificate(X509Utilities.CORDA_ROOT_CA, rootCert) setCertificate(X509Utilities.CORDA_ROOT_CA, rootCert)
} }
} }

View File

@ -56,6 +56,8 @@ dependencies {
testCompile project(':node-driver') testCompile project(':node-driver')
testCompile "junit:junit:$junit_version" testCompile "junit:junit:$junit_version"
testCompile "org.assertj:assertj-core:${assertj_version}" testCompile "org.assertj:assertj-core:${assertj_version}"
integrationTestCompile project(path: ":samples:irs-demo:web", configuration: "demoArtifacts")
} }
bootRepackage { bootRepackage {

View File

@ -94,6 +94,14 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
rpcUsers = ext.rpcUsers rpcUsers = ext.rpcUsers
useTestClock true useTestClock true
} }
node {
name "O=Regulator,L=Moscow,C=RU"
p2pPort 10010
rpcPort 10011
cordapps = ["${project.group}:finance:$corda_release_version"]
rpcUsers = ext.rpcUsers
useTestClock true
}
} }
task integrationTest(type: Test, dependsOn: []) { task integrationTest(type: Test, dependsOn: []) {

View File

@ -1,5 +1,6 @@
package net.corda.irs package net.corda.irs
import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_BANK_A_NAME
import net.corda.testing.core.DUMMY_BANK_B_NAME import net.corda.testing.core.DUMMY_BANK_B_NAME
@ -13,7 +14,8 @@ fun main(args: Array<String>) {
driver(useTestClock = true, isDebug = true, waitForAllNodesToFinish = true) { driver(useTestClock = true, isDebug = true, waitForAllNodesToFinish = true) {
val (nodeA, nodeB) = listOf( val (nodeA, nodeB) = listOf(
startNode(providedName = DUMMY_BANK_A_NAME), startNode(providedName = DUMMY_BANK_A_NAME),
startNode(providedName = DUMMY_BANK_B_NAME) startNode(providedName = DUMMY_BANK_B_NAME),
startNode(providedName = CordaX500Name("Regulator", "Moscow", "RU"))
).map { it.getOrThrow() } ).map { it.getOrThrow() }
val controller = defaultNotaryNode.getOrThrow() val controller = defaultNotaryNode.getOrThrow()

View File

@ -11,6 +11,7 @@ import com.fasterxml.jackson.module.kotlin.readValue
import net.corda.client.jackson.JacksonSupport import net.corda.client.jackson.JacksonSupport
import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCClient
import net.corda.core.contracts.UniqueIdentifier import net.corda.core.contracts.UniqueIdentifier
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.messaging.vaultTrackBy import net.corda.core.messaging.vaultTrackBy
import net.corda.core.toFuture import net.corda.core.toFuture
@ -64,7 +65,8 @@ class IRSDemoTest : IntegrationTest() {
) { ) {
val (nodeA, nodeB) = listOf( val (nodeA, nodeB) = listOf(
startNode(providedName = DUMMY_BANK_A_NAME, rpcUsers = rpcUsers), startNode(providedName = DUMMY_BANK_A_NAME, rpcUsers = rpcUsers),
startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = rpcUsers) startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = rpcUsers),
startNode(providedName = CordaX500Name("Regulator", "Moscow", "RU"))
).map { it.getOrThrow() } ).map { it.getOrThrow() }
val controller = defaultNotaryNode.getOrThrow() val controller = defaultNotaryNode.getOrThrow()

View File

@ -1,34 +1,34 @@
buildscript { buildscript {
repositories { repositories {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlin_version}") classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlin_version}")
} }
} }
plugins { plugins {
id 'com.craigburke.client-dependencies' version '1.4.0' id 'com.craigburke.client-dependencies' version '1.4.0'
} }
clientDependencies { clientDependencies {
registry 'realBower', type:'bower', url:'https://registry.bower.io' registry 'realBower', type:'bower', url:'https://registry.bower.io'
realBower { realBower {
"angular"("1.5.8") "angular"("1.5.8")
"jquery"("^3.0.0") "jquery"("^3.0.0")
"angular-route"("1.5.8") "angular-route"("1.5.8")
"lodash"("^4.13.1") "lodash"("^4.13.1")
"angular-fcsa-number"("^1.5.3") "angular-fcsa-number"("^1.5.3")
"jquery.maskedinput"("^1.4.1") "jquery.maskedinput"("^1.4.1")
"requirejs"("^2.2.0") "requirejs"("^2.2.0")
"semantic-ui"("^2.2.2", into: "semantic") "semantic-ui"("^2.2.2", into: "semantic")
} }
// put the JS dependencies into src directory so it can easily be referenced // put the JS dependencies into src directory so it can easily be referenced
// from HTML files in webapp frontend, useful for testing/development // from HTML files in webapp frontend, useful for testing/development
// Note that this dir is added to .gitignore // Note that this dir is added to .gitignore
installDir = 'src/main/resources/static/js/bower_components' installDir = 'src/main/resources/static/js/bower_components'
} }
// Spring Boot plugin adds a numerous hardcoded dependencies in the version much lower then Corda expects // Spring Boot plugin adds a numerous hardcoded dependencies in the version much lower then Corda expects
@ -44,36 +44,49 @@ apply plugin: 'org.springframework.boot'
apply plugin: 'project-report' apply plugin: 'project-report'
apply plugin: 'application' apply plugin: 'application'
configurations {
demoArtifacts.extendsFrom testRuntime
}
dependencies { dependencies {
compile('org.springframework.boot:spring-boot-starter-web') { compile('org.springframework.boot:spring-boot-starter-web') {
exclude module: "spring-boot-starter-logging" exclude module: "spring-boot-starter-logging"
exclude module: "logback-classic" exclude module: "logback-classic"
} }
compile("com.fasterxml.jackson.module:jackson-module-kotlin:2.8.9") compile("com.fasterxml.jackson.module:jackson-module-kotlin:2.8.9")
compile project(":client:rpc") compile project(":client:rpc")
compile project(":client:jackson") compile project(":client:jackson")
compile project(":test-utils") compile project(":test-utils")
compile project(path: ":samples:irs-demo:cordapp", configuration: "demoArtifacts") compile project(path: ":samples:irs-demo:cordapp", configuration: "demoArtifacts")
testCompile('org.springframework.boot:spring-boot-starter-test') { testCompile('org.springframework.boot:spring-boot-starter-test') {
exclude module: "spring-boot-starter-logging" exclude module: "spring-boot-starter-logging"
exclude module: "logback-classic" exclude module: "logback-classic"
} }
} }
jar { jar {
from sourceSets.test.output from sourceSets.test.output
dependsOn clientInstall dependsOn clientInstall
} }
task deployWebapps(type: Copy, dependsOn: ['jar', 'bootRepackage']) { task deployWebapps(type: Copy, dependsOn: ['jar', 'bootRepackage']) {
ext.webappDir = file("build/webapps") ext.webappDir = file("build/webapps")
from(jar.outputs) from(jar.outputs)
from("src/test/resources/scripts/") { from("src/test/resources/scripts/") {
filter { it filter { it
.replace('#JAR_PATH#', jar.archiveName) .replace('#JAR_PATH#', jar.archiveName)
.replace('#DIR#', ext.webappDir.getAbsolutePath()) .replace('#DIR#', ext.webappDir.getAbsolutePath())
} }
} }
into ext.webappDir into ext.webappDir
}
task demoJar(type: Jar) {
classifier "test"
from sourceSets.test.output
}
artifacts {
demoArtifacts demoJar
} }

View File

@ -33,6 +33,7 @@ import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.addShutdownHook
import net.corda.nodeapi.internal.config.parseAs import net.corda.nodeapi.internal.config.parseAs
import net.corda.nodeapi.internal.config.toConfig import net.corda.nodeapi.internal.config.toConfig
import net.corda.nodeapi.internal.crypto.X509KeyStore
import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.network.NetworkParametersCopier
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier import net.corda.nodeapi.internal.network.NodeInfoFilesCopier
@ -239,17 +240,24 @@ class DriverDSLImpl(
)) ))
config.corda.certificatesDirectory.createDirectories() config.corda.certificatesDirectory.createDirectories()
config.corda.loadTrustStore(createNew = true).update { // Create network root truststore.
val rootTruststorePath = config.corda.certificatesDirectory / "network-root-truststore.jks"
// The network truststore will be provided by the network operator via out-of-band communication.
val rootTruststorePassword = "corda-root-password"
X509KeyStore.fromFile(rootTruststorePath, rootTruststorePassword, createNew = true).update {
setCertificate(X509Utilities.CORDA_ROOT_CA, rootCert) setCertificate(X509Utilities.CORDA_ROOT_CA, rootCert)
} }
return if (startNodesInProcess) { return if (startNodesInProcess) {
executorService.fork { executorService.fork {
NetworkRegistrationHelper(config.corda, HTTPNetworkRegistrationService(compatibilityZoneURL)).buildKeystore() NetworkRegistrationHelper(config.corda, HTTPNetworkRegistrationService(compatibilityZoneURL), rootTruststorePath, rootTruststorePassword).buildKeystore()
config config
} }
} else { } else {
startOutOfProcessMiniNode(config, "--initial-registration").map { config } startOutOfProcessMiniNode(config,
"--initial-registration",
"--network-root-truststore=${rootTruststorePath.toAbsolutePath()}",
"--network-root-truststore-password=$rootTruststorePassword").map { config }
} }
} }
@ -482,8 +490,8 @@ class DriverDSLImpl(
when (it.cluster) { when (it.cluster) {
null -> startSingleNotary(it, localNetworkMap) null -> startSingleNotary(it, localNetworkMap)
is ClusterSpec.Raft, is ClusterSpec.Raft,
// DummyCluster is used for testing the notary communication path, and it does not matter // DummyCluster is used for testing the notary communication path, and it does not matter
// which underlying consensus algorithm is used, so we just stick to Raft // which underlying consensus algorithm is used, so we just stick to Raft
is DummyClusterSpec -> startRaftNotaryCluster(it, localNetworkMap) is DummyClusterSpec -> startRaftNotaryCluster(it, localNetworkMap)
else -> throw IllegalArgumentException("BFT-SMaRt not supported") else -> throw IllegalArgumentException("BFT-SMaRt not supported")
} }
@ -595,7 +603,7 @@ class DriverDSLImpl(
* Start the node with the given flag which is expected to start the node for some function, which once complete will * Start the node with the given flag which is expected to start the node for some function, which once complete will
* terminate the node. * terminate the node.
*/ */
private fun startOutOfProcessMiniNode(config: NodeConfig, extraCmdLineFlag: String): CordaFuture<Unit> { private fun startOutOfProcessMiniNode(config: NodeConfig, vararg extraCmdLineFlag: String): CordaFuture<Unit> {
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
val monitorPort = if (jmxPolicy.startJmxHttpServer) jmxPolicy.jmxHttpServerPortAllocation?.nextPort() else null val monitorPort = if (jmxPolicy.startJmxHttpServer) jmxPolicy.jmxHttpServerPortAllocation?.nextPort() else null
val process = startOutOfProcessNode( val process = startOutOfProcessNode(
@ -607,7 +615,7 @@ class DriverDSLImpl(
systemProperties, systemProperties,
cordappPackages, cordappPackages,
"200m", "200m",
extraCmdLineFlag *extraCmdLineFlag
) )
return poll(executorService, "$extraCmdLineFlag (${config.corda.myLegalName})") { return poll(executorService, "$extraCmdLineFlag (${config.corda.myLegalName})") {
@ -651,7 +659,7 @@ class DriverDSLImpl(
} else { } else {
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
val monitorPort = if (jmxPolicy.startJmxHttpServer) jmxPolicy.jmxHttpServerPortAllocation?.nextPort() else null val monitorPort = if (jmxPolicy.startJmxHttpServer) jmxPolicy.jmxHttpServerPortAllocation?.nextPort() else null
val process = startOutOfProcessNode(config, quasarJarPath, debugPort, jolokiaJarPath, monitorPort, systemProperties, cordappPackages, maximumHeapSize, null) val process = startOutOfProcessNode(config, quasarJarPath, debugPort, jolokiaJarPath, monitorPort, systemProperties, cordappPackages, maximumHeapSize)
if (waitForNodesToFinish) { if (waitForNodesToFinish) {
state.locked { state.locked {
processes += process processes += process
@ -762,7 +770,7 @@ class DriverDSLImpl(
overriddenSystemProperties: Map<String, String>, overriddenSystemProperties: Map<String, String>,
cordappPackages: List<String>, cordappPackages: List<String>,
maximumHeapSize: String, maximumHeapSize: String,
extraCmdLineFlag: String? vararg extraCmdLineFlag: String
): Process { ): Process {
log.info("Starting out-of-process Node ${config.corda.myLegalName.organisation}, " + log.info("Starting out-of-process Node ${config.corda.myLegalName.organisation}, " +
"debug port is " + (debugPort ?: "not enabled") + ", " + "debug port is " + (debugPort ?: "not enabled") + ", " +
@ -800,9 +808,7 @@ class DriverDSLImpl(
"--base-directory=${config.corda.baseDirectory}", "--base-directory=${config.corda.baseDirectory}",
"--logging-level=$loggingLevel", "--logging-level=$loggingLevel",
"--no-local-shell").also { "--no-local-shell").also {
if (extraCmdLineFlag != null) { it += extraCmdLineFlag
it += extraCmdLineFlag
}
}.toList() }.toList()
return ProcessUtilities.startCordaProcess( return ProcessUtilities.startCordaProcess(