From 720bb55827ff84fc8f749f34aa22263a72fd1657 Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Mon, 21 Nov 2016 16:39:46 +0000 Subject: [PATCH 1/2] docs: setting-up-a-corda-network --- docs/generate-docsite.sh | 2 +- .../src/main/resources/example-node.conf | 11 ++++ .../net/corda/docs/ExampleNodeConfTest.kt | 23 +++++++ docs/source/setting-up-a-corda-network.rst | 63 +++++++++++++++++++ docs/source/tutorial-clientrpc-api.rst | 4 +- 5 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 docs/source/example-code/src/main/resources/example-node.conf create mode 100644 docs/source/example-code/src/test/kotlin/net/corda/docs/ExampleNodeConfTest.kt create mode 100644 docs/source/setting-up-a-corda-network.rst diff --git a/docs/generate-docsite.sh b/docs/generate-docsite.sh index b3bc4405ee..51c3e4e1d8 100755 --- a/docs/generate-docsite.sh +++ b/docs/generate-docsite.sh @@ -21,7 +21,7 @@ fi virtualenv -p python2.7 virtualenv fi . virtualenv/bin/activate - if [ ! -d "docs/virtualenv/lib/python2.7/site-packages/sphinx" ] + if [ ! -d "virtualenv/lib/python2.7/site-packages/sphinx" ] then echo "Installing pip dependencies ... " pip install -r requirements.txt diff --git a/docs/source/example-code/src/main/resources/example-node.conf b/docs/source/example-code/src/main/resources/example-node.conf new file mode 100644 index 0000000000..402694896a --- /dev/null +++ b/docs/source/example-code/src/main/resources/example-node.conf @@ -0,0 +1,11 @@ +basedir : "./standalone/regular-node" +myLegalName : "Some Node" +nearestCity : "London" +keyStorePassword : "cordacadevpass" +trustStorePassword : "trustpass" +artemisAddress : "cordaload-node1:31337" +webAddress : "localhost:31339" +extraAdvertisedServiceIds: "" +useHTTPS : false +devMode : false +networkMapAddress : "cordaload-nameserver:31337" diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/ExampleNodeConfTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/ExampleNodeConfTest.kt new file mode 100644 index 0000000000..b4ab14cca8 --- /dev/null +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/ExampleNodeConfTest.kt @@ -0,0 +1,23 @@ +package net.corda.docs + +import net.corda.node.services.config.ConfigHelper +import net.corda.node.services.config.FullNodeConfiguration +import org.junit.Test +import java.nio.file.Paths +import kotlin.reflect.declaredMemberProperties + +class ExampleNodeConfTest { + @Test + fun exampleNodeConfParsesFine() { + val configResource = ExampleNodeConfTest::class.java.classLoader.getResource("example-node.conf") + val nodeConfig = FullNodeConfiguration( + ConfigHelper.loadConfig( + baseDirectoryPath = Paths.get("some-example-base-dir"), + configFileOverride = Paths.get(configResource.toURI()) + ) + ) + nodeConfig.javaClass.kotlin.declaredMemberProperties.forEach { member -> + member.get(nodeConfig) + } + } +} \ No newline at end of file diff --git a/docs/source/setting-up-a-corda-network.rst b/docs/source/setting-up-a-corda-network.rst new file mode 100644 index 0000000000..448cee750d --- /dev/null +++ b/docs/source/setting-up-a-corda-network.rst @@ -0,0 +1,63 @@ +A Corda network + + +Introduction - What is a corda network? +======================================================== + +A Corda network consists of a number of machines running ``node``s, including a single node operating as the network map service. These nodes communicate using persistent protocols in order to create and validate transactions. + +There are four broader categories of functionality one such node may have. These pieces of functionality are provided as services, and one node may run several of them. + +* Network map: The node running the network map provides a way to resolve identities to physical node addresses. +* Notary: Nodes running a notary service witness state spends and have the final say in whether a transaction is a double-spend or not. +* Oracle: Nodes providing some oracle functionality like exchange rate or interest rate witnesses. +* Regular node: All nodes have a vault and may start protocols communicating with other nodes, notaries and oracles and evolve their private ledger. + +Setting up your own network +=========================== + +Certificates +------------ + +All node certificates' root must be the same. Later R3 will provide the root for production use, but for testing you can use ``certSigningRequestUtility.jar`` to generate a node certificate with a fixed test root: + +.. sourcecode:: bash + + # Build the jars + ./gradlew buildCordaJAR + # Generate certificate + java -jar build/libs/certSigningRequestUtility.jar --base-dir NODE_DIRECTORY/ + +Configuration +------------- + +A node can be configured by adding/editing ``node.conf`` in the node's directory. + +An example configuration: + +.. literalinclude:: example-code/src/main/resources/example-node.conf + :language: cfg + +The most important fields regarding network configuration are: + +* ``artemisAddress``: This specifies a host and port. Note that the address bound will **NOT** be ``cordaload-node1``, but rather ``::`` (all addresses on all interfaces). The hostname specified is the hostname *that must be externally resolvable by other nodes in the network*. In the above configuration this is the resolvable name of a node in a vpn. +* ``webAddress``: The address the webserver should bind. Note that the port should be distinct from that of ``artemisAddress``. +* ``networkMapAddress``: The resolvable name and artemis port of the network map node. Note that if this node itself is to be the network map this field should not be specified. + +Starting the nodes +------------------ + +You may now start the nodes in any order. You should see lots of log lines about the startup. + +Note that the node is not fully started until it has successfully registered with the network map! A good way of determining whether a node is up is to check whether its ``webAddress`` is bound. + +In terms of process management there is no pre-described method, you may start the jars by hand or perhaps use systemd and friends. + +Connecting to the nodes +----------------------- + +Once a node has started up successfully you may connect to it as a client to initiate protocols/query state etc. Depending on your network setup you may need to tunnel to do this remotely. + +See the :doc:`tutorial-clientrpc-api` on how to establish an RPC link, or you can use the web apis as well. + +Sidenote: A client is always associated with a single node with a single identity, which only sees their part of the ledger. diff --git a/docs/source/tutorial-clientrpc-api.rst b/docs/source/tutorial-clientrpc-api.rst index 1cbdf18987..58e3a2fdb7 100644 --- a/docs/source/tutorial-clientrpc-api.rst +++ b/docs/source/tutorial-clientrpc-api.rst @@ -66,6 +66,8 @@ The RPC we need to initiate a Cash transaction is ``startFlowDynamic`` which may Finally we have everything in place: we start a couple of nodes, connect to them, and start creating transactions while listening on successfully created ones, which are dumped to the console. We just need to run it!: +.. sourcecode:: bash + # Build the example ./gradlew docs/source/example-code:installDist # Start it @@ -94,4 +96,4 @@ requests or responses with the `Kryo` instance RPC uses. Here's an example of h See more on plugins in :doc:`creating-a-cordapp`. .. warning:: We will be replacing the use of Kryo in RPC with a stable message format and this will mean that this plugin - customisation point will either go away completely or change. \ No newline at end of file + customisation point will either go away completely or change. From 44d1b79ef98ef1c600b8fbed4a981dcd6907cb25 Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Tue, 22 Nov 2016 18:10:50 +0000 Subject: [PATCH 2/2] docs: Address PR 513 comments --- docs/source/corda-configuration-files.rst | 23 +--------- .../resources/example-network-map-node.conf | 9 ++++ .../src/main/resources/example-node.conf | 22 ++++++--- .../net/corda/docs/ExampleNodeConfTest.kt | 25 +++++++---- docs/source/setting-up-a-corda-network.rst | 42 +++++++++++++----- .../archive/node-voodoo.2016-11-22-1.log.gz | Bin 0 -> 6318 bytes .../archive/node-voodoo.2016-11-23-1.log.gz | Bin 0 -> 12349 bytes .../kotlin/net/corda/node/internal/Node.kt | 2 +- .../net/corda/node/services/RPCUserService.kt | 19 ++------ .../node/services/config/NodeConfiguration.kt | 10 +++++ .../node/services/ArtemisMessagingTests.kt | 3 +- .../node/services/RPCUserServiceImplTest.kt | 3 +- 12 files changed, 91 insertions(+), 67 deletions(-) create mode 100644 docs/source/example-code/src/main/resources/example-network-map-node.conf create mode 100644 node/logs/archive/node-voodoo.2016-11-22-1.log.gz create mode 100644 node/logs/archive/node-voodoo.2016-11-23-1.log.gz diff --git a/docs/source/corda-configuration-files.rst b/docs/source/corda-configuration-files.rst index e7e9d441fb..e0fbbdf9a7 100644 --- a/docs/source/corda-configuration-files.rst +++ b/docs/source/corda-configuration-files.rst @@ -23,27 +23,8 @@ Configuration File Examples General node configuration file for hosting the IRSDemo services. -.. code-block:: text - - basedir : "./nodea" - myLegalName : "Bank A" - nearestCity : "London" - keyStorePassword : "cordacadevpass" - trustStorePassword : "trustpass" - dataSourceProperties : { - dataSourceClassName : org.h2.jdbcx.JdbcDataSource - "dataSource.url" : "jdbc:h2:"${basedir}"/persistence" - "dataSource.user" : sa - "dataSource.password" : "" - } - artemisAddress : "localhost:31337" - webAddress : "localhost:31339" - extraAdvertisedServiceIds: "corda.interest_rates" - networkMapAddress : "localhost:12345" - useHTTPS : false - rpcUsers : [ - { user=user1, password=letmein, permissions=[ cash ] } - ] +.. literalinclude:: example-code/src/main/resources/example-node.conf + :language: cfg NetworkMapService plus Simple Notary configuration file. diff --git a/docs/source/example-code/src/main/resources/example-network-map-node.conf b/docs/source/example-code/src/main/resources/example-network-map-node.conf new file mode 100644 index 0000000000..6a5c944631 --- /dev/null +++ b/docs/source/example-code/src/main/resources/example-network-map-node.conf @@ -0,0 +1,9 @@ +basedir : "./nameserver" +myLegalName : "Notary Service" +nearestCity : "London" +keyStorePassword : "cordacadevpass" +trustStorePassword : "trustpass" +artemisAddress : "my-network-map:10000" +webAddress : "localhost:10001" +extraAdvertisedServiceIds: "" +useHTTPS : false diff --git a/docs/source/example-code/src/main/resources/example-node.conf b/docs/source/example-code/src/main/resources/example-node.conf index 402694896a..c52a6f3e01 100644 --- a/docs/source/example-code/src/main/resources/example-node.conf +++ b/docs/source/example-code/src/main/resources/example-node.conf @@ -1,11 +1,19 @@ -basedir : "./standalone/regular-node" -myLegalName : "Some Node" +basedir : "./nodea" +myLegalName : "Bank A" nearestCity : "London" keyStorePassword : "cordacadevpass" trustStorePassword : "trustpass" -artemisAddress : "cordaload-node1:31337" -webAddress : "localhost:31339" -extraAdvertisedServiceIds: "" +dataSourceProperties : { + dataSourceClassName : org.h2.jdbcx.JdbcDataSource + "dataSource.url" : "jdbc:h2:"${basedir}"/persistence" + "dataSource.user" : sa + "dataSource.password" : "" +} +artemisAddress : "my-corda-node:10002" +webAddress : "localhost:10003" +extraAdvertisedServiceIds: "corda.interest_rates" +networkMapAddress : "my-network-map:10000" useHTTPS : false -devMode : false -networkMapAddress : "cordaload-nameserver:31337" +rpcUsers : [ + { user=user1, password=letmein, permissions=[ StartProtocol.net.corda.protocols.CashProtocol ] } +] diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/ExampleNodeConfTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/ExampleNodeConfTest.kt index b4ab14cca8..7a898d17b1 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/ExampleNodeConfTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/ExampleNodeConfTest.kt @@ -9,15 +9,24 @@ import kotlin.reflect.declaredMemberProperties class ExampleNodeConfTest { @Test fun exampleNodeConfParsesFine() { - val configResource = ExampleNodeConfTest::class.java.classLoader.getResource("example-node.conf") - val nodeConfig = FullNodeConfiguration( - ConfigHelper.loadConfig( - baseDirectoryPath = Paths.get("some-example-base-dir"), - configFileOverride = Paths.get(configResource.toURI()) - ) + val exampleNodeConfFilenames = arrayOf( + "example-node.conf", + "example-network-map-node.conf" ) - nodeConfig.javaClass.kotlin.declaredMemberProperties.forEach { member -> - member.get(nodeConfig) + + exampleNodeConfFilenames.forEach { + println("Checking $it") + val configResource = ExampleNodeConfTest::class.java.classLoader.getResource(it) + val nodeConfig = FullNodeConfiguration( + ConfigHelper.loadConfig( + baseDirectoryPath = Paths.get("some-example-base-dir"), + configFileOverride = Paths.get(configResource.toURI()) + ) + ) + // Force the config fields as they are resolved lazily + nodeConfig.javaClass.kotlin.declaredMemberProperties.forEach { member -> + member.get(nodeConfig) + } } } } \ No newline at end of file diff --git a/docs/source/setting-up-a-corda-network.rst b/docs/source/setting-up-a-corda-network.rst index 448cee750d..f840d6fdda 100644 --- a/docs/source/setting-up-a-corda-network.rst +++ b/docs/source/setting-up-a-corda-network.rst @@ -1,16 +1,15 @@ -A Corda network - +.. _log4j2: http://logging.apache.org/log4j/2.x/ Introduction - What is a corda network? ======================================================== -A Corda network consists of a number of machines running ``node``s, including a single node operating as the network map service. These nodes communicate using persistent protocols in order to create and validate transactions. +A Corda network consists of a number of machines running nodes, including a single node operating as the network map service. These nodes communicate using persistent protocols in order to create and validate transactions. There are four broader categories of functionality one such node may have. These pieces of functionality are provided as services, and one node may run several of them. -* Network map: The node running the network map provides a way to resolve identities to physical node addresses. +* Network map: The node running the network map provides a way to resolve identities to physical node addresses and associated public keys. * Notary: Nodes running a notary service witness state spends and have the final say in whether a transaction is a double-spend or not. -* Oracle: Nodes providing some oracle functionality like exchange rate or interest rate witnesses. +* Oracle: Network services that link the ledger to the outside world by providing facts that affect the validity of transactions. * Regular node: All nodes have a vault and may start protocols communicating with other nodes, notaries and oracles and evolve their private ledger. Setting up your own network @@ -19,7 +18,13 @@ Setting up your own network Certificates ------------ -All node certificates' root must be the same. Later R3 will provide the root for production use, but for testing you can use ``certSigningRequestUtility.jar`` to generate a node certificate with a fixed test root: +If two nodes are to communicate successfully then both need to have +each other's root certificate in their truststores. The simplest way +to achieve this is to have all nodes sign off of a single root. + +Later R3 will provide this root for production use, but for testing you +can use ``certSigningRequestUtility.jar`` to generate a node +certificate with a fixed test root: .. sourcecode:: bash @@ -31,7 +36,7 @@ All node certificates' root must be the same. Later R3 will provide the root for Configuration ------------- -A node can be configured by adding/editing ``node.conf`` in the node's directory. +A node can be configured by adding/editing ``node.conf`` in the node's directory. For details see :doc:`corda-configuration-files` An example configuration: @@ -40,24 +45,37 @@ An example configuration: The most important fields regarding network configuration are: -* ``artemisAddress``: This specifies a host and port. Note that the address bound will **NOT** be ``cordaload-node1``, but rather ``::`` (all addresses on all interfaces). The hostname specified is the hostname *that must be externally resolvable by other nodes in the network*. In the above configuration this is the resolvable name of a node in a vpn. +* ``artemisAddress``: This specifies a host and port. Note that the address bound will **NOT** be ``my-corda-node``, but rather ``::`` (all addresses on all interfaces). The hostname specified is the hostname *that must be externally resolvable by other nodes in the network*. In the above configuration this is the resolvable name of a machine in a vpn. * ``webAddress``: The address the webserver should bind. Note that the port should be distinct from that of ``artemisAddress``. * ``networkMapAddress``: The resolvable name and artemis port of the network map node. Note that if this node itself is to be the network map this field should not be specified. Starting the nodes ------------------ -You may now start the nodes in any order. You should see lots of log lines about the startup. +You may now start the nodes in any order. Note that the node is not fully started until it has successfully registered with the network map! -Note that the node is not fully started until it has successfully registered with the network map! A good way of determining whether a node is up is to check whether its ``webAddress`` is bound. +You should see a banner, some log lines and eventually ``Node started up and registered``, indicating that the node is fully started. + +.. TODO: Add a better way of polling for startup +A programmatic way of determining whether a node is up is to check whether it's ``webAddress`` is bound. + +In terms of process management there is no prescribed method. You may start the jars by hand or perhaps use systemd and friends. + +Logging +------- + +Only a handful of important lines are printed to the console. For +details/diagnosing problems check the logs. + +Logging is standard log4j2_ and may be configured accordingly. Logs +are by default redirected to files in ``NODE_DIRECTORY/logs/``. -In terms of process management there is no pre-described method, you may start the jars by hand or perhaps use systemd and friends. Connecting to the nodes ----------------------- Once a node has started up successfully you may connect to it as a client to initiate protocols/query state etc. Depending on your network setup you may need to tunnel to do this remotely. -See the :doc:`tutorial-clientrpc-api` on how to establish an RPC link, or you can use the web apis as well. +See the :doc:`tutorial-clientrpc-api` on how to establish an RPC link. Sidenote: A client is always associated with a single node with a single identity, which only sees their part of the ledger. diff --git a/node/logs/archive/node-voodoo.2016-11-22-1.log.gz b/node/logs/archive/node-voodoo.2016-11-22-1.log.gz new file mode 100644 index 0000000000000000000000000000000000000000..39ef8d16a90f8e149e6befd812c63110b3d57943 GIT binary patch literal 6318 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 - - init { - _users = config.getListOrElse("rpcUsers") { emptyList() } - .map { - val username = it.getString("user") - require(username.matches("\\w+".toRegex())) { "Username $username contains invalid characters" } - val password = it.getString("password") - val permissions = it.getListOrElse("permissions") { emptyList() }.toSet() - User(username, password, permissions) - } - .associateBy(User::username) - } + private val _users = config.rpcUsers.associateBy(User::username) override fun getUser(username: String): User? = _users[username] diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index d7a5bbd6d8..9d2ace3310 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -7,6 +7,7 @@ import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.services.ServiceInfo import net.corda.node.internal.Node import net.corda.node.serialization.NodeClock +import net.corda.node.services.User import net.corda.node.services.messaging.NodeMessagingClient import net.corda.node.services.network.NetworkMapService import net.corda.node.utilities.TestClock @@ -51,6 +52,15 @@ class FullNodeConfiguration(val config: Config) : NodeConfiguration { val useTestClock: Boolean by config.getOrElse { false } val notaryNodeAddress: HostAndPort? by config.getOrElse { null } val notaryClusterAddresses: List = config.getListOrElse("notaryClusterAddresses") { emptyList() }.map { HostAndPort.fromString(it) } + val rpcUsers: List = + config.getListOrElse("rpcUsers") { emptyList() } + .map { + val username = it.getString("user") + require(username.matches("\\w+".toRegex())) { "Username $username contains invalid characters" } + val password = it.getString("password") + val permissions = it.getListOrElse("permissions") { emptyList() }.toSet() + User(username, password, permissions) + } fun createNode(): Node { // This is a sanity feature do not remove. diff --git a/node/src/test/kotlin/net/corda/node/services/ArtemisMessagingTests.kt b/node/src/test/kotlin/net/corda/node/services/ArtemisMessagingTests.kt index b93d45f44b..c93126dde3 100644 --- a/node/src/test/kotlin/net/corda/node/services/ArtemisMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/ArtemisMessagingTests.kt @@ -11,6 +11,7 @@ import net.corda.core.messaging.Message import net.corda.core.messaging.createMessage import net.corda.core.node.services.DEFAULT_SESSION_ID import net.corda.core.utilities.LogHelper +import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.ArtemisMessagingServer import net.corda.node.services.messaging.NodeMessagingClient @@ -65,7 +66,7 @@ class ArtemisMessagingTests { @Before fun setUp() { - userService = RPCUserServiceImpl(ConfigFactory.empty()) + userService = RPCUserServiceImpl(FullNodeConfiguration(ConfigFactory.empty())) // TODO: create a base class that provides a default implementation config = object : NodeConfiguration { override val basedir: Path = temporaryFolder.newFolder().toPath() diff --git a/node/src/test/kotlin/net/corda/node/services/RPCUserServiceImplTest.kt b/node/src/test/kotlin/net/corda/node/services/RPCUserServiceImplTest.kt index d26967d5ea..9d11ef23e0 100644 --- a/node/src/test/kotlin/net/corda/node/services/RPCUserServiceImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/RPCUserServiceImplTest.kt @@ -1,6 +1,7 @@ package net.corda.node.services import com.typesafe.config.ConfigFactory +import net.corda.node.services.config.FullNodeConfiguration import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Test @@ -68,6 +69,6 @@ class RPCUserServiceImplTest { } private fun loadWithContents(configString: String): RPCUserServiceImpl { - return RPCUserServiceImpl(ConfigFactory.parseString(configString)) + return RPCUserServiceImpl(FullNodeConfiguration(ConfigFactory.parseString(configString))) } } \ No newline at end of file