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 net.corda.core.utilities.NonEmptySet
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The node asked a remote peer for the transaction identified by [hash] because it is a dependency of a transaction
|
||||||
|
* being resolved, but the remote peer would not provide it.
|
||||||
|
*
|
||||||
|
* @property hash Merkle root of the transaction being resolved, see [net.corda.core.transactions.WireTransaction.id]
|
||||||
|
*/
|
||||||
class TransactionResolutionException(val hash: SecureHash) : FlowException("Transaction resolution failure for $hash")
|
class TransactionResolutionException(val hash: SecureHash) : FlowException("Transaction resolution failure for $hash")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The node asked a remote peer for the attachment identified by [hash] because it is a dependency of a transaction
|
||||||
|
* being resolved, but the remote peer would not provide it.
|
||||||
|
*
|
||||||
|
* @property hash Hash of the bytes of the attachment, see [Attachment.id]
|
||||||
|
*/
|
||||||
class AttachmentResolutionException(val hash: SecureHash) : FlowException("Attachment resolution failure for $hash")
|
class AttachmentResolutionException(val hash: SecureHash) : FlowException("Attachment resolution failure for $hash")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that some aspect of the transaction named by [txId] violates the platform rules. The exact type of failure
|
||||||
|
* is expressed using a subclass. TransactionVerificationException is a [FlowException] and thus when thrown inside
|
||||||
|
* a flow, the details of the failure will be serialised, propagated to the peer and rethrown.
|
||||||
|
*
|
||||||
|
* @property txId the Merkle root hash (identifier) of the transaction that failed verification.
|
||||||
|
*/
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
@CordaSerializable
|
||||||
sealed class TransactionVerificationException(val txId: SecureHash, message: String, cause: Throwable?)
|
sealed class TransactionVerificationException(val txId: SecureHash, message: String, cause: Throwable?)
|
||||||
: FlowException("$message, transaction: $txId", cause) {
|
: FlowException("$message, transaction: $txId", cause) {
|
||||||
|
|
||||||
class ContractRejection(txId: SecureHash, contract: Contract, cause: Throwable)
|
/**
|
||||||
: TransactionVerificationException(txId, "Contract verification failed: ${cause.message}, contract: $contract", cause)
|
* Indicates that one of the [Contract.verify] methods selected by the contract constraints and attachments
|
||||||
|
* rejected the transaction by throwing an exception.
|
||||||
|
*
|
||||||
|
* @property contractClass The fully qualified class name of the failing contract.
|
||||||
|
*/
|
||||||
|
class ContractRejection(txId: SecureHash, val contractClass: String, cause: Throwable) : TransactionVerificationException(txId, "Contract verification failed: ${cause.message}, contract: $contractClass", cause) {
|
||||||
|
constructor(txId: SecureHash, contract: Contract, cause: Throwable) : this(txId, contract.javaClass.name, cause)
|
||||||
|
}
|
||||||
|
|
||||||
class ContractConstraintRejection(txId: SecureHash, contractClass: String)
|
/**
|
||||||
|
* The transaction attachment that contains the [contractClass] class didn't meet the constraints specified by
|
||||||
|
* the [TransactionState.constraint] object. This usually implies a version mismatch of some kind.
|
||||||
|
*
|
||||||
|
* @property contractClass The fully qualified class name of the failing contract.
|
||||||
|
*/
|
||||||
|
class ContractConstraintRejection(txId: SecureHash, val contractClass: String)
|
||||||
: TransactionVerificationException(txId, "Contract constraints failed for $contractClass", null)
|
: TransactionVerificationException(txId, "Contract constraints failed for $contractClass", null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A state requested a contract class via its [TransactionState.contract] field that didn't appear in any attached
|
||||||
|
* JAR at all. This usually implies the attachments were forgotten or a version mismatch.
|
||||||
|
*
|
||||||
|
* @property contractClass The fully qualified class name of the failing contract.
|
||||||
|
*/
|
||||||
class MissingAttachmentRejection(txId: SecureHash, val contractClass: String)
|
class MissingAttachmentRejection(txId: SecureHash, val contractClass: String)
|
||||||
: TransactionVerificationException(txId, "Contract constraints failed, could not find attachment for: $contractClass", null)
|
: TransactionVerificationException(txId, "Contract constraints failed, could not find attachment for: $contractClass", null)
|
||||||
|
|
||||||
class ConflictingAttachmentsRejection(txId: SecureHash, contractClass: String)
|
/**
|
||||||
|
* Indicates this transaction violates the "no overlap" rule: two attachments are trying to provide the same file
|
||||||
|
* path. Whereas Java classpaths would normally allow that with the first class taking precedence, this is not
|
||||||
|
* allowed in transactions for security reasons. This usually indicates that two separate apps share a dependency,
|
||||||
|
* in which case you could try 'shading the fat jars' to rename classes of dependencies. Or you could manually
|
||||||
|
* attach dependency JARs when building the transaction.
|
||||||
|
*
|
||||||
|
* @property contractClass The fully qualified class name of the failing contract.
|
||||||
|
*/
|
||||||
|
class ConflictingAttachmentsRejection(txId: SecureHash, val contractClass: String)
|
||||||
: TransactionVerificationException(txId, "Contract constraints failed for: $contractClass, because multiple attachments providing this contract were attached.", null)
|
: TransactionVerificationException(txId, "Contract constraints failed for: $contractClass, because multiple attachments providing this contract were attached.", null)
|
||||||
|
|
||||||
class ContractCreationError(txId: SecureHash, contractClass: String, cause: Throwable)
|
/**
|
||||||
|
* A [Contract] class named by a state could not be constructed. Most likely you do not have a no-argument
|
||||||
|
* constructor, or the class doesn't subclass [Contract].
|
||||||
|
*
|
||||||
|
* @property contractClass The fully qualified class name of the failing contract.
|
||||||
|
*/
|
||||||
|
class ContractCreationError(txId: SecureHash, val contractClass: String, cause: Throwable)
|
||||||
: TransactionVerificationException(txId, "Contract verification failed: ${cause.message}, could not create contract class: $contractClass", cause)
|
: TransactionVerificationException(txId, "Contract verification failed: ${cause.message}, could not create contract class: $contractClass", cause)
|
||||||
|
|
||||||
class MoreThanOneNotary(txId: SecureHash)
|
/**
|
||||||
: TransactionVerificationException(txId, "More than one notary", null)
|
* An output state has a notary that doesn't match the transaction's notary field. It must!
|
||||||
|
*
|
||||||
|
* @property txNotary the [Party] specified by the transaction header.
|
||||||
|
* @property outputNotary the [Party] specified by the errant state.
|
||||||
|
*/
|
||||||
|
class NotaryChangeInWrongTransactionType(txId: SecureHash, val txNotary: Party, val outputNotary: Party)
|
||||||
|
: TransactionVerificationException(txId, "Found unexpected notary change in transaction. Tx notary: $txNotary, found: $outputNotary", null)
|
||||||
|
|
||||||
class SignersMissing(txId: SecureHash, missing: List<PublicKey>)
|
/**
|
||||||
: TransactionVerificationException(txId, "Signers missing: ${missing.joinToString()}", null)
|
* If a state is encumbered (the [TransactionState.encumbrance] field is set) then its encumbrance must be used
|
||||||
|
* as an input to any transaction that uses it. In this way states can be tied together in chains, thus composing
|
||||||
|
* logic. Note that encumbrances aren't fully supported by all aspects of the platform at this time so if you use
|
||||||
|
* them, you may find transactions created by the platform don't always respect the encumbrance rule.
|
||||||
|
*
|
||||||
|
* @property missing the index of the state missing the encumbrance.
|
||||||
|
* @property inOut whether the issue exists in the input list or output list.
|
||||||
|
*/
|
||||||
|
class TransactionMissingEncumbranceException(txId: SecureHash, val missing: Int, val inOut: Direction)
|
||||||
|
: TransactionVerificationException(txId, "Missing required encumbrance $missing in $inOut", null)
|
||||||
|
|
||||||
|
/** Whether the inputs or outputs list contains an encumbrance issue, see [TransactionMissingEncumbranceException]. */
|
||||||
|
@CordaSerializable
|
||||||
|
enum class Direction {
|
||||||
|
/** Issue in the inputs list */ INPUT,
|
||||||
|
/** Issue in the outputs list */ OUTPUT
|
||||||
|
}
|
||||||
|
|
||||||
|
// We could revisit and throw this more appropriate type in a future release that uses targetVersion to
|
||||||
|
// avoid the compatibility break, because IllegalStateException isn't ideal for this. Or we could use this
|
||||||
|
// as a cause.
|
||||||
|
/** @suppress This class is not used: duplicate inputs throw a [IllegalStateException] instead. */
|
||||||
|
@Deprecated("This class is not used: duplicate inputs throw a [IllegalStateException] instead.")
|
||||||
class DuplicateInputStates(txId: SecureHash, val duplicates: NonEmptySet<StateRef>)
|
class DuplicateInputStates(txId: SecureHash, val duplicates: NonEmptySet<StateRef>)
|
||||||
: TransactionVerificationException(txId, "Duplicate inputs: ${duplicates.joinToString()}", null)
|
: TransactionVerificationException(txId, "Duplicate inputs: ${duplicates.joinToString()}", null)
|
||||||
|
|
||||||
|
/** @suppress This class is obsolete and nothing has ever used it. */
|
||||||
|
@Deprecated("This class is obsolete and nothing has ever used it.")
|
||||||
|
class MoreThanOneNotary(txId: SecureHash) : TransactionVerificationException(txId, "More than one notary", null)
|
||||||
|
|
||||||
|
/** @suppress This class is obsolete and nothing has ever used it. */
|
||||||
|
@Deprecated("This class is obsolete and nothing has ever used it.")
|
||||||
|
class SignersMissing(txId: SecureHash, val missing: List<PublicKey>) : TransactionVerificationException(txId, "Signers missing: ${missing.joinToString()}", null)
|
||||||
|
|
||||||
|
/** @suppress This class is obsolete and nothing has ever used it. */
|
||||||
|
@Deprecated("This class is obsolete and nothing has ever used it.")
|
||||||
class InvalidNotaryChange(txId: SecureHash)
|
class InvalidNotaryChange(txId: SecureHash)
|
||||||
: TransactionVerificationException(txId, "Detected a notary change. Outputs must use the same notary as inputs", null)
|
: TransactionVerificationException(txId, "Detected a notary change. Outputs must use the same notary as inputs", null)
|
||||||
|
|
||||||
class NotaryChangeInWrongTransactionType(txId: SecureHash, txNotary: Party, outputNotary: Party)
|
|
||||||
: TransactionVerificationException(txId, "Found unexpected notary change in transaction. Tx notary: $txNotary, found: $outputNotary", null)
|
|
||||||
|
|
||||||
class TransactionMissingEncumbranceException(txId: SecureHash, missing: Int, inOut: Direction)
|
|
||||||
: TransactionVerificationException(txId, "Missing required encumbrance $missing in $inOut", null)
|
|
||||||
|
|
||||||
@CordaSerializable
|
|
||||||
enum class Direction {
|
|
||||||
INPUT,
|
|
||||||
OUTPUT
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -116,10 +116,9 @@ data class LedgerTransaction @JvmOverloads constructor(
|
|||||||
* If any contract fails to verify, the whole transaction is considered to be invalid.
|
* If any contract fails to verify, the whole transaction is considered to be invalid.
|
||||||
*/
|
*/
|
||||||
private fun verifyContracts() {
|
private fun verifyContracts() {
|
||||||
for (contractEntry in contracts.entries) {
|
for ((key, result) in contracts) {
|
||||||
val result = contractEntry.value
|
|
||||||
when (result) {
|
when (result) {
|
||||||
is Try.Failure -> throw TransactionVerificationException.ContractCreationError(id, contractEntry.key, result.exception)
|
is Try.Failure -> throw TransactionVerificationException.ContractCreationError(id, key, result.exception)
|
||||||
is Try.Success -> {
|
is Try.Success -> {
|
||||||
val contract = result.value
|
val contract = result.value
|
||||||
try {
|
try {
|
||||||
|
@ -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.
|
:p2pAddress: The host and port on which the node is available for protocol operations over ArtemisMQ.
|
||||||
|
|
||||||
.. note:: In practice the ArtemisMQ messaging services bind to all local addresses on the specified port. However,
|
.. note:: In practice the ArtemisMQ messaging services bind to all local addresses on the specified port. However,
|
||||||
note that the host is the included as the advertised entry in the NetworkMapService. As a result the value listed
|
note that the host is the included as the advertised entry in the network map. As a result the value listed
|
||||||
here must be externally accessible when running nodes across a cluster of machines. If the provided host is unreachable,
|
here must be externally accessible when running nodes across a cluster of machines. If the provided host is unreachable,
|
||||||
the node will try to auto-discover its public one.
|
the node will try to auto-discover its public one.
|
||||||
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
Creating nodes locally
|
Creating nodes locally
|
||||||
======================
|
======================
|
||||||
|
|
||||||
.. contents::
|
|
||||||
|
|
||||||
Node structure
|
Node structure
|
||||||
--------------
|
--------------
|
||||||
Each Corda node has the following structure:
|
Each Corda node has the following structure:
|
||||||
@ -91,8 +89,8 @@ The OID and format for these extensions will be described in a further specifica
|
|||||||
The Cordform task
|
The Cordform task
|
||||||
-----------------
|
-----------------
|
||||||
Corda provides a gradle plugin called ``Cordform`` that allows you to automatically generate and configure a set of
|
Corda provides a gradle plugin called ``Cordform`` that allows you to automatically generate and configure a set of
|
||||||
nodes. Here is an example ``Cordform`` task called ``deployNodes`` that creates three nodes, defined in the
|
nodes for testing and demos. Here is an example ``Cordform`` task called ``deployNodes`` that creates three nodes, defined
|
||||||
`Kotlin CorDapp Template <https://github.com/corda/cordapp-template-kotlin/blob/release-V2/build.gradle#L97>`_:
|
in the `Kotlin CorDapp Template <https://github.com/corda/cordapp-template-kotlin/blob/release-V3/build.gradle#L100>`_:
|
||||||
|
|
||||||
.. sourcecode:: groovy
|
.. sourcecode:: groovy
|
||||||
|
|
||||||
@ -155,7 +153,7 @@ You can extend ``deployNodes`` to generate additional nodes.
|
|||||||
.. warning:: When adding nodes, make sure that there are no port clashes!
|
.. warning:: When adding nodes, make sure that there are no port clashes!
|
||||||
|
|
||||||
Specifying a custom webserver
|
Specifying a custom webserver
|
||||||
-----------------------------
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
By default, any node listing a webport will use the default development webserver, which is not production-ready. You
|
By default, any node listing a webport will use the default development webserver, which is not production-ready. You
|
||||||
can use your own webserver JAR instead by using the ``webserverJar`` argument in a ``Cordform`` ``node`` configuration
|
can use your own webserver JAR instead by using the ``webserverJar`` argument in a ``Cordform`` ``node`` configuration
|
||||||
block:
|
block:
|
||||||
@ -174,7 +172,7 @@ The webserver JAR will be copied into the node's ``build`` folder with the name
|
|||||||
node's ``node.conf`` file.
|
node's ``node.conf`` file.
|
||||||
|
|
||||||
Running deployNodes
|
Running deployNodes
|
||||||
-------------------
|
~~~~~~~~~~~~~~~~~~~
|
||||||
To create the nodes defined in our ``deployNodes`` task, run the following command in a terminal window from the root
|
To create the nodes defined in our ``deployNodes`` task, run the following command in a terminal window from the root
|
||||||
of the project where the ``deployNodes`` task is defined:
|
of the project where the ``deployNodes`` task is defined:
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ service.
|
|||||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||||
directory "./build/nodes"
|
directory "./build/nodes"
|
||||||
node {
|
node {
|
||||||
name "O=NetworkMapAndNotary,L=London,C=GB"
|
name "O=Notary,L=London,C=GB"
|
||||||
notary = [validating : true]
|
notary = [validating : true]
|
||||||
p2pPort 10002
|
p2pPort 10002
|
||||||
rpcPort 10003
|
rpcPort 10003
|
||||||
@ -142,7 +142,7 @@ The vaults of PartyA and PartyB should both display the following output:
|
|||||||
- "C=GB,L=London,O=PartyA"
|
- "C=GB,L=London,O=PartyA"
|
||||||
- "C=US,L=New York,O=PartyB"
|
- "C=US,L=New York,O=PartyB"
|
||||||
contract: "com.template.contract.IOUContract"
|
contract: "com.template.contract.IOUContract"
|
||||||
notary: "C=GB,L=London,O=NetworkMapAndNotary,CN=corda.notary.validating"
|
notary: "C=GB,L=London,O=Notary"
|
||||||
encumbrance: null
|
encumbrance: null
|
||||||
constraint:
|
constraint:
|
||||||
attachmentId: "F578320232CAB87BB1E919F3E5DB9D81B7346F9D7EA6D9155DC0F7BA8E472552"
|
attachmentId: "F578320232CAB87BB1E919F3E5DB9D81B7346F9D7EA6D9155DC0F7BA8E472552"
|
||||||
@ -157,7 +157,7 @@ The vaults of PartyA and PartyB should both display the following output:
|
|||||||
recordedTime: 1506415268.875000000
|
recordedTime: 1506415268.875000000
|
||||||
consumedTime: null
|
consumedTime: null
|
||||||
status: "UNCONSUMED"
|
status: "UNCONSUMED"
|
||||||
notary: "C=GB,L=London,O=NetworkMapAndNotary,CN=corda.notary.validating"
|
notary: "C=GB,L=London,O=Notary"
|
||||||
lockId: null
|
lockId: null
|
||||||
lockUpdateTime: 1506415269.548000000
|
lockUpdateTime: 1506415269.548000000
|
||||||
totalStatesAvailable: -1
|
totalStatesAvailable: -1
|
||||||
|
@ -107,6 +107,9 @@ The current set of network parameters:
|
|||||||
:modifiedTime: The time when the network parameters were last modified by the compatibility zone operator.
|
:modifiedTime: The time when the network parameters were last modified by the compatibility zone operator.
|
||||||
:epoch: Version number of the network parameters. Starting from 1, this will always increment whenever any of the
|
:epoch: Version number of the network parameters. Starting from 1, this will always increment whenever any of the
|
||||||
parameters change.
|
parameters change.
|
||||||
|
:whitelistedContractImplementations: List of whitelisted versions of contract code.
|
||||||
|
For each contract class there is a list of hashes of the approved CorDapp jar versions containing that contract.
|
||||||
|
Read more about *Zone constraints* here :doc:`api-contract-constraints`
|
||||||
|
|
||||||
More parameters will be added in future releases to regulate things like allowed port numbers, how long a node can be
|
More parameters will be added in future releases to regulate things like allowed port numbers, how long a node can be
|
||||||
offline before it is evicted from the zone, whether or not IPv6 connectivity is required for zone members, required
|
offline before it is evicted from the zone, whether or not IPv6 connectivity is required for zone members, required
|
||||||
|
@ -57,9 +57,9 @@ in its local network map cache. The node generates its own node-info file on sta
|
|||||||
In addition to the network map, all the nodes on a network must use the same set of network parameters. These are a set
|
In addition to the network map, all the nodes on a network must use the same set of network parameters. These are a set
|
||||||
of constants which guarantee interoperability between nodes. The HTTP network map distributes the network parameters
|
of constants which guarantee interoperability between nodes. The HTTP network map distributes the network parameters
|
||||||
which the node downloads automatically. In the absence of this the network parameters must be generated locally. This can
|
which the node downloads automatically. In the absence of this the network parameters must be generated locally. This can
|
||||||
be done with the network bootstrapper. This a tool that scans all the node configurations from a common directory to
|
be done with the network bootstrapper. This is a tool that scans all the node configurations from a common directory to
|
||||||
generate the network parameters file which is copied to the nodes' directories. It also copies each node's node-info file
|
generate the network parameters file which is copied to the nodes' directories. It also copies each node's node-info file
|
||||||
to every other node.
|
to every other node so that they can all transact with each other.
|
||||||
|
|
||||||
The bootstrapper tool can be built with the command:
|
The bootstrapper tool can be built with the command:
|
||||||
|
|
||||||
@ -82,6 +82,56 @@ For example running the command on a directory containing these files :
|
|||||||
|
|
||||||
Would generate directories containing three nodes: notary, partya and partyb.
|
Would generate directories containing three nodes: notary, partya and partyb.
|
||||||
|
|
||||||
|
This tool only bootstraps a network. It cannot dynamically update if a new node needs to join the network or if an existing
|
||||||
|
one has changed something in their node-info, e.g. their P2P address. For this the new node-info file will need to be placed
|
||||||
|
in the other nodes' ``additional-node-infos`` directory. A simple way to do this is to use `rsync <https://en.wikipedia.org/wiki/Rsync>`_.
|
||||||
|
However, if it's known beforehand the set of nodes that will eventually the node folders can be pregenerated in the bootstrap
|
||||||
|
and only started when needed.
|
||||||
|
|
||||||
|
Whitelisting Contracts
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
If you want to create a *Zone whitelist* (see :doc:`api-contract-constraints`), you can pass in a list of CorDapp jars:
|
||||||
|
|
||||||
|
``java -jar network-bootstrapper.jar <nodes-root-dir> <path-to-first-corDapp> <path-to-second-corDapp> ..``
|
||||||
|
|
||||||
|
The CorDapp jars will be hashed and scanned for ``Contract`` classes.
|
||||||
|
By default the tool would generate a file named ``whitelist.txt`` containing an entry for each contract with the hash of the jar.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
.. sourcecode:: none
|
||||||
|
|
||||||
|
net.corda.finance.contracts.asset.Obligation:decd098666b9657314870e192ced0c3519c2c9d395507a238338f8d003929de8
|
||||||
|
net.corda.finance.contracts.asset.Cash:decd098666b9657314870e192ced0c3519c2c9d395507a238338f8d003929de9
|
||||||
|
|
||||||
|
These will be added to the ``NetworkParameters.whitelistedContractImplementations``. See :doc:`network-map`.
|
||||||
|
|
||||||
|
This means that by default the Network bootstrapper tool will whitelist all contracts found in all passed CorDapps.
|
||||||
|
|
||||||
|
In case there is a ``whitelist.txt`` file in the root dir already, the tool will append the new jar hashes or contracts to it.
|
||||||
|
|
||||||
|
The zone operator will maintain this whitelist file, and, using the tool, will append new versions of CorDapps to it.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
- The zone operator must ensure that this file is *append only*.
|
||||||
|
- If the operator removes hashes from the list, all transactions pointing to that version will suddenly fail the constraint verification, and the entire chain is compromised.
|
||||||
|
- If a contract is removed from the whitelist, then all states created from that moment on will be constrained by the HashAttachmentConstraint.
|
||||||
|
|
||||||
|
Note: In future releases, we will provider a tamper-proof way of maintaining the contract whitelist.
|
||||||
|
|
||||||
|
For fine-grained control of constraints, in case multiple contracts live in the same jar, the tool reads from another file:
|
||||||
|
``exclude_whitelist.txt``, which contains a list of contracts that should not be whitelisted, and thus default to the very restrictive:
|
||||||
|
``HashAttachmentConstraint``
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
.. sourcecode:: none
|
||||||
|
|
||||||
|
net.corda.finance.contracts.asset.Cash
|
||||||
|
net.corda.finance.contracts.asset.CommercialPaper
|
||||||
|
|
||||||
|
|
||||||
Starting the nodes
|
Starting the nodes
|
||||||
~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ The example CorDapp allows nodes to agree IOUs with each other, as long as they
|
|||||||
|
|
||||||
We will deploy and run the CorDapp on four test nodes:
|
We will deploy and run the CorDapp on four test nodes:
|
||||||
|
|
||||||
* **NetworkMapAndNotary**, which hosts a validating notary service
|
* **Notary**, which hosts a validating notary service
|
||||||
* **PartyA**
|
* **PartyA**
|
||||||
* **PartyB**
|
* **PartyB**
|
||||||
* **PartyC**
|
* **PartyC**
|
||||||
@ -245,7 +245,7 @@ For each node, the ``runnodes`` script creates a node tab/window:
|
|||||||
|
|
||||||
Fri Jul 07 10:33:47 BST 2017>>>
|
Fri Jul 07 10:33:47 BST 2017>>>
|
||||||
|
|
||||||
For every node except the network map/notary, the script also creates a webserver terminal tab/window:
|
For every node except the notary, the script also creates a webserver terminal tab/window:
|
||||||
|
|
||||||
.. sourcecode:: none
|
.. sourcecode:: none
|
||||||
|
|
||||||
@ -442,23 +442,27 @@ For more information on the client RPC interface and how to build an RPC client
|
|||||||
|
|
||||||
Running nodes across machines
|
Running nodes across machines
|
||||||
-----------------------------
|
-----------------------------
|
||||||
The nodes can be split across machines and configured to communicate across the network.
|
The nodes can be split across different machines and configured to communicate across the network.
|
||||||
|
|
||||||
After deploying the nodes, navigate to the build folder (``kotlin-source/build/nodes``) and move some of the individual
|
After deploying the nodes, navigate to the build folder (``kotlin-source/build/nodes``) and for each node that needs to
|
||||||
node folders to a different machine (e.g. using a USB key). It is important that none of the nodes - including the
|
be moved to another machine open its config file and change the Artemis messaging address to the IP address of the machine
|
||||||
network map/notary node - end up on more than one machine. Each computer should also have a copy of ``runnodes`` and
|
where the node will run (e.g. ``p2pAddress="10.18.0.166:10006"``).
|
||||||
``runnodes.bat``.
|
|
||||||
|
These changes require new node-info files to be distributed amongst the nodes. Use the network bootstrapper tool
|
||||||
|
(see :doc:`setting-up-a-corda-network` for more information on this and how to built it) to update the files and have
|
||||||
|
them distributed locally.
|
||||||
|
|
||||||
|
``java -jar network-bootstrapper.jar kotlin-source/build/nodes``
|
||||||
|
|
||||||
|
Once that's done move the node folders to their designated machines (e.g. using a USB key). It is important that none of the
|
||||||
|
nodes - including the notary - end up on more than one machine. Each computer should also have a copy of ``runnodes``
|
||||||
|
and ``runnodes.bat``.
|
||||||
|
|
||||||
For example, you may end up with the following layout:
|
For example, you may end up with the following layout:
|
||||||
|
|
||||||
* Machine 1: ``NetworkMapAndNotary``, ``PartyA``, ``runnodes``, ``runnodes.bat``
|
* Machine 1: ``Notary``, ``PartyA``, ``runnodes``, ``runnodes.bat``
|
||||||
* Machine 2: ``PartyB``, ``PartyC``, ``runnodes``, ``runnodes.bat``
|
* Machine 2: ``PartyB``, ``PartyC``, ``runnodes``, ``runnodes.bat``
|
||||||
|
|
||||||
You must now edit the configuration file for each node, including the network map/notary. Open each node's config file,
|
|
||||||
and make the following changes:
|
|
||||||
|
|
||||||
* Change the Artemis messaging address to the machine's IP address (e.g. ``p2pAddress="10.18.0.166:10006"``)
|
|
||||||
|
|
||||||
After starting each node, the nodes will be able to see one another and agree IOUs among themselves.
|
After starting each node, the nodes will be able to see one another and agree IOUs among themselves.
|
||||||
|
|
||||||
Testing and debugging
|
Testing and debugging
|
||||||
|
@ -53,3 +53,13 @@ inline fun NodeInfo.sign(signer: (PublicKey, SerializedBytes<NodeInfo>) -> Digit
|
|||||||
val signatures = owningKeys.map { signer(it, serialised) }
|
val signatures = owningKeys.map { signer(it, serialised) }
|
||||||
return SignedNodeInfo(serialised, signatures)
|
return SignedNodeInfo(serialised, signatures)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A container for a [SignedNodeInfo] and its cached [NodeInfo].
|
||||||
|
*/
|
||||||
|
class NodeInfoAndSigned private constructor(val nodeInfo: NodeInfo, val signed: SignedNodeInfo) {
|
||||||
|
constructor(nodeInfo: NodeInfo, signer: (PublicKey, SerializedBytes<NodeInfo>) -> DigitalSignature) : this(nodeInfo, nodeInfo.sign(signer))
|
||||||
|
constructor(signedNodeInfo: SignedNodeInfo) : this(signedNodeInfo.verified(), signedNodeInfo)
|
||||||
|
operator fun component1(): NodeInfo = nodeInfo
|
||||||
|
operator fun component2(): SignedNodeInfo = signed
|
||||||
|
}
|
||||||
|
@ -4,6 +4,7 @@ import com.google.common.hash.Hashing
|
|||||||
import com.google.common.hash.HashingInputStream
|
import com.google.common.hash.HashingInputStream
|
||||||
import com.typesafe.config.ConfigFactory
|
import com.typesafe.config.ConfigFactory
|
||||||
import net.corda.cordform.CordformNode
|
import net.corda.cordform.CordformNode
|
||||||
|
import net.corda.core.contracts.ContractClassName
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.SecureHash.Companion.parse
|
import net.corda.core.crypto.SecureHash.Companion.parse
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
@ -77,9 +78,9 @@ class NetworkBootstrapper {
|
|||||||
distributeNodeInfos(nodeDirs, nodeInfoFiles)
|
distributeNodeInfos(nodeDirs, nodeInfoFiles)
|
||||||
println("Gathering notary identities")
|
println("Gathering notary identities")
|
||||||
val notaryInfos = gatherNotaryInfos(nodeInfoFiles)
|
val notaryInfos = gatherNotaryInfos(nodeInfoFiles)
|
||||||
println("Notary identities to be used in network-parameters file: ${notaryInfos.joinToString("; ") { it.prettyPrint() }}")
|
println("Notary identities to be used in network parameters: ${notaryInfos.joinToString("; ") { it.prettyPrint() }}")
|
||||||
val mergedWhiteList = generateWhitelist(directory / WHITELIST_FILE_NAME, cordapps?.distinct())
|
val mergedWhiteList = generateWhitelist(directory / WHITELIST_FILE_NAME, cordapps?.distinct())
|
||||||
println("Updating whitelist.")
|
println("Updating whitelist")
|
||||||
overwriteWhitelist(directory / WHITELIST_FILE_NAME, mergedWhiteList)
|
overwriteWhitelist(directory / WHITELIST_FILE_NAME, mergedWhiteList)
|
||||||
installNetworkParameters(notaryInfos, nodeDirs, mergedWhiteList)
|
installNetworkParameters(notaryInfos, nodeDirs, mergedWhiteList)
|
||||||
println("Bootstrapping complete!")
|
println("Bootstrapping complete!")
|
||||||
@ -189,16 +190,18 @@ class NetworkBootstrapper {
|
|||||||
private fun generateWhitelist(whitelistFile: Path, cordapps: List<String>?): Map<String, List<AttachmentId>> {
|
private fun generateWhitelist(whitelistFile: Path, cordapps: List<String>?): Map<String, List<AttachmentId>> {
|
||||||
val existingWhitelist = if (whitelistFile.exists()) readContractWhitelist(whitelistFile) else emptyMap()
|
val existingWhitelist = if (whitelistFile.exists()) readContractWhitelist(whitelistFile) else emptyMap()
|
||||||
|
|
||||||
println("Found existing whitelist: $existingWhitelist")
|
println("Found existing whitelist:")
|
||||||
|
existingWhitelist.forEach { println(it.outputString()) }
|
||||||
|
|
||||||
val newWhiteList = cordapps?.flatMap { cordappJarPath ->
|
val newWhiteList: Map<ContractClassName, AttachmentId> = cordapps?.flatMap { cordappJarPath ->
|
||||||
val jarHash = getJarHash(cordappJarPath)
|
val jarHash = getJarHash(cordappJarPath)
|
||||||
scanJarForContracts(cordappJarPath).map { contract ->
|
scanJarForContracts(cordappJarPath).map { contract ->
|
||||||
contract to jarHash
|
contract to jarHash
|
||||||
}
|
}
|
||||||
}?.toMap() ?: emptyMap()
|
}?.toMap() ?: emptyMap()
|
||||||
|
|
||||||
println("Calculating whitelist for current cordapps: $newWhiteList")
|
println("Calculating whitelist for current CorDapps:")
|
||||||
|
newWhiteList.forEach { (contract, attachment) -> println("$contract:$attachment") }
|
||||||
|
|
||||||
val merged = (newWhiteList.keys + existingWhitelist.keys).map { contractClassName ->
|
val merged = (newWhiteList.keys + existingWhitelist.keys).map { contractClassName ->
|
||||||
val existing = existingWhitelist[contractClassName] ?: emptyList()
|
val existing = existingWhitelist[contractClassName] ?: emptyList()
|
||||||
@ -206,16 +209,15 @@ class NetworkBootstrapper {
|
|||||||
contractClassName to (if (newHash == null || newHash in existing) existing else existing + newHash)
|
contractClassName to (if (newHash == null || newHash in existing) existing else existing + newHash)
|
||||||
}.toMap()
|
}.toMap()
|
||||||
|
|
||||||
println("Final whitelist: $merged")
|
println("Final whitelist:")
|
||||||
|
merged.forEach { println(it.outputString()) }
|
||||||
|
|
||||||
return merged
|
return merged
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun overwriteWhitelist(whitelistFile: Path, mergedWhiteList: Map<String, List<AttachmentId>>) {
|
private fun overwriteWhitelist(whitelistFile: Path, mergedWhiteList: Map<String, List<AttachmentId>>) {
|
||||||
PrintStream(whitelistFile.toFile().outputStream()).use { out ->
|
PrintStream(whitelistFile.toFile().outputStream()).use { out ->
|
||||||
mergedWhiteList.forEach { (contract, attachments )->
|
mergedWhiteList.forEach { out.println(it.outputString()) }
|
||||||
out.println("${contract}:${attachments.joinToString(",")}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,15 +237,17 @@ class NetworkBootstrapper {
|
|||||||
|
|
||||||
private fun NodeInfo.notaryIdentity(): Party {
|
private fun NodeInfo.notaryIdentity(): Party {
|
||||||
return when (legalIdentities.size) {
|
return when (legalIdentities.size) {
|
||||||
// Single node notaries have just one identity like all other nodes. This identity is the notary identity
|
// Single node notaries have just one identity like all other nodes. This identity is the notary identity
|
||||||
1 -> legalIdentities[0]
|
1 -> legalIdentities[0]
|
||||||
// Nodes which are part of a distributed notary have a second identity which is the composite identity of the
|
// Nodes which are part of a distributed notary have a second identity which is the composite identity of the
|
||||||
// cluster and is shared by all the other members. This is the notary identity.
|
// cluster and is shared by all the other members. This is the notary identity.
|
||||||
2 -> legalIdentities[1]
|
2 -> legalIdentities[1]
|
||||||
else -> throw IllegalArgumentException("Not sure how to get the notary identity in this scenerio: $this")
|
else -> throw IllegalArgumentException("Not sure how to get the notary identity in this scenerio: $this")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Map.Entry<ContractClassName, List<AttachmentId>>.outputString() = "$key:${value.joinToString(",")}"
|
||||||
|
|
||||||
// We need to to set serialization env, because generation of parameters is run from Cordform.
|
// We need to to set serialization env, because generation of parameters is run from Cordform.
|
||||||
// KryoServerSerializationScheme is not accessible from nodeapi.
|
// KryoServerSerializationScheme is not accessible from nodeapi.
|
||||||
private fun initialiseSerialization() {
|
private fun initialiseSerialization() {
|
||||||
|
@ -7,7 +7,7 @@ import net.corda.core.internal.createDirectories
|
|||||||
import net.corda.core.internal.div
|
import net.corda.core.internal.div
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.node.services.KeyManagementService
|
import net.corda.core.node.services.KeyManagementService
|
||||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
||||||
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier
|
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier
|
||||||
import net.corda.testing.core.ALICE_NAME
|
import net.corda.testing.core.ALICE_NAME
|
||||||
import net.corda.testing.core.SerializationEnvironmentRule
|
import net.corda.testing.core.SerializationEnvironmentRule
|
||||||
@ -39,8 +39,7 @@ class NodeInfoWatcherTest {
|
|||||||
private val scheduler = TestScheduler()
|
private val scheduler = TestScheduler()
|
||||||
private val testSubscriber = TestSubscriber<NodeInfo>()
|
private val testSubscriber = TestSubscriber<NodeInfo>()
|
||||||
|
|
||||||
private lateinit var nodeInfo: NodeInfo
|
private lateinit var nodeInfoAndSigned: NodeInfoAndSigned
|
||||||
private lateinit var signedNodeInfo: SignedNodeInfo
|
|
||||||
private lateinit var nodeInfoPath: Path
|
private lateinit var nodeInfoPath: Path
|
||||||
private lateinit var keyManagementService: KeyManagementService
|
private lateinit var keyManagementService: KeyManagementService
|
||||||
|
|
||||||
@ -49,9 +48,7 @@ class NodeInfoWatcherTest {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun start() {
|
fun start() {
|
||||||
val nodeInfoAndSigned = createNodeInfoAndSigned(ALICE_NAME)
|
nodeInfoAndSigned = createNodeInfoAndSigned(ALICE_NAME)
|
||||||
nodeInfo = nodeInfoAndSigned.first
|
|
||||||
signedNodeInfo = nodeInfoAndSigned.second
|
|
||||||
val identityService = makeTestIdentityService()
|
val identityService = makeTestIdentityService()
|
||||||
keyManagementService = MockKeyManagementService(identityService)
|
keyManagementService = MockKeyManagementService(identityService)
|
||||||
nodeInfoWatcher = NodeInfoWatcher(tempFolder.root.toPath(), scheduler)
|
nodeInfoWatcher = NodeInfoWatcher(tempFolder.root.toPath(), scheduler)
|
||||||
@ -62,7 +59,7 @@ class NodeInfoWatcherTest {
|
|||||||
fun `save a NodeInfo`() {
|
fun `save a NodeInfo`() {
|
||||||
assertEquals(0,
|
assertEquals(0,
|
||||||
tempFolder.root.list().filter { it.startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) }.size)
|
tempFolder.root.list().filter { it.startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) }.size)
|
||||||
NodeInfoWatcher.saveToFile(tempFolder.root.toPath(), signedNodeInfo)
|
NodeInfoWatcher.saveToFile(tempFolder.root.toPath(), nodeInfoAndSigned)
|
||||||
|
|
||||||
val nodeInfoFiles = tempFolder.root.list().filter { it.startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) }
|
val nodeInfoFiles = tempFolder.root.list().filter { it.startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) }
|
||||||
assertEquals(1, nodeInfoFiles.size)
|
assertEquals(1, nodeInfoFiles.size)
|
||||||
@ -76,8 +73,8 @@ class NodeInfoWatcherTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `save a NodeInfo to JimFs`() {
|
fun `save a NodeInfo to JimFs`() {
|
||||||
val jimFs = Jimfs.newFileSystem(Configuration.unix())
|
val jimFs = Jimfs.newFileSystem(Configuration.unix())
|
||||||
val jimFolder = jimFs.getPath("/nodeInfo")
|
val jimFolder = jimFs.getPath("/nodeInfo").createDirectories()
|
||||||
NodeInfoWatcher.saveToFile(jimFolder, signedNodeInfo)
|
NodeInfoWatcher.saveToFile(jimFolder, nodeInfoAndSigned)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -104,7 +101,7 @@ class NodeInfoWatcherTest {
|
|||||||
try {
|
try {
|
||||||
val readNodes = testSubscriber.onNextEvents.distinct()
|
val readNodes = testSubscriber.onNextEvents.distinct()
|
||||||
assertEquals(1, readNodes.size)
|
assertEquals(1, readNodes.size)
|
||||||
assertEquals(nodeInfo, readNodes.first())
|
assertEquals(nodeInfoAndSigned.nodeInfo, readNodes.first())
|
||||||
} finally {
|
} finally {
|
||||||
subscription.unsubscribe()
|
subscription.unsubscribe()
|
||||||
}
|
}
|
||||||
@ -129,7 +126,7 @@ class NodeInfoWatcherTest {
|
|||||||
testSubscriber.awaitValueCount(1, 5, TimeUnit.SECONDS)
|
testSubscriber.awaitValueCount(1, 5, TimeUnit.SECONDS)
|
||||||
// The same folder can be reported more than once, so take unique values.
|
// The same folder can be reported more than once, so take unique values.
|
||||||
val readNodes = testSubscriber.onNextEvents.distinct()
|
val readNodes = testSubscriber.onNextEvents.distinct()
|
||||||
assertEquals(nodeInfo, readNodes.first())
|
assertEquals(nodeInfoAndSigned.nodeInfo, readNodes.first())
|
||||||
} finally {
|
} finally {
|
||||||
subscription.unsubscribe()
|
subscription.unsubscribe()
|
||||||
}
|
}
|
||||||
@ -141,6 +138,6 @@ class NodeInfoWatcherTest {
|
|||||||
|
|
||||||
// Write a nodeInfo under the right path.
|
// Write a nodeInfo under the right path.
|
||||||
private fun createNodeInfoFileInPath() {
|
private fun createNodeInfoFileInPath() {
|
||||||
NodeInfoWatcher.saveToFile(nodeInfoPath, signedNodeInfo)
|
NodeInfoWatcher.saveToFile(nodeInfoPath, nodeInfoAndSigned)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,9 +61,13 @@ import net.corda.node.utilities.AffinityExecutor
|
|||||||
import net.corda.node.utilities.JVMAgentRegistry
|
import net.corda.node.utilities.JVMAgentRegistry
|
||||||
import net.corda.node.utilities.NodeBuildProperties
|
import net.corda.node.utilities.NodeBuildProperties
|
||||||
import net.corda.nodeapi.internal.DevIdentityGenerator
|
import net.corda.nodeapi.internal.DevIdentityGenerator
|
||||||
|
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
||||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||||
import net.corda.nodeapi.internal.persistence.*
|
import net.corda.nodeapi.internal.persistence.*
|
||||||
import net.corda.nodeapi.internal.sign
|
import net.corda.nodeapi.internal.sign
|
||||||
|
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||||
|
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||||
|
import net.corda.nodeapi.internal.persistence.HibernateConfiguration
|
||||||
import net.corda.nodeapi.internal.storeLegalIdentity
|
import net.corda.nodeapi.internal.storeLegalIdentity
|
||||||
import org.apache.activemq.artemis.utils.ReusableLatch
|
import org.apache.activemq.artemis.utils.ReusableLatch
|
||||||
import org.hibernate.type.descriptor.java.JavaTypeDescriptorRegistry
|
import org.hibernate.type.descriptor.java.JavaTypeDescriptorRegistry
|
||||||
@ -181,11 +185,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
|||||||
val persistentNetworkMapCache = PersistentNetworkMapCache(database, notaries = emptyList())
|
val persistentNetworkMapCache = PersistentNetworkMapCache(database, notaries = emptyList())
|
||||||
persistentNetworkMapCache.start()
|
persistentNetworkMapCache.start()
|
||||||
val (keyPairs, nodeInfo) = initNodeInfo(persistentNetworkMapCache, identity, identityKeyPair)
|
val (keyPairs, nodeInfo) = initNodeInfo(persistentNetworkMapCache, identity, identityKeyPair)
|
||||||
val signedNodeInfo = nodeInfo.sign { publicKey, serialised ->
|
val nodeInfoAndSigned = NodeInfoAndSigned(nodeInfo) { publicKey, serialised ->
|
||||||
val privateKey = keyPairs.single { it.public == publicKey }.private
|
val privateKey = keyPairs.single { it.public == publicKey }.private
|
||||||
privateKey.sign(serialised.bytes)
|
privateKey.sign(serialised.bytes)
|
||||||
}
|
}
|
||||||
NodeInfoWatcher.saveToFile(configuration.baseDirectory, signedNodeInfo)
|
NodeInfoWatcher.saveToFile(configuration.baseDirectory, nodeInfoAndSigned)
|
||||||
nodeInfo
|
nodeInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -265,11 +269,12 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
|||||||
configuration.baseDirectory)
|
configuration.baseDirectory)
|
||||||
runOnStop += networkMapUpdater::close
|
runOnStop += networkMapUpdater::close
|
||||||
|
|
||||||
networkMapUpdater.updateNodeInfo(services.myInfo) {
|
log.info("Node-info for this node: ${services.myInfo}")
|
||||||
it.sign { publicKey, serialised ->
|
|
||||||
services.keyManagementService.sign(serialised.bytes, publicKey).withoutKey()
|
val nodeInfoAndSigned = NodeInfoAndSigned(services.myInfo) { publicKey, serialised ->
|
||||||
}
|
services.keyManagementService.sign(serialised.bytes, publicKey).withoutKey()
|
||||||
}
|
}
|
||||||
|
networkMapUpdater.updateNodeInfo(nodeInfoAndSigned)
|
||||||
networkMapUpdater.subscribeToNetworkMap()
|
networkMapUpdater.subscribeToNetworkMap()
|
||||||
|
|
||||||
// If we successfully loaded network data from database, we set this future to Unit.
|
// If we successfully loaded network data from database, we set this future to Unit.
|
||||||
|
@ -168,7 +168,7 @@ open class Node(configuration: NodeConfiguration,
|
|||||||
val advertisedAddress = info.addresses[0]
|
val advertisedAddress = info.addresses[0]
|
||||||
bridgeControlListener = BridgeControlListener(configuration, serverAddress, networkParameters.maxMessageSize)
|
bridgeControlListener = BridgeControlListener(configuration, serverAddress, networkParameters.maxMessageSize)
|
||||||
|
|
||||||
printBasicNodeInfo("Incoming connection address", advertisedAddress.toString())
|
printBasicNodeInfo("Advertised P2P messaging addresses", info.addresses.joinToString())
|
||||||
|
|
||||||
val rpcServerConfiguration = RPCServerConfiguration.default.copy(
|
val rpcServerConfiguration = RPCServerConfiguration.default.copy(
|
||||||
rpcThreadPoolSize = configuration.enterpriseConfiguration.tuning.rpcThreadPoolSize
|
rpcThreadPoolSize = configuration.enterpriseConfiguration.tuning.rpcThreadPoolSize
|
||||||
|
@ -120,7 +120,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
|
|||||||
}
|
}
|
||||||
// Config driven switch between legacy CORE bridges and the newer AMQP protocol bridges.
|
// Config driven switch between legacy CORE bridges and the newer AMQP protocol bridges.
|
||||||
activeMQServer.start()
|
activeMQServer.start()
|
||||||
Node.printBasicNodeInfo("Listening on port", p2pPort.toString())
|
log.info("P2P messaging server listening on port $p2pPort")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createArtemisConfig() = SecureArtemisConfiguration().apply {
|
private fun createArtemisConfig() = SecureArtemisConfiguration().apply {
|
||||||
|
@ -7,12 +7,12 @@ import net.corda.core.internal.copyTo
|
|||||||
import net.corda.core.internal.div
|
import net.corda.core.internal.div
|
||||||
import net.corda.core.messaging.DataFeed
|
import net.corda.core.messaging.DataFeed
|
||||||
import net.corda.core.messaging.ParametersUpdateInfo
|
import net.corda.core.messaging.ParametersUpdateInfo
|
||||||
import net.corda.core.node.NodeInfo
|
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.core.utilities.minutes
|
import net.corda.core.utilities.minutes
|
||||||
import net.corda.node.services.api.NetworkMapCacheInternal
|
import net.corda.node.services.api.NetworkMapCacheInternal
|
||||||
import net.corda.node.utilities.NamedThreadFactory
|
import net.corda.node.utilities.NamedThreadFactory
|
||||||
|
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
||||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||||
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME
|
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME
|
||||||
import net.corda.nodeapi.internal.network.ParametersUpdate
|
import net.corda.nodeapi.internal.network.ParametersUpdate
|
||||||
@ -54,18 +54,19 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
|
|||||||
return DataFeed(currentUpdateInfo, parametersUpdatesTrack)
|
return DataFeed(currentUpdateInfo, parametersUpdatesTrack)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateNodeInfo(newInfo: NodeInfo, signer: (NodeInfo) -> SignedNodeInfo) {
|
fun updateNodeInfo(nodeInfoAndSigned: NodeInfoAndSigned) {
|
||||||
val oldInfo = networkMapCache.getNodeByLegalIdentity(newInfo.legalIdentities.first())
|
// TODO We've already done this lookup and check in AbstractNode.initNodeInfo
|
||||||
|
val oldNodeInfo = networkMapCache.getNodeByLegalIdentity(nodeInfoAndSigned.nodeInfo.legalIdentities[0])
|
||||||
// Compare node info without timestamp.
|
// Compare node info without timestamp.
|
||||||
if (newInfo.copy(serial = 0L) == oldInfo?.copy(serial = 0L)) return
|
if (nodeInfoAndSigned.nodeInfo.copy(serial = 0L) == oldNodeInfo?.copy(serial = 0L)) return
|
||||||
|
|
||||||
|
logger.info("Node-info has changed so submitting update. Old node-info was $oldNodeInfo")
|
||||||
// Only publish and write to disk if there are changes to the node info.
|
// Only publish and write to disk if there are changes to the node info.
|
||||||
val signedNodeInfo = signer(newInfo)
|
networkMapCache.addNode(nodeInfoAndSigned.nodeInfo)
|
||||||
networkMapCache.addNode(newInfo)
|
fileWatcher.saveToFile(nodeInfoAndSigned)
|
||||||
fileWatcher.saveToFile(signedNodeInfo)
|
|
||||||
|
|
||||||
if (networkMapClient != null) {
|
if (networkMapClient != null) {
|
||||||
tryPublishNodeInfoAsync(signedNodeInfo, networkMapClient)
|
tryPublishNodeInfoAsync(nodeInfoAndSigned.signed, networkMapClient)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,17 +4,26 @@ import net.corda.cordform.CordformNode
|
|||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.internal.*
|
import net.corda.core.internal.*
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
|
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
|
||||||
|
import net.corda.core.serialization.internal._contextSerializationEnv
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.core.utilities.seconds
|
import net.corda.core.utilities.seconds
|
||||||
|
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
||||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||||
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier
|
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier
|
||||||
|
import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
|
||||||
|
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
||||||
|
import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Scheduler
|
import rx.Scheduler
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.stream.Stream
|
||||||
import kotlin.streams.toList
|
import kotlin.streams.toList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -31,34 +40,29 @@ import kotlin.streams.toList
|
|||||||
class NodeInfoWatcher(private val nodePath: Path,
|
class NodeInfoWatcher(private val nodePath: Path,
|
||||||
private val scheduler: Scheduler,
|
private val scheduler: Scheduler,
|
||||||
private val pollInterval: Duration = 5.seconds) {
|
private val pollInterval: Duration = 5.seconds) {
|
||||||
private val nodeInfoDirectory = nodePath / CordformNode.NODE_INFO_DIRECTORY
|
|
||||||
private val processedNodeInfoFiles = mutableSetOf<Path>()
|
|
||||||
private val _processedNodeInfoHashes = mutableSetOf<SecureHash>()
|
|
||||||
val processedNodeInfoHashes: Set<SecureHash> get() = _processedNodeInfoHashes.toSet()
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val logger = contextLogger()
|
private val logger = contextLogger()
|
||||||
/**
|
|
||||||
* Saves the given [NodeInfo] to a path.
|
// TODO This method doesn't belong in this class
|
||||||
* The node is 'encoded' as a SignedNodeInfo, signed with the owning key of its first identity.
|
fun saveToFile(path: Path, nodeInfoAndSigned: NodeInfoAndSigned) {
|
||||||
* The name of the written file will be "nodeInfo-" followed by the hash of the content. The hash in the filename
|
// By using the hash of the node's first name we ensure:
|
||||||
* is used so that one can freely copy these files without fearing to overwrite another one.
|
// 1) node info files for the same node map to the same filename and thus avoid having duplicate files for
|
||||||
*
|
// the same node
|
||||||
* @param path the path where to write the file, if non-existent it will be created.
|
// 2) avoid having to deal with characters in the X.500 name which are incompatible with the local filesystem
|
||||||
* @param signedNodeInfo the signed NodeInfo.
|
val fileNameHash = nodeInfoAndSigned.nodeInfo.legalIdentities[0].name.serialize().hash
|
||||||
*/
|
nodeInfoAndSigned
|
||||||
fun saveToFile(path: Path, signedNodeInfo: SignedNodeInfo) {
|
.signed
|
||||||
try {
|
.serialize()
|
||||||
path.createDirectories()
|
.open()
|
||||||
signedNodeInfo.serialize()
|
.copyTo(path / "${NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX}$fileNameHash", REPLACE_EXISTING)
|
||||||
.open()
|
|
||||||
.copyTo(path / "${NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX}${signedNodeInfo.raw.hash}")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logger.warn("Couldn't write node info to file", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val nodeInfoDirectory = nodePath / CordformNode.NODE_INFO_DIRECTORY
|
||||||
|
|
||||||
|
private val _processedNodeInfoHashes = HashSet<SecureHash>()
|
||||||
|
val processedNodeInfoHashes: Set<SecureHash> get() = _processedNodeInfoHashes
|
||||||
|
|
||||||
init {
|
init {
|
||||||
require(pollInterval >= 5.seconds) { "Poll interval must be 5 seconds or longer." }
|
require(pollInterval >= 5.seconds) { "Poll interval must be 5 seconds or longer." }
|
||||||
if (!nodeInfoDirectory.isDirectory()) {
|
if (!nodeInfoDirectory.isDirectory()) {
|
||||||
@ -84,7 +88,10 @@ class NodeInfoWatcher(private val nodePath: Path,
|
|||||||
.flatMapIterable { loadFromDirectory() }
|
.flatMapIterable { loadFromDirectory() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveToFile(signedNodeInfo: SignedNodeInfo) = Companion.saveToFile(nodePath, signedNodeInfo)
|
// TODO This method doesn't belong in this class
|
||||||
|
fun saveToFile(nodeInfoAndSigned: NodeInfoAndSigned) {
|
||||||
|
return Companion.saveToFile(nodePath, nodeInfoAndSigned)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads all the files contained in a given path and returns the deserialized [NodeInfo]s.
|
* Loads all the files contained in a given path and returns the deserialized [NodeInfo]s.
|
||||||
@ -97,16 +104,15 @@ class NodeInfoWatcher(private val nodePath: Path,
|
|||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
val result = nodeInfoDirectory.list { paths ->
|
val result = nodeInfoDirectory.list { paths ->
|
||||||
paths.filter { it !in processedNodeInfoFiles }
|
paths
|
||||||
.filter { it.isRegularFile() }
|
.filter { it.isRegularFile() }
|
||||||
.map { path ->
|
.flatMap { path ->
|
||||||
processFile(path)?.apply {
|
val nodeInfo = processFile(path)?.let {
|
||||||
processedNodeInfoFiles.add(path)
|
if (_processedNodeInfoHashes.add(it.signed.raw.hash)) it.nodeInfo else null
|
||||||
_processedNodeInfoHashes.add(this.serialize().hash)
|
|
||||||
}
|
}
|
||||||
|
if (nodeInfo != null) Stream.of(nodeInfo) else Stream.empty()
|
||||||
}
|
}
|
||||||
.toList()
|
.toList()
|
||||||
.filterNotNull()
|
|
||||||
}
|
}
|
||||||
if (result.isNotEmpty()) {
|
if (result.isNotEmpty()) {
|
||||||
logger.info("Successfully read ${result.size} NodeInfo files from disk.")
|
logger.info("Successfully read ${result.size} NodeInfo files from disk.")
|
||||||
@ -114,14 +120,25 @@ class NodeInfoWatcher(private val nodePath: Path,
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processFile(file: Path): NodeInfo? {
|
private fun processFile(file: Path): NodeInfoAndSigned? {
|
||||||
return try {
|
return try {
|
||||||
logger.info("Reading NodeInfo from file: $file")
|
logger.info("Reading NodeInfo from file: $file")
|
||||||
val signedData = file.readObject<SignedNodeInfo>()
|
val signedNodeInfo = file.readObject<SignedNodeInfo>()
|
||||||
signedData.verified()
|
NodeInfoAndSigned(signedNodeInfo)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.warn("Exception parsing NodeInfo from file. $file", e)
|
logger.warn("Exception parsing NodeInfo from file. $file", e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO Remove this once we have a tool that can read AMQP serialised files
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
_contextSerializationEnv.set(SerializationEnvironmentImpl(
|
||||||
|
SerializationFactoryImpl().apply {
|
||||||
|
registerScheme(AMQPServerSerializationScheme())
|
||||||
|
},
|
||||||
|
AMQP_P2P_CONTEXT)
|
||||||
|
)
|
||||||
|
println(Paths.get(args[0]).readObject<SignedNodeInfo>().verified())
|
||||||
|
}
|
||||||
|
@ -18,6 +18,7 @@ import net.corda.core.node.NodeInfo
|
|||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.utilities.millis
|
import net.corda.core.utilities.millis
|
||||||
import net.corda.node.services.api.NetworkMapCacheInternal
|
import net.corda.node.services.api.NetworkMapCacheInternal
|
||||||
|
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
||||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||||
import net.corda.nodeapi.internal.createDevNetworkMapCa
|
import net.corda.nodeapi.internal.createDevNetworkMapCa
|
||||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||||
@ -68,33 +69,33 @@ class NetworkMapUpdaterTest {
|
|||||||
fun `publish node info`() {
|
fun `publish node info`() {
|
||||||
nodeInfoBuilder.addIdentity(ALICE_NAME)
|
nodeInfoBuilder.addIdentity(ALICE_NAME)
|
||||||
|
|
||||||
val (nodeInfo1, signedNodeInfo1) = nodeInfoBuilder.buildWithSigned()
|
val nodeInfo1AndSigned = nodeInfoBuilder.buildWithSigned()
|
||||||
val (sameNodeInfoDifferentTime, signedSameNodeInfoDifferentTime) = nodeInfoBuilder.buildWithSigned(serial = System.currentTimeMillis())
|
val sameNodeInfoDifferentTimeAndSigned = nodeInfoBuilder.buildWithSigned(serial = System.currentTimeMillis())
|
||||||
|
|
||||||
// Publish node info for the first time.
|
// Publish node info for the first time.
|
||||||
updater.updateNodeInfo(nodeInfo1) { signedNodeInfo1 }
|
updater.updateNodeInfo(nodeInfo1AndSigned)
|
||||||
// Sleep as publish is asynchronous.
|
// Sleep as publish is asynchronous.
|
||||||
// TODO: Remove sleep in unit test
|
// TODO: Remove sleep in unit test
|
||||||
Thread.sleep(2L * cacheExpiryMs)
|
Thread.sleep(2L * cacheExpiryMs)
|
||||||
verify(networkMapClient, times(1)).publish(any())
|
verify(networkMapClient, times(1)).publish(any())
|
||||||
|
|
||||||
networkMapCache.addNode(nodeInfo1)
|
networkMapCache.addNode(nodeInfo1AndSigned.nodeInfo)
|
||||||
|
|
||||||
// Publish the same node info, but with different serial.
|
// Publish the same node info, but with different serial.
|
||||||
updater.updateNodeInfo(sameNodeInfoDifferentTime) { signedSameNodeInfoDifferentTime }
|
updater.updateNodeInfo(sameNodeInfoDifferentTimeAndSigned)
|
||||||
// TODO: Remove sleep in unit test.
|
// TODO: Remove sleep in unit test.
|
||||||
Thread.sleep(2L * cacheExpiryMs)
|
Thread.sleep(2L * cacheExpiryMs)
|
||||||
|
|
||||||
// Same node info should not publish twice
|
// Same node info should not publish twice
|
||||||
verify(networkMapClient, times(0)).publish(signedSameNodeInfoDifferentTime)
|
verify(networkMapClient, times(0)).publish(sameNodeInfoDifferentTimeAndSigned.signed)
|
||||||
|
|
||||||
val (differentNodeInfo, signedDifferentNodeInfo) = createNodeInfoAndSigned("Bob")
|
val differentNodeInfoAndSigned = createNodeInfoAndSigned("Bob")
|
||||||
|
|
||||||
// Publish different node info.
|
// Publish different node info.
|
||||||
updater.updateNodeInfo(differentNodeInfo) { signedDifferentNodeInfo }
|
updater.updateNodeInfo(differentNodeInfoAndSigned)
|
||||||
// TODO: Remove sleep in unit test.
|
// TODO: Remove sleep in unit test.
|
||||||
Thread.sleep(200)
|
Thread.sleep(200)
|
||||||
verify(networkMapClient, times(1)).publish(signedDifferentNodeInfo)
|
verify(networkMapClient, times(1)).publish(differentNodeInfoAndSigned.signed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -103,7 +104,7 @@ class NetworkMapUpdaterTest {
|
|||||||
val (nodeInfo2, signedNodeInfo2) = createNodeInfoAndSigned("Info 2")
|
val (nodeInfo2, signedNodeInfo2) = createNodeInfoAndSigned("Info 2")
|
||||||
val (nodeInfo3, signedNodeInfo3) = createNodeInfoAndSigned("Info 3")
|
val (nodeInfo3, signedNodeInfo3) = createNodeInfoAndSigned("Info 3")
|
||||||
val (nodeInfo4, signedNodeInfo4) = createNodeInfoAndSigned("Info 4")
|
val (nodeInfo4, signedNodeInfo4) = createNodeInfoAndSigned("Info 4")
|
||||||
val (fileNodeInfo, signedFileNodeInfo) = createNodeInfoAndSigned("Info from file")
|
val fileNodeInfoAndSigned = createNodeInfoAndSigned("Info from file")
|
||||||
|
|
||||||
// Test adding new node.
|
// Test adding new node.
|
||||||
networkMapClient.publish(signedNodeInfo1)
|
networkMapClient.publish(signedNodeInfo1)
|
||||||
@ -119,7 +120,7 @@ class NetworkMapUpdaterTest {
|
|||||||
verify(networkMapCache, times(1)).addNode(nodeInfo1)
|
verify(networkMapCache, times(1)).addNode(nodeInfo1)
|
||||||
verify(networkMapCache, times(1)).addNode(nodeInfo2)
|
verify(networkMapCache, times(1)).addNode(nodeInfo2)
|
||||||
|
|
||||||
NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, signedFileNodeInfo)
|
NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, fileNodeInfoAndSigned)
|
||||||
networkMapClient.publish(signedNodeInfo3)
|
networkMapClient.publish(signedNodeInfo3)
|
||||||
networkMapClient.publish(signedNodeInfo4)
|
networkMapClient.publish(signedNodeInfo4)
|
||||||
|
|
||||||
@ -131,7 +132,7 @@ class NetworkMapUpdaterTest {
|
|||||||
verify(networkMapCache, times(5)).addNode(any())
|
verify(networkMapCache, times(5)).addNode(any())
|
||||||
verify(networkMapCache, times(1)).addNode(nodeInfo3)
|
verify(networkMapCache, times(1)).addNode(nodeInfo3)
|
||||||
verify(networkMapCache, times(1)).addNode(nodeInfo4)
|
verify(networkMapCache, times(1)).addNode(nodeInfo4)
|
||||||
verify(networkMapCache, times(1)).addNode(fileNodeInfo)
|
verify(networkMapCache, times(1)).addNode(fileNodeInfoAndSigned.nodeInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -140,10 +141,10 @@ class NetworkMapUpdaterTest {
|
|||||||
val (nodeInfo2, signedNodeInfo2) = createNodeInfoAndSigned("Info 2")
|
val (nodeInfo2, signedNodeInfo2) = createNodeInfoAndSigned("Info 2")
|
||||||
val (nodeInfo3, signedNodeInfo3) = createNodeInfoAndSigned("Info 3")
|
val (nodeInfo3, signedNodeInfo3) = createNodeInfoAndSigned("Info 3")
|
||||||
val (nodeInfo4, signedNodeInfo4) = createNodeInfoAndSigned("Info 4")
|
val (nodeInfo4, signedNodeInfo4) = createNodeInfoAndSigned("Info 4")
|
||||||
val (fileNodeInfo, signedFileNodeInfo) = createNodeInfoAndSigned("Info from file")
|
val fileNodeInfoAndSigned = createNodeInfoAndSigned("Info from file")
|
||||||
|
|
||||||
// Add all nodes.
|
// Add all nodes.
|
||||||
NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, signedFileNodeInfo)
|
NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, fileNodeInfoAndSigned)
|
||||||
networkMapClient.publish(signedNodeInfo1)
|
networkMapClient.publish(signedNodeInfo1)
|
||||||
networkMapClient.publish(signedNodeInfo2)
|
networkMapClient.publish(signedNodeInfo2)
|
||||||
networkMapClient.publish(signedNodeInfo3)
|
networkMapClient.publish(signedNodeInfo3)
|
||||||
@ -157,7 +158,7 @@ class NetworkMapUpdaterTest {
|
|||||||
// 4 node info from network map, and 1 from file.
|
// 4 node info from network map, and 1 from file.
|
||||||
assertThat(nodeInfoMap).hasSize(4)
|
assertThat(nodeInfoMap).hasSize(4)
|
||||||
verify(networkMapCache, times(5)).addNode(any())
|
verify(networkMapCache, times(5)).addNode(any())
|
||||||
verify(networkMapCache, times(1)).addNode(fileNodeInfo)
|
verify(networkMapCache, times(1)).addNode(fileNodeInfoAndSigned.nodeInfo)
|
||||||
|
|
||||||
// Test remove node.
|
// Test remove node.
|
||||||
nodeInfoMap.clear()
|
nodeInfoMap.clear()
|
||||||
@ -170,25 +171,25 @@ class NetworkMapUpdaterTest {
|
|||||||
verify(networkMapCache, times(1)).removeNode(nodeInfo4)
|
verify(networkMapCache, times(1)).removeNode(nodeInfo4)
|
||||||
|
|
||||||
// Node info from file should not be deleted
|
// Node info from file should not be deleted
|
||||||
assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfo.serialize().hash)
|
assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfoAndSigned.nodeInfo.serialize().hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `receive node infos from directory, without a network map`() {
|
fun `receive node infos from directory, without a network map`() {
|
||||||
val (fileNodeInfo, signedFileNodeInfo) = createNodeInfoAndSigned("Info from file")
|
val fileNodeInfoAndSigned = createNodeInfoAndSigned("Info from file")
|
||||||
|
|
||||||
// Not subscribed yet.
|
// Not subscribed yet.
|
||||||
verify(networkMapCache, times(0)).addNode(any())
|
verify(networkMapCache, times(0)).addNode(any())
|
||||||
|
|
||||||
updater.subscribeToNetworkMap()
|
updater.subscribeToNetworkMap()
|
||||||
|
|
||||||
NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, signedFileNodeInfo)
|
NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, fileNodeInfoAndSigned)
|
||||||
scheduler.advanceTimeBy(10, TimeUnit.SECONDS)
|
scheduler.advanceTimeBy(10, TimeUnit.SECONDS)
|
||||||
|
|
||||||
verify(networkMapCache, times(1)).addNode(any())
|
verify(networkMapCache, times(1)).addNode(any())
|
||||||
verify(networkMapCache, times(1)).addNode(fileNodeInfo)
|
verify(networkMapCache, times(1)).addNode(fileNodeInfoAndSigned.nodeInfo)
|
||||||
|
|
||||||
assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfo.serialize().hash)
|
assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfoAndSigned.nodeInfo.serialize().hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -275,7 +276,7 @@ class NetworkMapUpdaterTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createNodeInfoAndSigned(org: String): Pair<NodeInfo, SignedNodeInfo> {
|
private fun createNodeInfoAndSigned(org: String): NodeInfoAndSigned {
|
||||||
return createNodeInfoAndSigned(CordaX500Name(org, "London", "GB"))
|
return createNodeInfoAndSigned(CordaX500Name(org, "London", "GB"))
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,6 +7,7 @@ import net.corda.core.identity.PartyAndCertificate
|
|||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
|
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
||||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||||
import net.corda.nodeapi.internal.createDevNodeCa
|
import net.corda.nodeapi.internal.createDevNodeCa
|
||||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||||
@ -47,10 +48,11 @@ class TestNodeInfoBuilder(private val intermediateAndRoot: Pair<CertificateAndKe
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildWithSigned(serial: Long = 1, platformVersion: Int = 1): Pair<NodeInfo, SignedNodeInfo> {
|
fun buildWithSigned(serial: Long = 1, platformVersion: Int = 1): NodeInfoAndSigned {
|
||||||
val nodeInfo = build(serial, platformVersion)
|
val nodeInfo = build(serial, platformVersion)
|
||||||
val privateKeys = identitiesAndPrivateKeys.map { it.second }
|
return NodeInfoAndSigned(nodeInfo) { publicKey, serialised ->
|
||||||
return Pair(nodeInfo, nodeInfo.signWith(privateKeys))
|
identitiesAndPrivateKeys.first { it.first.owningKey == publicKey }.second.sign(serialised.bytes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
@ -58,7 +60,7 @@ class TestNodeInfoBuilder(private val intermediateAndRoot: Pair<CertificateAndKe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createNodeInfoAndSigned(vararg names: CordaX500Name, serial: Long = 1, platformVersion: Int = 1): Pair<NodeInfo, SignedNodeInfo> {
|
fun createNodeInfoAndSigned(vararg names: CordaX500Name, serial: Long = 1, platformVersion: Int = 1): NodeInfoAndSigned {
|
||||||
val nodeInfoBuilder = TestNodeInfoBuilder()
|
val nodeInfoBuilder = TestNodeInfoBuilder()
|
||||||
names.forEach { nodeInfoBuilder.addIdentity(it) }
|
names.forEach { nodeInfoBuilder.addIdentity(it) }
|
||||||
return nodeInfoBuilder.buildWithSigned(serial, platformVersion)
|
return nodeInfoBuilder.buildWithSigned(serial, platformVersion)
|
||||||
|
Loading…
Reference in New Issue
Block a user