From 32b57feaa017f88e799df02aa5944bd79e185f95 Mon Sep 17 00:00:00 2001 From: jamesbr3 <33660060+jamesbr3@users.noreply.github.com> Date: Mon, 30 Apr 2018 10:24:08 +0100 Subject: [PATCH 01/25] Update jolokia to 1.5.0 (#2617) * Update jolokia to 1.5.0 * adding jolokia update to changelog --- docs/source/changelog.rst | 2 ++ node/src/main/resources/build.properties | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index debca55b65..53dadbeead 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -287,6 +287,8 @@ Version 3.0 * ``TransactionSignature`` includes a new ``partialMerkleTree`` property, required for future support of signing over multiple transactions at once. +* Updating Jolokia dependency to latest version (includes security fixes) + .. _changelog_v1: Release 1.0 diff --git a/node/src/main/resources/build.properties b/node/src/main/resources/build.properties index 72577915e7..5f1344f9d4 100644 --- a/node/src/main/resources/build.properties +++ b/node/src/main/resources/build.properties @@ -2,4 +2,4 @@ # Note: sadly, due to present limitation of IntelliJ-IDEA in processing resource files, these constants cannot be # imported from top-level 'constants.properties' file -jolokiaAgentVersion=1.3.7 \ No newline at end of file +jolokiaAgentVersion=1.5.0 From 0a8043ccc44b0efa3a86ed8d5cd43e1d5d448e54 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Mon, 30 Apr 2018 11:05:57 +0100 Subject: [PATCH 02/25] CORDA-1261: Document bootstrapping cannot use the same H2 port (#3032) --- docs/source/setting-up-a-corda-network.rst | 5 +++-- docs/source/tutorial-cordapp.rst | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/source/setting-up-a-corda-network.rst b/docs/source/setting-up-a-corda-network.rst index 7a227d45e3..492bc4e24a 100644 --- a/docs/source/setting-up-a-corda-network.rst +++ b/docs/source/setting-up-a-corda-network.rst @@ -42,7 +42,8 @@ The most important fields regarding network configuration are: is the hostname *that must be externally resolvable by other nodes in the network*. In the above configuration this is the resolvable name of a machine in a VPN. * ``rpcAddress``: The address to which Artemis will bind for RPC calls. -* ``webAddress``: The address the webserver should bind. Note that the port must be distinct from that of ``p2pAddress`` and ``rpcAddress`` if they are on the same machine. +* ``webAddress``: The address the webserver should bind. Note that the port must be distinct from that of ``p2pAddress`` + and ``rpcAddress`` if they are on the same machine. Bootstrapping the network ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -61,7 +62,7 @@ be done with the network bootstrapper. This is a tool that scans all the node co generate the network parameters file which is copied to the nodes' directories. It also copies each node's node-info file to every other node so that they can all transact with each other. -The bootstrapper tool can be downloaded from http://downloads.corda.net/network-bootstrapper-corda-X.Y.jar, where ``X`` +The bootstrapper tool can be downloaded from https://downloads.corda.net/network-bootstrapper-corda-X.Y.jar, where ``X`` is the major Corda version and ``Y`` is the minor Corda version. To use it, create a directory containing a node config file, ending in "_node.conf", for each node you want to create. diff --git a/docs/source/tutorial-cordapp.rst b/docs/source/tutorial-cordapp.rst index aba7647a98..ec4da46586 100644 --- a/docs/source/tutorial-cordapp.rst +++ b/docs/source/tutorial-cordapp.rst @@ -449,6 +449,10 @@ For example, you may end up with the following layout: After starting each node, the nodes will be able to see one another and agree IOUs among themselves. +.. note:: If you are using H2 and wish to use the same ``h2port`` value for all the nodes, then only assign them that + value after the nodes have been moved to their machines. The initial bootstrapping process requires access to the nodes' + databases and if they share the same H2 port then the process will fail. + Testing and debugging --------------------- From d039ede5373323d2e051a9d23d0a17fcce9622fc Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Mon, 30 Apr 2018 12:47:04 +0100 Subject: [PATCH 03/25] Configure Gradle's wrapper task. (#3033) --- build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build.gradle b/build.gradle index 39e47470cd..f663e89984 100644 --- a/build.gradle +++ b/build.gradle @@ -378,3 +378,8 @@ if(file('corda-docs-only-build').exists() || (System.getenv('CORDA_DOCS_ONLY_BUI } } } + +wrapper { + gradleVersion = "4.4.1" + distributionType = Wrapper.DistributionType.ALL +} From beef7bdfba492bafc6d22d2a2da785a4dad40add Mon Sep 17 00:00:00 2001 From: PyPie Date: Mon, 30 Apr 2018 18:24:14 +0530 Subject: [PATCH 04/25] A typo fix. Line 108 Was: We want to create an IOU of 100 with PartyB. We start the ``IOUFlow`` by typing: Should be We want to create an IOU of 99 with PartyB. We start the ``IOUFlow`` by typing: --- docs/source/hello-world-running.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index 765c936c22..5c936f9b30 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -105,7 +105,7 @@ commands. .. note:: Local terminal shell is available only in a development mode. In production environment SSH server can be enabled. More about SSH and how to connect can be found on the :doc:`shell` page. -We want to create an IOU of 100 with PartyB. We start the ``IOUFlow`` by typing: +We want to create an IOU of 99 with PartyB. We start the ``IOUFlow`` by typing: .. container:: codeset @@ -188,4 +188,4 @@ There are a number of improvements we could make to this CorDapp: * We could add an API, to make it easier to interact with the CorDapp But for now, the biggest priority is to add an ``IOUContract`` imposing constraints on the evolution of each -``IOUState`` over time. This will be the focus of our next tutorial. \ No newline at end of file +``IOUState`` over time. This will be the focus of our next tutorial. From 79cbaf8adf6918ce848cc381e19c7a44fcce063f Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Mon, 30 Apr 2018 14:58:13 +0100 Subject: [PATCH 05/25] Makes it clear that notaries are clusters or 1+ nodes. --- docs/source/key-concepts-notaries.rst | 81 ++++++++++++++------------- 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/docs/source/key-concepts-notaries.rst b/docs/source/key-concepts-notaries.rst index 5408fae4b6..e92af20ed4 100644 --- a/docs/source/key-concepts-notaries.rst +++ b/docs/source/key-concepts-notaries.rst @@ -3,9 +3,9 @@ Notaries .. topic:: Summary - * *Notaries prevent "double-spends"* - * *Notaries may optionally also validate transactions* - * *A network can have several notaries, each running a different consensus algorithm* + * *Notary clusters prevent "double-spends"* + * *Notary clusters may optionally also validate transactions* + * *A network can have several notary clusters, each running a different consensus algorithm* Video ----- @@ -16,80 +16,83 @@ Video Overview -------- -A *notary* is a network service that provides **uniqueness consensus** by attesting that, for a given transaction, it -has not already signed other transactions that consumes any of the proposed transaction's input states. +A *notary cluster* is a network service that provides **uniqueness consensus** by attesting that, for a given +transaction, it has not already signed other transactions that consumes any of the proposed transaction's input states. -Upon being sent asked to notarise a transaction, a notary will either: +Upon being sent asked to notarise a transaction, a notary cluster will either: * Sign the transaction if it has not already signed other transactions consuming any of the proposed transaction's input states * Reject the transaction and flag that a double-spend attempt has occurred otherwise -In doing so, the notary provides the point of finality in the system. Until the notary's signature is obtained, parties -cannot be sure that an equally valid, but conflicting, transaction will not be regarded as the "valid" attempt to spend -a given input state. However, after the notary's signature is obtained, we can be sure that the proposed -transaction's input states had not already been consumed by a prior transaction. Hence, notarisation is the point -of finality in the system. +In doing so, the notary cluster provides the point of finality in the system. Until the notary cluster's signature is +obtained, parties cannot be sure that an equally valid, but conflicting, transaction will not be regarded as the +"valid" attempt to spend a given input state. However, after the notary cluster's signature is obtained, we can be sure +that the proposed transaction's input states have not already been consumed by a prior transaction. Hence, notarisation +is the point of finality in the system. -Every state has an appointed notary, and a notary will only notarise a transaction if it is the appointed notary -of all the transaction's input states. +Every state has an appointed notary cluster, and a notary cluster will only notarise a transaction if it is the +appointed notary cluster of all the transaction's input states. Consensus algorithms -------------------- -Corda has "pluggable" consensus, allowing notaries to choose a consensus algorithm based on their requirements in +Corda has "pluggable" consensus, allowing notary clusters to choose a consensus algorithm based on their requirements in terms of privacy, scalability, legal-system compatibility and algorithmic agility. -In particular, notaries may differ in terms of: +In particular, notary clusters may differ in terms of: -* **Structure** - a notary may be a single network node, a cluster of mutually-trusting nodes, or a cluster of +* **Structure** - a notary cluster may be a single node, several mutually-trusting nodes, or several mutually-distrusting nodes -* **Consensus algorithm** - a notary service may choose to run a high-speed, high-trust algorithm such as RAFT, a +* **Consensus algorithm** - a notary cluster may choose to run a high-speed, high-trust algorithm such as RAFT, a low-speed, low-trust algorithm such as BFT, or any other consensus algorithm it chooses Validation ^^^^^^^^^^ -A notary service must also decide whether or not to provide **validity consensus** by validating each transaction -before committing it. In making this decision, they face the following trade-off: +A notary cluster must also decide whether or not to provide **validity consensus** by validating each transaction +before committing it. In making this decision, it faces the following trade-off: * If a transaction **is not** checked for validity, it creates the risk of "denial of state" attacks, where a node knowingly builds an invalid transaction consuming some set of existing states and sends it to the - notary, causing the states to be marked as consumed + notary cluster, causing the states to be marked as consumed * If the transaction **is** checked for validity, the notary will need to see the full contents of the transaction and - its dependencies. This leaks potentially private data to the notary + its dependencies. This leaks potentially private data to the notary cluster There are several further points to keep in mind when evaluating this trade-off. In the case of the non-validating model, Corda's controlled data distribution model means that information on unconsumed states is not widely shared. -Additionally, Corda's permissioned network means that the notary can store to the identity of the party that created -the "denial of state" transaction, allowing the attack to be resolved off-ledger. +Additionally, Corda's permissioned network means that the notary cluster can store the identity of the party that +created the "denial of state" transaction, allowing the attack to be resolved off-ledger. In the case of the validating model, the use of anonymous, freshly-generated public keys instead of legal identities to -identify parties in a transaction limit the information the notary sees. +identify parties in a transaction limit the information the notary cluster sees. Multiple notaries ----------------- -Each Corda network can have multiple notaries, each potentially running a different consensus algorithm. This provides -several benefits: +Each Corda network can have multiple notary clusters, each potentially running a different consensus algorithm. This +provides several benefits: -* **Privacy** - we can have both validating and non-validating notary services on the same network, each running a - different algorithm. This allows nodes to choose the preferred notary on a per-transaction basis -* **Load balancing** - spreading the transaction load over multiple notaries allows higher transaction throughput for - the platform overall -* **Low latency** - latency can be minimised by choosing a notary physically closer to the transacting parties +* **Privacy** - we can have both validating and non-validating notary clusters on the same network, each running a + different algorithm. This allows nodes to choose the preferred notary cluster on a per-transaction basis +* **Load balancing** - spreading the transaction load over multiple notary clusters allows higher transaction + throughput for the platform overall +* **Low latency** - latency can be minimised by choosing a notary cluster physically closer to the transacting parties Changing notaries ^^^^^^^^^^^^^^^^^ -Remember that a notary will only sign a transaction if it is the appointed notary of all of the transaction's input -states. However, there are cases in which we may need to change a state's appointed notary. These include: +Remember that a notary cluster will only sign a transaction if it is the appointed notary cluster of all of the +transaction's input states. However, there are cases in which we may need to change a state's appointed notary cluster. +These include: -* When a single transaction needs to consume several states that have different appointed notaries -* When a node would prefer to use a different notary for a given transaction due to privacy or efficiency concerns +* When a single transaction needs to consume several states that have different appointed notary clusters +* When a node would prefer to use a different notary cluster for a given transaction due to privacy or efficiency + concerns -Before these transactions can be created, the states must first be repointed to all have the same notary. This is +Before these transactions can be created, the states must first all be repointed to the same notary cluster. This is achieved using a special notary-change transaction that takes: * A single input state -* An output state identical to the input state, except that the appointed notary has been changed +* An output state identical to the input state, except that the appointed notary cluster has been changed -The input state's appointed notary will sign the transaction if it doesn't constitute a double-spend, at which point -a state will enter existence that has all the properties of the old state, but has a different appointed notary. \ No newline at end of file +The input state's appointed notary cluster will sign the transaction if it doesn't constitute a double-spend, at which +point a state will enter existence that has all the properties of the old state, but has a different appointed notary +cluster. \ No newline at end of file From 09a35f8e68006312845cded2e5053639dca841fb Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Mon, 30 Apr 2018 15:19:59 +0100 Subject: [PATCH 06/25] Splits node structure from node creation. Tweaks. (#3015) --- docs/source/corda-nodes-index.rst | 1 + docs/source/generating-a-node.rst | 109 ++++++------------------------ docs/source/node-structure.rst | 96 ++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 88 deletions(-) create mode 100644 docs/source/node-structure.rst diff --git a/docs/source/corda-nodes-index.rst b/docs/source/corda-nodes-index.rst index c1dfa0b508..6bf9c361b9 100644 --- a/docs/source/corda-nodes-index.rst +++ b/docs/source/corda-nodes-index.rst @@ -4,6 +4,7 @@ Corda nodes .. toctree:: :maxdepth: 1 + node-structure generating-a-node running-a-node deploying-a-node diff --git a/docs/source/generating-a-node.rst b/docs/source/generating-a-node.rst index f2bcfd6d6d..9e02b826b5 100644 --- a/docs/source/generating-a-node.rst +++ b/docs/source/generating-a-node.rst @@ -3,92 +3,24 @@ Creating nodes locally .. contents:: -Node structure --------------- -A Corda node has the following structure: +Handcrafting a node +------------------- +A node can be created manually by creating a folder that contains the following items: -.. sourcecode:: none +* The Corda JAR - . - ├── certificates // The node's certificates - ├── corda-webserver.jar // The built-in node webserver - ├── corda.jar // The core Corda libraries - ├── logs // The node logs - ├── node.conf // The node's configuration files - ├── persistence.mv.db // The node's database - └── cordapps // The CorDapps jars installed on the node + * Can be downloaded from https://r3.bintray.com/corda/net/corda/corda/ (under /VERSION_NUMBER/corda-VERSION_NUMBER.jar) -The node is configured by editing its ``node.conf`` file. You install CorDapps on the node by dropping the CorDapp JARs -into the ``cordapps`` folder. +* A node configuration file entitled ``node.conf``, configured as per :doc:`corda-configuration-file` -In development mode (i.e. when ``devMode = true``, see :doc:`corda-configuration-file` for more information), the ``certificates`` -directory is filled with pre-configured keystores if the required keystores do not exist. This ensures that developers -can get the nodes working as quickly as possible. However, these pre-configured keystores are not secure, to learn more see :doc:`permissioning`. +* A folder entitled ``cordapps`` containing any CorDapp JARs you want the node to load -.. _node_naming: +* **Optional:** A webserver JAR entitled ``corda-webserver.jar`` that will connect to the node via RPC -Node naming ------------ -A node's name must be a valid X.500 distinguished name. In order to be compatible with other implementations -(particularly TLS implementations), we constrain the allowed X.500 name attribute types to a subset of the minimum -supported set for X.509 certificates (specified in RFC 3280), plus the locality attribute: + * The (deprecated) default webserver can be downloaded from http://r3.bintray.com/corda/net/corda/corda-webserver/ (under /VERSION_NUMBER/corda-VERSION_NUMBER.jar) + * A Spring Boot alternative can be found here: https://github.com/corda/spring-webserver -* Organization (O) -* State (ST) -* Locality (L) -* Country (C) -* Organizational-unit (OU) -* Common name (CN) - -Note that the serial number is intentionally excluded in order to minimise scope for uncertainty in the distinguished name format. -The distinguished name qualifier has been removed due to technical issues; consideration was given to "Corda" as qualifier, -however the qualifier needs to reflect the compatibility zone, not the technology involved. There may be many Corda namespaces, -but only one R3 namespace on Corda. The ordering of attributes is important. - -``State`` should be avoided unless required to differentiate from other ``localities`` with the same or similar names at the -country level. For example, London (GB) would not need a ``state``, but St Ives would (there are two, one in Cornwall, one -in Cambridgeshire). As legal entities in Corda are likely to be located in major cities, this attribute is not expected to be -present in the majority of names, but is an option for the cases which require it. - -The name must also obey the following constraints: - -* The ``organisation``, ``locality`` and ``country`` attributes are present - - * The ``state``, ``organisational-unit`` and ``common name`` attributes are optional - -* The fields of the name have the following maximum character lengths: - - * Common name: 64 - * Organisation: 128 - * Organisation unit: 64 - * Locality: 64 - * State: 64 - -* The ``country`` attribute is a valid ISO 3166-1 two letter code in upper-case - -* All attributes must obey the following constraints: - - * Upper-case first letter - * Has at least two letters - * No leading or trailing whitespace - * Does not include the following characters: ``,`` , ``=`` , ``$`` , ``"`` , ``'`` , ``\`` - * Is in NFKC normalization form - * Does not contain the null character - * Only the latin, common and inherited unicode scripts are supported - -* The ``organisation`` field of the name also obeys the following constraints: - - * No double-spacing - - * This is to avoid right-to-left issues, debugging issues when we can't pronounce names over the phone, and - character confusability attacks - -External identifiers -^^^^^^^^^^^^^^^^^^^^ -Mappings to external identifiers such as Companies House nos., LEI, BIC, etc. should be stored in custom X.509 -certificate extensions. These values may change for operational reasons, without the identity they're associated with -necessarily changing, and their inclusion in the distinguished name would cause significant logistical complications. -The OID and format for these extensions will be described in a further specification. +The remaining files and folders described in :doc:`node-structure` will be generated at runtime. The Cordform task ----------------- @@ -205,9 +137,9 @@ The webserver JAR will be copied into the node's ``build`` folder with the name The Dockerform task ------------------- -The ```Dockerform``` is a sister task of ```Cordform```. It has nearly the same syntax and produces very -similar results - enhanced by an extra file to enable easy spin up of nodes using ```docker-compose```. -Below you can find the example task from the ```IRS Demo``` +The ``Dockerform`` is a sister task of ``Cordform``. It has nearly the same syntax and produces very +similar results - enhanced by an extra file to enable easy spin up of nodes using ``docker-compose``. +Below you can find the example task from the ``IRS Demo`` included in the samples directory of main Corda GitHub repository: .. sourcecode:: groovy @@ -228,7 +160,7 @@ included in the samples directory of main Corda GitHub repository: // (...) - task prepareDockerNodes(type: net.corda.plugins.Dockerform, dependsOn: ['jar']) { + task deployNodes(type: net.corda.plugins.Dockerform, dependsOn: ['jar']) { node { name "O=Notary Service,L=Zurich,C=CH" @@ -257,12 +189,10 @@ included in the samples directory of main Corda GitHub repository: } } -There is no need to specify the ports, as every node is a separated container, so no ports conflict will occur. -Running the task will create the same folders structure as described in :ref:`The Cordform task` with an additional -```Dockerfile`` in each node directory, and ```docker-compose.yml``` in ```build/nodes``` directory. Every node -by default exposes port 10003 which is the default one for RPC connections. +There is no need to specify the ports, as every node is a separated container, so no ports conflict will occur. Every +node by default will expose port 10003 which is the default port for RPC connections. -.. warning:: Webserver is not supported by this task! +.. warning:: The node webserver is not supported by this task! .. warning:: Nodes are run without the local shell enabled! @@ -279,4 +209,7 @@ in the ``deployNodes`` task, plus a ``runnodes`` shell script (or batch file on for testing and development purposes. If you make any changes to your CorDapp source or ``deployNodes`` task, you will need to re-run the task to see the changes take effect. +If the task is a ``Dockerform`` task, running the task will also create an additional ``Dockerfile`` in each node +directory, and a ``docker-compose.yml`` file in the ``build/nodes`` directory. + You can now run the nodes by following the instructions in :doc:`Running a node `. diff --git a/docs/source/node-structure.rst b/docs/source/node-structure.rst new file mode 100644 index 0000000000..a098bcdfec --- /dev/null +++ b/docs/source/node-structure.rst @@ -0,0 +1,96 @@ +Node structure +============== + +.. contents:: + +A Corda node has the following structure: + +.. sourcecode:: none + + . + ├── additional-node-infos // Additional node infos to load into the network map cache, beyond what the network map server provides + ├── artemis // Stores buffered P2P messages + ├── brokers // Stores buffered RPC messages + ├── certificates // The node's certificates + ├── corda-webserver.jar // The built-in node webserver + ├── corda.jar // The core Corda libraries + ├── cordapps // The CorDapp JARs installed on the node + ├── drivers // Contains a Jolokia driver used to export JMX metrics + ├── logs // The node logs + ├── network-parameters // The network parameters automatically downloaded from the network map server + ├── node.conf // The node's configuration files + ├── persistence.mv.db // The node's database + └── shell-commands // Custom shell commands defined by the node owner + +The node is configured by editing its ``node.conf`` file (see :doc:`corda-configuration-file`). You install CorDapps on +the node by dropping CorDapp JARs into the ``cordapps`` folder. + +In development mode (i.e. when ``devMode = true``, see :doc:`corda-configuration-file`), the ``certificates`` +directory is filled with pre-configured keystores if the required keystores do not exist. This ensures that developers +can get the nodes working as quickly as possible. However, these pre-configured keystores are not secure, to learn more +see :doc:`permissioning`. + +.. _node_naming: + +Node naming +----------- +A node's name must be a valid X.500 distinguished name. In order to be compatible with other implementations +(particularly TLS implementations), we constrain the allowed X.500 name attribute types to a subset of the minimum +supported set for X.509 certificates (specified in RFC 3280), plus the locality attribute: + +* Organization (O) +* State (ST) +* Locality (L) +* Country (C) +* Organizational-unit (OU) +* Common name (CN) + +Note that the serial number is intentionally excluded in order to minimise scope for uncertainty in the distinguished name format. +The distinguished name qualifier has been removed due to technical issues; consideration was given to "Corda" as qualifier, +however the qualifier needs to reflect the compatibility zone, not the technology involved. There may be many Corda namespaces, +but only one R3 namespace on Corda. The ordering of attributes is important. + +``State`` should be avoided unless required to differentiate from other ``localities`` with the same or similar names at the +country level. For example, London (GB) would not need a ``state``, but St Ives would (there are two, one in Cornwall, one +in Cambridgeshire). As legal entities in Corda are likely to be located in major cities, this attribute is not expected to be +present in the majority of names, but is an option for the cases which require it. + +The name must also obey the following constraints: + +* The ``organisation``, ``locality`` and ``country`` attributes are present + + * The ``state``, ``organisational-unit`` and ``common name`` attributes are optional + +* The fields of the name have the following maximum character lengths: + + * Common name: 64 + * Organisation: 128 + * Organisation unit: 64 + * Locality: 64 + * State: 64 + +* The ``country`` attribute is a valid ISO 3166-1 two letter code in upper-case + +* All attributes must obey the following constraints: + + * Upper-case first letter + * Has at least two letters + * No leading or trailing whitespace + * Does not include the following characters: ``,`` , ``=`` , ``$`` , ``"`` , ``'`` , ``\`` + * Is in NFKC normalization form + * Does not contain the null character + * Only the latin, common and inherited unicode scripts are supported + +* The ``organisation`` field of the name also obeys the following constraints: + + * No double-spacing + + * This is to avoid right-to-left issues, debugging issues when we can't pronounce names over the phone, and + character confusability attacks + +External identifiers +^^^^^^^^^^^^^^^^^^^^ +Mappings to external identifiers such as Companies House nos., LEI, BIC, etc. should be stored in custom X.509 +certificate extensions. These values may change for operational reasons, without the identity they're associated with +necessarily changing, and their inclusion in the distinguished name would cause significant logistical complications. +The OID and format for these extensions will be described in a further specification. From 42edf58b924b9096aad7f46e24d21a2a2ac14c84 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Mon, 30 Apr 2018 22:22:51 +0100 Subject: [PATCH 07/25] Introducing AbstractArgsParser which removes the boilerplate of printing help and exiting the process on cmd line errors. (#3040) --- .../node/{ArgsParser.kt => NodeArgsParser.kt} | 23 +++++-------- .../net/corda/node/internal/NodeStartup.kt | 26 ++------------ .../node/utilities/AbstractArgsParser.kt | 34 +++++++++++++++++++ ...rgsParserTest.kt => NodeArgsParserTest.kt} | 6 ++-- 4 files changed, 47 insertions(+), 42 deletions(-) rename node/src/main/kotlin/net/corda/node/{ArgsParser.kt => NodeArgsParser.kt} (91%) create mode 100644 node/src/main/kotlin/net/corda/node/utilities/AbstractArgsParser.kt rename node/src/test/kotlin/net/corda/node/{ArgsParserTest.kt => NodeArgsParserTest.kt} (98%) diff --git a/node/src/main/kotlin/net/corda/node/ArgsParser.kt b/node/src/main/kotlin/net/corda/node/NodeArgsParser.kt similarity index 91% rename from node/src/main/kotlin/net/corda/node/ArgsParser.kt rename to node/src/main/kotlin/net/corda/node/NodeArgsParser.kt index 5c9e45c09b..eae62e5ff8 100644 --- a/node/src/main/kotlin/net/corda/node/ArgsParser.kt +++ b/node/src/main/kotlin/net/corda/node/NodeArgsParser.kt @@ -1,7 +1,7 @@ package net.corda.node import com.typesafe.config.ConfigFactory -import joptsimple.OptionParser +import joptsimple.OptionSet import joptsimple.util.EnumConverter import joptsimple.util.PathConverter import net.corda.core.internal.div @@ -9,21 +9,21 @@ import net.corda.core.internal.exists import net.corda.node.services.config.ConfigHelper import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.parseAsNodeConfiguration +import net.corda.node.utilities.AbstractArgsParser import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy import org.slf4j.event.Level -import java.io.PrintStream import java.nio.file.Path import java.nio.file.Paths // NOTE: Do not use any logger in this class as args parsing is done before the logger is setup. -class ArgsParser { - private val optionParser = OptionParser() +class NodeArgsParser : AbstractArgsParser() { // The intent of allowing a command line configurable directory and config path is to allow deployment flexibility. // Other general configuration should live inside the config file unless we regularly need temporary overrides on the command line private val baseDirectoryArg = optionParser .accepts("base-directory", "The node working directory where all the files are kept") .withRequiredArg() - .defaultsTo(".") + .withValuesConvertedBy(PathConverter()) + .defaultsTo(Paths.get(".")) private val configFileArg = optionParser .accepts("config-file", "The path to the config file") .withRequiredArg() @@ -43,7 +43,7 @@ class ArgsParser { .defaultsTo((Paths.get("certificates") / "network-root-truststore.jks")) private val networkRootTrustStorePasswordArg = optionParser.accepts("network-root-truststore-password", "Network root trust store password obtained from network operator.") .withRequiredArg() - private val unknownConfigKeysPolicy = optionParser.accepts("on-unknown-config-keys", "How to behave on unknown node configuration property keys: [WARN, FAIL, IGNORE].") + private val unknownConfigKeysPolicy = optionParser.accepts("on-unknown-config-keys", "How to behave on unknown node configuration.") .withRequiredArg() .withValuesConvertedBy(object : EnumConverter(UnknownConfigKeysPolicy::class.java) {}) .defaultsTo(UnknownConfigKeysPolicy.FAIL) @@ -52,16 +52,13 @@ class ArgsParser { private val justGenerateNodeInfoArg = optionParser.accepts("just-generate-node-info", "Perform the node start-up task necessary to generate its nodeInfo, save it to disk, then quit") private val bootstrapRaftClusterArg = optionParser.accepts("bootstrap-raft-cluster", "Bootstraps Raft cluster. The node forms a single node cluster (ignoring otherwise configured peer addresses), acting as a seed for other nodes to join the cluster.") - private val helpArg = optionParser.accepts("help").forHelp() - fun parse(vararg args: String): CmdLineOptions { - val optionSet = optionParser.parse(*args) + override fun doParse(optionSet: OptionSet): CmdLineOptions { require(!optionSet.has(baseDirectoryArg) || !optionSet.has(configFileArg)) { "${baseDirectoryArg.options()[0]} and ${configFileArg.options()[0]} cannot be specified together" } - val baseDirectory = Paths.get(optionSet.valueOf(baseDirectoryArg)).normalize().toAbsolutePath() + val baseDirectory = optionSet.valueOf(baseDirectoryArg).normalize().toAbsolutePath() val configFile = baseDirectory / optionSet.valueOf(configFileArg) - val help = optionSet.has(helpArg) val loggingLevel = optionSet.valueOf(loggerLevel) val logToConsole = optionSet.has(logToConsoleArg) val isRegistration = optionSet.has(isRegistrationArg) @@ -84,7 +81,6 @@ class ArgsParser { return CmdLineOptions(baseDirectory, configFile, - help, loggingLevel, logToConsole, registrationConfig, @@ -95,15 +91,12 @@ class ArgsParser { bootstrapRaftCluster, unknownConfigKeysPolicy) } - - fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink) } data class NodeRegistrationOption(val networkRootTrustStorePath: Path, val networkRootTrustStorePassword: String) data class CmdLineOptions(val baseDirectory: Path, val configFile: Path, - val help: Boolean, val loggingLevel: Level, val logToConsole: Boolean, val nodeRegistrationOption: NodeRegistrationOption?, diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index 8b8ec8cb30..3398a312b6 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -1,7 +1,6 @@ package net.corda.node.internal import com.jcabi.manifests.Manifests -import joptsimple.OptionException import net.corda.core.internal.Emoji import net.corda.core.internal.concurrent.thenMatch import net.corda.core.internal.createDirectories @@ -29,14 +28,13 @@ import java.net.InetAddress import java.nio.file.Path import java.nio.file.Paths import java.util.* -import kotlin.system.exitProcess /** This class is responsible for starting a Node from command line arguments. */ open class NodeStartup(val args: Array) { companion object { private val logger by lazy { loggerFor() } // I guess this is lazy to allow for logging init, but why Node? - val LOGS_DIRECTORY_NAME = "logs" - val LOGS_CAN_BE_FOUND_IN_STRING = "Logs can be found in" + const val LOGS_DIRECTORY_NAME = "logs" + const val LOGS_CAN_BE_FOUND_IN_STRING = "Logs can be found in" } /** @@ -49,7 +47,7 @@ open class NodeStartup(val args: Array) { println("Corda will now exit...") return false } - val (argsParser, cmdlineOptions) = parseArguments() + val cmdlineOptions = NodeArgsParser().parseOrExit(*args) // We do the single node check before we initialise logging so that in case of a double-node start it // doesn't mess with the running node's logs. @@ -66,12 +64,6 @@ open class NodeStartup(val args: Array) { return true } - // Maybe render command line help. - if (cmdlineOptions.help) { - argsParser.printHelp(System.out) - return true - } - drawBanner(versionInfo) Node.printBasicNodeInfo(LOGS_CAN_BE_FOUND_IN_STRING, System.getProperty("log-path")) val conf = try { @@ -246,18 +238,6 @@ open class NodeStartup(val args: Array) { pidFileRw.write(ourProcessID.toByteArray()) } - private fun parseArguments(): Pair { - val argsParser = ArgsParser() - val cmdlineOptions = try { - argsParser.parse(*args) - } catch (ex: OptionException) { - println("Invalid command line arguments: ${ex.message}") - argsParser.printHelp(System.out) - exitProcess(1) - } - return Pair(argsParser, cmdlineOptions) - } - open protected fun initLogging(cmdlineOptions: CmdLineOptions) { val loggingLevel = cmdlineOptions.loggingLevel.name.toLowerCase(Locale.ENGLISH) System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file. diff --git a/node/src/main/kotlin/net/corda/node/utilities/AbstractArgsParser.kt b/node/src/main/kotlin/net/corda/node/utilities/AbstractArgsParser.kt new file mode 100644 index 0000000000..c31fdc1814 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/utilities/AbstractArgsParser.kt @@ -0,0 +1,34 @@ +package net.corda.node.utilities + +import joptsimple.OptionException +import joptsimple.OptionParser +import joptsimple.OptionSet +import kotlin.system.exitProcess + +abstract class AbstractArgsParser { + protected val optionParser = OptionParser() + private val helpOption = optionParser.acceptsAll(listOf("h", "help"), "show help").forHelp() + + /** + * Parses the given [args] or exits the process if unable to, printing the help output to stderr. + * If the help option is specified then the process is also shutdown after printing the help output to stdout. + */ + fun parseOrExit(vararg args: String): T { + val optionSet = try { + optionParser.parse(*args) + } catch (e: OptionException) { + System.err.println(e.message ?: "Unable to parse arguments.") + optionParser.printHelpOn(System.err) + exitProcess(1) + } + if (optionSet.has(helpOption)) { + optionParser.printHelpOn(System.out) + exitProcess(0) + } + return doParse(optionSet) + } + + fun parse(vararg args: String): T = doParse(optionParser.parse(*args)) + + protected abstract fun doParse(optionSet: OptionSet): T +} \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/ArgsParserTest.kt b/node/src/test/kotlin/net/corda/node/NodeArgsParserTest.kt similarity index 98% rename from node/src/test/kotlin/net/corda/node/ArgsParserTest.kt rename to node/src/test/kotlin/net/corda/node/NodeArgsParserTest.kt index 83f782e902..66fdee718f 100644 --- a/node/src/test/kotlin/net/corda/node/ArgsParserTest.kt +++ b/node/src/test/kotlin/net/corda/node/NodeArgsParserTest.kt @@ -15,8 +15,8 @@ import java.nio.file.Paths import kotlin.test.assertEquals import kotlin.test.assertNotNull -class ArgsParserTest { - private val parser = ArgsParser() +class NodeArgsParserTest { + private val parser = NodeArgsParser() companion object { private lateinit var workingDirectory: Path @@ -35,7 +35,6 @@ class ArgsParserTest { assertThat(parser.parse()).isEqualTo(CmdLineOptions( baseDirectory = workingDirectory, configFile = workingDirectory / "node.conf", - help = false, logToConsole = false, loggingLevel = Level.INFO, nodeRegistrationOption = null, @@ -166,7 +165,6 @@ class ArgsParserTest { @Test fun `on-unknown-config-keys options`() { - UnknownConfigKeysPolicy.values().forEach { onUnknownConfigKeyPolicy -> val cmdLineOptions = parser.parse("--on-unknown-config-keys", onUnknownConfigKeyPolicy.name) assertThat(cmdLineOptions.unknownConfigKeysPolicy).isEqualTo(onUnknownConfigKeyPolicy) From adef57f1274aa02ec755d511f68481dcba69c2ac Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Tue, 1 May 2018 07:48:50 +0100 Subject: [PATCH 08/25] Including FlowException in the RPC exception whitelist (CORDA-1264) (#3037) These exceptions are designed to be propagated in P2P and so makes sense to keep them visible if the recipient is an RPC user. --- .../corda/client/rpc/CordaRPCClientTest.kt | 10 --- .../exceptions/InternalNodeException.kt | 3 +- .../services/rpc/RpcExceptionHandlingTest.kt | 75 ++++++++++--------- 3 files changed, 43 insertions(+), 45 deletions(-) diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt index 7826a549ba..e7c4b06f1b 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt @@ -18,7 +18,6 @@ import net.corda.finance.schemas.CashSchemaV1 import net.corda.node.internal.Node import net.corda.node.internal.StartedNode import net.corda.node.services.Permissions.Companion.all -import net.corda.nodeapi.exceptions.InternalNodeException import net.corda.testing.core.* import net.corda.testing.node.User import net.corda.testing.node.internal.NodeBasedTest @@ -154,15 +153,6 @@ class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance.contracts", C println("Result: ${flowHandle.returnValue.getOrThrow()}") } - @Test - fun `sub-type of FlowException thrown by flow`() { - login(rpcUser.username, rpcUser.password) - val handle = connection!!.proxy.startFlow(::CashPaymentFlow, 100.DOLLARS, identity) - assertThatExceptionOfType(InternalNodeException::class.java).isThrownBy { - handle.returnValue.getOrThrow() - } - } - @Test fun `check basic flow has no progress`() { login(rpcUser.username, rpcUser.password) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/InternalNodeException.kt b/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/InternalNodeException.kt index e0ee6c3c9b..0751e2681a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/InternalNodeException.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/exceptions/InternalNodeException.kt @@ -2,10 +2,12 @@ package net.corda.nodeapi.exceptions import net.corda.core.CordaRuntimeException import net.corda.core.contracts.TransactionVerificationException +import net.corda.core.flows.FlowException import java.io.InvalidClassException // could change to use package name matching but trying to avoid reflection for now private val whitelisted = setOf( + FlowException::class, InvalidClassException::class, RpcSerializableError::class, TransactionVerificationException::class @@ -23,7 +25,6 @@ class InternalNodeException(message: String) : CordaRuntimeException(message) { fun defaultMessage(): String = DEFAULT_MESSAGE fun obfuscateIfInternal(wrapped: Throwable): Throwable { - (wrapped as? CordaRuntimeException)?.setCause(null) return when { whitelisted.any { it.isInstance(wrapped) } -> wrapped diff --git a/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcExceptionHandlingTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcExceptionHandlingTest.kt index d2f4f8a63b..21c6be7f50 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcExceptionHandlingTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcExceptionHandlingTest.kt @@ -29,61 +29,70 @@ class RpcExceptionHandlingTest { @Test fun `rpc client handles exceptions thrown on node side`() { - - driver(DriverParameters(startNodesInProcess = true)) { + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { val node = startNode(NodeParameters(rpcUsers = users)).getOrThrow() - assertThatCode { node.rpc.startFlow(::Flow).returnValue.getOrThrow() }.isInstanceOfSatisfying(InternalNodeException::class.java) { exception -> - - assertThat(exception).hasNoCause() - assertThat(exception.stackTrace).isEmpty() - assertThat(exception.message).isEqualTo(InternalNodeException.defaultMessage()) - } + assertThatCode { node.rpc.startFlow(::Flow).returnValue.getOrThrow() } + .isInstanceOfSatisfying(InternalNodeException::class.java) { exception -> + assertThat(exception).hasNoCause() + assertThat(exception.stackTrace).isEmpty() + assertThat(exception.message).isEqualTo(InternalNodeException.defaultMessage()) + } } } @Test fun `rpc client handles client-relevant exceptions thrown on node side`() { - - driver(DriverParameters(startNodesInProcess = true)) { + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { val node = startNode(NodeParameters(rpcUsers = users)).getOrThrow() val clientRelevantMessage = "This is for the players!" - assertThatCode { node.rpc.startFlow(::ClientRelevantErrorFlow, clientRelevantMessage).returnValue.getOrThrow() }.isInstanceOfSatisfying(ClientRelevantException::class.java) { exception -> + assertThatCode { node.rpc.startFlow(::ClientRelevantErrorFlow, clientRelevantMessage).returnValue.getOrThrow() } + .isInstanceOfSatisfying(ClientRelevantException::class.java) { exception -> + assertThat(exception).hasNoCause() + assertThat(exception.stackTrace).isEmpty() + assertThat(exception.message).isEqualTo(clientRelevantMessage) + } + } + } - assertThat(exception).hasNoCause() - assertThat(exception.stackTrace).isEmpty() - assertThat(exception.message).isEqualTo(clientRelevantMessage) - } + @Test + fun `FlowException is received by the RPC client`() { + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { + val node = startNode(NodeParameters(rpcUsers = users)).getOrThrow() + val exceptionMessage = "Flow error!" + assertThatCode { node.rpc.startFlow(::FlowExceptionFlow, exceptionMessage).returnValue.getOrThrow() } + .isInstanceOfSatisfying(FlowException::class.java) { exception -> + assertThat(exception).hasNoCause() + assertThat(exception.stackTrace).isEmpty() + assertThat(exception.message).isEqualTo(exceptionMessage) + } } } @Test fun `rpc client handles exceptions thrown on counter-party side`() { - - driver(DriverParameters(startNodesInProcess = true)) { + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { val nodeA = startNode(NodeParameters(providedName = ALICE_NAME, rpcUsers = users)).getOrThrow() val nodeB = startNode(NodeParameters(providedName = BOB_NAME, rpcUsers = users)).getOrThrow() - assertThatCode { nodeA.rpc.startFlow(::InitFlow, nodeB.nodeInfo.singleIdentity()).returnValue.getOrThrow() }.isInstanceOfSatisfying(InternalNodeException::class.java) { exception -> - - assertThat(exception).hasNoCause() - assertThat(exception.stackTrace).isEmpty() - assertThat(exception.message).isEqualTo(InternalNodeException.defaultMessage()) - } + assertThatCode { nodeA.rpc.startFlow(::InitFlow, nodeB.nodeInfo.singleIdentity()).returnValue.getOrThrow() } + .isInstanceOfSatisfying(InternalNodeException::class.java) { exception -> + assertThat(exception).hasNoCause() + assertThat(exception.stackTrace).isEmpty() + assertThat(exception.message).isEqualTo(InternalNodeException.defaultMessage()) + } } } } @StartableByRPC class Flow : FlowLogic() { - @Suspendable override fun call(): String { - throw GenericJDBCException("Something went wrong!", SQLException("Oops!")) } } @@ -91,10 +100,8 @@ class Flow : FlowLogic() { @StartableByRPC @InitiatingFlow class InitFlow(private val party: Party) : FlowLogic() { - @Suspendable override fun call(): String { - val session = initiateFlow(party) return session.sendAndReceive("hey").unwrap { it } } @@ -102,10 +109,8 @@ class InitFlow(private val party: Party) : FlowLogic() { @InitiatedBy(InitFlow::class) class InitiatedFlow(private val initiatingSession: FlowSession) : FlowLogic() { - @Suspendable override fun call() { - initiatingSession.receive().unwrap { it } throw GenericJDBCException("Something went wrong!", SQLException("Oops!")) } @@ -113,10 +118,12 @@ class InitiatedFlow(private val initiatingSession: FlowSession) : FlowLogic() { - @Suspendable - override fun call(): String { + override fun call(): String = throw ClientRelevantException(message, SQLException("Oops!")) +} - throw ClientRelevantException(message, SQLException("Oops!")) - } -} \ No newline at end of file +@StartableByRPC +class FlowExceptionFlow(private val message: String) : FlowLogic() { + @Suspendable + override fun call(): String = throw FlowException(message) +} From 4ba794bc1ad9a76c5256e7c09f851d740759758e Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Tue, 1 May 2018 09:18:35 +0100 Subject: [PATCH 09/25] Section in contributors.md for code contributions. (#3038) * Section in contributors.md for code contributions. * Removes design/PR distinction. Expands list. --- CONTRIBUTORS.md | 70 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index f2195fa5c7..10bb16f4cd 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -1,35 +1,49 @@ # List of Contributors -We'd like to thank the following people for contributing ideas to Corda, -either during architecture review sessions of the R3 Architecture Working Group, -or in design reviews since Corda has been open-sourced. Some people have moved to -a different organisation since their contribution. Please forgive any omissions, and -create a pull request, or email , if you wish to see -changes to this list. +We'd like to thank the following people for contributing to Corda, either by +contributing to the design of Corda during the architecture review sessions of the +R3 Architecture Working Group and during design reviews since Corda has been +open-sourced, or by contributing code via pull requests. Some people have +moved to a different organisation since their contribution. Please forgive any +omissions, and create a pull request, or email , if you wish to +see changes to this list. +* acetheultimate +* Adrian Flethcehr (TD) * Alberto Arri (R3) +* amiracam * Andras Slemmer (R3) * Andrius Dagys (R3) * Andrzej Cichocki (R3) * Andrzej Grzesik (R3) * Anthony Coates (Deutsche Bank) +* Anthony Keenan (R3) +* Anthony Woolley (Société Générale) * Anton Semenov (Commerzbank) * Antonio Cerrato (SEB) -* Anthony Woolley (Société Générale) -* Arnaud Stevens (Natixis) +* Antony Lewis (R3) * Arijit Das (Northern Trust) +* Arnaud Stevens (Natixis) * Arun Battu (BNY Mellon) * Austin Moothart (R3) +* balajimore * Barry Childe (HSBC) * Barry Flower (Westpac) +* Bart van den Bosch (KBC) +* Ben Wyeth (RBS) * Benjamin Abineri (R3) * Benoit Lafontaine (OCTO) * Berit Bourgonje (ING) +* BitcoinErrorLog * Bob Crozier (AIA) * Bogdan Paunescu (R3) +* C-Otto * Cais Manai (R3) * Carl Worrall (BCS) +* Carlos Kuchovsky (BBVA) +* Cédric Wahl (Société Générale) * Chaitanya Jadhav (HSBC) +* chalkido * Chris Akers (R3) * Chris Burlinchon (R3) * Chris Rankin (R3) @@ -41,14 +55,22 @@ changes to this list. * Clay Ratliff (Thoughtworks) * Clemens Wan (R3) * Clinton Alexander (R3) +* cncorda +* cyrsis * Daniel Roig (SEB) * Dave Hudson (R3) +* David John Grundy (Dankse Bank) * David Lee (BCS) +* Dirk Hermans (KBC) +* Edward Greenwood (State Street) * Farzad Pezeshkpour (RBS) +* fracting * Frederic Dalibard (Natixis) * Garrett Macey (Wells Fargo) +* gary-rowe * Gavin Thomas (R3) * George Marcel Smetana (Bradesco) +* George Smetana (Bradesco) * Giulio Katis (Westpac) * Giuseppe Cardone (Intesa Sanpaolo) * Guy Hochstetler (IBM) @@ -60,9 +82,11 @@ changes to this list. * James Brown (R3) * James Carlyle (R3) * Jared Harwayne-Gidansky (BNY Mellon) +* Jayavaradhan Sambedu (Société Générale) * Joel Dudley (R3) * Johan Hörmark (SEB) * Johann Palychata (BNP Paribas) +* johnnyychiu * Jonathan Sartin (R3) * Jose Coll (R3) * Jose Luu (Natixis) @@ -70,6 +94,7 @@ changes to this list. * Justin Chapman (Northern Trust) * Kai-Michael Schramm (Credit Suisse) * Karel Hajek (Barclays Capital) +* karnauskas * Kasia Streich (R3) * Kat Baker (R3) * Khaild Ahmed (Northern Trust) @@ -81,6 +106,7 @@ changes to this list. * Lucas Salmen (Itau) * Maksymillian Pawlak (R3) * Marek Scocovsky (ABSA) +* marekdapps * Mark Lauer (Westpac) * Mark Oldfield (R3) * Mark Raynes (Thomson Reuters) @@ -103,18 +129,28 @@ changes to this list. * Oscar Zibordi de Paiva (Bradesco) * Patrick Kuo (R3) * Pekka Kaipio (OP Financial) +* Phillip Griffin * Piotr Piskorski (Nordea) * Przemyslaw Bak (R3) +* quiark +* RangerOfFire +* renlulu * Rex Maudsley (Société Générale) +* Rhett Brewer (Goldman Sachs) +* Richard Crook (RBS) +* Richard Gendal Brown (R3) * Richard Green (R3) * Rick Parker (R3) -* Rhett Brewer (Goldman Sachs) * Roberto Karpinski (Bradesco) * Robin Green (CIBC) * Rodrigo Bueno (Itau) +* Rodrigo Gonçalves (Itau Unibanco) * Roger Willis (R3) * Ross Burnett (Macquarie) * Ross Nicoll (R3) +* Rui Hu (Nordea) +* s-matthew-english +* sadysnaat * Sajindra Jayasena (Deutsche Bank) * Saket Sharma (BNY Mellon) * Sam Chadwick (Thomson Reuters) @@ -123,16 +159,24 @@ changes to this list. * Shams Asari (R3) * Simon Taylor (Barclays) * Sofus Mortensen (Digital Asset Holdings) -* Szymon Sztuka (R3) * Stephen Lane-Smith (BMO) +* stevenroose +* Szymon Sztuka (R3) +* Thiago Rafael Ferreira (Scorpius IT Solutions) * Thomas O'Donnell (Macquarie) * Thomas Schroeter (R3) -* Tom Menner (R3) -* Tudor Malene (R3) * Tim Swanson (R3) * Timothy Smith (Credit Suisse) +* Tom Menner (R3) +* tomconte * Tommy Lillehagen (R3) +* tomtau +* Tudor Malene (R3) +* varunkm +* verymahler * Viktor Kolomeyko (R3) +* Vipin Bharathan * Wawrzek Niewodniczanski (R3) * Wei Wu Zhang (Commonwealth Bank of Australia) -* Zabrina Smith (Northern Trust) \ No newline at end of file +* Zabrina Smith (Northern Trust) +* zorenmith (Northern Trust) \ No newline at end of file From 5bbfde3d3593252ed84f926883b1731e7c7b29a7 Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Tue, 1 May 2018 13:24:06 +0100 Subject: [PATCH 10/25] Run filterMyKeys only when required (in NodeVaultService) (#3014) --- .../corda/node/services/vault/NodeVaultService.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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 b1cdf318f5..59c865b0ad 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 @@ -128,10 +128,9 @@ class NodeVaultService( private fun makeUpdates(batch: Iterable, statesToRecord: StatesToRecord): List> { fun makeUpdate(tx: WireTransaction): Vault.Update? { - val myKeys = keyManagementService.filterMyKeys(tx.outputs.flatMap { it.data.participants.map { it.owningKey } }) val ourNewStates = when (statesToRecord) { StatesToRecord.NONE -> throw AssertionError("Should not reach here") - StatesToRecord.ONLY_RELEVANT -> tx.outputs.filter { isRelevant(it.data, myKeys.toSet()) } + StatesToRecord.ONLY_RELEVANT -> tx.outputs.filter { isRelevant(it.data, keyManagementService.filterMyKeys(tx.outputs.flatMap { it.data.participants.map { it.owningKey } }).toSet()) } StatesToRecord.ALL_VISIBLE -> tx.outputs }.map { tx.outRef(it.data) } @@ -156,12 +155,15 @@ class NodeVaultService( is ContractUpgradeWireTransaction -> tx.resolve(servicesForResolution, emptyList()) else -> throw IllegalArgumentException("Unsupported transaction type: ${tx.javaClass.name}") } - val myKeys = keyManagementService.filterMyKeys(ltx.outputs.flatMap { it.data.participants.map { it.owningKey } }) + val myKeys by lazy { keyManagementService.filterMyKeys(ltx.outputs.flatMap { it.data.participants.map { it.owningKey } }) } val (consumedStateAndRefs, producedStates) = ltx.inputs. zip(ltx.outputs). filter { (_, output) -> - if (statesToRecord == StatesToRecord.ONLY_RELEVANT) isRelevant(output.data, myKeys.toSet()) - else true + if (statesToRecord == StatesToRecord.ONLY_RELEVANT) { + isRelevant(output.data, myKeys.toSet()) + } else { + true + } }. unzip() From eb0fbb03e37c4ca5e582e96f508ce7b211cf3726 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Tue, 1 May 2018 13:27:58 +0100 Subject: [PATCH 11/25] Adds contributors who contributed to example and template CorDapps. (#3045) --- CONTRIBUTORS.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 10bb16f4cd..a715a3daf1 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -10,6 +10,7 @@ see changes to this list. * acetheultimate * Adrian Flethcehr (TD) +* agoldvarg * Alberto Arri (R3) * amiracam * Andras Slemmer (R3) @@ -22,6 +23,7 @@ see changes to this list. * Anton Semenov (Commerzbank) * Antonio Cerrato (SEB) * Antony Lewis (R3) +* anttiai * Arijit Das (Northern Trust) * Arnaud Stevens (Natixis) * Arun Battu (BNY Mellon) @@ -162,6 +164,7 @@ see changes to this list. * Stephen Lane-Smith (BMO) * stevenroose * Szymon Sztuka (R3) +* tb-pq * Thiago Rafael Ferreira (Scorpius IT Solutions) * Thomas O'Donnell (Macquarie) * Thomas Schroeter (R3) @@ -179,4 +182,4 @@ see changes to this list. * Wawrzek Niewodniczanski (R3) * Wei Wu Zhang (Commonwealth Bank of Australia) * Zabrina Smith (Northern Trust) -* zorenmith (Northern Trust) \ No newline at end of file +* zorenmith (Northern Trust) From 5565b3e80db960511f5b7de2a6831b13d58a9767 Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Tue, 1 May 2018 19:32:29 +0700 Subject: [PATCH 12/25] [CORDA-1411]: Prevent MappedSchema caching from leaking memory. (#3042) --- .../net/corda/core/schemas/PersistentTypes.kt | 20 +++++++++++++++++++ docs/source/changelog.rst | 1 + .../internal/persistence/CordaPersistence.kt | 3 ++- .../persistence/HibernateConfiguration.kt | 7 +++---- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt b/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt index fbf9b3ccf9..799ed787c7 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt @@ -42,6 +42,26 @@ open class MappedSchema(schemaFamily: Class<*>, val mappedTypes: Iterable>) { val name: String = schemaFamily.name override fun toString(): String = "${this.javaClass.simpleName}(name=$name, version=$version)" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MappedSchema + + if (version != other.version) return false + if (mappedTypes != other.mappedTypes) return false + if (name != other.name) return false + + return true + } + + override fun hashCode(): Int { + var result = version + result = 31 * result + mappedTypes.hashCode() + result = 31 * result + name.hashCode() + return result + } } //DOCEND MappedSchema diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 53dadbeead..bb7eb2e01d 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -6,6 +6,7 @@ release, see :doc:`upgrade-notes`. Unreleased ========== +* Avoided a memory leak deriving from incorrect MappedSchema caching strategy. * Added program line argument ``on-unknown-config-keys`` to allow specifying behaviour on unknown node configuration property keys. Values are: [FAIL, WARN, IGNORE], default to FAIL if unspecified. 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 4884c21832..0c8c5724ed 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 @@ -25,7 +25,8 @@ data class DatabaseConfig( val initialiseSchema: Boolean = true, val serverNameTablePrefix: String = "", val transactionIsolationLevel: TransactionIsolationLevel = TransactionIsolationLevel.REPEATABLE_READ, - val exportHibernateJMXStatistics: Boolean = false + val exportHibernateJMXStatistics: Boolean = false, + val mappedSchemaCacheSize: Long = 100 ) // This class forms part of the node config and so any changes to it must be handled with care 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 ea3c11d252..acc995d3d1 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 @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.persistence +import com.github.benmanes.caffeine.cache.Caffeine import net.corda.core.internal.castIfPossible import net.corda.core.schemas.MappedSchema import net.corda.core.utilities.contextLogger @@ -21,7 +22,6 @@ import org.hibernate.type.descriptor.sql.BlobTypeDescriptor import org.hibernate.type.descriptor.sql.VarbinaryTypeDescriptor import java.lang.management.ManagementFactory import java.sql.Connection -import java.util.concurrent.ConcurrentHashMap import javax.management.ObjectName import javax.persistence.AttributeConverter @@ -35,8 +35,7 @@ class HibernateConfiguration( private val logger = contextLogger() } - // TODO: make this a guava cache or similar to limit ability for this to grow forever. - private val sessionFactories = ConcurrentHashMap, SessionFactory>() + private val sessionFactories = Caffeine.newBuilder().maximumSize(databaseConfig.mappedSchemaCacheSize).build, SessionFactory>() val sessionFactoryForRegisteredSchemas = schemas.let { logger.info("Init HibernateConfiguration for schemas: $it") @@ -44,7 +43,7 @@ class HibernateConfiguration( } /** @param key must be immutable, not just read-only. */ - fun sessionFactoryForSchemas(key: Set) = sessionFactories.computeIfAbsent(key, { makeSessionFactoryForSchemas(key) }) + fun sessionFactoryForSchemas(key: Set): SessionFactory = sessionFactories.get(key, ::makeSessionFactoryForSchemas)!! private fun makeSessionFactoryForSchemas(schemas: Set): SessionFactory { logger.info("Creating session factory for schemas: $schemas") From 92922b874cfb2631733d1542a9feeae08b26522f Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Tue, 1 May 2018 19:33:13 +0700 Subject: [PATCH 13/25] [CORDA-1397]: Fixed incorrect exception handling in `NodeVaultService._query()`. (#3043) --- docs/source/changelog.rst | 3 + .../node/services/vault/NodeVaultService.kt | 107 +++++++++--------- 2 files changed, 54 insertions(+), 56 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index bb7eb2e01d..0c16d1444d 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -6,6 +6,9 @@ release, see :doc:`upgrade-notes`. Unreleased ========== + +* Fixed incorrect exception handling in ``NodeVaultService._query()``. + * Avoided a memory leak deriving from incorrect MappedSchema caching strategy. * Added program line argument ``on-unknown-config-keys`` to allow specifying behaviour on unknown node configuration property keys. 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 59c865b0ad..5dc658b093 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 @@ -406,65 +406,60 @@ class NodeVaultService( // TODO: revisit (use single instance of parser for all queries) val criteriaParser = HibernateQueryCriteriaParser(contractStateType, contractStateTypeMappings, criteriaBuilder, criteriaQuery, queryRootVaultStates) - try { - // parse criteria and build where predicates - criteriaParser.parse(criteria, sorting) + // parse criteria and build where predicates + criteriaParser.parse(criteria, sorting) - // prepare query for execution - val query = session.createQuery(criteriaQuery) + // prepare query for execution + val query = session.createQuery(criteriaQuery) - // pagination checks - if (!paging.isDefault) { - // pagination - if (paging.pageNumber < DEFAULT_PAGE_NUM) throw VaultQueryException("Page specification: invalid page number ${paging.pageNumber} [page numbers start from $DEFAULT_PAGE_NUM]") - if (paging.pageSize < 1) throw VaultQueryException("Page specification: invalid page size ${paging.pageSize} [must be a value between 1 and $MAX_PAGE_SIZE]") - } - - query.firstResult = (paging.pageNumber - 1) * paging.pageSize - query.maxResults = paging.pageSize + 1 // detection too many results - - // execution - val results = query.resultList - - // final pagination check (fail-fast on too many results when no pagination specified) - if (paging.isDefault && results.size > DEFAULT_PAGE_SIZE) - throw VaultQueryException("Please specify a `PageSpecification` as there are more results [${results.size}] than the default page size [$DEFAULT_PAGE_SIZE]") - - val statesAndRefs: MutableList> = mutableListOf() - val statesMeta: MutableList = mutableListOf() - val otherResults: MutableList = mutableListOf() - val stateRefs = mutableSetOf() - - results.asSequence() - .forEachIndexed { index, result -> - if (result[0] is VaultSchemaV1.VaultStates) { - if (!paging.isDefault && index == paging.pageSize) // skip last result if paged - return@forEachIndexed - val vaultState = result[0] as VaultSchemaV1.VaultStates - val stateRef = StateRef(SecureHash.parse(vaultState.stateRef!!.txId!!), vaultState.stateRef!!.index!!) - stateRefs.add(stateRef) - statesMeta.add(Vault.StateMetadata(stateRef, - vaultState.contractStateClassName, - vaultState.recordedTime, - vaultState.consumedTime, - vaultState.stateStatus, - vaultState.notary, - vaultState.lockId, - vaultState.lockUpdateTime)) - } else { - // TODO: improve typing of returned other results - log.debug { "OtherResults: ${Arrays.toString(result.toArray())}" } - otherResults.addAll(result.toArray().asList()) - } - } - if (stateRefs.isNotEmpty()) - statesAndRefs.addAll(servicesForResolution.loadStates(stateRefs) as Collection>) - - return Vault.Page(states = statesAndRefs, statesMetadata = statesMeta, stateTypes = criteriaParser.stateTypes, totalStatesAvailable = totalStates, otherResults = otherResults) - } catch (e: java.lang.Exception) { - log.error(e.message) - throw e.cause ?: e + // pagination checks + if (!paging.isDefault) { + // pagination + if (paging.pageNumber < DEFAULT_PAGE_NUM) throw VaultQueryException("Page specification: invalid page number ${paging.pageNumber} [page numbers start from $DEFAULT_PAGE_NUM]") + if (paging.pageSize < 1) throw VaultQueryException("Page specification: invalid page size ${paging.pageSize} [must be a value between 1 and $MAX_PAGE_SIZE]") } + + query.firstResult = (paging.pageNumber - 1) * paging.pageSize + query.maxResults = paging.pageSize + 1 // detection too many results + + // execution + val results = query.resultList + + // final pagination check (fail-fast on too many results when no pagination specified) + if (paging.isDefault && results.size > DEFAULT_PAGE_SIZE) + throw VaultQueryException("Please specify a `PageSpecification` as there are more results [${results.size}] than the default page size [$DEFAULT_PAGE_SIZE]") + + val statesAndRefs: MutableList> = mutableListOf() + val statesMeta: MutableList = mutableListOf() + val otherResults: MutableList = mutableListOf() + val stateRefs = mutableSetOf() + + results.asSequence() + .forEachIndexed { index, result -> + if (result[0] is VaultSchemaV1.VaultStates) { + if (!paging.isDefault && index == paging.pageSize) // skip last result if paged + return@forEachIndexed + val vaultState = result[0] as VaultSchemaV1.VaultStates + val stateRef = StateRef(SecureHash.parse(vaultState.stateRef!!.txId!!), vaultState.stateRef!!.index!!) + stateRefs.add(stateRef) + statesMeta.add(Vault.StateMetadata(stateRef, + vaultState.contractStateClassName, + vaultState.recordedTime, + vaultState.consumedTime, + vaultState.stateStatus, + vaultState.notary, + vaultState.lockId, + vaultState.lockUpdateTime)) + } else { + // TODO: improve typing of returned other results + log.debug { "OtherResults: ${Arrays.toString(result.toArray())}" } + otherResults.addAll(result.toArray().asList()) + } + } + if (stateRefs.isNotEmpty()) + statesAndRefs.addAll(servicesForResolution.loadStates(stateRefs) as Collection>) + + return Vault.Page(states = statesAndRefs, statesMetadata = statesMeta, stateTypes = criteriaParser.stateTypes, totalStatesAvailable = totalStates, otherResults = otherResults) } @Throws(VaultQueryException::class) From 33b9040efb945968a3f9d1db756db2a2ce5327bd Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Tue, 1 May 2018 16:00:19 +0100 Subject: [PATCH 14/25] CORDA-1389: gradle plugins version bump (#3048) --- constants.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants.properties b/constants.properties index 8dbf104812..57d7f119e1 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=4.0.14 +gradlePluginsVersion=4.0.15 kotlinVersion=1.2.20 platformVersion=4 guavaVersion=21.0 From 0c680ae530139398e1f7b83f1f6df3194daba274 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Tue, 1 May 2018 16:27:54 +0100 Subject: [PATCH 15/25] CORDA-1403 - Generics serialization issue (#3030) When implementing a generic interface subtype check fails, need to compare to the actual raw type --- docs/source/changelog.rst | 4 +++ .../serialization/amqp/SerializationHelper.kt | 4 +-- .../amqp/SerializationOutputTests.kt | 25 +++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 0c16d1444d..3d84a6dcb5 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -7,6 +7,10 @@ release, see :doc:`upgrade-notes`. Unreleased ========== +* Fix CORDA-1403 where a property of a class that implemented a generic interface could not be deserialised in + a factory without a serialiser as the subtype check for the class instance failed. Fix is to compare the raw + type. + * Fixed incorrect exception handling in ``NodeVaultService._query()``. * Avoided a memory leak deriving from incorrect MappedSchema caching strategy. diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt index 119b1e0211..e589716672 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt @@ -480,10 +480,10 @@ internal fun Type.asParameterizedType(): ParameterizedType { } internal fun Type.isSubClassOf(type: Type): Boolean { - return TypeToken.of(this).isSubtypeOf(type) + return TypeToken.of(this).isSubtypeOf(TypeToken.of(type).rawType) } -// ByteArrays, primtives and boxed primitives are not stored in the object history +// ByteArrays, primitives and boxed primitives are not stored in the object history internal fun suitableForObjectReference(type: Type): Boolean { val clazz = type.asClass() return type != ByteArray::class.java && (clazz != null && !clazz.isPrimitive && !Primitives.unwrap(clazz).isPrimitive) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt index 5b10cde6a9..19fddfafe0 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt @@ -1310,5 +1310,30 @@ class SerializationOutputTests(private val compression: CordaSerializationEncodi C(12).serializeE() }.withMessageContaining("has synthetic fields and is likely a nested inner class") } + + interface DataClassByInterface { + val v : V + } + + @Test + fun dataClassBy() { + data class C (val s: String) : DataClassByInterface { + override val v: String = "-- $s" + } + + data class Inner(val wrapped: DataClassByInterface) : DataClassByInterface by wrapped { + override val v = wrapped.v + } + + val i = Inner(C("hello")) + + val bytes = SerializationOutput(testDefaultFactory()).serialize(i) + + try { + val i2 = DeserializationInput(testDefaultFactory()).deserialize(bytes) + } catch (e : NotSerializableException) { + throw Error ("Deserializing serialized \$C should not throw") + } + } } From e338414cd4e5a150fc4b332ad752297547386723 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Tue, 1 May 2018 20:52:19 +0100 Subject: [PATCH 16/25] CORDA-847 - RPC Clent lib refactoring (#3052) Move Kryo into it's own sub module --- .../net/corda/client/rpc/CordaRPCClient.kt | 2 +- .../rpc/internal/RPCClientProxyHandler.kt | 60 +-------------- .../kryo}/KryoClientSerializationScheme.kt | 2 +- .../kryo/RpcClientObservableSerializer.kt | 75 +++++++++++++++++++ docs/source/changelog.rst | 2 + .../kotlin/net/corda/node/internal/Node.kt | 2 +- .../corda/testing/node/internal/RPCDriver.kt | 2 +- .../net/corda/smoketesting/NodeProcess.kt | 2 +- .../testing/core/SerializationTestHelpers.kt | 10 --- .../InternalSerializationTestHelpers.kt | 2 +- .../kotlin/net/corda/demobench/DemoBench.kt | 2 +- 11 files changed, 85 insertions(+), 76 deletions(-) rename client/rpc/src/main/kotlin/net/corda/client/rpc/internal/{ => serialization/kryo}/KryoClientSerializationScheme.kt (97%) create mode 100644 client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/kryo/RpcClientObservableSerializer.kt diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt index cf82f270c9..6080107d3d 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt @@ -1,6 +1,6 @@ package net.corda.client.rpc -import net.corda.client.rpc.internal.KryoClientSerializationScheme +import net.corda.client.rpc.internal.serialization.kryo.KryoClientSerializationScheme import net.corda.client.rpc.internal.RPCClient import net.corda.client.rpc.internal.CordaRPCClientConfigurationImpl import net.corda.core.context.Actor 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 2ddb44cbad..b5146bc8ef 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 @@ -14,6 +14,7 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder import net.corda.client.rpc.CordaRPCClientConfiguration import net.corda.client.rpc.RPCException import net.corda.client.rpc.RPCSinceVersion +import net.corda.client.rpc.internal.serialization.kryo.RpcClientObservableSerializer import net.corda.core.context.Actor import net.corda.core.context.Trace import net.corda.core.context.Trace.InvocationId @@ -547,62 +548,3 @@ data class ObservableContext( val hardReferenceStore: MutableSet> ) -/** - * A [Serializer] to deserialise Observables once the corresponding Kryo instance has been provided with an [ObservableContext]. - */ -object RpcClientObservableSerializer : Serializer>() { - private object RpcObservableContextKey - - fun createContext(serializationContext: SerializationContext, observableContext: ObservableContext): SerializationContext { - return serializationContext.withProperty(RpcObservableContextKey, observableContext) - } - - private fun pinInSubscriptions(observable: Observable, hardReferenceStore: MutableSet>): Observable { - val refCount = AtomicInteger(0) - return observable.doOnSubscribe { - if (refCount.getAndIncrement() == 0) { - require(hardReferenceStore.add(observable)) { "Reference store already contained reference $this on add" } - } - }.doOnUnsubscribe { - if (refCount.decrementAndGet() == 0) { - require(hardReferenceStore.remove(observable)) { "Reference store did not contain reference $this on remove" } - } - } - } - - override fun read(kryo: Kryo, input: Input, type: Class>): Observable { - val observableContext = kryo.context[RpcObservableContextKey] as ObservableContext - val observableId = input.readInvocationId() ?: throw IllegalStateException("Unable to read invocationId from Input.") - val observable = UnicastSubject.create>() - require(observableContext.observableMap.getIfPresent(observableId) == null) { - "Multiple Observables arrived with the same ID $observableId" - } - val rpcCallSite = getRpcCallSite(kryo, observableContext) - observableContext.observableMap.put(observableId, observable) - observableContext.callSiteMap?.put(observableId, rpcCallSite) - // We pin all Observables into a hard reference store (rooted in the RPC proxy) on subscription so that users - // don't need to store a reference to the Observables themselves. - return pinInSubscriptions(observable, observableContext.hardReferenceStore).doOnUnsubscribe { - // This causes Future completions to give warnings because the corresponding OnComplete sent from the server - // will arrive after the client unsubscribes from the observable and consequently invalidates the mapping. - // The unsubscribe is due to [ObservableToFuture]'s use of first(). - observableContext.observableMap.invalidate(observableId) - }.dematerialize() - } - - private fun Input.readInvocationId() : InvocationId? { - - val value = readString() ?: return null - val timestamp = readLong() - return InvocationId(value, Instant.ofEpochMilli(timestamp)) - } - - override fun write(kryo: Kryo, output: Output, observable: Observable<*>) { - throw UnsupportedOperationException("Cannot serialise Observables on the client side") - } - - private fun getRpcCallSite(kryo: Kryo, observableContext: ObservableContext): Throwable? { - val rpcRequestOrObservableId = kryo.context[RPCApi.RpcRequestOrObservableIdKey] as InvocationId - return observableContext.callSiteMap?.get(rpcRequestOrObservableId) - } -} diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/KryoClientSerializationScheme.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/kryo/KryoClientSerializationScheme.kt similarity index 97% rename from client/rpc/src/main/kotlin/net/corda/client/rpc/internal/KryoClientSerializationScheme.kt rename to client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/kryo/KryoClientSerializationScheme.kt index 998ac3c927..0fa4878018 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/KryoClientSerializationScheme.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/kryo/KryoClientSerializationScheme.kt @@ -1,4 +1,4 @@ -package net.corda.client.rpc.internal +package net.corda.client.rpc.internal.serialization.kryo import com.esotericsoftware.kryo.pool.KryoPool import net.corda.core.serialization.SerializationContext diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/kryo/RpcClientObservableSerializer.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/kryo/RpcClientObservableSerializer.kt new file mode 100644 index 0000000000..2a8f6f5283 --- /dev/null +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/kryo/RpcClientObservableSerializer.kt @@ -0,0 +1,75 @@ +package net.corda.client.rpc.internal.serialization.kryo + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.Serializer +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import net.corda.client.rpc.internal.ObservableContext +import net.corda.core.context.Trace +import net.corda.core.serialization.SerializationContext +import net.corda.nodeapi.RPCApi +import rx.Notification +import rx.Observable +import rx.subjects.UnicastSubject +import java.time.Instant +import java.util.concurrent.atomic.AtomicInteger + +/** + * A [Serializer] to deserialise Observables once the corresponding Kryo instance has been provided with an [ObservableContext]. + */ +object RpcClientObservableSerializer : Serializer>() { + private object RpcObservableContextKey + + fun createContext(serializationContext: SerializationContext, observableContext: ObservableContext): SerializationContext { + return serializationContext.withProperty(RpcObservableContextKey, observableContext) + } + + private fun pinInSubscriptions(observable: Observable, hardReferenceStore: MutableSet>): Observable { + val refCount = AtomicInteger(0) + return observable.doOnSubscribe { + if (refCount.getAndIncrement() == 0) { + require(hardReferenceStore.add(observable)) { "Reference store already contained reference $this on add" } + } + }.doOnUnsubscribe { + if (refCount.decrementAndGet() == 0) { + require(hardReferenceStore.remove(observable)) { "Reference store did not contain reference $this on remove" } + } + } + } + + override fun read(kryo: Kryo, input: Input, type: Class>): Observable { + val observableContext = kryo.context[RpcObservableContextKey] as ObservableContext + val observableId = input.readInvocationId() ?: throw IllegalStateException("Unable to read invocationId from Input.") + val observable = UnicastSubject.create>() + require(observableContext.observableMap.getIfPresent(observableId) == null) { + "Multiple Observables arrived with the same ID $observableId" + } + val rpcCallSite = getRpcCallSite(kryo, observableContext) + observableContext.observableMap.put(observableId, observable) + observableContext.callSiteMap?.put(observableId, rpcCallSite) + // We pin all Observables into a hard reference store (rooted in the RPC proxy) on subscription so that users + // don't need to store a reference to the Observables themselves. + return pinInSubscriptions(observable, observableContext.hardReferenceStore).doOnUnsubscribe { + // This causes Future completions to give warnings because the corresponding OnComplete sent from the server + // will arrive after the client unsubscribes from the observable and consequently invalidates the mapping. + // The unsubscribe is due to [ObservableToFuture]'s use of first(). + observableContext.observableMap.invalidate(observableId) + }.dematerialize() + } + + private fun Input.readInvocationId() : Trace.InvocationId? { + + val value = readString() ?: return null + val timestamp = readLong() + return Trace.InvocationId(value, Instant.ofEpochMilli(timestamp)) + } + + override fun write(kryo: Kryo, output: Output, observable: Observable<*>) { + throw UnsupportedOperationException("Cannot serialise Observables on the client side") + } + + private fun getRpcCallSite(kryo: Kryo, observableContext: ObservableContext): Throwable? { + val rpcRequestOrObservableId = kryo.context[RPCApi.RpcRequestOrObservableIdKey] as Trace.InvocationId + return observableContext.callSiteMap?.get(rpcRequestOrObservableId) + } +} diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 3d84a6dcb5..e602cf0f1a 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -7,6 +7,8 @@ release, see :doc:`upgrade-notes`. Unreleased ========== +* Refactor RPC Client Kryo observable serialiser into it's own sub module + * Fix CORDA-1403 where a property of a class that implemented a generic interface could not be deserialised in a factory without a serialiser as the subtype check for the class instance failed. Fix is to compare the raw type. 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 0ea0fc399b..3b4b2719f1 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -1,7 +1,7 @@ package net.corda.node.internal import com.codahale.metrics.JmxReporter -import net.corda.client.rpc.internal.KryoClientSerializationScheme +import net.corda.client.rpc.internal.serialization.kryo.KryoClientSerializationScheme import net.corda.core.concurrent.CordaFuture import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.concurrent.thenMatch diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt index bc7a0cc379..289bc07139 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt @@ -1,7 +1,7 @@ package net.corda.testing.node.internal import net.corda.client.mock.Generator -import net.corda.client.rpc.internal.KryoClientSerializationScheme +import net.corda.client.rpc.internal.serialization.kryo.KryoClientSerializationScheme import net.corda.client.rpc.internal.RPCClient import net.corda.client.rpc.internal.CordaRPCClientConfigurationImpl import net.corda.core.concurrent.CordaFuture diff --git a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt index 3c89bcb97d..ee3936c4c2 100644 --- a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt +++ b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt @@ -2,7 +2,7 @@ package net.corda.smoketesting import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCConnection -import net.corda.client.rpc.internal.KryoClientSerializationScheme +import net.corda.client.rpc.internal.serialization.kryo.KryoClientSerializationScheme import net.corda.core.internal.* import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/core/SerializationTestHelpers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/core/SerializationTestHelpers.kt index 2cebaecec3..5cfe318790 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/core/SerializationTestHelpers.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/core/SerializationTestHelpers.kt @@ -2,19 +2,10 @@ package net.corda.testing.core import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.doAnswer -import com.nhaarman.mockito_kotlin.doNothing import com.nhaarman.mockito_kotlin.whenever -import net.corda.client.rpc.internal.KryoClientSerializationScheme -import net.corda.core.DoNotImplement import net.corda.core.internal.staticField import net.corda.core.serialization.internal.SerializationEnvironment -import net.corda.core.serialization.internal.SerializationEnvironmentImpl -import net.corda.core.serialization.internal._globalSerializationEnv import net.corda.core.serialization.internal.effectiveSerializationEnv -import net.corda.node.serialization.KryoServerSerializationScheme -import net.corda.nodeapi.internal.serialization.* -import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme -import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme import net.corda.testing.common.internal.asContextEnv import net.corda.testing.internal.createTestSerializationEnv import net.corda.testing.internal.inVMExecutors @@ -24,7 +15,6 @@ import org.apache.activemq.artemis.core.remoting.impl.invm.InVMConnector import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement -import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ExecutorService import java.util.concurrent.Executors diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt index 5c706e686c..d270ab1d93 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt @@ -2,7 +2,7 @@ package net.corda.testing.internal import com.nhaarman.mockito_kotlin.doNothing import com.nhaarman.mockito_kotlin.whenever -import net.corda.client.rpc.internal.KryoClientSerializationScheme +import net.corda.client.rpc.internal.serialization.kryo.KryoClientSerializationScheme import net.corda.core.DoNotImplement import net.corda.core.serialization.internal.* import net.corda.node.serialization.KryoServerSerializationScheme diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/DemoBench.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/DemoBench.kt index 09233c0a02..df5a534352 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/DemoBench.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/DemoBench.kt @@ -1,7 +1,7 @@ package net.corda.demobench import javafx.scene.image.Image -import net.corda.client.rpc.internal.KryoClientSerializationScheme +import net.corda.client.rpc.internal.serialization.kryo.KryoClientSerializationScheme import net.corda.core.serialization.internal.SerializationEnvironmentImpl import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.demobench.views.DemoBenchView From 2750017b8e7a536c81c80bdabe84540926f31d94 Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Wed, 2 May 2018 09:54:28 +0100 Subject: [PATCH 17/25] Bug fix: registration tool doesn't register for service identity if keystore already contains node cert (#756) * fix a bug where registration tool will refuse to register for a service identity if the keystore contain NODE_CA key already. some refactoring (cherry picked from commit 5ed60ab) --- .../net/corda/node/internal/NodeStartup.kt | 4 +- .../registration/NetworkRegistrationHelper.kt | 153 ++++++++++-------- .../NetworkRegistrationHelperTest.kt | 29 ++-- .../testing/node/internal/DriverDSLImpl.kt | 27 ++-- 4 files changed, 122 insertions(+), 91 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index 3398a312b6..19a58b25fa 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -14,7 +14,7 @@ import net.corda.node.services.config.shouldStartLocalShell import net.corda.node.services.config.shouldStartSSHDaemon import net.corda.node.services.transactions.bftSMaRtSerialFilter import net.corda.node.utilities.registration.HTTPNetworkRegistrationService -import net.corda.node.utilities.registration.NetworkRegistrationHelper +import net.corda.node.utilities.registration.NodeRegistrationHelper import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.config.UnknownConfigurationKeysException import net.corda.tools.shell.InteractiveShell @@ -192,7 +192,7 @@ open class NodeStartup(val args: Array) { println("* Registering as a new participant with Corda network *") println("* *") println("******************************************************************") - NetworkRegistrationHelper(conf, HTTPNetworkRegistrationService(compatibilityZoneURL), nodeRegistrationConfig).buildKeystore() + NodeRegistrationHelper(conf, HTTPNetworkRegistrationService(compatibilityZoneURL), nodeRegistrationConfig).buildKeystore() } open protected fun loadConfigFile(cmdlineOptions: CmdLineOptions): NodeConfiguration = cmdlineOptions.loadConfig() diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt index 1a253a701a..d36a2f8a11 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt @@ -5,7 +5,6 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.internal.* import net.corda.node.NodeRegistrationOption import net.corda.node.services.config.NodeConfiguration -import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509KeyStore @@ -25,19 +24,17 @@ import java.security.cert.X509Certificate * Helper for managing the node registration process, which checks for any existing certificates and requests them if * needed. */ -class NetworkRegistrationHelper(private val config: SSLConfiguration, - private val myLegalName: CordaX500Name, - private val emailAddress: String, - private val certService: NetworkRegistrationService, - private val networkRootTrustStorePath: Path, - networkRootTrustStorePassword: String, - private val certRole: CertRole) { +// TODO: Use content signer instead of keypairs. +open class NetworkRegistrationHelper(private val config: SSLConfiguration, + private val myLegalName: CordaX500Name, + private val emailAddress: String, + private val certService: NetworkRegistrationService, + private val networkRootTrustStorePath: Path, + networkRootTrustStorePassword: String, + private val keyAlias: String, + private val certRole: CertRole) { - // Constructor for corda node, cert role is restricted to [CertRole.NODE_CA]. - constructor(config: NodeConfiguration, certService: NetworkRegistrationService, regConfig: NodeRegistrationOption) : - this(config, config.myLegalName, config.emailAddress, certService, regConfig.networkRootTrustStorePath, regConfig.networkRootTrustStorePassword, CertRole.NODE_CA) - - private companion object { + companion object { const val SELF_SIGNED_PRIVATE_KEY = "Self Signed Private Key" } @@ -70,22 +67,13 @@ class NetworkRegistrationHelper(private val config: SSLConfiguration, fun buildKeystore() { config.certificatesDirectory.createDirectories() val nodeKeyStore = config.loadNodeKeyStore(createNew = true) - if (CORDA_CLIENT_CA in nodeKeyStore) { + if (keyAlias in nodeKeyStore) { println("Certificate already exists, Corda node will now terminate...") return } - // Create or load self signed keypair from the key store. - // We use the self sign certificate to store the key temporarily in the keystore while waiting for the request approval. - if (SELF_SIGNED_PRIVATE_KEY !in nodeKeyStore) { - val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val selfSignCert = X509Utilities.createSelfSignedCACertificate(myLegalName.x500Principal, keyPair) - // Save to the key store. - nodeKeyStore.setPrivateKey(SELF_SIGNED_PRIVATE_KEY, keyPair.private, listOf(selfSignCert), keyPassword = privateKeyPassword) - nodeKeyStore.save() - } + val keyPair = nodeKeyStore.loadOrCreateKeyPair(SELF_SIGNED_PRIVATE_KEY) - val keyPair = nodeKeyStore.getCertificateAndKeyPair(SELF_SIGNED_PRIVATE_KEY, privateKeyPassword).keyPair val requestId = submitOrResumeCertificateSigningRequest(keyPair) val certificates = try { @@ -97,11 +85,18 @@ class NetworkRegistrationHelper(private val config: SSLConfiguration, requestIdStore.deleteIfExists() throw certificateRequestException } + validateCertificates(certificates) + storePrivateKeyWithCertificates(nodeKeyStore, keyPair, certificates, keyAlias) + onSuccess(keyPair, certificates) + // All done, clean up temp files. + requestIdStore.deleteIfExists() + } - val certificate = certificates.first() + private fun validateCertificates(certificates: List) { + val nodeCACertificate = certificates.first() val nodeCaSubject = try { - CordaX500Name.build(certificate.subjectX500Principal) + CordaX500Name.build(nodeCACertificate.subjectX500Principal) } catch (e: IllegalArgumentException) { throw CertificateRequestException("Received node CA cert has invalid subject name: ${e.message}") } @@ -110,56 +105,39 @@ class NetworkRegistrationHelper(private val config: SSLConfiguration, } val nodeCaCertRole = try { - CertRole.extract(certificate) + CertRole.extract(nodeCACertificate) } catch (e: IllegalArgumentException) { throw CertificateRequestException("Unable to extract cert role from received node CA cert: ${e.message}") } + if (certRole != nodeCaCertRole) { + throw CertificateRequestException("Received certificate contains invalid cert role, expected '$certRole', got '$nodeCaCertRole'.") + } + // Validate certificate chain returned from the doorman with the root cert obtained via out-of-band process, to prevent MITM attack on doorman server. X509Utilities.validateCertificateChain(rootCert, certificates) - println("Certificate signing request approved, storing private key with the certificate chain.") + } - when (nodeCaCertRole) { - CertRole.NODE_CA -> { - // Save private key and certificate chain to the key store. - nodeKeyStore.setPrivateKey(CORDA_CLIENT_CA, keyPair.private, certificates, keyPassword = privateKeyPassword) - nodeKeyStore.internal.deleteEntry(SELF_SIGNED_PRIVATE_KEY) - nodeKeyStore.save() - println("Node private key and certificate stored in ${config.nodeKeystore}.") + private fun storePrivateKeyWithCertificates(nodeKeystore: X509KeyStore, keyPair: KeyPair, certificates: List, keyAlias: String) { + // Save private key and certificate chain to the key store. + nodeKeystore.setPrivateKey(keyAlias, keyPair.private, certificates, keyPassword = config.keyStorePassword) + nodeKeystore.internal.deleteEntry(SELF_SIGNED_PRIVATE_KEY) + nodeKeystore.save() + println("Private key '$keyAlias' and certificate stored in ${config.nodeKeystore}.") + } - config.loadSslKeyStore(createNew = true).update { - println("Generating SSL certificate for node messaging service.") - val sslKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val sslCert = X509Utilities.createCertificate( - CertificateType.TLS, - certificate, - keyPair, - myLegalName.x500Principal, - sslKeyPair.public) - setPrivateKey(CORDA_CLIENT_TLS, sslKeyPair.private, listOf(sslCert) + certificates) - } - println("SSL private key and certificate stored in ${config.sslKeystore}.") - } - // TODO: Fix this, this is not needed in corda node. - CertRole.SERVICE_IDENTITY -> { - // Only create keystore containing notary's key for service identity role. - nodeKeyStore.setPrivateKey("${DevIdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX}-private-key", keyPair.private, certificates, keyPassword = privateKeyPassword) - nodeKeyStore.internal.deleteEntry(SELF_SIGNED_PRIVATE_KEY) - nodeKeyStore.save() - println("Service identity private key and certificate stored in ${config.nodeKeystore}.") - } - else -> throw CertificateRequestException("Received node CA cert has invalid role: $nodeCaCertRole") + private fun X509KeyStore.loadOrCreateKeyPair(alias: String): KeyPair { + // Create or load self signed keypair from the key store. + // We use the self sign certificate to store the key temporarily in the keystore while waiting for the request approval. + if (alias !in this) { + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val selfSignCert = X509Utilities.createSelfSignedCACertificate(myLegalName.x500Principal, keyPair) + // Save to the key store. + setPrivateKey(alias, keyPair.private, listOf(selfSignCert), keyPassword = privateKeyPassword) + save() } - // Save root certificates to trust store. - config.loadTrustStore(createNew = true).update { - println("Generating trust store for corda node.") - // Assumes certificate chain always starts with client certificate and end with root certificate. - setCertificate(CORDA_ROOT_CA, certificates.last()) - } - println("Node trust store stored in ${config.trustStoreFile}.") - // All done, clean up temp files. - requestIdStore.deleteIfExists() + return getCertificateAndKeyPair(alias, privateKeyPassword).keyPair } /** @@ -215,4 +193,47 @@ class NetworkRegistrationHelper(private val config: SSLConfiguration, requestId } } + + protected open fun onSuccess(nodeCAKeyPair: KeyPair, certificates: List) {} +} + +class NodeRegistrationHelper(private val config: NodeConfiguration, certService: NetworkRegistrationService, regConfig: NodeRegistrationOption) : + NetworkRegistrationHelper(config, + config.myLegalName, + config.emailAddress, + certService, + regConfig.networkRootTrustStorePath, + regConfig.networkRootTrustStorePassword, + CORDA_CLIENT_CA, + CertRole.NODE_CA) { + + override fun onSuccess(nodeCAKeyPair: KeyPair, certificates: List) { + createSSLKeystore(nodeCAKeyPair, certificates) + createTruststore(certificates.last()) + } + + private fun createSSLKeystore(nodeCAKeyPair: KeyPair, certificates: List) { + config.loadSslKeyStore(createNew = true).update { + println("Generating SSL certificate for node messaging service.") + val sslKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val sslCert = X509Utilities.createCertificate( + CertificateType.TLS, + certificates.first(), + nodeCAKeyPair, + config.myLegalName.x500Principal, + sslKeyPair.public) + setPrivateKey(CORDA_CLIENT_TLS, sslKeyPair.private, listOf(sslCert) + certificates) + } + println("SSL private key and certificate stored in ${config.sslKeystore}.") + } + + private fun createTruststore(rootCertificate: X509Certificate) { + // Save root certificates to trust store. + config.loadTrustStore(createNew = true).update { + println("Generating trust store for corda node.") + // Assumes certificate chain always starts with client certificate and end with root certificate. + setCertificate(CORDA_ROOT_CA, rootCertificate) + } + println("Node trust store stored in ${config.trustStoreFile}.") + } } diff --git a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt index d77e61bcae..e93805b717 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt @@ -9,6 +9,7 @@ import com.nhaarman.mockito_kotlin.whenever import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.CertRole import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.internal.x500Name @@ -152,11 +153,12 @@ class NetworkRegistrationHelperTest { val serviceIdentityCertPath = createServiceIdentityCertPath() saveNetworkTrustStore(serviceIdentityCertPath.last()) - createRegistrationHelper(serviceIdentityCertPath).buildKeystore() + createRegistrationHelper(serviceIdentityCertPath, CertRole.SERVICE_IDENTITY).buildKeystore() val nodeKeystore = config.loadNodeKeyStore() - val trustStore = config.loadTrustStore() + assertThat(config.sslKeystore).doesNotExist() + assertThat(config.trustStoreFile).doesNotExist() val serviceIdentityAlias = "${DevIdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX}-private-key" @@ -167,12 +169,6 @@ class NetworkRegistrationHelperTest { assertFalse(contains(X509Utilities.CORDA_CLIENT_CA)) assertThat(getCertificateChain(serviceIdentityAlias)).containsExactlyElementsOf(serviceIdentityCertPath) } - - trustStore.run { - assertFalse(contains(X509Utilities.CORDA_CLIENT_CA)) - assertFalse(contains(X509Utilities.CORDA_INTERMEDIATE_CA)) - assertThat(getCertificate(X509Utilities.CORDA_ROOT_CA)).isEqualTo(serviceIdentityCertPath.last()) - } } private fun createNodeCaCertPath(type: CertificateType = CertificateType.NODE_CA, @@ -203,12 +199,25 @@ class NetworkRegistrationHelperTest { return listOf(serviceIdentityCert, intermediateCa.certificate, rootCa.certificate) } - private fun createRegistrationHelper(response: List): NetworkRegistrationHelper { + private fun createRegistrationHelper(response: List, certRole: CertRole = CertRole.NODE_CA): NetworkRegistrationHelper { val certService = rigorousMock().also { doReturn(requestId).whenever(it).submitRequest(any()) doReturn(CertificateResponse(5.seconds, response)).whenever(it).retrieveCertificates(eq(requestId)) } - return NetworkRegistrationHelper(config, certService, NodeRegistrationOption(config.certificatesDirectory / networkRootTrustStoreFileName, networkRootTrustStorePassword)) + + return when (certRole) { + CertRole.NODE_CA -> NodeRegistrationHelper(config, certService, NodeRegistrationOption(config.certificatesDirectory / networkRootTrustStoreFileName, networkRootTrustStorePassword)) + CertRole.SERVICE_IDENTITY -> NetworkRegistrationHelper( + config, + config.myLegalName, + config.emailAddress, + certService, + config.certificatesDirectory / networkRootTrustStoreFileName, + networkRootTrustStorePassword, + "${DevIdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX}-private-key", + CertRole.SERVICE_IDENTITY) + else -> throw IllegalArgumentException("Unsupported cert role.") + } } private fun saveNetworkTrustStore(rootCert: X509Certificate) { 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 fc9430a245..80a282fd0a 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 @@ -29,7 +29,7 @@ import net.corda.node.internal.StartedNode import net.corda.node.services.Permissions import net.corda.node.services.config.* import net.corda.node.utilities.registration.HTTPNetworkRegistrationService -import net.corda.node.utilities.registration.NetworkRegistrationHelper +import net.corda.node.utilities.registration.NodeRegistrationHelper import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.addShutdownHook @@ -247,7 +247,7 @@ class DriverDSLImpl( return if (startNodesInProcess) { executorService.fork { - NetworkRegistrationHelper( + NodeRegistrationHelper( config.corda, HTTPNetworkRegistrationService(compatibilityZoneURL), NodeRegistrationOption(rootTruststorePath, rootTruststorePassword) @@ -849,8 +849,7 @@ class DriverDSLImpl( val index = stackTrace.indexOfLast { it.className == "net.corda.testing.driver.Driver" } // In this case we're dealing with the the RPCDriver or one of it's cousins which are internal and we don't care about them if (index == -1) return emptyList() - val callerPackage = Class.forName(stackTrace[index + 1].className).`package` ?: - throw IllegalStateException("Function instantiating driver must be defined in a package.") + val callerPackage = Class.forName(stackTrace[index + 1].className).`package` ?: throw IllegalStateException("Function instantiating driver must be defined in a package.") return listOf(callerPackage.name) } @@ -898,16 +897,18 @@ private class NetworkVisibilityController { val (snapshot, updates) = rpc.networkMapFeed() visibleNodeCount = snapshot.size checkIfAllVisible() - subscription = updates.subscribe { when (it) { - is NetworkMapCache.MapChange.Added -> { - visibleNodeCount++ - checkIfAllVisible() + subscription = updates.subscribe { + when (it) { + is NetworkMapCache.MapChange.Added -> { + visibleNodeCount++ + checkIfAllVisible() + } + is NetworkMapCache.MapChange.Removed -> { + visibleNodeCount-- + checkIfAllVisible() + } } - is NetworkMapCache.MapChange.Removed -> { - visibleNodeCount-- - checkIfAllVisible() - } - } } + } return future } From 514287e694c85c44a4dc4a04daec9caed792faaa Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Wed, 2 May 2018 10:10:23 +0100 Subject: [PATCH 18/25] Fixed invalid page ref in upgrade-notes.rst (#3053) --- docs/source/upgrade-notes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/upgrade-notes.rst b/docs/source/upgrade-notes.rst index b9634d252e..f9734f59b4 100644 --- a/docs/source/upgrade-notes.rst +++ b/docs/source/upgrade-notes.rst @@ -77,7 +77,7 @@ With the re-designed network map service the following changes need to be made: * The network map is no longer provided by a node and thus the ``networkMapService`` config is ignored. Instead the network map is either provided by the compatibility zone (CZ) operator (who operates the doorman) and available using the ``compatibilityZoneURL`` config, or is provided using signed node info files which are copied locally. - See :doc:`network-map` for more details, and :doc:`setting-up-a-corda-network.rst` on how to use the network + See :doc:`network-map` for more details, and :doc:`setting-up-a-corda-network` on how to use the network bootstrapper for deploying a local network. * Configuration for a notary has been simplified. ``extraAdvertisedServiceIds``, ``notaryNodeAddress``, ``notaryClusterAddresses`` From 3bf1e803d97ab191acab6c098ef8427c33d666c0 Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Wed, 2 May 2018 12:21:38 +0100 Subject: [PATCH 19/25] =?UTF-8?q?Move=20notary=20service=20related=20class?= =?UTF-8?q?es=20and=20interfaces=20in=20core=20to=20interna=E2=80=A6=20(#2?= =?UTF-8?q?827)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Move notary service related classes and interfaces in core to internal, since we won't be able to stabilise the APIs for writing custom notary services any time soon (the docs already mention it). I left out the wire protocol related classes so we don't accidentally break it. --- .ci/api-current.txt | 85 ---------- .../net/corda/core/flows/NotaryError.kt | 73 +++++++++ .../kotlin/net/corda/core/flows/NotaryFlow.kt | 151 +----------------- ...risationRequest.kt => NotaryWireFormat.kt} | 36 +---- .../net/corda/core/internal/InternalUtils.kt | 20 --- .../net/corda/core/internal/NotaryUtils.kt | 36 ----- .../core/internal/notary/NotaryService.kt | 23 +++ .../core/internal/notary/NotaryServiceFlow.kt | 89 +++++++++++ .../corda/core/internal/notary/NotaryUtils.kt | 77 +++++++++ .../notary/TrustedAuthorityNotaryService.kt} | 66 +------- .../internal/notary/UniquenessProvider.kt | 24 +++ .../core/node/services/UniquenessProvider.kt | 44 ----- docs/source/changelog.rst | 6 +- .../net/corda/node/internal/AbstractNode.kt | 1 + .../net/corda/node/internal/StartedNode.kt | 2 +- .../BFTNonValidatingNotaryService.kt | 4 +- .../node/services/transactions/BFTSMaRt.kt | 10 +- .../transactions/NonValidatingNotaryFlow.kt | 10 +- .../PersistentUniquenessProvider.kt | 8 +- .../RaftNonValidatingNotaryService.kt | 6 +- .../transactions/RaftTransactionCommitLog.kt | 4 +- .../transactions/RaftUniquenessProvider.kt | 4 +- .../RaftValidatingNotaryService.kt | 6 +- .../transactions/SimpleNotaryService.kt | 6 +- .../transactions/ValidatingNotaryFlow.kt | 12 +- .../transactions/ValidatingNotaryService.kt | 6 +- .../PersistentUniquenessProviderTests.kt | 2 +- .../ValidatingNotaryServiceTests.kt | 2 +- .../corda/notarydemo/MyCustomNotaryService.kt | 7 +- 29 files changed, 352 insertions(+), 468 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/flows/NotaryError.kt rename core/src/main/kotlin/net/corda/core/flows/{NotarisationRequest.kt => NotaryWireFormat.kt} (68%) delete mode 100644 core/src/main/kotlin/net/corda/core/internal/NotaryUtils.kt create mode 100644 core/src/main/kotlin/net/corda/core/internal/notary/NotaryService.kt create mode 100644 core/src/main/kotlin/net/corda/core/internal/notary/NotaryServiceFlow.kt create mode 100644 core/src/main/kotlin/net/corda/core/internal/notary/NotaryUtils.kt rename core/src/main/kotlin/net/corda/core/{node/services/NotaryService.kt => internal/notary/TrustedAuthorityNotaryService.kt} (51%) create mode 100644 core/src/main/kotlin/net/corda/core/internal/notary/UniquenessProvider.kt delete mode 100644 core/src/main/kotlin/net/corda/core/node/services/UniquenessProvider.kt diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 109ba6a38c..572cde8e4a 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -1369,7 +1369,6 @@ public @interface net.corda.core.flows.InitiatingFlow public (List, net.corda.core.crypto.SecureHash) @org.jetbrains.annotations.NotNull public final List getStatesToConsume() @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getTransactionId() - public final void verifySignature(net.corda.core.flows.NotarisationRequestSignature, net.corda.core.identity.Party) public static final net.corda.core.flows.NotarisationRequest$Companion Companion ## public static final class net.corda.core.flows.NotarisationRequest$Companion extends java.lang.Object @@ -1483,18 +1482,6 @@ public static final class net.corda.core.flows.NotaryFlow$Client$Companion exten @net.corda.core.serialization.CordaSerializable public static final class net.corda.core.flows.NotaryFlow$Client$Companion$VALIDATING extends net.corda.core.utilities.ProgressTracker$Step public static final net.corda.core.flows.NotaryFlow$Client$Companion$VALIDATING INSTANCE ## -public abstract static class net.corda.core.flows.NotaryFlow$Service extends net.corda.core.flows.FlowLogic - public (net.corda.core.flows.FlowSession, net.corda.core.node.services.TrustedAuthorityNotaryService) - @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.Nullable public Void call() - @co.paralleluniverse.fibers.Suspendable protected final void checkNotary(net.corda.core.identity.Party) - @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowSession getOtherSideSession() - @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.TrustedAuthorityNotaryService getService() - @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull protected abstract net.corda.core.flows.TransactionParts validateRequest(net.corda.core.flows.NotarisationPayload) -## -@net.corda.core.serialization.CordaSerializable public final class net.corda.core.flows.NotaryInternalException extends net.corda.core.flows.FlowException - public (net.corda.core.flows.NotaryError) - @org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotaryError getError() -## public final class net.corda.core.flows.ReceiveStateAndRefFlow extends net.corda.core.flows.FlowLogic public (net.corda.core.flows.FlowSession) @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public List call() @@ -1574,21 +1561,6 @@ public static final class net.corda.core.flows.StateMachineRunId$Companion exten public (String) public (String, Throwable) ## -public final class net.corda.core.flows.TransactionParts extends java.lang.Object - public (net.corda.core.crypto.SecureHash, List, net.corda.core.contracts.TimeWindow, net.corda.core.identity.Party) - @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component1() - @org.jetbrains.annotations.NotNull public final List component2() - @org.jetbrains.annotations.Nullable public final net.corda.core.contracts.TimeWindow component3() - @org.jetbrains.annotations.Nullable public final net.corda.core.identity.Party component4() - @org.jetbrains.annotations.NotNull public final net.corda.core.flows.TransactionParts copy(net.corda.core.crypto.SecureHash, List, net.corda.core.contracts.TimeWindow, net.corda.core.identity.Party) - public boolean equals(Object) - @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getId() - @org.jetbrains.annotations.NotNull public final List getInputs() - @org.jetbrains.annotations.Nullable public final net.corda.core.identity.Party getNotary() - @org.jetbrains.annotations.Nullable public final net.corda.core.contracts.TimeWindow getTimestamp() - public int hashCode() - public String toString() -## @net.corda.core.serialization.CordaSerializable public final class net.corda.core.flows.UnexpectedFlowEndException extends net.corda.core.CordaRuntimeException implements net.corda.core.flows.IdentifiableException public (String) public (String, Throwable) @@ -2052,21 +2024,6 @@ public @interface net.corda.core.node.services.CordaService public abstract boolean isValidatingNotary(net.corda.core.identity.Party) @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed track() ## -@net.corda.core.serialization.CordaSerializable public abstract class net.corda.core.node.services.NotaryService extends net.corda.core.serialization.SingletonSerializeAsToken - public () - @org.jetbrains.annotations.NotNull public abstract net.corda.core.flows.FlowLogic createServiceFlow(net.corda.core.flows.FlowSession) - @org.jetbrains.annotations.NotNull public abstract java.security.PublicKey getNotaryIdentityKey() - @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.ServiceHub getServices() - public abstract void start() - public abstract void stop() - public static final void validateTimeWindow(java.time.Clock, net.corda.core.contracts.TimeWindow) - public static final net.corda.core.node.services.NotaryService$Companion Companion - @org.jetbrains.annotations.NotNull public static final String ID_PREFIX = "corda.notary." -## -public static final class net.corda.core.node.services.NotaryService$Companion extends java.lang.Object - @kotlin.Deprecated @org.jetbrains.annotations.NotNull public final String constructId(boolean, boolean, boolean, boolean) - public final void validateTimeWindow(java.time.Clock, net.corda.core.contracts.TimeWindow) -## public abstract class net.corda.core.node.services.PartyInfo extends java.lang.Object @org.jetbrains.annotations.NotNull public abstract net.corda.core.identity.Party getParty() ## @@ -2110,48 +2067,6 @@ public final class net.corda.core.node.services.TimeWindowChecker extends java.l @net.corda.core.DoNotImplement public interface net.corda.core.node.services.TransactionVerifierService @org.jetbrains.annotations.NotNull public abstract net.corda.core.concurrent.CordaFuture verify(net.corda.core.transactions.LedgerTransaction) ## -@net.corda.core.serialization.CordaSerializable public abstract class net.corda.core.node.services.TrustedAuthorityNotaryService extends net.corda.core.node.services.NotaryService - public () - public final void commitInputStates(List, net.corda.core.crypto.SecureHash, net.corda.core.identity.Party, net.corda.core.flows.NotarisationRequestSignature, net.corda.core.contracts.TimeWindow) - @org.jetbrains.annotations.NotNull protected org.slf4j.Logger getLog() - @org.jetbrains.annotations.NotNull protected net.corda.core.node.services.TimeWindowChecker getTimeWindowChecker() - @org.jetbrains.annotations.NotNull protected abstract net.corda.core.node.services.UniquenessProvider getUniquenessProvider() - @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.TransactionSignature sign(net.corda.core.crypto.SecureHash) - @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.DigitalSignature$WithKey sign(byte[]) - public final void validateTimeWindow(net.corda.core.contracts.TimeWindow) - public static final net.corda.core.node.services.TrustedAuthorityNotaryService$Companion Companion -## -public static final class net.corda.core.node.services.TrustedAuthorityNotaryService$Companion extends java.lang.Object -## -@net.corda.core.serialization.CordaSerializable public final class net.corda.core.node.services.UniquenessException extends net.corda.core.CordaException - public (net.corda.core.node.services.UniquenessProvider$Conflict) - @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.UniquenessProvider$Conflict getError() -## -public interface net.corda.core.node.services.UniquenessProvider - public abstract void commit(List, net.corda.core.crypto.SecureHash, net.corda.core.identity.Party, net.corda.core.flows.NotarisationRequestSignature, net.corda.core.contracts.TimeWindow) -## -@net.corda.core.serialization.CordaSerializable public static final class net.corda.core.node.services.UniquenessProvider$Conflict extends java.lang.Object - public (Map) - @org.jetbrains.annotations.NotNull public final Map component1() - @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.UniquenessProvider$Conflict copy(Map) - public boolean equals(Object) - @org.jetbrains.annotations.NotNull public final Map getStateHistory() - public int hashCode() - public String toString() -## -@net.corda.core.serialization.CordaSerializable public static final class net.corda.core.node.services.UniquenessProvider$ConsumingTx extends java.lang.Object - public (net.corda.core.crypto.SecureHash, int, net.corda.core.identity.Party) - @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash component1() - public final int component2() - @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party component3() - @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.UniquenessProvider$ConsumingTx copy(net.corda.core.crypto.SecureHash, int, net.corda.core.identity.Party) - public boolean equals(Object) - @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getId() - public final int getInputIndex() - @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party getRequestingParty() - public int hashCode() - public String toString() -## @net.corda.core.serialization.CordaSerializable public final class net.corda.core.node.services.UnknownAnonymousPartyException extends net.corda.core.CordaException public (String) ## diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryError.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryError.kt new file mode 100644 index 0000000000..281f3dfff6 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryError.kt @@ -0,0 +1,73 @@ +package net.corda.core.flows + +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TimeWindow +import net.corda.core.crypto.SecureHash +import net.corda.core.serialization.CordaSerializable +import java.time.Instant + +/** + * Exception thrown by the notary service if any issues are encountered while trying to commit a transaction. The + * underlying [error] specifies the cause of failure. + */ +class NotaryException( + /** Cause of notarisation failure. */ + val error: NotaryError, + /** Id of the transaction to be notarised. Can be _null_ if an error occurred before the id could be resolved. */ + val txId: SecureHash? = null +) : FlowException("Unable to notarise transaction${txId ?: " "}: $error") + +/** Specifies the cause for notarisation request failure. */ +@CordaSerializable +sealed class NotaryError { + /** Occurs when one or more input states have already been consumed by another transaction. */ + data class Conflict( + /** Id of the transaction that was attempted to be notarised. */ + val txId: SecureHash, + /** Specifies which states have already been consumed in another transaction. */ + val consumedStates: Map + ) : NotaryError() { + override fun toString() = "One or more input states have been used in another transaction" + } + + /** Occurs when time specified in the [TimeWindow] command is outside the allowed tolerance. */ + data class TimeWindowInvalid(val currentTime: Instant, val txTimeWindow: TimeWindow) : NotaryError() { + override fun toString() = "Current time $currentTime is outside the time bounds specified by the transaction: $txTimeWindow" + + companion object { + @JvmField + @Deprecated("Here only for binary compatibility purposes, do not use.") + val INSTANCE = TimeWindowInvalid(Instant.EPOCH, TimeWindow.fromOnly(Instant.EPOCH)) + } + } + + /** Occurs when the provided transaction fails to verify. */ + data class TransactionInvalid(val cause: Throwable) : NotaryError() { + override fun toString() = cause.toString() + } + + /** Occurs when the transaction sent for notarisation is assigned to a different notary identity. */ + object WrongNotary : NotaryError() + + /** Occurs when the notarisation request signature does not verify for the provided transaction. */ + data class RequestSignatureInvalid(val cause: Throwable) : NotaryError() { + override fun toString() = "Request signature invalid: $cause" + } + + /** Occurs when the notary service encounters an unexpected issue or becomes temporarily unavailable. */ + data class General(val cause: Throwable) : NotaryError() { + override fun toString() = cause.toString() + } +} + +/** Contains information about the consuming transaction for a particular state. */ +// TODO: include notary timestamp? +@CordaSerializable +data class StateConsumptionDetails( + /** + * Hash of the consuming transaction id. + * + * Note that this is NOT the transaction id itself – revealing it could lead to privacy leaks. + */ + val hashOfTransactionId: SecureHash +) \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt index f746a63bf9..b4a4f874ec 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt @@ -4,22 +4,17 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.DoNotImplement import net.corda.core.contracts.StateRef import net.corda.core.contracts.TimeWindow -import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.identity.Party import net.corda.core.internal.FetchDataFlow -import net.corda.core.internal.generateSignature -import net.corda.core.internal.validateSignatures -import net.corda.core.node.services.NotaryService -import net.corda.core.node.services.TrustedAuthorityNotaryService -import net.corda.core.serialization.CordaSerializable +import net.corda.core.internal.notary.generateSignature +import net.corda.core.internal.notary.validateSignatures import net.corda.core.transactions.ContractUpgradeWireTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.unwrap -import java.time.Instant import java.util.function.Predicate class NotaryFlow { @@ -120,144 +115,4 @@ class NotaryFlow { } } } - - /** - * A flow run by a notary service that handles notarisation requests. - * - * It checks that the time-window command is valid (if present) and commits the input state, or returns a conflict - * if any of the input states have been previously committed. - * - * Additional transaction validation logic can be added when implementing [validateRequest]. - */ - // See AbstractStateReplacementFlow.Acceptor for why it's Void? - abstract class Service(val otherSideSession: FlowSession, val service: TrustedAuthorityNotaryService) : FlowLogic() { - companion object { - // TODO: Determine an appropriate limit and also enforce in the network parameters and the transaction builder. - private const val maxAllowedInputs = 10_000 - } - - @Suspendable - override fun call(): Void? { - check(serviceHub.myInfo.legalIdentities.any { serviceHub.networkMapCache.isNotary(it) }) { - "We are not a notary on the network" - } - val requestPayload = otherSideSession.receive().unwrap { it } - var txId: SecureHash? = null - try { - val parts = validateRequest(requestPayload) - txId = parts.id - checkNotary(parts.notary) - service.commitInputStates(parts.inputs, txId, otherSideSession.counterparty, requestPayload.requestSignature, parts.timestamp) - signTransactionAndSendResponse(txId) - } catch (e: NotaryInternalException) { - throw NotaryException(e.error, txId) - } - return null - } - - /** Checks whether the number of input states is too large. */ - protected fun checkInputs(inputs: List) { - if (inputs.size > maxAllowedInputs) { - val error = NotaryError.TransactionInvalid( - IllegalArgumentException("A transaction cannot have more than $maxAllowedInputs inputs, received: ${inputs.size}") - ) - throw NotaryInternalException(error) - } - } - - /** - * Implement custom logic to perform transaction verification based on validity and privacy requirements. - */ - @Suspendable - protected abstract fun validateRequest(requestPayload: NotarisationPayload): TransactionParts - - /** Check if transaction is intended to be signed by this notary. */ - @Suspendable - protected fun checkNotary(notary: Party?) { - if (notary?.owningKey != service.notaryIdentityKey) { - throw NotaryInternalException(NotaryError.WrongNotary) - } - } - - @Suspendable - private fun signTransactionAndSendResponse(txId: SecureHash) { - val signature = service.sign(txId) - otherSideSession.send(NotarisationResponse(listOf(signature))) - } - } -} - -/** - * The minimum amount of information needed to notarise a transaction. Note that this does not include - * any sensitive transaction details. - */ -data class TransactionParts(val id: SecureHash, val inputs: List, val timestamp: TimeWindow?, val notary: Party?) - -/** - * Exception thrown by the notary service if any issues are encountered while trying to commit a transaction. The - * underlying [error] specifies the cause of failure. - */ -class NotaryException( - /** Cause of notarisation failure. */ - val error: NotaryError, - /** Id of the transaction to be notarised. Can be _null_ if an error occurred before the id could be resolved. */ - val txId: SecureHash? = null -) : FlowException("Unable to notarise transaction${txId ?: " "}: $error") - -/** Exception internal to the notary service. Does not get exposed to CorDapps and flows calling [NotaryFlow.Client]. */ -class NotaryInternalException(val error: NotaryError) : FlowException("Unable to notarise: $error") - -/** Specifies the cause for notarisation request failure. */ -@CordaSerializable -sealed class NotaryError { - /** Occurs when one or more input states have already been consumed by another transaction. */ - data class Conflict( - /** Id of the transaction that was attempted to be notarised. */ - val txId: SecureHash, - /** Specifies which states have already been consumed in another transaction. */ - val consumedStates: Map - ) : NotaryError() { - override fun toString() = "One or more input states have been used in another transaction" - } - - /** Occurs when time specified in the [TimeWindow] command is outside the allowed tolerance. */ - data class TimeWindowInvalid(val currentTime: Instant, val txTimeWindow: TimeWindow) : NotaryError() { - override fun toString() = "Current time $currentTime is outside the time bounds specified by the transaction: $txTimeWindow" - - companion object { - @JvmField - @Deprecated("Here only for binary compatibility purposes, do not use.") - val INSTANCE = TimeWindowInvalid(Instant.EPOCH, TimeWindow.fromOnly(Instant.EPOCH)) - } - } - - /** Occurs when the provided transaction fails to verify. */ - data class TransactionInvalid(val cause: Throwable) : NotaryError() { - override fun toString() = cause.toString() - } - - /** Occurs when the transaction sent for notarisation is assigned to a different notary identity. */ - object WrongNotary : NotaryError() - - /** Occurs when the notarisation request signature does not verify for the provided transaction. */ - data class RequestSignatureInvalid(val cause: Throwable) : NotaryError() { - override fun toString() = "Request signature invalid: $cause" - } - - /** Occurs when the notary service encounters an unexpected issue or becomes temporarily unavailable. */ - data class General(val cause: Throwable) : NotaryError() { - override fun toString() = cause.toString() - } -} - -/** Contains information about the consuming transaction for a particular state. */ -// TODO: include notary timestamp? -@CordaSerializable -data class StateConsumptionDetails( - /** - * Hash of the consuming transaction id. - * - * Note that this is NOT the transaction id itself – revealing it could lead to privacy leaks. - */ - val hashOfTransactionId: SecureHash -) \ No newline at end of file +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/flows/NotarisationRequest.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryWireFormat.kt similarity index 68% rename from core/src/main/kotlin/net/corda/core/flows/NotarisationRequest.kt rename to core/src/main/kotlin/net/corda/core/flows/NotaryWireFormat.kt index 24ea4bd675..2871cec4d3 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotarisationRequest.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryWireFormat.kt @@ -1,14 +1,12 @@ package net.corda.core.flows import net.corda.core.contracts.StateRef -import net.corda.core.crypto.* -import net.corda.core.identity.Party +import net.corda.core.crypto.DigitalSignature +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.TransactionSignature import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.serialize import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.SignedTransaction -import java.security.InvalidKeyException -import java.security.SignatureException /** * A notarisation request specifies a list of states to consume and the id of the consuming transaction. Its primary @@ -36,34 +34,6 @@ class NotarisationRequest(statesToConsume: List, val transactionId: Se /** States this request specifies to be consumed. Sorted to ensure the serialized form does not get affected by the state order. */ val statesToConsume: List get() = _statesToConsumeSorted // Getter required for AMQP serialization - - /** Verifies the signature against this notarisation request. Checks that the signature is issued by the right party. */ - fun verifySignature(requestSignature: NotarisationRequestSignature, intendedSigner: Party) { - val signature = requestSignature.digitalSignature - if (intendedSigner.owningKey != signature.by) { - val errorMessage = "Expected a signature by ${intendedSigner.owningKey}, but received by ${signature.by}}" - throw NotaryInternalException(NotaryError.RequestSignatureInvalid(IllegalArgumentException(errorMessage))) - } - // TODO: if requestSignature was generated over an old version of NotarisationRequest, we need to be able to - // reserialize it in that version to get the exact same bytes. Modify the serialization logic once that's - // available. - val expectedSignedBytes = this.serialize().bytes - verifyCorrectBytesSigned(signature, expectedSignedBytes) - } - - private fun verifyCorrectBytesSigned(signature: DigitalSignature.WithKey, bytes: ByteArray) { - try { - signature.verify(bytes) - } catch (e: Exception) { - when (e) { - is InvalidKeyException, is SignatureException -> { - val error = NotaryError.RequestSignatureInvalid(e) - throw NotaryInternalException(error) - } - else -> throw e - } - } - } } /** diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index eafbaea2ac..8590cb5a64 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -8,11 +8,7 @@ import net.corda.core.cordapp.Cordapp import net.corda.core.cordapp.CordappConfig import net.corda.core.cordapp.CordappContext import net.corda.core.crypto.* -import net.corda.core.flows.NotarisationRequest -import net.corda.core.flows.NotarisationRequestSignature -import net.corda.core.flows.NotaryFlow import net.corda.core.identity.CordaX500Name -import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializedBytes @@ -412,22 +408,6 @@ fun createCordappContext(cordapp: Cordapp, attachmentId: SecureHash?, classLoade return CordappContext(cordapp, attachmentId, classLoader, config) } -/** Verifies that the correct notarisation request was signed by the counterparty. */ -fun NotaryFlow.Service.validateRequestSignature(request: NotarisationRequest, signature: NotarisationRequestSignature) { - val requestingParty = otherSideSession.counterparty - request.verifySignature(signature, requestingParty) -} - -/** Creates a signature over the notarisation request using the legal identity key. */ -fun NotarisationRequest.generateSignature(serviceHub: ServiceHub): NotarisationRequestSignature { - val serializedRequest = this.serialize().bytes - val signature = with(serviceHub) { - val myLegalIdentity = myInfo.legalIdentitiesAndCerts.first().owningKey - keyManagementService.sign(serializedRequest, myLegalIdentity) - } - return NotarisationRequestSignature(signature, serviceHub.myInfo.platformVersion) -} - val PublicKey.hash: SecureHash get() = encoded.sha256() /** diff --git a/core/src/main/kotlin/net/corda/core/internal/NotaryUtils.kt b/core/src/main/kotlin/net/corda/core/internal/NotaryUtils.kt deleted file mode 100644 index 21e639c2e1..0000000000 --- a/core/src/main/kotlin/net/corda/core/internal/NotaryUtils.kt +++ /dev/null @@ -1,36 +0,0 @@ -package net.corda.core.internal - -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TimeWindow -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.isFulfilledBy -import net.corda.core.flows.NotarisationResponse -import net.corda.core.flows.NotaryError -import net.corda.core.flows.StateConsumptionDetails -import net.corda.core.identity.Party -import java.time.Instant - -/** - * Checks that there are sufficient signatures to satisfy the notary signing requirement and validates the signatures - * against the given transaction id. - */ -fun NotarisationResponse.validateSignatures(txId: SecureHash, notary: Party) { - val signingKeys = signatures.map { it.by } - require(notary.owningKey.isFulfilledBy(signingKeys)) { "Insufficient signatures to fulfill the notary signing requirement for $notary" } - signatures.forEach { it.verify(txId) } -} - -/** Checks if the provided states were used as inputs in the specified transaction. */ -fun isConsumedByTheSameTx(txIdHash: SecureHash, consumedStates: Map): Boolean { - val conflicts = consumedStates.filter { (_, cause) -> - cause.hashOfTransactionId != txIdHash - } - return conflicts.isEmpty() -} - -/** Returns [NotaryError.TimeWindowInvalid] if [currentTime] is outside the [timeWindow], and *null* otherwise. */ -fun validateTimeWindow(currentTime: Instant, timeWindow: TimeWindow?): NotaryError.TimeWindowInvalid? { - return if (timeWindow != null && currentTime !in timeWindow) { - NotaryError.TimeWindowInvalid(currentTime, timeWindow) - } else null -} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/notary/NotaryService.kt b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryService.kt new file mode 100644 index 0000000000..68041a760a --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryService.kt @@ -0,0 +1,23 @@ +package net.corda.core.internal.notary + +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.NotaryFlow +import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub +import net.corda.core.serialization.SingletonSerializeAsToken +import java.security.PublicKey + +abstract class NotaryService : SingletonSerializeAsToken() { + abstract val services: ServiceHub + abstract val notaryIdentityKey: PublicKey + + abstract fun start() + abstract fun stop() + + /** + * Produces a notary service flow which has the corresponding sends and receives as [NotaryFlow.Client]. + * @param otherPartySession client [Party] making the request + */ + abstract fun createServiceFlow(otherPartySession: FlowSession): FlowLogic +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/notary/NotaryServiceFlow.kt b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryServiceFlow.kt new file mode 100644 index 0000000000..ad1ad7293e --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryServiceFlow.kt @@ -0,0 +1,89 @@ +package net.corda.core.internal.notary + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TimeWindow +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.* +import net.corda.core.identity.Party +import net.corda.core.utilities.unwrap + +/** + * A flow run by a notary service that handles notarisation requests. + * + * It checks that the time-window command is valid (if present) and commits the input state, or returns a conflict + * if any of the input states have been previously committed. + * + * Additional transaction validation logic can be added when implementing [validateRequest]. + */ +// See AbstractStateReplacementFlow.Acceptor for why it's Void? +abstract class NotaryServiceFlow(val otherSideSession: FlowSession, val service: TrustedAuthorityNotaryService) : FlowLogic() { + companion object { + // TODO: Determine an appropriate limit and also enforce in the network parameters and the transaction builder. + private const val maxAllowedInputs = 10_000 + } + + @Suspendable + override fun call(): Void? { + check(serviceHub.myInfo.legalIdentities.any { serviceHub.networkMapCache.isNotary(it) }) { + "We are not a notary on the network" + } + val requestPayload = otherSideSession.receive().unwrap { it } + var txId: SecureHash? = null + try { + val parts = validateRequest(requestPayload) + txId = parts.id + checkNotary(parts.notary) + service.commitInputStates(parts.inputs, txId, otherSideSession.counterparty, requestPayload.requestSignature, parts.timestamp) + signTransactionAndSendResponse(txId) + } catch (e: NotaryInternalException) { + throw NotaryException(e.error, txId) + } + return null + } + + /** Checks whether the number of input states is too large. */ + protected fun checkInputs(inputs: List) { + if (inputs.size > maxAllowedInputs) { + val error = NotaryError.TransactionInvalid( + IllegalArgumentException("A transaction cannot have more than $maxAllowedInputs inputs, received: ${inputs.size}") + ) + throw NotaryInternalException(error) + } + } + + /** + * Implement custom logic to perform transaction verification based on validity and privacy requirements. + */ + @Suspendable + protected abstract fun validateRequest(requestPayload: NotarisationPayload): TransactionParts + + /** Verifies that the correct notarisation request was signed by the counterparty. */ + protected fun validateRequestSignature(request: NotarisationRequest, signature: NotarisationRequestSignature) { + val requestingParty = otherSideSession.counterparty + request.verifySignature(signature, requestingParty) + } + + /** Check if transaction is intended to be signed by this notary. */ + @Suspendable + protected fun checkNotary(notary: Party?) { + if (notary?.owningKey != service.notaryIdentityKey) { + throw NotaryInternalException(NotaryError.WrongNotary) + } + } + + @Suspendable + private fun signTransactionAndSendResponse(txId: SecureHash) { + val signature = service.sign(txId) + otherSideSession.send(NotarisationResponse(listOf(signature))) + } + + /** + * The minimum amount of information needed to notarise a transaction. Note that this does not include + * any sensitive transaction details. + */ + protected data class TransactionParts(val id: SecureHash, val inputs: List, val timestamp: TimeWindow?, val notary: Party?) +} + +/** Exception internal to the notary service. Does not get exposed to CorDapps and flows calling [NotaryFlow.Client]. */ +class NotaryInternalException(val error: NotaryError) : FlowException("Unable to notarise: $error") \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/notary/NotaryUtils.kt b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryUtils.kt new file mode 100644 index 0000000000..008e2efe60 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryUtils.kt @@ -0,0 +1,77 @@ +package net.corda.core.internal.notary + +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TimeWindow +import net.corda.core.crypto.DigitalSignature +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.isFulfilledBy +import net.corda.core.flows.* +import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub +import net.corda.core.serialization.serialize +import java.security.InvalidKeyException +import java.security.SignatureException +import java.time.Instant + +/** Verifies the signature against this notarisation request. Checks that the signature is issued by the right party. */ +fun NotarisationRequest.verifySignature(requestSignature: NotarisationRequestSignature, intendedSigner: Party) { + val signature = requestSignature.digitalSignature + if (intendedSigner.owningKey != signature.by) { + val errorMessage = "Expected a signature by ${intendedSigner.owningKey}, but received by ${signature.by}}" + throw NotaryInternalException(NotaryError.RequestSignatureInvalid(IllegalArgumentException(errorMessage))) + } + // TODO: if requestSignature was generated over an old version of NotarisationRequest, we need to be able to + // reserialize it in that version to get the exact same bytes. Modify the serialization logic once that's + // available. + val expectedSignedBytes = this.serialize().bytes + verifyCorrectBytesSigned(signature, expectedSignedBytes) +} + +private fun verifyCorrectBytesSigned(signature: DigitalSignature.WithKey, bytes: ByteArray) { + try { + signature.verify(bytes) + } catch (e: Exception) { + when (e) { + is InvalidKeyException, is SignatureException -> { + val error = NotaryError.RequestSignatureInvalid(e) + throw NotaryInternalException(error) + } + else -> throw e + } + } +} + +/** + * Checks that there are sufficient signatures to satisfy the notary signing requirement and validates the signatures + * against the given transaction id. + */ +fun NotarisationResponse.validateSignatures(txId: SecureHash, notary: Party) { + val signingKeys = signatures.map { it.by } + require(notary.owningKey.isFulfilledBy(signingKeys)) { "Insufficient signatures to fulfill the notary signing requirement for $notary" } + signatures.forEach { it.verify(txId) } +} + +/** Creates a signature over the notarisation request using the legal identity key. */ +fun NotarisationRequest.generateSignature(serviceHub: ServiceHub): NotarisationRequestSignature { + val serializedRequest = this.serialize().bytes + val signature = with(serviceHub) { + val myLegalIdentity = myInfo.legalIdentitiesAndCerts.first().owningKey + keyManagementService.sign(serializedRequest, myLegalIdentity) + } + return NotarisationRequestSignature(signature, serviceHub.myInfo.platformVersion) +} + +/** Checks if the provided states were used as inputs in the specified transaction. */ +fun isConsumedByTheSameTx(txIdHash: SecureHash, consumedStates: Map): Boolean { + val conflicts = consumedStates.filter { (_, cause) -> + cause.hashOfTransactionId != txIdHash + } + return conflicts.isEmpty() +} + +/** Returns [NotaryError.TimeWindowInvalid] if [currentTime] is outside the [timeWindow], and *null* otherwise. */ +fun validateTimeWindow(currentTime: Instant, timeWindow: TimeWindow?): NotaryError.TimeWindowInvalid? { + return if (timeWindow != null && currentTime !in timeWindow) { + NotaryError.TimeWindowInvalid(currentTime, timeWindow) + } else null +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt b/core/src/main/kotlin/net/corda/core/internal/notary/TrustedAuthorityNotaryService.kt similarity index 51% rename from core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt rename to core/src/main/kotlin/net/corda/core/internal/notary/TrustedAuthorityNotaryService.kt index a189ea0687..3d6bda0a7b 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt +++ b/core/src/main/kotlin/net/corda/core/internal/notary/TrustedAuthorityNotaryService.kt @@ -1,66 +1,13 @@ -package net.corda.core.node.services +package net.corda.core.internal.notary -import com.google.common.primitives.Booleans import net.corda.core.contracts.StateRef import net.corda.core.contracts.TimeWindow import net.corda.core.crypto.* -import net.corda.core.flows.* +import net.corda.core.flows.NotarisationRequestSignature +import net.corda.core.flows.NotaryError import net.corda.core.identity.Party -import net.corda.core.node.ServiceHub -import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.contextLogger import org.slf4j.Logger -import java.security.PublicKey -import java.time.Clock - -abstract class NotaryService : SingletonSerializeAsToken() { - companion object { - @Deprecated("No longer used") - const val ID_PREFIX = "corda.notary." - - @Deprecated("No longer used") - fun constructId(validating: Boolean, raft: Boolean = false, bft: Boolean = false, custom: Boolean = false): String { - require(Booleans.countTrue(raft, bft, custom) <= 1) { "At most one of raft, bft or custom may be true" } - return StringBuffer(ID_PREFIX).apply { - append(if (validating) "validating" else "simple") - if (raft) append(".raft") - if (bft) append(".bft") - if (custom) append(".custom") - }.toString() - } - - /** - * Checks if the current instant provided by the clock falls within the specified time window. Should only be - * used by a notary service flow. - * - * @throws NotaryInternalException if current time is outside the specified time window. The exception contains - * the [NotaryError.TimeWindowInvalid] error. - */ - @JvmStatic - @Throws(NotaryInternalException::class) - fun validateTimeWindow(clock: Clock, timeWindow: TimeWindow?) { - if (timeWindow == null) return - val currentTime = clock.instant() - if (currentTime !in timeWindow) { - throw NotaryInternalException( - NotaryError.TimeWindowInvalid(currentTime, timeWindow) - ) - } - } - } - - abstract val services: ServiceHub - abstract val notaryIdentityKey: PublicKey - - abstract fun start() - abstract fun stop() - - /** - * Produces a notary service flow which has the corresponding sends and receives as [NotaryFlow.Client]. - * @param otherPartySession client [Party] making the request - */ - abstract fun createServiceFlow(otherPartySession: FlowSession): FlowLogic -} /** * A base notary service implementation that provides functionality for cases where a signature by a single member @@ -74,8 +21,6 @@ abstract class TrustedAuthorityNotaryService : NotaryService() { protected open val log: Logger get() = staticLog protected abstract val uniquenessProvider: UniquenessProvider - fun validateTimeWindow(t: TimeWindow?) = NotaryService.validateTimeWindow(services.clock, t) - /** * A NotaryException is thrown if any of the states have been consumed by a different transaction. Note that * this method does not throw an exception when input states are present multiple times within the transaction. @@ -113,9 +58,4 @@ abstract class TrustedAuthorityNotaryService : NotaryService() { } // TODO: Sign multiple transactions at once by building their Merkle tree and then signing over its root. - - @Deprecated("This property is no longer used") - @Suppress("DEPRECATION") - protected open val timeWindowChecker: TimeWindowChecker - get() = throw UnsupportedOperationException("No default implementation, need to override") } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/notary/UniquenessProvider.kt b/core/src/main/kotlin/net/corda/core/internal/notary/UniquenessProvider.kt new file mode 100644 index 0000000000..3e31c95978 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/notary/UniquenessProvider.kt @@ -0,0 +1,24 @@ +package net.corda.core.internal.notary + +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TimeWindow +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.NotarisationRequestSignature +import net.corda.core.identity.Party + +/** + * A service that records input states of the given transaction and provides conflict information + * if any of the inputs have already been used in another transaction. + * + * A uniqueness provider is expected to be used from within the context of a flow. + */ +interface UniquenessProvider { + /** Commits all input states of the given transaction. */ + fun commit( + states: List, + txId: SecureHash, + callerIdentity: Party, + requestSignature: NotarisationRequestSignature, + timeWindow: TimeWindow? = null + ) +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/node/services/UniquenessProvider.kt b/core/src/main/kotlin/net/corda/core/node/services/UniquenessProvider.kt deleted file mode 100644 index d89d141b03..0000000000 --- a/core/src/main/kotlin/net/corda/core/node/services/UniquenessProvider.kt +++ /dev/null @@ -1,44 +0,0 @@ -package net.corda.core.node.services - -import net.corda.core.CordaException -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TimeWindow -import net.corda.core.crypto.SecureHash -import net.corda.core.flows.NotarisationRequestSignature -import net.corda.core.identity.Party -import net.corda.core.serialization.CordaSerializable - -/** - * A service that records input states of the given transaction and provides conflict information - * if any of the inputs have already been used in another transaction. - * - * A uniqueness provider is expected to be used from within the context of a flow. - */ -interface UniquenessProvider { - /** Commits all input states of the given transaction. */ - fun commit( - states: List, - txId: SecureHash, - callerIdentity: Party, - requestSignature: NotarisationRequestSignature, - timeWindow: TimeWindow? = null - ) - - /** Specifies the consuming transaction for every conflicting state. */ - @CordaSerializable - @Deprecated("No longer used due to potential privacy leak") - @Suppress("DEPRECATION") - data class Conflict(val stateHistory: Map) - - /** - * Specifies the transaction id, the position of the consumed state in the inputs, and - * the caller identity requesting the commit. - */ - @CordaSerializable - @Deprecated("No longer used") - data class ConsumingTx(val id: SecureHash, val inputIndex: Int, val requestingParty: Party) -} - -@Deprecated("No longer used due to potential privacy leak") -@Suppress("DEPRECATION") -class UniquenessException(val error: UniquenessProvider.Conflict) : CordaException(UniquenessException::class.java.name) \ No newline at end of file diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index e602cf0f1a..b1aa48af5d 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -6,13 +6,17 @@ release, see :doc:`upgrade-notes`. Unreleased ========== - + * Refactor RPC Client Kryo observable serialiser into it's own sub module * Fix CORDA-1403 where a property of a class that implemented a generic interface could not be deserialised in a factory without a serialiser as the subtype check for the class instance failed. Fix is to compare the raw type. +* Due to ongoing work the experimental interfaces for defining custom notary services have been moved to the internal package. + CorDapps implementing custom notary services will need to be updated, see ``samples/notary-demo`` for an example. + Further changes may be required in the future. + * Fixed incorrect exception handling in ``NodeVaultService._query()``. * Avoided a memory leak deriving from incorrect MappedSchema caching strategy. 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 b20b0022f7..45126d306e 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -18,6 +18,7 @@ import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.concurrent.map import net.corda.core.internal.concurrent.openFuture +import net.corda.core.internal.notary.NotaryService import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.* import net.corda.core.node.* diff --git a/node/src/main/kotlin/net/corda/node/internal/StartedNode.kt b/node/src/main/kotlin/net/corda/node/internal/StartedNode.kt index 90df5b2b5c..6025daa50b 100644 --- a/node/src/main/kotlin/net/corda/node/internal/StartedNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/StartedNode.kt @@ -3,9 +3,9 @@ package net.corda.node.internal import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatedBy import net.corda.core.internal.VisibleForTesting +import net.corda.core.internal.notary.NotaryService import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.NodeInfo -import net.corda.core.node.services.NotaryService import net.corda.node.services.api.CheckpointStorage import net.corda.node.services.api.StartedNodeServices import net.corda.node.services.messaging.MessagingService diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt index dd116645b4..bd6b16544f 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt @@ -7,7 +7,9 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SignedData import net.corda.core.flows.* import net.corda.core.identity.Party -import net.corda.core.node.services.NotaryService +import net.corda.core.internal.notary.NotaryInternalException +import net.corda.core.internal.notary.NotaryService +import net.corda.core.internal.notary.verifySignature import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt index 3246fe2553..0fa84e0577 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt @@ -15,13 +15,17 @@ import bftsmart.tom.util.Extractor import net.corda.core.contracts.StateRef import net.corda.core.contracts.TimeWindow import net.corda.core.crypto.* -import net.corda.core.flows.* +import net.corda.core.flows.NotarisationPayload +import net.corda.core.flows.NotarisationRequestSignature +import net.corda.core.flows.NotaryError +import net.corda.core.flows.StateConsumptionDetails import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.declaredField -import net.corda.core.internal.isConsumedByTheSameTx +import net.corda.core.internal.notary.NotaryInternalException +import net.corda.core.internal.notary.isConsumedByTheSameTx +import net.corda.core.internal.notary.validateTimeWindow import net.corda.core.internal.toTypedArray -import net.corda.core.internal.validateTimeWindow import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt index 18e6c755e4..2105f9e278 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt @@ -2,15 +2,17 @@ package net.corda.node.services.transactions import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.ComponentGroupEnum -import net.corda.core.flows.* -import net.corda.core.internal.validateRequestSignature -import net.corda.core.node.services.TrustedAuthorityNotaryService +import net.corda.core.flows.FlowSession +import net.corda.core.flows.NotarisationPayload +import net.corda.core.flows.NotarisationRequest +import net.corda.core.internal.notary.NotaryServiceFlow +import net.corda.core.internal.notary.TrustedAuthorityNotaryService import net.corda.core.transactions.ContractUpgradeFilteredTransaction import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.FilteredTransaction import net.corda.core.transactions.NotaryChangeWireTransaction -class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthorityNotaryService) : NotaryFlow.Service(otherSideSession, service) { +class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthorityNotaryService) : NotaryServiceFlow(otherSideSession, service) { /** * The received transaction is not checked for contract-validity, as that would require fully * resolving it into a [TransactionForVerification], for which the caller would have to reveal the whole transaction diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt index 9ee6a87fdc..f54b442b9e 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt @@ -6,13 +6,13 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 import net.corda.core.flows.NotarisationRequestSignature import net.corda.core.flows.NotaryError -import net.corda.core.flows.NotaryInternalException import net.corda.core.flows.StateConsumptionDetails import net.corda.core.identity.Party import net.corda.core.internal.ThreadBox -import net.corda.core.internal.isConsumedByTheSameTx -import net.corda.core.internal.validateTimeWindow -import net.corda.core.node.services.UniquenessProvider +import net.corda.core.internal.notary.NotaryInternalException +import net.corda.core.internal.notary.UniquenessProvider +import net.corda.core.internal.notary.isConsumedByTheSameTx +import net.corda.core.internal.notary.validateTimeWindow import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt index 63fef9fdfe..158d86b3c7 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt @@ -1,9 +1,9 @@ package net.corda.node.services.transactions import net.corda.core.flows.FlowSession -import net.corda.core.flows.NotaryFlow +import net.corda.core.internal.notary.NotaryServiceFlow +import net.corda.core.internal.notary.TrustedAuthorityNotaryService import net.corda.core.node.ServiceHub -import net.corda.core.node.services.TrustedAuthorityNotaryService import java.security.PublicKey /** A non-validating notary service operated by a group of mutually trusting parties, uses the Raft algorithm to achieve consensus. */ @@ -12,7 +12,7 @@ class RaftNonValidatingNotaryService( override val notaryIdentityKey: PublicKey, override val uniquenessProvider: RaftUniquenessProvider ) : TrustedAuthorityNotaryService() { - override fun createServiceFlow(otherPartySession: FlowSession): NotaryFlow.Service { + override fun createServiceFlow(otherPartySession: FlowSession): NotaryServiceFlow { return NonValidatingNotaryFlow(otherPartySession, this) } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLog.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLog.kt index 4fa43efb02..ebb1bc0586 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLog.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLog.kt @@ -18,8 +18,8 @@ import net.corda.core.crypto.sha256 import net.corda.core.flows.NotaryError import net.corda.core.flows.StateConsumptionDetails import net.corda.core.internal.VisibleForTesting -import net.corda.core.internal.isConsumedByTheSameTx -import net.corda.core.internal.validateTimeWindow +import net.corda.core.internal.notary.isConsumedByTheSameTx +import net.corda.core.internal.notary.validateTimeWindow import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.SerializationFactory import net.corda.core.serialization.deserialize diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt index d9df0b4a49..03fa2f3b73 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt @@ -17,9 +17,9 @@ import net.corda.core.contracts.StateRef import net.corda.core.contracts.TimeWindow import net.corda.core.crypto.SecureHash import net.corda.core.flows.NotarisationRequestSignature -import net.corda.core.flows.NotaryInternalException import net.corda.core.identity.Party -import net.corda.core.node.services.UniquenessProvider +import net.corda.core.internal.notary.NotaryInternalException +import net.corda.core.internal.notary.UniquenessProvider import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.serialize diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt index 0ddbb14ee0..f0cb8c1f8e 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt @@ -1,9 +1,9 @@ package net.corda.node.services.transactions import net.corda.core.flows.FlowSession -import net.corda.core.flows.NotaryFlow +import net.corda.core.internal.notary.NotaryServiceFlow +import net.corda.core.internal.notary.TrustedAuthorityNotaryService import net.corda.core.node.ServiceHub -import net.corda.core.node.services.TrustedAuthorityNotaryService import java.security.PublicKey /** A validating notary service operated by a group of mutually trusting parties, uses the Raft algorithm to achieve consensus. */ @@ -12,7 +12,7 @@ class RaftValidatingNotaryService( override val notaryIdentityKey: PublicKey, override val uniquenessProvider: RaftUniquenessProvider ) : TrustedAuthorityNotaryService() { - override fun createServiceFlow(otherPartySession: FlowSession): NotaryFlow.Service { + override fun createServiceFlow(otherPartySession: FlowSession): NotaryServiceFlow { return ValidatingNotaryFlow(otherPartySession, this) } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt index 65f64d5a8c..093c0d905b 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt @@ -1,8 +1,8 @@ package net.corda.node.services.transactions import net.corda.core.flows.FlowSession -import net.corda.core.flows.NotaryFlow -import net.corda.core.node.services.TrustedAuthorityNotaryService +import net.corda.core.internal.notary.NotaryServiceFlow +import net.corda.core.internal.notary.TrustedAuthorityNotaryService import net.corda.node.services.api.ServiceHubInternal import java.security.PublicKey @@ -10,7 +10,7 @@ import java.security.PublicKey class SimpleNotaryService(override val services: ServiceHubInternal, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { override val uniquenessProvider = PersistentUniquenessProvider(services.clock) - override fun createServiceFlow(otherPartySession: FlowSession): NotaryFlow.Service = NonValidatingNotaryFlow(otherPartySession, this) + override fun createServiceFlow(otherPartySession: FlowSession): NotaryServiceFlow = NonValidatingNotaryFlow(otherPartySession, this) override fun start() {} override fun stop() {} diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt index 0b966533ad..216d2b03b5 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt @@ -3,10 +3,14 @@ package net.corda.node.services.transactions import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.TimeWindow import net.corda.core.contracts.TransactionVerificationException -import net.corda.core.flows.* +import net.corda.core.flows.FlowSession +import net.corda.core.flows.NotarisationPayload +import net.corda.core.flows.NotarisationRequest +import net.corda.core.flows.NotaryError import net.corda.core.internal.ResolveTransactionsFlow -import net.corda.core.internal.validateRequestSignature -import net.corda.core.node.services.TrustedAuthorityNotaryService +import net.corda.core.internal.notary.NotaryInternalException +import net.corda.core.internal.notary.NotaryServiceFlow +import net.corda.core.internal.notary.TrustedAuthorityNotaryService import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionWithSignatures import net.corda.core.transactions.WireTransaction @@ -18,7 +22,7 @@ import java.security.SignatureException * has its input states "blocked" by a transaction from another party, and needs to establish whether that transaction was * indeed valid. */ -class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthorityNotaryService) : NotaryFlow.Service(otherSideSession, service) { +class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthorityNotaryService) : NotaryServiceFlow(otherSideSession, service) { /** * Fully resolves the received transaction and its dependencies, runs contract verification logic and checks that * the transaction in question has all required signatures apart from the notary's. diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt index 24f33c539f..08b4fcb211 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt @@ -1,8 +1,8 @@ package net.corda.node.services.transactions import net.corda.core.flows.FlowSession -import net.corda.core.flows.NotaryFlow -import net.corda.core.node.services.TrustedAuthorityNotaryService +import net.corda.core.internal.notary.NotaryServiceFlow +import net.corda.core.internal.notary.TrustedAuthorityNotaryService import net.corda.node.services.api.ServiceHubInternal import java.security.PublicKey @@ -10,7 +10,7 @@ import java.security.PublicKey class ValidatingNotaryService(override val services: ServiceHubInternal, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { override val uniquenessProvider = PersistentUniquenessProvider(services.clock) - override fun createServiceFlow(otherPartySession: FlowSession): NotaryFlow.Service = ValidatingNotaryFlow(otherPartySession, this) + override fun createServiceFlow(otherPartySession: FlowSession): NotaryServiceFlow = ValidatingNotaryFlow(otherPartySession, this) override fun start() {} override fun stop() {} 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 c444263b00..b882004502 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 @@ -6,8 +6,8 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 import net.corda.core.flows.NotarisationRequestSignature import net.corda.core.flows.NotaryError -import net.corda.core.flows.NotaryInternalException import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.notary.NotaryInternalException import net.corda.node.internal.configureDatabase import net.corda.node.services.schema.NodeSchemaService import net.corda.nodeapi.internal.persistence.CordaPersistence diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt index 03b90230f8..c3aee36b7b 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt @@ -7,7 +7,7 @@ import net.corda.core.contracts.StateRef import net.corda.core.crypto.* import net.corda.core.flows.* import net.corda.core.identity.Party -import net.corda.core.internal.generateSignature +import net.corda.core.internal.notary.generateSignature import net.corda.core.messaging.MessageRecipients import net.corda.core.node.ServiceHub import net.corda.core.serialization.deserialize diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt index fc58760a5c..d81d2d24aa 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt @@ -5,10 +5,11 @@ import net.corda.core.contracts.TimeWindow import net.corda.core.contracts.TransactionVerificationException import net.corda.core.flows.* import net.corda.core.internal.ResolveTransactionsFlow -import net.corda.core.internal.validateRequestSignature +import net.corda.core.internal.notary.NotaryInternalException +import net.corda.core.internal.notary.NotaryServiceFlow +import net.corda.core.internal.notary.TrustedAuthorityNotaryService import net.corda.core.node.AppServiceHub import net.corda.core.node.services.CordaService -import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionWithSignatures import net.corda.core.transactions.WireTransaction @@ -36,7 +37,7 @@ class MyCustomValidatingNotaryService(override val services: AppServiceHub, over @Suppress("UNUSED_PARAMETER") // START 2 -class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidatingNotaryService) : NotaryFlow.Service(otherSide, service) { +class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidatingNotaryService) : NotaryServiceFlow(otherSide, service) { /** * The received transaction is checked for contract-validity, for which the caller also has to to reveal the whole * transaction dependency chain. From 884928c956a849a35ad14f949a38ba17e6208fd2 Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Wed, 2 May 2018 15:14:45 +0100 Subject: [PATCH 20/25] CORDA-1416: Upgrade version of Proton-J library (#3050) * CORDA-1416: Upgrade version of Proton-J library * CORDA-1416: Compilation fixes following Proton-J upgrade Reflects: https://issues.apache.org/jira/browse/PROTON-1712 and https://issues.apache.org/jira/browse/PROTON-1672 * CORDA-1416: Add an integration test to prove that data saved by from previous version can be read. * CORDA-1416: Add additional check validate serialized form. --- finance/build.gradle | 3 ++ .../corda/finance/compat/CompatibilityTest.kt | 38 ++++++++++++++++++ .../compatibilityData/v3/node_transaction.dat | Bin 0 -> 11687 bytes node-api/build.gradle | 2 +- .../internal/bridging/AMQPBridgeManager.kt | 2 +- .../protonwrapper/engine/NettyWritable.kt | 5 +++ .../messages/ApplicationMessage.kt | 2 +- .../messages/impl/ReceivedMessageImpl.kt | 2 +- .../messages/impl/SendableMessageImpl.kt | 2 +- .../protonwrapper/netty/AMQPClient.kt | 2 +- .../protonwrapper/netty/AMQPServer.kt | 2 +- .../net/corda/node/amqp/ProtonWrapperTests.kt | 2 +- 12 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 finance/src/integration-test/kotlin/net/corda/finance/compat/CompatibilityTest.kt create mode 100644 finance/src/integration-test/resources/compatibilityData/v3/node_transaction.dat diff --git a/finance/build.gradle b/finance/build.gradle index 7180a5f9eb..8f54fc8c7d 100644 --- a/finance/build.gradle +++ b/finance/build.gradle @@ -17,6 +17,9 @@ sourceSets { runtimeClasspath += main.output + test.output srcDir file('src/integration-test/kotlin') } + resources { + srcDir file('src/integration-test/resources') + } } } diff --git a/finance/src/integration-test/kotlin/net/corda/finance/compat/CompatibilityTest.kt b/finance/src/integration-test/kotlin/net/corda/finance/compat/CompatibilityTest.kt new file mode 100644 index 0000000000..bf1dc2cb88 --- /dev/null +++ b/finance/src/integration-test/kotlin/net/corda/finance/compat/CompatibilityTest.kt @@ -0,0 +1,38 @@ +package net.corda.finance.compat + +import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize +import net.corda.core.transactions.SignedTransaction +import net.corda.finance.contracts.asset.Cash +import net.corda.testing.core.SerializationEnvironmentRule +import org.junit.Rule +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +// TODO: If this type of testing gets momentum, we can create a mini-framework that rides through list of files +// and performs necessary validation on all of them. +class CompatibilityTest { + + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule() + + @Test + fun issueCashTansactionReadTest() { + val inputStream = javaClass.classLoader.getResourceAsStream("compatibilityData/v3/node_transaction.dat") + assertNotNull(inputStream) + val inByteArray: ByteArray = inputStream.readBytes() + val transaction = inByteArray.deserialize(context = SerializationDefaults.STORAGE_CONTEXT) + assertNotNull(transaction) + val commands = transaction.tx.commands + assertEquals(1, commands.size) + assertTrue(commands.first().value is Cash.Commands.Issue) + + // Serialize back and check that representation is byte-to-byte identical to what it was originally. + val serializedForm = transaction.serialize(context = SerializationDefaults.STORAGE_CONTEXT) + assertTrue(inByteArray.contentEquals(serializedForm.bytes)) + } +} \ No newline at end of file diff --git a/finance/src/integration-test/resources/compatibilityData/v3/node_transaction.dat b/finance/src/integration-test/resources/compatibilityData/v3/node_transaction.dat new file mode 100644 index 0000000000000000000000000000000000000000..21392230dd6631b92933e0747a74bcd08a29f36b GIT binary patch literal 11687 zcmeHNd5jy?8TU1I2ndpfilYPtvRv8VbM5tp>@q&rUhl5GK6bqpYCE>a_V}3b&Wz7i zO@Rnd8>ywJp`xP7K&47lP-y8N6{_^8psG+&ODoz+NVIBzNK`#UXpt&f; zH-}0k{zx?I`R2Xv{C(g1zL#UzJfYQSG+%r<3;)+>w7=A7?))lz&}*K&nI?I24pAKG zCYkUe<@OaPyE7~EPP?BACTEJXvygfs0jYH|YL2#~^F?=UJ@0p=h)|>C;R_AV>@0X! zE2HZ)GXYPaVp(FUj-tnAvpH+U#tNkSi9z$^O@rObM3XRcWUj$de8(JXWUEvzLUv9a zwjQ!#M*SpN*WaXB{#*K5*8?l3e|C22y4xTAZtCUJrPqJ=rt>fNTzT?av+?MjQ#gCq zo@b8z#(1mWX@2tl)BDR$-~H(C|M2#i-TypUn|osaU)O$n@)p+k`WyPgU%KiE4SDV9 z1Nr-2O@45n^YmxukG+42$8Z_*;ck$3=i-qL&n;{?nk$o?GB*!Z0x(_Dkc0wA)aP?1 zSE#r*8t@b*BT<{Jn=GedP*AA8gI6Q1KMNl&7gen^#?9{!86zEKoN1P}+G@GgGE)gx zLpHJk)eBeK|JIN(5{s?0b*aSHqs&@#W6|Qxmo3fS-utVN8k2DvqRB+eSDmr%0+vIzWP%`YcH(V$!v9f+Q+sngl&N;wHJJ@mdMGzKS64_xfj=+&h?bP+>W}Z zqSaQSmFhMVJ-+w-7*Rtl?0bHs7WO?3-^*Qc#oA5HFIyt6R;$%%gsaO1N1W*GtxrK} zxm?9cIJg=LvDsvaC`E&%^EkbpNu*K^-sf6x@@a=7Qe|cqkW~+APaXK+ z*OU8OdfoqVjm5wNl4`L&+LwXZ@sG|GjZBq zSaq}}Cwn^7KGM_CjeRDLh=Efh_>$R675!w6su5MeU^QE9_d;5z@dr-Hc(I1VDQhS0 z%Coj&!j+>tlLQkvx4IM3%XvvJgoDW?Z!T6UWZDF`>h^?!Ye=W>$mv2ektpRRim3)y zj6^8MQo!qIBb`1Xr=yGcR+e{F;z?`ONz*o0qu`!~yMybDxS*T&3dTi-&NH-N@*z+< z;KyM^UTm0<>7!muS|d3JZ^<_4F2p7aot^R_&JEgATIe`8C{*3xsjAu6U|EvRbp-vq zH`WK%U2z3|sA}aK+tkX93awnx)5d#Rd3 zilGxc!IOBP|GggEs*^&eTA1T#pBvCV{Z9=bXcBhAfa74qQ8X`T4*{!IxKou456=_1 zQjMf}U;iT<_jn~MUTYF|ZHBO5gbP!2F`%HCF$sn|!4qh|JG4>|P>2n85wV~r^Uy|@ zL$7JnvQRfUQqV&I;6`34Gk2N`1VDN6sMzPGURN!p;@fSDMm- zOT~iA!r49LN`^?fE!orzy_|qBAu&1z9>TV*w;kwMm8XoIvU_Wc(;g@^a=~=Q?X~+* zLYDFL8n%U+CDclx)uI~*pVRfZG5`m39h#_-!Yyj{6oWeLv|5)TncS`hU@-}oF)bSU zBrJ!J`(3XUu2HF$WKJtasb7rJ?d?29R5jSf!D*vJ)a%9AtkoDANg|0QnTuaz5==R! z0pp}2AsGtEDw89s(B0K&ZZHW~Fl>>aDULuWqegeYBT z35-mk0L!W#TnbvN98~wg@ZzZMIW$$g#=w1;aNAa*Pb5MfyVEsB8nu!U2RBQ#zJ)Ym zM51~R6Alk}8dpV5^wJ3Ufk8Mj_(G9#f(^PzWviqI6`w~%Xpj{tb3@28aM@^!Cg7*y zHrqw#q2;(-iL4Qvd8I+Jo&IutD=TqRts2I`b5h*QL(3C2gcCwiRd$a^a8&hunBy8` zew0$?+Lrh@F~Zr1HFTbr1P=zD!M4c`4r01(l5W>KRXMlUR()%QN~&NFT5K-&#(X+5 z%^^e@6s99NAMM>EIUh}W!JK>D_FN+9N?NCqzF47JOmp^Z1s+kPoTE~CR)m&I5-gIf zl;Ny)T*blDQlvM-G@PfndX?xTnF>iK5N2OJ>h!7%OjMt5GzhWw6%17dR_6+sHvlgT zULGwgIL-qLdFVjhJcixm#3$|0hUht9{z0*OwcLnY%u zSYXvzCf~^M!Y79Ffl}Ht*iJSZBcxih00+mVLL#88BLp3a{Otg(0ZhY+nBA0Jm{8~m zr9O(MNX{Is6B`ZEi>eEy$d3&J)abp(NL!9J^F*eyIyn`a%rpw5hpvQB8wm$Tr8U9% z*YDsMwms>_Fsatv*jeFn*^63DQr<)x(?rziAP&hJ9Alb@+FwCx`PyTeh}v%>>Sy7< zZ^9~QxUVg?CFD&+Cu4&J5|{5zcAbp9lvyOL8wmFYu#b8j+Bvi^dgwL5Hk;)b_&O#y z2R=5oTJtfr8bRl|39;t@v7MMkU*3sn^heK`M(-Fl4Gcm(>iQ`-RqHVSfAmsy zs5nFTqML#bgLH_2k6TcAVzc=r!8K)>`Ed&h`i({-#;|%eEU z;GO{-8$r}4>>X6ZixkHny?YK1z+(k`?8CUy_tdzTxT|OvqfB=;4jwkWL@s6X6l;rV z_mZvab|9J2obmqH;}5qVxOe%iLATp?`@f0@^}D|J(sx`(zx)LC;l`Vp6MbG#abox> zQZ;%&GgMRmo!C&?3xC3d$w7$%twEY$pJH?T!kO17mP`(Qkg%Lv8Rb(Xe=$R37GJ){(10@~P4|YJrY}V^ZTR^}6sq)rxkR z)(TS!uUFV*{sA8qagDt#qvhToQ$0ADtu9NLB`Axx{59 z{3{Kvh9EmTy0|J#pCiAmedl$&R9Zd*+(&iaJ$Z) zL%3bb z*;+!W>NosCyR}?tG$U--+ss#4BD=7#D(|dsC!dY=l&vsU?N*;uCoo#Iy@*t1wJ}@c zU`{LCCFYH&to4ZOp1G-KLds2Pa4>f)A-t;+sk+UdaTbQ=FSF zTFNvTZMz8HjMIWb3M>x}1qR@OBV?9g_~E914|RvOoH+LjGPhFil@YUpZ7efJiVbFQ zvE*B~O~*2=)#bP=u+}bk() + val properties = HashMap() for (key in P2PMessagingHeaders.whitelistedHeaders) { if (artemisMessage.containsProperty(key)) { var value = artemisMessage.getObjectProperty(key) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/NettyWritable.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/NettyWritable.kt index 1bb97851ed..230b712fe8 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/NettyWritable.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/NettyWritable.kt @@ -1,6 +1,7 @@ package net.corda.nodeapi.internal.protonwrapper.engine import io.netty.buffer.ByteBuf +import org.apache.qpid.proton.codec.ReadableBuffer import org.apache.qpid.proton.codec.WritableBuffer import java.nio.ByteBuffer @@ -57,6 +58,10 @@ internal class NettyWritable(val nettyBuffer: ByteBuf) : WritableBuffer { nettyBuffer.writeBytes(payload) } + override fun put(payload: ReadableBuffer) { + nettyBuffer.writeBytes(payload.byteBuffer()) + } + override fun limit(): Int { return nettyBuffer.capacity() } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/messages/ApplicationMessage.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/messages/ApplicationMessage.kt index f42116f950..03911bb50f 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/messages/ApplicationMessage.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/messages/ApplicationMessage.kt @@ -10,5 +10,5 @@ interface ApplicationMessage { val topic: String val destinationLegalName: String val destinationLink: NetworkHostAndPort - val applicationProperties: Map + val applicationProperties: Map } \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/messages/impl/ReceivedMessageImpl.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/messages/impl/ReceivedMessageImpl.kt index 15c893e41a..7bf83505c5 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/messages/impl/ReceivedMessageImpl.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/messages/impl/ReceivedMessageImpl.kt @@ -16,7 +16,7 @@ internal class ReceivedMessageImpl(override val payload: ByteArray, override val sourceLink: NetworkHostAndPort, override val destinationLegalName: String, override val destinationLink: NetworkHostAndPort, - override val applicationProperties: Map, + override val applicationProperties: Map, private val channel: Channel, private val delivery: Delivery) : ReceivedMessage { data class MessageCompleter(val status: MessageStatus, val delivery: Delivery) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/messages/impl/SendableMessageImpl.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/messages/impl/SendableMessageImpl.kt index 13065c960e..6adc9b2bbc 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/messages/impl/SendableMessageImpl.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/messages/impl/SendableMessageImpl.kt @@ -15,7 +15,7 @@ internal class SendableMessageImpl(override val payload: ByteArray, override val topic: String, override val destinationLegalName: String, override val destinationLink: NetworkHostAndPort, - override val applicationProperties: Map) : SendableMessage { + override val applicationProperties: Map) : SendableMessage { var buf: ByteBuf? = null @Volatile var status: MessageStatus = MessageStatus.Unsent diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPClient.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPClient.kt index 62838ba626..3c7bb32db2 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPClient.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPClient.kt @@ -170,7 +170,7 @@ class AMQPClient(val targets: List, fun createMessage(payload: ByteArray, topic: String, destinationLegalName: String, - properties: Map): SendableMessage { + properties: Map): SendableMessage { return SendableMessageImpl(payload, topic, destinationLegalName, currentTarget, properties) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt index 007729f512..d8ead67682 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt @@ -155,7 +155,7 @@ class AMQPServer(val hostName: String, topic: String, destinationLegalName: String, destinationLink: NetworkHostAndPort, - properties: Map): SendableMessage { + properties: Map): SendableMessage { val dest = InetSocketAddress(destinationLink.host, destinationLink.port) require(dest in clientChannels.keys) { "Destination not available" 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 b8c857846e..1090aef766 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 @@ -152,7 +152,7 @@ class ProtonWrapperTests { artemis.session.createQueue(sendAddress, RoutingType.ANYCAST, "queue", true) val consumer = artemis.session.createConsumer("queue") val testData = "Test".toByteArray() - val testProperty = mutableMapOf() + val testProperty = mutableMapOf() testProperty["TestProp"] = "1" val message = amqpClient.createMessage(testData, sendAddress, CHARLIE_NAME.toString(), testProperty) amqpClient.write(message) From 0d3c7e77623a6ba52c8733e2fffcc118751c723a Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Wed, 2 May 2018 15:48:41 +0100 Subject: [PATCH 21/25] CORDA-847 - RPC Server lib refactoring (#3056) Just as we did for the RPC CLient, refactor kryo specific elements into their own sub module. Also move kryo specific components out of generic RPC files. Thus, adding AMQP support will be a much smoother operation --- docs/source/changelog.rst | 10 ++- .../internal/crypto/X509UtilitiesTest.kt | 2 +- .../internal/serialization/kryo/KryoTests.kt | 2 +- .../kotlin/net/corda/node/internal/Node.kt | 2 +- .../KryoServerSerializationScheme.kt | 3 +- .../kryo/RpcServerObservableSerializer.kt | 87 +++++++++++++++++++ .../node/services/messaging/RPCServer.kt | 80 +---------------- .../InternalSerializationTestHelpers.kt | 2 +- 8 files changed, 99 insertions(+), 89 deletions(-) rename node/src/main/kotlin/net/corda/node/serialization/{ => kryo}/KryoServerSerializationScheme.kt (91%) create mode 100644 node/src/main/kotlin/net/corda/node/serialization/kryo/RpcServerObservableSerializer.kt diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index b1aa48af5d..3f7a0481bb 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -6,11 +6,13 @@ release, see :doc:`upgrade-notes`. Unreleased ========== - -* Refactor RPC Client Kryo observable serialiser into it's own sub module -* Fix CORDA-1403 where a property of a class that implemented a generic interface could not be deserialised in - a factory without a serialiser as the subtype check for the class instance failed. Fix is to compare the raw +* Refactor RPC Server Kryo observable serializer into it's own sub module + +* Refactor RPC Client Kryo observable serializer into it's own sub module + +* Fix CORDA-1403 where a property of a class that implemented a generic interface could not be deserialized in + a factory without a serializer as the subtype check for the class instance failed. Fix is to compare the raw type. * Due to ongoing work the experimental interfaces for defining custom notary services have been moved to the internal package. diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt index 5a76745aee..5986848c74 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/crypto/X509UtilitiesTest.kt @@ -8,7 +8,7 @@ import net.corda.core.internal.div import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize -import net.corda.node.serialization.KryoServerSerializationScheme +import net.corda.node.serialization.kryo.KryoServerSerializationScheme import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.createDevKeyStores import net.corda.nodeapi.internal.serialization.AllWhitelist diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoTests.kt index d7acb47e7f..7f8a8c302b 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/kryo/KryoTests.kt @@ -14,7 +14,7 @@ import net.corda.core.internal.FetchDataFlow import net.corda.core.serialization.* import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.sequence -import net.corda.node.serialization.KryoServerSerializationScheme +import net.corda.node.serialization.kryo.KryoServerSerializationScheme import net.corda.node.services.persistence.NodeAttachmentService import net.corda.nodeapi.internal.serialization.* import net.corda.testing.core.ALICE_NAME 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 3b4b2719f1..993a26e7a4 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -25,7 +25,7 @@ import net.corda.node.internal.artemis.BrokerAddresses import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.internal.security.RPCSecurityManagerWithAdditionalUser -import net.corda.node.serialization.KryoServerSerializationScheme +import net.corda.node.serialization.kryo.KryoServerSerializationScheme import net.corda.node.services.api.NodePropertiesStore import net.corda.node.services.api.SchemaService import net.corda.node.services.config.* diff --git a/node/src/main/kotlin/net/corda/node/serialization/KryoServerSerializationScheme.kt b/node/src/main/kotlin/net/corda/node/serialization/kryo/KryoServerSerializationScheme.kt similarity index 91% rename from node/src/main/kotlin/net/corda/node/serialization/KryoServerSerializationScheme.kt rename to node/src/main/kotlin/net/corda/node/serialization/kryo/KryoServerSerializationScheme.kt index 2941bc479a..acf4c7f461 100644 --- a/node/src/main/kotlin/net/corda/node/serialization/KryoServerSerializationScheme.kt +++ b/node/src/main/kotlin/net/corda/node/serialization/kryo/KryoServerSerializationScheme.kt @@ -1,9 +1,8 @@ -package net.corda.node.serialization +package net.corda.node.serialization.kryo import com.esotericsoftware.kryo.pool.KryoPool import net.corda.core.serialization.SerializationContext import net.corda.nodeapi.internal.serialization.CordaSerializationMagic -import net.corda.node.services.messaging.RpcServerObservableSerializer import net.corda.nodeapi.internal.serialization.kryo.AbstractKryoSerializationScheme import net.corda.nodeapi.internal.serialization.kryo.DefaultKryoCustomizer import net.corda.nodeapi.internal.serialization.kryo.kryoMagic diff --git a/node/src/main/kotlin/net/corda/node/serialization/kryo/RpcServerObservableSerializer.kt b/node/src/main/kotlin/net/corda/node/serialization/kryo/RpcServerObservableSerializer.kt new file mode 100644 index 0000000000..51013a9bce --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/serialization/kryo/RpcServerObservableSerializer.kt @@ -0,0 +1,87 @@ +package net.corda.node.serialization.kryo + +import com.esotericsoftware.kryo.Kryo +import com.esotericsoftware.kryo.Serializer +import com.esotericsoftware.kryo.io.Input +import com.esotericsoftware.kryo.io.Output +import net.corda.core.context.Trace +import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.SerializationDefaults +import net.corda.node.services.messaging.ObservableSubscription +import net.corda.node.services.messaging.RPCServer +import net.corda.nodeapi.RPCApi +import org.slf4j.LoggerFactory +import rx.Notification +import rx.Observable +import rx.Subscriber + +object RpcServerObservableSerializer : Serializer>() { + private object RpcObservableContextKey + + private val log = LoggerFactory.getLogger(javaClass) + fun createContext(observableContext: RPCServer.ObservableContext): SerializationContext { + return SerializationDefaults.RPC_SERVER_CONTEXT.withProperty(RpcServerObservableSerializer.RpcObservableContextKey, observableContext) + } + + override fun read(kryo: Kryo?, input: Input?, type: Class>?): Observable { + throw UnsupportedOperationException() + } + + override fun write(kryo: Kryo, output: Output, observable: Observable<*>) { + val observableId = Trace.InvocationId.newInstance() + val observableContext = kryo.context[RpcObservableContextKey] as RPCServer.ObservableContext + output.writeInvocationId(observableId) + val observableWithSubscription = ObservableSubscription( + // We capture [observableContext] in the subscriber. Note that all synchronisation/kryo borrowing + // must be done again within the subscriber + subscription = observable.materialize().subscribe( + object : Subscriber>() { + override fun onNext(observation: Notification<*>) { + if (!isUnsubscribed) { + val message = RPCApi.ServerToClient.Observation( + id = observableId, + content = observation, + deduplicationIdentity = observableContext.deduplicationIdentity + ) + observableContext.sendMessage(message) + } + } + + override fun onError(exception: Throwable) { + log.error("onError called in materialize()d RPC Observable", exception) + } + + override fun onCompleted() { + observableContext.clientAddressToObservables.compute(observableContext.clientAddress) { _, observables -> + if (observables != null) { + observables.remove(observableId) + if (observables.isEmpty()) { + null + } else { + observables + } + } else { + null + } + } + } + } + ) + ) + observableContext.clientAddressToObservables.compute(observableContext.clientAddress) { _, observables -> + if (observables == null) { + hashSetOf(observableId) + } else { + observables.add(observableId) + observables + } + } + observableContext.observableMap.put(observableId, observableWithSubscription) + } + + private fun Output.writeInvocationId(id: Trace.InvocationId) { + + writeString(id.value) + writeLong(id.timestamp.toEpochMilli()) + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt index 39c8a8c3b5..5f7d193582 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt @@ -1,10 +1,6 @@ package net.corda.node.services.messaging import co.paralleluniverse.common.util.SameThreadExecutor -import com.esotericsoftware.kryo.Kryo -import com.esotericsoftware.kryo.Serializer -import com.esotericsoftware.kryo.io.Input -import com.esotericsoftware.kryo.io.Output import com.github.benmanes.caffeine.cache.Cache import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.RemovalListener @@ -24,6 +20,7 @@ import net.corda.core.serialization.deserialize import net.corda.core.utilities.* import net.corda.node.internal.security.AuthorizingSubject import net.corda.node.internal.security.RPCSecurityManager +import net.corda.node.serialization.kryo.RpcServerObservableSerializer import net.corda.node.services.logging.pushToLoggingContext import net.corda.nodeapi.RPCApi import net.corda.nodeapi.externalTrace @@ -39,11 +36,7 @@ import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BA import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl import org.apache.activemq.artemis.api.core.management.CoreNotificationType import org.apache.activemq.artemis.api.core.management.ManagementHelper -import org.slf4j.LoggerFactory import org.slf4j.MDC -import rx.Notification -import rx.Observable -import rx.Subscriber import rx.Subscription import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method @@ -486,74 +479,3 @@ class ObservableSubscription( ) typealias ObservableSubscriptionMap = Cache - -object RpcServerObservableSerializer : Serializer>() { - private object RpcObservableContextKey - - private val log = LoggerFactory.getLogger(javaClass) - fun createContext(observableContext: RPCServer.ObservableContext): SerializationContext { - return RPC_SERVER_CONTEXT.withProperty(RpcServerObservableSerializer.RpcObservableContextKey, observableContext) - } - - override fun read(kryo: Kryo?, input: Input?, type: Class>?): Observable { - throw UnsupportedOperationException() - } - - override fun write(kryo: Kryo, output: Output, observable: Observable<*>) { - val observableId = InvocationId.newInstance() - val observableContext = kryo.context[RpcObservableContextKey] as RPCServer.ObservableContext - output.writeInvocationId(observableId) - val observableWithSubscription = ObservableSubscription( - // We capture [observableContext] in the subscriber. Note that all synchronisation/kryo borrowing - // must be done again within the subscriber - subscription = observable.materialize().subscribe( - object : Subscriber>() { - override fun onNext(observation: Notification<*>) { - if (!isUnsubscribed) { - val message = RPCApi.ServerToClient.Observation( - id = observableId, - content = observation, - deduplicationIdentity = observableContext.deduplicationIdentity - ) - observableContext.sendMessage(message) - } - } - - override fun onError(exception: Throwable) { - log.error("onError called in materialize()d RPC Observable", exception) - } - - override fun onCompleted() { - observableContext.clientAddressToObservables.compute(observableContext.clientAddress) { _, observables -> - if (observables != null) { - observables.remove(observableId) - if (observables.isEmpty()) { - null - } else { - observables - } - } else { - null - } - } - } - } - ) - ) - observableContext.clientAddressToObservables.compute(observableContext.clientAddress) { _, observables -> - if (observables == null) { - hashSetOf(observableId) - } else { - observables.add(observableId) - observables - } - } - observableContext.observableMap.put(observableId, observableWithSubscription) - } - - private fun Output.writeInvocationId(id: InvocationId) { - - writeString(id.value) - writeLong(id.timestamp.toEpochMilli()) - } -} diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt index d270ab1d93..ae2c734f96 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt @@ -5,7 +5,7 @@ import com.nhaarman.mockito_kotlin.whenever import net.corda.client.rpc.internal.serialization.kryo.KryoClientSerializationScheme import net.corda.core.DoNotImplement import net.corda.core.serialization.internal.* -import net.corda.node.serialization.KryoServerSerializationScheme +import net.corda.node.serialization.kryo.KryoServerSerializationScheme import net.corda.nodeapi.internal.serialization.* import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme From 23c604b1db531e679552116b8e4b1840ac0eb14a Mon Sep 17 00:00:00 2001 From: szymonsztuka Date: Wed, 2 May 2018 16:59:13 +0100 Subject: [PATCH 22/25] Vault queries - change SQL generation for aggregate function in 'group by' clause. (#3016) Vault queries: 'order by' function aggregation e.g. 'order by sum(col)' is replaced by the position of the selected column (e.g. 'order by 1'). Rationale: A database may be unable to support aggregation function in order by clause e.g. 'order by (sum col_x)' and Hibernate can't produce alias in this case e.g. ' select sum(col_x) as alias_x... order by also_x ...', the best universal solution is to use positional parameter e.g. 'select sum(col_x) as alias_x... order by 1 ...' (cherry picked from commit 416d4ec) --- .../vault/HibernateQueryCriteriaParser.kt | 25 ++++--- .../node/services/vault/VaultQueryTests.kt | 66 +++++++++++++++++++ 2 files changed, 82 insertions(+), 9 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt index 042ff71c01..f5c1de3c5d 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt @@ -289,29 +289,36 @@ class HibernateQueryCriteriaParser(val contractStateType: Class criteriaBuilder.asc(aggregateExpression) - Sort.Direction.DESC -> criteriaBuilder.desc(aggregateExpression) - } - criteriaQuery.orderBy(orderCriteria) - } + // Some databases may not support aggregate expression in 'group by' clause e.g. 'group by sum(col)', + // Hibernate Criteria Builder can't produce alias 'group by col_alias', and the only solution is to use a positional parameter 'group by 1' + val orderByColumnPosition = aggregateExpressions.size + var shiftLeft = 0 // add optional group by clauses expression.groupByColumns?.let { columns -> val groupByExpressions = columns.map { _column -> val path = root.get(getColumnName(_column)) + val columnNumberBeforeRemoval = aggregateExpressions.size if (path is SingularAttributePath) //remove the same columns from different joins to match the single column in 'group by' only (from the last join) aggregateExpressions.removeAll { elem -> if (elem is SingularAttributePath) elem.attribute.javaMember == path.attribute.javaMember else false } + shiftLeft += columnNumberBeforeRemoval - aggregateExpressions.size //record how many times a duplicated column was removed (from the previous 'parseAggregateFunction' run) aggregateExpressions.add(path) path } criteriaQuery.groupBy(groupByExpressions) } + // optionally order by this aggregate function + expression.orderBy?.let { + val orderCriteria = + when (expression.orderBy!!) { + // when adding column position of 'group by' shift in case columns were removed + Sort.Direction.ASC -> criteriaBuilder.asc(criteriaBuilder.literal(orderByColumnPosition - shiftLeft)) + Sort.Direction.DESC -> criteriaBuilder.desc(criteriaBuilder.literal(orderByColumnPosition - shiftLeft)) + } + criteriaQuery.orderBy(orderCriteria) + } return aggregateExpression } else -> throw VaultQueryException("Not expecting $columnPredicate") diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index c7902e56e6..69dede7361 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -772,6 +772,72 @@ class VaultQueryTests { } } + @Test + fun `aggregate functions with single group clause desc first column`() { + database.transaction { + listOf(100.DOLLARS, 200.DOLLARS, 300.DOLLARS, 400.POUNDS, 500.SWISS_FRANCS).zip(1..5).forEach { (howMuch, states) -> + vaultFiller.fillWithSomeTestCash(howMuch, notaryServices, states, DUMMY_CASH_ISSUER) + } + val sum = builder { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency), orderBy = Sort.Direction.DESC) } + val max = builder { CashSchemaV1.PersistentCashState::pennies.max(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) } + val min = builder { CashSchemaV1.PersistentCashState::pennies.min(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) } + + val results = vaultService.queryBy>(VaultCustomQueryCriteria(sum) + .and(VaultCustomQueryCriteria(max)) + .and(VaultCustomQueryCriteria(min))) + + assertThat(results.otherResults).hasSize(12) + + assertThat(results.otherResults.subList(0,4)).isEqualTo(listOf(60000L, 11298L, 8702L, "USD")) + assertThat(results.otherResults.subList(4,8)).isEqualTo(listOf(50000L, 10274L, 9481L, "CHF")) + assertThat(results.otherResults.subList(8,12)).isEqualTo(listOf(40000L, 10343L, 9351L, "GBP")) + } + } + + @Test + fun `aggregate functions with single group clause desc mid column`() { + database.transaction { + listOf(100.DOLLARS, 200.DOLLARS, 300.DOLLARS, 400.POUNDS, 500.SWISS_FRANCS).zip(1..5).forEach { (howMuch, states) -> + vaultFiller.fillWithSomeTestCash(howMuch, notaryServices, states, DUMMY_CASH_ISSUER) + } + val sum = builder { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) } + val max = builder { CashSchemaV1.PersistentCashState::pennies.max(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency), orderBy = Sort.Direction.DESC) } + val min = builder { CashSchemaV1.PersistentCashState::pennies.min(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) } + + val results = vaultService.queryBy>(VaultCustomQueryCriteria(sum) + .and(VaultCustomQueryCriteria(max)) + .and(VaultCustomQueryCriteria(min))) + + assertThat(results.otherResults).hasSize(12) + + assertThat(results.otherResults.subList(0,4)).isEqualTo(listOf(60000L, 11298L, 8702L, "USD")) + assertThat(results.otherResults.subList(4,8)).isEqualTo(listOf(40000L, 10343L, 9351L, "GBP")) + assertThat(results.otherResults.subList(8,12)).isEqualTo(listOf(50000L, 10274L, 9481L, "CHF")) + } + } + + @Test + fun `aggregate functions with single group clause desc last column`() { + database.transaction { + listOf(100.DOLLARS, 200.DOLLARS, 300.DOLLARS, 400.POUNDS, 500.SWISS_FRANCS).zip(1..5).forEach { (howMuch, states) -> + vaultFiller.fillWithSomeTestCash(howMuch, notaryServices, states, DUMMY_CASH_ISSUER) + } + val sum = builder { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) } + val max = builder { CashSchemaV1.PersistentCashState::pennies.max(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) } + val min = builder { CashSchemaV1.PersistentCashState::pennies.min(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency), orderBy = Sort.Direction.DESC) } + + val results = vaultService.queryBy>(VaultCustomQueryCriteria(sum) + .and(VaultCustomQueryCriteria(max)) + .and(VaultCustomQueryCriteria(min))) + + assertThat(results.otherResults).hasSize(12) + + assertThat(results.otherResults.subList(0,4)).isEqualTo(listOf(50000L, 10274L, 9481L, "CHF")) + assertThat(results.otherResults.subList(4,8)).isEqualTo(listOf(40000L, 10343L, 9351L, "GBP")) + assertThat(results.otherResults.subList(8,12)).isEqualTo(listOf(60000L, 11298L, 8702L, "USD")) + } + } + @Test fun `aggregate functions sum by issuer and currency and sort by aggregate sum`() { database.transaction { From 6b65ea06ac68174209c19c32dea0ae99ec5963ea Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Thu, 3 May 2018 10:31:22 +0100 Subject: [PATCH 23/25] Link back to contributing.md. Instructions on updating docs. Tweaks. (#3064) --- docs/source/contributing.rst | 37 +++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst index 17d894dddb..e1b5396534 100644 --- a/docs/source/contributing.rst +++ b/docs/source/contributing.rst @@ -1,7 +1,9 @@ Contributing ============ -Corda is an open-source project and we welcome contributions. This guide explains how to contribute back to Corda. +Corda is an open-source project and contributions are welcome. Our contributing philosophy is described in +`CONTRIBUTING.md `_. This guide explains the mechanics +of contributing to Corda. .. contents:: @@ -9,19 +11,19 @@ Identifying an area to contribute --------------------------------- There are several ways to identify an area where you can contribute to Corda: +* Ask in the ``#design`` channel of the `Corda Slack `_ + +* Browse the `Corda GitHub issues `_ + + * It's always worth checking in the ``#design`` channel whether a given issue is a good target for your + contribution. Someone else may already be working on it, or it may be blocked by an on-going piece of work + * Browse issues labelled as ``HelpWanted`` on the `Corda JIRA board `_ * Any issue with a ``HelpWanted`` label is considered ideal for open-source contributions * If there is a feature you would like to add and there isn't a corresponding issue labelled as ``HelpWanted``, that - doesn't mean your contribution isn't welcome. Please reach out on the Corda Slack channel (see below) to clarify - -* Check the `Corda GitHub issues `_ - - * It's always worth checking in the Corda Slack channel (see below) whether a given issue is a good target for your - contribution. Someone else may already be working on it, or it may be blocked by an on-going piece of work - -* Ask in the `Corda Slack channel `_ + doesn't mean your contribution isn't welcome. Please reach out on the ``#design`` channel to clarify Making the required changes --------------------------- @@ -39,8 +41,17 @@ Your changes must pass the tests described :doc:`here `. Building against the master branch ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -You may also want to test your changes against a CorDapp defined outside of the Corda repo. To do so, please follow the -instructions :doc:`here `. +You can test your changes against CorDapps defined in other repos by following the instructions :doc:`here `. + +Updating the docs +----------------- + +Any changes to Corda's public API must be documented as follows: + +1. Update the relevant `.rst file(s) `_ +2. Include the change in the :doc:`changelog ` and :doc:`release notes ` where applicable +3. :doc:`Build the docs locally ` +4. Open the built .html files for the modified pages to ensure they render correctly Merging the changes back into Corda ----------------------------------- @@ -55,10 +66,10 @@ Merging the changes back into Corda * State that you are in agreement with the terms of `CONTRIBUTING.md `_ -3. Request a review from a member of the Corda platform team via the `Corda Slack channel `_ +3. Request a review from a member of the Corda platform team via the `#design channel `_ 4. Wait for your PR to pass all four types of continuous integration tests (integration, API stability, build and unit) * Currently, external contributors cannot see the output of these tests. If your PR fails a test that passed locally, ask the reviewer for further details -5. Once a reviewer has approved the PR and the tests have passed, squash-and-merge the PR as a single commit \ No newline at end of file +5. Once a reviewer has approved the PR and the tests have passed, squash-and-merge the PR as a single commit From a6b72574914239d6c94444c5b1538791f2710313 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Thu, 3 May 2018 12:12:54 +0100 Subject: [PATCH 24/25] CORDA-1441: Upgrade to Kotlin 1.2.41 (#3063) --- build.gradle | 8 ++++++++ constants.properties | 2 +- .../net/corda/finance/flows/CashConfigDataFlow.kt | 1 - .../kotlin/net/corda/finance/flows/CashExitFlow.kt | 3 +++ .../kotlin/net/corda/finance/flows/CashIssueFlow.kt | 3 +++ .../net/corda/finance/flows/CashPaymentFlow.kt | 4 ++++ .../net/corda/node/services/vault/VaultQueryTests.kt | 12 ++++++------ tools/demobench/build.gradle | 9 --------- 8 files changed, 25 insertions(+), 17 deletions(-) diff --git a/build.gradle b/build.gradle index f663e89984..f8270f2808 100644 --- a/build.gradle +++ b/build.gradle @@ -209,6 +209,14 @@ allprojects { } configurations { + all { + resolutionStrategy { + // Force dependencies to use the same version of Kotlin as Corda. + force "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + force "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + force "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + } + } compile { // We want to use SLF4J's version of these bindings: jcl-over-slf4j // Remove any transitive dependency on Apache's version. diff --git a/constants.properties b/constants.properties index 57d7f119e1..6e5d6a60eb 100644 --- a/constants.properties +++ b/constants.properties @@ -1,5 +1,5 @@ gradlePluginsVersion=4.0.15 -kotlinVersion=1.2.20 +kotlinVersion=1.2.41 platformVersion=4 guavaVersion=21.0 bouncycastleVersion=1.57 diff --git a/finance/src/main/kotlin/net/corda/finance/flows/CashConfigDataFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashConfigDataFlow.kt index e1978905fa..a9e9abcb53 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/CashConfigDataFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashConfigDataFlow.kt @@ -17,7 +17,6 @@ import net.corda.finance.GBP import net.corda.finance.USD import net.corda.finance.flows.ConfigHolder.Companion.supportedCurrencies import java.io.IOException -import java.nio.file.Path import java.util.* // TODO Until apps have access to their own config, we'll hack things by first getting the baseDirectory, read the node.conf diff --git a/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt index adc6f1b55a..eae18a7faf 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt @@ -15,6 +15,9 @@ import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.ProgressTracker import net.corda.finance.contracts.asset.Cash import net.corda.finance.contracts.asset.cash.selection.AbstractCashSelection +import net.corda.finance.flows.AbstractCashFlow.Companion.FINALISING_TX +import net.corda.finance.flows.AbstractCashFlow.Companion.GENERATING_TX +import net.corda.finance.flows.AbstractCashFlow.Companion.SIGNING_TX import net.corda.finance.issuedBy import java.util.* diff --git a/finance/src/main/kotlin/net/corda/finance/flows/CashIssueFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashIssueFlow.kt index 39ef76823b..385767703e 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/CashIssueFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashIssueFlow.kt @@ -9,6 +9,9 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.ProgressTracker import net.corda.finance.contracts.asset.Cash +import net.corda.finance.flows.AbstractCashFlow.Companion.FINALISING_TX +import net.corda.finance.flows.AbstractCashFlow.Companion.GENERATING_TX +import net.corda.finance.flows.AbstractCashFlow.Companion.SIGNING_TX import net.corda.finance.issuedBy import java.util.* diff --git a/finance/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt index dc71e54884..f3afc3a107 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt @@ -11,6 +11,10 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import net.corda.finance.contracts.asset.Cash +import net.corda.finance.flows.AbstractCashFlow.Companion.FINALISING_TX +import net.corda.finance.flows.AbstractCashFlow.Companion.GENERATING_ID +import net.corda.finance.flows.AbstractCashFlow.Companion.GENERATING_TX +import net.corda.finance.flows.AbstractCashFlow.Companion.SIGNING_TX import java.util.* /** diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 69dede7361..6fcf1365c3 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -99,17 +99,17 @@ class VaultQueryTests { // register additional identities val databaseAndServices = makeTestDatabaseAndMockServices( cordappPackages, - makeTestIdentityService(Companion.MEGA_CORP_IDENTITY, Companion.MINI_CORP_IDENTITY, Companion.dummyCashIssuer.identity, Companion.dummyNotary.identity), + makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, dummyCashIssuer.identity, dummyNotary.identity), Companion.megaCorp, - moreKeys = Companion.DUMMY_NOTARY_KEY) + moreKeys = DUMMY_NOTARY_KEY) database = databaseAndServices.first services = databaseAndServices.second - vaultFiller = VaultFiller(services, Companion.dummyNotary) - vaultFillerCashNotary = VaultFiller(services, Companion.dummyNotary, Companion.CASH_NOTARY) - notaryServices = MockServices(cordappPackages, Companion.dummyNotary, rigorousMock(), Companion.dummyCashIssuer.keyPair, Companion.BOC_KEY, Companion.MEGA_CORP_KEY) + vaultFiller = VaultFiller(services, dummyNotary) + vaultFillerCashNotary = VaultFiller(services, dummyNotary, CASH_NOTARY) + notaryServices = MockServices(cordappPackages, dummyNotary, rigorousMock(), dummyCashIssuer.keyPair, BOC_KEY, MEGA_CORP_KEY) identitySvc = services.identityService // Register all of the identities we're going to use - (notaryServices.myInfo.legalIdentitiesAndCerts + Companion.BOC_IDENTITY + Companion.CASH_NOTARY_IDENTITY + Companion.MINI_CORP_IDENTITY + Companion.MEGA_CORP_IDENTITY).forEach { identity -> + (notaryServices.myInfo.legalIdentitiesAndCerts + BOC_IDENTITY + CASH_NOTARY_IDENTITY + MINI_CORP_IDENTITY + MEGA_CORP_IDENTITY).forEach { identity -> services.identityService.verifyAndRegisterIdentity(identity) } } diff --git a/tools/demobench/build.gradle b/tools/demobench/build.gradle index 9742b25325..35d5c82e2b 100644 --- a/tools/demobench/build.gradle +++ b/tools/demobench/build.gradle @@ -39,15 +39,6 @@ repositories { } } -configurations.all { - resolutionStrategy { - // Force TornadoFX to use the same version of Kotlin as Corda. - force "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - force "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - force "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - } -} - dependencies { // TornadoFX: A lightweight Kotlin framework for working with JavaFX UI's. compile "no.tornado:tornadofx:$tornadofx_version" From 9ffb43f3f7a39010ef567c746284f557885373aa Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Thu, 3 May 2018 12:28:36 +0100 Subject: [PATCH 25/25] CORDA-1385: Ignore duplicate packages and sub-packages in driver extraCordappPackagesToScan (#3066) Otherwise duplicate test CorDapps are loaded into the node --- .../corda/node/CordappScanningDriverTest.kt | 4 +- .../kotlin/net/corda/node/internal/Node.kt | 6 +- .../node/internal/cordapp/CordappLoader.kt | 69 ++++++++++--------- .../internal/cordapp/CordappLoaderTest.kt | 40 ++++++----- 4 files changed, 68 insertions(+), 51 deletions(-) diff --git a/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt b/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt index 0d5c9deb52..327cbd06eb 100644 --- a/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt @@ -37,13 +37,13 @@ class CordappScanningDriverTest { @StartableByRPC @InitiatingFlow - class ReceiveFlow(val otherParty: Party) : FlowLogic() { + class ReceiveFlow(private val otherParty: Party) : FlowLogic() { @Suspendable override fun call(): String = initiateFlow(otherParty).receive().unwrap { it } } @InitiatedBy(ReceiveFlow::class) - open class SendClassFlow(val otherPartySession: FlowSession) : FlowLogic() { + open class SendClassFlow(private val otherPartySession: FlowSession) : FlowLogic() { @Suspendable override fun call() = otherPartySession.send(javaClass.name) } 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 993a26e7a4..f8f57e16fc 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -86,8 +86,10 @@ open class Node(configuration: NodeConfiguration, } private val sameVmNodeCounter = AtomicInteger() - val scanPackagesSystemProperty = "net.corda.node.cordapp.scan.packages" - val scanPackagesSeparator = "," + + const val scanPackagesSystemProperty = "net.corda.node.cordapp.scan.packages" + const val scanPackagesSeparator = "," + private fun makeCordappLoader(configuration: NodeConfiguration): CordappLoader { return System.getProperty(scanPackagesSystemProperty)?.let { scanPackages -> CordappLoader.createDefaultWithTestPackages(configuration, scanPackages.split(scanPackagesSeparator)) diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt index fb29a65633..d8f974caa0 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt @@ -1,10 +1,8 @@ package net.corda.node.internal.cordapp +import com.github.benmanes.caffeine.cache.Caffeine import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult -import net.corda.core.contracts.Contract -import net.corda.core.contracts.UpgradedContract -import net.corda.core.contracts.UpgradedContractWithLegacyConstraint import net.corda.core.cordapp.Cordapp import net.corda.core.flows.* import net.corda.core.internal.* @@ -22,7 +20,6 @@ import net.corda.nodeapi.internal.serialization.DefaultWhitelist import org.apache.commons.collections4.map.LRUMap import java.lang.reflect.Modifier import java.net.JarURLConnection -import java.net.URI import java.net.URL import java.net.URLClassLoader import java.nio.file.Path @@ -30,6 +27,7 @@ import java.nio.file.Paths import java.nio.file.attribute.FileTime import java.time.Instant import java.util.* +import java.util.concurrent.ConcurrentHashMap import java.util.jar.JarOutputStream import java.util.zip.ZipEntry import kotlin.reflect.KClass @@ -67,10 +65,21 @@ class CordappLoader private constructor(private val cordappJarPaths: List, CordappLoader>(1000) + private val cordappLoadersCache = Caffeine.newBuilder().softValues().build, CordappLoader>() + private val generatedCordapps = ConcurrentHashMap() + + private fun simplifyScanPackages(scanPackages: List): List { + return scanPackages.sorted().fold(emptyList()) { listSoFar, packageName -> + when { + listSoFar.isEmpty() -> listOf(packageName) + packageName.startsWith(listSoFar.last()) -> listSoFar // Squash ["com.foo", "com.foo.bar"] into just ["com.foo"] + else -> listSoFar + packageName + } + } + } /** * Create a dev mode CordappLoader for test environments that creates and loads cordapps from the classpath @@ -83,8 +92,8 @@ class CordappLoader private constructor(private val cordappJarPaths: List): CordappLoader { - return cordappLoadersCache.computeIfAbsent(testPackages, { CordappLoader(testPackages.flatMap(this::createScanPackage)) }) + val urls = simplifyScanPackages(testPackages).flatMap(this::getPackageURLs) + return cordappLoadersCache.asMap().computeIfAbsent(urls, ::CordappLoader) } /** @@ -107,34 +117,33 @@ class CordappLoader private constructor(private val cordappJarPaths: List) = CordappLoader(scanJars.map { RestrictedURL(it, null) }) - private fun getCordappsPath(baseDir: Path): Path = baseDir / CORDAPPS_DIR_NAME - - private fun createScanPackage(scanPackage: String): List { + private fun getPackageURLs(scanPackage: String): List { val resource = scanPackage.replace('.', '/') return this::class.java.classLoader.getResources(resource) .asSequence() - .map { path -> - if (path.protocol == "jar") { + .map { url -> + if (url.protocol == "jar") { // When running tests from gradle this may be a corda module jar, so restrict to scanPackage: - RestrictedURL((path.openConnection() as JarURLConnection).jarFileURL, scanPackage) + RestrictedURL((url.openConnection() as JarURLConnection).jarFileURL, scanPackage) } else { // No need to restrict as createDevCordappJar has already done that: - RestrictedURL(createDevCordappJar(scanPackage, path, resource).toURL(), null) + RestrictedURL(createDevCordappJar(scanPackage, url, resource).toUri().toURL(), null) } } .toList() } /** Takes a package of classes and creates a JAR from them - only use in tests. */ - private fun createDevCordappJar(scanPackage: String, url: URL, jarPackageName: String): URI { + private fun createDevCordappJar(scanPackage: String, url: URL, resource: String): Path { return generatedCordapps.computeIfAbsent(url) { + // TODO Using the driver in out-of-process mode causes each node to have their own copy of the same dev CorDapps val cordappDir = (Paths.get("build") / "tmp" / "generated-test-cordapps").createDirectories() - val cordappJAR = cordappDir / "$scanPackage-${UUID.randomUUID()}.jar" - logger.info("Generating a test-only cordapp of classes discovered in $scanPackage at $cordappJAR") - JarOutputStream(cordappJAR.outputStream()).use { jos -> + val cordappJar = cordappDir / "$scanPackage-${UUID.randomUUID()}.jar" + logger.info("Generating a test-only CorDapp of classes discovered for package $scanPackage in $url: $cordappJar") + JarOutputStream(cordappJar.outputStream()).use { jos -> val scanDir = url.toPath() scanDir.walk { it.forEach { - val entryPath = "$jarPackageName/${scanDir.relativize(it).toString().replace('\\', '/')}" + val entryPath = "$resource/${scanDir.relativize(it).toString().replace('\\', '/')}" val time = FileTime.from(Instant.EPOCH) val entry = ZipEntry(entryPath).setCreationTime(time).setLastAccessTime(time).setLastModifiedTime(time) jos.putNextEntry(entry) @@ -144,22 +153,21 @@ class CordappLoader private constructor(private val cordappJarPaths: List { + private fun getNodeCordappURLs(baseDir: Path): List { + val cordappsDir = baseDir / CORDAPPS_DIR_NAME return if (!cordappsDir.exists()) { emptyList() } else { cordappsDir.list { - it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.map { RestrictedURL(it.toUri().toURL(), null) }.toList() + it.filter { it.toString().endsWith(".jar") }.map { RestrictedURL(it.toUri().toURL(), null) }.toList() } } } - private val generatedCordapps = mutableMapOf() - /** A list of the core RPC flows present in Corda */ private val coreRPCFlows = listOf( ContractUpgradeFlow.Initiate::class.java, @@ -253,7 +261,7 @@ class CordappLoader private constructor(private val cordappJarPaths: List(1000) private fun scanCordapp(cordappJarPath: RestrictedURL): RestrictedScanResult { - logger.info("Scanning CorDapp in $cordappJarPath") + logger.info("Scanning CorDapp in ${cordappJarPath.url}") return cachedScanResult.computeIfAbsent(cordappJarPath, { RestrictedScanResult(FastClasspathScanner().addClassLoader(appClassLoader).overrideClasspath(cordappJarPath.url).scan(), cordappJarPath.qualifiedNamePrefix) }) @@ -283,10 +291,9 @@ class CordappLoader private constructor(private val cordappJarPaths: List() { @Suspendable - override fun call() { - } + override fun call() = Unit } @InitiatedBy(DummyFlow::class) -class LoaderTestFlow(unusedSession: FlowSession) : FlowLogic() { +class LoaderTestFlow(@Suppress("UNUSED_PARAMETER") unusedSession: FlowSession) : FlowLogic() { @Suspendable - override fun call() { - } + override fun call() = Unit } @SchedulableFlow class DummySchedulableFlow : FlowLogic() { @Suspendable - override fun call() { - } + override fun call() = Unit } @StartableByRPC class DummyRPCFlow : FlowLogic() { @Suspendable - override fun call() { - } + override fun call() = Unit } class CordappLoaderTest { private companion object { - val testScanPackages = listOf("net.corda.node.internal.cordapp") - val isolatedContractId = "net.corda.finance.contracts.isolated.AnotherDummyContract" - val isolatedFlowName = "net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator" + const val testScanPackage = "net.corda.node.internal.cordapp" + const val isolatedContractId = "net.corda.finance.contracts.isolated.AnotherDummyContract" + const val isolatedFlowName = "net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator" } @Test fun `test that classes that aren't in cordapps aren't loaded`() { // Basedir will not be a corda node directory so the dummy flow shouldn't be recognised as a part of a cordapp val loader = CordappLoader.createDefault(Paths.get(".")) - assertThat(loader.cordapps) - .hasSize(1) - .contains(CordappLoader.coreCordapp) + assertThat(loader.cordapps).containsOnly(CordappLoader.coreCordapp) } @Test @@ -71,7 +65,7 @@ class CordappLoaderTest { @Test fun `flows are loaded by loader`() { - val loader = CordappLoader.createWithTestPackages(testScanPackages) + val loader = CordappLoader.createWithTestPackages(listOf(testScanPackage)) val actual = loader.cordapps.toTypedArray() // One core cordapp, one cordapp from this source tree, and two others due to identically named locations @@ -85,6 +79,20 @@ class CordappLoaderTest { assertThat(actualCordapp.schedulableFlows).first().hasSameClassAs(DummySchedulableFlow::class.java) } + @Test + fun `duplicate packages are ignored`() { + val loader = CordappLoader.createWithTestPackages(listOf(testScanPackage, testScanPackage)) + val cordapps = loader.cordapps.filter { LoaderTestFlow::class.java in it.initiatedFlows } + assertThat(cordapps).hasSize(1) + } + + @Test + fun `sub-packages are ignored`() { + val loader = CordappLoader.createWithTestPackages(listOf("net.corda", testScanPackage)) + val cordapps = loader.cordapps.filter { LoaderTestFlow::class.java in it.initiatedFlows } + assertThat(cordapps).hasSize(1) + } + // This test exists because the appClassLoader is used by serialisation and we need to ensure it is the classloader // being used internally. Later iterations will use a classloader per cordapp and this test can be retired. @Test