diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 7234acdbc7..b5126dbc00 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -179,4 +179,7 @@
+
+
+
\ No newline at end of file
diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt
index 9de4fd6afb..813569dc89 100644
--- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt
+++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt
@@ -29,7 +29,7 @@ import net.corda.core.utilities.getOrThrow
import net.corda.nodeapi.ArtemisConsumer
import net.corda.nodeapi.ArtemisProducer
import net.corda.nodeapi.RPCApi
-import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration
+import org.apache.activemq.artemis.api.core.RoutingType
import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
import org.apache.activemq.artemis.api.core.client.ClientMessage
@@ -194,7 +194,7 @@ class RPCClientProxyHandler(
TimeUnit.MILLISECONDS
)
sessionAndProducerPool.run {
- it.session.createTemporaryQueue(clientAddress, ActiveMQDefaultConfiguration.getDefaultRoutingType(), clientAddress)
+ it.session.createTemporaryQueue(clientAddress, RoutingType.ANYCAST, clientAddress)
}
val sessionFactory = serverLocator.createSessionFactory()
val session = sessionFactory.createSession(rpcUsername, rpcPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE)
diff --git a/config/dev/generalnodea.conf b/config/dev/generalnodea.conf
index 12a3200105..7547300ca6 100644
--- a/config/dev/generalnodea.conf
+++ b/config/dev/generalnodea.conf
@@ -4,4 +4,3 @@ trustStorePassword : "trustpass"
p2pAddress : "localhost:10002"
rpcAddress : "localhost:10003"
webAddress : "localhost:10004"
-useHTTPS : false
diff --git a/config/dev/generalnodeb.conf b/config/dev/generalnodeb.conf
index 55ebb1e690..f922e91536 100644
--- a/config/dev/generalnodeb.conf
+++ b/config/dev/generalnodeb.conf
@@ -4,4 +4,3 @@ trustStorePassword : "trustpass"
p2pAddress : "localhost:10005"
rpcAddress : "localhost:10006"
webAddress : "localhost:10007"
-useHTTPS : false
diff --git a/config/dev/nameservernode.conf b/config/dev/nameservernode.conf
index 0519e56872..3f700628dc 100644
--- a/config/dev/nameservernode.conf
+++ b/config/dev/nameservernode.conf
@@ -6,4 +6,3 @@ webAddress : "localhost:10001"
notary : {
validating : true
}
-useHTTPS : false
diff --git a/core/src/main/kotlin/net/corda/core/internal/schemas/NodeInfoSchema.kt b/core/src/main/kotlin/net/corda/core/internal/schemas/NodeInfoSchema.kt
index 1d08af2759..e77bdd95bf 100644
--- a/core/src/main/kotlin/net/corda/core/internal/schemas/NodeInfoSchema.kt
+++ b/core/src/main/kotlin/net/corda/core/internal/schemas/NodeInfoSchema.kt
@@ -66,26 +66,24 @@ object NodeInfoSchemaV1 : MappedSchema(
}
}
- @Embeddable
- data class PKHostAndPort(
- val host: String? = null,
- val port: Int? = null
- ) : Serializable
-
@Entity
@Table(name = "node_info_hosts")
data class DBHostAndPort(
- @EmbeddedId
- private val pk: PKHostAndPort
+ @Id
+ @GeneratedValue
+ @Column(name = "hosts_id")
+ var id: Int,
+ val host: String? = null,
+ val port: Int? = null
) {
companion object {
fun fromHostAndPort(hostAndPort: NetworkHostAndPort) = DBHostAndPort(
- PKHostAndPort(hostAndPort.host, hostAndPort.port)
+ 0, hostAndPort.host, hostAndPort.port
)
}
fun toHostAndPort(): NetworkHostAndPort {
- return NetworkHostAndPort(this.pk.host!!, this.pk.port!!)
+ return NetworkHostAndPort(host!!, port!!)
}
}
diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst
index 08d5020fe7..29970a8b30 100644
--- a/docs/source/changelog.rst
+++ b/docs/source/changelog.rst
@@ -158,6 +158,13 @@ R3 Corda 3.0 Developer Preview
* Peer-to-peer communications is now via AMQP 1.0 as default.
Although the legacy Artemis CORE bridging can still be used by setting the ``useAMQPBridges`` configuration property to false.
+* The Artemis topics used for peer-to-peer communication have been changed to be more consistent with future cryptographic
+ agility and to open up the future possibility of sharing brokers between nodes. This is a breaking wire level change
+ as it means that nodes after this change will not be able to communicate correctly with nodes running the previous version.
+ Also, any pending enqueued messages in the Artemis message store will not be delivered correctly to their original target.
+ However, assuming a clean reset of the artemis data and that the nodes are consistent versions,
+ data persisted via the AMQP serializer will be forward compatible.
+
* Enterprise Corda only: Compatibility with SQL Server 2017 and SQL Azure databases.
* Enterprise Corda only: node configuration property ``database.schema`` and documented existing database properties.
diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst
index 25490f055d..fe034e92c6 100644
--- a/docs/source/corda-configuration-file.rst
+++ b/docs/source/corda-configuration-file.rst
@@ -43,7 +43,6 @@ Simple Notary configuration file.
notary : {
validating : false
}
- useHTTPS : false
devMode : true
compatibilityZoneURL : "https://cz.corda.net"
@@ -136,10 +135,6 @@ path to the node's base directory.
Only one of ``raft``, ``bftSMaRt`` or ``custom`` configuration values may be specified.
-:useHTTPS: If false the node's web server will be plain HTTP. If true the node will use the same certificate and private
- key from the ``/certificates/sslkeystore.jks`` file as the ArtemisMQ port for HTTPS. If HTTPS is enabled
- then unencrypted HTTP traffic to the node's **webAddress** port is not supported.
-
: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:
diff --git a/docs/source/deploying-a-node.rst b/docs/source/deploying-a-node.rst
index 4f94c02c95..6b17b9dd5d 100644
--- a/docs/source/deploying-a-node.rst
+++ b/docs/source/deploying-a-node.rst
@@ -46,7 +46,6 @@ handling, and ensures the Corda service is run at boot.
myLegalName : "O=Bank of Breakfast Tea, L=London, C=GB"
keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass"
- useHTTPS : false
devMode : false
rpcUsers=[
{
@@ -217,7 +216,6 @@ at boot, and means the Corda service stays running with no users connected to th
keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass"
extraAdvertisedServiceIds: [ "" ]
- useHTTPS : false
devMode : false
rpcUsers=[
{
diff --git a/docs/source/example-code/src/main/resources/example-network-map-node.conf b/docs/source/example-code/src/main/resources/example-network-map-node.conf
index e75807bbc7..980f5f7031 100644
--- a/docs/source/example-code/src/main/resources/example-network-map-node.conf
+++ b/docs/source/example-code/src/main/resources/example-network-map-node.conf
@@ -4,4 +4,3 @@ trustStorePassword : "trustpass"
p2pAddress : "my-network-map:10000"
webAddress : "localhost:10001"
sshdAddress : "localhost:10002"
-useHTTPS : false
diff --git a/docs/source/example-code/src/main/resources/example-node.conf b/docs/source/example-code/src/main/resources/example-node.conf
index c01a10fd78..3715ff22e8 100644
--- a/docs/source/example-code/src/main/resources/example-node.conf
+++ b/docs/source/example-code/src/main/resources/example-node.conf
@@ -10,7 +10,6 @@ dataSourceProperties : {
p2pAddress : "my-corda-node:10002"
rpcAddress : "my-corda-node:10003"
webAddress : "localhost:10004"
-useHTTPS : false
rpcUsers : [
{ username=user1, password=letmein, permissions=[ StartFlow.net.corda.protocols.CashProtocol ] }
]
diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst
index 4432ddb2a2..1fa01d7a59 100644
--- a/docs/source/glossary.rst
+++ b/docs/source/glossary.rst
@@ -1,6 +1,8 @@
Glossary
========
+AMQP
+ The serialisation mechanism used within Corda for everything except flow checkpoints and RPC.
Artemis
The message queuing middleware used within Corda
Attachment
@@ -30,7 +32,7 @@ Gradle
Kotlin
The language used to code Corda. Fully compatible with any JVM language, including (obviously) Java.
Kryo
- The serialisation mechanism used within Corda - which is subject to change in a future release.
+ The serialisation mechanism used within Corda for flow checkpoints and RPC.
Input
In Corda terms, an input state is one that is used and consumed within a transaction. Once consumed, it cannot be re-used.
JVM
diff --git a/docs/source/messaging.rst b/docs/source/messaging.rst
index 55f48418d3..6dd5b33cc2 100644
--- a/docs/source/messaging.rst
+++ b/docs/source/messaging.rst
@@ -46,7 +46,7 @@ Message queues
The node makes use of various queues for its operation. The more important ones are described below. Others are used
for maintenance and other minor purposes.
-:``p2p.inbound``:
+:``p2p.inbound.$identity``:
The node listens for messages sent from other peer nodes on this queue. Only clients who are authenticated to be
nodes on the same network are given permission to send. Messages which are routed internally are also sent to this
queue (e.g. two flows on the same node communicating with each other).
@@ -54,7 +54,7 @@ for maintenance and other minor purposes.
:``internal.peers.$identity``:
These are a set of private queues only available to the node which it uses to route messages destined to other peers.
The queue name ends in the base 58 encoding of the peer's identity key. There is at most one queue per peer. The broker
- creates a bridge from this queue to the peer's ``p2p.inbound`` queue, using the network map service to lookup the
+ creates a bridge from this queue to the peer's ``p2p.inbound.$identity`` queue, using the network map service to lookup the
peer's network address.
:``internal.services.$identity``:
@@ -86,7 +86,7 @@ Clients attempting to connect to the node's broker fall in one of four groups:
#. Anyone connecting with the username ``SystemUsers/Peer`` is treated as a peer on the same Corda network as the node. Their
TLS root CA must be the same as the node's root CA - the root CA is the doorman of the network and having the same root CA
implies we've been let in by the same doorman. If they are part of the same network then they are only given permission
- to send to our ``p2p.inbound`` queue, otherwise they are rejected.
+ to send to our ``p2p.inbound.$identity`` queue, otherwise they are rejected.
#. Every other username is treated as a RPC user and authenticated against the node's list of valid RPC users. If that
is successful then they are only given sufficient permission to perform RPC, otherwise they are rejected.
diff --git a/docs/source/serialization.rst b/docs/source/serialization.rst
index 4e07a37048..96c74e6b2f 100644
--- a/docs/source/serialization.rst
+++ b/docs/source/serialization.rst
@@ -37,7 +37,7 @@ It's reproduced here as an example of both ways you can do this for a couple of
:end-before: END 7
.. note:: Several of the core interfaces at the heart of Corda are already annotated and so any classes that implement
- them will automatically be whitelisted. This includes `Contract`, `ContractState` and `CommandData`.
+ them will automatically be whitelisted. This includes ``Contract``, ``ContractState`` and ``CommandData``.
.. warning:: Java 8 Lambda expressions are not serializable except in flow checkpoints, and then not by default. The syntax to declare a serializable Lambda
expression that will work with Corda is ``Runnable r = (Runnable & Serializable) () -> System.out.println("Hello World");``, or
@@ -52,62 +52,62 @@ AMQP
====
Originally Corda used a ``Kryo``-based serialization scheme throughout for all serialization contexts. However, it was realised there
-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
+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 along-side the actual data:
+ #. 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 long ago archived data (e.g. trades from
- a decade ago, long after the code has changed) and between differing code versions.
- #. 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.
+ #. 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 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.
+ 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 do not need to be defined in the schema *upfront*, which is key to many Corda concepts, such as contract states.
- #. Increased security from deserialized objects being constructed through supported constructors rather than having
- data poked directly into their fields without an opportunity to validate consistency or intercept attempts to manipulate
- supposed invariants.
+ 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
-both the client and server side. However, it is planned that this will move to the AMQP framework when ready.
+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:
- #. The peer to peer context, representing inter-node communication.
- #. The persistence layer, representing contract states persisted into the vault.
+ #. 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.
+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
-attempting to force a one size fits all approach. Where ``Kryo`` is more suited to the serialization of a programs stack frames, being more flexible
-than our AMQP framework in what it can construct and serialize, that flexibility makes it exceptionally difficult to make secure. Conversely
-our AMQP framework allows us to concentrate on a robust a secure framework that can be reasoned about thus made safer with far fewer unforeseen
+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,
+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.
.. note:: Selection of serialization context should, for the most part, be opaque to CorDapp developers, the Corda framework selecting
- the correct context as confugred.
+ the correct context as configured.
-.. For information on our choice of AMQP 1.0, see :doc:`amqp-choice`. For detail on how we utilise AMQP 1.0 and represent
+.. 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`.
-We describe here what is and will be supported in the Corda AMQP format from the perspective
-of CorDapp developers, to allow for CorDapps to take into consideration the future state. The AMQP serialization format will of
-course continue to apply the whitelisting functionality that is already in place and described in :doc:`serialization`.
+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
+continue to apply the whitelisting functionality that is already in place and described in :doc:`serialization`.
Core Types
----------
-Here we describe the classes and interfaces that the AMQP serialization format will support.
+This section describes the classes and interfaces that the AMQP serialization format supports.
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.
-e.g. If you, for example, use a Guava implementation of a collection it will deserialize as a different implementation,
-but will continue to adhere to the most specific of any of the following interfaces. You should use only these types
-as the declared types of fields and properties, and not the concrete implementation types. Collections must be used
-in their generic form, the generic type parameters will be included in the schema, and the elements type checked against the
-generic parameters when deserialized.
+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.
+Guava implementations). Collections must specify their generic type, the generic type parameters will be included in
+the schema, and the element's type will be checked against the generic parameters when deserialized.
::
@@ -121,8 +121,8 @@ generic parameters when deserialized.
java.util.SortedMap
java.util.NavigableMap
-However, we will support the concrete implementation types below explicitly and also as the declared type of a field, as
-a convenience.
+However, as a convenience, we explicitly support the concrete implementation types below, and they can be used as the
+declared types of properties.
::
@@ -151,12 +151,12 @@ All the primitive types are supported.
Arrays
``````
-We also support arrays of any supported type, primitive or otherwise.
+Arrays of any type are supported, primitive or otherwise.
JDK Types
`````````
-The following types are supported from the JDK libraries.
+The following JDK library types are supported:
::
@@ -200,10 +200,10 @@ The following types are supported from the JDK libraries.
java.util.Currency
java.util.UUID
-Third Party Types
+Third-Party Types
`````````````````
-The following 3rd party types are supported.
+The following 3rd-party types are supported:
::
@@ -215,11 +215,11 @@ The following 3rd party types are supported.
Corda Types
```````````
-Classes and interfaces in the Corda codebase annotated with ``@CordaSerializable`` are of course supported.
+Any classes and interfaces in the Corda codebase annotated with ``@CordaSerializable`` are supported.
-All Corda exceptions that are expected to be serialized inherit from ``CordaThrowable`` via either ``CordaException``, for
-checked exceptions, or ``CordaRuntimeException``, for unchecked exceptions. Any ``Throwable`` that is serialized but does
-not conform to ``CordaThrowable`` will be converted to a ``CordaRuntimeException`` with the original exception type
+All Corda exceptions that are expected to be serialized inherit from ``CordaThrowable`` via either ``CordaException`` (for
+checked exceptions) or ``CordaRuntimeException`` (for unchecked exceptions). Any ``Throwable`` that is serialized but does
+not conform to ``CordaThrowable`` will be converted to a ``CordaRuntimeException``, with the original exception type
and other properties retained within it.
.. _amqp_custom_types_ref:
@@ -227,7 +227,7 @@ and other properties retained within it.
Custom Types
------------
-Here are the rules to adhere to for support of your own types:
+You own types must adhere to the following rules to be supported:
Classes
```````
@@ -236,34 +236,40 @@ General Rules
'''''''''''''
#. The class must be compiled with parameter names included in the ``.class`` file. This is the default in Kotlin
- but must be turned on in Java (``-parameters`` command line option to ``javac``).
- #. The class is annotated with ``@CordaSerializable``.
- #. The declared types of constructor arguments, getters, and setters must be supported, and where generics are used the
+ but must be turned on in Java using the ``-parameters`` command line option to ``javac``
+
+ .. 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
+ :doc:`cordapp-custom-serializers` page.
+
+ #. 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
generic parameter must be a supported type, an open wildcard (``*``), or a bounded wildcard which is currently
- widened to an open wildcard.
- #. Any superclass must adhere to the same rules, but can be abstract.
- #. Object graph cycles are not supported, so an object cannot refer to itself, directly or indirectly.
+ widened to an open wildcard
+ #. Any superclass must adhere to the same rules, but can be abstract
+ #. Object graph cycles are not supported, so an object cannot refer to itself, directly or indirectly
Constructor Instantiation
'''''''''''''''''''''''''
-The primary way the AMQP serialization framework for Corda instantiates objects is via a defined constructor. This is
-used to first determine which properties of an object are to be serialised then, on deserialization, it is used to
+The primary way Corda's AMQP serialization framework instantiates objects is via a specified constructor. This is
+used to first determine which properties of an object are to be serialised, then, on deserialization, it is used to
instantiate the object with the serialized values.
-This is the recommended design idiom for serializable objects in Corda as it allows for immutable state objects to
-be created
+It is recommended that serializable objects in Corda adhere to the following rules, as they allow immutable state
+objects to be deserialised:
- #. A Java Bean getter for each of the properties in the constructor, with the names matching up. For example, for a constructor
- parameter ``foo``, there must be a getter called ``getFoo()``. If the type of ``foo`` is boolean, the getter may
- optionally be called ``isFoo()``. This is why the class must be compiled with parameter names turned on.
+ #. A Java Bean getter for each of the properties in the constructor, with a name of the form ``getX``. For example, for a constructor
+ parameter ``foo``, there must be a getter called ``getFoo()``. If ``foo`` is a boolean, the getter may
+ optionally be called ``isFoo()`` (this is why the class must be compiled with parameter names turned on)
#. A constructor which takes all of the properties that you wish to record in the serialized form. This is required in
- order for the serialization framework to reconstruct an instance of your class.
+ order for the serialization framework to reconstruct an instance of your class
#. If more than one constructor is provided, the serialization framework needs to know which one to use. The ``@ConstructorForDeserialization``
annotation can be used to indicate which one. For a Kotlin class, without the ``@ConstructorForDeserialization`` annotation, the
- *primary constructor* will be selected.
+ *primary constructor* will be selected
-In Kotlin, this maps cleanly to a data class where there getters are synthesized automatically. For example,
+In Kotlin, this maps cleanly to a data class where there getters are synthesized automatically. For example, suppose we
+have the following data class:
.. container:: codeset
@@ -271,9 +277,10 @@ In Kotlin, this maps cleanly to a data class where there getters are synthesized
data class Example (val a: Int, val b: String)
-Both properties a and b will be included in the serialised form. However, as stated above, properties not mentioned in
-the constructor will not be serialised. For example, in the following code property c will not be considered part of the
-serialised form
+Properties ``a`` and ``b`` will be included in the serialised form.
+
+However, properties not mentioned in the constructor will not be serialised. For example, in the following code,
+property ``c`` will not be considered part of the serialised form:
.. container:: codeset
@@ -291,14 +298,14 @@ serialised form
Setter Instantiation
''''''''''''''''''''
-As an alternative to constructor based initialisation Corda can also determine the important elements of an
-object by inspecting the getter and setter methods present on a class. If a class has **only** a default
+As an alternative to constructor-based initialisation, Corda can also determine the important elements of an
+object by inspecting the getter and setter methods present on the class. If a class has **only** a default
constructor **and** properties then the serializable properties will be determined by the presence of
-both a getter and setter for that property that are both publicly visible. I.e. the class adheres to
-the classic *idiom* of mutable JavaBeans.
+both a getter and setter for that property that are both publicly visible (i.e. the class adheres to
+the classic *idiom* of mutable JavaBeans).
-On deserialization, a default instance will first be created and then, in turn, the setters invoked
-on that object to populate the correct values.
+On deserialization, a default instance will first be created, and then the setters will be invoked on that object to
+populate it with the correct values.
For example:
@@ -324,7 +331,7 @@ Inaccessible Private Properties
```````````````````````````````
Whilst the Corda AMQP serialization framework supports private object properties without publicly
-accessible getter methods this development idiom is strongly discouraged.
+accessible getter methods, this development idiom is strongly discouraged.
For example.
@@ -350,15 +357,14 @@ For example.
}
}
-When designing stateful objects is should be remembered that they are not, despite appearances, traditional
+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 indiciate erroneously that properties can be given something other than public
- visibility. Ignore this as whilst it will work, as discussed above there are many reasons why this isn't
- a good idea and those are beyond the scope of the IDEs inference rules
+.. warning:: IDEs will indicate erroneously that properties can be given something other than public visibility. Ignore
+ this, as whilst it will work, as discussed above there are many reasons why this isn't a good idea.
-Providing a public getter, as per the following example, is acceptable
+Providing a public getter, as per the following example, is acceptable:
.. container:: codeset
@@ -392,7 +398,7 @@ Providing a public getter, as per the following example, is acceptable
Enums
`````
- #. All enums are supported, provided they are annotated with ``@CordaSerializable``.
+ #. All enums are supported, provided they are annotated with ``@CordaSerializable``
Exceptions
@@ -401,24 +407,25 @@ Exceptions
The following rules apply to supported ``Throwable`` implementations.
#. If you wish for your exception to be serializable and transported type safely it should inherit from either
- ``CordaException`` or ``CordaRuntimeException``.
+ ``CordaException`` or ``CordaRuntimeException``
#. If not, the ``Throwable`` will deserialize to a ``CordaRuntimeException`` with the details of the original
- ``Throwable`` contained within it, including the class name of the original ``Throwable``.
+ ``Throwable`` contained within it, including the class name of the original ``Throwable``
Kotlin Objects
``````````````
- #. Kotlin ``object`` s are singletons and 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,
- and they will deserialize to new instances of the class.
- #. Kotlin's anonymous ``object`` s are not currently supported. I.e. constructs like:
- ``object : Contract {...}`` will not serialize correctly and need to be re-written as an explicit class declaration.
+ #. 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
+ singleton instance. Currently, the same is not true of Java singletons, which will deserialize to new instances
+ of the class
+ #. 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
`````````````
-We will support a class carpenter that can dynamically manufacture classes from the supplied schema when deserializing
-in the JVM without the supporting classes on the classpath. This can be useful where other components might expect to
+We support a class carpenter that can dynamically manufacture classes from the supplied schema when deserializing,
+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.
@@ -427,25 +434,25 @@ Future Enhancements
```````````````````
#. 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
#. Instance internalizing support. We will add support for identifying classes that should be resolved against an instances map to avoid
- creating many duplicate instances that are equal. Similar to ``String.intern()``.
+ creating many duplicate instances that are equal (similar to ``String.intern()``)
.. Type Evolution:
Type Evolution
--------------
-Type evolution is the mechanisms by which classes can be altered over time yet still remain serializable and deserializable across
+Type evolution is the mechanism by which classes can be altered over time yet still remain serializable and deserializable across
all versions of the class. This ensures an object serialized with an older idea of what the class "looked like" can be deserialized
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``
+backward (or forward) compatibility. The rules and mechanisms for doing this are discussed in :doc:`serialization-enum-evolution``.
diff --git a/docs/source/upgrading-cordapps.rst b/docs/source/upgrading-cordapps.rst
index f7f8a6b038..893884bb78 100644
--- a/docs/source/upgrading-cordapps.rst
+++ b/docs/source/upgrading-cordapps.rst
@@ -1,3 +1,9 @@
+.. highlight:: kotlin
+.. raw:: html
+
+
+
+
Upgrading a CorDapp (outside of platform version upgrades)
==========================================================
@@ -127,17 +133,39 @@ The ``InitiatingFlow`` version number is included in the flow session handshake
the flow running on the other side. In particular, it has a ``flowVersion`` property which can be used to
programmatically evolve flows across versions. For example:
-.. sourcecode:: kotlin
+.. container:: codeset
- @Suspendable
- override fun call() {
- val otherFlowVersion = otherSession.getCounterpartyFlowInfo().flowVersion
- val receivedString = if (otherFlowVersion == 1) {
- receive(otherParty).unwrap { it.toString() }
- } else {
- receive(otherParty).unwrap { it }
+ .. sourcecode:: kotlin
+
+ @Suspendable
+ override fun call() {
+ val otherFlowVersion = otherSession.getCounterpartyFlowInfo().flowVersion
+ val receivedString = if (otherFlowVersion == 1) {
+ otherSession.receive().unwrap { it.toString() }
+ } else {
+ otherSession.receive().unwrap { it }
+ }
+ }
+
+ .. sourcecode:: java
+
+ @Suspendable
+ @Override public Void call() throws FlowException {
+ int otherFlowVersion = otherSession.getCounterpartyFlowInfo().getFlowVersion();
+ String receivedString;
+
+ if (otherFlowVersion == 1) {
+ receivedString = otherSession.receive(Integer.class).unwrap(integer -> {
+ return integer.toString();
+ });
+ } else {
+ receivedString = otherSession.receive(String.class).unwrap(string -> {
+ return string;
+ });
+ }
+
+ return null;
}
- }
This code shows a flow that in its first version expected to receive an Int, but in subsequent versions was modified to
expect a String. This flow is still able to communicate with parties that are running the older CorDapp containing
@@ -147,30 +175,73 @@ How do I deal with interface changes to inlined subflows?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Here is an example of an in-lined subflow:
-.. sourcecode:: kotlin
+.. container:: codeset
- @StartableByRPC
- @InitiatingFlow
- class FlowA(val recipient: Party) : FlowLogic() {
- @Suspendable
- override fun call() {
- subFlow(FlowB(recipient))
+ .. sourcecode:: kotlin
+
+ @StartableByRPC
+ @InitiatingFlow
+ class FlowA(val recipient: Party) : FlowLogic() {
+ @Suspendable
+ override fun call() {
+ subFlow(FlowB(recipient))
+ }
}
- }
- @InitiatedBy(FlowA::class)
- class FlowC(val otherSession: FlowSession) : FlowLogic() {
- // Omitted.
- }
-
- // Note: No annotations. This is used as an inlined subflow.
- class FlowB(val recipient: Party) : FlowLogic() {
- @Suspendable
- override fun call() {
- val message = "I'm an inlined subflow, so I inherit the @InitiatingFlow's session ID and type."
- initiateFlow(recipient).send(message)
+ @InitiatedBy(FlowA::class)
+ class FlowC(val otherSession: FlowSession) : FlowLogic() {
+ // Omitted.
+ }
+
+ // Note: No annotations. This is used as an inlined subflow.
+ class FlowB(val recipient: Party) : FlowLogic() {
+ @Suspendable
+ override fun call() {
+ val message = "I'm an inlined subflow, so I inherit the @InitiatingFlow's session ID and type."
+ initiateFlow(recipient).send(message)
+ }
+ }
+
+ .. sourcecode:: java
+
+ @StartableByRPC
+ @InitiatingFlow
+ class FlowA extends FlowLogic {
+ private final Party recipient;
+
+ public FlowA(Party recipient) {
+ this.recipient = recipient;
+ }
+
+ @Suspendable
+ @Override public Void call() throws FlowException {
+ subFlow(new FlowB(recipient));
+
+ return null;
+ }
+ }
+
+ @InitiatedBy(FlowA.class)
+ class FlowC extends FlowLogic {
+ // Omitted.
+ }
+
+ // Note: No annotations. This is used as an inlined subflow.
+ class FlowB extends FlowLogic {
+ private final Party recipient;
+
+ public FlowB(Party recipient) {
+ this.recipient = recipient;
+ }
+
+ @Suspendable
+ @Override public Void call() {
+ String message = "I'm an inlined subflow, so I inherit the @InitiatingFlow's session ID and type.";
+ initiateFlow(recipient).send(message);
+
+ return null;
+ }
}
- }
Inlined subflows are treated as being the flow that invoked them when initiating a new flow session with a counterparty.
Suppose flow ``A`` calls inlined subflow B, which, in turn, initiates a session with a counterparty. The ``FlowLogic``
@@ -361,34 +432,138 @@ for details.
For backwards compatible changes such as adding columns, the procedure for upgrading a state schema is to extend the
existing object relational mapper. For example, we can update:
-.. sourcecode:: kotlin
+.. container:: codeset
- object ObligationSchemaV1 : MappedSchema(Obligation::class.java, 1, listOf(ObligationEntity::class.java)) {
- @Entity @Table(name = "obligations")
- class ObligationEntity(obligation: Obligation) : PersistentState() {
- @Column var currency: String = obligation.amount.token.toString()
- @Column var amount: Long = obligation.amount.quantity
- @Column @Lob var lender: ByteArray = obligation.lender.owningKey.encoded
- @Column @Lob var borrower: ByteArray = obligation.borrower.owningKey.encoded
- @Column var linear_id: String = obligation.linearId.id.toString()
+ .. sourcecode:: kotlin
+
+ object ObligationSchemaV1 : MappedSchema(Obligation::class.java, 1, listOf(ObligationEntity::class.java)) {
+ @Entity @Table(name = "obligations")
+ class ObligationEntity(obligation: Obligation) : PersistentState() {
+ @Column var currency: String = obligation.amount.token.toString()
+ @Column var amount: Long = obligation.amount.quantity
+ @Column @Lob var lender: ByteArray = obligation.lender.owningKey.encoded
+ @Column @Lob var borrower: ByteArray = obligation.borrower.owningKey.encoded
+ @Column var linear_id: String = obligation.linearId.id.toString()
+ }
+ }
+
+ .. sourcecode:: java
+
+ public class ObligationSchemaV1 extends MappedSchema {
+ public IOUSchemaV1() {
+ super(Obligation.class, 1, ImmutableList.of(ObligationEntity.class));
+ }
+
+ @Entity
+ @Table(name = "obligations")
+ public static class ObligationEntity extends PersistentState {
+ @Column(name = "currency") private final String currency;
+ @Column(name = "amount") private final Long amount;
+ @Column(name = "lender") @Lob private final Byte[] lender;
+ @Column(name = "borrower") @Lob private final Byte[] borrower;
+ @Column(name = "linear_id") private final UUID linearId;
+
+
+ public ObligationEntity(String currency, Long amount, Byte[] lender, Byte[] borrower, UUID linearId) {
+ this.currency = currency;
+ this.amount = amount;
+ this.lender = lender;
+ this.borrower = borrower;
+ this.linearId = linearId;
+ }
+
+ public String getCurrency() {
+ return currency;
+ }
+
+ public Long getAmount() {
+ return amount;
+ }
+
+ public ByteArray getLender() {
+ return lender;
+ }
+
+ public ByteArray getBorrower() {
+ return borrower;
+ }
+
+ public UUID getId() {
+ return linearId;
+ }
+ }
}
- }
To:
-.. sourcecode:: kotlin
+.. container:: codeset
- object ObligationSchemaV1 : MappedSchema(Obligation::class.java, 1, listOf(ObligationEntity::class.java)) {
- @Entity @Table(name = "obligations")
- class ObligationEntity(obligation: Obligation) : PersistentState() {
- @Column var currency: String = obligation.amount.token.toString()
- @Column var amount: Long = obligation.amount.quantity
- @Column @Lob var lender: ByteArray = obligation.lender.owningKey.encoded
- @Column @Lob var borrower: ByteArray = obligation.borrower.owningKey.encoded
- @Column var linear_id: String = obligation.linearId.id.toString()
- @Column var defaulted: Bool = obligation.amount.inDefault // NEW COLUNM!
+ .. sourcecode:: kotlin
+
+ object ObligationSchemaV1 : MappedSchema(Obligation::class.java, 1, listOf(ObligationEntity::class.java)) {
+ @Entity @Table(name = "obligations")
+ class ObligationEntity(obligation: Obligation) : PersistentState() {
+ @Column var currency: String = obligation.amount.token.toString()
+ @Column var amount: Long = obligation.amount.quantity
+ @Column @Lob var lender: ByteArray = obligation.lender.owningKey.encoded
+ @Column @Lob var borrower: ByteArray = obligation.borrower.owningKey.encoded
+ @Column var linear_id: String = obligation.linearId.id.toString()
+ @Column var defaulted: Bool = obligation.amount.inDefault // NEW COLUMN!
+ }
+ }
+
+ .. sourcecode:: java
+
+ public class ObligationSchemaV1 extends MappedSchema {
+ public IOUSchemaV1() {
+ super(Obligation.class, 1, ImmutableList.of(ObligationEntity.class));
+ }
+
+ @Entity
+ @Table(name = "obligations")
+ public static class ObligationEntity extends PersistentState {
+ @Column(name = "currency") private final String currency;
+ @Column(name = "amount") private final Long amount;
+ @Column(name = "lender") @Lob private final Byte[] lender;
+ @Column(name = "borrower") @Lob private final Byte[] borrower;
+ @Column(name = "linear_id") private final UUID linearId;
+ @Column(name = "defaulted") private final Boolean defaulted; // NEW COLUMN!
+
+
+ public ObligationEntity(String currency, Long amount, Byte[] lender, Byte[] borrower, UUID linearId, Boolean defaulted) {
+ this.currency = currency;
+ this.amount = amount;
+ this.lender = lender;
+ this.borrower = borrower;
+ this.linearId = linearId;
+ this.defaulted = defaulted;
+ }
+
+ public String getCurrency() {
+ return currency;
+ }
+
+ public Long getAmount() {
+ return amount;
+ }
+
+ public ByteArray getLender() {
+ return lender;
+ }
+
+ public ByteArray getBorrower() {
+ return borrower;
+ }
+
+ public UUID getId() {
+ return linearId;
+ }
+
+ public Boolean isDefaulted() {
+ return defaulted;
+ }
+ }
}
- }
Thus adding a new column with a default value.
@@ -397,21 +572,43 @@ used, as changes to the state are required. To make a backwards-incompatible cha
because a property was removed from a state object), the procedure is to define another object relational mapper, then
add it to the ``supportedSchemas`` property of your ``QueryableState``, like so:
-.. sourcecode:: kotlin
+.. container:: codeset
- override fun supportedSchemas(): Iterable = listOf(ExampleSchemaV1, ExampleSchemaV2)
+ .. sourcecode:: kotlin
+
+ override fun supportedSchemas(): Iterable = listOf(ExampleSchemaV1, ExampleSchemaV2)
+
+ .. sourcecode:: java
+
+ @Override public Iterable supportedSchemas() {
+ return ImmutableList.of(new ExampleSchemaV1(), new ExampleSchemaV2());
+ }
Then, in ``generateMappedObject``, add support for the new schema:
-.. sourcecode:: kotlin
+.. container:: codeset
- override fun generateMappedObject(schema: MappedSchema): PersistentState {
- return when (schema) {
- is DummyLinearStateSchemaV1 -> // Omitted.
- is DummyLinearStateSchemaV2 -> // Omitted.
- else -> throw IllegalArgumentException("Unrecognised schema $schema")
+ .. sourcecode:: kotlin
+
+ override fun generateMappedObject(schema: MappedSchema): PersistentState {
+ return when (schema) {
+ is DummyLinearStateSchemaV1 -> // Omitted.
+ is DummyLinearStateSchemaV2 -> // Omitted.
+ else -> throw IllegalArgumentException("Unrecognised schema $schema")
+ }
+ }
+
+ .. sourcecode:: java
+
+ @Override public PersistentState generateMappedObject(MappedSchema schema) {
+ if (schema instanceof DummyLinearStateSchemaV1) {
+ // Omitted.
+ } else if (schema instanceof DummyLinearStateSchemaV2) {
+ // Omitted.
+ } else {
+ throw new IllegalArgumentException("Unrecognised schema $schema");
+ }
}
- }
With this approach, whenever the state object is stored in the vault, a representation of it will be stored in two
separate database tables where possible - one for each supported schema.
\ No newline at end of file
diff --git a/experimental/flow-hook/src/main/kotlin/net/corda/flowhook/FlowHookContainer.kt b/experimental/flow-hook/src/main/kotlin/net/corda/flowhook/FlowHookContainer.kt
index 686e3d46a4..abb55cca2c 100644
--- a/experimental/flow-hook/src/main/kotlin/net/corda/flowhook/FlowHookContainer.kt
+++ b/experimental/flow-hook/src/main/kotlin/net/corda/flowhook/FlowHookContainer.kt
@@ -2,7 +2,7 @@ package net.corda.flowhook
import co.paralleluniverse.fibers.Fiber
import net.corda.node.services.statemachine.Event
-import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager
+import net.corda.nodeapi.internal.persistence.contextTransactionOrNull
import java.sql.Connection
@Suppress("UNUSED")
@@ -156,7 +156,7 @@ object FlowHookContainer {
private fun currentTransactionOrThread(): Any {
return try {
- DatabaseTransactionManager.currentOrNull()
+ contextTransactionOrNull
} catch (exception: IllegalStateException) {
null
} ?: Thread.currentThread()
diff --git a/network-management/README.md b/network-management/README.md
index 356c6c5348..11a60535d3 100644
--- a/network-management/README.md
+++ b/network-management/README.md
@@ -65,13 +65,15 @@ The doorman service can use JIRA to manage the certificate signing request appro
projectCode = "TD"
username = "username"
password = "password"
- doneTransitionCode = 41
}
.
.
.
}
```
+#### JIRA project configuration
+* The JIRA project should setup as "Business Project" with "Task" workflow.
+* Custom text field input "Request ID", and "Reject Reason" should be created in JIRA, doorman will exit with error without these custom fields.
### Auto approval
When `approveAll` is set to `true`, the doorman will approve all requests on receive. (*This should only be enabled in a test environment)
@@ -118,7 +120,6 @@ doormanConfig {
projectCode = "TD"
username = "username"
password = "password"
- doneTransitionCode = 41
}
}
diff --git a/network-management/build.gradle b/network-management/build.gradle
index 40abcf0792..c022c203cc 100644
--- a/network-management/build.gradle
+++ b/network-management/build.gradle
@@ -88,7 +88,7 @@ dependencies {
testCompile "com.nhaarman:mockito-kotlin:0.6.1"
testCompile "com.spotify:docker-client:8.9.1"
- compile('com.atlassian.jira:jira-rest-java-client-core:4.0.0') {
+ compile('com.atlassian.jira:jira-rest-java-client-core:5.0.4') {
// The jira client includes jersey-core 1.5 which breaks everything.
exclude module: 'jersey-core'
}
diff --git a/network-management/doorman.conf b/network-management/doorman.conf
index e3a10650f8..ed58ea2d6f 100644
--- a/network-management/doorman.conf
+++ b/network-management/doorman.conf
@@ -23,7 +23,6 @@ doormanConfig{
projectCode = "TD"
username = "username"
password = "password"
- doneTransitionCode = 41
}
}
diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NodeRegistrationTest.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NodeRegistrationTest.kt
index 4ece2a3a21..6fa65c1787 100644
--- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NodeRegistrationTest.kt
+++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NodeRegistrationTest.kt
@@ -1,6 +1,7 @@
package com.r3.corda.networkmanage.doorman
import com.r3.corda.networkmanage.common.persistence.configureDatabase
+import com.r3.corda.networkmanage.common.utils.CertPathAndKey
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
import net.corda.cordform.CordformNode
import net.corda.core.crypto.random63BitValue
@@ -141,11 +142,11 @@ class NodeRegistrationTest : IntegrationTest() {
start(
serverAddress,
configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true)),
- LocalSigner(csrCa.keyPair, arrayOf(csrCa.certificate, rootCaCert)),
+ CertPathAndKey(listOf(csrCa.certificate, rootCaCert), csrCa.keyPair.private),
DoormanConfig(approveAll = true, jiraConfig = null, approveInterval = timeoutMillis),
networkParameters?.let {
NetworkMapStartParams(
- LocalSigner(networkMapCa.keyPair, arrayOf(networkMapCa.certificate, rootCaCert)),
+ LocalSigner(networkMapCa),
networkParameters,
NetworkMapConfig(cacheTimeout = timeoutMillis, signInterval = timeoutMillis)
)
diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/HsmTest.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/HsmTest.kt
index 33c9cbef3a..c78f1ca852 100644
--- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/HsmTest.kt
+++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/HsmTest.kt
@@ -12,6 +12,7 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
+import java.util.concurrent.atomic.AtomicBoolean
import kotlin.test.assertTrue
class HsmTest {
@@ -19,7 +20,12 @@ class HsmTest {
@Rule
@JvmField
val hsmSimulator: HsmSimulator = HsmSimulator()
- val testParameters = Parameters(
+
+ @Rule
+ @JvmField
+ val tempFolder = TemporaryFolder()
+
+ private val testParameters = Parameters(
dataSourceProperties = mock(),
device = "${hsmSimulator.port}@${hsmSimulator.host}",
keySpecifier = 1,
@@ -30,10 +36,6 @@ class HsmTest {
validDays = 3650
)
- @Rule
- @JvmField
- val tempFolder = TemporaryFolder()
-
private lateinit var inputReader: InputReader
@Before
@@ -47,14 +49,12 @@ class HsmTest {
fun `Authenticator executes the block once user is successfully authenticated`() {
// given
val authenticator = Authenticator(testParameters.createProvider(), inputReader = inputReader)
- var executed = false
+ val executed = AtomicBoolean(false)
// when
- authenticator.connectAndAuthenticate({ provider, signers ->
- executed = true
- })
+ authenticator.connectAndAuthenticate { _, _ -> executed.set(true) }
// then
- assertTrue(executed)
+ assertTrue(executed.get())
}
}
\ No newline at end of file
diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt
index 765d47d5f0..8c4a3b8215 100644
--- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt
+++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt
@@ -90,7 +90,12 @@ class SigningServiceIntegrationTest {
val database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true))
NetworkManagementServer().use { server ->
- server.start(NetworkHostAndPort(HOST, 0), database, doormanServiceParameter = DoormanConfig(approveAll = true, approveInterval = 2.seconds.toMillis(), jiraConfig = null), startNetworkMap = null)
+ server.start(
+ hostAndPort = NetworkHostAndPort(HOST, 0),
+ database = database,
+ csrCertPathAndKey = null,
+ doormanServiceParameter = DoormanConfig(approveAll = true, approveInterval = 2.seconds.toMillis(), jiraConfig = null),
+ startNetworkMap = null)
val doormanHostAndPort = server.hostAndPort
// Start Corda network registration.
val config = createConfig().also {
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificationRequestStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificationRequestStorage.kt
index b1115ff1df..f0f8280406 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificationRequestStorage.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificationRequestStorage.kt
@@ -1,12 +1,14 @@
package com.r3.corda.networkmanage.common.persistence
+import net.corda.core.crypto.SecureHash
import org.bouncycastle.pkcs.PKCS10CertificationRequest
import java.security.cert.CertPath
-data class CertificateData(val publicKeyHash: String, val certStatus: CertificateStatus, val certPath: CertPath)
+data class CertificateData(val certStatus: CertificateStatus, val certPath: CertPath)
data class CertificateSigningRequest(val requestId: String,
val legalName: String,
+ val publicKeyHash: SecureHash,
val status: RequestStatus,
val request: PKCS10CertificationRequest,
val remark: String?,
@@ -59,7 +61,7 @@ interface CertificationRequestStorage {
* @param rejectedBy authority (its identifier) rejecting this request.
* @param rejectReason brief description of the rejection reason
*/
- fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String)
+ fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String?)
/**
* Store certificate path with [requestId], this will store the encoded [CertPath] and transit request status to [RequestStatus.SIGNED].
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NetworkMapStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NetworkMapStorage.kt
index cb86c9af6f..59c83b0abc 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NetworkMapStorage.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NetworkMapStorage.kt
@@ -39,10 +39,9 @@ interface NetworkMapStorage {
fun getSignedNetworkParameters(hash: SecureHash): SignedNetworkParameters?
/**
- * Retrieve network map parameters.
- * @return signed current network map parameters or null if they don't exist
+ * Retrieve the network parameters of the current network map, or null if there's no network map.
*/
- fun getCurrentSignedNetworkParameters(): SignedNetworkParameters?
+ fun getNetworkParametersOfNetworkMap(): SignedNetworkParameters?
/**
* Persists given network parameters with signature if provided.
@@ -55,5 +54,5 @@ interface NetworkMapStorage {
* Note that they may not have been signed up yet.
* @return latest network parameters
*/
- fun getLatestUnsignedNetworkParameters(): NetworkParameters
-}
\ No newline at end of file
+ fun getLatestNetworkParameters(): NetworkParameters?
+}
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorage.kt
index 339fcb7978..5a4b1011bf 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorage.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorage.kt
@@ -3,6 +3,7 @@ package com.r3.corda.networkmanage.common.persistence
import com.r3.corda.networkmanage.common.persistence.entity.CertificateDataEntity
import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity
import com.r3.corda.networkmanage.common.utils.hashString
+import net.corda.core.crypto.Crypto.toSupportedPublicKey
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name
import net.corda.nodeapi.internal.persistence.CordaPersistence
@@ -26,14 +27,12 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence
builder.and(requestIdEq, statusEq)
}
request ?: throw IllegalArgumentException("Cannot retrieve 'APPROVED' certificate signing request for request id: $requestId")
- val publicKeyHash = certificates.certificates.first().publicKey.hashString()
val certificateSigningRequest = request.copy(
modifiedBy = signedBy,
modifiedAt = Instant.now(),
status = RequestStatus.SIGNED)
session.merge(certificateSigningRequest)
val certificateDataEntity = CertificateDataEntity(
- publicKeyHash = publicKeyHash,
certificateStatus = CertificateStatus.VALID,
certificatePathBytes = certificates.encoded,
certificateSigningRequest = certificateSigningRequest)
@@ -48,6 +47,7 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence
session.save(CertificateSigningRequestEntity(
requestId = requestId,
legalName = legalName,
+ publicKeyHash = toSupportedPublicKey(request.subjectPublicKeyInfo).hashString(),
requestBytes = request.encoded,
remark = rejectReason,
modifiedBy = emptyList(),
@@ -93,7 +93,7 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence
}
}
- override fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String) {
+ override fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String?) {
database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
val request = findRequest(requestId)
request ?: throw IllegalArgumentException("Error when rejecting request with id: $requestId. Request does not exist.")
@@ -134,7 +134,7 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence
return Pair(request.subject.toString(), "Name validation failed: ${e.message}")
}
- val query = session.criteriaBuilder.run {
+ val duplicateNameQuery = session.criteriaBuilder.run {
val criteriaQuery = createQuery(CertificateSigningRequestEntity::class.java)
criteriaQuery.from(CertificateSigningRequestEntity::class.java).run {
criteriaQuery.where(equal(get(CertificateSigningRequestEntity::legalName.name), legalName))
@@ -144,10 +144,27 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence
// TODO consider scenario: There is a CSR that is signed but the certificate itself has expired or was revoked
// Also, at the moment we assume that once the CSR is approved it cannot be rejected.
// What if we approved something by mistake.
- val duplicates = session.createQuery(query).resultList.filter {
+ val nameDuplicates = session.createQuery(duplicateNameQuery).resultList.filter {
it.status != RequestStatus.REJECTED
}
- return Pair(legalName, if (duplicates.isEmpty()) null else "Duplicate legal name")
+ if (nameDuplicates.isNotEmpty()) {
+ return Pair(legalName, "Duplicate legal name")
+ }
+
+ val publicKey = toSupportedPublicKey(request.subjectPublicKeyInfo).hashString()
+ val duplicatePkQuery = session.criteriaBuilder.run {
+ val criteriaQuery = createQuery(CertificateSigningRequestEntity::class.java)
+ criteriaQuery.from(CertificateSigningRequestEntity::class.java).run {
+ criteriaQuery.where(equal(get(CertificateSigningRequestEntity::publicKeyHash.name), publicKey))
+ }
+ }
+
+ //TODO Consider following scenario: There is a CSR that is signed but the certificate itself has expired or was revoked
+ val pkDuplicates = session.createQuery(duplicatePkQuery).resultList.filter {
+ it.status != RequestStatus.REJECTED
+ }
+
+ return Pair(legalName, if (pkDuplicates.isEmpty()) null else "Duplicate public key")
}
}
\ No newline at end of file
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorage.kt
index 0e328f853a..de7fed484b 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorage.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorage.kt
@@ -6,10 +6,7 @@ import com.r3.corda.networkmanage.common.utils.SignedNetworkParameters
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
import net.corda.core.internal.DigitalSignatureWithCert
-import net.corda.core.serialization.SerializedBytes
-import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
-import net.corda.nodeapi.internal.network.NetworkMap
import net.corda.nodeapi.internal.network.NetworkParameters
import net.corda.nodeapi.internal.persistence.CordaPersistence
@@ -19,18 +16,16 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence
class PersistentNetworkMapStorage(private val database: CordaPersistence) : NetworkMapStorage {
override fun getCurrentNetworkMap(): SignedNetworkMap? {
return database.transaction {
- getCurrentNetworkMapEntity()?.let {
- val signatureAndCertPath = it.signatureAndCertificate()
- SignedNetworkMap(SerializedBytes(it.networkMap), signatureAndCertPath)
- }
+ getCurrentNetworkMapEntity()?.toSignedNetworkMap()
}
}
- override fun getCurrentSignedNetworkParameters(): SignedNetworkParameters? {
+ override fun getNetworkParametersOfNetworkMap(): SignedNetworkParameters? {
return database.transaction {
getCurrentNetworkMapEntity()?.let {
- val netParamsHash = it.networkMap.deserialize().networkParameterHash
- getSignedNetworkParameters(netParamsHash)
+ val netParamsHash = it.toNetworkMap().networkParameterHash
+ getSignedNetworkParameters(netParamsHash) ?:
+ throw IllegalStateException("Current network map is pointing to network parameters that do not exist: $netParamsHash")
}
}
}
@@ -47,7 +42,9 @@ class PersistentNetworkMapStorage(private val database: CordaPersistence) : Netw
}
override fun getSignedNetworkParameters(hash: SecureHash): SignedNetworkParameters? {
- return getNetworkParametersEntity(hash.toString())?.signedParameters()
+ return getNetworkParametersEntity(hash.toString())?.let {
+ if (it.isSigned) it.toSignedNetworkParameters() else null
+ }
}
override fun getNodeInfoHashes(certificateStatus: CertificateStatus): List {
@@ -79,18 +76,17 @@ class PersistentNetworkMapStorage(private val database: CordaPersistence) : Netw
}
}
- override fun getLatestUnsignedNetworkParameters(): NetworkParameters = getLatestNetworkParametersEntity().networkParameters()
-
- private fun getLatestNetworkParametersEntity(): NetworkParametersEntity {
+ override fun getLatestNetworkParameters(): NetworkParameters? {
return database.transaction {
- val builder = session.criteriaBuilder
- val query = builder.createQuery(NetworkParametersEntity::class.java).run {
- from(NetworkParametersEntity::class.java).run {
- orderBy(builder.desc(get(NetworkParametersEntity::created.name)))
+ val query = session.criteriaBuilder.run {
+ createQuery(NetworkParametersEntity::class.java).run {
+ from(NetworkParametersEntity::class.java).run {
+ orderBy(desc(get(NetworkParametersEntity::created.name)))
+ }
}
}
// We just want the last entry
- session.createQuery(query).setMaxResults(1).resultList.singleOrNull() ?: throw IllegalArgumentException("No network parameters found in network map storage")
+ session.createQuery(query).setMaxResults(1).uniqueResult()?.toNetworkParameters()
}
}
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorage.kt
index 5c8d249eac..4d97e79629 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorage.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorage.kt
@@ -1,6 +1,5 @@
package com.r3.corda.networkmanage.common.persistence
-import com.r3.corda.networkmanage.common.persistence.entity.CertificateDataEntity
import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity
import com.r3.corda.networkmanage.common.persistence.entity.NodeInfoEntity
import com.r3.corda.networkmanage.common.utils.buildCertPath
@@ -10,7 +9,7 @@ import net.corda.core.internal.CertRole
import net.corda.core.serialization.serialize
import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.persistence.CordaPersistence
-import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel
+import net.corda.nodeapi.internal.persistence.DatabaseTransaction
import java.security.cert.CertPath
/**
@@ -21,16 +20,13 @@ class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeIn
val nodeInfo = nodeInfoWithSigned.nodeInfo
val signedNodeInfo = nodeInfoWithSigned.signedNodeInfo
val nodeCaCert = nodeInfo.legalIdentitiesAndCerts[0].certPath.certificates.find { CertRole.extract(it) == CertRole.NODE_CA }
- return database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
+ return database.transaction {
// TODO Move these checks out of data access layer
val request = nodeCaCert?.let {
- singleRequestWhere(CertificateDataEntity::class.java) { builder, path ->
- val certPublicKeyHashEq = builder.equal(path.get(CertificateDataEntity::publicKeyHash.name), it.publicKey.encoded.sha256().toString())
- val certStatusValid = builder.equal(path.get(CertificateDataEntity::certificateStatus.name), CertificateStatus.VALID)
- builder.and(certPublicKeyHashEq, certStatusValid)
- }
+ getSignedRequestByPublicHash(it.publicKey.encoded.sha256(), this)
}
request ?: throw IllegalArgumentException("Unknown node info, this public key is not registered with the network management service.")
+ require(request.certificateData!!.certificateStatus == CertificateStatus.VALID) { "Certificate is no longer valid" }
/*
* Delete any previous [NodeInfoEntity] instance for this CSR
@@ -40,13 +36,13 @@ class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeIn
* but it has been confirmed that this fact has been acknowledged at the design time and we are fine with it.
*/
deleteRequest(NodeInfoEntity::class.java) { builder, path ->
- builder.equal(path.get(NodeInfoEntity::certificateSigningRequest.name), request.certificateSigningRequest)
+ builder.equal(path.get(NodeInfoEntity::certificateSigningRequest.name), request)
}
val hash = signedNodeInfo.raw.hash
val hashedNodeInfo = NodeInfoEntity(
nodeInfoHash = hash.toString(),
- certificateSigningRequest = request.certificateSigningRequest,
+ certificateSigningRequest = request,
signedNodeInfoBytes = signedNodeInfo.serialize().bytes)
session.save(hashedNodeInfo)
hash
@@ -61,16 +57,16 @@ class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeIn
override fun getCertificatePath(publicKeyHash: SecureHash): CertPath? {
return database.transaction {
- val builder = session.criteriaBuilder
- val query = builder.createQuery(ByteArray::class.java).run {
- from(CertificateSigningRequestEntity::class.java).run {
- select(get(CertificateSigningRequestEntity::certificateData.name)
- .get(CertificateDataEntity::certificatePathBytes.name))
- where(builder.equal(get(CertificateSigningRequestEntity::certificateData.name)
- .get(CertificateDataEntity::publicKeyHash.name), publicKeyHash.toString()))
- }
- }
- session.createQuery(query).uniqueResultOptional().orElseGet { null }?.let { buildCertPath(it) }
+ val request = getSignedRequestByPublicHash(publicKeyHash, this)
+ request?.let { buildCertPath(it.certificateData!!.certificatePathBytes) }
+ }
+ }
+
+ private fun getSignedRequestByPublicHash(publicKeyHash: SecureHash, transaction: DatabaseTransaction): CertificateSigningRequestEntity? {
+ return transaction.singleRequestWhere(CertificateSigningRequestEntity::class.java) { builder, path ->
+ val publicKeyEq = builder.equal(path.get(CertificateSigningRequestEntity::publicKeyHash.name), publicKeyHash.toString())
+ val statusEq = builder.equal(path.get(CertificateSigningRequestEntity::status.name), RequestStatus.SIGNED)
+ builder.and(publicKeyEq, statusEq)
}
}
}
\ No newline at end of file
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/SchemaService.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/SchemaService.kt
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/CertificateSigningRequestEntity.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/CertificateSigningRequestEntity.kt
index 40f44c6599..aa803cdef2 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/CertificateSigningRequestEntity.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/CertificateSigningRequestEntity.kt
@@ -4,6 +4,7 @@ import com.r3.corda.networkmanage.common.persistence.CertificateData
import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest
import com.r3.corda.networkmanage.common.persistence.CertificateStatus
import com.r3.corda.networkmanage.common.persistence.RequestStatus
+import net.corda.core.crypto.SecureHash
import org.bouncycastle.pkcs.PKCS10CertificationRequest
import org.hibernate.envers.Audited
import java.security.cert.CertPath
@@ -12,7 +13,7 @@ import java.time.Instant
import javax.persistence.*
@Entity
-@Table(name = "certificate_signing_request")
+@Table(name = "certificate_signing_request", indexes = arrayOf(Index(name = "IDX_PUB_KEY_HASH", columnList = "public_key_hash")))
class CertificateSigningRequestEntity(
@Id
@Column(name = "request_id", length = 64)
@@ -22,6 +23,9 @@ class CertificateSigningRequestEntity(
@Column(name = "legal_name", length = 256, nullable = false)
val legalName: String,
+ @Column(name = "public_key_hash", length = 64)
+ val publicKeyHash: String,
+
@Audited
@Column(name = "status", nullable = false)
@Enumerated(EnumType.STRING)
@@ -50,6 +54,7 @@ class CertificateSigningRequestEntity(
fun toCertificateSigningRequest() = CertificateSigningRequest(
requestId = requestId,
legalName = legalName,
+ publicKeyHash = SecureHash.parse(publicKeyHash),
status = status,
request = request(),
remark = remark,
@@ -59,6 +64,7 @@ class CertificateSigningRequestEntity(
fun copy(requestId: String = this.requestId,
legalName: String = this.legalName,
+ publicKeyHash: String = this.publicKeyHash,
status: RequestStatus = this.status,
modifiedBy: List = this.modifiedBy,
modifiedAt: Instant = this.modifiedAt,
@@ -69,6 +75,7 @@ class CertificateSigningRequestEntity(
return CertificateSigningRequestEntity(
requestId = requestId,
legalName = legalName,
+ publicKeyHash = publicKeyHash,
status = status,
modifiedAt = modifiedAt,
modifiedBy = modifiedBy,
@@ -82,16 +89,13 @@ class CertificateSigningRequestEntity(
}
@Entity
-@Table(name = "certificate_data", indexes = arrayOf(Index(name = "IDX_PUB_KEY_HASH", columnList = "public_key_hash")))
+@Table(name = "certificate_data")
class CertificateDataEntity(
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
val id: Long? = null,
- @Column(name = "public_key_hash", length = 64)
- val publicKeyHash: String,
-
@Column(name = "certificate_status")
val certificateStatus: CertificateStatus,
@@ -105,7 +109,6 @@ class CertificateDataEntity(
) {
fun toCertificateData(): CertificateData {
return CertificateData(
- publicKeyHash = publicKeyHash,
certStatus = certificateStatus,
certPath = toCertificatePath()
)
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkMapEntity.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkMapEntity.kt
index f71a252213..65c14f560a 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkMapEntity.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkMapEntity.kt
@@ -1,7 +1,11 @@
package com.r3.corda.networkmanage.common.persistence.entity
+import com.r3.corda.networkmanage.common.utils.SignedNetworkMap
import net.corda.core.internal.DigitalSignatureWithCert
+import net.corda.core.serialization.SerializedBytes
+import net.corda.core.serialization.deserialize
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
+import net.corda.nodeapi.internal.network.NetworkMap
import javax.persistence.*
@Entity
@@ -23,11 +27,12 @@ class NetworkMapEntity(
@Column(name = "certificate")
val certificate: ByteArray
) {
- /**
- * Deserializes NetworkMapEntity.signatureBytes into the [DigitalSignatureWithCert] instance
- */
- fun signatureAndCertificate(): DigitalSignatureWithCert {
- return DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature)
- }
+ fun toNetworkMap(): NetworkMap = networkMap.deserialize()
-}
\ No newline at end of file
+ fun toSignedNetworkMap(): SignedNetworkMap {
+ return SignedNetworkMap(
+ SerializedBytes(networkMap),
+ DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature)
+ )
+ }
+}
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkParametersEntity.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkParametersEntity.kt
index ca445a1f02..8cd860486c 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkParametersEntity.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkParametersEntity.kt
@@ -35,13 +35,15 @@ class NetworkParametersEntity(
@Column(name = "certificate")
val certificate: ByteArray?
) {
- fun networkParameters(): NetworkParameters = parametersBytes.deserialize()
+ val isSigned: Boolean get() = certificate != null && signature != null
- // Return signed network parameters or null if they haven't been signed yet.
- fun signedParameters(): SignedNetworkParameters? {
- return if (certificate != null && signature != null) {
- val sigWithCert = DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature)
- SignedDataWithCert(SerializedBytes(parametersBytes), sigWithCert)
- } else null
+ fun toNetworkParameters(): NetworkParameters = parametersBytes.deserialize()
+
+ fun toSignedNetworkParameters(): SignedNetworkParameters {
+ if (certificate == null || signature == null) throw IllegalStateException("Network parameters entity is not signed: $parametersHash")
+ return SignedDataWithCert(
+ SerializedBytes(parametersBytes),
+ DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature)
+ )
}
-}
\ No newline at end of file
+}
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSigner.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSigner.kt
index 5d3d3b15e8..e75baa4c5b 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSigner.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSigner.kt
@@ -4,20 +4,30 @@ import com.r3.corda.networkmanage.common.persistence.CertificateStatus
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
import net.corda.core.internal.SignedDataWithCert
import net.corda.core.serialization.serialize
+import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.network.NetworkMap
import net.corda.nodeapi.internal.network.NetworkParameters
class NetworkMapSigner(private val networkMapStorage: NetworkMapStorage, private val signer: Signer) {
+ private companion object {
+ val logger = contextLogger()
+ }
+
/**
* Signs the network map and latest network parameters if they haven't been signed yet.
*/
fun signNetworkMap() {
// TODO There is no network parameters update process in place yet. We assume that latest parameters are to be used
// in current network map.
- val latestNetworkParameters = networkMapStorage.getLatestUnsignedNetworkParameters()
- val currentNetworkParameters = networkMapStorage.getCurrentSignedNetworkParameters()
- if (currentNetworkParameters?.verified() != latestNetworkParameters)
- signNetworkParameters(latestNetworkParameters)
+ val latestNetworkParameters = networkMapStorage.getLatestNetworkParameters()
+ if (latestNetworkParameters == null) {
+ logger.debug("No network parameters present")
+ return
+ }
+ val currentNetworkParameters = networkMapStorage.getNetworkParametersOfNetworkMap()
+ if (currentNetworkParameters?.verified() != latestNetworkParameters) {
+ persistSignedNetworkParameters(latestNetworkParameters)
+ }
val currentSignedNetworkMap = networkMapStorage.getCurrentNetworkMap()
val nodeInfoHashes = networkMapStorage.getNodeInfoHashes(CertificateStatus.VALID)
val serialisedNetworkMap = NetworkMap(nodeInfoHashes, latestNetworkParameters.serialize().hash).serialize()
@@ -27,10 +37,8 @@ class NetworkMapSigner(private val networkMapStorage: NetworkMapStorage, private
}
}
- /**
- * Signs latest inserted network parameters.
- */
- fun signNetworkParameters(networkParameters: NetworkParameters) {
+ fun persistSignedNetworkParameters(networkParameters: NetworkParameters) {
+ logger.info("Signing and persisting network parameters: $networkParameters")
val digitalSignature = signer.signObject(networkParameters).sig
networkMapStorage.saveNetworkParameters(networkParameters, digitalSignature)
}
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/Utils.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/Utils.kt
index 8ac987e03a..3554580976 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/Utils.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/Utils.kt
@@ -10,14 +10,20 @@ import net.corda.core.internal.SignedDataWithCert
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
import net.corda.nodeapi.internal.network.NetworkMap
import net.corda.nodeapi.internal.network.NetworkParameters
+import java.security.KeyPair
+import java.security.PrivateKey
import java.security.PublicKey
import java.security.cert.CertPath
-import java.security.cert.Certificate
+import java.security.cert.X509Certificate
// TODO These should be defined in node-api
typealias SignedNetworkParameters = SignedDataWithCert
typealias SignedNetworkMap = SignedDataWithCert
+data class CertPathAndKey(val certPath: List, val key: PrivateKey) {
+ fun toKeyPair(): KeyPair = KeyPair(certPath[0].publicKey, key)
+}
+
// TODO: replace this with Crypto.hash when its available.
/**
* Returns SHA256 hash of this public key
@@ -42,7 +48,7 @@ fun Array.toConfigWithOptions(registerOptions: OptionParser.() -> Un
class ShowHelpException(val parser: OptionParser, val errorMessage: String? = null) : Exception()
-fun buildCertPath(vararg certificates: Certificate): CertPath = X509CertificateFactory().delegate.generateCertPath(certificates.asList())
+fun buildCertPath(vararg certificates: X509Certificate): CertPath = X509CertificateFactory().generateCertPath(certificates.asList())
fun buildCertPath(certPathBytes: ByteArray): CertPath = X509CertificateFactory().delegate.generateCertPath(certPathBytes.inputStream())
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt
index 4ffaf397ce..5c2aabae0e 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt
@@ -65,8 +65,7 @@ data class JiraConfig(
val address: String,
val projectCode: String,
val username: String,
- val password: String,
- val doneTransitionCode: Int
+ val password: String
)
/**
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/JiraCient.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/JiraCient.kt
index d7ebf4abc0..9cc722a325 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/JiraCient.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/JiraCient.kt
@@ -1,13 +1,15 @@
package com.r3.corda.networkmanage.doorman
+import com.atlassian.jira.rest.client.api.IssueRestClient
import com.atlassian.jira.rest.client.api.JiraRestClient
+import com.atlassian.jira.rest.client.api.domain.Comment
import com.atlassian.jira.rest.client.api.domain.Field
import com.atlassian.jira.rest.client.api.domain.Issue
import com.atlassian.jira.rest.client.api.domain.IssueType
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder
import com.atlassian.jira.rest.client.api.domain.input.TransitionInput
import net.corda.core.identity.CordaX500Name
-import net.corda.core.utilities.loggerFor
+import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.crypto.X509Utilities
import org.bouncycastle.asn1.x500.style.BCStyle
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
@@ -17,15 +19,20 @@ import java.io.StringWriter
import java.security.cert.CertPath
import javax.security.auth.x500.X500Principal
-class JiraClient(private val restClient: JiraRestClient, private val projectCode: String, private val doneTransitionCode: Int) {
+class JiraClient(private val restClient: JiraRestClient, private val projectCode: String) {
companion object {
- val logger = loggerFor()
+ val logger = contextLogger()
}
- // The JIRA project must have a Request ID field and the Task issue type.
+ // The JIRA project must have a Request ID and reject reason field, and the Task issue type.
private val requestIdField: Field = restClient.metadataClient.fields.claim().find { it.name == "Request ID" } ?: throw IllegalArgumentException("Request ID field not found in JIRA '$projectCode'")
+ private val rejectReasonField: Field = restClient.metadataClient.fields.claim().find { it.name == "Reject Reason" } ?: throw IllegalArgumentException("Reject Reason field not found in JIRA '$projectCode'")
private val taskIssueType: IssueType = restClient.metadataClient.issueTypes.claim().find { it.name == "Task" } ?: throw IllegalArgumentException("Task issue type field not found in JIRA '$projectCode'")
+ private var doneTransitionId: Int = -1
+ private var canceledTransitionId: Int = -1
+ private var startProgressTransitionId: Int = -1
+
fun createRequestTicket(requestId: String, signingRequest: PKCS10CertificationRequest) {
// Check there isn't already a ticket for this request.
if (getIssueById(requestId) != null) {
@@ -54,14 +61,26 @@ class JiraClient(private val restClient: JiraRestClient, private val projectCode
restClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim()
}
- fun getApprovedRequests(): List> {
+ fun getApprovedRequests(): List {
val issues = restClient.searchClient.searchJql("project = $projectCode AND status = Approved").claim().issues
- return issues.map { issue ->
- issue.getField(requestIdField.id)?.value?.toString().let {
- val requestId = it ?: throw IllegalArgumentException("RequestId cannot be null.")
- val approvedBy = issue.assignee?.displayName ?: "Unknown"
- Pair(requestId, approvedBy)
- }
+ return issues.mapNotNull { issue ->
+ val requestId = issue.getField(requestIdField.id)?.value?.toString() ?: throw IllegalArgumentException("Error processing request '${issue.key}' : RequestId cannot be null.")
+ // Issue retrieved via search doesn't contain change logs.
+ val fullIssue = restClient.issueClient.getIssue(issue.key, listOf(IssueRestClient.Expandos.CHANGELOG)).claim()
+ val approvedBy = fullIssue.changelog?.last { it.items.any { it.field == "status" && it.toString == "Approved" } }
+ ApprovedRequest(requestId, approvedBy?.author?.displayName ?: "Unknown")
+ }
+ }
+
+ fun getRejectedRequests(): List {
+ val issues = restClient.searchClient.searchJql("project = $projectCode AND status = Rejected").claim().issues
+ return issues.mapNotNull { issue ->
+ val requestId = issue.getField(requestIdField.id)?.value?.toString() ?: throw IllegalArgumentException("Error processing request '${issue.key}' : RequestId cannot be null.")
+ val rejectedReason = issue.getField(rejectReasonField.id)?.value?.toString()
+ // Issue retrieved via search doesn't contain comments.
+ val fullIssue = restClient.issueClient.getIssue(issue.key, listOf(IssueRestClient.Expandos.CHANGELOG)).claim()
+ val rejectedBy = fullIssue.changelog?.last { it.items.any { it.field == "status" && it.toString == "Rejected" } }
+ RejectedRequest(requestId, rejectedBy?.author?.displayName ?: "Unknown", rejectedReason)
}
}
@@ -69,16 +88,41 @@ class JiraClient(private val restClient: JiraRestClient, private val projectCode
// Retrieving certificates for signed CSRs to attach to the jira tasks.
signedRequests.forEach { (id, certPath) ->
val certificate = certPath.certificates.first()
- // Jira only support ~ (contains) search for custom textfield.
val issue = getIssueById(id)
if (issue != null) {
- restClient.issueClient.transition(issue, TransitionInput(doneTransitionCode)).fail { logger.error("Exception when transiting JIRA status.", it) }.claim()
- restClient.issueClient.addAttachment(issue.attachmentsUri, certificate?.encoded?.inputStream(), "${X509Utilities.CORDA_CLIENT_CA}.cer")
- .fail { logger.error("Exception when uploading attachment to JIRA.", it) }.claim()
+ if (doneTransitionId == -1) {
+ doneTransitionId = restClient.issueClient.getTransitions(issue.transitionsUri).claim().single { it.name == "Done" }.id
+ }
+ restClient.issueClient.transition(issue, TransitionInput(doneTransitionId)).fail { logger.error("Exception when transiting JIRA status.", it) }.claim()
+ restClient.issueClient.addAttachment(issue.attachmentsUri, certificate.encoded.inputStream(), "${X509Utilities.CORDA_CLIENT_CA}.cer")
+ .fail { logger.error("Error processing request '${issue.key}' : Exception when uploading attachment to JIRA.", it) }.claim()
}
}
}
- private fun getIssueById(requestId: String): Issue? =
- restClient.searchClient.searchJql("'Request ID' ~ $requestId").claim().issues.firstOrNull()
+ fun updateRejectedRequests(rejectedRequests: List) {
+ rejectedRequests.mapNotNull { getIssueById(it) }
+ .forEach { issue ->
+ // Move status to in progress.
+ if (startProgressTransitionId == -1) {
+ startProgressTransitionId = restClient.issueClient.getTransitions(issue.transitionsUri).claim().single { it.name == "Start Progress" }.id
+ }
+ restClient.issueClient.transition(issue, TransitionInput(startProgressTransitionId)).fail { logger.error("Error processing request '${issue.key}' : Exception when transiting JIRA status.", it) }.claim()
+ // Move status to cancelled.
+ if (canceledTransitionId == -1) {
+ canceledTransitionId = restClient.issueClient.getTransitions(issue.transitionsUri).claim().single { it.name == "Stop Progress" }.id
+ }
+ restClient.issueClient.transition(issue, TransitionInput(canceledTransitionId)).fail { logger.error("Error processing request '${issue.key}' : Exception when transiting JIRA status.", it) }.claim()
+ restClient.issueClient.addComment(issue.commentsUri, Comment.valueOf("Request cancelled by doorman.")).claim()
+ }
+ }
+
+ private fun getIssueById(requestId: String): Issue? {
+ // Jira only support ~ (contains) search for custom textfield.
+ return restClient.searchClient.searchJql("'Request ID' ~ $requestId").claim().issues.firstOrNull()
+ }
}
+
+data class ApprovedRequest(val requestId: String, val approvedBy: String)
+
+data class RejectedRequest(val requestId: String, val rejectedBy: String, val reason: String?)
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt
index 74fe5885b0..c7dd3f0271 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt
@@ -1,16 +1,11 @@
package com.r3.corda.networkmanage.doorman
-import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory
-import com.r3.corda.networkmanage.common.persistence.*
+import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage
import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE
-import com.r3.corda.networkmanage.common.signer.NetworkMapSigner
+import com.r3.corda.networkmanage.common.persistence.configureDatabase
+import com.r3.corda.networkmanage.common.utils.CertPathAndKey
import com.r3.corda.networkmanage.common.utils.ShowHelpException
-import com.r3.corda.networkmanage.doorman.signer.DefaultCsrHandler
-import com.r3.corda.networkmanage.doorman.signer.JiraCsrHandler
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
-import com.r3.corda.networkmanage.doorman.webservice.MonitoringWebService
-import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService
-import com.r3.corda.networkmanage.doorman.webservice.RegistrationWebService
import com.r3.corda.networkmanage.hsm.configuration.Parameters.Companion.DEFAULT_CSR_CERTIFICATE_NAME
import com.r3.corda.networkmanage.hsm.configuration.Parameters.Companion.DEFAULT_NETWORK_MAP_CERTIFICATE_NAME
import net.corda.core.crypto.Crypto
@@ -21,141 +16,19 @@ import net.corda.core.internal.div
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
import net.corda.core.serialization.internal.nodeSerializationEnv
import net.corda.core.utilities.NetworkHostAndPort
-import net.corda.core.utilities.loggerFor
import net.corda.nodeapi.internal.crypto.*
import net.corda.nodeapi.internal.network.NetworkParameters
-import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme
import org.bouncycastle.pkcs.PKCS10CertificationRequest
-import java.io.Closeable
-import java.net.URI
import java.nio.file.Path
import java.security.cert.X509Certificate
import java.time.Instant
-import java.util.*
-import java.util.concurrent.Executors
-import java.util.concurrent.TimeUnit
import javax.security.auth.x500.X500Principal
import kotlin.concurrent.thread
import kotlin.system.exitProcess
-class NetworkManagementServer : Closeable {
- private val doOnClose = mutableListOf<() -> Unit>()
- lateinit var hostAndPort: NetworkHostAndPort
-
- override fun close() = doOnClose.forEach { it() }
-
- companion object {
- private val logger = loggerFor()
- }
-
- private fun getNetworkMapService(config: NetworkMapConfig, database: CordaPersistence, signer: LocalSigner?, updateNetworkParameters: NetworkParameters?): NodeInfoWebService {
- val networkMapStorage = PersistentNetworkMapStorage(database)
- val nodeInfoStorage = PersistentNodeInfoStorage(database)
- val localNetworkMapSigner = if (signer != null) NetworkMapSigner(networkMapStorage, signer) else null
-
- updateNetworkParameters?.let {
- // Persisting new network parameters
- val currentNetworkParameters = networkMapStorage.getCurrentSignedNetworkParameters()
- if (currentNetworkParameters == null) {
- localNetworkMapSigner?.signNetworkParameters(it) ?: networkMapStorage.saveNetworkParameters(it, null)
- } else {
- throw UnsupportedOperationException("Network parameters already exist. Updating them via the file config is not supported yet.")
- }
- }
-
- // This call will fail if parameter is null in DB.
- try {
- val latestParameter = networkMapStorage.getLatestUnsignedNetworkParameters()
- logger.info("Starting network map service with network parameters : $latestParameter")
- } catch (e: NoSuchElementException) {
- logger.error("No network parameter found, please upload new network parameter before starting network map service. The server will now exit.")
- exitProcess(-1)
- }
-
- // Thread sign network map in case of change (i.e. a new node info has been added or a node info has been removed).
- if (localNetworkMapSigner != null) {
- val scheduledExecutor = Executors.newScheduledThreadPool(1)
- val signingThread = Runnable {
- try {
- localNetworkMapSigner.signNetworkMap()
- } catch (e: Exception) {
- // Log the error and carry on.
- logger.error("Error encountered when processing node info changes.", e)
- }
- }
- scheduledExecutor.scheduleAtFixedRate(signingThread, config.signInterval, config.signInterval, TimeUnit.MILLISECONDS)
- doOnClose += { scheduledExecutor.shutdown() }
- }
-
- return NodeInfoWebService(nodeInfoStorage, networkMapStorage, config)
- }
-
-
- private fun getDoormanService(config: DoormanConfig, database: CordaPersistence, signer: LocalSigner?, serverStatus: NetworkManagementServerStatus): RegistrationWebService {
- logger.info("Starting Doorman server.")
- val requestService = if (config.approveAll) {
- logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing requests.")
- ApproveAllCertificateRequestStorage(PersistentCertificateRequestStorage(database))
- } else {
- PersistentCertificateRequestStorage(database)
- }
-
- val jiraConfig = config.jiraConfig
- val requestProcessor = if (jiraConfig != null) {
- val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password)
- val jiraClient = JiraClient(jiraWebAPI, jiraConfig.projectCode, jiraConfig.doneTransitionCode)
- JiraCsrHandler(jiraClient, requestService, DefaultCsrHandler(requestService, signer))
- } else {
- DefaultCsrHandler(requestService, signer)
- }
-
- val scheduledExecutor = Executors.newScheduledThreadPool(1)
- val approvalThread = Runnable {
- try {
- serverStatus.lastRequestCheckTime = Instant.now()
- // Create tickets for requests which don't have one yet.
- requestProcessor.createTickets()
- // Process Jira approved tickets.
- requestProcessor.processApprovedRequests()
- } catch (e: Exception) {
- // Log the error and carry on.
- logger.error("Error encountered when approving request.", e)
- }
- }
- scheduledExecutor.scheduleAtFixedRate(approvalThread, config.approveInterval, config.approveInterval, TimeUnit.MILLISECONDS)
- doOnClose += { scheduledExecutor.shutdown() }
-
- return RegistrationWebService(requestProcessor)
- }
-
- fun start(hostAndPort: NetworkHostAndPort,
- database: CordaPersistence,
- doormanSigner: LocalSigner? = null,
- doormanServiceParameter: DoormanConfig?, // TODO Doorman config shouldn't be optional as the doorman is always required to run
- startNetworkMap: NetworkMapStartParams?
- ) {
- val services = mutableListOf()
- val serverStatus = NetworkManagementServerStatus()
-
- startNetworkMap?.let { services += getNetworkMapService(it.config, database, it.signer, it.updateNetworkParameters) }
- doormanServiceParameter?.let { services += getDoormanService(it, database, doormanSigner, serverStatus) }
-
- require(services.isNotEmpty()) { "No service created, please provide at least one service config." }
-
- // TODO: use mbean to expose audit data?
- services += MonitoringWebService(serverStatus)
-
- val webServer = NetworkManagementWebServer(hostAndPort, *services.toTypedArray())
- webServer.start()
-
- doOnClose += webServer::close
- this.hostAndPort = webServer.hostAndPort
- }
-}
-
data class NetworkMapStartParams(val signer: LocalSigner?, val updateNetworkParameters: NetworkParameters?, val config: NetworkMapConfig)
data class NetworkManagementServerStatus(var serverStartTime: Instant = Instant.now(), var lastRequestCheckTime: Instant? = null)
@@ -181,9 +54,8 @@ fun generateRootKeyPair(rootStoreFile: Path, rootKeystorePass: String?, rootPriv
val rootPrivateKeyPassword = rootPrivateKeyPass ?: readPassword("Root Private Key Password: ")
if (rootStore.containsAlias(X509Utilities.CORDA_ROOT_CA)) {
- val oldKey = loadOrCreateKeyStore(rootStoreFile, rootKeystorePassword).getCertificate(X509Utilities.CORDA_ROOT_CA).publicKey
- println("Key ${X509Utilities.CORDA_ROOT_CA} already exists in keystore, process will now terminate.")
- println(oldKey)
+ println("${X509Utilities.CORDA_ROOT_CA} already exists in keystore, process will now terminate.")
+ println(rootStore.getCertificate(X509Utilities.CORDA_ROOT_CA))
exitProcess(1)
}
@@ -203,7 +75,7 @@ fun generateRootKeyPair(rootStoreFile: Path, rootKeystorePass: String?, rootPriv
println("Trust store for distribution to nodes created in $nodeTrustStore")
println("Root CA keypair and certificate stored in ${rootStoreFile.toAbsolutePath()}.")
- println(loadKeyStore(rootStoreFile, rootKeystorePassword).getCertificate(X509Utilities.CORDA_ROOT_CA).publicKey)
+ println(selfSignCert)
}
fun generateSigningKeyPairs(keystoreFile: Path, rootStoreFile: Path, rootKeystorePass: String?, rootPrivateKeyPass: String?, keystorePass: String?, caPrivateKeyPass: String?) {
@@ -262,7 +134,7 @@ fun generateSigningKeyPairs(keystoreFile: Path, rootStoreFile: Path, rootKeystor
}
-private fun buildLocalSigners(parameters: NetworkManagementServerParameters): Pair? {
+private fun processKeyStore(parameters: NetworkManagementServerParameters): Pair? {
if (parameters.keystorePath == null) return null
// Get password from console if not in config.
@@ -270,19 +142,22 @@ private fun buildLocalSigners(parameters: NetworkManagementServerParameters): Pa
val privateKeyPassword = parameters.caPrivateKeyPassword ?: readPassword("Private key password: ")
val keyStore = loadOrCreateKeyStore(parameters.keystorePath, keyStorePassword)
- val (doormanSigner, networkMapSigner) = listOf(DEFAULT_CSR_CERTIFICATE_NAME, DEFAULT_NETWORK_MAP_CERTIFICATE_NAME).map {
- val keyPair = keyStore.getKeyPair(it, privateKeyPassword)
- val certPath = keyStore.getCertificateChain(it).map { it as X509Certificate }
- LocalSigner(keyPair, certPath.toTypedArray())
+ val csrCertPathAndKey = keyStore.run {
+ CertPathAndKey(
+ keyStore.getCertificateChain(DEFAULT_CSR_CERTIFICATE_NAME).map { it as X509Certificate },
+ keyStore.getSupportedKey(DEFAULT_CSR_CERTIFICATE_NAME, privateKeyPassword)
+ )
}
- return Pair(doormanSigner, networkMapSigner)
+ val networkMapSigner = LocalSigner(keyStore.getCertificateAndKeyPair(DEFAULT_NETWORK_MAP_CERTIFICATE_NAME, privateKeyPassword))
+
+ return Pair(csrCertPathAndKey, networkMapSigner)
}
/**
* This storage automatically approves all created requests.
*/
-private class ApproveAllCertificateRequestStorage(private val delegate: CertificationRequestStorage) : CertificationRequestStorage by delegate {
+class ApproveAllCertificateRequestStorage(private val delegate: CertificationRequestStorage) : CertificationRequestStorage by delegate {
override fun saveRequest(request: PKCS10CertificationRequest): String {
val requestId = delegate.saveRequest(request)
delegate.markRequestTicketCreated(requestId)
@@ -311,9 +186,9 @@ fun main(args: Array) {
initialiseSerialization()
val database = configureDatabase(dataSourceProperties)
// TODO: move signing to signing server.
- val localSigners = buildLocalSigners(this)
+ val csrAndNetworkMap = processKeyStore(this)
- if (localSigners != null) {
+ if (csrAndNetworkMap != null) {
println("Starting network management services with local signing")
}
@@ -325,10 +200,10 @@ fun main(args: Array) {
parseNetworkParametersFrom(it)
}
val networkMapStartParams = networkMapConfig?.let {
- NetworkMapStartParams(localSigners?.second, networkParameters, it)
+ NetworkMapStartParams(csrAndNetworkMap?.second, networkParameters, it)
}
- networkManagementServer.start(NetworkHostAndPort(host, port), database, localSigners?.first, doormanConfig, networkMapStartParams)
+ networkManagementServer.start(NetworkHostAndPort(host, port), database, csrAndNetworkMap?.first, doormanConfig, networkMapStartParams)
Runtime.getRuntime().addShutdownHook(thread(start = false) {
networkManagementServer.close()
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementServer.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementServer.kt
index e849c3b086..919ca722be 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementServer.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementServer.kt
@@ -1,58 +1,140 @@
package com.r3.corda.networkmanage.doorman
+import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory
+import com.r3.corda.networkmanage.common.persistence.PersistentCertificateRequestStorage
+import com.r3.corda.networkmanage.common.persistence.PersistentNetworkMapStorage
+import com.r3.corda.networkmanage.common.persistence.PersistentNodeInfoStorage
+import com.r3.corda.networkmanage.common.signer.NetworkMapSigner
+import com.r3.corda.networkmanage.common.utils.CertPathAndKey
+import com.r3.corda.networkmanage.doorman.signer.DefaultCsrHandler
+import com.r3.corda.networkmanage.doorman.signer.JiraCsrHandler
+import com.r3.corda.networkmanage.doorman.signer.LocalSigner
+import com.r3.corda.networkmanage.doorman.webservice.MonitoringWebService
+import com.r3.corda.networkmanage.doorman.webservice.NetworkMapWebService
+import com.r3.corda.networkmanage.doorman.webservice.RegistrationWebService
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.loggerFor
-import org.eclipse.jetty.server.Server
-import org.eclipse.jetty.server.ServerConnector
-import org.eclipse.jetty.server.handler.HandlerCollection
-import org.eclipse.jetty.servlet.ServletContextHandler
-import org.eclipse.jetty.servlet.ServletHolder
-import org.glassfish.jersey.server.ResourceConfig
-import org.glassfish.jersey.servlet.ServletContainer
+import net.corda.nodeapi.internal.network.NetworkParameters
+import net.corda.nodeapi.internal.persistence.CordaPersistence
import java.io.Closeable
-import java.net.InetSocketAddress
+import java.net.URI
+import java.time.Instant
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
-/**
- * NetworkManagementWebServer runs on Jetty server and provides service via http.
- */
-class NetworkManagementWebServer(hostAndPort: NetworkHostAndPort, private vararg val webServices: Any) : Closeable {
+class NetworkManagementServer : Closeable {
companion object {
- val logger = loggerFor()
+ private val logger = loggerFor()
}
- private val server: Server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply {
- handler = HandlerCollection().apply {
- addHandler(buildServletContextHandler())
- }
- }
-
- val hostAndPort: NetworkHostAndPort
- get() = server.connectors.mapNotNull { it as? ServerConnector }
- .map { NetworkHostAndPort(it.host, it.localPort) }
- .first()
+ private val closeActions = mutableListOf<() -> Unit>()
+ lateinit var hostAndPort: NetworkHostAndPort
override fun close() {
- logger.info("Shutting down network management web services...")
- server.stop()
- server.join()
- }
-
- fun start() {
- logger.info("Starting network management web services...")
- server.start()
- logger.info("Network management web services started on $hostAndPort with ${webServices.map { it.javaClass.simpleName }}")
- println("Network management web services started on $hostAndPort with ${webServices.map { it.javaClass.simpleName }}")
- }
-
- private fun buildServletContextHandler(): ServletContextHandler {
- return ServletContextHandler().apply {
- contextPath = "/"
- val resourceConfig = ResourceConfig().apply {
- // Add your API provider classes (annotated for JAX-RS) here
- webServices.forEach { register(it) }
+ for (closeAction in closeActions) {
+ try {
+ closeAction()
+ } catch (e: Exception) {
+ logger.warn("Discregarding exception thrown during close", e)
}
- val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { initOrder = 0 }// Initialise at server start
- addServlet(jerseyServlet, "/*")
}
}
+
+ private fun getNetworkMapService(config: NetworkMapConfig, database: CordaPersistence, signer: LocalSigner?, newNetworkParameters: NetworkParameters?): NetworkMapWebService {
+ val networkMapStorage = PersistentNetworkMapStorage(database)
+ val nodeInfoStorage = PersistentNodeInfoStorage(database)
+ val localNetworkMapSigner = signer?.let { NetworkMapSigner(networkMapStorage, it) }
+
+ newNetworkParameters?.let {
+ val netParamsOfNetworkMap = networkMapStorage.getNetworkParametersOfNetworkMap()
+ if (netParamsOfNetworkMap == null) {
+ localNetworkMapSigner?.persistSignedNetworkParameters(it) ?: networkMapStorage.saveNetworkParameters(it, null)
+ } else {
+ throw UnsupportedOperationException("Network parameters already exist. Updating them is not supported yet.")
+ }
+ }
+
+ val latestParameters = networkMapStorage.getLatestNetworkParameters() ?:
+ throw IllegalStateException("No network parameters were found. Please upload new network parameters before starting network map service")
+ logger.info("Starting network map service with network parameters: $latestParameters")
+
+ if (localNetworkMapSigner != null) {
+ logger.info("Starting background worker for signing the network map using the local key store")
+ val scheduledExecutor = Executors.newScheduledThreadPool(1)
+ scheduledExecutor.scheduleAtFixedRate({
+ try {
+ localNetworkMapSigner.signNetworkMap()
+ } catch (e: Exception) {
+ // Log the error and carry on.
+ logger.error("Unable to sign network map", e)
+ }
+ }, config.signInterval, config.signInterval, TimeUnit.MILLISECONDS)
+ closeActions += scheduledExecutor::shutdown
+ }
+
+ return NetworkMapWebService(nodeInfoStorage, networkMapStorage, config)
+ }
+
+
+ private fun getDoormanService(config: DoormanConfig,
+ database: CordaPersistence,
+ csrCertPathAndKey: CertPathAndKey?,
+ serverStatus: NetworkManagementServerStatus): RegistrationWebService {
+ logger.info("Starting Doorman server.")
+ val requestService = if (config.approveAll) {
+ logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing requests.")
+ ApproveAllCertificateRequestStorage(PersistentCertificateRequestStorage(database))
+ } else {
+ PersistentCertificateRequestStorage(database)
+ }
+
+ val jiraConfig = config.jiraConfig
+ val requestProcessor = if (jiraConfig != null) {
+ val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password)
+ val jiraClient = JiraClient(jiraWebAPI, jiraConfig.projectCode)
+ JiraCsrHandler(jiraClient, requestService, DefaultCsrHandler(requestService, csrCertPathAndKey))
+ } else {
+ DefaultCsrHandler(requestService, csrCertPathAndKey)
+ }
+
+ val scheduledExecutor = Executors.newScheduledThreadPool(1)
+ val approvalThread = Runnable {
+ try {
+ serverStatus.lastRequestCheckTime = Instant.now()
+ // Process Jira approved tickets.
+ requestProcessor.processRequests()
+ } catch (e: Exception) {
+ // Log the error and carry on.
+ logger.error("Error encountered when approving request.", e)
+ }
+ }
+ scheduledExecutor.scheduleAtFixedRate(approvalThread, config.approveInterval, config.approveInterval, TimeUnit.MILLISECONDS)
+ closeActions += scheduledExecutor::shutdown
+
+ return RegistrationWebService(requestProcessor)
+ }
+
+ fun start(hostAndPort: NetworkHostAndPort,
+ database: CordaPersistence,
+ csrCertPathAndKey: CertPathAndKey?,
+ doormanServiceParameter: DoormanConfig?, // TODO Doorman config shouldn't be optional as the doorman is always required to run
+ startNetworkMap: NetworkMapStartParams?
+ ) {
+ val services = mutableListOf()
+ val serverStatus = NetworkManagementServerStatus()
+
+ startNetworkMap?.let { services += getNetworkMapService(it.config, database, it.signer, it.updateNetworkParameters) }
+ doormanServiceParameter?.let { services += getDoormanService(it, database, csrCertPathAndKey, serverStatus) }
+
+ require(services.isNotEmpty()) { "No service created, please provide at least one service config." }
+
+ // TODO: use mbean to expose audit data?
+ services += MonitoringWebService(serverStatus)
+
+ val webServer = NetworkManagementWebServer(hostAndPort, *services.toTypedArray())
+ webServer.start()
+
+ closeActions += webServer::close
+ this.hostAndPort = webServer.hostAndPort
+ }
}
\ No newline at end of file
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementWebServer.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementWebServer.kt
new file mode 100644
index 0000000000..fb99e68c44
--- /dev/null
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementWebServer.kt
@@ -0,0 +1,59 @@
+package com.r3.corda.networkmanage.doorman
+
+import net.corda.core.utilities.NetworkHostAndPort
+import net.corda.core.utilities.contextLogger
+import net.corda.core.utilities.loggerFor
+import org.eclipse.jetty.server.Server
+import org.eclipse.jetty.server.ServerConnector
+import org.eclipse.jetty.server.handler.HandlerCollection
+import org.eclipse.jetty.servlet.ServletContextHandler
+import org.eclipse.jetty.servlet.ServletHolder
+import org.glassfish.jersey.server.ResourceConfig
+import org.glassfish.jersey.servlet.ServletContainer
+import java.io.Closeable
+import java.net.InetSocketAddress
+
+/**
+ * NetworkManagementWebServer runs on Jetty server and provides service via http.
+ */
+class NetworkManagementWebServer(hostAndPort: NetworkHostAndPort, private vararg val webServices: Any) : Closeable {
+ companion object {
+ val logger = contextLogger()
+ }
+
+ private val server: Server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply {
+ handler = HandlerCollection().apply {
+ addHandler(buildServletContextHandler())
+ }
+ }
+
+ val hostAndPort: NetworkHostAndPort
+ get() = server.connectors.mapNotNull { it as? ServerConnector }
+ .map { NetworkHostAndPort(it.host, it.localPort) }
+ .first()
+
+ override fun close() {
+ logger.info("Shutting down network management web services...")
+ server.stop()
+ server.join()
+ }
+
+ fun start() {
+ logger.info("Starting network management web services...")
+ server.start()
+ logger.info("Network management web services started on $hostAndPort with ${webServices.map { it.javaClass.simpleName }}")
+ println("Network management web services started on $hostAndPort with ${webServices.map { it.javaClass.simpleName }}")
+ }
+
+ private fun buildServletContextHandler(): ServletContextHandler {
+ return ServletContextHandler().apply {
+ contextPath = "/"
+ val resourceConfig = ResourceConfig().apply {
+ // Add your API provider classes (annotated for JAX-RS) here
+ webServices.forEach { register(it) }
+ }
+ val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { initOrder = 0 }// Initialise at server start
+ addServlet(jerseyServlet, "/*")
+ }
+ }
+}
\ No newline at end of file
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt
index 2a6a9e5ee4..2048459c75 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt
@@ -1,41 +1,41 @@
package com.r3.corda.networkmanage.doorman.signer
import com.r3.corda.networkmanage.common.persistence.CertificateResponse
-import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest
import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage
import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE
import com.r3.corda.networkmanage.common.persistence.RequestStatus
-import com.r3.corda.networkmanage.doorman.JiraClient
-import net.corda.core.utilities.loggerFor
+import com.r3.corda.networkmanage.common.utils.CertPathAndKey
+import net.corda.nodeapi.internal.crypto.CertificateType
+import net.corda.nodeapi.internal.crypto.X509CertificateFactory
+import net.corda.nodeapi.internal.crypto.X509Utilities
+import org.bouncycastle.asn1.x509.GeneralName
+import org.bouncycastle.asn1.x509.GeneralSubtree
+import org.bouncycastle.asn1.x509.NameConstraints
import org.bouncycastle.pkcs.PKCS10CertificationRequest
+import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
+import java.security.cert.CertPath
+import javax.security.auth.x500.X500Principal
interface CsrHandler {
fun saveRequest(rawRequest: PKCS10CertificationRequest): String
- fun createTickets()
- fun processApprovedRequests()
+ fun processRequests()
fun getResponse(requestId: String): CertificateResponse
}
-class DefaultCsrHandler(private val storage: CertificationRequestStorage, private val signer: LocalSigner?) : CsrHandler {
- override fun processApprovedRequests() {
- storage.getRequests(RequestStatus.APPROVED)
- .forEach { processRequest(it.requestId, it.request) }
- }
+class DefaultCsrHandler(private val storage: CertificationRequestStorage,
+ private val csrCertPathAndKey: CertPathAndKey?) : CsrHandler {
- override fun createTickets() {}
-
- private fun processRequest(requestId: String, request: PKCS10CertificationRequest) {
- if (signer != null) {
- val certs = signer.createSignedClientCertificate(request)
+ override fun processRequests() {
+ if (csrCertPathAndKey == null) return
+ storage.getRequests(RequestStatus.APPROVED).forEach {
+ val nodeCertPath = createSignedNodeCertificate(it.request, csrCertPathAndKey)
// Since Doorman is deployed in the auto-signing mode (i.e. signer != null),
// we use DOORMAN_SIGNATURE as the signer.
- storage.putCertificatePath(requestId, certs, listOf(DOORMAN_SIGNATURE))
+ storage.putCertificatePath(it.requestId, nodeCertPath, listOf(DOORMAN_SIGNATURE))
}
}
- override fun saveRequest(rawRequest: PKCS10CertificationRequest): String {
- return storage.saveRequest(rawRequest)
- }
+ override fun saveRequest(rawRequest: PKCS10CertificationRequest): String = storage.saveRequest(rawRequest)
override fun getResponse(requestId: String): CertificateResponse {
val response = storage.getRequest(requestId)
@@ -45,63 +45,25 @@ class DefaultCsrHandler(private val storage: CertificationRequestStorage, privat
RequestStatus.SIGNED -> CertificateResponse.Ready(response.certData?.certPath ?: throw IllegalArgumentException("Certificate should not be null."))
}
}
-}
-class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: CertificationRequestStorage, private val delegate: CsrHandler) : CsrHandler by delegate {
- private companion object {
- val log = loggerFor()
- }
-
- override fun saveRequest(rawRequest: PKCS10CertificationRequest): String {
- val requestId = delegate.saveRequest(rawRequest)
- // Make sure request has been accepted.
- try {
- if (delegate.getResponse(requestId) !is CertificateResponse.Unauthorised) {
- jiraClient.createRequestTicket(requestId, rawRequest)
- storage.markRequestTicketCreated(requestId)
- }
- } catch (e: Exception) {
- log.warn("There was an error while creating Jira tickets", e)
- } finally {
- return requestId
- }
- }
-
- override fun processApprovedRequests() {
- val approvedRequest = jiraClient.getApprovedRequests()
- approvedRequest.forEach { (id, approvedBy) -> storage.approveRequest(id, approvedBy) }
- delegate.processApprovedRequests()
-
- val signedRequests = approvedRequest.mapNotNull { (id, _) ->
- val request = storage.getRequest(id)
-
- if (request != null && request.status == RequestStatus.SIGNED) {
- request.certData?.certPath?.let { certs -> id to certs }
- } else {
- null
- }
- }.toMap()
- jiraClient.updateSignedRequests(signedRequests)
- }
-
- /**
- * Creates Jira tickets for all request in [RequestStatus.NEW] state.
- *
- * Usually requests are expected to move to the [RequestStatus.TICKET_CREATED] state immediately,
- * they might be left in the [RequestStatus.NEW] state if Jira is down.
- */
- override fun createTickets() {
- try {
- for (signingRequest in storage.getRequests(RequestStatus.NEW)) {
- createTicket(signingRequest)
- }
- } catch (e: Exception) {
- log.warn("There were errors while creating Jira tickets", e)
- }
- }
-
- private fun createTicket(signingRequest: CertificateSigningRequest) {
- jiraClient.createRequestTicket(signingRequest.requestId, signingRequest.request)
- storage.markRequestTicketCreated(signingRequest.requestId)
+ private fun createSignedNodeCertificate(certificationRequest: PKCS10CertificationRequest,
+ csrCertPathAndKey: CertPathAndKey): CertPath {
+ // The sub certs issued by the client must satisfy this directory name (or legal name in Corda) constraints,
+ // sub certs' directory name must be within client CA's name's subtree,
+ // please see [sun.security.x509.X500Name.isWithinSubtree()] for more information.
+ // We assume all attributes in the subject name has been checked prior approval.
+ // TODO: add validation to subject name.
+ val request = JcaPKCS10CertificationRequest(certificationRequest)
+ val nameConstraints = NameConstraints(
+ arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, request.subject))),
+ arrayOf())
+ val nodeCaCert = X509Utilities.createCertificate(
+ CertificateType.NODE_CA,
+ csrCertPathAndKey.certPath[0],
+ csrCertPathAndKey.toKeyPair(),
+ X500Principal(request.subject.encoded),
+ request.publicKey,
+ nameConstraints = nameConstraints)
+ return X509CertificateFactory().generateCertPath(listOf(nodeCaCert) + csrCertPathAndKey.certPath)
}
}
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandler.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandler.kt
new file mode 100644
index 0000000000..b7bea5490e
--- /dev/null
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandler.kt
@@ -0,0 +1,84 @@
+package com.r3.corda.networkmanage.doorman.signer
+
+import com.r3.corda.networkmanage.common.persistence.CertificateResponse
+import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest
+import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage
+import com.r3.corda.networkmanage.common.persistence.RequestStatus
+import com.r3.corda.networkmanage.doorman.ApprovedRequest
+import com.r3.corda.networkmanage.doorman.JiraClient
+import com.r3.corda.networkmanage.doorman.RejectedRequest
+import net.corda.core.utilities.contextLogger
+import net.corda.core.utilities.loggerFor
+import org.bouncycastle.pkcs.PKCS10CertificationRequest
+
+class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: CertificationRequestStorage, private val delegate: CsrHandler) : CsrHandler by delegate {
+ private companion object {
+ val log = contextLogger()
+ }
+
+ override fun saveRequest(rawRequest: PKCS10CertificationRequest): String {
+ val requestId = delegate.saveRequest(rawRequest)
+ // Make sure request has been accepted.
+ try {
+ if (delegate.getResponse(requestId) !is CertificateResponse.Unauthorised) {
+ jiraClient.createRequestTicket(requestId, rawRequest)
+ storage.markRequestTicketCreated(requestId)
+ }
+ } catch (e: Exception) {
+ log.warn("There was an error while creating Jira tickets", e)
+ } finally {
+ return requestId
+ }
+ }
+
+ override fun processRequests() {
+ createTickets()
+ val (approvedRequests, rejectedRequests) = updateRequestStatus()
+ delegate.processRequests()
+ updateJiraTickets(approvedRequests, rejectedRequests)
+ }
+
+ private fun updateRequestStatus(): Pair, List> {
+ // Update local request statuses.
+ val approvedRequest = jiraClient.getApprovedRequests()
+ approvedRequest.forEach { (id, approvedBy) -> storage.approveRequest(id, approvedBy) }
+ val rejectedRequest = jiraClient.getRejectedRequests()
+ rejectedRequest.forEach { (id, rejectedBy, reason) -> storage.rejectRequest(id, rejectedBy, reason) }
+ return Pair(approvedRequest, rejectedRequest)
+ }
+
+ private fun updateJiraTickets(approvedRequest: List, rejectedRequest: List) {
+ // Reconfirm request status and update jira status
+ val signedRequests = approvedRequest.mapNotNull { storage.getRequest(it.requestId) }
+ .filter { it.status == RequestStatus.SIGNED && it.certData != null }
+ .associateBy { it.requestId }
+ .mapValues { it.value.certData!!.certPath }
+ jiraClient.updateSignedRequests(signedRequests)
+
+ val rejectedRequestIDs = rejectedRequest.mapNotNull { storage.getRequest(it.requestId) }
+ .filter { it.status == RequestStatus.REJECTED }
+ .map { it.requestId }
+ jiraClient.updateRejectedRequests(rejectedRequestIDs)
+ }
+
+ /**
+ * Creates Jira tickets for all request in [RequestStatus.NEW] state.
+ *
+ * Usually requests are expected to move to the [RequestStatus.TICKET_CREATED] state immediately,
+ * they might be left in the [RequestStatus.NEW] state if Jira is down.
+ */
+ private fun createTickets() {
+ storage.getRequests(RequestStatus.NEW).forEach {
+ try {
+ createTicket(it)
+ } catch (e: Exception) {
+ log.warn("There were errors while creating Jira tickets for request '${it.requestId}'", e)
+ }
+ }
+ }
+
+ private fun createTicket(signingRequest: CertificateSigningRequest) {
+ jiraClient.createRequestTicket(signingRequest.requestId, signingRequest.request)
+ storage.markRequestTicketCreated(signingRequest.requestId)
+ }
+}
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/LocalSigner.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/LocalSigner.kt
index 18343d7e44..aed84388c6 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/LocalSigner.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/LocalSigner.kt
@@ -1,48 +1,20 @@
package com.r3.corda.networkmanage.doorman.signer
import com.r3.corda.networkmanage.common.signer.Signer
-import com.r3.corda.networkmanage.common.utils.buildCertPath
import net.corda.core.crypto.Crypto
import net.corda.core.internal.DigitalSignatureWithCert
-import net.corda.nodeapi.internal.crypto.CertificateType
-import net.corda.nodeapi.internal.crypto.X509Utilities
-import org.bouncycastle.asn1.x509.GeneralName
-import org.bouncycastle.asn1.x509.GeneralSubtree
-import org.bouncycastle.asn1.x509.NameConstraints
-import org.bouncycastle.pkcs.PKCS10CertificationRequest
-import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
-import java.security.KeyPair
-import java.security.cert.CertPath
+import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
+import java.security.KeyStore
+import java.security.PrivateKey
import java.security.cert.X509Certificate
-import javax.security.auth.x500.X500Principal
/**
- * The [LocalSigner] class signs [PKCS10CertificationRequest] using provided CA key pair and certificate path.
- * This is intended to be used in testing environment where hardware signing module is not available.
+ * This local signer is intended to be used in testing environment where hardware signing module is not available.
*/
-//TODO Use a list instead of array
-class LocalSigner(private val signingKeyPair: KeyPair, private val signingCertPath: Array) : Signer {
- // TODO This doesn't belong in this class
- fun createSignedClientCertificate(certificationRequest: PKCS10CertificationRequest): CertPath {
- // The sub certs issued by the client must satisfy this directory name (or legal name in Corda) constraints, sub certs' directory name must be within client CA's name's subtree,
- // please see [sun.security.x509.X500Name.isWithinSubtree()] for more information.
- // We assume all attributes in the subject name has been checked prior approval.
- // TODO: add validation to subject name.
- val request = JcaPKCS10CertificationRequest(certificationRequest)
- val nameConstraints = NameConstraints(
- arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, request.subject))),
- arrayOf())
- val nodeCaCert = X509Utilities.createCertificate(
- CertificateType.NODE_CA,
- signingCertPath[0],
- signingKeyPair,
- X500Principal(request.subject.encoded),
- request.publicKey,
- nameConstraints = nameConstraints)
- return buildCertPath(nodeCaCert, *signingCertPath)
- }
+class LocalSigner(private val signingKey: PrivateKey, private val signingCert: X509Certificate) : Signer {
+ constructor(certAndKeyPair: CertificateAndKeyPair) : this(certAndKeyPair.keyPair.private, certAndKeyPair.certificate)
override fun signBytes(data: ByteArray): DigitalSignatureWithCert {
- return DigitalSignatureWithCert(signingCertPath[0], Crypto.doSign(signingKeyPair.private, data))
+ return DigitalSignatureWithCert(signingCert, Crypto.doSign(signingKey, data))
}
}
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NodeInfoWebService.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebService.kt
similarity index 90%
rename from network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NodeInfoWebService.kt
rename to network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebService.kt
index 57418b3d9c..cf881f2712 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NodeInfoWebService.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebService.kt
@@ -7,9 +7,8 @@ import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage
import com.r3.corda.networkmanage.common.persistence.NodeInfoWithSigned
import com.r3.corda.networkmanage.common.utils.SignedNetworkMap
-import com.r3.corda.networkmanage.common.utils.SignedNetworkParameters
import com.r3.corda.networkmanage.doorman.NetworkMapConfig
-import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService.Companion.NETWORK_MAP_PATH
+import com.r3.corda.networkmanage.doorman.webservice.NetworkMapWebService.Companion.NETWORK_MAP_PATH
import net.corda.core.crypto.SecureHash
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.deserialize
@@ -31,9 +30,9 @@ import javax.ws.rs.core.Response.ok
import javax.ws.rs.core.Response.status
@Path(NETWORK_MAP_PATH)
-class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage,
- private val networkMapStorage: NetworkMapStorage,
- private val config: NetworkMapConfig) {
+class NetworkMapWebService(private val nodeInfoStorage: NodeInfoStorage,
+ private val networkMapStorage: NetworkMapStorage,
+ private val config: NetworkMapConfig) {
companion object {
val log = contextLogger()
@@ -42,7 +41,7 @@ class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage,
private val networkMapCache: LoadingCache> = CacheBuilder.newBuilder()
.expireAfterWrite(config.cacheTimeout, TimeUnit.MILLISECONDS)
- .build(CacheLoader.from { _ -> Pair(networkMapStorage.getCurrentNetworkMap(), networkMapStorage.getCurrentSignedNetworkParameters()?.verified()) })
+ .build(CacheLoader.from { _ -> Pair(networkMapStorage.getCurrentNetworkMap(), networkMapStorage.getNetworkParametersOfNetworkMap()?.verified()) })
@POST
@Path("publish")
@@ -87,7 +86,6 @@ class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage,
@GET
@Path("my-ip")
fun myIp(@Context request: HttpServletRequest): Response {
- // TODO: Verify this returns IP correctly.
return ok(request.getHeader("X-Forwarded-For")?.split(",")?.first() ?: "${request.remoteHost}:${request.remotePort}").build()
}
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/Main.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/Main.kt
index 4e25900f68..a586ef5329 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/Main.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/Main.kt
@@ -1,7 +1,10 @@
package com.r3.corda.networkmanage.hsm
+import com.google.common.util.concurrent.MoreExecutors
+import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
import com.r3.corda.networkmanage.common.persistence.PersistentNetworkMapStorage
import com.r3.corda.networkmanage.common.persistence.configureDatabase
+import com.r3.corda.networkmanage.common.signer.NetworkMapSigner
import com.r3.corda.networkmanage.common.utils.ShowHelpException
import com.r3.corda.networkmanage.hsm.authentication.AuthMode
import com.r3.corda.networkmanage.hsm.authentication.Authenticator
@@ -16,10 +19,19 @@ import com.r3.corda.networkmanage.hsm.persistence.DBSignedCertificateRequestStor
import com.r3.corda.networkmanage.hsm.signer.HsmCsrSigner
import com.r3.corda.networkmanage.hsm.signer.HsmNetworkMapSigner
import com.r3.corda.networkmanage.hsm.utils.mapCryptoServerException
+import net.corda.core.utilities.minutes
+import org.apache.logging.log4j.LogManager
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.Security
+import java.time.Duration
+import java.util.concurrent.Executors
+import java.util.concurrent.ScheduledExecutorService
+import java.util.concurrent.TimeUnit.MILLISECONDS
+import java.util.concurrent.TimeUnit.SECONDS
import javax.crypto.Cipher
+private val log = LogManager.getLogger("com.r3.corda.networkmanage.hsm.Main")
+
fun main(args: Array) {
// Grabbed from https://stackoverflow.com/questions/7953567/checking-if-unlimited-cryptography-is-available
if (Cipher.getMaxAllowedKeyLength("AES") < 256) {
@@ -46,12 +58,15 @@ fun run(parameters: Parameters) {
checkNotNull(dataSourceProperties)
val database = configureDatabase(dataSourceProperties, databaseConfig)
val csrStorage = DBSignedCertificateRequestStorage(database)
- val networkMapStorage = PersistentNetworkMapStorage(database)
- val hsmNetworkMapSigningThread = HsmNetworkMapSigner(
- networkMapStorage,
+ val hsmSigner = HsmNetworkMapSigner(
networkMapCertificateName,
networkMapPrivateKeyPassword,
- Authenticator(createProvider(), AuthMode.KEY_FILE, autoUsername, authKeyFilePath, authKeyFilePassword, signAuthThreshold)).start()
+ Authenticator(createProvider(), AuthMode.KEY_FILE, autoUsername, authKeyFilePath, authKeyFilePassword, signAuthThreshold))
+
+ val networkMapStorage = PersistentNetworkMapStorage(database)
+ val scheduler = Executors.newSingleThreadScheduledExecutor()
+ startNetworkingMapSigningPolling(networkMapStorage, hsmSigner, scheduler, 10.minutes)
+
val sign: (List) -> Unit = {
val signer = HsmCsrSigner(
csrStorage,
@@ -62,6 +77,7 @@ fun run(parameters: Parameters) {
Authenticator(createProvider(), authMode, autoUsername, authKeyFilePath, authKeyFilePassword, signAuthThreshold))
signer.sign(it)
}
+
Menu().withExceptionHandler(::processError).addItem("1", "Generate root and intermediate certificates", {
if (confirmedKeyGen()) {
val generator = KeyCertificateGenerator(
@@ -104,13 +120,29 @@ fun run(parameters: Parameters) {
println("There is no approved and unsigned CSR")
}
}).showMenu()
- hsmNetworkMapSigningThread.stop()
+
+ MoreExecutors.shutdownAndAwaitTermination(scheduler, 30, SECONDS)
}
}
+private fun startNetworkingMapSigningPolling(networkMapStorage: NetworkMapStorage,
+ signer: HsmNetworkMapSigner,
+ executor: ScheduledExecutorService,
+ signingPeriod: Duration) {
+ val networkMapSigner = NetworkMapSigner(networkMapStorage, signer)
+ executor.scheduleAtFixedRate({
+ try {
+ networkMapSigner.signNetworkMap()
+ } catch (e: Exception) {
+ log.warn("Exception thrown while signing network map", e)
+ }
+ }, signingPeriod.toMillis(), signingPeriod.toMillis(), MILLISECONDS)
+}
+
private fun processError(exception: Exception) {
val processed = mapCryptoServerException(exception)
- System.err.println("An error occurred: ${processed.message}")
+ System.err.println("An error occurred:")
+ processed.printStackTrace()
}
private fun confirmedSign(selectedItems: List): Boolean {
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/HsmNetworkMapSigner.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/HsmNetworkMapSigner.kt
index 322233220f..aa0c4e6e17 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/HsmNetworkMapSigner.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/HsmNetworkMapSigner.kt
@@ -1,78 +1,39 @@
package com.r3.corda.networkmanage.hsm.signer
-import com.google.common.util.concurrent.MoreExecutors
-import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
-import com.r3.corda.networkmanage.common.signer.NetworkMapSigner
import com.r3.corda.networkmanage.common.signer.Signer
import com.r3.corda.networkmanage.hsm.authentication.Authenticator
import com.r3.corda.networkmanage.hsm.utils.X509Utilities
import com.r3.corda.networkmanage.hsm.utils.X509Utilities.getAndInitializeKeyStore
import com.r3.corda.networkmanage.hsm.utils.X509Utilities.verify
import net.corda.core.internal.DigitalSignatureWithCert
-import net.corda.core.utilities.loggerFor
-import net.corda.core.utilities.minutes
+import net.corda.nodeapi.internal.crypto.getX509Certificate
import java.security.PrivateKey
import java.security.Signature
-import java.security.cert.X509Certificate
-import java.time.Duration
-import java.util.concurrent.Executors
-import java.util.concurrent.ScheduledExecutorService
-import java.util.concurrent.TimeUnit
/**
- * Encapsulates logic for periodic network map signing execution.
- * It uses HSM as the signing entity with keys and certificates specified at the construction time.
+ * Signer which connects to a HSM using the given [authenticator] to sign bytes.
*/
// TODO Rename this to HsmSigner
-class HsmNetworkMapSigner(networkMapStorage: NetworkMapStorage,
- private val caCertificateKeyName: String,
- private val caPrivateKeyPass: String,
- private val authenticator: Authenticator,
- private val signingPeriod: Duration = DEFAULT_SIGNING_PERIOD_MS) : Signer {
-
- companion object {
- val log = loggerFor()
- val DEFAULT_SIGNING_PERIOD_MS = 10.minutes
-
- private val TERMINATION_TIMEOUT_SEC = 2L
- }
-
- private val networkMapSigner = NetworkMapSigner(networkMapStorage, this)
- private lateinit var scheduledExecutor: ScheduledExecutorService
-
- // TODO This doesn't belong in this class
- fun start(): HsmNetworkMapSigner {
- val signingPeriodMillis = signingPeriod.toMillis()
- scheduledExecutor = Executors.newSingleThreadScheduledExecutor()
- scheduledExecutor.scheduleAtFixedRate({
- try {
- networkMapSigner.signNetworkMap()
- } catch (exception: Exception) {
- log.warn("Exception thrown while signing network map", exception)
- }
- }, signingPeriodMillis, signingPeriodMillis, TimeUnit.MILLISECONDS)
- return this
- }
-
- fun stop() {
- MoreExecutors.shutdownAndAwaitTermination(scheduledExecutor, TERMINATION_TIMEOUT_SEC, TimeUnit.SECONDS)
- }
-
+class HsmNetworkMapSigner(private val certificateKeyName: String,
+ private val privateKeyPassword: String,
+ private val authenticator: Authenticator) : Signer {
/**
* Signs given data using [CryptoServerJCE.CryptoServerProvider], which connects to the underlying HSM.
*/
override fun signBytes(data: ByteArray): DigitalSignatureWithCert {
return authenticator.connectAndAuthenticate { provider, _ ->
val keyStore = getAndInitializeKeyStore(provider)
- val caCertificateChain = keyStore.getCertificateChain(caCertificateKeyName)
- val caKey = keyStore.getKey(caCertificateKeyName, caPrivateKeyPass.toCharArray()) as PrivateKey
+ val certificate = keyStore.getX509Certificate(certificateKeyName)
+ // Don't worry this is not a real private key but a pointer to one that resides in the HSM. It only works
+ // when used with the given provider.
+ val key = keyStore.getKey(certificateKeyName, privateKeyPassword.toCharArray()) as PrivateKey
val signature = Signature.getInstance(X509Utilities.SIGNATURE_ALGORITHM, provider).run {
- initSign(caKey)
+ initSign(key)
update(data)
sign()
}
- verify(data, signature, caCertificateChain[0].publicKey)
- DigitalSignatureWithCert(caCertificateChain[0] as X509Certificate, signature)
+ verify(data, signature, certificate.publicKey)
+ DigitalSignatureWithCert(certificate, signature)
}
}
}
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/utils/HsmErrors.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/utils/HsmErrors.kt
index 10ecec3ee1..9a017b5675 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/utils/HsmErrors.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/utils/HsmErrors.kt
@@ -33,9 +33,9 @@ object HsmErrors {
fun mapCryptoServerException(exception: Exception): Exception {
// Try to decode the error code
val crypto = exception as? CryptoServerException ?: exception.cause as? CryptoServerException
- if (crypto != null) {
- return Exception("(CryptoServer) ${HsmErrors.errors[crypto.ErrorCode]}", exception)
+ return if (crypto != null) {
+ Exception("(CryptoServer) ${HsmErrors.errors[crypto.ErrorCode]}", exception)
} else {
- return exception
+ exception
}
}
\ No newline at end of file
diff --git a/network-management/src/main/resources/migration/network-manager.changelog-init.xml b/network-management/src/main/resources/migration/network-manager.changelog-init.xml
index 0b9be92b91..59a5f4985e 100644
--- a/network-management/src/main/resources/migration/network-manager.changelog-init.xml
+++ b/network-management/src/main/resources/migration/network-manager.changelog-init.xml
@@ -93,13 +93,11 @@
-
+
-
-
-
-
+
+
@@ -122,7 +120,7 @@
-
+
@@ -169,11 +167,6 @@
-
-
-
-
-
+
+
diff --git a/network-management/src/main/resources/migration/network-manager.changelog-pub-key-move.xml b/network-management/src/main/resources/migration/network-manager.changelog-pub-key-move.xml
new file mode 100644
index 0000000000..3e5625b441
--- /dev/null
+++ b/network-management/src/main/resources/migration/network-manager.changelog-pub-key-move.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/network-management/src/main/resources/migration/network-manager.changelog-signing-network-params.xml b/network-management/src/main/resources/migration/network-manager.changelog-signing-network-params.xml
new file mode 100644
index 0000000000..cdb43a5362
--- /dev/null
+++ b/network-management/src/main/resources/migration/network-manager.changelog-signing-network-params.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/TestBase.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/TestBase.kt
index a7e37bc829..abc8b28401 100644
--- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/TestBase.kt
+++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/TestBase.kt
@@ -20,6 +20,7 @@ abstract class TestBase {
requestId: String = SecureHash.randomSHA256().toString(),
status: RequestStatus = RequestStatus.NEW,
legalName: String = "TestLegalName",
+ publicKeyHash: SecureHash = SecureHash.randomSHA256(),
remark: String = "Test remark",
request: PKCS10CertificationRequest = mock(),
certData: CertificateData = mock(),
@@ -29,6 +30,7 @@ abstract class TestBase {
requestId = requestId,
status = status,
legalName = legalName,
+ publicKeyHash = publicKeyHash,
remark = remark,
certData = certData,
request = request,
@@ -36,11 +38,9 @@ abstract class TestBase {
)
}
- protected fun certificateData(publicKeyHash: String = SecureHash.randomSHA256().toString(),
- certStatus: CertificateStatus = CertificateStatus.VALID,
+ protected fun certificateData(certStatus: CertificateStatus = CertificateStatus.VALID,
certPath: CertPath = mock()): CertificateData {
return CertificateData(
- publicKeyHash = publicKeyHash,
certStatus = certStatus,
certPath = certPath
)
diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorageTest.kt
index ec16998e8f..cfe2541c58 100644
--- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorageTest.kt
+++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorageTest.kt
@@ -19,6 +19,7 @@ import org.junit.After
import org.junit.Before
import org.junit.Test
import java.security.KeyPair
+import java.security.cert.CertPath
import java.util.*
import javax.security.auth.x500.X500Principal
import kotlin.test.*
@@ -69,7 +70,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
@Test
fun `sign request`() {
- val (csr, _) = createRequest("LegalName")
+ val (csr, nodeKeyPair) = createRequest("LegalName")
// Add request to DB.
val requestId = storage.saveRequest(csr)
// New request should equals to 1.
@@ -86,11 +87,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
// Sign certificate
storage.putCertificatePath(
requestId,
- JcaPKCS10CertificationRequest(csr).run {
- // TODO We need a utility in InternalUtils for converting X500Name -> CordaX500Name
- val (rootCa, intermediateCa, nodeCa) = createDevNodeCaCertPath(CordaX500Name.build(X500Principal(subject.encoded)))
- buildCertPath(nodeCa.certificate, intermediateCa.certificate, rootCa.certificate)
- },
+ generateSignedCertPath(csr, nodeKeyPair),
listOf(DOORMAN_SIGNATURE)
)
// Check request is ready
@@ -99,33 +96,51 @@ class PersistentCertificateRequestStorageTest : TestBase() {
@Test
fun `sign request ignores subsequent sign requests`() {
- val (csr, _) = createRequest("LegalName")
+ val (csr, nodeKeyPair) = createRequest("LegalName")
// Add request to DB.
val requestId = storage.saveRequest(csr)
// Store certificate to DB.
storage.markRequestTicketCreated(requestId)
storage.approveRequest(requestId, DOORMAN_SIGNATURE)
+ // Sign certificate
storage.putCertificatePath(
requestId,
- JcaPKCS10CertificationRequest(csr).run {
- val (rootCa, intermediateCa, nodeCa) = createDevNodeCaCertPath(CordaX500Name.build(X500Principal(subject.encoded)))
- buildCertPath(nodeCa.certificate, intermediateCa.certificate, rootCa.certificate)
- },
+ generateSignedCertPath(csr, nodeKeyPair),
listOf(DOORMAN_SIGNATURE)
)
- // Sign certificate
// When subsequent signature requested
assertFailsWith(IllegalArgumentException::class) {
storage.putCertificatePath(
requestId,
- JcaPKCS10CertificationRequest(csr).run {
- val (rootCa, intermediateCa, nodeCa) = createDevNodeCaCertPath(CordaX500Name.build(X500Principal(subject.encoded)))
- buildCertPath(nodeCa.certificate, intermediateCa.certificate, rootCa.certificate)
- },
+ generateSignedCertPath(csr, nodeKeyPair),
listOf(DOORMAN_SIGNATURE))
}
}
+ @Test
+ fun `sign request rejects requests with the same public key`() {
+ val (csr, nodeKeyPair) = createRequest("LegalName")
+ // Add request to DB.
+ val requestId = storage.saveRequest(csr)
+ // Store certificate to DB.
+ storage.markRequestTicketCreated(requestId)
+ storage.approveRequest(requestId, DOORMAN_SIGNATURE)
+ // Sign certificate
+ storage.putCertificatePath(
+ requestId,
+ generateSignedCertPath(csr, nodeKeyPair),
+ listOf(DOORMAN_SIGNATURE)
+ )
+ // Sign certificate
+ // When request with the same public key is requested
+ val (newCsr, _) = createRequest("NewLegalName", nodeKeyPair)
+ val duplicateRequestId = storage.saveRequest(newCsr)
+ assertThat(storage.getRequests(RequestStatus.NEW)).isEmpty()
+ val duplicateRequest = storage.getRequest(duplicateRequestId)
+ assertThat(duplicateRequest!!.status).isEqualTo(RequestStatus.REJECTED)
+ assertThat(duplicateRequest.remark).isEqualTo("Duplicate public key")
+ }
+
@Test
fun `reject request`() {
val requestId = storage.saveRequest(createRequest("BankA").first)
@@ -159,18 +174,14 @@ class PersistentCertificateRequestStorageTest : TestBase() {
@Test
fun `request with the same legal name as a previously signed request`() {
- val csr = createRequest("BankA").first
+ val (csr, nodeKeyPair) = createRequest("BankA")
val requestId = storage.saveRequest(csr)
storage.markRequestTicketCreated(requestId)
storage.approveRequest(requestId, DOORMAN_SIGNATURE)
// Sign certificate
storage.putCertificatePath(
requestId,
- JcaPKCS10CertificationRequest(csr).run {
- // TODO We need a utility in InternalUtils for converting X500Name -> CordaX500Name
- val (rootCa, intermediateCa, nodeCa) = createDevNodeCaCertPath(CordaX500Name.build(X500Principal(subject.encoded)))
- buildCertPath(nodeCa.certificate, intermediateCa.certificate, rootCa.certificate)
- },
+ generateSignedCertPath(csr, nodeKeyPair),
listOf(DOORMAN_SIGNATURE)
)
val rejectedRequestId = storage.saveRequest(createRequest("BankA").first)
@@ -215,6 +226,14 @@ class PersistentCertificateRequestStorageTest : TestBase() {
}
}
+ private fun generateSignedCertPath(csr: PKCS10CertificationRequest, keyPair: KeyPair): CertPath {
+ return JcaPKCS10CertificationRequest(csr).run {
+ // TODO We need a utility in InternalUtils for converting X500Name -> CordaX500Name
+ val (rootCa, intermediateCa, nodeCa) = createDevNodeCaCertPath(CordaX500Name.build(X500Principal(subject.encoded)), keyPair)
+ buildCertPath(nodeCa.certificate, intermediateCa.certificate, rootCa.certificate)
+ }
+ }
+
private fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().toString()): Properties {
val props = Properties()
props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource")
@@ -225,8 +244,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
}
}
-internal fun createRequest(organisation: String): Pair {
- val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
+internal fun createRequest(organisation: String, keyPair: KeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)): Pair {
val request = X509Utilities.createCertificateSigningRequest(X500Principal("O=$organisation,L=London,C=GB"), "my@mail.com", keyPair)
return Pair(request, keyPair)
-}
+}
\ No newline at end of file
diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorageTest.kt
index af75cf07b7..cbc79e1902 100644
--- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorageTest.kt
+++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorageTest.kt
@@ -5,13 +5,11 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.signWithCert
import net.corda.nodeapi.internal.createDevNetworkMapCa
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
-import net.corda.nodeapi.internal.crypto.X509CertificateFactory
import net.corda.nodeapi.internal.network.NetworkMap
import net.corda.nodeapi.internal.network.verifiedNetworkMapCert
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.common.internal.testNetworkParameters
-import net.corda.testing.internal.TestNodeInfoBuilder
import net.corda.testing.internal.createDevIntermediateCaCertPath
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import org.assertj.core.api.Assertions.assertThat
@@ -50,7 +48,7 @@ class PersistentNetworkMapStorageTest : TestBase() {
fun `saveNetworkMap and saveNetworkParameters create current network map and parameters`() {
// given
// Create node info.
- val signedNodeInfo = createValidSignedNodeInfo("Test")
+ val (signedNodeInfo) = createValidSignedNodeInfo("Test", requestStorage)
val nodeInfoHash = nodeInfoStorage.putNodeInfo(signedNodeInfo)
val networkParameters = testNetworkParameters(emptyList())
@@ -65,7 +63,7 @@ class PersistentNetworkMapStorageTest : TestBase() {
// then
val persistedSignedNetworkMap = networkMapStorage.getCurrentNetworkMap()
- val persistedSignedParameters = networkMapStorage.getCurrentSignedNetworkParameters()
+ val persistedSignedParameters = networkMapStorage.getNetworkParametersOfNetworkMap()
assertEquals(networkParameters, persistedSignedParameters?.verifiedNetworkMapCert(rootCaCert))
assertEquals(parametersSignature, persistedSignedParameters?.sig)
@@ -84,13 +82,13 @@ class PersistentNetworkMapStorageTest : TestBase() {
networkMapStorage.saveNetworkParameters(params2, null)
// when
- val latest = networkMapStorage.getLatestUnsignedNetworkParameters()
+ val latest = networkMapStorage.getLatestNetworkParameters()?.minimumPlatformVersion
// then
- assertEquals(2, latest.minimumPlatformVersion)
+ assertEquals(2, latest)
}
@Test
- fun `getCurrentNetworkParameters returns current network map parameters`() {
+ fun `getNetworkParametersOfNetworkMap returns current network map parameters`() {
// given
// Create network parameters
val testParameters1 = testNetworkParameters(emptyList())
@@ -107,7 +105,7 @@ class PersistentNetworkMapStorageTest : TestBase() {
networkMapStorage.saveNetworkParameters(testParameters2, testParameters2.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate).sig)
// when
- val result = networkMapStorage.getCurrentSignedNetworkParameters()?.verifiedNetworkMapCert(rootCaCert)
+ val result = networkMapStorage.getNetworkParametersOfNetworkMap()?.verifiedNetworkMapCert(rootCaCert)
// then
assertEquals(1, result?.minimumPlatformVersion)
@@ -117,8 +115,8 @@ class PersistentNetworkMapStorageTest : TestBase() {
fun `getValidNodeInfoHashes returns only valid and signed node info hashes`() {
// given
// Create node infos.
- val signedNodeInfoA = createValidSignedNodeInfo("TestA")
- val signedNodeInfoB = createValidSignedNodeInfo("TestB")
+ val (signedNodeInfoA) = createValidSignedNodeInfo("TestA", requestStorage)
+ val (signedNodeInfoB) = createValidSignedNodeInfo("TestB", requestStorage)
// Put signed node info data
val nodeInfoHashA = nodeInfoStorage.putNodeInfo(signedNodeInfoA)
@@ -139,15 +137,4 @@ class PersistentNetworkMapStorageTest : TestBase() {
// then
assertThat(validNodeInfoHash).containsOnly(nodeInfoHashA, nodeInfoHashB)
}
-
- private fun createValidSignedNodeInfo(organisation: String): NodeInfoWithSigned {
- val nodeInfoBuilder = TestNodeInfoBuilder()
- val requestId = requestStorage.saveRequest(createRequest(organisation).first)
- requestStorage.markRequestTicketCreated(requestId)
- requestStorage.approveRequest(requestId, "TestUser")
- val (identity) = nodeInfoBuilder.addIdentity(CordaX500Name(organisation, "London", "GB"))
- val nodeCaCertPath = X509CertificateFactory().generateCertPath(identity.certPath.certificates.drop(1))
- requestStorage.putCertificatePath(requestId, nodeCaCertPath, emptyList())
- return NodeInfoWithSigned(nodeInfoBuilder.buildWithSigned().second)
- }
}
\ No newline at end of file
diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorageTest.kt
index 01eba0be7a..47260c73d1 100644
--- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorageTest.kt
+++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorageTest.kt
@@ -9,7 +9,6 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.serialization.serialize
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.nodeapi.internal.crypto.CertificateType
-import net.corda.nodeapi.internal.crypto.X509CertificateFactory
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
@@ -23,6 +22,7 @@ import org.junit.Before
import org.junit.Test
import java.security.PrivateKey
import java.security.cert.X509Certificate
+import javax.security.auth.x500.X500Principal
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
@@ -83,8 +83,8 @@ class PersistentNodeInfoStorageTest : TestBase() {
@Test
fun `getNodeInfo returns persisted SignedNodeInfo using the hash of just the NodeInfo`() {
// given
- val (nodeA) = createValidSignedNodeInfo("TestA")
- val (nodeB) = createValidSignedNodeInfo("TestB")
+ val (nodeA) = createValidSignedNodeInfo("TestA", requestStorage)
+ val (nodeB) = createValidSignedNodeInfo("TestB", requestStorage)
// Put signed node info data
nodeInfoStorage.putNodeInfo(nodeA)
@@ -102,7 +102,7 @@ class PersistentNodeInfoStorageTest : TestBase() {
@Test
fun `same public key with different node info`() {
// Create node info.
- val (node1, key) = createValidSignedNodeInfo("Test", serial = 1)
+ val (node1, key) = createValidSignedNodeInfo("Test", requestStorage)
val nodeInfo2 = node1.nodeInfo.copy(serial = 2)
val node2 = NodeInfoWithSigned(nodeInfo2.signWith(listOf(key)))
@@ -120,7 +120,7 @@ class PersistentNodeInfoStorageTest : TestBase() {
@Test
fun `putNodeInfo persists SignedNodeInfo with its signature`() {
// given
- val (nodeInfoWithSigned) = createValidSignedNodeInfo("Test")
+ val (nodeInfoWithSigned) = createValidSignedNodeInfo("Test", requestStorage)
// when
val nodeInfoHash = nodeInfoStorage.putNodeInfo(nodeInfoWithSigned)
@@ -129,16 +129,17 @@ class PersistentNodeInfoStorageTest : TestBase() {
val persistedSignedNodeInfo = nodeInfoStorage.getNodeInfo(nodeInfoHash)
assertThat(persistedSignedNodeInfo?.signatures).isEqualTo(nodeInfoWithSigned.signedNodeInfo.signatures)
}
+}
- private fun createValidSignedNodeInfo(organisation: String, serial: Long = 1): Pair {
- val nodeInfoBuilder = TestNodeInfoBuilder()
- val requestId = requestStorage.saveRequest(createRequest(organisation).first)
- requestStorage.markRequestTicketCreated(requestId)
- requestStorage.approveRequest(requestId, "TestUser")
- val (identity, key) = nodeInfoBuilder.addIdentity(CordaX500Name(organisation, "London", "GB"))
- val nodeCaCertPath = X509CertificateFactory().generateCertPath(identity.certPath.certificates.drop(1))
- requestStorage.putCertificatePath(requestId, nodeCaCertPath, emptyList())
- val (_, signedNodeInfo) = nodeInfoBuilder.buildWithSigned(serial)
- return Pair(NodeInfoWithSigned(signedNodeInfo), key)
- }
+internal fun createValidSignedNodeInfo(organisation: String,
+ storage: CertificationRequestStorage): Pair {
+ val (csr, nodeKeyPair) = createRequest(organisation)
+ val requestId = storage.saveRequest(csr)
+ storage.markRequestTicketCreated(requestId)
+ storage.approveRequest(requestId, "TestUser")
+ val nodeInfoBuilder = TestNodeInfoBuilder()
+ val (identity, key) = nodeInfoBuilder.addIdentity(CordaX500Name.build(X500Principal(csr.subject.encoded)), nodeKeyPair)
+ storage.putCertificatePath(requestId, identity.certPath, listOf("Test"))
+ val (_, signedNodeInfo) = nodeInfoBuilder.buildWithSigned(1)
+ return Pair(NodeInfoWithSigned(signedNodeInfo), key)
}
\ No newline at end of file
diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSignerTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSignerTest.kt
index dc29826d1b..25d9f7e7f5 100644
--- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSignerTest.kt
+++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSignerTest.kt
@@ -51,8 +51,8 @@ class NetworkMapSignerTest : TestBase() {
val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(signedNetworkMap)
whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(signedNodeInfoHashes)
- whenever(networkMapStorage.getLatestUnsignedNetworkParameters()).thenReturn(latestNetworkParameters)
- whenever(networkMapStorage.getCurrentSignedNetworkParameters()).thenReturn(currentParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate))
+ whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(latestNetworkParameters)
+ whenever(networkMapStorage.getNetworkParametersOfNetworkMap()).thenReturn(currentParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate))
whenever(signer.signBytes(any())).then {
DigitalSignatureWithCert(networkMapCa.certificate, Crypto.doSign(networkMapCa.keyPair.private, it.arguments[0] as ByteArray))
}
@@ -67,8 +67,8 @@ class NetworkMapSignerTest : TestBase() {
// then
// Verify networkMapStorage calls
verify(networkMapStorage).getNodeInfoHashes(any())
- verify(networkMapStorage).getLatestUnsignedNetworkParameters()
- verify(networkMapStorage).getCurrentSignedNetworkParameters()
+ verify(networkMapStorage).getLatestNetworkParameters()
+ verify(networkMapStorage).getNetworkParametersOfNetworkMap()
argumentCaptor().apply {
verify(networkMapStorage).saveNetworkMap(capture())
val capturedNetworkMap = firstValue.verifiedNetworkMapCert(rootCaCert)
@@ -87,8 +87,8 @@ class NetworkMapSignerTest : TestBase() {
val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(signedNetworkMap)
whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(emptyList())
- whenever(networkMapStorage.getLatestUnsignedNetworkParameters()).thenReturn(networkParameters)
- whenever(networkMapStorage.getCurrentSignedNetworkParameters()).thenReturn(networkParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate))
+ whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(networkParameters)
+ whenever(networkMapStorage.getNetworkParametersOfNetworkMap()).thenReturn(networkParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate))
// when
networkMapSigner.signNetworkMap()
@@ -104,7 +104,7 @@ class NetworkMapSignerTest : TestBase() {
val networkParameters = testNetworkParameters(emptyList())
whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(null)
whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(emptyList())
- whenever(networkMapStorage.getLatestUnsignedNetworkParameters()).thenReturn(networkParameters)
+ whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(networkParameters)
whenever(signer.signBytes(any())).then {
DigitalSignatureWithCert(networkMapCa.certificate, Crypto.doSign(networkMapCa.keyPair.private, it.arguments[0] as ByteArray))
}
@@ -118,7 +118,7 @@ class NetworkMapSignerTest : TestBase() {
// then
// Verify networkMapStorage calls
verify(networkMapStorage).getNodeInfoHashes(any())
- verify(networkMapStorage).getLatestUnsignedNetworkParameters()
+ verify(networkMapStorage).getLatestNetworkParameters()
argumentCaptor().apply {
verify(networkMapStorage).saveNetworkMap(capture())
val networkMap = firstValue.verifiedNetworkMapCert(rootCaCert)
diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DefaultRequestProcessorTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DefaultRequestProcessorTest.kt
deleted file mode 100644
index b0091a4cf6..0000000000
--- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DefaultRequestProcessorTest.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-package com.r3.corda.networkmanage.doorman
-
-import com.nhaarman.mockito_kotlin.any
-import com.nhaarman.mockito_kotlin.mock
-import com.nhaarman.mockito_kotlin.times
-import com.nhaarman.mockito_kotlin.verify
-import com.r3.corda.networkmanage.TestBase
-import com.r3.corda.networkmanage.common.persistence.CertificateResponse
-import com.r3.corda.networkmanage.common.persistence.CertificateStatus
-import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage
-import com.r3.corda.networkmanage.common.persistence.RequestStatus
-import com.r3.corda.networkmanage.common.utils.buildCertPath
-import com.r3.corda.networkmanage.doorman.signer.DefaultCsrHandler
-import com.r3.corda.networkmanage.doorman.signer.LocalSigner
-import net.corda.core.crypto.Crypto
-import net.corda.nodeapi.internal.crypto.X509Utilities
-import org.junit.Test
-import javax.security.auth.x500.X500Principal
-import kotlin.test.assertEquals
-
-class DefaultRequestProcessorTest : TestBase() {
- @Test
- fun `get response`() {
- val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
- val cert = X509Utilities.createSelfSignedCACertificate(X500Principal("O=Test,L=London,C=GB"), keyPair)
-
- val requestStorage: CertificationRequestStorage = mock {
- on { getRequest("New") }.thenReturn(certificateSigningRequest())
- on { getRequest("Signed") }.thenReturn(certificateSigningRequest(status = RequestStatus.SIGNED, certData = certificateData("", CertificateStatus.VALID, buildCertPath(cert))))
- on { getRequest("Rejected") }.thenReturn(certificateSigningRequest(status = RequestStatus.REJECTED, remark = "Random reason"))
- }
- val signer: LocalSigner = mock()
- val requestProcessor = DefaultCsrHandler(requestStorage, signer)
-
- assertEquals(CertificateResponse.NotReady, requestProcessor.getResponse("random"))
- assertEquals(CertificateResponse.NotReady, requestProcessor.getResponse("New"))
- assertEquals(CertificateResponse.Ready(buildCertPath(cert)), requestProcessor.getResponse("Signed"))
- assertEquals(CertificateResponse.Unauthorised("Random reason"), requestProcessor.getResponse("Rejected"))
- }
-
- @Test
- fun `process request`() {
- val (request1, request2, request3) = (1..3).map {
- X509Utilities.createCertificateSigningRequest(X500Principal("O=Test1,L=London,C=GB"), "my@email.com", Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME))
- }
-
- val requestStorage: CertificationRequestStorage = mock {
- on { getRequests(RequestStatus.APPROVED) }.thenReturn(listOf(
- certificateSigningRequest(requestId = "1", request = request1, status = RequestStatus.APPROVED),
- certificateSigningRequest(requestId = "2", request = request2, status = RequestStatus.APPROVED),
- certificateSigningRequest(requestId = "3", request = request3, status = RequestStatus.APPROVED)
- ))
- }
- val signer: LocalSigner = mock()
- val requestProcessor = DefaultCsrHandler(requestStorage, signer)
-
- requestProcessor.processApprovedRequests()
-
- verify(signer, times(3)).createSignedClientCertificate(any())
- verify(requestStorage, times(1)).getRequests(any())
- }
-}
diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DoormanParametersTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DoormanParametersTest.kt
index d2b92b752a..d44ff5304a 100644
--- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DoormanParametersTest.kt
+++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DoormanParametersTest.kt
@@ -52,6 +52,5 @@ class DoormanParametersTest {
assertEquals("TD", parameter.jiraConfig?.projectCode)
assertEquals("username", parameter.jiraConfig?.username)
assertEquals("password", parameter.jiraConfig?.password)
- assertEquals(41, parameter.jiraConfig?.doneTransitionCode)
}
}
diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/JiraClientTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/JiraClientTest.kt
new file mode 100644
index 0000000000..d3558d0c6e
--- /dev/null
+++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/JiraClientTest.kt
@@ -0,0 +1,55 @@
+package com.r3.corda.networkmanage.doorman
+
+import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory
+import com.r3.corda.networkmanage.common.utils.buildCertPath
+import net.corda.core.crypto.Crypto
+import net.corda.core.crypto.SecureHash
+import net.corda.core.identity.CordaX500Name
+import net.corda.nodeapi.internal.crypto.X509Utilities
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Test
+import java.net.URI
+
+@Ignore
+// This is manual test for testing Jira API.
+class JiraClientTest {
+ private lateinit var jiraClient: JiraClient
+ @Before
+ fun init() {
+ val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI("http://jira.url.com"), "username", "password")
+ jiraClient = JiraClient(jiraWebAPI, "DOOR")
+ }
+
+ @Test
+ fun createRequestTicket() {
+ val request = X509Utilities.createCertificateSigningRequest(CordaX500Name("JiraAPITest", "R3 Ltd 3", "London", "GB").x500Principal, "test@test.com", Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME))
+ jiraClient.createRequestTicket(SecureHash.randomSHA256().toString(), request)
+ }
+
+ @Test
+ fun getApprovedRequests() {
+ jiraClient.getApprovedRequests().forEach { println(it) }
+ }
+
+ @Test
+ fun getRejectedRequests() {
+ val requests = jiraClient.getRejectedRequests()
+ requests.forEach { println(it) }
+ }
+
+ @Test
+ fun updateSignedRequests() {
+ val requests = jiraClient.getApprovedRequests()
+ val selfSignedCA = X509Utilities.createSelfSignedCACertificate(CordaX500Name("test", "london", "GB").x500Principal, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME))
+ jiraClient.updateSignedRequests(requests.map { it.requestId to buildCertPath(selfSignedCA) }.toMap())
+ }
+
+ @Test
+ fun updateRejectedRequests() {
+ val requests = jiraClient.getRejectedRequests()
+ jiraClient.updateRejectedRequests(requests.map { it.requestId })
+
+ assert(jiraClient.getRejectedRequests().isEmpty())
+ }
+}
diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandlerTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandlerTest.kt
deleted file mode 100644
index 42314f4b38..0000000000
--- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandlerTest.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-package com.r3.corda.networkmanage.doorman.signer
-
-import com.nhaarman.mockito_kotlin.*
-import com.r3.corda.networkmanage.common.persistence.CertificateResponse
-import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest
-import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage
-import com.r3.corda.networkmanage.common.persistence.RequestStatus
-import com.r3.corda.networkmanage.doorman.JiraClient
-import net.corda.core.crypto.Crypto
-import net.corda.core.identity.CordaX500Name
-import net.corda.nodeapi.internal.crypto.X509Utilities
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.mockito.Mock
-import org.mockito.junit.MockitoJUnit
-import java.security.cert.CertPath
-
-class JiraCsrHandlerTest {
-
- @Rule
- @JvmField
- val mockitoRule = MockitoJUnit.rule()
-
- @Mock
- lateinit var jiraClient: JiraClient
-
- @Mock
- lateinit var certificationRequestStorage: CertificationRequestStorage
-
- @Mock
- lateinit var defaultCsrHandler: DefaultCsrHandler
-
- @Mock
- var certPath: CertPath = mock()
-
- private lateinit var jiraCsrHandler: JiraCsrHandler
- private val requestId = "id"
- private lateinit var certificateResponse: CertificateResponse.Ready
-
- private val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
- private val pkcS10CertificationRequest = X509Utilities.createCertificateSigningRequest(
- CordaX500Name(locality = "London", organisation = "LegalName", country = "GB").x500Principal,
- "my@mail.com",
- keyPair)
-
- @Before
- fun setup() {
- jiraCsrHandler = JiraCsrHandler(jiraClient, certificationRequestStorage, defaultCsrHandler)
- certificateResponse = CertificateResponse.Ready(certPath)
- }
-
- @Test
- fun `If jira connection fails we don't mark the ticket as created`() {
- whenever(defaultCsrHandler.saveRequest(any())).thenReturn(requestId)
- whenever(defaultCsrHandler.getResponse(requestId)).thenReturn(certificateResponse)
- whenever(jiraClient.createRequestTicket(eq(requestId), any())).thenThrow(IllegalStateException("something broke"))
-
- // Test
- jiraCsrHandler.saveRequest(pkcS10CertificationRequest)
-
- verify(certificationRequestStorage, never()).markRequestTicketCreated(requestId)
- }
-
- @Test
- fun `If jira connection works we mark the ticket as created`() {
- whenever(defaultCsrHandler.saveRequest(any())).thenReturn(requestId)
- whenever(defaultCsrHandler.getResponse(requestId)).thenReturn(certificateResponse)
-
- // Test
- jiraCsrHandler.saveRequest(pkcS10CertificationRequest)
-
- verify(certificationRequestStorage, times(1)).markRequestTicketCreated(requestId)
- }
-
- @Test
- fun `create tickets`() {
- val csr = CertificateSigningRequest(requestId, "name", RequestStatus.NEW, pkcS10CertificationRequest, null, emptyList(), null)
- whenever(certificationRequestStorage.getRequests(RequestStatus.NEW)).thenReturn(listOf(csr))
-
- // Test
- jiraCsrHandler.createTickets()
-
- verify(jiraClient).createRequestTicket(requestId, csr.request)
- verify(certificationRequestStorage).markRequestTicketCreated(requestId)
- }
-}
\ No newline at end of file
diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt
new file mode 100644
index 0000000000..221d7aec11
--- /dev/null
+++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt
@@ -0,0 +1,85 @@
+package com.r3.corda.networkmanage.doorman.signer
+
+import com.nhaarman.mockito_kotlin.*
+import com.r3.corda.networkmanage.TestBase
+import com.r3.corda.networkmanage.common.persistence.CertificateResponse
+import com.r3.corda.networkmanage.common.persistence.CertificateStatus
+import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage
+import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE
+import com.r3.corda.networkmanage.common.persistence.RequestStatus
+import com.r3.corda.networkmanage.common.utils.CertPathAndKey
+import com.r3.corda.networkmanage.common.utils.buildCertPath
+import net.corda.core.crypto.Crypto
+import net.corda.core.internal.CertRole
+import net.corda.nodeapi.internal.crypto.X509Utilities
+import net.corda.testing.internal.createDevIntermediateCaCertPath
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+import java.security.cert.CertPath
+import java.security.cert.X509Certificate
+import javax.security.auth.x500.X500Principal
+import kotlin.test.assertEquals
+
+class DefaultCsrHandlerTest : TestBase() {
+ @Test
+ fun getResponse() {
+ val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
+ val cert = X509Utilities.createSelfSignedCACertificate(X500Principal("O=Test,L=London,C=GB"), keyPair)
+
+ val requestStorage: CertificationRequestStorage = mock {
+ on { getRequest("New") }.thenReturn(certificateSigningRequest())
+ on { getRequest("Signed") }.thenReturn(certificateSigningRequest(status = RequestStatus.SIGNED, certData = certificateData(CertificateStatus.VALID, buildCertPath(cert))))
+ on { getRequest("Rejected") }.thenReturn(certificateSigningRequest(status = RequestStatus.REJECTED, remark = "Random reason"))
+ }
+ val requestProcessor = DefaultCsrHandler(requestStorage, null)
+
+ assertEquals(CertificateResponse.NotReady, requestProcessor.getResponse("random"))
+ assertEquals(CertificateResponse.NotReady, requestProcessor.getResponse("New"))
+ assertEquals(CertificateResponse.Ready(buildCertPath(cert)), requestProcessor.getResponse("Signed"))
+ assertEquals(CertificateResponse.Unauthorised("Random reason"), requestProcessor.getResponse("Rejected"))
+ }
+
+ @Test
+ fun processApprovedRequests() {
+ val requests = (1..3).map {
+ X509Utilities.createCertificateSigningRequest(
+ X500Principal("O=Test$it,L=London,C=GB"),
+ "my@email.com",
+ Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME))
+ }
+
+ val requestStorage: CertificationRequestStorage = mock {
+ on { getRequests(RequestStatus.APPROVED) }.thenReturn(listOf(
+ certificateSigningRequest(requestId = "1", request = requests[0], status = RequestStatus.APPROVED),
+ certificateSigningRequest(requestId = "2", request = requests[1], status = RequestStatus.APPROVED)
+ ))
+ on { getRequests(RequestStatus.REJECTED) }.thenReturn(listOf(
+ certificateSigningRequest(requestId = "3", request = requests[2], status = RequestStatus.REJECTED)
+ ))
+ }
+
+ val (rootCa, csrCa) = createDevIntermediateCaCertPath()
+ val csrCertPathAndKey = CertPathAndKey(listOf(csrCa.certificate, rootCa.certificate), csrCa.keyPair.private)
+ val requestProcessor = DefaultCsrHandler(requestStorage, csrCertPathAndKey)
+
+ requestProcessor.processRequests()
+
+ val certPathCapture = argumentCaptor()
+
+ // Verify only the approved requests are taken
+ verify(requestStorage, times(1)).getRequests(RequestStatus.APPROVED)
+ verify(requestStorage, times(1)).putCertificatePath(eq("1"), certPathCapture.capture(), eq(listOf(DOORMAN_SIGNATURE)))
+ verify(requestStorage, times(1)).putCertificatePath(eq("2"), certPathCapture.capture(), eq(listOf(DOORMAN_SIGNATURE)))
+
+ // Then make sure the generated node cert paths are correct
+ certPathCapture.allValues.forEachIndexed { index, certPath ->
+ X509Utilities.validateCertificateChain(rootCa.certificate, *certPath.certificates.toTypedArray())
+ assertThat(certPath.certificates).hasSize(3).element(1).isEqualTo(csrCa.certificate)
+ (certPath.certificates[0] as X509Certificate).apply {
+ assertThat(CertRole.extract(this)).isEqualTo(CertRole.NODE_CA)
+ assertThat(publicKey).isEqualTo(Crypto.toSupportedPublicKey(requests[index].subjectPublicKeyInfo))
+ assertThat(subjectX500Principal).isEqualTo(X500Principal("O=Test${index + 1},L=London,C=GB"))
+ }
+ }
+ }
+}
diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandlerTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandlerTest.kt
new file mode 100644
index 0000000000..7a9d18d92f
--- /dev/null
+++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandlerTest.kt
@@ -0,0 +1,151 @@
+package com.r3.corda.networkmanage.doorman.signer
+
+import com.nhaarman.mockito_kotlin.*
+import com.r3.corda.networkmanage.TestBase
+import com.r3.corda.networkmanage.common.persistence.*
+import com.r3.corda.networkmanage.doorman.ApprovedRequest
+import com.r3.corda.networkmanage.doorman.JiraClient
+import com.r3.corda.networkmanage.doorman.RejectedRequest
+import net.corda.core.crypto.Crypto
+import net.corda.core.crypto.SecureHash
+import net.corda.core.identity.CordaX500Name
+import net.corda.nodeapi.internal.crypto.X509Utilities
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import java.security.cert.CertPath
+import kotlin.test.assertEquals
+
+class JiraCsrHandlerTest : TestBase() {
+ @Rule
+ @JvmField
+ val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+ @Mock
+ private lateinit var jiraClient: JiraClient
+
+ @Mock
+ private lateinit var certificationRequestStorage: CertificationRequestStorage
+
+ @Mock
+ private lateinit var defaultCsrHandler: DefaultCsrHandler
+
+ @Mock
+ private val certPath: CertPath = mock()
+
+ private lateinit var jiraCsrHandler: JiraCsrHandler
+ private val requestId = "id"
+ private lateinit var certificateResponse: CertificateResponse.Ready
+
+ private val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
+ private val pkcS10CertificationRequest = X509Utilities.createCertificateSigningRequest(
+ CordaX500Name(locality = "London", organisation = "LegalName", country = "GB").x500Principal,
+ "my@mail.com",
+ keyPair)
+
+ @Before
+ fun setup() {
+ jiraCsrHandler = JiraCsrHandler(jiraClient, certificationRequestStorage, defaultCsrHandler)
+ certificateResponse = CertificateResponse.Ready(certPath)
+ }
+
+ @Test
+ fun `If jira connection fails we don't mark the ticket as created`() {
+ whenever(defaultCsrHandler.saveRequest(any())).thenReturn(requestId)
+ whenever(defaultCsrHandler.getResponse(requestId)).thenReturn(certificateResponse)
+ whenever(jiraClient.createRequestTicket(eq(requestId), any())).thenThrow(IllegalStateException("something broke"))
+
+ // Test
+ jiraCsrHandler.saveRequest(pkcS10CertificationRequest)
+
+ verify(certificationRequestStorage, never()).markRequestTicketCreated(requestId)
+ }
+
+ @Test
+ fun `If jira connection works we mark the ticket as created`() {
+ whenever(defaultCsrHandler.saveRequest(any())).thenReturn(requestId)
+ whenever(defaultCsrHandler.getResponse(requestId)).thenReturn(certificateResponse)
+
+ // Test
+ jiraCsrHandler.saveRequest(pkcS10CertificationRequest)
+
+ verify(certificationRequestStorage, times(1)).markRequestTicketCreated(requestId)
+ }
+
+ @Test
+ fun `create tickets`() {
+ val csr = certificateSigningRequest(
+ requestId = requestId,
+ legalName = "name",
+ status = RequestStatus.NEW,
+ request = pkcS10CertificationRequest)
+ whenever(certificationRequestStorage.getRequests(RequestStatus.NEW)).thenReturn(listOf(csr))
+
+ // Test
+ jiraCsrHandler.processRequests()
+
+ verify(jiraClient).createRequestTicket(requestId, csr.request)
+ verify(certificationRequestStorage).markRequestTicketCreated(requestId)
+ }
+
+ @Test
+ fun `sync tickets status`() {
+ val id1 = SecureHash.randomSHA256().toString()
+ val id2 = SecureHash.randomSHA256().toString()
+ val csr1 = CertificateSigningRequest(id1, "name1", SecureHash.randomSHA256(), RequestStatus.NEW, pkcS10CertificationRequest, null, emptyList(), null)
+ val csr2 = CertificateSigningRequest(id2, "name2", SecureHash.randomSHA256(), RequestStatus.NEW, pkcS10CertificationRequest, null, emptyList(), null)
+
+ val requests = mutableMapOf(id1 to csr1, id2 to csr2)
+
+ // Mocking storage behaviour.
+ whenever(certificationRequestStorage.getRequests(RequestStatus.NEW)).thenReturn(requests.values.filter { it.status == RequestStatus.NEW })
+ whenever(certificationRequestStorage.getRequest(any())).thenAnswer { requests[it.getArgument(0)] }
+ whenever(certificationRequestStorage.approveRequest(any(), any())).then {
+ val id = it.getArgument(0)
+ if (requests[id]?.status == RequestStatus.NEW) {
+ requests[id] = requests[id]!!.copy(status = RequestStatus.APPROVED, modifiedBy = listOf(it.getArgument(1)))
+ }
+ null
+ }
+ whenever(certificationRequestStorage.rejectRequest(any(), any(), any())).then {
+ val id = it.getArgument(0)
+ requests[id] = requests[id]!!.copy(status = RequestStatus.REJECTED, modifiedBy = listOf(it.getArgument(1)), remark = it.getArgument(2))
+ null
+ }
+
+ // Status change from jira.
+ whenever(jiraClient.getApprovedRequests()).thenReturn(listOf(ApprovedRequest(id1, "Me")))
+ whenever(jiraClient.getRejectedRequests()).thenReturn(listOf(RejectedRequest(id2, "Me", "Test reject")))
+
+ // Test.
+ jiraCsrHandler.processRequests()
+
+ verify(jiraClient).createRequestTicket(id1, csr1.request)
+ verify(jiraClient).createRequestTicket(id2, csr2.request)
+
+ verify(certificationRequestStorage).markRequestTicketCreated(id1)
+ verify(certificationRequestStorage).markRequestTicketCreated(id2)
+
+ // Verify request has the correct status in DB.
+ assertEquals(RequestStatus.APPROVED, requests[id1]!!.status)
+ assertEquals(RequestStatus.REJECTED, requests[id2]!!.status)
+
+ // Verify jira client get the correct call.
+ verify(jiraClient).updateRejectedRequests(listOf(id2))
+ verify(jiraClient).updateSignedRequests(emptyMap())
+
+ // Sign request 1
+ val certPath = mock()
+ val certData = CertificateData(CertificateStatus.VALID, certPath)
+ requests[id1] = requests[id1]!!.copy(status = RequestStatus.SIGNED, certData = certData)
+
+ // Process request again.
+ jiraCsrHandler.processRequests()
+
+ // Update signed request should be called.
+ verify(jiraClient).updateSignedRequests(mapOf(id1 to certPath))
+ }
+}
diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/NodeInfoWebServiceTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebServiceTest.kt
similarity index 86%
rename from network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/NodeInfoWebServiceTest.kt
rename to network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebServiceTest.kt
index 037daf4892..430d298a47 100644
--- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/NodeInfoWebServiceTest.kt
+++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebServiceTest.kt
@@ -1,4 +1,4 @@
-package com.r3.corda.networkmanage.doorman
+package com.r3.corda.networkmanage.doorman.webservice
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.times
@@ -7,7 +7,8 @@ import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage
import com.r3.corda.networkmanage.common.utils.SignedNetworkMap
import com.r3.corda.networkmanage.common.utils.SignedNetworkParameters
-import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService
+import com.r3.corda.networkmanage.doorman.NetworkManagementWebServer
+import com.r3.corda.networkmanage.doorman.NetworkMapConfig
import net.corda.core.crypto.SecureHash.Companion.randomSHA256
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.checkOkResponse
@@ -36,7 +37,7 @@ import java.security.cert.X509Certificate
import javax.ws.rs.core.MediaType
import kotlin.test.assertEquals
-class NodeInfoWebServiceTest {
+class NetworkMapWebServiceTest {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule(true)
@@ -56,12 +57,12 @@ class NodeInfoWebServiceTest {
@Test
fun `submit nodeInfo`() {
val networkMapStorage: NetworkMapStorage = mock {
- on { getCurrentSignedNetworkParameters() }.thenReturn(testNetworkParameters(emptyList()).signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate))
+ on { getNetworkParametersOfNetworkMap() }.thenReturn(testNetworkParameters(emptyList()).signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate))
}
// Create node info.
val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"))
- NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
+ NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
it.start()
val nodeInfoAndSignature = signedNodeInfo.serialize().bytes
// Post node info and signature to doorman, this should pass without any exception.
@@ -72,12 +73,12 @@ class NodeInfoWebServiceTest {
@Test
fun `submit old nodeInfo`() {
val networkMapStorage: NetworkMapStorage = mock {
- on { getCurrentSignedNetworkParameters() }.thenReturn(testNetworkParameters(emptyList(), minimumPlatformVersion = 2).signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate))
+ on { getNetworkParametersOfNetworkMap() }.thenReturn(testNetworkParameters(emptyList(), minimumPlatformVersion = 2).signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate))
}
// Create node info.
val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"), platformVersion = 1)
- NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
+ NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
it.start()
val nodeInfoAndSignature = signedNodeInfo.serialize().bytes
assertThatThrownBy { it.doPost("publish", nodeInfoAndSignature) }
@@ -88,12 +89,12 @@ class NodeInfoWebServiceTest {
@Test
fun `submit nodeInfo when no network parameters`() {
val networkMapStorage: NetworkMapStorage = mock {
- on { getCurrentSignedNetworkParameters() }.thenReturn(null)
+ on { getNetworkParametersOfNetworkMap() }.thenReturn(null)
}
// Create node info.
val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"), platformVersion = 1)
- NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
+ NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
it.start()
val nodeInfoAndSignature = signedNodeInfo.serialize().bytes
assertThatThrownBy { it.doPost("publish", nodeInfoAndSignature) }
@@ -110,7 +111,7 @@ class NodeInfoWebServiceTest {
on { getCurrentNetworkMap() }.thenReturn(signedNetworkMap)
}
- NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
+ NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
it.start()
val signedNetworkMapResponse = it.doGet("")
verify(networkMapStorage, times(1)).getCurrentNetworkMap()
@@ -127,7 +128,7 @@ class NodeInfoWebServiceTest {
on { getNodeInfo(nodeInfoHash) }.thenReturn(signedNodeInfo)
}
- NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage, mock(), testNetworkMapConfig)).use {
+ NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(nodeInfoStorage, mock(), testNetworkMapConfig)).use {
it.start()
val nodeInfoResponse = it.doGet("node-info/$nodeInfoHash")
verify(nodeInfoStorage, times(1)).getNodeInfo(nodeInfoHash)
@@ -149,7 +150,7 @@ class NodeInfoWebServiceTest {
on { getSignedNetworkParameters(networkParametersHash) }.thenReturn(signedNetworkParameters)
}
- NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
+ NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
it.start()
val netParamsResponse = it.doGet("network-parameters/$networkParametersHash")
verify(networkMapStorage, times(1)).getSignedNetworkParameters(networkParametersHash)
diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/RegistrationWebServiceTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebServiceTest.kt
similarity index 96%
rename from network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/RegistrationWebServiceTest.kt
rename to network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebServiceTest.kt
index dc1b0d1949..ead1e2f91a 100644
--- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/RegistrationWebServiceTest.kt
+++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebServiceTest.kt
@@ -1,11 +1,11 @@
-package com.r3.corda.networkmanage.doorman
+package com.r3.corda.networkmanage.doorman.webservice
import com.nhaarman.mockito_kotlin.*
import com.r3.corda.networkmanage.TestBase
import com.r3.corda.networkmanage.common.persistence.CertificateResponse
import com.r3.corda.networkmanage.common.utils.buildCertPath
+import com.r3.corda.networkmanage.doorman.NetworkManagementWebServer
import com.r3.corda.networkmanage.doorman.signer.CsrHandler
-import com.r3.corda.networkmanage.doorman.webservice.RegistrationWebService
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name
@@ -100,7 +100,7 @@ class RegistrationWebServiceTest : TestBase() {
CertificateResponse.Ready(it)
} ?: CertificateResponse.NotReady
}
- on { processApprovedRequests() }.then {
+ on { processRequests() }.then {
val request = X509Utilities.createCertificateSigningRequest(subject, "my@mail.com", keyPair)
certificateStore[id] = JcaPKCS10CertificationRequest(request).run {
val tlsCert = X509Utilities.createCertificate(
@@ -118,7 +118,7 @@ class RegistrationWebServiceTest : TestBase() {
startSigningServer(requestProcessor)
assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady)
- requestProcessor.processApprovedRequests()
+ requestProcessor.processRequests()
val certificates = (pollForResponse(id) as PollResponse.Ready).certChain
verify(requestProcessor, times(2)).getResponse(any())
@@ -141,7 +141,7 @@ class RegistrationWebServiceTest : TestBase() {
CertificateResponse.Ready(it)
} ?: CertificateResponse.NotReady
}
- on { processApprovedRequests() }.then {
+ on { processRequests() }.then {
val request = X509Utilities.createCertificateSigningRequest(
CordaX500Name(locality = "London", organisation = "Legal Name", country = "GB").x500Principal,
"my@mail.com",
@@ -165,7 +165,7 @@ class RegistrationWebServiceTest : TestBase() {
startSigningServer(storage)
assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady)
- storage.processApprovedRequests()
+ storage.processRequests()
val certificates = (pollForResponse(id) as PollResponse.Ready).certChain
verify(storage, times(2)).getResponse(any())
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt
index 7c11cf4bb7..906205b765 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt
@@ -1,11 +1,12 @@
package net.corda.nodeapi.internal
+import net.corda.core.crypto.toStringShort
+import net.corda.core.identity.Party
import net.corda.core.messaging.MessageRecipientGroup
import net.corda.core.messaging.MessageRecipients
import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.NetworkHostAndPort
-import net.corda.core.utilities.toBase58String
import java.security.PublicKey
/**
@@ -23,7 +24,7 @@ class ArtemisMessagingComponent {
const val PEER_USER = "SystemUsers/Peer"
const val INTERNAL_PREFIX = "internal."
const val PEERS_PREFIX = "${INTERNAL_PREFIX}peers." //TODO Come up with better name for common peers/services queue
- const val P2P_QUEUE = "p2p.inbound"
+ const val P2P_PREFIX = "p2p.inbound."
const val NOTIFICATIONS_ADDRESS = "${INTERNAL_PREFIX}activemq.notifications"
}
@@ -49,7 +50,7 @@ class ArtemisMessagingComponent {
@CordaSerializable
data class NodeAddress(override val queueName: String, override val hostAndPort: NetworkHostAndPort) : ArtemisPeerAddress {
constructor(peerIdentity: PublicKey, hostAndPort: NetworkHostAndPort) :
- this("$PEERS_PREFIX${peerIdentity.toBase58String()}", hostAndPort)
+ this("$PEERS_PREFIX${peerIdentity.toStringShort()}", hostAndPort)
}
/**
@@ -62,6 +63,30 @@ class ArtemisMessagingComponent {
* @param identity The service identity's owning key.
*/
data class ServiceAddress(val identity: PublicKey) : ArtemisAddress, MessageRecipientGroup {
- override val queueName: String = "$PEERS_PREFIX${identity.toBase58String()}"
+ override val queueName: String = "$PEERS_PREFIX${identity.toStringShort()}"
}
+
+ /**
+ * [RemoteInboxAddress] implements [SingleMessageRecipient]. It represents the non-local address of a remote inbox.
+ * @param identity The Node public identity
+ */
+ data class RemoteInboxAddress(val identity: PublicKey) : ArtemisAddress, SingleMessageRecipient {
+ constructor(party: Party) : this(party.owningKey)
+
+ companion object {
+ /**
+ * When transferring a message from the local holding queue to the remote inbox queue
+ * this method provides a simple translation of the address string.
+ * The topics are distinct so that proper segregation of internal
+ * and external access permissions can be made.
+ */
+ fun translateLocalQueueToInboxAddress(address: String): String {
+ require(address.startsWith(PEERS_PREFIX)) { "Failed to map address: $address to a remote topic as it is not in the $PEERS_PREFIX namespace" }
+ return P2P_PREFIX + address.substring(PEERS_PREFIX.length)
+ }
+ }
+
+ override val queueName: String = "$P2P_PREFIX${identity.toStringShort()}"
+ }
+
}
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt
index 2ba6192454..dd93ce2269 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt
@@ -1,6 +1,7 @@
package net.corda.nodeapi.internal
import net.corda.core.crypto.Crypto
+import net.corda.core.crypto.Crypto.generateKeyPair
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.x500Name
import net.corda.nodeapi.internal.config.SSLConfiguration
@@ -8,6 +9,7 @@ import net.corda.nodeapi.internal.crypto.*
import org.bouncycastle.asn1.x509.GeneralName
import org.bouncycastle.asn1.x509.GeneralSubtree
import org.bouncycastle.asn1.x509.NameConstraints
+import java.security.KeyPair
import java.security.cert.X509Certificate
import javax.security.auth.x500.X500Principal
@@ -31,7 +33,7 @@ fun SSLConfiguration.createDevKeyStores(legalName: CordaX500Name,
save(nodeKeystore, keyStorePassword)
}
- val tlsKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
+ val tlsKeyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, nodeCaCert, nodeCaKeyPair, legalName.x500Principal, tlsKeyPair.public)
loadOrCreateKeyStore(sslKeystore, keyStorePassword).apply {
@@ -59,17 +61,18 @@ fun createDevNetworkMapCa(rootCa: CertificateAndKeyPair = DEV_ROOT_CA): Certific
* Create a dev node CA cert, as a sub-cert of the given [intermediateCa], and matching key pair using the given
* [CordaX500Name] as the cert subject.
*/
-fun createDevNodeCa(intermediateCa: CertificateAndKeyPair, legalName: CordaX500Name): CertificateAndKeyPair {
- val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
+fun createDevNodeCa(intermediateCa: CertificateAndKeyPair,
+ legalName: CordaX500Name,
+ nodeKeyPair: KeyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)): CertificateAndKeyPair {
val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.x500Name))), arrayOf())
val cert = X509Utilities.createCertificate(
CertificateType.NODE_CA,
intermediateCa.certificate,
intermediateCa.keyPair,
legalName.x500Principal,
- keyPair.public,
+ nodeKeyPair.public,
nameConstraints = nameConstraints)
- return CertificateAndKeyPair(cert, keyPair)
+ return CertificateAndKeyPair(cert, nodeKeyPair)
}
val DEV_INTERMEDIATE_CA: CertificateAndKeyPair get() = DevCaHelper.loadDevCa(X509Utilities.CORDA_INTERMEDIATE_CA)
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt
index 5303b202f3..a1c6210a5e 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt
@@ -1,13 +1,16 @@
package net.corda.nodeapi.internal.persistence
+import co.paralleluniverse.strands.Strand
import net.corda.core.schemas.MappedSchema
import net.corda.core.utilities.contextLogger
import rx.Observable
import rx.Subscriber
+import rx.subjects.PublishSubject
import rx.subjects.UnicastSubject
import java.io.Closeable
import java.sql.Connection
import java.sql.SQLException
+import java.util.*
import java.util.concurrent.CopyOnWriteArrayList
import javax.persistence.AttributeConverter
import javax.sql.DataSource
@@ -40,6 +43,11 @@ enum class TransactionIsolationLevel {
val jdbcValue: Int = java.sql.Connection::class.java.getField(jdbcString).get(null) as Int
}
+private val _contextDatabase = ThreadLocal()
+var contextDatabase: CordaPersistence
+ get() = _contextDatabase.get() ?: error("Was expecting to find CordaPersistence set on current thread: ${Strand.currentStrand()}")
+ set(database) = _contextDatabase.set(database)
+
class CordaPersistence(
val dataSource: DataSource,
databaseConfig: DatabaseConfig,
@@ -51,7 +59,7 @@ class CordaPersistence(
private val log = contextLogger()
}
- val defaultIsolationLevel = databaseConfig.transactionIsolationLevel
+ private val defaultIsolationLevel = databaseConfig.transactionIsolationLevel
val hibernateConfig: HibernateConfiguration by lazy {
transaction {
@@ -60,8 +68,19 @@ class CordaPersistence(
}
val entityManagerFactory get() = hibernateConfig.sessionFactoryForRegisteredSchemas
+ data class Boundary(val txId: UUID)
+
+ internal val transactionBoundaries = PublishSubject.create().toSerialized()
+
init {
- DatabaseTransactionManager(this)
+ // Found a unit test that was forgetting to close the database transactions. When you close() on the top level
+ // database transaction it will reset the threadLocalTx back to null, so if it isn't then there is still a
+ // database transaction open. The [transaction] helper above handles this in a finally clause for you
+ // but any manual database transaction management is liable to have this problem.
+ contextTransactionOrNull?.let {
+ error("Was not expecting to find existing database transaction on current strand when setting database: ${Strand.currentStrand()}, $it")
+ }
+ _contextDatabase.set(this)
// Check not in read-only mode.
transaction {
check(!connection.metaData.isReadOnly) { "Database should not be readonly." }
@@ -72,25 +91,29 @@ class CordaPersistence(
const val DATA_SOURCE_URL = "dataSource.url"
}
- /**
- * Creates an instance of [DatabaseTransaction], with the given transaction isolation level.
- */
- fun createTransaction(isolationLevel: TransactionIsolationLevel): DatabaseTransaction {
- // We need to set the database for the current [Thread] or [Fiber] here as some tests share threads across databases.
- DatabaseTransactionManager.dataSource = this
- return DatabaseTransactionManager.currentOrNew(isolationLevel)
+ fun currentOrNew(isolation: TransactionIsolationLevel = defaultIsolationLevel): DatabaseTransaction {
+ return contextTransactionOrNull ?: newTransaction(isolation)
+ }
+
+ fun newTransaction(isolation: TransactionIsolationLevel = defaultIsolationLevel): DatabaseTransaction {
+ return DatabaseTransaction(isolation.jdbcValue, contextTransactionOrNull, this).also {
+ contextTransactionOrNull = it
+ }
}
/**
- * Creates an instance of [DatabaseTransaction], with the default transaction isolation level.
+ * Creates an instance of [DatabaseTransaction], with the given transaction isolation level.
*/
- fun createTransaction(): DatabaseTransaction = createTransaction(defaultIsolationLevel)
+ fun createTransaction(isolationLevel: TransactionIsolationLevel = defaultIsolationLevel): DatabaseTransaction {
+ // We need to set the database for the current [Thread] or [Fiber] here as some tests share threads across databases.
+ _contextDatabase.set(this)
+ return currentOrNew(isolationLevel)
+ }
fun createSession(): Connection {
// We need to set the database for the current [Thread] or [Fiber] here as some tests share threads across databases.
- DatabaseTransactionManager.dataSource = this
- val ctx = DatabaseTransactionManager.currentOrNull()
- return ctx?.connection ?: throw IllegalStateException("Was expecting to find database transaction: must wrap calling code within a transaction.")
+ _contextDatabase.set(this)
+ return contextTransaction.connection
}
/**
@@ -99,7 +122,7 @@ class CordaPersistence(
* @param statement to be executed in the scope of this transaction.
*/
fun transaction(isolationLevel: TransactionIsolationLevel, statement: DatabaseTransaction.() -> T): T {
- DatabaseTransactionManager.dataSource = this
+ _contextDatabase.set(this)
return transaction(isolationLevel, 2, statement)
}
@@ -110,7 +133,7 @@ class CordaPersistence(
fun transaction(statement: DatabaseTransaction.() -> T): T = transaction(defaultIsolationLevel, statement)
private fun transaction(isolationLevel: TransactionIsolationLevel, recoverableFailureTolerance: Int, statement: DatabaseTransaction.() -> T): T {
- val outer = DatabaseTransactionManager.currentOrNull()
+ val outer = contextTransactionOrNull
return if (outer != null) {
outer.statement()
} else {
@@ -126,7 +149,7 @@ class CordaPersistence(
log.warn("Cleanup task failed:", t)
}
while (true) {
- val transaction = DatabaseTransactionManager.currentOrNew(isolationLevel)
+ val transaction = contextDatabase.currentOrNew(isolationLevel) // XXX: Does this code really support statement changing the contextDatabase?
try {
val answer = transaction.statement()
transaction.commit()
@@ -160,8 +183,8 @@ class CordaPersistence(
* For examples, see the call hierarchy of this function.
*/
fun rx.Observer.bufferUntilDatabaseCommit(): rx.Observer {
- val currentTxId = DatabaseTransactionManager.transactionId
- val databaseTxBoundary: Observable = DatabaseTransactionManager.transactionBoundaries.first { it.txId == currentTxId }
+ val currentTxId = contextTransaction.id
+ val databaseTxBoundary: Observable = contextDatabase.transactionBoundaries.first { it.txId == currentTxId }
val subject = UnicastSubject.create()
subject.delaySubscription(databaseTxBoundary).subscribe(this)
databaseTxBoundary.doOnCompleted { subject.onCompleted() }
@@ -169,12 +192,12 @@ fun rx.Observer.bufferUntilDatabaseCommit(): rx.Observer {
}
// A subscriber that delegates to multiple others, wrapping a database transaction around the combination.
-private class DatabaseTransactionWrappingSubscriber(val db: CordaPersistence?) : Subscriber() {
+private class DatabaseTransactionWrappingSubscriber(private val db: CordaPersistence?) : Subscriber() {
// Some unsubscribes happen inside onNext() so need something that supports concurrent modification.
val delegates = CopyOnWriteArrayList>()
fun forEachSubscriberWithDbTx(block: Subscriber.() -> Unit) {
- (db ?: DatabaseTransactionManager.dataSource).transaction {
+ (db ?: contextDatabase).transaction {
delegates.filter { !it.isUnsubscribed }.forEach {
it.block()
}
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt
index 4bb1bbd42e..ffcdd55c24 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt
@@ -1,23 +1,29 @@
package net.corda.nodeapi.internal.persistence
+import co.paralleluniverse.strands.Strand
import org.hibernate.Session
import org.hibernate.Transaction
-import rx.subjects.Subject
import java.sql.Connection
import java.util.*
+fun currentDBSession(): Session = contextTransaction.session
+private val _contextTransaction = ThreadLocal()
+var contextTransactionOrNull: DatabaseTransaction?
+ get() = _contextTransaction.get()
+ set(transaction) = _contextTransaction.set(transaction)
+val contextTransaction get() = contextTransactionOrNull ?: error("Was expecting to find transaction set on current strand: ${Strand.currentStrand()}")
+
class DatabaseTransaction(
isolation: Int,
- private val threadLocal: ThreadLocal,
- private val transactionBoundaries: Subject,
- val cordaPersistence: CordaPersistence
+ val outerTransaction: DatabaseTransaction?,
+ val database: CordaPersistence
) {
val id: UUID = UUID.randomUUID()
private var _connectionCreated = false
val connectionCreated get() = _connectionCreated
val connection: Connection by lazy(LazyThreadSafetyMode.NONE) {
- cordaPersistence.dataSource.connection
+ database.dataSource.connection
.apply {
_connectionCreated = true
// only set the transaction isolation level if it's actually changed - setting isn't free.
@@ -28,16 +34,13 @@ class DatabaseTransaction(
}
private val sessionDelegate = lazy {
- val session = cordaPersistence.entityManagerFactory.withOptions().connection(connection).openSession()
+ val session = database.entityManagerFactory.withOptions().connection(connection).openSession()
hibernateTransaction = session.beginTransaction()
session
}
val session: Session by sessionDelegate
private lateinit var hibernateTransaction: Transaction
-
- val outerTransaction: DatabaseTransaction? = threadLocal.get()
-
fun commit() {
if (sessionDelegate.isInitialized()) {
hibernateTransaction.commit()
@@ -63,9 +66,9 @@ class DatabaseTransaction(
if (_connectionCreated) {
connection.close()
}
- threadLocal.set(outerTransaction)
+ contextTransactionOrNull = outerTransaction
if (outerTransaction == null) {
- transactionBoundaries.onNext(DatabaseTransactionManager.Boundary(id))
+ database.transactionBoundaries.onNext(CordaPersistence.Boundary(id))
}
}
}
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransactionManager.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransactionManager.kt
deleted file mode 100644
index ade1603002..0000000000
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransactionManager.kt
+++ /dev/null
@@ -1,77 +0,0 @@
-package net.corda.nodeapi.internal.persistence
-
-import co.paralleluniverse.strands.Strand
-import org.hibernate.Session
-import rx.subjects.PublishSubject
-import rx.subjects.Subject
-import java.util.*
-import java.util.concurrent.ConcurrentHashMap
-
-fun currentDBSession(): Session = DatabaseTransactionManager.current().session
-
-class DatabaseTransactionManager(initDataSource: CordaPersistence) {
- companion object {
- private val threadLocalDb = ThreadLocal()
- private val threadLocalTx = ThreadLocal()
- private val databaseToInstance = ConcurrentHashMap()
-
- fun setThreadLocalTx(tx: DatabaseTransaction?): DatabaseTransaction? {
- val oldTx = threadLocalTx.get()
- threadLocalTx.set(tx)
- return oldTx
- }
-
- fun restoreThreadLocalTx(context: DatabaseTransaction?) {
- if (context != null) {
- threadLocalDb.set(context.cordaPersistence)
- }
- threadLocalTx.set(context)
- }
-
- var dataSource: CordaPersistence
- get() = threadLocalDb.get() ?: throw IllegalStateException("Was expecting to find CordaPersistence set on current thread: ${Strand.currentStrand()}")
- set(value) = threadLocalDb.set(value)
-
- val transactionId: UUID
- get() = threadLocalTx.get()?.id ?: throw IllegalStateException("Was expecting to find transaction set on current strand: ${Strand.currentStrand()}")
-
- val manager: DatabaseTransactionManager get() = databaseToInstance[dataSource]!!
-
- val transactionBoundaries: Subject get() = manager._transactionBoundaries
-
- fun currentOrNull(): DatabaseTransaction? = manager.currentOrNull()
-
- fun currentOrNew(isolation: TransactionIsolationLevel = dataSource.defaultIsolationLevel): DatabaseTransaction {
- return currentOrNull() ?: manager.newTransaction(isolation.jdbcValue)
- }
-
- fun current(): DatabaseTransaction = currentOrNull() ?: error("No transaction in context.")
-
- fun newTransaction(isolation: TransactionIsolationLevel = dataSource.defaultIsolationLevel): DatabaseTransaction {
- return manager.newTransaction(isolation.jdbcValue)
- }
- }
-
- data class Boundary(val txId: UUID)
-
- private val _transactionBoundaries = PublishSubject.create().toSerialized()
-
- init {
- // Found a unit test that was forgetting to close the database transactions. When you close() on the top level
- // database transaction it will reset the threadLocalTx back to null, so if it isn't then there is still a
- // database transaction open. The [transaction] helper above handles this in a finally clause for you
- // but any manual database transaction management is liable to have this problem.
- if (threadLocalTx.get() != null) {
- throw IllegalStateException("Was not expecting to find existing database transaction on current strand when setting database: ${Strand.currentStrand()}, ${threadLocalTx.get()}")
- }
- dataSource = initDataSource
- databaseToInstance[dataSource] = this
- }
-
- private fun newTransaction(isolation: Int) =
- DatabaseTransaction(isolation, threadLocalTx, transactionBoundaries, dataSource).apply {
- threadLocalTx.set(this)
- }
-
- private fun currentOrNull(): DatabaseTransaction? = threadLocalTx.get()
-}
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt
index 0e17c06e4c..3861edb4a3 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt
@@ -128,15 +128,16 @@ class HibernateConfiguration(
class NodeDatabaseConnectionProvider : ConnectionProvider {
override fun closeConnection(conn: Connection) {
conn.autoCommit = false
- val tx = DatabaseTransactionManager.current()
- tx.commit()
- tx.close()
+ contextTransaction.run {
+ commit()
+ close()
+ }
}
override fun supportsAggressiveRelease(): Boolean = true
override fun getConnection(): Connection {
- return DatabaseTransactionManager.newTransaction().connection
+ return contextDatabase.newTransaction().connection
}
override fun unwrap(unwrapType: Class): T {
diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt
index 32d43cb5e3..e04b6c75c3 100644
--- a/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt
+++ b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt
@@ -3,18 +3,18 @@ package net.corda.node.amqp
import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
+import net.corda.core.crypto.toStringShort
import net.corda.core.internal.div
import net.corda.core.node.NodeInfo
import net.corda.core.node.services.NetworkMapCache
import net.corda.core.utilities.NetworkHostAndPort
-import net.corda.core.utilities.toBase58String
import net.corda.node.internal.protonwrapper.netty.AMQPServer
import net.corda.node.internal.security.RPCSecurityManager
+import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.node.services.config.*
import net.corda.node.services.messaging.ArtemisMessagingClient
import net.corda.node.services.messaging.ArtemisMessagingServer
import net.corda.nodeapi.internal.ArtemisMessagingComponent
-import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE
import net.corda.nodeapi.internal.crypto.loadKeyStore
import net.corda.testing.*
import net.corda.testing.internal.rigorousMock
@@ -53,7 +53,7 @@ class AMQPBridgeTest {
@Test
fun `test acked and nacked messages`() {
// Create local queue
- val sourceQueueName = "internal.peers." + BOB.publicKey.toBase58String()
+ val sourceQueueName = "internal.peers." + BOB.publicKey.toStringShort()
val (artemisServer, artemisClient) = createArtemis(sourceQueueName)
// Pre-populate local queue with 3 messages
@@ -133,11 +133,13 @@ class AMQPBridgeTest {
@Test
fun `Test legacy bridge still works`() {
// Create local queue
- val sourceQueueName = "internal.peers." + ALICE.publicKey.toBase58String()
+ val sourceQueueName = "internal.peers." + BOB.publicKey.toStringShort()
val (artemisLegacyServer, artemisLegacyClient) = createLegacyArtemis(sourceQueueName)
val (artemisServer, artemisClient) = createArtemis(null)
+ val inbox = ArtemisMessagingComponent.RemoteInboxAddress(BOB.party).queueName
+ artemisClient.started!!.session.createQueue(inbox, RoutingType.ANYCAST, inbox, true)
val artemis = artemisLegacyClient.started!!
for (i in 0 until 3) {
@@ -150,8 +152,7 @@ class AMQPBridgeTest {
artemis.producer.send(sourceQueueName, artemisMessage)
}
-
- val subs = artemisClient.started!!.session.createConsumer(P2P_QUEUE)
+ val subs = artemisClient.started!!.session.createConsumer(inbox)
for (i in 0 until 3) {
val msg = subs.receive()
val messageBody = ByteArray(msg.bodySize).apply { msg.bodyBuffer.readBytes(this) }
@@ -177,9 +178,9 @@ class AMQPBridgeTest {
doReturn(true).whenever(it).useAMQPBridges
}
artemisConfig.configureWithDevSSLCertificate()
- val networkMap = rigorousMock().also {
+ val networkMap = rigorousMock().also {
doReturn(Observable.never()).whenever(it).changed
- doReturn(listOf(NodeInfo(listOf(amqpAddress), listOf(BOB.identity), 1, 1L))).whenever(it).getNodesByLegalIdentityKey(any())
+ doReturn(listOf(NodeInfo(listOf(amqpAddress), listOf(BOB.identity), 1, 1L))).whenever(it).getNodesByOwningKeyIndex(any())
}
val userService = rigorousMock()
val artemisServer = ArtemisMessagingServer(artemisConfig, artemisPort, null, networkMap, userService, MAX_MESSAGE_SIZE)
@@ -189,7 +190,7 @@ class AMQPBridgeTest {
val artemis = artemisClient.started!!
if (sourceQueueName != null) {
// Local queue for outgoing messages
- artemis.session.createQueue(sourceQueueName, RoutingType.MULTICAST, sourceQueueName, true)
+ artemis.session.createQueue(sourceQueueName, RoutingType.ANYCAST, sourceQueueName, true)
}
return Pair(artemisServer, artemisClient)
}
@@ -206,9 +207,9 @@ class AMQPBridgeTest {
doReturn(ActiveMqServerConfiguration(BridgeConfiguration(0, 0, 0.0))).whenever(it).activeMQServer
}
artemisConfig.configureWithDevSSLCertificate()
- val networkMap = rigorousMock().also {
+ val networkMap = rigorousMock().also {
doReturn(Observable.never()).whenever(it).changed
- doReturn(listOf(NodeInfo(listOf(artemisAddress), listOf(ALICE.identity), 1, 1L))).whenever(it).getNodesByLegalIdentityKey(any())
+ doReturn(listOf(NodeInfo(listOf(artemisAddress), listOf(ALICE.identity), 1, 1L))).whenever(it).getNodesByOwningKeyIndex(any())
}
val userService = rigorousMock()
val artemisServer = ArtemisMessagingServer(artemisConfig, artemisPort2, null, networkMap, userService, MAX_MESSAGE_SIZE)
@@ -217,7 +218,7 @@ class AMQPBridgeTest {
artemisClient.start()
val artemis = artemisClient.started!!
// Local queue for outgoing messages
- artemis.session.createQueue(sourceQueueName, RoutingType.MULTICAST, sourceQueueName, true)
+ artemis.session.createQueue(sourceQueueName, RoutingType.ANYCAST, sourceQueueName, true)
return Pair(artemisServer, artemisClient)
}
diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt
index 88c299dd41..6c06fa607c 100644
--- a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt
+++ b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt
@@ -13,11 +13,13 @@ import net.corda.node.internal.protonwrapper.messages.MessageStatus
import net.corda.node.internal.protonwrapper.netty.AMQPClient
import net.corda.node.internal.protonwrapper.netty.AMQPServer
import net.corda.node.internal.security.RPCSecurityManager
+import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.node.services.config.CertChainPolicyConfig
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.configureWithDevSSLCertificate
import net.corda.node.services.messaging.ArtemisMessagingClient
import net.corda.node.services.messaging.ArtemisMessagingServer
+import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
import net.corda.nodeapi.internal.crypto.loadKeyStore
import net.corda.testing.*
@@ -48,7 +50,7 @@ class ProtonWrapperTests {
amqpServer.start()
val receiveSubs = amqpServer.onReceive.subscribe {
assertEquals(BOB_NAME.toString(), it.sourceLegalName)
- assertEquals("p2p.inbound", it.topic)
+ assertEquals(P2P_PREFIX + "Test", it.topic)
assertEquals("Test", String(it.payload))
it.complete(true)
}
@@ -64,7 +66,7 @@ class ProtonWrapperTests {
assertEquals(true, clientConnect.connected)
assertEquals(ALICE_NAME, CordaX500Name.build(clientConnect.remoteCert!!.subjectX500Principal))
val msg = amqpClient.createMessage("Test".toByteArray(),
- "p2p.inbound",
+ P2P_PREFIX + "Test",
ALICE_NAME.toString(),
emptyMap())
amqpClient.write(msg)
@@ -151,8 +153,8 @@ class ProtonWrapperTests {
assertEquals(true, clientConnected.get().connected)
assertEquals(CHARLIE_NAME, CordaX500Name.build(clientConnected.get().remoteCert!!.subjectX500Principal))
val artemis = artemisClient.started!!
- val sendAddress = "p2p.inbound"
- artemis.session.createQueue(sendAddress, RoutingType.MULTICAST, "queue", true)
+ val sendAddress = P2P_PREFIX + "Test"
+ artemis.session.createQueue(sendAddress, RoutingType.ANYCAST, "queue", true)
val consumer = artemis.session.createConsumer("queue")
val testData = "Test".toByteArray()
val testProperty = mutableMapOf()
@@ -230,7 +232,7 @@ class ProtonWrapperTests {
}
artemisConfig.configureWithDevSSLCertificate()
- val networkMap = rigorousMock().also {
+ val networkMap = rigorousMock().also {
doReturn(never()).whenever(it).changed
}
val userService = rigorousMock()
diff --git a/node/src/integration-test/kotlin/net/corda/node/services/MySQLNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/MySQLNotaryServiceTests.kt
index 273f18690a..af431a2a0d 100644
--- a/node/src/integration-test/kotlin/net/corda/node/services/MySQLNotaryServiceTests.kt
+++ b/node/src/integration-test/kotlin/net/corda/node/services/MySQLNotaryServiceTests.kt
@@ -13,6 +13,7 @@ import net.corda.core.identity.Party
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.getOrThrow
import net.corda.node.internal.StartedNode
+import net.corda.node.services.config.MySQLConfiguration
import net.corda.node.services.config.NotaryConfig
import net.corda.nodeapi.internal.DevIdentityGenerator
import net.corda.nodeapi.internal.network.NetworkParametersCopier
@@ -135,7 +136,7 @@ class MySQLNotaryServiceTests : IntegrationTest() {
legalName = notaryName,
entropyRoot = BigInteger.valueOf(60L),
configOverrides = {
- val notaryConfig = NotaryConfig(validating = false, mysql = dataStoreProperties)
+ val notaryConfig = NotaryConfig(validating = false, mysql = MySQLConfiguration(dataStoreProperties))
doReturn(notaryConfig).whenever(it).notary
}
)
diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt
index 7b82b44b22..77be9ab1a5 100644
--- a/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt
+++ b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt
@@ -1,5 +1,6 @@
package net.corda.node.services.network
+import net.corda.core.crypto.generateKeyPair
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.node.NodeInfo
@@ -15,6 +16,7 @@ import org.junit.ClassRule
import org.junit.Test
import kotlin.test.assertEquals
+// TODO Clean up these tests, they were written with old network map design in place.
class PersistentNetworkMapCacheTest : NodeBasedTest() {
companion object {
val ALICE = TestIdentity(ALICE_NAME, 70).party
@@ -60,6 +62,19 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() {
}
}
+ // This test has to be done as normal node not mock, because MockNodes don't have addresses.
+ @Test
+ fun `insert two node infos with the same host and port`() {
+ val aliceNode = startNode(ALICE_NAME)
+ val charliePartyCert = getTestPartyAndCertificate(CHARLIE_NAME, generateKeyPair().public)
+ val aliceCache = aliceNode.services.networkMapCache
+ aliceCache.addNode(aliceNode.info.copy(legalIdentitiesAndCerts = listOf(charliePartyCert)))
+ val res = aliceNode.database.transaction {
+ aliceCache.allNodes.filter { aliceNode.info.addresses[0] in it.addresses }
+ }
+ assertEquals(2, res.size)
+ }
+
@Test
fun `restart node with DB map cache`() {
val alice = startNodesWithPort(listOf(ALICE))[0]
diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt
index d1a69c9be5..f723202758 100644
--- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt
+++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt
@@ -5,6 +5,7 @@ import net.corda.client.rpc.CordaRPCClient
import net.corda.client.rpc.CordaRPCConnection
import net.corda.core.crypto.generateKeyPair
import net.corda.core.crypto.random63BitValue
+import net.corda.core.crypto.toStringShort
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.flows.InitiatedBy
@@ -14,14 +15,13 @@ import net.corda.core.identity.Party
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.getOrThrow
-import net.corda.core.utilities.toBase58String
import net.corda.core.utilities.unwrap
import net.corda.node.internal.Node
import net.corda.node.internal.StartedNode
import net.corda.nodeapi.RPCApi
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS
-import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE
+import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.testing.ALICE_NAME
@@ -79,30 +79,30 @@ abstract class MQSecurityTest : NodeBasedTest() {
@Test
fun `consume message from P2P queue`() {
- assertConsumeAttackFails(P2P_QUEUE)
+ assertConsumeAttackFails("$P2P_PREFIX${alice.info.chooseIdentity().owningKey.toStringShort()}")
}
@Test
fun `consume message from peer queue`() {
val bobParty = startBobAndCommunicateWithAlice()
- assertConsumeAttackFails("$PEERS_PREFIX${bobParty.owningKey.toBase58String()}")
+ assertConsumeAttackFails("$PEERS_PREFIX${bobParty.owningKey.toStringShort()}")
}
@Test
fun `send message to address of peer which has been communicated with`() {
val bobParty = startBobAndCommunicateWithAlice()
- assertSendAttackFails("$PEERS_PREFIX${bobParty.owningKey.toBase58String()}")
+ assertSendAttackFails("$PEERS_PREFIX${bobParty.owningKey.toStringShort()}")
}
@Test
fun `create queue for peer which has not been communicated with`() {
val bob = startNode(BOB_NAME)
- assertAllQueueCreationAttacksFail("$PEERS_PREFIX${bob.info.chooseIdentity().owningKey.toBase58String()}")
+ assertAllQueueCreationAttacksFail("$PEERS_PREFIX${bob.info.chooseIdentity().owningKey.toStringShort()}")
}
@Test
fun `create queue for unknown peer`() {
- val invalidPeerQueue = "$PEERS_PREFIX${generateKeyPair().public.toBase58String()}"
+ val invalidPeerQueue = "$PEERS_PREFIX${generateKeyPair().public.toStringShort()}"
assertAllQueueCreationAttacksFail(invalidPeerQueue)
}
diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt
index 2c07f39c19..fb1a294345 100644
--- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt
+++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt
@@ -222,7 +222,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
check(started == null) { "Node has already been started" }
log.info("Node starting up ...")
initCertificate()
- val schemaService = NodeSchemaService(cordappLoader.cordappSchemas)
+ val schemaService = NodeSchemaService(cordappLoader.cordappSchemas, configuration.notary != null)
val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null)
val lh = lazyHub()
configure(lh)
diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt
index f96ce6e97f..195f819e21 100644
--- a/node/src/main/kotlin/net/corda/node/internal/Node.kt
+++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt
@@ -40,6 +40,7 @@ import org.slf4j.Logger
import org.slf4j.LoggerFactory
import rx.Scheduler
import rx.schedulers.Schedulers
+import java.security.PublicKey
import java.time.Clock
import java.util.concurrent.atomic.AtomicInteger
import javax.management.ObjectName
@@ -166,11 +167,14 @@ open class Node(configuration: NodeConfiguration,
VerifierType.OutOfProcess -> VerifierMessagingClient(configuration, serverAddress, services.monitoringService.metrics, networkParameters.maxMessageSize)
VerifierType.InMemory -> null
}
+ require(info.legalIdentities.size in 1..2) { "Currently nodes must have a primary address and optionally one serviced address" }
+ val serviceIdentity: PublicKey? = if (info.legalIdentities.size == 1) null else info.legalIdentities[1].owningKey
return P2PMessagingClient(
configuration,
versionInfo,
serverAddress,
info.legalIdentities[0].owningKey,
+ serviceIdentity,
serverThread,
database,
advertisedAddress,
diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt
index 7a4f2f669d..e0094054a1 100644
--- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt
+++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt
@@ -29,6 +29,10 @@ interface NetworkMapCacheBaseInternal : NetworkMapCacheBase {
fun getNodeByHash(nodeHash: SecureHash): NodeInfo?
+ /** Find nodes from the [PublicKey] toShortString representation.
+ * This is used for Artemis bridge lookup process. */
+ fun getNodesByOwningKeyIndex(identityKeyIndex: String): List
+
/** Adds a node to the local cache (generally only used for adding ourselves). */
fun addNode(node: NodeInfo)
diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt
index bd50ac1562..d8ad742d74 100644
--- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt
+++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt
@@ -18,8 +18,6 @@ import java.util.*
val Int.MB: Long get() = this * 1024L * 1024L
interface NodeConfiguration : NodeSSLConfiguration {
- // myLegalName should be only used in the initial network registration, we should use the name from the certificate instead of this.
- // TODO: Remove this so we don't accidentally use this identity in the code?
val myLegalName: CordaX500Name
val emailAddress: String
val exportJMXto: String
@@ -35,8 +33,6 @@ interface NodeConfiguration : NodeSSLConfiguration {
val notary: NotaryConfig?
val activeMQServer: ActiveMqServerConfiguration
val additionalNodeInfoPollingFrequencyMsec: Long
- // TODO Remove as this is only used by the driver
- val useHTTPS: Boolean
val p2pAddress: NetworkHostAndPort
val rpcAddress: NetworkHostAndPort?
val messagingServerAddress: NetworkHostAndPort?
@@ -71,7 +67,7 @@ data class NotaryConfig(val validating: Boolean,
val raft: RaftConfig? = null,
val bftSMaRt: BFTSMaRtConfiguration? = null,
val custom: Boolean = false,
- val mysql: Properties? = null
+ val mysql: MySQLConfiguration? = null
) {
init {
require(raft == null || bftSMaRt == null || !custom || mysql == null) {
@@ -81,6 +77,15 @@ data class NotaryConfig(val validating: Boolean,
val isClusterConfig: Boolean get() = raft != null || bftSMaRt != null
}
+data class MySQLConfiguration(
+ val dataSource: Properties,
+ val connectionRetries: Int = 0
+) {
+ init {
+ require(connectionRetries >= 0) { "connectionRetries cannot be negative" }
+ }
+}
+
data class RaftConfig(val nodeAddress: NetworkHostAndPort, val clusterAddresses: List)
/** @param exposeRaces for testing only, so its default is not in reference.conf but here. */
@@ -118,7 +123,6 @@ data class NodeConfigurationImpl(
// TODO typesafe config supports the notion of durations. Make use of that by mapping it to java.time.Duration.
// Then rename this to messageRedeliveryDelay and make it of type Duration
override val messageRedeliveryDelaySeconds: Int = 30,
- override val useHTTPS: Boolean,
override val p2pAddress: NetworkHostAndPort,
override val rpcAddress: NetworkHostAndPort?,
override val relay: RelayConfiguration?,
diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/AMQPBridgeManager.kt b/node/src/main/kotlin/net/corda/node/services/messaging/AMQPBridgeManager.kt
index 8b285bf999..93b1137a00 100644
--- a/node/src/main/kotlin/net/corda/node/services/messaging/AMQPBridgeManager.kt
+++ b/node/src/main/kotlin/net/corda/node/services/messaging/AMQPBridgeManager.kt
@@ -12,8 +12,8 @@ import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.messaging.AMQPBridgeManager.AMQPBridge.Companion.getBridgeName
import net.corda.nodeapi.internal.ArtemisMessagingComponent
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
-import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
+import net.corda.nodeapi.internal.ArtemisMessagingComponent.RemoteInboxAddress.Companion.translateLocalQueueToInboxAddress
import net.corda.nodeapi.internal.crypto.loadKeyStore
import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
@@ -132,7 +132,8 @@ internal class AMQPBridgeManager(val config: NodeConfiguration, val p2pAddress:
properties[key.toString()] = value
}
log.debug { "Bridged Send to ${legalNames.first()} uuid: ${artemisMessage.getObjectProperty("_AMQ_DUPL_ID")}" }
- val sendableMessage = amqpClient.createMessage(data, P2P_QUEUE,
+ val peerInbox = translateLocalQueueToInboxAddress(queueName)
+ val sendableMessage = amqpClient.createMessage(data, peerInbox,
legalNames.first().toString(),
properties)
sendableMessage.onComplete.then {
diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt
index 285d4ce94c..7eacea9520 100644
--- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt
+++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt
@@ -8,16 +8,15 @@ import net.corda.core.internal.div
import net.corda.core.internal.noneOrSingle
import net.corda.core.internal.uncheckedCast
import net.corda.core.node.NodeInfo
-import net.corda.core.node.services.NetworkMapCache
import net.corda.core.node.services.NetworkMapCache.MapChange
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
-import net.corda.core.utilities.parsePublicKeyBase58
import net.corda.node.internal.Node
import net.corda.node.internal.security.Password
import net.corda.node.internal.security.RPCSecurityManager
+import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.messaging.NodeLoginModule.Companion.NODE_ROLE
import net.corda.node.services.messaging.NodeLoginModule.Companion.PEER_ROLE
@@ -31,7 +30,7 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.ArtemisPeerAddress
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS
-import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE
+import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
import net.corda.nodeapi.internal.ArtemisMessagingComponent.NodeAddress
@@ -94,7 +93,7 @@ import javax.security.cert.CertificateException
class ArtemisMessagingServer(private val config: NodeConfiguration,
private val p2pPort: Int,
val rpcPort: Int?,
- val networkMapCache: NetworkMapCache,
+ val networkMapCache: NetworkMapCacheInternal,
val securityManager: RPCSecurityManager,
val maxMessageSize: Int) : SingletonSerializeAsToken() {
companion object {
@@ -191,7 +190,6 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
// by having its password be an unknown securely random 128-bit value.
clusterPassword = BigInteger(128, newSecureRandom()).toString(16)
queueConfigurations = listOf(
- queueConfig(P2P_QUEUE, durable = true),
// Create an RPC queue: this will service locally connected clients only (not via a bridge) and those
// clients must have authenticated. We could use a single consumer for everything and perhaps we should,
// but these queues are not worth persisting.
@@ -243,7 +241,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
private fun ConfigurationImpl.configureAddressSecurity(): Pair {
val nodeInternalRole = Role(NODE_ROLE, true, true, true, true, true, true, true, true)
securityRoles["$INTERNAL_PREFIX#"] = setOf(nodeInternalRole) // Do not add any other roles here as it's only for the node
- securityRoles[P2P_QUEUE] = setOf(nodeInternalRole, restrictedRole(PEER_ROLE, send = true))
+ securityRoles["$P2P_PREFIX#"] = setOf(nodeInternalRole, restrictedRole(PEER_ROLE, send = true))
securityRoles[RPCApi.RPC_SERVER_QUEUE_NAME] = setOf(nodeInternalRole, restrictedRole(RPC_ROLE, send = true))
// Each RPC user must have its own role and its own queue. This prevents users accessing each other's queues
// and stealing RPC responses.
@@ -309,8 +307,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
if (queueName.startsWith(PEERS_PREFIX)) {
try {
- val identity = parsePublicKeyBase58(queueName.substring(PEERS_PREFIX.length))
- val nodeInfos = networkMapCache.getNodesByLegalIdentityKey(identity)
+ val nodeInfos = networkMapCache.getNodesByOwningKeyIndex(queueName.substring(PEERS_PREFIX.length))
if (nodeInfos.isNotEmpty()) {
nodeInfos.forEach { deployBridgeToPeer(it) }
} else {
diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/CoreBridgeManager.kt b/node/src/main/kotlin/net/corda/node/services/messaging/CoreBridgeManager.kt
index 4161d89836..b821d13cff 100644
--- a/node/src/main/kotlin/net/corda/node/services/messaging/CoreBridgeManager.kt
+++ b/node/src/main/kotlin/net/corda/node/services/messaging/CoreBridgeManager.kt
@@ -10,15 +10,17 @@ import net.corda.node.services.config.NodeConfiguration
import net.corda.nodeapi.ArtemisTcpTransport
import net.corda.nodeapi.ConnectionDirection
import net.corda.nodeapi.internal.ArtemisMessagingComponent
-import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
+import net.corda.nodeapi.internal.ArtemisMessagingComponent.RemoteInboxAddress.Companion.translateLocalQueueToInboxAddress
import net.corda.nodeapi.internal.crypto.X509Utilities
+import org.apache.activemq.artemis.api.core.Message
import org.apache.activemq.artemis.core.config.BridgeConfiguration
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnection
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnector
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants
import org.apache.activemq.artemis.core.server.ActiveMQServer
+import org.apache.activemq.artemis.core.server.cluster.Transformer
import org.apache.activemq.artemis.spi.core.remoting.*
import org.apache.activemq.artemis.utils.ConfigurationHelper
import java.time.Duration
@@ -46,7 +48,7 @@ internal class CoreBridgeManager(val config: NodeConfiguration, val activeMQServ
/**
- * All nodes are expected to have a public facing address called [ArtemisMessagingComponent.P2P_QUEUE] for receiving
+ * All nodes are expected to have a public facing address called p2p.inbound.$identity for receiving
* messages from other nodes. When we want to send a message to a node we send it to our internal address/queue for it,
* as defined by ArtemisAddress.queueName. A bridge is then created to forward messages from this queue to the node's
* P2P address.
@@ -64,7 +66,6 @@ internal class CoreBridgeManager(val config: NodeConfiguration, val activeMQServ
activeMQServer.deployBridge(BridgeConfiguration().apply {
name = getBridgeName(queueName, target)
this.queueName = queueName
- forwardingAddress = P2P_QUEUE
staticConnectors = listOf(target.toString())
confirmationWindowSize = 100000 // a guess
isUseDuplicateDetection = true // Enable the bridge's automatic deduplication logic
@@ -77,6 +78,7 @@ internal class CoreBridgeManager(val config: NodeConfiguration, val activeMQServ
// our TLS certificate.
user = PEER_USER
password = PEER_USER
+ transformerClassName = InboxTopicTransformer::class.java.name
})
}
@@ -99,6 +101,13 @@ internal class CoreBridgeManager(val config: NodeConfiguration, val activeMQServ
}
}
+class InboxTopicTransformer : Transformer {
+ override fun transform(message: Message): Message {
+ message.address = translateLocalQueueToInboxAddress(message.address)
+ return message
+ }
+}
+
class VerifyingNettyConnectorFactory : NettyConnectorFactory() {
override fun createConnector(configuration: MutableMap,
handler: BufferHandler?,
diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt
index 0e305c6f4c..b103254608 100644
--- a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt
+++ b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt
@@ -19,7 +19,6 @@ import net.corda.node.utilities.AffinityExecutor
import net.corda.node.utilities.AppendOnlyPersistentMap
import net.corda.node.utilities.PersistentMap
import net.corda.nodeapi.internal.ArtemisMessagingComponent.*
-import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException
@@ -54,21 +53,28 @@ import javax.persistence.Lob
* invoke methods on the provided implementation. There is more documentation on this in the docsite and the
* CordaRPCClient class.
*
- * @param serverAddress The address of the broker instance to connect to (might be running in the same process).
- * @param myIdentity The public key to be used as the ArtemisMQ address and queue name for the node.
- * @param nodeExecutor An executor to run received message tasks upon.
- * @param advertisedAddress The node address for inbound connections, advertised to the network map service and peers.
- * If not provided, will default to [serverAddress].
+ * @param config The configuration of the node, which is used for controlling the message redelivery options.
+ * @param versionInfo All messages from the node carry the version info and received messages are checked against this for compatibility.
+ * @param serverAddress The host and port of the Artemis broker.
+ * @param myIdentity The primary identity of the node, which defines the messaging address for externally received messages.
+ * It is also used to construct the myAddress field, which is ultimately advertised in the network map.
+ * @param serviceIdentity An optional second identity if the node is also part of a group address, for example a notary.
+ * @param nodeExecutor The received messages are marshalled onto the server executor to prevent Netty buffers leaking during fiber suspends.
+ * @param database The nodes database, which is used to deduplicate messages.
+ * @param advertisedAddress The externally advertised version of the Artemis broker address used to construct myAddress and included
+ * in the network map data.
+ * @param maxMessageSize A bound applied to the message size.
*/
@ThreadSafe
class P2PMessagingClient(config: NodeConfiguration,
private val versionInfo: VersionInfo,
serverAddress: NetworkHostAndPort,
private val myIdentity: PublicKey,
+ private val serviceIdentity: PublicKey?,
private val nodeExecutor: AffinityExecutor.ServiceAffinityExecutor,
private val database: CordaPersistence,
advertisedAddress: NetworkHostAndPort = serverAddress,
- private val maxMessageSize: Int
+ maxMessageSize: Int
) : SingletonSerializeAsToken(), MessagingService {
companion object {
private val log = contextLogger()
@@ -126,6 +132,7 @@ class P2PMessagingClient(config: NodeConfiguration,
private class InnerState {
var running = false
var p2pConsumer: ClientConsumer? = null
+ var serviceConsumer: ClientConsumer? = null
}
private val messagesToRedeliver = database.transaction {
@@ -181,8 +188,20 @@ class P2PMessagingClient(config: NodeConfiguration,
fun start() {
state.locked {
val session = artemis.start().session
+ val inbox = RemoteInboxAddress(myIdentity).queueName
// Create a queue, consumer and producer for handling P2P network messages.
- p2pConsumer = session.createConsumer(P2P_QUEUE)
+ createQueueIfAbsent(inbox)
+ p2pConsumer = session.createConsumer(inbox)
+ if (serviceIdentity != null) {
+ val serviceAddress = RemoteInboxAddress(serviceIdentity).queueName
+ createQueueIfAbsent(serviceAddress)
+ val serviceHandler = session.createConsumer(serviceAddress)
+ serviceHandler.setMessageHandler { msg ->
+ val message: ReceivedMessage? = artemisToCordaMessage(msg)
+ if (message != null)
+ deliver(msg, message)
+ }
+ }
}
resumeMessageRedelivery()
@@ -331,6 +350,13 @@ class P2PMessagingClient(config: NodeConfiguration,
// Ignore it: this can happen if the server has gone away before we do.
}
p2pConsumer = null
+ val s = serviceConsumer
+ try {
+ s?.close()
+ } catch (e: ActiveMQObjectClosedException) {
+ // Ignore it: this can happen if the server has gone away before we do.
+ }
+ serviceConsumer = null
prevRunning
}
if (running && !nodeExecutor.isOnThread) {
@@ -430,7 +456,7 @@ class P2PMessagingClient(config: NodeConfiguration,
private fun getMQAddress(target: MessageRecipients): String {
return if (target == myAddress) {
// If we are sending to ourselves then route the message directly to our P2P queue.
- P2P_QUEUE
+ RemoteInboxAddress(myIdentity).queueName
} else {
// Otherwise we send the message to an internal queue for the target residing on our broker. It's then the
// broker's job to route the message to the target's P2P queue.
@@ -447,7 +473,7 @@ class P2PMessagingClient(config: NodeConfiguration,
val queueQuery = session.queueQuery(SimpleString(queueName))
if (!queueQuery.isExists) {
log.info("Create fresh queue $queueName bound on same address")
- session.createQueue(queueName, RoutingType.MULTICAST, queueName, true)
+ session.createQueue(queueName, RoutingType.ANYCAST, queueName, true)
}
}
}
diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/VerifierMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/VerifierMessagingClient.kt
index 9a8c0cae66..9a8c82c4eb 100644
--- a/node/src/main/kotlin/net/corda/node/services/messaging/VerifierMessagingClient.kt
+++ b/node/src/main/kotlin/net/corda/node/services/messaging/VerifierMessagingClient.kt
@@ -7,15 +7,15 @@ import net.corda.core.transactions.LedgerTransaction
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.loggerFor
import net.corda.node.services.transactions.OutOfProcessTransactionVerifierService
-import net.corda.node.utilities.*
+import net.corda.node.utilities.AffinityExecutor
import net.corda.nodeapi.VerifierApi
import net.corda.nodeapi.VerifierApi.VERIFICATION_REQUESTS_QUEUE_NAME
import net.corda.nodeapi.VerifierApi.VERIFICATION_RESPONSES_QUEUE_NAME_PREFIX
import net.corda.nodeapi.internal.config.SSLConfiguration
import org.apache.activemq.artemis.api.core.RoutingType
import org.apache.activemq.artemis.api.core.SimpleString
-import org.apache.activemq.artemis.api.core.client.*
-import java.util.concurrent.*
+import org.apache.activemq.artemis.api.core.client.ClientConsumer
+import java.util.concurrent.TimeUnit
class VerifierMessagingClient(config: SSLConfiguration, serverAddress: NetworkHostAndPort, metrics: MetricRegistry, private val maxMessageSize: Int) : SingletonSerializeAsToken() {
companion object {
@@ -40,7 +40,7 @@ class VerifierMessagingClient(config: SSLConfiguration, serverAddress: NetworkHo
val queueQuery = session.queueQuery(SimpleString(queueName))
if (!queueQuery.isExists) {
log.info("Create fresh queue $queueName bound on same address")
- session.createQueue(queueName, RoutingType.MULTICAST, queueName, true)
+ session.createQueue(queueName, RoutingType.ANYCAST, queueName, true)
}
}
createQueueIfAbsent(VERIFICATION_REQUESTS_QUEUE_NAME)
diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt
index e6a1a9f972..0e6d2c2993 100644
--- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt
+++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt
@@ -160,6 +160,12 @@ open class PersistentNetworkMapCache(
private val nodesByKeyCache = NonInvalidatingCache>(1024, 8, { key -> database.transaction { queryByIdentityKey(session, key) } })
+ override fun getNodesByOwningKeyIndex(identityKeyIndex: String): List {
+ return database.transaction {
+ queryByIdentityKeyIndex(session, identityKeyIndex)
+ }
+ }
+
override fun getNodeByAddress(address: NetworkHostAndPort): NodeInfo? = database.transaction { queryByAddress(session, address) }
override fun getPeerCertificateByLegalName(name: CordaX500Name): PartyAndCertificate? = identityByLegalNameCache.get(name).orElse(null)
@@ -245,15 +251,23 @@ open class PersistentNetworkMapCache(
}
private fun findByIdentityKey(session: Session, identityKey: PublicKey): List {
+ return findByIdentityKeyIndex(session, identityKey.toStringShort())
+ }
+
+ private fun findByIdentityKeyIndex(session: Session, identityKeyIndex: String): List {
val query = session.createQuery(
"SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.owningKeyHash = :owningKeyHash",
NodeInfoSchemaV1.PersistentNodeInfo::class.java)
- query.setParameter("owningKeyHash", identityKey.toStringShort())
+ query.setParameter("owningKeyHash", identityKeyIndex)
return query.resultList
}
private fun queryByIdentityKey(session: Session, identityKey: PublicKey): List {
- val result = findByIdentityKey(session, identityKey)
+ return queryByIdentityKeyIndex(session, identityKey.toStringShort())
+ }
+
+ private fun queryByIdentityKeyIndex(session: Session, identityKeyIndex: String): List {
+ val result = findByIdentityKeyIndex(session, identityKeyIndex)
return result.map { it.toNodeInfo() }
}
@@ -279,13 +293,13 @@ open class PersistentNetworkMapCache(
private fun queryByAddress(session: Session, hostAndPort: NetworkHostAndPort): NodeInfo? {
val query = session.createQuery(
- "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.addresses a WHERE a.pk.host = :host AND a.pk.port = :port",
+ "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.addresses a WHERE a.host = :host AND a.port = :port",
NodeInfoSchemaV1.PersistentNodeInfo::class.java)
query.setParameter("host", hostAndPort.host)
query.setParameter("port", hostAndPort.port)
+ query.setMaxResults(1)
val result = query.resultList
- return if (result.isEmpty()) null
- else result.map { it.toNodeInfo() }.singleOrNull() ?: throw IllegalStateException("More than one node with the same host and port")
+ return result.map { it.toNodeInfo() }.singleOrNull()
}
/** Object Relational Mapping support. */
diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt
index 91b64b81e6..6fed52b7d8 100644
--- a/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt
+++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt
@@ -5,7 +5,6 @@ import net.corda.core.serialization.SerializedBytes
import net.corda.core.utilities.debug
import net.corda.node.services.api.CheckpointStorage
import net.corda.node.services.statemachine.Checkpoint
-import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
import net.corda.nodeapi.internal.persistence.currentDBSession
import org.slf4j.LoggerFactory
@@ -43,7 +42,7 @@ class DBCheckpointStorage : CheckpointStorage {
}
override fun removeCheckpoint(id: StateMachineRunId): Boolean {
- val session = DatabaseTransactionManager.current().session
+ val session = currentDBSession()
val criteriaBuilder = session.criteriaBuilder
val delete = criteriaBuilder.createCriteriaDelete(DBCheckpoint::class.java)
val root = delete.from(DBCheckpoint::class.java)
diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt
index da6dd489f8..60c5eb373a 100644
--- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt
+++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt
@@ -17,7 +17,6 @@ import net.corda.core.node.services.vault.AttachmentSort
import net.corda.core.serialization.*
import net.corda.core.utilities.contextLogger
import net.corda.node.services.vault.HibernateAttachmentQueryCriteriaParser
-import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
import net.corda.nodeapi.internal.persistence.currentDBSession
import java.io.*
@@ -242,8 +241,7 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single
override fun queryAttachments(criteria: AttachmentQueryCriteria, sorting: AttachmentSort?): List {
log.info("Attachment query criteria: $criteria, sorting: $sorting")
-
- val session = DatabaseTransactionManager.current().session
+ val session = currentDBSession()
val criteriaBuilder = session.criteriaBuilder
val criteriaQuery = criteriaBuilder.createQuery(DBAttachment::class.java)
diff --git a/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt b/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt
index eb58d0871e..62d5cee4d8 100644
--- a/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt
+++ b/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt
@@ -10,8 +10,8 @@ import net.corda.core.schemas.PersistentStateRef
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.node.services.api.SchemaService
-import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager
import net.corda.nodeapi.internal.persistence.HibernateConfiguration
+import net.corda.nodeapi.internal.persistence.contextTransaction
import org.hibernate.FlushMode
import rx.Observable
@@ -54,7 +54,7 @@ class HibernateObserver private constructor(private val config: HibernateConfigu
internal fun persistStatesWithSchema(statesAndRefs: List, schema: MappedSchema) {
val sessionFactory = config.sessionFactoryForSchemas(setOf(schema))
val session = sessionFactory.withOptions().
- connection(DatabaseTransactionManager.current().connection).
+ connection(contextTransaction.connection).
flushMode(FlushMode.MANUAL).
openSession()
session.use { thisSession ->
diff --git a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt
index 6f452ba8fe..c8ee5a16a9 100644
--- a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt
+++ b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt
@@ -29,24 +29,20 @@ import net.corda.node.services.vault.VaultSchemaV1
* TODO: support plugins for schema version upgrading or custom mapping not supported by original [QueryableState].
* TODO: create whitelisted tables when a CorDapp is first installed
*/
-class NodeSchemaService(extraSchemas: Set = emptySet()) : SchemaService, SingletonSerializeAsToken() {
- // Entities for compulsory services
- object NodeServices
+class NodeSchemaService(extraSchemas: Set = emptySet(), includeNotarySchemas: Boolean = false) : SchemaService, SingletonSerializeAsToken() {
+ // Core Entities used by a Node
+ object NodeCore
- object NodeServicesV1 : MappedSchema(schemaFamily = NodeServices.javaClass, version = 1,
+ object NodeCoreV1 : MappedSchema(schemaFamily = NodeCore.javaClass, version = 1,
mappedTypes = listOf(DBCheckpointStorage.DBCheckpoint::class.java,
DBTransactionStorage.DBTransaction::class.java,
DBTransactionMappingStorage.DBTransactionMapping::class.java,
PersistentKeyManagementService.PersistentKey::class.java,
- PersistentUniquenessProvider.PersistentUniqueness::class.java,
- PersistentUniquenessProvider.PersistentNotaryCommit::class.java,
NodeSchedulerService.PersistentScheduledState::class.java,
NodeAttachmentService.DBAttachment::class.java,
P2PMessagingClient.ProcessedMessage::class.java,
P2PMessagingClient.RetryMessage::class.java,
NodeAttachmentService.DBAttachment::class.java,
- RaftUniquenessProvider.RaftState::class.java,
- BFTNonValidatingNotaryService.PersistedCommittedState::class.java,
PersistentIdentityService.PersistentIdentity::class.java,
PersistentIdentityService.PersistentIdentityNames::class.java,
ContractUpgradeServiceImpl.DBContractUpgrade::class.java,
@@ -67,15 +63,25 @@ class NodeSchemaService(extraSchemas: Set = emptySet()) : SchemaSe
override val migrationResource = "node-notary.changelog-master"
}
+ // Entities used by a Notary
+ object NodeNotary
+
+ object NodeNotaryV1 : MappedSchema(schemaFamily = NodeNotary.javaClass, version = 1,
+ mappedTypes = listOf(PersistentUniquenessProvider.PersistentUniqueness::class.java,
+ PersistentUniquenessProvider.PersistentNotaryCommit::class.java,
+ RaftUniquenessProvider.RaftState::class.java,
+ BFTNonValidatingNotaryService.PersistedCommittedState::class.java
+ ))
+
// Required schemas are those used by internal Corda services
- // For example, cash is used by the vault for coin selection (but will be extracted as a standalone CorDapp in future)
private val requiredSchemas: Map =
mapOf(Pair(CommonSchemaV1, SchemaOptions()),
- Pair(VaultSchemaV1, SchemaOptions()),
- Pair(NodeInfoSchemaV1, SchemaOptions()),
- Pair(NodeServicesV1, SchemaOptions()))
+ Pair(VaultSchemaV1, SchemaOptions()),
+ Pair(NodeInfoSchemaV1, SchemaOptions()),
+ Pair(NodeCoreV1, SchemaOptions()))
+ private val notarySchemas = if (includeNotarySchemas) mapOf(Pair(NodeNotaryV1, SchemaOptions())) else emptyMap()
- override val schemaOptions: Map = requiredSchemas + extraSchemas.associateBy({ it }, { SchemaOptions() })
+ override val schemaOptions: Map = requiredSchemas + notarySchemas + extraSchemas.associateBy({ it }, { SchemaOptions() })
// Currently returns all schemas supported by the state, with no filtering or enrichment.
override fun selectSchemas(state: ContractState): Iterable {
diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt
index 6449a8b090..6efdc09f7b 100644
--- a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt
+++ b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt
@@ -10,7 +10,9 @@ import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.trace
import net.corda.node.services.api.CheckpointStorage
import net.corda.node.services.api.ServiceHubInternal
-import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager
+import net.corda.nodeapi.internal.persistence.contextDatabase
+import net.corda.nodeapi.internal.persistence.contextTransaction
+import net.corda.nodeapi.internal.persistence.contextTransactionOrNull
import java.time.Duration
import java.time.Instant
import java.util.concurrent.TimeUnit
@@ -163,24 +165,24 @@ class ActionExecutorImpl(
@Suspendable
private fun executeCreateTransaction() {
- if (DatabaseTransactionManager.currentOrNull() != null) {
+ if (contextTransactionOrNull != null) {
throw IllegalStateException("Refusing to create a second transaction")
}
- DatabaseTransactionManager.newTransaction()
+ contextDatabase.newTransaction()
}
@Suspendable
private fun executeRollbackTransaction() {
- DatabaseTransactionManager.currentOrNull()?.close()
+ contextTransactionOrNull?.close()
}
@Suspendable
private fun executeCommitTransaction() {
try {
- DatabaseTransactionManager.current().commit()
+ contextTransaction.commit()
} finally {
- DatabaseTransactionManager.current().close()
- DatabaseTransactionManager.setThreadLocalTx(null)
+ contextTransaction.close()
+ contextTransactionOrNull = null
}
}
diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt
index 4b0408c622..dca556ea59 100644
--- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt
+++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt
@@ -27,7 +27,8 @@ import net.corda.node.services.statemachine.transitions.FlowContinuation
import net.corda.node.services.statemachine.transitions.StateMachine
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseTransaction
-import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager
+import net.corda.nodeapi.internal.persistence.contextTransaction
+import net.corda.nodeapi.internal.persistence.contextTransactionOrNull
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.nio.file.Paths
@@ -58,8 +59,8 @@ class FlowStateMachineImpl(override val id: StateMachineRunId,
}
private fun extractThreadLocalTransaction(): TransientReference {
- val transaction = DatabaseTransactionManager.current()
- DatabaseTransactionManager.setThreadLocalTx(null)
+ val transaction = contextTransaction
+ contextTransactionOrNull = null
return TransientReference(transaction)
}
}
@@ -234,7 +235,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId,
parkAndSerialize { _, _ ->
logger.trace { "Suspended on $ioRequest" }
- DatabaseTransactionManager.setThreadLocalTx(transaction.value)
+ contextTransactionOrNull = transaction.value
val event = try {
Event.Suspend(
ioRequest = ioRequest,
diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/TransitionExecutorImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/TransitionExecutorImpl.kt
index b895a40863..2cf328a450 100644
--- a/node/src/main/kotlin/net/corda/node/services/statemachine/TransitionExecutorImpl.kt
+++ b/node/src/main/kotlin/net/corda/node/services/statemachine/TransitionExecutorImpl.kt
@@ -5,7 +5,8 @@ import net.corda.core.utilities.contextLogger
import net.corda.node.services.statemachine.transitions.FlowContinuation
import net.corda.node.services.statemachine.transitions.TransitionResult
import net.corda.nodeapi.internal.persistence.CordaPersistence
-import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager
+import net.corda.nodeapi.internal.persistence.contextDatabase
+import net.corda.nodeapi.internal.persistence.contextTransactionOrNull
import java.security.SecureRandom
/**
@@ -31,12 +32,12 @@ class TransitionExecutorImpl(
transition: TransitionResult,
actionExecutor: ActionExecutor
): Pair {
- DatabaseTransactionManager.dataSource = database
+ contextDatabase = database
for (action in transition.actions) {
try {
actionExecutor.executeAction(fiber, action)
} catch (exception: Throwable) {
- DatabaseTransactionManager.currentOrNull()?.close()
+ contextTransactionOrNull?.close()
if (transition.newState.checkpoint.errorState is ErrorState.Errored) {
// If we errored while transitioning to an error state then we cannot record the additional
// error as that may result in an infinite loop, e.g. error propagation fails -> record error -> propagate fails again.
diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/MySQLNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/MySQLNotaryService.kt
index adfe3e9744..f32dac307b 100644
--- a/node/src/main/kotlin/net/corda/node/services/transactions/MySQLNotaryService.kt
+++ b/node/src/main/kotlin/net/corda/node/services/transactions/MySQLNotaryService.kt
@@ -5,6 +5,7 @@ import net.corda.core.flows.FlowSession
import net.corda.core.node.services.TimeWindowChecker
import net.corda.core.node.services.TrustedAuthorityNotaryService
import net.corda.node.services.api.ServiceHubInternal
+import net.corda.node.services.config.MySQLConfiguration
import java.security.PublicKey
import java.util.*
@@ -12,14 +13,14 @@ import java.util.*
abstract class MySQLNotaryService(
final override val services: ServiceHubInternal,
override val notaryIdentityKey: PublicKey,
- dataSourceProperties: Properties,
+ configuration: MySQLConfiguration,
/** Database table will be automatically created in dev mode */
val devMode: Boolean) : TrustedAuthorityNotaryService() {
override val timeWindowChecker = TimeWindowChecker(services.clock)
override val uniquenessProvider = MySQLUniquenessProvider(
services.monitoringService.metrics,
- dataSourceProperties
+ configuration
)
override fun start() {
@@ -33,14 +34,14 @@ abstract class MySQLNotaryService(
class MySQLNonValidatingNotaryService(services: ServiceHubInternal,
notaryIdentityKey: PublicKey,
- dataSourceProperties: Properties,
- devMode: Boolean = false) : MySQLNotaryService(services, notaryIdentityKey, dataSourceProperties, devMode) {
+ configuration: MySQLConfiguration,
+ devMode: Boolean = false) : MySQLNotaryService(services, notaryIdentityKey, configuration, devMode) {
override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic = NonValidatingNotaryFlow(otherPartySession, this)
}
class MySQLValidatingNotaryService(services: ServiceHubInternal,
notaryIdentityKey: PublicKey,
- dataSourceProperties: Properties,
- devMode: Boolean = false) : MySQLNotaryService(services, notaryIdentityKey, dataSourceProperties, devMode) {
+ configuration: MySQLConfiguration,
+ devMode: Boolean = false) : MySQLNotaryService(services, notaryIdentityKey, configuration, devMode) {
override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic = ValidatingNotaryFlow(otherPartySession, this)
}
\ No newline at end of file
diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/MySQLUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/MySQLUniquenessProvider.kt
index 33fe84d79c..5b8e2be796 100644
--- a/node/src/main/kotlin/net/corda/node/services/transactions/MySQLUniquenessProvider.kt
+++ b/node/src/main/kotlin/net/corda/node/services/transactions/MySQLUniquenessProvider.kt
@@ -14,9 +14,11 @@ import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.utilities.loggerFor
+import net.corda.node.services.config.MySQLConfiguration
import java.security.PublicKey
import java.sql.BatchUpdateException
import java.sql.Connection
+import java.sql.SQLTransientConnectionException
import java.util.*
/**
@@ -27,7 +29,7 @@ import java.util.*
*/
class MySQLUniquenessProvider(
metrics: MetricRegistry,
- dataSourceProperties: Properties
+ configuration: MySQLConfiguration
) : UniquenessProvider, SingletonSerializeAsToken() {
companion object {
private val log = loggerFor()
@@ -57,13 +59,29 @@ class MySQLUniquenessProvider(
* This is a useful heath metric.
*/
private val rollbackCounter = metrics.counter("$metricPrefix.Rollback")
+ /** Incremented when we can not obtain a DB connection. */
+ private val connectionExceptionCounter = metrics.counter("$metricPrefix.ConnectionException")
/** Track double spend attempts. Note that this will also include notarisation retries. */
private val conflictCounter = metrics.counter("$metricPrefix.Conflicts")
- val dataSource = HikariDataSource(HikariConfig(dataSourceProperties))
+ val dataSource = HikariDataSource(HikariConfig(configuration.dataSource))
+ private val connectionRetries = configuration.connectionRetries
private val connection: Connection
- get() = dataSource.connection
+ get() = getConnection()
+
+ private fun getConnection(nRetries: Int = 0): Connection =
+ try {
+ dataSource.connection
+ } catch (e: SQLTransientConnectionException) {
+ if (nRetries == connectionRetries) {
+ log.warn("Couldn't obtain connection with {} retries, giving up, {}", nRetries, e)
+ throw e
+ }
+ log.warn("Connection exception, retrying", nRetries+1)
+ connectionExceptionCounter.inc()
+ getConnection(nRetries + 1)
+ }
fun createTable() {
log.debug("Attempting to create DB table if it does not yet exist: $createTableStatement")
diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt
index f215d87cee..a731f4841e 100644
--- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt
+++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt
@@ -17,12 +17,8 @@ import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.*
import net.corda.node.services.api.VaultServiceInternal
-import net.corda.nodeapi.internal.persistence.HibernateConfiguration
import net.corda.node.services.statemachine.FlowStateMachineImpl
-import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager
-import net.corda.nodeapi.internal.persistence.bufferUntilDatabaseCommit
-import net.corda.nodeapi.internal.persistence.currentDBSession
-import net.corda.nodeapi.internal.persistence.wrapWithDatabaseTransaction
+import net.corda.nodeapi.internal.persistence.*
import org.hibernate.Session
import rx.Observable
import rx.subjects.PublishSubject
@@ -479,8 +475,7 @@ class NodeVaultService(
}
}
- private fun getSession() = DatabaseTransactionManager.currentOrNew().session
-
+ private fun getSession() = contextDatabase.currentOrNew().session
/**
* Derive list from existing vault states and then incrementally update using vault observables
*/
diff --git a/node/src/main/kotlin/net/corda/node/services/vault/VaultSoftLockManager.kt b/node/src/main/kotlin/net/corda/node/services/vault/VaultSoftLockManager.kt
index 7e13cfab85..33cd73e74b 100644
--- a/node/src/main/kotlin/net/corda/node/services/vault/VaultSoftLockManager.kt
+++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSoftLockManager.kt
@@ -10,7 +10,7 @@ import net.corda.core.utilities.toNonEmptySet
import net.corda.core.utilities.trace
import net.corda.node.services.statemachine.FlowStateMachineImpl
import net.corda.node.services.statemachine.StateMachineManager
-import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager
+import net.corda.nodeapi.internal.persistence.contextDatabase
import java.util.*
class VaultSoftLockManager private constructor(private val vault: VaultService) {
@@ -52,14 +52,14 @@ class VaultSoftLockManager private constructor(private val vault: VaultService)
private fun registerSoftLocks(flowId: UUID, stateRefs: NonEmptySet) {
log.trace { "Reserving soft locks for flow id $flowId and states $stateRefs" }
- DatabaseTransactionManager.dataSource.transaction {
+ contextDatabase.transaction {
vault.softLockReserve(flowId, stateRefs)
}
}
private fun unregisterSoftLocks(flowId: UUID, logic: FlowLogic<*>) {
log.trace { "Releasing soft locks for flow ${logic.javaClass.simpleName} with flow id $flowId" }
- DatabaseTransactionManager.dataSource.transaction {
+ contextDatabase.transaction {
vault.softLockRelease(flowId)
}
}
diff --git a/node/src/main/resources/migration/node-info.changelog-init.xml b/node/src/main/resources/migration/node-info.changelog-init.xml
index 0396ad675c..a4bea223d2 100644
--- a/node/src/main/resources/migration/node-info.changelog-init.xml
+++ b/node/src/main/resources/migration/node-info.changelog-init.xml
@@ -67,4 +67,17 @@
constraintName="FK__info_hosts__infos"
referencedColumnNames="node_info_id" referencedTableName="node_infos"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/node/src/main/resources/reference.conf b/node/src/main/resources/reference.conf
index 7ce5aaa6b7..eab9ede6e0 100644
--- a/node/src/main/resources/reference.conf
+++ b/node/src/main/resources/reference.conf
@@ -14,7 +14,6 @@ database = {
exportHibernateJMXStatistics = "false"
}
devMode = true
-useHTTPS = false
h2port = 0
useTestClock = false
verifierType = InMemory
diff --git a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt
index 18388b6338..9df1ef43c6 100644
--- a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt
+++ b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt
@@ -76,7 +76,6 @@ class NodeConfigurationImplTest {
dataSourceProperties = makeTestDataSourceProperties(ALICE_NAME.organisation),
rpcUsers = emptyList(),
verifierType = VerifierType.InMemory,
- useHTTPS = false,
p2pAddress = NetworkHostAndPort("localhost", 0),
rpcAddress = NetworkHostAndPort("localhost", 1),
messagingServerAddress = null,
diff --git a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt
index 1fb6840b71..255e47bcbc 100644
--- a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt
+++ b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt
@@ -178,6 +178,7 @@ class ArtemisMessagingTests {
MOCK_VERSION_INFO.copy(platformVersion = platformVersion),
server,
identity.public,
+ null,
ServiceAffinityExecutor("ArtemisMessagingTests", 1),
database,
maxMessageSize = maxMessageSize).apply {
diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt
index eb95a985dd..b56728fcf6 100644
--- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt
+++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt
@@ -1,9 +1,11 @@
package net.corda.node.services.network
+import net.corda.core.crypto.generateKeyPair
import net.corda.core.node.services.NetworkMapCache
import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.testing.ALICE_NAME
import net.corda.testing.BOB_NAME
+import net.corda.testing.getTestPartyAndCertificate
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockNodeParameters
import net.corda.testing.singleIdentity
@@ -106,4 +108,14 @@ class NetworkMapCacheTest {
assertThat(bobCache.getNodeByLegalName(alice.name) == null)
}
}
+
+ @Test
+ fun `add two nodes the same name different keys`() {
+ val aliceNode = mockNet.createPartyNode(ALICE_NAME)
+ val aliceCache = aliceNode.services.networkMapCache
+ val alicePartyAndCert2 = getTestPartyAndCertificate(ALICE_NAME, generateKeyPair().public)
+ aliceCache.addNode(aliceNode.info.copy(legalIdentitiesAndCerts = listOf(alicePartyAndCert2)))
+ // This is correct behaviour as we may have distributed service nodes.
+ assertEquals(2, aliceCache.getNodesByLegalName(ALICE_NAME).size)
+ }
}
diff --git a/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt b/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt
index 0a657ad95e..c83b75b89f 100644
--- a/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt
+++ b/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt
@@ -15,7 +15,6 @@ import net.corda.core.schemas.QueryableState
import net.corda.node.services.api.SchemaService
import net.corda.node.internal.configureDatabase
import net.corda.nodeapi.internal.persistence.DatabaseConfig
-import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager
import net.corda.testing.internal.LogHelper
import net.corda.testing.TestIdentity
import net.corda.testing.contracts.DummyContract
@@ -74,11 +73,11 @@ class HibernateObserverTests {
database.transaction {
val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party
rawUpdatesPublisher.onNext(Vault.Update(emptySet(), setOf(StateAndRef(TransactionState(TestState(), DummyContract.PROGRAM_ID, MEGA_CORP), StateRef(SecureHash.sha256("dummy"), 0)))))
- val parentRowCountResult = DatabaseTransactionManager.current().connection.prepareStatement("select count(*) from Parents").executeQuery()
+ val parentRowCountResult = connection.prepareStatement("select count(*) from Parents").executeQuery()
parentRowCountResult.next()
val parentRows = parentRowCountResult.getInt(1)
parentRowCountResult.close()
- val childrenRowCountResult = DatabaseTransactionManager.current().connection.prepareStatement("select count(*) from Children").executeQuery()
+ val childrenRowCountResult = connection.prepareStatement("select count(*) from Children").executeQuery()
childrenRowCountResult.next()
val childrenRows = childrenRowCountResult.getInt(1)
childrenRowCountResult.close()
diff --git a/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt
index ac9a88afe0..521a1f34b3 100644
--- a/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt
+++ b/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt
@@ -9,15 +9,19 @@ import net.corda.core.schemas.MappedSchema
import net.corda.core.schemas.PersistentState
import net.corda.core.utilities.getOrThrow
import net.corda.node.services.api.ServiceHubInternal
+import net.corda.node.services.schema.NodeSchemaService.NodeCoreV1
+import net.corda.node.services.schema.NodeSchemaService.NodeNotaryV1
import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.driver
-import net.corda.testing.node.MockNetwork
import net.corda.testing.internal.vault.DummyLinearStateSchemaV1
+import net.corda.testing.node.MockNetwork
import org.hibernate.annotations.Cascade
import org.hibernate.annotations.CascadeType
+import org.junit.Ignore
import org.junit.Test
import javax.persistence.*
import kotlin.test.assertEquals
+import kotlin.test.assertFalse
import kotlin.test.assertTrue
class NodeSchemaServiceTest {
@@ -30,7 +34,30 @@ class NodeSchemaServiceTest {
val mockNode = mockNet.createNode()
val schemaService = mockNode.services.schemaService
assertTrue(schemaService.schemaOptions.containsKey(DummyLinearStateSchemaV1))
+ mockNet.stopNodes()
+ }
+ @Test
+ fun `check node runs with minimal core schema set`() {
+ val mockNet = MockNetwork(cordappPackages = emptyList())
+ val mockNode = mockNet.createNode()
+ val schemaService = mockNode.services.schemaService
+
+ // check against NodeCore schemas
+ assertTrue(schemaService.schemaOptions.containsKey(NodeCoreV1))
+ assertFalse(schemaService.schemaOptions.containsKey(NodeNotaryV1))
+ mockNet.stopNodes()
+ }
+
+ @Test
+ fun `check node runs inclusive of notary node schema set`() {
+ val mockNet = MockNetwork(cordappPackages = emptyList())
+ val mockNotaryNode = mockNet.notaryNodes.first()
+ val schemaService = mockNotaryNode.services.schemaService
+
+ // check against NodeCore + NodeNotary Schemas
+ assertTrue(schemaService.schemaOptions.containsKey(NodeCoreV1))
+ assertTrue(schemaService.schemaOptions.containsKey(NodeNotaryV1))
mockNet.stopNodes()
}
@@ -59,6 +86,34 @@ class NodeSchemaServiceTest {
assertEquals>(expected, tables.toMutableSet().apply { retainAll(expected) })
}
+ @Ignore
+ @Test
+ fun `check node runs with minimal core schema set using driverDSL`() {
+ // TODO: driver limitation: cannot restrict CorDapps that get automatically created by default,
+ // can ONLY specify additional ones using `extraCordappPackagesToScan` constructor argument.
+ driver(startNodesInProcess = true, notarySpecs = emptyList()) {
+ val node = startNode().getOrThrow()
+ val result = node.rpc.startFlow(::MappedSchemasFlow)
+ val mappedSchemas = result.returnValue.getOrThrow()
+ // check against NodeCore schemas
+ assertTrue(mappedSchemas.contains(NodeCoreV1.name))
+ assertFalse(mappedSchemas.contains(NodeNotaryV1.name)) // still gets loaded due TODO restriction
+ }
+
+ }
+
+ @Test
+ fun `check node runs inclusive of notary node schema set using driverDSL`() {
+ driver(startNodesInProcess = true) {
+ val notaryNode = defaultNotaryNode.getOrThrow().rpc.startFlow(::MappedSchemasFlow)
+ val mappedSchemas = notaryNode.returnValue.getOrThrow()
+ // check against NodeCore + NodeNotary Schemas
+ assertTrue(mappedSchemas.contains(NodeCoreV1.name))
+ assertTrue(mappedSchemas.contains(NodeNotaryV1.name))
+ }
+
+ }
+
@StartableByRPC
class MappedSchemasFlow : FlowLogic>() {
@Suspendable
diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt
index 1c60db0ba6..6b78bb53c2 100644
--- a/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt
+++ b/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt
@@ -11,6 +11,7 @@ import net.corda.core.internal.concurrent.transpose
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.getOrThrow
import net.corda.node.internal.configureDatabase
+import net.corda.node.services.schema.NodeSchemaService
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.internal.LogHelper
@@ -88,7 +89,7 @@ class DistributedImmutableMapTests {
private fun createReplica(myAddress: NetworkHostAndPort, clusterAddress: NetworkHostAndPort? = null): CompletableFuture {
val storage = Storage.builder().withStorageLevel(StorageLevel.MEMORY).build()
val address = Address(myAddress.host, myAddress.port)
- val database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true), rigorousMock())
+ val database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true), rigorousMock(), NodeSchemaService(includeNotarySchemas = true))
databases.add(database)
val stateMachineFactory = { DistributedImmutableMap(database, RaftUniquenessProvider.Companion::createMap) }
diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt
index 7e25d42c9e..cf833dbcbe 100644
--- a/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt
+++ b/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt
@@ -4,6 +4,7 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name
import net.corda.core.node.services.UniquenessException
import net.corda.node.internal.configureDatabase
+import net.corda.node.services.schema.NodeSchemaService
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.*
@@ -29,7 +30,7 @@ class PersistentUniquenessProviderTests {
@Before
fun setUp() {
LogHelper.setLevel(PersistentUniquenessProvider::class)
- database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true), rigorousMock())
+ database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true), rigorousMock(), NodeSchemaService(includeNotarySchemas = true))
}
@After
diff --git a/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt b/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt
index 68918167f0..9aeb4a96d8 100644
--- a/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt
+++ b/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt
@@ -16,9 +16,7 @@ import java.io.Closeable
import java.util.*
class ObservablesTests {
-
- private fun isInDatabaseTransaction(): Boolean = (DatabaseTransactionManager.currentOrNull() != null)
-
+ private fun isInDatabaseTransaction() = contextTransactionOrNull != null
private val toBeClosed = mutableListOf()
private fun createDatabase(): CordaPersistence {
@@ -168,7 +166,7 @@ class ObservablesTests {
observableWithDbTx.first().subscribe { undelayedEvent.set(it to isInDatabaseTransaction()) }
fun observeSecondEvent(event: Int, future: SettableFuture>) {
- future.set(event to if (isInDatabaseTransaction()) DatabaseTransactionManager.transactionId else null)
+ future.set(event to if (isInDatabaseTransaction()) contextTransaction.id else null)
}
observableWithDbTx.skip(1).first().subscribe { observeSecondEvent(it, delayedEventFromSecondObserver) }
diff --git a/samples/irs-demo/src/integration-test/kotlin/net/corda/test/spring/SpringDriver.kt b/samples/irs-demo/src/integration-test/kotlin/net/corda/test/spring/SpringDriver.kt
index c7d7a57ffd..9d83d481ba 100644
--- a/samples/irs-demo/src/integration-test/kotlin/net/corda/test/spring/SpringDriver.kt
+++ b/samples/irs-demo/src/integration-test/kotlin/net/corda/test/spring/SpringDriver.kt
@@ -71,7 +71,7 @@ data class SpringBootDriverDSL(private val driverDSL: DriverDSLImpl) : InternalD
}
private fun queryWebserver(handle: NodeHandle, process: Process, checkUrl: String): WebserverHandle {
- val protocol = if (handle.configuration.useHTTPS) "https://" else "http://"
+ val protocol = if (handle.useHTTPS) "https://" else "http://"
val url = URL(URL("$protocol${handle.webAddress}"), checkUrl)
val client = OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build()
diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt
index 4a8c6101c9..a7316c18f4 100644
--- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt
+++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt
@@ -15,8 +15,8 @@ import net.corda.node.internal.StartedNode
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.VerifierType
import net.corda.testing.DUMMY_NOTARY_NAME
-import net.corda.testing.node.User
import net.corda.testing.node.NotarySpec
+import net.corda.testing.node.User
import net.corda.testing.node.internal.DriverDSLImpl
import net.corda.testing.node.internal.genericDriver
import net.corda.testing.node.internal.getTimestampAsDirectoryName
@@ -41,6 +41,7 @@ sealed class NodeHandle {
abstract val rpc: CordaRPCOps
abstract val configuration: NodeConfiguration
abstract val webAddress: NetworkHostAndPort
+ abstract val useHTTPS: Boolean
/**
* Stops the referenced node.
@@ -52,6 +53,7 @@ sealed class NodeHandle {
override val rpc: CordaRPCOps,
override val configuration: NodeConfiguration,
override val webAddress: NetworkHostAndPort,
+ override val useHTTPS: Boolean,
val debugPort: Int?,
val process: Process,
private val onStopCallback: () -> Unit
@@ -70,6 +72,7 @@ sealed class NodeHandle {
override val rpc: CordaRPCOps,
override val configuration: NodeConfiguration,
override val webAddress: NetworkHostAndPort,
+ override val useHTTPS: Boolean,
val node: StartedNode,
val nodeThread: Thread,
private val onStopCallback: () -> Unit
diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt
index f9df016fdb..962469c20e 100644
--- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt
+++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt
@@ -330,7 +330,7 @@ class DriverDSLImpl(
}
private fun queryWebserver(handle: NodeHandle, process: Process): WebserverHandle {
- val protocol = if (handle.configuration.useHTTPS) "https://" else "http://"
+ val protocol = if (handle.useHTTPS) "https://" else "http://"
val url = URL("$protocol${handle.webAddress}/api/status")
val client = OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS).readTimeout(60, TimeUnit.SECONDS).build()
@@ -604,10 +604,14 @@ class DriverDSLImpl(
val baseDirectory = config.corda.baseDirectory.createDirectories()
localNetworkMap?.networkParametersCopier?.install(baseDirectory)
localNetworkMap?.nodeInfosCopier?.addConfig(baseDirectory)
+
val onNodeExit: () -> Unit = {
localNetworkMap?.nodeInfosCopier?.removeConfig(baseDirectory)
countObservables.remove(config.corda.myLegalName)
}
+
+ val useHTTPS = config.typesafe.run { hasPath("useHTTPS") && getBoolean("useHTTPS") }
+
if (startInProcess ?: startNodesInProcess) {
val nodeAndThreadFuture = startInProcessNode(executorService, config, cordappPackages)
shutdownManager.registerShutdown(
@@ -621,7 +625,7 @@ class DriverDSLImpl(
return nodeAndThreadFuture.flatMap { (node, thread) ->
establishRpc(config, openFuture()).flatMap { rpc ->
allNodesConnected(rpc).map {
- NodeHandle.InProcess(rpc.nodeInfo(), rpc, config.corda, webAddress, node, thread, onNodeExit)
+ NodeHandle.InProcess(rpc.nodeInfo(), rpc, config.corda, webAddress, useHTTPS, node, thread, onNodeExit)
}
}
}
@@ -650,7 +654,7 @@ class DriverDSLImpl(
}
processDeathFuture.cancel(false)
log.info("Node handle is ready. NodeInfo: ${rpc.nodeInfo()}, WebAddress: $webAddress")
- NodeHandle.OutOfProcess(rpc.nodeInfo(), rpc, config.corda, webAddress, debugPort, process, onNodeExit)
+ NodeHandle.OutOfProcess(rpc.nodeInfo(), rpc, config.corda, webAddress, useHTTPS, debugPort, process, onNodeExit)
}
}
}
diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt
index 60a7c40b00..1d73ec5363 100644
--- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt
+++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt
@@ -2,6 +2,7 @@ package net.corda.testing.internal
import com.nhaarman.mockito_kotlin.doAnswer
import net.corda.core.crypto.Crypto
+import net.corda.core.crypto.Crypto.generateKeyPair
import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.loggerFor
import net.corda.node.services.config.configureDevKeyAndTrustStores
@@ -15,6 +16,7 @@ import org.mockito.Mockito
import org.mockito.internal.stubbing.answers.ThrowsException
import java.lang.reflect.Modifier
import java.nio.file.Files
+import java.security.KeyPair
import java.util.*
import javax.security.auth.x500.X500Principal
@@ -102,11 +104,12 @@ fun createDevIntermediateCaCertPath(
*/
fun createDevNodeCaCertPath(
legalName: CordaX500Name,
+ nodeKeyPair: KeyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME),
rootCaName: X500Principal = defaultRootCaName,
intermediateCaName: X500Principal = defaultIntermediateCaName
): Triple {
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath(rootCaName, intermediateCaName)
- val nodeCa = createDevNodeCa(intermediateCa, legalName)
+ val nodeCa = createDevNodeCa(intermediateCa, legalName, nodeKeyPair)
return Triple(rootCa, intermediateCa, nodeCa)
}
diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt
index bdeba56fc6..3eb84a4471 100644
--- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt
+++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt
@@ -8,16 +8,35 @@ import net.corda.core.node.NodeInfo
import net.corda.core.serialization.serialize
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.nodeapi.internal.SignedNodeInfo
-import net.corda.testing.getTestPartyAndCertificate
+import net.corda.nodeapi.internal.createDevNodeCa
+import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
+import net.corda.nodeapi.internal.crypto.CertificateType
+import net.corda.nodeapi.internal.crypto.X509CertificateFactory
+import net.corda.nodeapi.internal.crypto.X509Utilities
+import net.corda.testing.DEV_INTERMEDIATE_CA
+import net.corda.testing.DEV_ROOT_CA
+import java.security.KeyPair
import java.security.PrivateKey
+import java.security.cert.X509Certificate
-class TestNodeInfoBuilder {
+class TestNodeInfoBuilder(private val intermediateAndRoot: Pair = DEV_INTERMEDIATE_CA to DEV_ROOT_CA.certificate) {
private val identitiesAndPrivateKeys = ArrayList>()
- fun addIdentity(name: CordaX500Name): Pair {
+ fun addIdentity(name: CordaX500Name, nodeKeyPair: KeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)): Pair {
+ val nodeCertificateAndKeyPair = createDevNodeCa(intermediateAndRoot.first, name, nodeKeyPair)
val identityKeyPair = Crypto.generateKeyPair()
- val identity = getTestPartyAndCertificate(name, identityKeyPair.public)
- return Pair(identity, identityKeyPair.private).also {
+ val identityCert = X509Utilities.createCertificate(
+ CertificateType.LEGAL_IDENTITY,
+ nodeCertificateAndKeyPair.certificate,
+ nodeCertificateAndKeyPair.keyPair,
+ nodeCertificateAndKeyPair.certificate.subjectX500Principal,
+ identityKeyPair.public)
+ val certPath = X509CertificateFactory()
+ .generateCertPath(identityCert,
+ nodeCertificateAndKeyPair.certificate,
+ intermediateAndRoot.first.certificate,
+ intermediateAndRoot.second)
+ return Pair(PartyAndCertificate(certPath), identityKeyPair.private).also {
identitiesAndPrivateKeys += it
}
}