diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index a4deaac21e..01b615c417 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -71,7 +71,7 @@ absolute path to the node's base directory. :p2pAddress: The host and port on which the node is available for protocol operations over ArtemisMQ. .. note:: In practice the ArtemisMQ messaging services bind to all local addresses on the specified port. However, - note that the host is the included as the advertised entry in the NetworkMapService. As a result the value listed + note that the host is the included as the advertised entry in the network map. As a result the value listed here must be externally accessible when running nodes across a cluster of machines. If the provided host is unreachable, the node will try to auto-discover its public one. diff --git a/docs/source/generating-a-node.rst b/docs/source/generating-a-node.rst index c506e2aab4..6c9d901235 100644 --- a/docs/source/generating-a-node.rst +++ b/docs/source/generating-a-node.rst @@ -1,8 +1,6 @@ Creating nodes locally ====================== -.. contents:: - Node structure -------------- Each Corda node has the following structure: @@ -91,8 +89,8 @@ The OID and format for these extensions will be described in a further specifica The Cordform task ----------------- Corda provides a gradle plugin called ``Cordform`` that allows you to automatically generate and configure a set of -nodes. Here is an example ``Cordform`` task called ``deployNodes`` that creates three nodes, defined in the -`Kotlin CorDapp Template `_: +nodes for testing and demos. Here is an example ``Cordform`` task called ``deployNodes`` that creates three nodes, defined +in the `Kotlin CorDapp Template `_: .. sourcecode:: groovy @@ -155,7 +153,7 @@ You can extend ``deployNodes`` to generate additional nodes. .. warning:: When adding nodes, make sure that there are no port clashes! Specifying a custom webserver ------------------------------ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ By default, any node listing a webport will use the default development webserver, which is not production-ready. You can use your own webserver JAR instead by using the ``webserverJar`` argument in a ``Cordform`` ``node`` configuration block: @@ -174,7 +172,7 @@ The webserver JAR will be copied into the node's ``build`` folder with the name node's ``node.conf`` file. Running deployNodes -------------------- +~~~~~~~~~~~~~~~~~~~ To create the nodes defined in our ``deployNodes`` task, run the following command in a terminal window from the root of the project where the ``deployNodes`` task is defined: diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index ac42ce69cd..765c936c22 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -21,7 +21,7 @@ service. task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" node { - name "O=NetworkMapAndNotary,L=London,C=GB" + name "O=Notary,L=London,C=GB" notary = [validating : true] p2pPort 10002 rpcPort 10003 @@ -142,7 +142,7 @@ The vaults of PartyA and PartyB should both display the following output: - "C=GB,L=London,O=PartyA" - "C=US,L=New York,O=PartyB" contract: "com.template.contract.IOUContract" - notary: "C=GB,L=London,O=NetworkMapAndNotary,CN=corda.notary.validating" + notary: "C=GB,L=London,O=Notary" encumbrance: null constraint: attachmentId: "F578320232CAB87BB1E919F3E5DB9D81B7346F9D7EA6D9155DC0F7BA8E472552" @@ -157,7 +157,7 @@ The vaults of PartyA and PartyB should both display the following output: recordedTime: 1506415268.875000000 consumedTime: null status: "UNCONSUMED" - notary: "C=GB,L=London,O=NetworkMapAndNotary,CN=corda.notary.validating" + notary: "C=GB,L=London,O=Notary" lockId: null lockUpdateTime: 1506415269.548000000 totalStatesAvailable: -1 diff --git a/docs/source/setting-up-a-corda-network.rst b/docs/source/setting-up-a-corda-network.rst index fad6eb9898..146904ffd4 100644 --- a/docs/source/setting-up-a-corda-network.rst +++ b/docs/source/setting-up-a-corda-network.rst @@ -57,9 +57,9 @@ in its local network map cache. The node generates its own node-info file on sta In addition to the network map, all the nodes on a network must use the same set of network parameters. These are a set of constants which guarantee interoperability between nodes. The HTTP network map distributes the network parameters which the node downloads automatically. In the absence of this the network parameters must be generated locally. This can -be done with the network bootstrapper. This a tool that scans all the node configurations from a common directory to +be done with the network bootstrapper. This is a tool that scans all the node configurations from a common directory to generate the network parameters file which is copied to the nodes' directories. It also copies each node's node-info file -to every other node. +to every other node so that they can all transact with each other. The bootstrapper tool can be built with the command: @@ -82,6 +82,11 @@ For example running the command on a directory containing these files : Would generate directories containing three nodes: notary, partya and partyb. +This tool only bootstraps a network. It cannot dynamically update if a new node needs to join the network or if an existing +one has changed something in their node-info, e.g. their P2P address. For this the new node-info file will need to be placed +in the other nodes' ``additional-node-infos`` directory. A simple way to do this is to use `rsync `_. +However, if it's known beforehand the set of nodes that will eventually the node folders can be pregenerated in the bootstrap +and only started when needed. Whitelisting Contracts ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/source/tutorial-cordapp.rst b/docs/source/tutorial-cordapp.rst index de6d19b43f..d64cc06718 100644 --- a/docs/source/tutorial-cordapp.rst +++ b/docs/source/tutorial-cordapp.rst @@ -16,7 +16,7 @@ The example CorDapp allows nodes to agree IOUs with each other, as long as they We will deploy and run the CorDapp on four test nodes: -* **NetworkMapAndNotary**, which hosts a validating notary service +* **Notary**, which hosts a validating notary service * **PartyA** * **PartyB** * **PartyC** @@ -245,7 +245,7 @@ For each node, the ``runnodes`` script creates a node tab/window: Fri Jul 07 10:33:47 BST 2017>>> -For every node except the network map/notary, the script also creates a webserver terminal tab/window: +For every node except the notary, the script also creates a webserver terminal tab/window: .. sourcecode:: none @@ -442,23 +442,27 @@ For more information on the client RPC interface and how to build an RPC client Running nodes across machines ----------------------------- -The nodes can be split across machines and configured to communicate across the network. +The nodes can be split across different machines and configured to communicate across the network. -After deploying the nodes, navigate to the build folder (``kotlin-source/build/nodes``) and move some of the individual -node folders to a different machine (e.g. using a USB key). It is important that none of the nodes - including the -network map/notary node - end up on more than one machine. Each computer should also have a copy of ``runnodes`` and -``runnodes.bat``. +After deploying the nodes, navigate to the build folder (``kotlin-source/build/nodes``) and for each node that needs to +be moved to another machine open its config file and change the Artemis messaging address to the IP address of the machine +where the node will run (e.g. ``p2pAddress="10.18.0.166:10006"``). + +These changes require new node-info files to be distributed amongst the nodes. Use the network bootstrapper tool +(see :doc:`setting-up-a-corda-network` for more information on this and how to built it) to update the files and have +them distributed locally. + +``java -jar network-bootstrapper.jar kotlin-source/build/nodes`` + +Once that's done move the node folders to their designated machines (e.g. using a USB key). It is important that none of the +nodes - including the notary - end up on more than one machine. Each computer should also have a copy of ``runnodes`` +and ``runnodes.bat``. For example, you may end up with the following layout: -* Machine 1: ``NetworkMapAndNotary``, ``PartyA``, ``runnodes``, ``runnodes.bat`` +* Machine 1: ``Notary``, ``PartyA``, ``runnodes``, ``runnodes.bat`` * Machine 2: ``PartyB``, ``PartyC``, ``runnodes``, ``runnodes.bat`` -You must now edit the configuration file for each node, including the network map/notary. Open each node's config file, -and make the following changes: - -* Change the Artemis messaging address to the machine's IP address (e.g. ``p2pAddress="10.18.0.166:10006"``) - After starting each node, the nodes will be able to see one another and agree IOUs among themselves. Testing and debugging diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/SignedNodeInfo.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/SignedNodeInfo.kt index 3dea456a05..b1fb7054d4 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/SignedNodeInfo.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/SignedNodeInfo.kt @@ -53,3 +53,13 @@ inline fun NodeInfo.sign(signer: (PublicKey, SerializedBytes) -> Digit val signatures = owningKeys.map { signer(it, serialised) } return SignedNodeInfo(serialised, signatures) } + +/** + * A container for a [SignedNodeInfo] and its cached [NodeInfo]. + */ +class NodeInfoAndSigned private constructor(val nodeInfo: NodeInfo, val signed: SignedNodeInfo) { + constructor(nodeInfo: NodeInfo, signer: (PublicKey, SerializedBytes) -> DigitalSignature) : this(nodeInfo, nodeInfo.sign(signer)) + constructor(signedNodeInfo: SignedNodeInfo) : this(signedNodeInfo.verified(), signedNodeInfo) + operator fun component1(): NodeInfo = nodeInfo + operator fun component2(): SignedNodeInfo = signed +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt index cc4a91f4f8..2e1598d197 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt @@ -4,6 +4,7 @@ import com.google.common.hash.Hashing import com.google.common.hash.HashingInputStream import com.typesafe.config.ConfigFactory import net.corda.cordform.CordformNode +import net.corda.core.contracts.ContractClassName import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash.Companion.parse import net.corda.core.identity.Party @@ -77,9 +78,9 @@ class NetworkBootstrapper { distributeNodeInfos(nodeDirs, nodeInfoFiles) println("Gathering notary identities") val notaryInfos = gatherNotaryInfos(nodeInfoFiles) - println("Notary identities to be used in network-parameters file: ${notaryInfos.joinToString("; ") { it.prettyPrint() }}") + println("Notary identities to be used in network parameters: ${notaryInfos.joinToString("; ") { it.prettyPrint() }}") val mergedWhiteList = generateWhitelist(directory / WHITELIST_FILE_NAME, cordapps?.distinct()) - println("Updating whitelist.") + println("Updating whitelist") overwriteWhitelist(directory / WHITELIST_FILE_NAME, mergedWhiteList) installNetworkParameters(notaryInfos, nodeDirs, mergedWhiteList) println("Bootstrapping complete!") @@ -189,16 +190,18 @@ class NetworkBootstrapper { private fun generateWhitelist(whitelistFile: Path, cordapps: List?): Map> { val existingWhitelist = if (whitelistFile.exists()) readContractWhitelist(whitelistFile) else emptyMap() - println("Found existing whitelist: $existingWhitelist") + println("Found existing whitelist:") + existingWhitelist.forEach { println(it.outputString()) } - val newWhiteList = cordapps?.flatMap { cordappJarPath -> + val newWhiteList: Map = cordapps?.flatMap { cordappJarPath -> val jarHash = getJarHash(cordappJarPath) scanJarForContracts(cordappJarPath).map { contract -> contract to jarHash } }?.toMap() ?: emptyMap() - println("Calculating whitelist for current cordapps: $newWhiteList") + println("Calculating whitelist for current CorDapps:") + newWhiteList.forEach { (contract, attachment) -> println("$contract:$attachment") } val merged = (newWhiteList.keys + existingWhitelist.keys).map { contractClassName -> val existing = existingWhitelist[contractClassName] ?: emptyList() @@ -206,16 +209,15 @@ class NetworkBootstrapper { contractClassName to (if (newHash == null || newHash in existing) existing else existing + newHash) }.toMap() - println("Final whitelist: $merged") + println("Final whitelist:") + merged.forEach { println(it.outputString()) } return merged } private fun overwriteWhitelist(whitelistFile: Path, mergedWhiteList: Map>) { PrintStream(whitelistFile.toFile().outputStream()).use { out -> - mergedWhiteList.forEach { (contract, attachments )-> - out.println("${contract}:${attachments.joinToString(",")}") - } + mergedWhiteList.forEach { out.println(it.outputString()) } } } @@ -235,15 +237,17 @@ class NetworkBootstrapper { private fun NodeInfo.notaryIdentity(): Party { return when (legalIdentities.size) { - // Single node notaries have just one identity like all other nodes. This identity is the notary identity + // Single node notaries have just one identity like all other nodes. This identity is the notary identity 1 -> legalIdentities[0] - // Nodes which are part of a distributed notary have a second identity which is the composite identity of the - // cluster and is shared by all the other members. This is the notary identity. + // Nodes which are part of a distributed notary have a second identity which is the composite identity of the + // cluster and is shared by all the other members. This is the notary identity. 2 -> legalIdentities[1] else -> throw IllegalArgumentException("Not sure how to get the notary identity in this scenerio: $this") } } + private fun Map.Entry>.outputString() = "$key:${value.joinToString(",")}" + // We need to to set serialization env, because generation of parameters is run from Cordform. // KryoServerSerializationScheme is not accessible from nodeapi. private fun initialiseSerialization() { diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt index 07ad4bf3a3..da2a56fa05 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NodeInfoWatcherTest.kt @@ -7,7 +7,7 @@ import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.node.NodeInfo import net.corda.core.node.services.KeyManagementService -import net.corda.nodeapi.internal.SignedNodeInfo +import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.network.NodeInfoFilesCopier import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.SerializationEnvironmentRule @@ -39,8 +39,7 @@ class NodeInfoWatcherTest { private val scheduler = TestScheduler() private val testSubscriber = TestSubscriber() - private lateinit var nodeInfo: NodeInfo - private lateinit var signedNodeInfo: SignedNodeInfo + private lateinit var nodeInfoAndSigned: NodeInfoAndSigned private lateinit var nodeInfoPath: Path private lateinit var keyManagementService: KeyManagementService @@ -49,9 +48,7 @@ class NodeInfoWatcherTest { @Before fun start() { - val nodeInfoAndSigned = createNodeInfoAndSigned(ALICE_NAME) - nodeInfo = nodeInfoAndSigned.first - signedNodeInfo = nodeInfoAndSigned.second + nodeInfoAndSigned = createNodeInfoAndSigned(ALICE_NAME) val identityService = makeTestIdentityService() keyManagementService = MockKeyManagementService(identityService) nodeInfoWatcher = NodeInfoWatcher(tempFolder.root.toPath(), scheduler) @@ -62,7 +59,7 @@ class NodeInfoWatcherTest { fun `save a NodeInfo`() { assertEquals(0, tempFolder.root.list().filter { it.startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) }.size) - NodeInfoWatcher.saveToFile(tempFolder.root.toPath(), signedNodeInfo) + NodeInfoWatcher.saveToFile(tempFolder.root.toPath(), nodeInfoAndSigned) val nodeInfoFiles = tempFolder.root.list().filter { it.startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) } assertEquals(1, nodeInfoFiles.size) @@ -76,8 +73,8 @@ class NodeInfoWatcherTest { @Test fun `save a NodeInfo to JimFs`() { val jimFs = Jimfs.newFileSystem(Configuration.unix()) - val jimFolder = jimFs.getPath("/nodeInfo") - NodeInfoWatcher.saveToFile(jimFolder, signedNodeInfo) + val jimFolder = jimFs.getPath("/nodeInfo").createDirectories() + NodeInfoWatcher.saveToFile(jimFolder, nodeInfoAndSigned) } @Test @@ -104,7 +101,7 @@ class NodeInfoWatcherTest { try { val readNodes = testSubscriber.onNextEvents.distinct() assertEquals(1, readNodes.size) - assertEquals(nodeInfo, readNodes.first()) + assertEquals(nodeInfoAndSigned.nodeInfo, readNodes.first()) } finally { subscription.unsubscribe() } @@ -129,7 +126,7 @@ class NodeInfoWatcherTest { testSubscriber.awaitValueCount(1, 5, TimeUnit.SECONDS) // The same folder can be reported more than once, so take unique values. val readNodes = testSubscriber.onNextEvents.distinct() - assertEquals(nodeInfo, readNodes.first()) + assertEquals(nodeInfoAndSigned.nodeInfo, readNodes.first()) } finally { subscription.unsubscribe() } @@ -141,6 +138,6 @@ class NodeInfoWatcherTest { // Write a nodeInfo under the right path. private fun createNodeInfoFileInPath() { - NodeInfoWatcher.saveToFile(nodeInfoPath, signedNodeInfo) + NodeInfoWatcher.saveToFile(nodeInfoPath, nodeInfoAndSigned) } } diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 0e3b1f2824..3ea2500904 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -61,11 +61,11 @@ import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.JVMAgentRegistry import net.corda.node.utilities.NodeBuildProperties import net.corda.nodeapi.internal.DevIdentityGenerator +import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.HibernateConfiguration -import net.corda.nodeapi.internal.sign import net.corda.nodeapi.internal.storeLegalIdentity import org.apache.activemq.artemis.utils.ReusableLatch import org.hibernate.type.descriptor.java.JavaTypeDescriptorRegistry @@ -181,11 +181,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val persistentNetworkMapCache = PersistentNetworkMapCache(database, notaries = emptyList()) persistentNetworkMapCache.start() val (keyPairs, nodeInfo) = initNodeInfo(persistentNetworkMapCache, identity, identityKeyPair) - val signedNodeInfo = nodeInfo.sign { publicKey, serialised -> + val nodeInfoAndSigned = NodeInfoAndSigned(nodeInfo) { publicKey, serialised -> val privateKey = keyPairs.single { it.public == publicKey }.private privateKey.sign(serialised.bytes) } - NodeInfoWatcher.saveToFile(configuration.baseDirectory, signedNodeInfo) + NodeInfoWatcher.saveToFile(configuration.baseDirectory, nodeInfoAndSigned) nodeInfo } } @@ -268,11 +268,12 @@ abstract class AbstractNode(val configuration: NodeConfiguration, configuration.baseDirectory) runOnStop += networkMapUpdater::close - networkMapUpdater.updateNodeInfo(services.myInfo) { - it.sign { publicKey, serialised -> - services.keyManagementService.sign(serialised.bytes, publicKey).withoutKey() - } + log.info("Node-info for this node: ${services.myInfo}") + + val nodeInfoAndSigned = NodeInfoAndSigned(services.myInfo) { publicKey, serialised -> + services.keyManagementService.sign(serialised.bytes, publicKey).withoutKey() } + networkMapUpdater.updateNodeInfo(nodeInfoAndSigned) networkMapUpdater.subscribeToNetworkMap() // If we successfully loaded network data from database, we set this future to Unit. diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt index 414f0f15c3..9013785798 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt @@ -7,12 +7,12 @@ import net.corda.core.internal.copyTo import net.corda.core.internal.div import net.corda.core.messaging.DataFeed import net.corda.core.messaging.ParametersUpdateInfo -import net.corda.core.node.NodeInfo import net.corda.core.serialization.serialize import net.corda.core.utilities.contextLogger import net.corda.core.utilities.minutes import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.utilities.NamedThreadFactory +import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME import net.corda.nodeapi.internal.network.ParametersUpdate @@ -54,18 +54,19 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, return DataFeed(currentUpdateInfo, parametersUpdatesTrack) } - fun updateNodeInfo(newInfo: NodeInfo, signer: (NodeInfo) -> SignedNodeInfo) { - val oldInfo = networkMapCache.getNodeByLegalIdentity(newInfo.legalIdentities.first()) + fun updateNodeInfo(nodeInfoAndSigned: NodeInfoAndSigned) { + // TODO We've already done this lookup and check in AbstractNode.initNodeInfo + val oldNodeInfo = networkMapCache.getNodeByLegalIdentity(nodeInfoAndSigned.nodeInfo.legalIdentities[0]) // Compare node info without timestamp. - if (newInfo.copy(serial = 0L) == oldInfo?.copy(serial = 0L)) return + if (nodeInfoAndSigned.nodeInfo.copy(serial = 0L) == oldNodeInfo?.copy(serial = 0L)) return + logger.info("Node-info has changed so submitting update. Old node-info was $oldNodeInfo") // Only publish and write to disk if there are changes to the node info. - val signedNodeInfo = signer(newInfo) - networkMapCache.addNode(newInfo) - fileWatcher.saveToFile(signedNodeInfo) + networkMapCache.addNode(nodeInfoAndSigned.nodeInfo) + fileWatcher.saveToFile(nodeInfoAndSigned) if (networkMapClient != null) { - tryPublishNodeInfoAsync(signedNodeInfo, networkMapClient) + tryPublishNodeInfoAsync(nodeInfoAndSigned.signed, networkMapClient) } } diff --git a/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt b/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt index aa55fb2552..5059276ed5 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NodeInfoWatcher.kt @@ -4,17 +4,26 @@ import net.corda.cordform.CordformNode import net.corda.core.crypto.SecureHash import net.corda.core.internal.* import net.corda.core.node.NodeInfo +import net.corda.core.serialization.internal.SerializationEnvironmentImpl +import net.corda.core.serialization.internal._contextSerializationEnv import net.corda.core.serialization.serialize import net.corda.core.utilities.contextLogger import net.corda.core.utilities.seconds +import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.network.NodeInfoFilesCopier +import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT +import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl +import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme import rx.Observable import rx.Scheduler import java.io.IOException import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.StandardCopyOption.REPLACE_EXISTING import java.time.Duration import java.util.concurrent.TimeUnit +import java.util.stream.Stream import kotlin.streams.toList /** @@ -31,34 +40,29 @@ import kotlin.streams.toList class NodeInfoWatcher(private val nodePath: Path, private val scheduler: Scheduler, private val pollInterval: Duration = 5.seconds) { - private val nodeInfoDirectory = nodePath / CordformNode.NODE_INFO_DIRECTORY - private val processedNodeInfoFiles = mutableSetOf() - private val _processedNodeInfoHashes = mutableSetOf() - val processedNodeInfoHashes: Set get() = _processedNodeInfoHashes.toSet() - companion object { private val logger = contextLogger() - /** - * Saves the given [NodeInfo] to a path. - * The node is 'encoded' as a SignedNodeInfo, signed with the owning key of its first identity. - * The name of the written file will be "nodeInfo-" followed by the hash of the content. The hash in the filename - * is used so that one can freely copy these files without fearing to overwrite another one. - * - * @param path the path where to write the file, if non-existent it will be created. - * @param signedNodeInfo the signed NodeInfo. - */ - fun saveToFile(path: Path, signedNodeInfo: SignedNodeInfo) { - try { - path.createDirectories() - signedNodeInfo.serialize() - .open() - .copyTo(path / "${NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX}${signedNodeInfo.raw.hash}") - } catch (e: Exception) { - logger.warn("Couldn't write node info to file", e) - } + + // TODO This method doesn't belong in this class + fun saveToFile(path: Path, nodeInfoAndSigned: NodeInfoAndSigned) { + // By using the hash of the node's first name we ensure: + // 1) node info files for the same node map to the same filename and thus avoid having duplicate files for + // the same node + // 2) avoid having to deal with characters in the X.500 name which are incompatible with the local filesystem + val fileNameHash = nodeInfoAndSigned.nodeInfo.legalIdentities[0].name.serialize().hash + nodeInfoAndSigned + .signed + .serialize() + .open() + .copyTo(path / "${NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX}$fileNameHash", REPLACE_EXISTING) } } + private val nodeInfoDirectory = nodePath / CordformNode.NODE_INFO_DIRECTORY + + private val _processedNodeInfoHashes = HashSet() + val processedNodeInfoHashes: Set get() = _processedNodeInfoHashes + init { require(pollInterval >= 5.seconds) { "Poll interval must be 5 seconds or longer." } if (!nodeInfoDirectory.isDirectory()) { @@ -84,7 +88,10 @@ class NodeInfoWatcher(private val nodePath: Path, .flatMapIterable { loadFromDirectory() } } - fun saveToFile(signedNodeInfo: SignedNodeInfo) = Companion.saveToFile(nodePath, signedNodeInfo) + // TODO This method doesn't belong in this class + fun saveToFile(nodeInfoAndSigned: NodeInfoAndSigned) { + return Companion.saveToFile(nodePath, nodeInfoAndSigned) + } /** * Loads all the files contained in a given path and returns the deserialized [NodeInfo]s. @@ -97,16 +104,15 @@ class NodeInfoWatcher(private val nodePath: Path, return emptyList() } val result = nodeInfoDirectory.list { paths -> - paths.filter { it !in processedNodeInfoFiles } + paths .filter { it.isRegularFile() } - .map { path -> - processFile(path)?.apply { - processedNodeInfoFiles.add(path) - _processedNodeInfoHashes.add(this.serialize().hash) + .flatMap { path -> + val nodeInfo = processFile(path)?.let { + if (_processedNodeInfoHashes.add(it.signed.raw.hash)) it.nodeInfo else null } + if (nodeInfo != null) Stream.of(nodeInfo) else Stream.empty() } .toList() - .filterNotNull() } if (result.isNotEmpty()) { logger.info("Successfully read ${result.size} NodeInfo files from disk.") @@ -114,14 +120,25 @@ class NodeInfoWatcher(private val nodePath: Path, return result } - private fun processFile(file: Path): NodeInfo? { + private fun processFile(file: Path): NodeInfoAndSigned? { return try { logger.info("Reading NodeInfo from file: $file") - val signedData = file.readObject() - signedData.verified() + val signedNodeInfo = file.readObject() + NodeInfoAndSigned(signedNodeInfo) } catch (e: Exception) { logger.warn("Exception parsing NodeInfo from file. $file", e) null } } } + +// TODO Remove this once we have a tool that can read AMQP serialised files +fun main(args: Array) { + _contextSerializationEnv.set(SerializationEnvironmentImpl( + SerializationFactoryImpl().apply { + registerScheme(AMQPServerSerializationScheme()) + }, + AMQP_P2P_CONTEXT) + ) + println(Paths.get(args[0]).readObject().verified()) +} diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt index 0647f46253..2e77979c24 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt @@ -18,6 +18,7 @@ import net.corda.core.node.NodeInfo import net.corda.core.serialization.serialize import net.corda.core.utilities.millis import net.corda.node.services.api.NetworkMapCacheInternal +import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.createDevNetworkMapCa import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair @@ -68,33 +69,33 @@ class NetworkMapUpdaterTest { fun `publish node info`() { nodeInfoBuilder.addIdentity(ALICE_NAME) - val (nodeInfo1, signedNodeInfo1) = nodeInfoBuilder.buildWithSigned() - val (sameNodeInfoDifferentTime, signedSameNodeInfoDifferentTime) = nodeInfoBuilder.buildWithSigned(serial = System.currentTimeMillis()) + val nodeInfo1AndSigned = nodeInfoBuilder.buildWithSigned() + val sameNodeInfoDifferentTimeAndSigned = nodeInfoBuilder.buildWithSigned(serial = System.currentTimeMillis()) // Publish node info for the first time. - updater.updateNodeInfo(nodeInfo1) { signedNodeInfo1 } + updater.updateNodeInfo(nodeInfo1AndSigned) // Sleep as publish is asynchronous. // TODO: Remove sleep in unit test Thread.sleep(2L * cacheExpiryMs) verify(networkMapClient, times(1)).publish(any()) - networkMapCache.addNode(nodeInfo1) + networkMapCache.addNode(nodeInfo1AndSigned.nodeInfo) // Publish the same node info, but with different serial. - updater.updateNodeInfo(sameNodeInfoDifferentTime) { signedSameNodeInfoDifferentTime } + updater.updateNodeInfo(sameNodeInfoDifferentTimeAndSigned) // TODO: Remove sleep in unit test. Thread.sleep(2L * cacheExpiryMs) // Same node info should not publish twice - verify(networkMapClient, times(0)).publish(signedSameNodeInfoDifferentTime) + verify(networkMapClient, times(0)).publish(sameNodeInfoDifferentTimeAndSigned.signed) - val (differentNodeInfo, signedDifferentNodeInfo) = createNodeInfoAndSigned("Bob") + val differentNodeInfoAndSigned = createNodeInfoAndSigned("Bob") // Publish different node info. - updater.updateNodeInfo(differentNodeInfo) { signedDifferentNodeInfo } + updater.updateNodeInfo(differentNodeInfoAndSigned) // TODO: Remove sleep in unit test. Thread.sleep(200) - verify(networkMapClient, times(1)).publish(signedDifferentNodeInfo) + verify(networkMapClient, times(1)).publish(differentNodeInfoAndSigned.signed) } @Test @@ -103,7 +104,7 @@ class NetworkMapUpdaterTest { val (nodeInfo2, signedNodeInfo2) = createNodeInfoAndSigned("Info 2") val (nodeInfo3, signedNodeInfo3) = createNodeInfoAndSigned("Info 3") val (nodeInfo4, signedNodeInfo4) = createNodeInfoAndSigned("Info 4") - val (fileNodeInfo, signedFileNodeInfo) = createNodeInfoAndSigned("Info from file") + val fileNodeInfoAndSigned = createNodeInfoAndSigned("Info from file") // Test adding new node. networkMapClient.publish(signedNodeInfo1) @@ -119,7 +120,7 @@ class NetworkMapUpdaterTest { verify(networkMapCache, times(1)).addNode(nodeInfo1) verify(networkMapCache, times(1)).addNode(nodeInfo2) - NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, signedFileNodeInfo) + NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, fileNodeInfoAndSigned) networkMapClient.publish(signedNodeInfo3) networkMapClient.publish(signedNodeInfo4) @@ -131,7 +132,7 @@ class NetworkMapUpdaterTest { verify(networkMapCache, times(5)).addNode(any()) verify(networkMapCache, times(1)).addNode(nodeInfo3) verify(networkMapCache, times(1)).addNode(nodeInfo4) - verify(networkMapCache, times(1)).addNode(fileNodeInfo) + verify(networkMapCache, times(1)).addNode(fileNodeInfoAndSigned.nodeInfo) } @Test @@ -140,10 +141,10 @@ class NetworkMapUpdaterTest { val (nodeInfo2, signedNodeInfo2) = createNodeInfoAndSigned("Info 2") val (nodeInfo3, signedNodeInfo3) = createNodeInfoAndSigned("Info 3") val (nodeInfo4, signedNodeInfo4) = createNodeInfoAndSigned("Info 4") - val (fileNodeInfo, signedFileNodeInfo) = createNodeInfoAndSigned("Info from file") + val fileNodeInfoAndSigned = createNodeInfoAndSigned("Info from file") // Add all nodes. - NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, signedFileNodeInfo) + NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, fileNodeInfoAndSigned) networkMapClient.publish(signedNodeInfo1) networkMapClient.publish(signedNodeInfo2) networkMapClient.publish(signedNodeInfo3) @@ -157,7 +158,7 @@ class NetworkMapUpdaterTest { // 4 node info from network map, and 1 from file. assertThat(nodeInfoMap).hasSize(4) verify(networkMapCache, times(5)).addNode(any()) - verify(networkMapCache, times(1)).addNode(fileNodeInfo) + verify(networkMapCache, times(1)).addNode(fileNodeInfoAndSigned.nodeInfo) // Test remove node. nodeInfoMap.clear() @@ -170,25 +171,25 @@ class NetworkMapUpdaterTest { verify(networkMapCache, times(1)).removeNode(nodeInfo4) // Node info from file should not be deleted - assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfo.serialize().hash) + assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfoAndSigned.nodeInfo.serialize().hash) } @Test fun `receive node infos from directory, without a network map`() { - val (fileNodeInfo, signedFileNodeInfo) = createNodeInfoAndSigned("Info from file") + val fileNodeInfoAndSigned = createNodeInfoAndSigned("Info from file") // Not subscribed yet. verify(networkMapCache, times(0)).addNode(any()) updater.subscribeToNetworkMap() - NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, signedFileNodeInfo) + NodeInfoWatcher.saveToFile(baseDir / NODE_INFO_DIRECTORY, fileNodeInfoAndSigned) scheduler.advanceTimeBy(10, TimeUnit.SECONDS) verify(networkMapCache, times(1)).addNode(any()) - verify(networkMapCache, times(1)).addNode(fileNodeInfo) + verify(networkMapCache, times(1)).addNode(fileNodeInfoAndSigned.nodeInfo) - assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfo.serialize().hash) + assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfoAndSigned.nodeInfo.serialize().hash) } @Test @@ -275,7 +276,7 @@ class NetworkMapUpdaterTest { } } - private fun createNodeInfoAndSigned(org: String): Pair { + private fun createNodeInfoAndSigned(org: String): NodeInfoAndSigned { return createNodeInfoAndSigned(CordaX500Name(org, "London", "GB")) } } \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt index 8757adde2b..c311fbaeea 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt @@ -7,6 +7,7 @@ import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.NodeInfo import net.corda.core.serialization.serialize import net.corda.core.utilities.NetworkHostAndPort +import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.createDevNodeCa import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair @@ -47,10 +48,11 @@ class TestNodeInfoBuilder(private val intermediateAndRoot: Pair { + fun buildWithSigned(serial: Long = 1, platformVersion: Int = 1): NodeInfoAndSigned { val nodeInfo = build(serial, platformVersion) - val privateKeys = identitiesAndPrivateKeys.map { it.second } - return Pair(nodeInfo, nodeInfo.signWith(privateKeys)) + return NodeInfoAndSigned(nodeInfo) { publicKey, serialised -> + identitiesAndPrivateKeys.first { it.first.owningKey == publicKey }.second.sign(serialised.bytes) + } } fun reset() { @@ -58,7 +60,7 @@ class TestNodeInfoBuilder(private val intermediateAndRoot: Pair { +fun createNodeInfoAndSigned(vararg names: CordaX500Name, serial: Long = 1, platformVersion: Int = 1): NodeInfoAndSigned { val nodeInfoBuilder = TestNodeInfoBuilder() names.forEach { nodeInfoBuilder.addIdentity(it) } return nodeInfoBuilder.buildWithSigned(serial, platformVersion)