mirror of
https://github.com/corda/corda.git
synced 2024-12-28 00:38:55 +00:00
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:
commit
a59083ceb2
@ -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<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>)
|
||||
: 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)
|
||||
: 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
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
||||
.. 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.
|
||||
|
||||
|
@ -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 <https://github.com/corda/cordapp-template-kotlin/blob/release-V2/build.gradle#L97>`_:
|
||||
nodes for testing and demos. Here is an example ``Cordform`` task called ``deployNodes`` that creates three nodes, defined
|
||||
in the `Kotlin CorDapp Template <https://github.com/corda/cordapp-template-kotlin/blob/release-V3/build.gradle#L100>`_:
|
||||
|
||||
.. 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:
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,56 @@ 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 <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
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -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
|
||||
|
@ -53,3 +53,13 @@ inline fun NodeInfo.sign(signer: (PublicKey, SerializedBytes<NodeInfo>) -> 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<NodeInfo>) -> DigitalSignature) : this(nodeInfo, nodeInfo.sign(signer))
|
||||
constructor(signedNodeInfo: SignedNodeInfo) : this(signedNodeInfo.verified(), signedNodeInfo)
|
||||
operator fun component1(): NodeInfo = nodeInfo
|
||||
operator fun component2(): SignedNodeInfo = signed
|
||||
}
|
||||
|
@ -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<String>?): Map<String, List<AttachmentId>> {
|
||||
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)
|
||||
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<String, List<AttachmentId>>) {
|
||||
PrintStream(whitelistFile.toFile().outputStream()).use { out ->
|
||||
mergedWhiteList.forEach { (contract, attachments )->
|
||||
out.println("${contract}:${attachments.joinToString(",")}")
|
||||
}
|
||||
mergedWhiteList.forEach { out.println(it.outputString()) }
|
||||
}
|
||||
}
|
||||
|
||||
@ -244,6 +246,8 @@ class NetworkBootstrapper {
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
// KryoServerSerializationScheme is not accessible from nodeapi.
|
||||
private fun initialiseSerialization() {
|
||||
|
@ -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<NodeInfo>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -61,9 +61,13 @@ 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.*
|
||||
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 org.apache.activemq.artemis.utils.ReusableLatch
|
||||
import org.hibernate.type.descriptor.java.JavaTypeDescriptorRegistry
|
||||
@ -181,11 +185,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
|
||||
}
|
||||
}
|
||||
@ -265,11 +269,12 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||
configuration.baseDirectory)
|
||||
runOnStop += networkMapUpdater::close
|
||||
|
||||
networkMapUpdater.updateNodeInfo(services.myInfo) {
|
||||
it.sign { publicKey, serialised ->
|
||||
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.
|
||||
|
@ -168,7 +168,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())
|
||||
|
||||
val rpcServerConfiguration = RPCServerConfiguration.default.copy(
|
||||
rpcThreadPoolSize = configuration.enterpriseConfiguration.tuning.rpcThreadPoolSize
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<Path>()
|
||||
private val _processedNodeInfoHashes = mutableSetOf<SecureHash>()
|
||||
val processedNodeInfoHashes: Set<SecureHash> 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()
|
||||
|
||||
// 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}${signedNodeInfo.raw.hash}")
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Couldn't write node info to file", e)
|
||||
}
|
||||
.copyTo(path / "${NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX}$fileNameHash", REPLACE_EXISTING)
|
||||
}
|
||||
}
|
||||
|
||||
private val nodeInfoDirectory = nodePath / CordformNode.NODE_INFO_DIRECTORY
|
||||
|
||||
private val _processedNodeInfoHashes = HashSet<SecureHash>()
|
||||
val processedNodeInfoHashes: Set<SecureHash> 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<SignedNodeInfo>()
|
||||
signedData.verified()
|
||||
val signedNodeInfo = file.readObject<SignedNodeInfo>()
|
||||
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<String>) {
|
||||
_contextSerializationEnv.set(SerializationEnvironmentImpl(
|
||||
SerializationFactoryImpl().apply {
|
||||
registerScheme(AMQPServerSerializationScheme())
|
||||
},
|
||||
AMQP_P2P_CONTEXT)
|
||||
)
|
||||
println(Paths.get(args[0]).readObject<SignedNodeInfo>().verified())
|
||||
}
|
||||
|
@ -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<NodeInfo, SignedNodeInfo> {
|
||||
private fun createNodeInfoAndSigned(org: String): NodeInfoAndSigned {
|
||||
return createNodeInfoAndSigned(CordaX500Name(org, "London", "GB"))
|
||||
}
|
||||
}
|
@ -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<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 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<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()
|
||||
names.forEach { nodeInfoBuilder.addIdentity(it) }
|
||||
return nodeInfoBuilder.buildWithSigned(serial, platformVersion)
|
||||
|
Loading…
Reference in New Issue
Block a user