CORDA-1048: Making it simpler to move an existing local deployment of nodes to across different machines. (#2672)

This was achieved by having the hash in the node-info file to be just of the node's X.500 name. This also solves existing duplicate node-info file issues that we've been having.

Also updated the docsite.
This commit is contained in:
Shams Asari 2018-03-01 21:24:10 +00:00 committed by GitHub
parent 14cafee66e
commit 8616f24523
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 162 additions and 122 deletions

View File

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

View File

@ -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:

View File

@ -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

View File

@ -57,9 +57,9 @@ in its local network map cache. The node generates its own node-info file on sta
In addition to the network map, all the nodes on a network must use the same set of network parameters. These are a set
of constants which guarantee interoperability between nodes. The HTTP network map distributes the network parameters
which the node downloads automatically. In the absence of this the network parameters must be generated locally. This can
be done with the network bootstrapper. This a tool that scans all the node configurations from a common directory to
be done with the network bootstrapper. This is a tool that scans all the node configurations from a common directory to
generate the network parameters file which is copied to the nodes' directories. It also copies each node's node-info file
to every other node.
to every other node so that they can all transact with each other.
The bootstrapper tool can be built with the command:
@ -82,6 +82,11 @@ For example running the command on a directory containing these files :
Would generate directories containing three nodes: notary, partya and partyb.
This tool only bootstraps a network. It cannot dynamically update if a new node needs to join the network or if an existing
one has changed something in their node-info, e.g. their P2P address. For this the new node-info file will need to be placed
in the other nodes' ``additional-node-infos`` directory. A simple way to do this is to use `rsync <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
~~~~~~~~~~~~~~~~~~~~~~

View File

@ -16,7 +16,7 @@ The example CorDapp allows nodes to agree IOUs with each other, as long as they
We will deploy and run the CorDapp on four test nodes:
* **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

View File

@ -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
}

View File

@ -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()) }
}
}
@ -235,15 +237,17 @@ class NetworkBootstrapper {
private fun NodeInfo.notaryIdentity(): Party {
return when (legalIdentities.size) {
// Single node notaries have just one identity like all other nodes. This identity is the notary identity
// Single node notaries have just one identity like all other nodes. This identity is the notary identity
1 -> legalIdentities[0]
// Nodes which are part of a distributed notary have a second identity which is the composite identity of the
// cluster and is shared by all the other members. This is the notary identity.
// Nodes which are part of a distributed notary have a second identity which is the composite identity of the
// cluster and is shared by all the other members. This is the notary identity.
2 -> legalIdentities[1]
else -> throw IllegalArgumentException("Not sure how to get the notary identity in this scenerio: $this")
}
}
private fun Map.Entry<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() {

View File

@ -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)
}
}

View File

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

View File

@ -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)
}
}

View File

@ -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()
.open()
.copyTo(path / "${NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX}${signedNodeInfo.raw.hash}")
} catch (e: Exception) {
logger.warn("Couldn't write node info to file", e)
}
// TODO This method doesn't belong in this class
fun saveToFile(path: Path, nodeInfoAndSigned: NodeInfoAndSigned) {
// By using the hash of the node's first name we ensure:
// 1) node info files for the same node map to the same filename and thus avoid having duplicate files for
// the same node
// 2) avoid having to deal with characters in the X.500 name which are incompatible with the local filesystem
val fileNameHash = nodeInfoAndSigned.nodeInfo.legalIdentities[0].name.serialize().hash
nodeInfoAndSigned
.signed
.serialize()
.open()
.copyTo(path / "${NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX}$fileNameHash", REPLACE_EXISTING)
}
}
private val nodeInfoDirectory = nodePath / CordformNode.NODE_INFO_DIRECTORY
private val _processedNodeInfoHashes = HashSet<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())
}

View File

@ -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"))
}
}

View File

@ -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)