From 06af213cdb39de5f1c6be4897e96c7e9819f8f95 Mon Sep 17 00:00:00 2001 From: Tudor Malene Date: Thu, 1 Mar 2018 17:37:33 +0000 Subject: [PATCH 1/4] CORDA-1050 docs on whitelisting CorDapps (#2671) * CORDA-1050 docs on whitelisting CorDapps * Addressed code review notes * Addressed code review notes --- docs/source/network-map.rst | 3 ++ docs/source/setting-up-a-corda-network.rst | 45 ++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/docs/source/network-map.rst b/docs/source/network-map.rst index d5eb8f03ff..0aac2ee0ba 100644 --- a/docs/source/network-map.rst +++ b/docs/source/network-map.rst @@ -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. :epoch: Version number of the network parameters. Starting from 1, this will always increment whenever any of the 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 offline before it is evicted from the zone, whether or not IPv6 connectivity is required for zone members, required diff --git a/docs/source/setting-up-a-corda-network.rst b/docs/source/setting-up-a-corda-network.rst index 1f1e65a966..fad6eb9898 100644 --- a/docs/source/setting-up-a-corda-network.rst +++ b/docs/source/setting-up-a-corda-network.rst @@ -82,6 +82,51 @@ For example running the command on a directory containing these files : Would generate directories containing three nodes: notary, partya and partyb. + +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 ..`` + +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 ~~~~~~~~~~~~~~~~~~ From 14cafee66e2779a7584bd84ffd644808747c193d Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Thu, 1 Mar 2018 19:03:54 +0100 Subject: [PATCH 2/4] Fix the serialisability of TransactionVerificationException and subtypes. (#2689) Add KDocs for the exceptions in this file, and deprecate exceptions that aren't actually used. --- .../TransactionVerificationException.kt | 123 +++++++++++++++--- .../core/transactions/LedgerTransaction.kt | 5 +- ...VerificationExceptionSerialisationTests.kt | 108 +++++++++++++++ 3 files changed, 212 insertions(+), 24 deletions(-) create mode 100644 core/src/test/kotlin/net/corda/core/contracts/TransactionVerificationExceptionSerialisationTests.kt diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt index 9f706479e1..71926f9e0a 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt @@ -7,48 +7,129 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.NonEmptySet 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") + +/** + * 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") +/** + * 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?) : 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) + /** + * 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) : 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) - 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) - 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) - : 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) : 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) : 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) : 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 - } } diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index 85fc1d0b81..09b3b7c98c 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -116,10 +116,9 @@ data class LedgerTransaction @JvmOverloads constructor( * If any contract fails to verify, the whole transaction is considered to be invalid. */ private fun verifyContracts() { - for (contractEntry in contracts.entries) { - val result = contractEntry.value + for ((key, result) in contracts) { 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 -> { val contract = result.value try { diff --git a/core/src/test/kotlin/net/corda/core/contracts/TransactionVerificationExceptionSerialisationTests.kt b/core/src/test/kotlin/net/corda/core/contracts/TransactionVerificationExceptionSerialisationTests.kt new file mode 100644 index 0000000000..721a78ec6f --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/contracts/TransactionVerificationExceptionSerialisationTests.kt @@ -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) + } +} \ No newline at end of file From 8616f245235fec3126f749044b2b8a044830b5b9 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Thu, 1 Mar 2018 21:24:10 +0000 Subject: [PATCH 3/4] CORDA-1048: Making it simpler to move an existing local deployment of nodes to across different machines. (#2672) This was achieved by having the hash in the node-info file to be just of the node's X.500 name. This also solves existing duplicate node-info file issues that we've been having. Also updated the docsite. --- docs/source/corda-configuration-file.rst | 2 +- docs/source/generating-a-node.rst | 10 +-- docs/source/hello-world-running.rst | 6 +- docs/source/setting-up-a-corda-network.rst | 9 +- docs/source/tutorial-cordapp.rst | 30 ++++--- .../corda/nodeapi/internal/SignedNodeInfo.kt | 10 +++ .../internal/network/NetworkBootstrapper.kt | 28 ++++--- .../services/network/NodeInfoWatcherTest.kt | 21 ++--- .../net/corda/node/internal/AbstractNode.kt | 15 ++-- .../services/network/NetworkMapUpdater.kt | 17 ++-- .../node/services/network/NodeInfoWatcher.kt | 83 +++++++++++-------- .../services/network/NetworkMapUpdaterTest.kt | 43 +++++----- .../testing/internal/TestNodeInfoBuilder.kt | 10 ++- 13 files changed, 162 insertions(+), 122 deletions(-) diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index a4deaac21e..01b615c417 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -71,7 +71,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. .. 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, the node will try to auto-discover its public one. diff --git a/docs/source/generating-a-node.rst b/docs/source/generating-a-node.rst index c506e2aab4..6c9d901235 100644 --- a/docs/source/generating-a-node.rst +++ b/docs/source/generating-a-node.rst @@ -1,8 +1,6 @@ Creating nodes locally ====================== -.. contents:: - Node 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 ----------------- 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 -`Kotlin CorDapp Template `_: +nodes for testing and demos. Here is an example ``Cordform`` task called ``deployNodes`` that creates three nodes, defined +in the `Kotlin CorDapp Template `_: .. 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! Specifying a custom webserver ------------------------------ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 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 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. Running deployNodes -------------------- +~~~~~~~~~~~~~~~~~~~ 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: diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index ac42ce69cd..765c936c22 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -21,7 +21,7 @@ service. task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" node { - name "O=NetworkMapAndNotary,L=London,C=GB" + name "O=Notary,L=London,C=GB" notary = [validating : true] p2pPort 10002 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=US,L=New York,O=PartyB" 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 constraint: attachmentId: "F578320232CAB87BB1E919F3E5DB9D81B7346F9D7EA6D9155DC0F7BA8E472552" @@ -157,7 +157,7 @@ The vaults of PartyA and PartyB should both display the following output: recordedTime: 1506415268.875000000 consumedTime: null status: "UNCONSUMED" - notary: "C=GB,L=London,O=NetworkMapAndNotary,CN=corda.notary.validating" + notary: "C=GB,L=London,O=Notary" lockId: null lockUpdateTime: 1506415269.548000000 totalStatesAvailable: -1 diff --git a/docs/source/setting-up-a-corda-network.rst b/docs/source/setting-up-a-corda-network.rst index fad6eb9898..146904ffd4 100644 --- a/docs/source/setting-up-a-corda-network.rst +++ b/docs/source/setting-up-a-corda-network.rst @@ -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 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 -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 -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: @@ -82,6 +82,11 @@ For example running the command on a directory containing these files : 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 `_. +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 ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/source/tutorial-cordapp.rst b/docs/source/tutorial-cordapp.rst index de6d19b43f..d64cc06718 100644 --- a/docs/source/tutorial-cordapp.rst +++ b/docs/source/tutorial-cordapp.rst @@ -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: -* **NetworkMapAndNotary**, which hosts a validating notary service +* **Notary**, which hosts a validating notary service * **PartyA** * **PartyB** * **PartyC** @@ -245,7 +245,7 @@ For each node, the ``runnodes`` script creates a node tab/window: 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 @@ -442,23 +442,27 @@ For more information on the client RPC interface and how to build an RPC client 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 -node folders to a different machine (e.g. using a USB key). It is important that none of the nodes - including the -network map/notary node - end up on more than one machine. Each computer should also have a copy of ``runnodes`` and -``runnodes.bat``. +After deploying the nodes, navigate to the build folder (``kotlin-source/build/nodes``) and for each node that needs to +be moved to another machine open its config file and change the Artemis messaging address to the IP address of the machine +where the node will run (e.g. ``p2pAddress="10.18.0.166:10006"``). + +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: -* Machine 1: ``NetworkMapAndNotary``, ``PartyA``, ``runnodes``, ``runnodes.bat`` +* Machine 1: ``Notary``, ``PartyA``, ``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. Testing and debugging diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/SignedNodeInfo.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/SignedNodeInfo.kt index 3dea456a05..b1fb7054d4 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/SignedNodeInfo.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/SignedNodeInfo.kt @@ -53,3 +53,13 @@ inline fun NodeInfo.sign(signer: (PublicKey, SerializedBytes) -> Digit val signatures = owningKeys.map { signer(it, serialised) } 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) -> DigitalSignature) : this(nodeInfo, nodeInfo.sign(signer)) + constructor(signedNodeInfo: SignedNodeInfo) : this(signedNodeInfo.verified(), signedNodeInfo) + operator fun component1(): NodeInfo = nodeInfo + operator fun component2(): SignedNodeInfo = signed +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt index cc4a91f4f8..2e1598d197 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt @@ -4,6 +4,7 @@ import com.google.common.hash.Hashing import com.google.common.hash.HashingInputStream import com.typesafe.config.ConfigFactory import net.corda.cordform.CordformNode +import net.corda.core.contracts.ContractClassName import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash.Companion.parse import net.corda.core.identity.Party @@ -77,9 +78,9 @@ class NetworkBootstrapper { distributeNodeInfos(nodeDirs, nodeInfoFiles) println("Gathering notary identities") 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()) - println("Updating whitelist.") + println("Updating whitelist") overwriteWhitelist(directory / WHITELIST_FILE_NAME, mergedWhiteList) installNetworkParameters(notaryInfos, nodeDirs, mergedWhiteList) println("Bootstrapping complete!") @@ -189,16 +190,18 @@ class NetworkBootstrapper { private fun generateWhitelist(whitelistFile: Path, cordapps: List?): Map> { 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 = cordapps?.flatMap { cordappJarPath -> val jarHash = getJarHash(cordappJarPath) scanJarForContracts(cordappJarPath).map { contract -> contract to jarHash } }?.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 existing = existingWhitelist[contractClassName] ?: emptyList() @@ -206,16 +209,15 @@ class NetworkBootstrapper { contractClassName to (if (newHash == null || newHash in existing) existing else existing + newHash) }.toMap() - println("Final whitelist: $merged") + println("Final whitelist:") + merged.forEach { println(it.outputString()) } return merged } private fun overwriteWhitelist(whitelistFile: Path, mergedWhiteList: Map>) { PrintStream(whitelistFile.toFile().outputStream()).use { out -> - mergedWhiteList.forEach { (contract, attachments )-> - out.println("${contract}:${attachments.joinToString(",")}") - } + mergedWhiteList.forEach { out.println(it.outputString()) } } } @@ -235,15 +237,17 @@ class NetworkBootstrapper { private fun NodeInfo.notaryIdentity(): Party { 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] - // 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. + // 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. 2 -> legalIdentities[1] else -> throw IllegalArgumentException("Not sure how to get the notary identity in this scenerio: $this") } } + private fun Map.Entry>.outputString() = "$key:${value.joinToString(",")}" + // We need to to set serialization env, because generation of parameters is run from Cordform. // KryoServerSerializationScheme is not accessible from nodeapi. private fun initialiseSerialization() { diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt index 07ad4bf3a3..da2a56fa05 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt @@ -7,7 +7,7 @@ import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.node.NodeInfo 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.testing.core.ALICE_NAME import net.corda.testing.core.SerializationEnvironmentRule @@ -39,8 +39,7 @@ class NodeInfoWatcherTest { private val scheduler = TestScheduler() private val testSubscriber = TestSubscriber() - private lateinit var nodeInfo: NodeInfo - private lateinit var signedNodeInfo: SignedNodeInfo + private lateinit var nodeInfoAndSigned: NodeInfoAndSigned private lateinit var nodeInfoPath: Path private lateinit var keyManagementService: KeyManagementService @@ -49,9 +48,7 @@ class NodeInfoWatcherTest { @Before fun start() { - val nodeInfoAndSigned = createNodeInfoAndSigned(ALICE_NAME) - nodeInfo = nodeInfoAndSigned.first - signedNodeInfo = nodeInfoAndSigned.second + nodeInfoAndSigned = createNodeInfoAndSigned(ALICE_NAME) val identityService = makeTestIdentityService() keyManagementService = MockKeyManagementService(identityService) nodeInfoWatcher = NodeInfoWatcher(tempFolder.root.toPath(), scheduler) @@ -62,7 +59,7 @@ class NodeInfoWatcherTest { fun `save a NodeInfo`() { assertEquals(0, 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) } assertEquals(1, nodeInfoFiles.size) @@ -76,8 +73,8 @@ class NodeInfoWatcherTest { @Test fun `save a NodeInfo to JimFs`() { val jimFs = Jimfs.newFileSystem(Configuration.unix()) - val jimFolder = jimFs.getPath("/nodeInfo") - NodeInfoWatcher.saveToFile(jimFolder, signedNodeInfo) + val jimFolder = jimFs.getPath("/nodeInfo").createDirectories() + NodeInfoWatcher.saveToFile(jimFolder, nodeInfoAndSigned) } @Test @@ -104,7 +101,7 @@ class NodeInfoWatcherTest { try { val readNodes = testSubscriber.onNextEvents.distinct() assertEquals(1, readNodes.size) - assertEquals(nodeInfo, readNodes.first()) + assertEquals(nodeInfoAndSigned.nodeInfo, readNodes.first()) } finally { subscription.unsubscribe() } @@ -129,7 +126,7 @@ class NodeInfoWatcherTest { testSubscriber.awaitValueCount(1, 5, TimeUnit.SECONDS) // The same folder can be reported more than once, so take unique values. val readNodes = testSubscriber.onNextEvents.distinct() - assertEquals(nodeInfo, readNodes.first()) + assertEquals(nodeInfoAndSigned.nodeInfo, readNodes.first()) } finally { subscription.unsubscribe() } @@ -141,6 +138,6 @@ class NodeInfoWatcherTest { // Write a nodeInfo under the right path. private fun createNodeInfoFileInPath() { - NodeInfoWatcher.saveToFile(nodeInfoPath, signedNodeInfo) + NodeInfoWatcher.saveToFile(nodeInfoPath, nodeInfoAndSigned) } } 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 0e3b1f2824..3ea2500904 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -61,11 +61,11 @@ import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.JVMAgentRegistry import net.corda.node.utilities.NodeBuildProperties import net.corda.nodeapi.internal.DevIdentityGenerator +import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.crypto.X509Utilities 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.sign import net.corda.nodeapi.internal.storeLegalIdentity import org.apache.activemq.artemis.utils.ReusableLatch import org.hibernate.type.descriptor.java.JavaTypeDescriptorRegistry @@ -181,11 +181,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val persistentNetworkMapCache = PersistentNetworkMapCache(database, notaries = emptyList()) persistentNetworkMapCache.start() 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 privateKey.sign(serialised.bytes) } - NodeInfoWatcher.saveToFile(configuration.baseDirectory, signedNodeInfo) + NodeInfoWatcher.saveToFile(configuration.baseDirectory, nodeInfoAndSigned) nodeInfo } } @@ -268,11 +268,12 @@ abstract class AbstractNode(val configuration: NodeConfiguration, configuration.baseDirectory) runOnStop += networkMapUpdater::close - networkMapUpdater.updateNodeInfo(services.myInfo) { - it.sign { publicKey, serialised -> - services.keyManagementService.sign(serialised.bytes, publicKey).withoutKey() - } + log.info("Node-info for this node: ${services.myInfo}") + + val nodeInfoAndSigned = NodeInfoAndSigned(services.myInfo) { publicKey, serialised -> + services.keyManagementService.sign(serialised.bytes, publicKey).withoutKey() } + networkMapUpdater.updateNodeInfo(nodeInfoAndSigned) networkMapUpdater.subscribeToNetworkMap() // If we successfully loaded network data from database, we set this future to Unit. diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt index 414f0f15c3..9013785798 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt @@ -7,12 +7,12 @@ import net.corda.core.internal.copyTo import net.corda.core.internal.div import net.corda.core.messaging.DataFeed import net.corda.core.messaging.ParametersUpdateInfo -import net.corda.core.node.NodeInfo import net.corda.core.serialization.serialize import net.corda.core.utilities.contextLogger import net.corda.core.utilities.minutes import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.utilities.NamedThreadFactory +import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME import net.corda.nodeapi.internal.network.ParametersUpdate @@ -54,18 +54,19 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, return DataFeed(currentUpdateInfo, parametersUpdatesTrack) } - fun updateNodeInfo(newInfo: NodeInfo, signer: (NodeInfo) -> SignedNodeInfo) { - val oldInfo = networkMapCache.getNodeByLegalIdentity(newInfo.legalIdentities.first()) + fun updateNodeInfo(nodeInfoAndSigned: NodeInfoAndSigned) { + // 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. - 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. - val signedNodeInfo = signer(newInfo) - networkMapCache.addNode(newInfo) - fileWatcher.saveToFile(signedNodeInfo) + networkMapCache.addNode(nodeInfoAndSigned.nodeInfo) + fileWatcher.saveToFile(nodeInfoAndSigned) if (networkMapClient != null) { - tryPublishNodeInfoAsync(signedNodeInfo, networkMapClient) + tryPublishNodeInfoAsync(nodeInfoAndSigned.signed, networkMapClient) } } diff --git a/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt b/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt index aa55fb2552..5059276ed5 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt @@ -4,17 +4,26 @@ import net.corda.cordform.CordformNode import net.corda.core.crypto.SecureHash import net.corda.core.internal.* 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.utilities.contextLogger import net.corda.core.utilities.seconds +import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.SignedNodeInfo 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.Scheduler import java.io.IOException import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.StandardCopyOption.REPLACE_EXISTING import java.time.Duration import java.util.concurrent.TimeUnit +import java.util.stream.Stream import kotlin.streams.toList /** @@ -31,34 +40,29 @@ import kotlin.streams.toList class NodeInfoWatcher(private val nodePath: Path, private val scheduler: Scheduler, private val pollInterval: Duration = 5.seconds) { - private val nodeInfoDirectory = nodePath / CordformNode.NODE_INFO_DIRECTORY - private val processedNodeInfoFiles = mutableSetOf() - private val _processedNodeInfoHashes = mutableSetOf() - val processedNodeInfoHashes: Set get() = _processedNodeInfoHashes.toSet() - companion object { private val logger = contextLogger() - /** - * Saves the given [NodeInfo] to a path. - * The node is 'encoded' as a SignedNodeInfo, signed with the owning key of its first identity. - * The name of the written file will be "nodeInfo-" followed by the hash of the content. The hash in the filename - * is used so that one can freely copy these files without fearing to overwrite another one. - * - * @param path the path where to write the file, if non-existent it will be created. - * @param signedNodeInfo the signed NodeInfo. - */ - fun saveToFile(path: Path, signedNodeInfo: SignedNodeInfo) { - try { - path.createDirectories() - signedNodeInfo.serialize() - .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) - } + + // TODO This method doesn't belong in this class + fun saveToFile(path: Path, nodeInfoAndSigned: NodeInfoAndSigned) { + // By using the hash of the node's first name we ensure: + // 1) node info files for the same node map to the same filename and thus avoid having duplicate files for + // the same node + // 2) avoid having to deal with characters in the X.500 name which are incompatible with the local filesystem + val fileNameHash = nodeInfoAndSigned.nodeInfo.legalIdentities[0].name.serialize().hash + nodeInfoAndSigned + .signed + .serialize() + .open() + .copyTo(path / "${NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX}$fileNameHash", REPLACE_EXISTING) } } + private val nodeInfoDirectory = nodePath / CordformNode.NODE_INFO_DIRECTORY + + private val _processedNodeInfoHashes = HashSet() + val processedNodeInfoHashes: Set get() = _processedNodeInfoHashes + init { require(pollInterval >= 5.seconds) { "Poll interval must be 5 seconds or longer." } if (!nodeInfoDirectory.isDirectory()) { @@ -84,7 +88,10 @@ class NodeInfoWatcher(private val nodePath: Path, .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. @@ -97,16 +104,15 @@ class NodeInfoWatcher(private val nodePath: Path, return emptyList() } val result = nodeInfoDirectory.list { paths -> - paths.filter { it !in processedNodeInfoFiles } + paths .filter { it.isRegularFile() } - .map { path -> - processFile(path)?.apply { - processedNodeInfoFiles.add(path) - _processedNodeInfoHashes.add(this.serialize().hash) + .flatMap { path -> + val nodeInfo = processFile(path)?.let { + if (_processedNodeInfoHashes.add(it.signed.raw.hash)) it.nodeInfo else null } + if (nodeInfo != null) Stream.of(nodeInfo) else Stream.empty() } .toList() - .filterNotNull() } if (result.isNotEmpty()) { logger.info("Successfully read ${result.size} NodeInfo files from disk.") @@ -114,14 +120,25 @@ class NodeInfoWatcher(private val nodePath: Path, return result } - private fun processFile(file: Path): NodeInfo? { + private fun processFile(file: Path): NodeInfoAndSigned? { return try { logger.info("Reading NodeInfo from file: $file") - val signedData = file.readObject() - signedData.verified() + val signedNodeInfo = file.readObject() + NodeInfoAndSigned(signedNodeInfo) } catch (e: Exception) { logger.warn("Exception parsing NodeInfo from file. $file", e) null } } } + +// TODO Remove this once we have a tool that can read AMQP serialised files +fun main(args: Array) { + _contextSerializationEnv.set(SerializationEnvironmentImpl( + SerializationFactoryImpl().apply { + registerScheme(AMQPServerSerializationScheme()) + }, + AMQP_P2P_CONTEXT) + ) + println(Paths.get(args[0]).readObject().verified()) +} diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt index 0647f46253..2e77979c24 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt @@ -18,6 +18,7 @@ import net.corda.core.node.NodeInfo import net.corda.core.serialization.serialize import net.corda.core.utilities.millis import net.corda.node.services.api.NetworkMapCacheInternal +import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.createDevNetworkMapCa import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair @@ -68,33 +69,33 @@ class NetworkMapUpdaterTest { fun `publish node info`() { nodeInfoBuilder.addIdentity(ALICE_NAME) - val (nodeInfo1, signedNodeInfo1) = nodeInfoBuilder.buildWithSigned() - val (sameNodeInfoDifferentTime, signedSameNodeInfoDifferentTime) = nodeInfoBuilder.buildWithSigned(serial = System.currentTimeMillis()) + val nodeInfo1AndSigned = nodeInfoBuilder.buildWithSigned() + val sameNodeInfoDifferentTimeAndSigned = nodeInfoBuilder.buildWithSigned(serial = System.currentTimeMillis()) // Publish node info for the first time. - updater.updateNodeInfo(nodeInfo1) { signedNodeInfo1 } + updater.updateNodeInfo(nodeInfo1AndSigned) // Sleep as publish is asynchronous. // TODO: Remove sleep in unit test Thread.sleep(2L * cacheExpiryMs) verify(networkMapClient, times(1)).publish(any()) - networkMapCache.addNode(nodeInfo1) + networkMapCache.addNode(nodeInfo1AndSigned.nodeInfo) // Publish the same node info, but with different serial. - updater.updateNodeInfo(sameNodeInfoDifferentTime) { signedSameNodeInfoDifferentTime } + updater.updateNodeInfo(sameNodeInfoDifferentTimeAndSigned) // TODO: Remove sleep in unit test. Thread.sleep(2L * cacheExpiryMs) // 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. - updater.updateNodeInfo(differentNodeInfo) { signedDifferentNodeInfo } + updater.updateNodeInfo(differentNodeInfoAndSigned) // TODO: Remove sleep in unit test. Thread.sleep(200) - verify(networkMapClient, times(1)).publish(signedDifferentNodeInfo) + verify(networkMapClient, times(1)).publish(differentNodeInfoAndSigned.signed) } @Test @@ -103,7 +104,7 @@ class NetworkMapUpdaterTest { val (nodeInfo2, signedNodeInfo2) = createNodeInfoAndSigned("Info 2") val (nodeInfo3, signedNodeInfo3) = createNodeInfoAndSigned("Info 3") val (nodeInfo4, signedNodeInfo4) = createNodeInfoAndSigned("Info 4") - val (fileNodeInfo, signedFileNodeInfo) = createNodeInfoAndSigned("Info from file") + val fileNodeInfoAndSigned = createNodeInfoAndSigned("Info from file") // Test adding new node. networkMapClient.publish(signedNodeInfo1) @@ -119,7 +120,7 @@ class NetworkMapUpdaterTest { verify(networkMapCache, times(1)).addNode(nodeInfo1) 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(signedNodeInfo4) @@ -131,7 +132,7 @@ class NetworkMapUpdaterTest { verify(networkMapCache, times(5)).addNode(any()) verify(networkMapCache, times(1)).addNode(nodeInfo3) verify(networkMapCache, times(1)).addNode(nodeInfo4) - verify(networkMapCache, times(1)).addNode(fileNodeInfo) + verify(networkMapCache, times(1)).addNode(fileNodeInfoAndSigned.nodeInfo) } @Test @@ -140,10 +141,10 @@ class NetworkMapUpdaterTest { val (nodeInfo2, signedNodeInfo2) = createNodeInfoAndSigned("Info 2") val (nodeInfo3, signedNodeInfo3) = createNodeInfoAndSigned("Info 3") val (nodeInfo4, signedNodeInfo4) = createNodeInfoAndSigned("Info 4") - val (fileNodeInfo, signedFileNodeInfo) = createNodeInfoAndSigned("Info from file") + val fileNodeInfoAndSigned = createNodeInfoAndSigned("Info from file") // Add all nodes. - NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, signedFileNodeInfo) + NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, fileNodeInfoAndSigned) networkMapClient.publish(signedNodeInfo1) networkMapClient.publish(signedNodeInfo2) networkMapClient.publish(signedNodeInfo3) @@ -157,7 +158,7 @@ class NetworkMapUpdaterTest { // 4 node info from network map, and 1 from file. assertThat(nodeInfoMap).hasSize(4) verify(networkMapCache, times(5)).addNode(any()) - verify(networkMapCache, times(1)).addNode(fileNodeInfo) + verify(networkMapCache, times(1)).addNode(fileNodeInfoAndSigned.nodeInfo) // Test remove node. nodeInfoMap.clear() @@ -170,25 +171,25 @@ class NetworkMapUpdaterTest { verify(networkMapCache, times(1)).removeNode(nodeInfo4) // Node info from file should not be deleted - assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfo.serialize().hash) + assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfoAndSigned.nodeInfo.serialize().hash) } @Test 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. verify(networkMapCache, times(0)).addNode(any()) updater.subscribeToNetworkMap() - NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, signedFileNodeInfo) + NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, fileNodeInfoAndSigned) scheduler.advanceTimeBy(10, TimeUnit.SECONDS) 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 @@ -275,7 +276,7 @@ class NetworkMapUpdaterTest { } } - private fun createNodeInfoAndSigned(org: String): Pair { + private fun createNodeInfoAndSigned(org: String): NodeInfoAndSigned { return createNodeInfoAndSigned(CordaX500Name(org, "London", "GB")) } } \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt index 8757adde2b..c311fbaeea 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt @@ -7,6 +7,7 @@ import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.NodeInfo import net.corda.core.serialization.serialize import net.corda.core.utilities.NetworkHostAndPort +import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.createDevNodeCa import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair @@ -47,10 +48,11 @@ class TestNodeInfoBuilder(private val intermediateAndRoot: Pair { + fun buildWithSigned(serial: Long = 1, platformVersion: Int = 1): NodeInfoAndSigned { val nodeInfo = build(serial, platformVersion) - val privateKeys = identitiesAndPrivateKeys.map { it.second } - return Pair(nodeInfo, nodeInfo.signWith(privateKeys)) + return NodeInfoAndSigned(nodeInfo) { publicKey, serialised -> + identitiesAndPrivateKeys.first { it.first.owningKey == publicKey }.second.sign(serialised.bytes) + } } fun reset() { @@ -58,7 +60,7 @@ class TestNodeInfoBuilder(private val intermediateAndRoot: Pair { +fun createNodeInfoAndSigned(vararg names: CordaX500Name, serial: Long = 1, platformVersion: Int = 1): NodeInfoAndSigned { val nodeInfoBuilder = TestNodeInfoBuilder() names.forEach { nodeInfoBuilder.addIdentity(it) } return nodeInfoBuilder.buildWithSigned(serial, platformVersion) From f9bfebe4a82d491d95234510a1a6efefba4e3a61 Mon Sep 17 00:00:00 2001 From: Thomas Schroeter Date: Fri, 2 Mar 2018 08:50:31 +0000 Subject: [PATCH 4/4] [CORDA-1136] Update startup message of P2P address and port (#2698) --- node/src/main/kotlin/net/corda/node/internal/Node.kt | 2 +- .../net/corda/node/services/messaging/ArtemisMessagingServer.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 2bd44bd75a..dca482551a 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -167,7 +167,7 @@ open class Node(configuration: NodeConfiguration, val advertisedAddress = info.addresses[0] bridgeControlListener = BridgeControlListener(configuration, serverAddress, networkParameters.maxMessageSize) - printBasicNodeInfo("Incoming connection address", advertisedAddress.toString()) + printBasicNodeInfo("Advertised P2P messaging addresses", info.addresses.joinToString()) rpcServerAddresses?.let { rpcMessagingClient = RPCMessagingClient(configuration.rpcOptions.sslConfig, it.admin, networkParameters.maxMessageSize) } diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt index ab07903a1e..326f5713aa 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt @@ -120,7 +120,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration, } // Config driven switch between legacy CORE bridges and the newer AMQP protocol bridges. activeMQServer.start() - Node.printBasicNodeInfo("Listening on port", p2pPort.toString()) + log.info("P2P messaging server listening on port $p2pPort") } private fun createArtemisConfig() = SecureArtemisConfiguration().apply {