Merge remote-tracking branch 'open/master' into os-merge-020318

# Conflicts:
#	node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt
#	node/src/main/kotlin/net/corda/node/internal/Node.kt
This commit is contained in:
Shams Asari 2018-03-02 10:42:03 +00:00
commit a59083ceb2
19 changed files with 427 additions and 147 deletions

View File

@ -7,48 +7,129 @@ import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.NonEmptySet import net.corda.core.utilities.NonEmptySet
import java.security.PublicKey import java.security.PublicKey
/**
* The node asked a remote peer for the transaction identified by [hash] because it is a dependency of a transaction
* being resolved, but the remote peer would not provide it.
*
* @property hash Merkle root of the transaction being resolved, see [net.corda.core.transactions.WireTransaction.id]
*/
class TransactionResolutionException(val hash: SecureHash) : FlowException("Transaction resolution failure for $hash") class TransactionResolutionException(val hash: SecureHash) : FlowException("Transaction resolution failure for $hash")
/**
* The node asked a remote peer for the attachment identified by [hash] because it is a dependency of a transaction
* being resolved, but the remote peer would not provide it.
*
* @property hash Hash of the bytes of the attachment, see [Attachment.id]
*/
class AttachmentResolutionException(val hash: SecureHash) : FlowException("Attachment resolution failure for $hash") class AttachmentResolutionException(val hash: SecureHash) : FlowException("Attachment resolution failure for $hash")
/**
* Indicates that some aspect of the transaction named by [txId] violates the platform rules. The exact type of failure
* is expressed using a subclass. TransactionVerificationException is a [FlowException] and thus when thrown inside
* a flow, the details of the failure will be serialised, propagated to the peer and rethrown.
*
* @property txId the Merkle root hash (identifier) of the transaction that failed verification.
*/
@Suppress("MemberVisibilityCanBePrivate")
@CordaSerializable
sealed class TransactionVerificationException(val txId: SecureHash, message: String, cause: Throwable?) sealed class TransactionVerificationException(val txId: SecureHash, message: String, cause: Throwable?)
: FlowException("$message, transaction: $txId", cause) { : FlowException("$message, transaction: $txId", cause) {
class ContractRejection(txId: SecureHash, contract: Contract, cause: Throwable) /**
: TransactionVerificationException(txId, "Contract verification failed: ${cause.message}, contract: $contract", cause) * Indicates that one of the [Contract.verify] methods selected by the contract constraints and attachments
* rejected the transaction by throwing an exception.
*
* @property contractClass The fully qualified class name of the failing contract.
*/
class ContractRejection(txId: SecureHash, val contractClass: String, cause: Throwable) : TransactionVerificationException(txId, "Contract verification failed: ${cause.message}, contract: $contractClass", cause) {
constructor(txId: SecureHash, contract: Contract, cause: Throwable) : this(txId, contract.javaClass.name, cause)
}
class ContractConstraintRejection(txId: SecureHash, contractClass: String) /**
* The transaction attachment that contains the [contractClass] class didn't meet the constraints specified by
* the [TransactionState.constraint] object. This usually implies a version mismatch of some kind.
*
* @property contractClass The fully qualified class name of the failing contract.
*/
class ContractConstraintRejection(txId: SecureHash, val contractClass: String)
: TransactionVerificationException(txId, "Contract constraints failed for $contractClass", null) : TransactionVerificationException(txId, "Contract constraints failed for $contractClass", null)
/**
* A state requested a contract class via its [TransactionState.contract] field that didn't appear in any attached
* JAR at all. This usually implies the attachments were forgotten or a version mismatch.
*
* @property contractClass The fully qualified class name of the failing contract.
*/
class MissingAttachmentRejection(txId: SecureHash, val contractClass: String) class MissingAttachmentRejection(txId: SecureHash, val contractClass: String)
: TransactionVerificationException(txId, "Contract constraints failed, could not find attachment for: $contractClass", null) : TransactionVerificationException(txId, "Contract constraints failed, could not find attachment for: $contractClass", null)
class ConflictingAttachmentsRejection(txId: SecureHash, contractClass: String) /**
* Indicates this transaction violates the "no overlap" rule: two attachments are trying to provide the same file
* path. Whereas Java classpaths would normally allow that with the first class taking precedence, this is not
* allowed in transactions for security reasons. This usually indicates that two separate apps share a dependency,
* in which case you could try 'shading the fat jars' to rename classes of dependencies. Or you could manually
* attach dependency JARs when building the transaction.
*
* @property contractClass The fully qualified class name of the failing contract.
*/
class ConflictingAttachmentsRejection(txId: SecureHash, val contractClass: String)
: TransactionVerificationException(txId, "Contract constraints failed for: $contractClass, because multiple attachments providing this contract were attached.", null) : TransactionVerificationException(txId, "Contract constraints failed for: $contractClass, because multiple attachments providing this contract were attached.", null)
class ContractCreationError(txId: SecureHash, contractClass: String, cause: Throwable) /**
* A [Contract] class named by a state could not be constructed. Most likely you do not have a no-argument
* constructor, or the class doesn't subclass [Contract].
*
* @property contractClass The fully qualified class name of the failing contract.
*/
class ContractCreationError(txId: SecureHash, val contractClass: String, cause: Throwable)
: TransactionVerificationException(txId, "Contract verification failed: ${cause.message}, could not create contract class: $contractClass", cause) : TransactionVerificationException(txId, "Contract verification failed: ${cause.message}, could not create contract class: $contractClass", cause)
class MoreThanOneNotary(txId: SecureHash) /**
: TransactionVerificationException(txId, "More than one notary", null) * An output state has a notary that doesn't match the transaction's notary field. It must!
*
* @property txNotary the [Party] specified by the transaction header.
* @property outputNotary the [Party] specified by the errant state.
*/
class NotaryChangeInWrongTransactionType(txId: SecureHash, val txNotary: Party, val outputNotary: Party)
: TransactionVerificationException(txId, "Found unexpected notary change in transaction. Tx notary: $txNotary, found: $outputNotary", null)
class SignersMissing(txId: SecureHash, missing: List<PublicKey>) /**
: TransactionVerificationException(txId, "Signers missing: ${missing.joinToString()}", null) * If a state is encumbered (the [TransactionState.encumbrance] field is set) then its encumbrance must be used
* as an input to any transaction that uses it. In this way states can be tied together in chains, thus composing
* logic. Note that encumbrances aren't fully supported by all aspects of the platform at this time so if you use
* them, you may find transactions created by the platform don't always respect the encumbrance rule.
*
* @property missing the index of the state missing the encumbrance.
* @property inOut whether the issue exists in the input list or output list.
*/
class TransactionMissingEncumbranceException(txId: SecureHash, val missing: Int, val inOut: Direction)
: TransactionVerificationException(txId, "Missing required encumbrance $missing in $inOut", null)
/** Whether the inputs or outputs list contains an encumbrance issue, see [TransactionMissingEncumbranceException]. */
@CordaSerializable
enum class Direction {
/** Issue in the inputs list */ INPUT,
/** Issue in the outputs list */ OUTPUT
}
// We could revisit and throw this more appropriate type in a future release that uses targetVersion to
// avoid the compatibility break, because IllegalStateException isn't ideal for this. Or we could use this
// as a cause.
/** @suppress This class is not used: duplicate inputs throw a [IllegalStateException] instead. */
@Deprecated("This class is not used: duplicate inputs throw a [IllegalStateException] instead.")
class DuplicateInputStates(txId: SecureHash, val duplicates: NonEmptySet<StateRef>) class DuplicateInputStates(txId: SecureHash, val duplicates: NonEmptySet<StateRef>)
: TransactionVerificationException(txId, "Duplicate inputs: ${duplicates.joinToString()}", null) : TransactionVerificationException(txId, "Duplicate inputs: ${duplicates.joinToString()}", null)
/** @suppress This class is obsolete and nothing has ever used it. */
@Deprecated("This class is obsolete and nothing has ever used it.")
class MoreThanOneNotary(txId: SecureHash) : TransactionVerificationException(txId, "More than one notary", null)
/** @suppress This class is obsolete and nothing has ever used it. */
@Deprecated("This class is obsolete and nothing has ever used it.")
class SignersMissing(txId: SecureHash, val missing: List<PublicKey>) : TransactionVerificationException(txId, "Signers missing: ${missing.joinToString()}", null)
/** @suppress This class is obsolete and nothing has ever used it. */
@Deprecated("This class is obsolete and nothing has ever used it.")
class InvalidNotaryChange(txId: SecureHash) class InvalidNotaryChange(txId: SecureHash)
: TransactionVerificationException(txId, "Detected a notary change. Outputs must use the same notary as inputs", null) : TransactionVerificationException(txId, "Detected a notary change. Outputs must use the same notary as inputs", null)
class NotaryChangeInWrongTransactionType(txId: SecureHash, txNotary: Party, outputNotary: Party)
: TransactionVerificationException(txId, "Found unexpected notary change in transaction. Tx notary: $txNotary, found: $outputNotary", null)
class TransactionMissingEncumbranceException(txId: SecureHash, missing: Int, inOut: Direction)
: TransactionVerificationException(txId, "Missing required encumbrance $missing in $inOut", null)
@CordaSerializable
enum class Direction {
INPUT,
OUTPUT
}
} }

View File

@ -116,10 +116,9 @@ data class LedgerTransaction @JvmOverloads constructor(
* If any contract fails to verify, the whole transaction is considered to be invalid. * If any contract fails to verify, the whole transaction is considered to be invalid.
*/ */
private fun verifyContracts() { private fun verifyContracts() {
for (contractEntry in contracts.entries) { for ((key, result) in contracts) {
val result = contractEntry.value
when (result) { when (result) {
is Try.Failure -> throw TransactionVerificationException.ContractCreationError(id, contractEntry.key, result.exception) is Try.Failure -> throw TransactionVerificationException.ContractCreationError(id, key, result.exception)
is Try.Success -> { is Try.Success -> {
val contract = result.value val contract = result.value
try { try {

View File

@ -0,0 +1,108 @@
package net.corda.core.contracts
import net.corda.core.crypto.SecureHash
import net.corda.core.transactions.LedgerTransaction
import net.corda.nodeapi.internal.serialization.AllWhitelist
import net.corda.nodeapi.internal.serialization.amqp.DeserializationInput
import net.corda.nodeapi.internal.serialization.amqp.SerializationOutput
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
import net.corda.nodeapi.internal.serialization.amqp.custom.PublicKeySerializer
import net.corda.testing.core.DUMMY_BANK_A_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.TestIdentity
import org.junit.Test
import kotlin.test.assertEquals
class TransactionVerificationExceptionSerialisationTests {
private fun defaultFactory() = SerializerFactory(
AllWhitelist,
ClassLoader.getSystemClassLoader()
)
private val txid = SecureHash.allOnesHash
private val factory = defaultFactory()
@Test
fun contractConstraintRejectionTest() {
val excp = TransactionVerificationException.ContractConstraintRejection(txid, "This is only a test")
val excp2 = DeserializationInput(factory).deserialize(SerializationOutput(factory).serialize(excp))
assertEquals(excp.message, excp2.message)
assertEquals(excp.cause, excp2.cause)
assertEquals(excp.txId, excp2.txId)
}
@Test
fun contractRejectionTest() {
class TestContract(val thing: Int) : Contract {
override fun verify(tx: LedgerTransaction) = Unit
}
val contract = TestContract(12)
val cause = Throwable("wibble")
val exception = TransactionVerificationException.ContractRejection(txid, contract, cause)
val exception2 = DeserializationInput(factory).deserialize(SerializationOutput(factory).serialize(exception))
assertEquals(exception.message, exception2.message)
assertEquals(exception.cause?.message, exception2.cause?.message)
assertEquals(exception.txId, exception2.txId)
}
@Test
fun missingAttachmentRejectionTest() {
val exception = TransactionVerificationException.MissingAttachmentRejection(txid, "Some contract class")
val exception2 = DeserializationInput(factory).deserialize(SerializationOutput(factory).serialize(exception))
assertEquals(exception.message, exception2.message)
assertEquals(exception.cause?.message, exception2.cause?.message)
assertEquals(exception.txId, exception2.txId)
}
@Test
fun conflictingAttachmentsRejectionTest() {
val exception = TransactionVerificationException.ContractConstraintRejection(txid, "Some contract class")
val exception2 = DeserializationInput(factory).deserialize(SerializationOutput(factory).serialize(exception))
assertEquals(exception.message, exception2.message)
assertEquals(exception.cause?.message, exception2.cause?.message)
assertEquals(exception.txId, exception2.txId)
}
@Test
fun contractCreationErrorTest() {
val cause = Throwable("wibble")
val exception = TransactionVerificationException.ContractCreationError(txid, "Some contract class", cause)
val exception2 = DeserializationInput(factory).deserialize(SerializationOutput(factory).serialize(exception))
assertEquals(exception.message, exception2.message)
assertEquals(exception.cause?.message, exception2.cause?.message)
assertEquals(exception.txId, exception2.txId)
}
@Test
fun transactionMissingEncumbranceTest() {
val exception = TransactionVerificationException.TransactionMissingEncumbranceException(
txid, 12, TransactionVerificationException.Direction.INPUT)
val exception2 = DeserializationInput(factory).deserialize(SerializationOutput(factory).serialize(exception))
assertEquals(exception.message, exception2.message)
assertEquals(exception.cause?.message, exception2.cause?.message)
assertEquals(exception.txId, exception2.txId)
}
@Test
fun notaryChangeInWrongTransactionTypeTest() {
val dummyBankA = TestIdentity(DUMMY_BANK_A_NAME, 40).party
val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20).party
val factory = defaultFactory()
factory.register(PublicKeySerializer)
val exception = TransactionVerificationException.NotaryChangeInWrongTransactionType(txid, dummyBankA, dummyNotary)
val exception2 = DeserializationInput(factory).deserialize(SerializationOutput(factory).serialize(exception))
assertEquals(exception.message, exception2.message)
assertEquals(exception.cause?.message, exception2.cause?.message)
assertEquals(exception.txId, exception2.txId)
}
}

View File

@ -78,7 +78,7 @@ absolute path to the node's base directory.
:p2pAddress: The host and port on which the node is available for protocol operations over ArtemisMQ. :p2pAddress: The host and port on which the node is available for protocol operations over ArtemisMQ.
.. note:: In practice the ArtemisMQ messaging services bind to all local addresses on the specified port. However, .. note:: In practice the ArtemisMQ messaging services bind to all local addresses on the specified port. However,
note that the host is the included as the advertised entry in the NetworkMapService. As a result the value listed note that the host is the included as the advertised entry in the network map. As a result the value listed
here must be externally accessible when running nodes across a cluster of machines. If the provided host is unreachable, here must be externally accessible when running nodes across a cluster of machines. If the provided host is unreachable,
the node will try to auto-discover its public one. the node will try to auto-discover its public one.

View File

@ -1,8 +1,6 @@
Creating nodes locally Creating nodes locally
====================== ======================
.. contents::
Node structure Node structure
-------------- --------------
Each Corda node has the following structure: Each Corda node has the following structure:
@ -91,8 +89,8 @@ The OID and format for these extensions will be described in a further specifica
The Cordform task The Cordform task
----------------- -----------------
Corda provides a gradle plugin called ``Cordform`` that allows you to automatically generate and configure a set of Corda provides a gradle plugin called ``Cordform`` that allows you to automatically generate and configure a set of
nodes. Here is an example ``Cordform`` task called ``deployNodes`` that creates three nodes, defined in the nodes for testing and demos. Here is an example ``Cordform`` task called ``deployNodes`` that creates three nodes, defined
`Kotlin CorDapp Template <https://github.com/corda/cordapp-template-kotlin/blob/release-V2/build.gradle#L97>`_: in the `Kotlin CorDapp Template <https://github.com/corda/cordapp-template-kotlin/blob/release-V3/build.gradle#L100>`_:
.. sourcecode:: groovy .. sourcecode:: groovy
@ -155,7 +153,7 @@ You can extend ``deployNodes`` to generate additional nodes.
.. warning:: When adding nodes, make sure that there are no port clashes! .. warning:: When adding nodes, make sure that there are no port clashes!
Specifying a custom webserver Specifying a custom webserver
----------------------------- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
By default, any node listing a webport will use the default development webserver, which is not production-ready. You By default, any node listing a webport will use the default development webserver, which is not production-ready. You
can use your own webserver JAR instead by using the ``webserverJar`` argument in a ``Cordform`` ``node`` configuration can use your own webserver JAR instead by using the ``webserverJar`` argument in a ``Cordform`` ``node`` configuration
block: block:
@ -174,7 +172,7 @@ The webserver JAR will be copied into the node's ``build`` folder with the name
node's ``node.conf`` file. node's ``node.conf`` file.
Running deployNodes Running deployNodes
------------------- ~~~~~~~~~~~~~~~~~~~
To create the nodes defined in our ``deployNodes`` task, run the following command in a terminal window from the root To create the nodes defined in our ``deployNodes`` task, run the following command in a terminal window from the root
of the project where the ``deployNodes`` task is defined: of the project where the ``deployNodes`` task is defined:

View File

@ -21,7 +21,7 @@ service.
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
directory "./build/nodes" directory "./build/nodes"
node { node {
name "O=NetworkMapAndNotary,L=London,C=GB" name "O=Notary,L=London,C=GB"
notary = [validating : true] notary = [validating : true]
p2pPort 10002 p2pPort 10002
rpcPort 10003 rpcPort 10003
@ -142,7 +142,7 @@ The vaults of PartyA and PartyB should both display the following output:
- "C=GB,L=London,O=PartyA" - "C=GB,L=London,O=PartyA"
- "C=US,L=New York,O=PartyB" - "C=US,L=New York,O=PartyB"
contract: "com.template.contract.IOUContract" contract: "com.template.contract.IOUContract"
notary: "C=GB,L=London,O=NetworkMapAndNotary,CN=corda.notary.validating" notary: "C=GB,L=London,O=Notary"
encumbrance: null encumbrance: null
constraint: constraint:
attachmentId: "F578320232CAB87BB1E919F3E5DB9D81B7346F9D7EA6D9155DC0F7BA8E472552" attachmentId: "F578320232CAB87BB1E919F3E5DB9D81B7346F9D7EA6D9155DC0F7BA8E472552"
@ -157,7 +157,7 @@ The vaults of PartyA and PartyB should both display the following output:
recordedTime: 1506415268.875000000 recordedTime: 1506415268.875000000
consumedTime: null consumedTime: null
status: "UNCONSUMED" status: "UNCONSUMED"
notary: "C=GB,L=London,O=NetworkMapAndNotary,CN=corda.notary.validating" notary: "C=GB,L=London,O=Notary"
lockId: null lockId: null
lockUpdateTime: 1506415269.548000000 lockUpdateTime: 1506415269.548000000
totalStatesAvailable: -1 totalStatesAvailable: -1

View File

@ -107,6 +107,9 @@ The current set of network parameters:
:modifiedTime: The time when the network parameters were last modified by the compatibility zone operator. :modifiedTime: The time when the network parameters were last modified by the compatibility zone operator.
:epoch: Version number of the network parameters. Starting from 1, this will always increment whenever any of the :epoch: Version number of the network parameters. Starting from 1, this will always increment whenever any of the
parameters change. parameters change.
:whitelistedContractImplementations: List of whitelisted versions of contract code.
For each contract class there is a list of hashes of the approved CorDapp jar versions containing that contract.
Read more about *Zone constraints* here :doc:`api-contract-constraints`
More parameters will be added in future releases to regulate things like allowed port numbers, how long a node can be More parameters will be added in future releases to regulate things like allowed port numbers, how long a node can be
offline before it is evicted from the zone, whether or not IPv6 connectivity is required for zone members, required offline before it is evicted from the zone, whether or not IPv6 connectivity is required for zone members, required

View File

@ -57,9 +57,9 @@ in its local network map cache. The node generates its own node-info file on sta
In addition to the network map, all the nodes on a network must use the same set of network parameters. These are a set In addition to the network map, all the nodes on a network must use the same set of network parameters. These are a set
of constants which guarantee interoperability between nodes. The HTTP network map distributes the network parameters of constants which guarantee interoperability between nodes. The HTTP network map distributes the network parameters
which the node downloads automatically. In the absence of this the network parameters must be generated locally. This can which the node downloads automatically. In the absence of this the network parameters must be generated locally. This can
be done with the network bootstrapper. This a tool that scans all the node configurations from a common directory to be done with the network bootstrapper. This is a tool that scans all the node configurations from a common directory to
generate the network parameters file which is copied to the nodes' directories. It also copies each node's node-info file 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. to every other node so that they can all transact with each other.
The bootstrapper tool can be built with the command: The bootstrapper tool can be built with the command:
@ -82,6 +82,56 @@ For example running the command on a directory containing these files :
Would generate directories containing three nodes: notary, partya and partyb. Would generate directories containing three nodes: notary, partya and partyb.
This tool only bootstraps a network. It cannot dynamically update if a new node needs to join the network or if an existing
one has changed something in their node-info, e.g. their P2P address. For this the new node-info file will need to be placed
in the other nodes' ``additional-node-infos`` directory. A simple way to do this is to use `rsync <https://en.wikipedia.org/wiki/Rsync>`_.
However, if it's known beforehand the set of nodes that will eventually the node folders can be pregenerated in the bootstrap
and only started when needed.
Whitelisting Contracts
~~~~~~~~~~~~~~~~~~~~~~
If you want to create a *Zone whitelist* (see :doc:`api-contract-constraints`), you can pass in a list of CorDapp jars:
``java -jar network-bootstrapper.jar <nodes-root-dir> <path-to-first-corDapp> <path-to-second-corDapp> ..``
The CorDapp jars will be hashed and scanned for ``Contract`` classes.
By default the tool would generate a file named ``whitelist.txt`` containing an entry for each contract with the hash of the jar.
For example:
.. sourcecode:: none
net.corda.finance.contracts.asset.Obligation:decd098666b9657314870e192ced0c3519c2c9d395507a238338f8d003929de8
net.corda.finance.contracts.asset.Cash:decd098666b9657314870e192ced0c3519c2c9d395507a238338f8d003929de9
These will be added to the ``NetworkParameters.whitelistedContractImplementations``. See :doc:`network-map`.
This means that by default the Network bootstrapper tool will whitelist all contracts found in all passed CorDapps.
In case there is a ``whitelist.txt`` file in the root dir already, the tool will append the new jar hashes or contracts to it.
The zone operator will maintain this whitelist file, and, using the tool, will append new versions of CorDapps to it.
.. warning::
- The zone operator must ensure that this file is *append only*.
- If the operator removes hashes from the list, all transactions pointing to that version will suddenly fail the constraint verification, and the entire chain is compromised.
- If a contract is removed from the whitelist, then all states created from that moment on will be constrained by the HashAttachmentConstraint.
Note: In future releases, we will provider a tamper-proof way of maintaining the contract whitelist.
For fine-grained control of constraints, in case multiple contracts live in the same jar, the tool reads from another file:
``exclude_whitelist.txt``, which contains a list of contracts that should not be whitelisted, and thus default to the very restrictive:
``HashAttachmentConstraint``
For example:
.. sourcecode:: none
net.corda.finance.contracts.asset.Cash
net.corda.finance.contracts.asset.CommercialPaper
Starting the nodes Starting the nodes
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~

View File

@ -16,7 +16,7 @@ The example CorDapp allows nodes to agree IOUs with each other, as long as they
We will deploy and run the CorDapp on four test nodes: We will deploy and run the CorDapp on four test nodes:
* **NetworkMapAndNotary**, which hosts a validating notary service * **Notary**, which hosts a validating notary service
* **PartyA** * **PartyA**
* **PartyB** * **PartyB**
* **PartyC** * **PartyC**
@ -245,7 +245,7 @@ For each node, the ``runnodes`` script creates a node tab/window:
Fri Jul 07 10:33:47 BST 2017>>> Fri Jul 07 10:33:47 BST 2017>>>
For every node except the network map/notary, the script also creates a webserver terminal tab/window: For every node except the notary, the script also creates a webserver terminal tab/window:
.. sourcecode:: none .. sourcecode:: none
@ -442,23 +442,27 @@ For more information on the client RPC interface and how to build an RPC client
Running nodes across machines Running nodes across machines
----------------------------- -----------------------------
The nodes can be split across machines and configured to communicate across the network. The nodes can be split across different machines and configured to communicate across the network.
After deploying the nodes, navigate to the build folder (``kotlin-source/build/nodes``) and move some of the individual After deploying the nodes, navigate to the build folder (``kotlin-source/build/nodes``) and for each node that needs to
node folders to a different machine (e.g. using a USB key). It is important that none of the nodes - including the be moved to another machine open its config file and change the Artemis messaging address to the IP address of the machine
network map/notary node - end up on more than one machine. Each computer should also have a copy of ``runnodes`` and where the node will run (e.g. ``p2pAddress="10.18.0.166:10006"``).
``runnodes.bat``.
These changes require new node-info files to be distributed amongst the nodes. Use the network bootstrapper tool
(see :doc:`setting-up-a-corda-network` for more information on this and how to built it) to update the files and have
them distributed locally.
``java -jar network-bootstrapper.jar kotlin-source/build/nodes``
Once that's done move the node folders to their designated machines (e.g. using a USB key). It is important that none of the
nodes - including the notary - end up on more than one machine. Each computer should also have a copy of ``runnodes``
and ``runnodes.bat``.
For example, you may end up with the following layout: For example, you may end up with the following layout:
* Machine 1: ``NetworkMapAndNotary``, ``PartyA``, ``runnodes``, ``runnodes.bat`` * Machine 1: ``Notary``, ``PartyA``, ``runnodes``, ``runnodes.bat``
* Machine 2: ``PartyB``, ``PartyC``, ``runnodes``, ``runnodes.bat`` * Machine 2: ``PartyB``, ``PartyC``, ``runnodes``, ``runnodes.bat``
You must now edit the configuration file for each node, including the network map/notary. Open each node's config file,
and make the following changes:
* Change the Artemis messaging address to the machine's IP address (e.g. ``p2pAddress="10.18.0.166:10006"``)
After starting each node, the nodes will be able to see one another and agree IOUs among themselves. After starting each node, the nodes will be able to see one another and agree IOUs among themselves.
Testing and debugging Testing and debugging

View File

@ -53,3 +53,13 @@ inline fun NodeInfo.sign(signer: (PublicKey, SerializedBytes<NodeInfo>) -> Digit
val signatures = owningKeys.map { signer(it, serialised) } val signatures = owningKeys.map { signer(it, serialised) }
return SignedNodeInfo(serialised, signatures) return SignedNodeInfo(serialised, signatures)
} }
/**
* A container for a [SignedNodeInfo] and its cached [NodeInfo].
*/
class NodeInfoAndSigned private constructor(val nodeInfo: NodeInfo, val signed: SignedNodeInfo) {
constructor(nodeInfo: NodeInfo, signer: (PublicKey, SerializedBytes<NodeInfo>) -> DigitalSignature) : this(nodeInfo, nodeInfo.sign(signer))
constructor(signedNodeInfo: SignedNodeInfo) : this(signedNodeInfo.verified(), signedNodeInfo)
operator fun component1(): NodeInfo = nodeInfo
operator fun component2(): SignedNodeInfo = signed
}

View File

@ -4,6 +4,7 @@ import com.google.common.hash.Hashing
import com.google.common.hash.HashingInputStream import com.google.common.hash.HashingInputStream
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import net.corda.cordform.CordformNode import net.corda.cordform.CordformNode
import net.corda.core.contracts.ContractClassName
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SecureHash.Companion.parse import net.corda.core.crypto.SecureHash.Companion.parse
import net.corda.core.identity.Party import net.corda.core.identity.Party
@ -77,9 +78,9 @@ class NetworkBootstrapper {
distributeNodeInfos(nodeDirs, nodeInfoFiles) distributeNodeInfos(nodeDirs, nodeInfoFiles)
println("Gathering notary identities") println("Gathering notary identities")
val notaryInfos = gatherNotaryInfos(nodeInfoFiles) val notaryInfos = gatherNotaryInfos(nodeInfoFiles)
println("Notary identities to be used in network-parameters file: ${notaryInfos.joinToString("; ") { it.prettyPrint() }}") println("Notary identities to be used in network parameters: ${notaryInfos.joinToString("; ") { it.prettyPrint() }}")
val mergedWhiteList = generateWhitelist(directory / WHITELIST_FILE_NAME, cordapps?.distinct()) val mergedWhiteList = generateWhitelist(directory / WHITELIST_FILE_NAME, cordapps?.distinct())
println("Updating whitelist.") println("Updating whitelist")
overwriteWhitelist(directory / WHITELIST_FILE_NAME, mergedWhiteList) overwriteWhitelist(directory / WHITELIST_FILE_NAME, mergedWhiteList)
installNetworkParameters(notaryInfos, nodeDirs, mergedWhiteList) installNetworkParameters(notaryInfos, nodeDirs, mergedWhiteList)
println("Bootstrapping complete!") println("Bootstrapping complete!")
@ -189,16 +190,18 @@ class NetworkBootstrapper {
private fun generateWhitelist(whitelistFile: Path, cordapps: List<String>?): Map<String, List<AttachmentId>> { private fun generateWhitelist(whitelistFile: Path, cordapps: List<String>?): Map<String, List<AttachmentId>> {
val existingWhitelist = if (whitelistFile.exists()) readContractWhitelist(whitelistFile) else emptyMap() val existingWhitelist = if (whitelistFile.exists()) readContractWhitelist(whitelistFile) else emptyMap()
println("Found existing whitelist: $existingWhitelist") println("Found existing whitelist:")
existingWhitelist.forEach { println(it.outputString()) }
val newWhiteList = cordapps?.flatMap { cordappJarPath -> val newWhiteList: Map<ContractClassName, AttachmentId> = cordapps?.flatMap { cordappJarPath ->
val jarHash = getJarHash(cordappJarPath) val jarHash = getJarHash(cordappJarPath)
scanJarForContracts(cordappJarPath).map { contract -> scanJarForContracts(cordappJarPath).map { contract ->
contract to jarHash contract to jarHash
} }
}?.toMap() ?: emptyMap() }?.toMap() ?: emptyMap()
println("Calculating whitelist for current cordapps: $newWhiteList") println("Calculating whitelist for current CorDapps:")
newWhiteList.forEach { (contract, attachment) -> println("$contract:$attachment") }
val merged = (newWhiteList.keys + existingWhitelist.keys).map { contractClassName -> val merged = (newWhiteList.keys + existingWhitelist.keys).map { contractClassName ->
val existing = existingWhitelist[contractClassName] ?: emptyList() val existing = existingWhitelist[contractClassName] ?: emptyList()
@ -206,16 +209,15 @@ class NetworkBootstrapper {
contractClassName to (if (newHash == null || newHash in existing) existing else existing + newHash) contractClassName to (if (newHash == null || newHash in existing) existing else existing + newHash)
}.toMap() }.toMap()
println("Final whitelist: $merged") println("Final whitelist:")
merged.forEach { println(it.outputString()) }
return merged return merged
} }
private fun overwriteWhitelist(whitelistFile: Path, mergedWhiteList: Map<String, List<AttachmentId>>) { private fun overwriteWhitelist(whitelistFile: Path, mergedWhiteList: Map<String, List<AttachmentId>>) {
PrintStream(whitelistFile.toFile().outputStream()).use { out -> PrintStream(whitelistFile.toFile().outputStream()).use { out ->
mergedWhiteList.forEach { (contract, attachments )-> mergedWhiteList.forEach { out.println(it.outputString()) }
out.println("${contract}:${attachments.joinToString(",")}")
}
} }
} }
@ -235,15 +237,17 @@ class NetworkBootstrapper {
private fun NodeInfo.notaryIdentity(): Party { private fun NodeInfo.notaryIdentity(): Party {
return when (legalIdentities.size) { return when (legalIdentities.size) {
// Single node notaries have just one identity like all other nodes. This identity is the notary identity // Single node notaries have just one identity like all other nodes. This identity is the notary identity
1 -> legalIdentities[0] 1 -> legalIdentities[0]
// Nodes which are part of a distributed notary have a second identity which is the composite identity of the // Nodes which are part of a distributed notary have a second identity which is the composite identity of the
// cluster and is shared by all the other members. This is the notary identity. // cluster and is shared by all the other members. This is the notary identity.
2 -> legalIdentities[1] 2 -> legalIdentities[1]
else -> throw IllegalArgumentException("Not sure how to get the notary identity in this scenerio: $this") else -> throw IllegalArgumentException("Not sure how to get the notary identity in this scenerio: $this")
} }
} }
private fun Map.Entry<ContractClassName, List<AttachmentId>>.outputString() = "$key:${value.joinToString(",")}"
// We need to to set serialization env, because generation of parameters is run from Cordform. // We need to to set serialization env, because generation of parameters is run from Cordform.
// KryoServerSerializationScheme is not accessible from nodeapi. // KryoServerSerializationScheme is not accessible from nodeapi.
private fun initialiseSerialization() { private fun initialiseSerialization() {

View File

@ -7,7 +7,7 @@ import net.corda.core.internal.createDirectories
import net.corda.core.internal.div import net.corda.core.internal.div
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.services.KeyManagementService import net.corda.core.node.services.KeyManagementService
import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.NodeInfoAndSigned
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier import net.corda.nodeapi.internal.network.NodeInfoFilesCopier
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.SerializationEnvironmentRule
@ -39,8 +39,7 @@ class NodeInfoWatcherTest {
private val scheduler = TestScheduler() private val scheduler = TestScheduler()
private val testSubscriber = TestSubscriber<NodeInfo>() private val testSubscriber = TestSubscriber<NodeInfo>()
private lateinit var nodeInfo: NodeInfo private lateinit var nodeInfoAndSigned: NodeInfoAndSigned
private lateinit var signedNodeInfo: SignedNodeInfo
private lateinit var nodeInfoPath: Path private lateinit var nodeInfoPath: Path
private lateinit var keyManagementService: KeyManagementService private lateinit var keyManagementService: KeyManagementService
@ -49,9 +48,7 @@ class NodeInfoWatcherTest {
@Before @Before
fun start() { fun start() {
val nodeInfoAndSigned = createNodeInfoAndSigned(ALICE_NAME) nodeInfoAndSigned = createNodeInfoAndSigned(ALICE_NAME)
nodeInfo = nodeInfoAndSigned.first
signedNodeInfo = nodeInfoAndSigned.second
val identityService = makeTestIdentityService() val identityService = makeTestIdentityService()
keyManagementService = MockKeyManagementService(identityService) keyManagementService = MockKeyManagementService(identityService)
nodeInfoWatcher = NodeInfoWatcher(tempFolder.root.toPath(), scheduler) nodeInfoWatcher = NodeInfoWatcher(tempFolder.root.toPath(), scheduler)
@ -62,7 +59,7 @@ class NodeInfoWatcherTest {
fun `save a NodeInfo`() { fun `save a NodeInfo`() {
assertEquals(0, assertEquals(0,
tempFolder.root.list().filter { it.startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) }.size) tempFolder.root.list().filter { it.startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) }.size)
NodeInfoWatcher.saveToFile(tempFolder.root.toPath(), signedNodeInfo) NodeInfoWatcher.saveToFile(tempFolder.root.toPath(), nodeInfoAndSigned)
val nodeInfoFiles = tempFolder.root.list().filter { it.startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) } val nodeInfoFiles = tempFolder.root.list().filter { it.startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) }
assertEquals(1, nodeInfoFiles.size) assertEquals(1, nodeInfoFiles.size)
@ -76,8 +73,8 @@ class NodeInfoWatcherTest {
@Test @Test
fun `save a NodeInfo to JimFs`() { fun `save a NodeInfo to JimFs`() {
val jimFs = Jimfs.newFileSystem(Configuration.unix()) val jimFs = Jimfs.newFileSystem(Configuration.unix())
val jimFolder = jimFs.getPath("/nodeInfo") val jimFolder = jimFs.getPath("/nodeInfo").createDirectories()
NodeInfoWatcher.saveToFile(jimFolder, signedNodeInfo) NodeInfoWatcher.saveToFile(jimFolder, nodeInfoAndSigned)
} }
@Test @Test
@ -104,7 +101,7 @@ class NodeInfoWatcherTest {
try { try {
val readNodes = testSubscriber.onNextEvents.distinct() val readNodes = testSubscriber.onNextEvents.distinct()
assertEquals(1, readNodes.size) assertEquals(1, readNodes.size)
assertEquals(nodeInfo, readNodes.first()) assertEquals(nodeInfoAndSigned.nodeInfo, readNodes.first())
} finally { } finally {
subscription.unsubscribe() subscription.unsubscribe()
} }
@ -129,7 +126,7 @@ class NodeInfoWatcherTest {
testSubscriber.awaitValueCount(1, 5, TimeUnit.SECONDS) testSubscriber.awaitValueCount(1, 5, TimeUnit.SECONDS)
// The same folder can be reported more than once, so take unique values. // The same folder can be reported more than once, so take unique values.
val readNodes = testSubscriber.onNextEvents.distinct() val readNodes = testSubscriber.onNextEvents.distinct()
assertEquals(nodeInfo, readNodes.first()) assertEquals(nodeInfoAndSigned.nodeInfo, readNodes.first())
} finally { } finally {
subscription.unsubscribe() subscription.unsubscribe()
} }
@ -141,6 +138,6 @@ class NodeInfoWatcherTest {
// Write a nodeInfo under the right path. // Write a nodeInfo under the right path.
private fun createNodeInfoFileInPath() { private fun createNodeInfoFileInPath() {
NodeInfoWatcher.saveToFile(nodeInfoPath, signedNodeInfo) NodeInfoWatcher.saveToFile(nodeInfoPath, nodeInfoAndSigned)
} }
} }

View File

@ -61,9 +61,13 @@ import net.corda.node.utilities.AffinityExecutor
import net.corda.node.utilities.JVMAgentRegistry import net.corda.node.utilities.JVMAgentRegistry
import net.corda.node.utilities.NodeBuildProperties import net.corda.node.utilities.NodeBuildProperties
import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.DevIdentityGenerator
import net.corda.nodeapi.internal.NodeInfoAndSigned
import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.persistence.* import net.corda.nodeapi.internal.persistence.*
import net.corda.nodeapi.internal.sign import net.corda.nodeapi.internal.sign
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.HibernateConfiguration
import net.corda.nodeapi.internal.storeLegalIdentity import net.corda.nodeapi.internal.storeLegalIdentity
import org.apache.activemq.artemis.utils.ReusableLatch import org.apache.activemq.artemis.utils.ReusableLatch
import org.hibernate.type.descriptor.java.JavaTypeDescriptorRegistry import org.hibernate.type.descriptor.java.JavaTypeDescriptorRegistry
@ -181,11 +185,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
val persistentNetworkMapCache = PersistentNetworkMapCache(database, notaries = emptyList()) val persistentNetworkMapCache = PersistentNetworkMapCache(database, notaries = emptyList())
persistentNetworkMapCache.start() persistentNetworkMapCache.start()
val (keyPairs, nodeInfo) = initNodeInfo(persistentNetworkMapCache, identity, identityKeyPair) val (keyPairs, nodeInfo) = initNodeInfo(persistentNetworkMapCache, identity, identityKeyPair)
val signedNodeInfo = nodeInfo.sign { publicKey, serialised -> val nodeInfoAndSigned = NodeInfoAndSigned(nodeInfo) { publicKey, serialised ->
val privateKey = keyPairs.single { it.public == publicKey }.private val privateKey = keyPairs.single { it.public == publicKey }.private
privateKey.sign(serialised.bytes) privateKey.sign(serialised.bytes)
} }
NodeInfoWatcher.saveToFile(configuration.baseDirectory, signedNodeInfo) NodeInfoWatcher.saveToFile(configuration.baseDirectory, nodeInfoAndSigned)
nodeInfo nodeInfo
} }
} }
@ -265,11 +269,12 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
configuration.baseDirectory) configuration.baseDirectory)
runOnStop += networkMapUpdater::close runOnStop += networkMapUpdater::close
networkMapUpdater.updateNodeInfo(services.myInfo) { log.info("Node-info for this node: ${services.myInfo}")
it.sign { publicKey, serialised ->
services.keyManagementService.sign(serialised.bytes, publicKey).withoutKey() val nodeInfoAndSigned = NodeInfoAndSigned(services.myInfo) { publicKey, serialised ->
} services.keyManagementService.sign(serialised.bytes, publicKey).withoutKey()
} }
networkMapUpdater.updateNodeInfo(nodeInfoAndSigned)
networkMapUpdater.subscribeToNetworkMap() networkMapUpdater.subscribeToNetworkMap()
// If we successfully loaded network data from database, we set this future to Unit. // If we successfully loaded network data from database, we set this future to Unit.

View File

@ -168,7 +168,7 @@ open class Node(configuration: NodeConfiguration,
val advertisedAddress = info.addresses[0] val advertisedAddress = info.addresses[0]
bridgeControlListener = BridgeControlListener(configuration, serverAddress, networkParameters.maxMessageSize) bridgeControlListener = BridgeControlListener(configuration, serverAddress, networkParameters.maxMessageSize)
printBasicNodeInfo("Incoming connection address", advertisedAddress.toString()) printBasicNodeInfo("Advertised P2P messaging addresses", info.addresses.joinToString())
val rpcServerConfiguration = RPCServerConfiguration.default.copy( val rpcServerConfiguration = RPCServerConfiguration.default.copy(
rpcThreadPoolSize = configuration.enterpriseConfiguration.tuning.rpcThreadPoolSize rpcThreadPoolSize = configuration.enterpriseConfiguration.tuning.rpcThreadPoolSize

View File

@ -120,7 +120,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
} }
// Config driven switch between legacy CORE bridges and the newer AMQP protocol bridges. // Config driven switch between legacy CORE bridges and the newer AMQP protocol bridges.
activeMQServer.start() activeMQServer.start()
Node.printBasicNodeInfo("Listening on port", p2pPort.toString()) log.info("P2P messaging server listening on port $p2pPort")
} }
private fun createArtemisConfig() = SecureArtemisConfiguration().apply { private fun createArtemisConfig() = SecureArtemisConfiguration().apply {

View File

@ -7,12 +7,12 @@ import net.corda.core.internal.copyTo
import net.corda.core.internal.div import net.corda.core.internal.div
import net.corda.core.messaging.DataFeed import net.corda.core.messaging.DataFeed
import net.corda.core.messaging.ParametersUpdateInfo import net.corda.core.messaging.ParametersUpdateInfo
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.minutes import net.corda.core.utilities.minutes
import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.node.utilities.NamedThreadFactory import net.corda.node.utilities.NamedThreadFactory
import net.corda.nodeapi.internal.NodeInfoAndSigned
import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME
import net.corda.nodeapi.internal.network.ParametersUpdate import net.corda.nodeapi.internal.network.ParametersUpdate
@ -54,18 +54,19 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
return DataFeed(currentUpdateInfo, parametersUpdatesTrack) return DataFeed(currentUpdateInfo, parametersUpdatesTrack)
} }
fun updateNodeInfo(newInfo: NodeInfo, signer: (NodeInfo) -> SignedNodeInfo) { fun updateNodeInfo(nodeInfoAndSigned: NodeInfoAndSigned) {
val oldInfo = networkMapCache.getNodeByLegalIdentity(newInfo.legalIdentities.first()) // TODO We've already done this lookup and check in AbstractNode.initNodeInfo
val oldNodeInfo = networkMapCache.getNodeByLegalIdentity(nodeInfoAndSigned.nodeInfo.legalIdentities[0])
// Compare node info without timestamp. // Compare node info without timestamp.
if (newInfo.copy(serial = 0L) == oldInfo?.copy(serial = 0L)) return if (nodeInfoAndSigned.nodeInfo.copy(serial = 0L) == oldNodeInfo?.copy(serial = 0L)) return
logger.info("Node-info has changed so submitting update. Old node-info was $oldNodeInfo")
// Only publish and write to disk if there are changes to the node info. // Only publish and write to disk if there are changes to the node info.
val signedNodeInfo = signer(newInfo) networkMapCache.addNode(nodeInfoAndSigned.nodeInfo)
networkMapCache.addNode(newInfo) fileWatcher.saveToFile(nodeInfoAndSigned)
fileWatcher.saveToFile(signedNodeInfo)
if (networkMapClient != null) { if (networkMapClient != null) {
tryPublishNodeInfoAsync(signedNodeInfo, networkMapClient) tryPublishNodeInfoAsync(nodeInfoAndSigned.signed, networkMapClient)
} }
} }

View File

@ -4,17 +4,26 @@ import net.corda.cordform.CordformNode
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.internal.* import net.corda.core.internal.*
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
import net.corda.core.serialization.internal._contextSerializationEnv
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
import net.corda.nodeapi.internal.NodeInfoAndSigned
import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier import net.corda.nodeapi.internal.network.NodeInfoFilesCopier
import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme
import rx.Observable import rx.Observable
import rx.Scheduler import rx.Scheduler
import java.io.IOException import java.io.IOException
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
import java.time.Duration import java.time.Duration
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.stream.Stream
import kotlin.streams.toList import kotlin.streams.toList
/** /**
@ -31,34 +40,29 @@ import kotlin.streams.toList
class NodeInfoWatcher(private val nodePath: Path, class NodeInfoWatcher(private val nodePath: Path,
private val scheduler: Scheduler, private val scheduler: Scheduler,
private val pollInterval: Duration = 5.seconds) { private val pollInterval: Duration = 5.seconds) {
private val nodeInfoDirectory = nodePath / CordformNode.NODE_INFO_DIRECTORY
private val processedNodeInfoFiles = mutableSetOf<Path>()
private val _processedNodeInfoHashes = mutableSetOf<SecureHash>()
val processedNodeInfoHashes: Set<SecureHash> get() = _processedNodeInfoHashes.toSet()
companion object { companion object {
private val logger = contextLogger() private val logger = contextLogger()
/**
* Saves the given [NodeInfo] to a path. // TODO This method doesn't belong in this class
* The node is 'encoded' as a SignedNodeInfo, signed with the owning key of its first identity. fun saveToFile(path: Path, nodeInfoAndSigned: NodeInfoAndSigned) {
* The name of the written file will be "nodeInfo-" followed by the hash of the content. The hash in the filename // By using the hash of the node's first name we ensure:
* is used so that one can freely copy these files without fearing to overwrite another one. // 1) node info files for the same node map to the same filename and thus avoid having duplicate files for
* // the same node
* @param path the path where to write the file, if non-existent it will be created. // 2) avoid having to deal with characters in the X.500 name which are incompatible with the local filesystem
* @param signedNodeInfo the signed NodeInfo. val fileNameHash = nodeInfoAndSigned.nodeInfo.legalIdentities[0].name.serialize().hash
*/ nodeInfoAndSigned
fun saveToFile(path: Path, signedNodeInfo: SignedNodeInfo) { .signed
try { .serialize()
path.createDirectories() .open()
signedNodeInfo.serialize() .copyTo(path / "${NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX}$fileNameHash", REPLACE_EXISTING)
.open()
.copyTo(path / "${NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX}${signedNodeInfo.raw.hash}")
} catch (e: Exception) {
logger.warn("Couldn't write node info to file", e)
}
} }
} }
private val nodeInfoDirectory = nodePath / CordformNode.NODE_INFO_DIRECTORY
private val _processedNodeInfoHashes = HashSet<SecureHash>()
val processedNodeInfoHashes: Set<SecureHash> get() = _processedNodeInfoHashes
init { init {
require(pollInterval >= 5.seconds) { "Poll interval must be 5 seconds or longer." } require(pollInterval >= 5.seconds) { "Poll interval must be 5 seconds or longer." }
if (!nodeInfoDirectory.isDirectory()) { if (!nodeInfoDirectory.isDirectory()) {
@ -84,7 +88,10 @@ class NodeInfoWatcher(private val nodePath: Path,
.flatMapIterable { loadFromDirectory() } .flatMapIterable { loadFromDirectory() }
} }
fun saveToFile(signedNodeInfo: SignedNodeInfo) = Companion.saveToFile(nodePath, signedNodeInfo) // TODO This method doesn't belong in this class
fun saveToFile(nodeInfoAndSigned: NodeInfoAndSigned) {
return Companion.saveToFile(nodePath, nodeInfoAndSigned)
}
/** /**
* Loads all the files contained in a given path and returns the deserialized [NodeInfo]s. * Loads all the files contained in a given path and returns the deserialized [NodeInfo]s.
@ -97,16 +104,15 @@ class NodeInfoWatcher(private val nodePath: Path,
return emptyList() return emptyList()
} }
val result = nodeInfoDirectory.list { paths -> val result = nodeInfoDirectory.list { paths ->
paths.filter { it !in processedNodeInfoFiles } paths
.filter { it.isRegularFile() } .filter { it.isRegularFile() }
.map { path -> .flatMap { path ->
processFile(path)?.apply { val nodeInfo = processFile(path)?.let {
processedNodeInfoFiles.add(path) if (_processedNodeInfoHashes.add(it.signed.raw.hash)) it.nodeInfo else null
_processedNodeInfoHashes.add(this.serialize().hash)
} }
if (nodeInfo != null) Stream.of(nodeInfo) else Stream.empty()
} }
.toList() .toList()
.filterNotNull()
} }
if (result.isNotEmpty()) { if (result.isNotEmpty()) {
logger.info("Successfully read ${result.size} NodeInfo files from disk.") logger.info("Successfully read ${result.size} NodeInfo files from disk.")
@ -114,14 +120,25 @@ class NodeInfoWatcher(private val nodePath: Path,
return result return result
} }
private fun processFile(file: Path): NodeInfo? { private fun processFile(file: Path): NodeInfoAndSigned? {
return try { return try {
logger.info("Reading NodeInfo from file: $file") logger.info("Reading NodeInfo from file: $file")
val signedData = file.readObject<SignedNodeInfo>() val signedNodeInfo = file.readObject<SignedNodeInfo>()
signedData.verified() NodeInfoAndSigned(signedNodeInfo)
} catch (e: Exception) { } catch (e: Exception) {
logger.warn("Exception parsing NodeInfo from file. $file", e) logger.warn("Exception parsing NodeInfo from file. $file", e)
null null
} }
} }
} }
// TODO Remove this once we have a tool that can read AMQP serialised files
fun main(args: Array<String>) {
_contextSerializationEnv.set(SerializationEnvironmentImpl(
SerializationFactoryImpl().apply {
registerScheme(AMQPServerSerializationScheme())
},
AMQP_P2P_CONTEXT)
)
println(Paths.get(args[0]).readObject<SignedNodeInfo>().verified())
}

View File

@ -18,6 +18,7 @@ import net.corda.core.node.NodeInfo
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.utilities.millis import net.corda.core.utilities.millis
import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.nodeapi.internal.NodeInfoAndSigned
import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.createDevNetworkMapCa import net.corda.nodeapi.internal.createDevNetworkMapCa
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
@ -68,33 +69,33 @@ class NetworkMapUpdaterTest {
fun `publish node info`() { fun `publish node info`() {
nodeInfoBuilder.addIdentity(ALICE_NAME) nodeInfoBuilder.addIdentity(ALICE_NAME)
val (nodeInfo1, signedNodeInfo1) = nodeInfoBuilder.buildWithSigned() val nodeInfo1AndSigned = nodeInfoBuilder.buildWithSigned()
val (sameNodeInfoDifferentTime, signedSameNodeInfoDifferentTime) = nodeInfoBuilder.buildWithSigned(serial = System.currentTimeMillis()) val sameNodeInfoDifferentTimeAndSigned = nodeInfoBuilder.buildWithSigned(serial = System.currentTimeMillis())
// Publish node info for the first time. // Publish node info for the first time.
updater.updateNodeInfo(nodeInfo1) { signedNodeInfo1 } updater.updateNodeInfo(nodeInfo1AndSigned)
// Sleep as publish is asynchronous. // Sleep as publish is asynchronous.
// TODO: Remove sleep in unit test // TODO: Remove sleep in unit test
Thread.sleep(2L * cacheExpiryMs) Thread.sleep(2L * cacheExpiryMs)
verify(networkMapClient, times(1)).publish(any()) verify(networkMapClient, times(1)).publish(any())
networkMapCache.addNode(nodeInfo1) networkMapCache.addNode(nodeInfo1AndSigned.nodeInfo)
// Publish the same node info, but with different serial. // Publish the same node info, but with different serial.
updater.updateNodeInfo(sameNodeInfoDifferentTime) { signedSameNodeInfoDifferentTime } updater.updateNodeInfo(sameNodeInfoDifferentTimeAndSigned)
// TODO: Remove sleep in unit test. // TODO: Remove sleep in unit test.
Thread.sleep(2L * cacheExpiryMs) Thread.sleep(2L * cacheExpiryMs)
// Same node info should not publish twice // Same node info should not publish twice
verify(networkMapClient, times(0)).publish(signedSameNodeInfoDifferentTime) verify(networkMapClient, times(0)).publish(sameNodeInfoDifferentTimeAndSigned.signed)
val (differentNodeInfo, signedDifferentNodeInfo) = createNodeInfoAndSigned("Bob") val differentNodeInfoAndSigned = createNodeInfoAndSigned("Bob")
// Publish different node info. // Publish different node info.
updater.updateNodeInfo(differentNodeInfo) { signedDifferentNodeInfo } updater.updateNodeInfo(differentNodeInfoAndSigned)
// TODO: Remove sleep in unit test. // TODO: Remove sleep in unit test.
Thread.sleep(200) Thread.sleep(200)
verify(networkMapClient, times(1)).publish(signedDifferentNodeInfo) verify(networkMapClient, times(1)).publish(differentNodeInfoAndSigned.signed)
} }
@Test @Test
@ -103,7 +104,7 @@ class NetworkMapUpdaterTest {
val (nodeInfo2, signedNodeInfo2) = createNodeInfoAndSigned("Info 2") val (nodeInfo2, signedNodeInfo2) = createNodeInfoAndSigned("Info 2")
val (nodeInfo3, signedNodeInfo3) = createNodeInfoAndSigned("Info 3") val (nodeInfo3, signedNodeInfo3) = createNodeInfoAndSigned("Info 3")
val (nodeInfo4, signedNodeInfo4) = createNodeInfoAndSigned("Info 4") val (nodeInfo4, signedNodeInfo4) = createNodeInfoAndSigned("Info 4")
val (fileNodeInfo, signedFileNodeInfo) = createNodeInfoAndSigned("Info from file") val fileNodeInfoAndSigned = createNodeInfoAndSigned("Info from file")
// Test adding new node. // Test adding new node.
networkMapClient.publish(signedNodeInfo1) networkMapClient.publish(signedNodeInfo1)
@ -119,7 +120,7 @@ class NetworkMapUpdaterTest {
verify(networkMapCache, times(1)).addNode(nodeInfo1) verify(networkMapCache, times(1)).addNode(nodeInfo1)
verify(networkMapCache, times(1)).addNode(nodeInfo2) verify(networkMapCache, times(1)).addNode(nodeInfo2)
NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, signedFileNodeInfo) NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, fileNodeInfoAndSigned)
networkMapClient.publish(signedNodeInfo3) networkMapClient.publish(signedNodeInfo3)
networkMapClient.publish(signedNodeInfo4) networkMapClient.publish(signedNodeInfo4)
@ -131,7 +132,7 @@ class NetworkMapUpdaterTest {
verify(networkMapCache, times(5)).addNode(any()) verify(networkMapCache, times(5)).addNode(any())
verify(networkMapCache, times(1)).addNode(nodeInfo3) verify(networkMapCache, times(1)).addNode(nodeInfo3)
verify(networkMapCache, times(1)).addNode(nodeInfo4) verify(networkMapCache, times(1)).addNode(nodeInfo4)
verify(networkMapCache, times(1)).addNode(fileNodeInfo) verify(networkMapCache, times(1)).addNode(fileNodeInfoAndSigned.nodeInfo)
} }
@Test @Test
@ -140,10 +141,10 @@ class NetworkMapUpdaterTest {
val (nodeInfo2, signedNodeInfo2) = createNodeInfoAndSigned("Info 2") val (nodeInfo2, signedNodeInfo2) = createNodeInfoAndSigned("Info 2")
val (nodeInfo3, signedNodeInfo3) = createNodeInfoAndSigned("Info 3") val (nodeInfo3, signedNodeInfo3) = createNodeInfoAndSigned("Info 3")
val (nodeInfo4, signedNodeInfo4) = createNodeInfoAndSigned("Info 4") val (nodeInfo4, signedNodeInfo4) = createNodeInfoAndSigned("Info 4")
val (fileNodeInfo, signedFileNodeInfo) = createNodeInfoAndSigned("Info from file") val fileNodeInfoAndSigned = createNodeInfoAndSigned("Info from file")
// Add all nodes. // Add all nodes.
NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, signedFileNodeInfo) NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, fileNodeInfoAndSigned)
networkMapClient.publish(signedNodeInfo1) networkMapClient.publish(signedNodeInfo1)
networkMapClient.publish(signedNodeInfo2) networkMapClient.publish(signedNodeInfo2)
networkMapClient.publish(signedNodeInfo3) networkMapClient.publish(signedNodeInfo3)
@ -157,7 +158,7 @@ class NetworkMapUpdaterTest {
// 4 node info from network map, and 1 from file. // 4 node info from network map, and 1 from file.
assertThat(nodeInfoMap).hasSize(4) assertThat(nodeInfoMap).hasSize(4)
verify(networkMapCache, times(5)).addNode(any()) verify(networkMapCache, times(5)).addNode(any())
verify(networkMapCache, times(1)).addNode(fileNodeInfo) verify(networkMapCache, times(1)).addNode(fileNodeInfoAndSigned.nodeInfo)
// Test remove node. // Test remove node.
nodeInfoMap.clear() nodeInfoMap.clear()
@ -170,25 +171,25 @@ class NetworkMapUpdaterTest {
verify(networkMapCache, times(1)).removeNode(nodeInfo4) verify(networkMapCache, times(1)).removeNode(nodeInfo4)
// Node info from file should not be deleted // Node info from file should not be deleted
assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfo.serialize().hash) assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfoAndSigned.nodeInfo.serialize().hash)
} }
@Test @Test
fun `receive node infos from directory, without a network map`() { fun `receive node infos from directory, without a network map`() {
val (fileNodeInfo, signedFileNodeInfo) = createNodeInfoAndSigned("Info from file") val fileNodeInfoAndSigned = createNodeInfoAndSigned("Info from file")
// Not subscribed yet. // Not subscribed yet.
verify(networkMapCache, times(0)).addNode(any()) verify(networkMapCache, times(0)).addNode(any())
updater.subscribeToNetworkMap() updater.subscribeToNetworkMap()
NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, signedFileNodeInfo) NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, fileNodeInfoAndSigned)
scheduler.advanceTimeBy(10, TimeUnit.SECONDS) scheduler.advanceTimeBy(10, TimeUnit.SECONDS)
verify(networkMapCache, times(1)).addNode(any()) verify(networkMapCache, times(1)).addNode(any())
verify(networkMapCache, times(1)).addNode(fileNodeInfo) verify(networkMapCache, times(1)).addNode(fileNodeInfoAndSigned.nodeInfo)
assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfo.serialize().hash) assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfoAndSigned.nodeInfo.serialize().hash)
} }
@Test @Test
@ -275,7 +276,7 @@ class NetworkMapUpdaterTest {
} }
} }
private fun createNodeInfoAndSigned(org: String): Pair<NodeInfo, SignedNodeInfo> { private fun createNodeInfoAndSigned(org: String): NodeInfoAndSigned {
return createNodeInfoAndSigned(CordaX500Name(org, "London", "GB")) return createNodeInfoAndSigned(CordaX500Name(org, "London", "GB"))
} }
} }

View File

@ -7,6 +7,7 @@ import net.corda.core.identity.PartyAndCertificate
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.nodeapi.internal.NodeInfoAndSigned
import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.createDevNodeCa import net.corda.nodeapi.internal.createDevNodeCa
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
@ -47,10 +48,11 @@ class TestNodeInfoBuilder(private val intermediateAndRoot: Pair<CertificateAndKe
) )
} }
fun buildWithSigned(serial: Long = 1, platformVersion: Int = 1): Pair<NodeInfo, SignedNodeInfo> { fun buildWithSigned(serial: Long = 1, platformVersion: Int = 1): NodeInfoAndSigned {
val nodeInfo = build(serial, platformVersion) val nodeInfo = build(serial, platformVersion)
val privateKeys = identitiesAndPrivateKeys.map { it.second } return NodeInfoAndSigned(nodeInfo) { publicKey, serialised ->
return Pair(nodeInfo, nodeInfo.signWith(privateKeys)) identitiesAndPrivateKeys.first { it.first.owningKey == publicKey }.second.sign(serialised.bytes)
}
} }
fun reset() { fun reset() {
@ -58,7 +60,7 @@ class TestNodeInfoBuilder(private val intermediateAndRoot: Pair<CertificateAndKe
} }
} }
fun createNodeInfoAndSigned(vararg names: CordaX500Name, serial: Long = 1, platformVersion: Int = 1): Pair<NodeInfo, SignedNodeInfo> { fun createNodeInfoAndSigned(vararg names: CordaX500Name, serial: Long = 1, platformVersion: Int = 1): NodeInfoAndSigned {
val nodeInfoBuilder = TestNodeInfoBuilder() val nodeInfoBuilder = TestNodeInfoBuilder()
names.forEach { nodeInfoBuilder.addIdentity(it) } names.forEach { nodeInfoBuilder.addIdentity(it) }
return nodeInfoBuilder.buildWithSigned(serial, platformVersion) return nodeInfoBuilder.buildWithSigned(serial, platformVersion)