ENT-990 Make doorman read an initial set of Network parameters from d… (#96)

ENT-990 Make doorman read an initial set of Network parameters from disk at start-up time
This commit is contained in:
Alberto Arri 2017-11-13 15:40:44 +00:00 committed by GitHub
parent 4c7dc58135
commit 523a6db0b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 224 additions and 20 deletions

4
.idea/compiler.xml generated
View File

@ -50,6 +50,8 @@
<module name="graphs_test" target="1.8" />
<module name="intellij-plugin_main" target="1.8" />
<module name="intellij-plugin_test" target="1.8" />
<module name="irs-demo-cordapp_main" target="1.8" />
<module name="irs-demo-cordapp_test" target="1.8" />
<module name="irs-demo_integrationTest" target="1.8" />
<module name="irs-demo_main" target="1.8" />
<module name="irs-demo_test" target="1.8" />
@ -96,6 +98,8 @@
<module name="samples_test" target="1.8" />
<module name="sandbox_main" target="1.8" />
<module name="sandbox_test" target="1.8" />
<module name="sgx-hsm-tool_main" target="1.8" />
<module name="sgx-hsm-tool_test" target="1.8" />
<module name="sgx-jvm_hsm-tool_main" target="1.8" />
<module name="sgx-jvm_hsm-tool_test" target="1.8" />
<module name="simm-valuation-demo_integrationTest" target="1.8" />

View File

@ -0,0 +1,32 @@
Running a doorman service
=========================
See the Readme in under ``network-management`` for detailed building instructions.
Configuration file
------------------
At startup Doorman reads a configuration file, passed with ``--configFile`` on the command line.
This is an example of what a Doorman configuration file might look like:
.. literalinclude:: ../../network-management/doorman.conf
Invoke Doorman with ``-?`` for a full list of supported command-line arguments.
Bootstrapping the network parameters
------------------------------------
When Doorman is running it will serve the current network parameters. The first time Doorman is
started it will need to know the initial value for the network parameters.
The initial values for the network parameters can be specified with a file, like this:
.. literalinclude:: ../../network-management/initial-network-parameters.conf
And the location of that file can be specified with: ``--initialNetworkParameters``.
Note that when reading from file:
1. ``epoch`` will always be set to 1,
2. ``modifiedTime`` will be the Doorman startup time
``epoch`` will increase by one every time the network parameters are updated.

View File

@ -0,0 +1,8 @@
The Doorman source code is located under `network-management/src`
To build a fat jar containing all the doorman code you can simply invoke
.. sourcecode:: bash
./gradlew network-management:buildDoormanJAR
The built file will appear in
``network-management/build/libs/doorman-<VERSION>-capsule.jar``

View File

@ -1,7 +1,7 @@
ext {
// We use Corda release artifact dependencies instead of project dependencies to make sure each doorman releases are
// align with the corresponding Corda release.
corda_dependency_version = '2.0-20171017.135310-6'
// We use Corda release artifact dependencies instead of project dependencies to make sure each doorman release is
// aligned with the corresponding Corda release.
corda_dependency_version = '2.0-20171104.000037-23'
}
version "$corda_dependency_version"
@ -92,6 +92,7 @@ dependencies {
compile "net.corda:corda-node-api:$corda_dependency_version"
testCompile "net.corda:corda-test-utils:$corda_dependency_version"
testCompile "net.corda:corda-node-driver:$corda_dependency_version"
testCompile "net.corda:corda-test-common:$corda_dependency_version"
// Log4J: logging framework (with SLF4J bindings)
compile "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}"

View File

@ -0,0 +1,12 @@
notaries : [{
name: "O=Notary A, L=Port Louis, C=MU, OU=Org Unit, CN=Service Name"
key: "GfHq2tTVk9z4eXgyWmExBB3JfHpeuYrk9jUc4zaVVSXpnW8FdCUNDhw6GRGN"
validating: true
}, {
name: "O=Notary B, L=Bali, C=ID, OU=Org Unit, CN=Service Name"
key: "GfHq2tTVk9z4eXgyEshv6vtBDjp7n76QZH5hk6VXLhk3vRTAmKcP9F9tRfPj"
validating: false
}]
minimumPlatformVersion = 1
maxMessageSize = 100
maxTransactionSize = 100

View File

@ -20,6 +20,7 @@ import org.junit.rules.TemporaryFolder
import java.net.URL
import java.util.*
import kotlin.test.assertEquals
import net.corda.testing.common.internal.testNetworkParameters
class DoormanIntegrationTest {
@Rule
@ -41,7 +42,7 @@ class DoormanIntegrationTest {
val signer = Signer(intermediateCAKey, arrayOf(intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()))
//Start doorman server
val doorman = startDoorman(NetworkHostAndPort("localhost", 0), database, true, signer, null)
val doorman = startDoorman(NetworkHostAndPort("localhost", 0), database, true, testNetworkParameters(emptyList()), signer, null)
// Start Corda network registration.
val config = testNodeConfiguration(

View File

@ -24,6 +24,7 @@ import net.corda.node.utilities.registration.NetworkRegistrationHelper
import net.corda.testing.ALICE
import net.corda.testing.BOB
import net.corda.testing.CHARLIE
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.testNodeConfiguration
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import org.h2.tools.Server
@ -90,7 +91,8 @@ class SigningServiceIntegrationTest {
// Identity service not needed doorman, corda persistence is not very generic.
throw UnsupportedOperationException()
}, SchemaService())
val doorman = startDoorman(NetworkHostAndPort(HOST, 0), database, approveAll = true)
val doorman = startDoorman(NetworkHostAndPort(HOST, 0), database, approveAll = true,
initialNetworkMapParameters = testNetworkParameters(emptyList()))
// Start Corda network registration.
val config = testNodeConfiguration(
@ -137,7 +139,8 @@ class SigningServiceIntegrationTest {
// Identity service not needed doorman, corda persistence is not very generic.
throw UnsupportedOperationException()
}, SchemaService())
val doorman = startDoorman(NetworkHostAndPort(HOST, 0), database, approveAll = true)
val doorman = startDoorman(NetworkHostAndPort(HOST, 0), database, approveAll = true,
initialNetworkMapParameters = testNetworkParameters(emptyList()))
thread(start = true, isDaemon = true) {
val h2ServerArgs = arrayOf("-tcpPort", H2_TCP_PORT, "-tcpAllowOthers")

View File

@ -22,7 +22,8 @@ data class DoormanParameters(val basedir: Path,
val databaseProperties: Properties? = null,
val jiraConfig: JiraConfig? = null,
val keystorePath: Path? = null, // basedir / "certificates" / "caKeystore.jks",
val rootStorePath: Path? = null // basedir / "certificates" / "rootCAKeystore.jks"
val rootStorePath: Path? = null, // basedir / "certificates" / "rootCAKeystore.jks"
val initialNetworkParameters: Path
) {
enum class Mode {
DOORMAN, CA_KEYGEN, ROOT_KEYGEN
@ -50,6 +51,7 @@ fun parseParameters(vararg args: String): DoormanParameters {
accepts("rootPrivateKeyPassword", "Root private key password.").withRequiredArg().describedAs("password")
accepts("host", "Doorman web service host override").withRequiredArg().describedAs("hostname")
accepts("port", "Doorman web service port override").withRequiredArg().ofType(Int::class.java).describedAs("port number")
accepts("initialNetworkParameters", "initial network parameters filepath").withRequiredArg().describedAs("The initial network map").describedAs("filepath")
}
val configFile = if (argConfig.hasPath("configFile")) {

View File

@ -16,6 +16,7 @@ import com.r3.corda.networkmanage.doorman.webservice.RegistrationWebService
import net.corda.core.crypto.Crypto
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.createDirectories
import net.corda.core.node.NetworkParameters
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.seconds
@ -159,6 +160,7 @@ fun generateCAKeyPair(keystorePath: Path, rootStorePath: Path, rootKeystorePass:
fun startDoorman(hostAndPort: NetworkHostAndPort,
database: CordaPersistence,
approveAll: Boolean,
initialNetworkMapParameters: NetworkParameters,
signer: Signer? = null,
jiraConfig: DoormanParameters.JiraConfig? = null): DoormanServer {
@ -179,7 +181,8 @@ fun startDoorman(hostAndPort: NetworkHostAndPort,
DefaultCsrHandler(requestService, signer)
}
val doorman = DoormanServer(hostAndPort, RegistrationWebService(requestProcessor, DoormanServer.serverStatus), NodeInfoWebService(PersistenceNodeInfoStorage(database)))
val doorman = DoormanServer(hostAndPort, RegistrationWebService(requestProcessor, DoormanServer.serverStatus),
NodeInfoWebService(PersistenceNodeInfoStorage(database), initialNetworkMapParameters))
doorman.start()
// Thread process approved request periodically.
@ -241,7 +244,9 @@ fun main(args: Array<String>) {
DoormanParameters.Mode.DOORMAN -> {
val database = configureDatabase(dataSourceProperties, databaseProperties, { throw UnsupportedOperationException() }, SchemaService())
val signer = buildLocalSigner(this)
startDoorman(NetworkHostAndPort(host, port), database, approveAll, signer, jiraConfig)
val networkParameters = parseNetworkParametersFrom(initialNetworkParameters)
startDoorman(NetworkHostAndPort(host, port), database, approveAll, networkParameters, signer, jiraConfig)
}
}
}

View File

@ -0,0 +1,67 @@
package com.r3.corda.networkmanage.doorman
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigParseOptions
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.exists
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NotaryInfo
import net.corda.core.utilities.days
import net.corda.core.utilities.parsePublicKeyBase58
import net.corda.nodeapi.config.parseAs
import java.nio.file.Path
import java.time.Instant
/**
* Initial value for [NetworkParameters.epoch].
*/
private const val DEFAULT_EPOCH = 1
/**
* Data class representing a [NotaryInfo] which can be easily parsed by a typesafe [ConfigFactory].
* @property name the X500Name of the notary.
* @property key the public key as serialized by [toBase58String]
* @property validating whether the notary is validating
*/
internal data class NotaryConfiguration(private val name: CordaX500Name,
private val key: String,
private val validating: Boolean) {
fun toNotaryInfo(): NotaryInfo = NotaryInfo(Party(name, parsePublicKeyBase58(key)), validating)
}
/**
* data class containing the fields from [NetworkParameters] which can be read at start-up time from doorman.
* It is a proper subset of [NetworkParameters] except for the [notaries] field which is replaced by a list of
* [NotaryConfiguration] which is parsable.
*
* This is public only because [parseAs] needs to be able to call its constructor.
*/
internal data class NetworkParametersConfiguration(val minimumPlatformVersion: Int,
val notaries: List<NotaryConfiguration>,
val eventHorizonDays: Int,
val maxMessageSize: Int,
val maxTransactionSize: Int)
/**
* Parses a file and returns a [NetworkParameters] instance.
*
* @return a [NetworkParameters] with values read from [configFile] except:
* an epoch of [DEFAULT_EPOCH],
* an eventHorizon of [DEFAULT_EVENT_HORIZON], and
* a modifiedTime initialized with [Instant.now].
* If [configFile] is null [DEFAULT_NETWORK_PARAMETERS] is returned.
*/
fun parseNetworkParametersFrom(configFile: Path): NetworkParameters {
check(configFile.exists()) { "File $configFile does not exist" }
val initialNetworkParameters = ConfigFactory.parseFile(configFile.toFile(), ConfigParseOptions.defaults())
.parseAs(NetworkParametersConfiguration::class)
return NetworkParameters(initialNetworkParameters.minimumPlatformVersion,
initialNetworkParameters.notaries.map { it.toNotaryInfo() },
initialNetworkParameters.eventHorizonDays.days,
initialNetworkParameters.maxMessageSize,
initialNetworkParameters.maxTransactionSize,
Instant.now(),
DEFAULT_EPOCH)
}

View File

@ -4,6 +4,7 @@ import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage
import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService.Companion.networkMapPath
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SignedData
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
@ -21,7 +22,7 @@ import javax.ws.rs.core.Response.ok
import javax.ws.rs.core.Response.status
@Path(networkMapPath)
class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage) {
class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage, private val networkParameters: NetworkParameters) {
companion object {
const val networkMapPath = "network-map"
}
@ -60,6 +61,7 @@ class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage) {
@GET
fun getNetworkMap(): Response {
// TODO: Cache the response?
// TODO: Add the networkParamters to this returned response.
return ok(ObjectMapper().writeValueAsString(nodeInfoStorage.getNodeInfoHashes())).build()
}

View File

@ -0,0 +1,40 @@
package com.r3.corda.networkmanage
import com.r3.corda.networkmanage.doorman.parseNetworkParametersFrom
import net.corda.core.utilities.days
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Test
import java.io.File
import java.nio.file.Paths
import java.time.Instant
class NetworkParametersConfigurationTest {
private val validInitialNetworkConfigPath = File(javaClass.getResource("/initial-network-parameters.conf").toURI())
@Test
fun `reads an existing file`() {
val confFile = validInitialNetworkConfigPath.toPath()
val networkParameters = parseNetworkParametersFrom(confFile)
assertThat(networkParameters.minimumPlatformVersion).isEqualTo(1)
assertThat(networkParameters.eventHorizon).isEqualTo(100.days)
val notaries = networkParameters.notaries
assertThat(notaries).hasSize(2)
assertThat(notaries[0].validating).isTrue()
assertThat(notaries[1].validating).isFalse()
assertThat(networkParameters.maxMessageSize).isEqualTo(100)
assertThat(networkParameters.maxTransactionSize).isEqualTo(100)
// This is rather weak, though making this an exact test will require mocking a clock.
assertThat(networkParameters.modifiedTime).isBefore(Instant.now())
assertThat(networkParameters.epoch).isEqualTo(1)
}
@Test
fun `throws on a non-existing file`() {
assertThatThrownBy {
parseNetworkParametersFrom(Paths.get("notHere"))
}.isInstanceOf(IllegalStateException::class.java)
}
}

View File

@ -17,6 +17,7 @@ import net.corda.node.utilities.CordaPersistence
import net.corda.node.utilities.X509Utilities
import net.corda.node.utilities.configureDatabase
import net.corda.nodeapi.internal.serialization.*
import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseProperties
import org.junit.After

View File

@ -9,23 +9,26 @@ import kotlin.test.assertFailsWith
class DoormanParametersTest {
private val testDummyPath = ".${File.separator}testDummyPath.jks"
private val validInitialNetworkConfigPath = File(javaClass.getResource("/initial-network-parameters.conf").toURI()).absolutePath
private val validConfigPath = File(javaClass.getResource("/doorman.conf").toURI()).absolutePath
private val invalidConfigPath = File(javaClass.getResource("/doorman_fail.conf").toURI()).absolutePath
private val requiredArgs = arrayOf("--configFile", validConfigPath, "--initialNetworkParameters", validInitialNetworkConfigPath)
@Test
fun `parse mode flag arg correctly`() {
assertEquals(DoormanParameters.Mode.CA_KEYGEN, parseParameters("--mode", "CA_KEYGEN", "--configFile", validConfigPath).mode)
assertEquals(DoormanParameters.Mode.ROOT_KEYGEN, parseParameters("--mode", "ROOT_KEYGEN", "--configFile", validConfigPath).mode)
assertEquals(DoormanParameters.Mode.DOORMAN, parseParameters("--mode", "DOORMAN", "--configFile", validConfigPath).mode)
assertEquals(DoormanParameters.Mode.CA_KEYGEN, callParseParametersWithRequiredArgs("--mode", "CA_KEYGEN").mode)
assertEquals(DoormanParameters.Mode.ROOT_KEYGEN, callParseParametersWithRequiredArgs("--mode", "ROOT_KEYGEN").mode)
assertEquals(DoormanParameters.Mode.DOORMAN, callParseParametersWithRequiredArgs("--mode", "DOORMAN").mode)
}
@Test
fun `command line arg should override config file`() {
val params = parseParameters("--keystorePath", testDummyPath, "--port", "1000", "--configFile", validConfigPath)
val params = callParseParametersWithRequiredArgs("--keystorePath", testDummyPath, "--port", "1000")
assertEquals(testDummyPath, params.keystorePath.toString())
assertEquals(1000, params.port)
val params2 = parseParameters("--configFile", validConfigPath)
val params2 = callParseParametersWithRequiredArgs()
assertEquals(Paths.get("/opt/doorman/certificates/caKeystore.jks"), params2.keystorePath)
assertEquals(8080, params2.port)
}
@ -40,11 +43,15 @@ class DoormanParametersTest {
@Test
fun `should parse jira config correctly`() {
val parameter = parseParameters("--configFile", validConfigPath)
val parameter = callParseParametersWithRequiredArgs()
assertEquals("https://doorman-jira-host.com/", parameter.jiraConfig?.address)
assertEquals("TD", parameter.jiraConfig?.projectCode)
assertEquals("username", parameter.jiraConfig?.username)
assertEquals("password", parameter.jiraConfig?.password)
assertEquals(41, parameter.jiraConfig?.doneTransitionCode)
}
private fun callParseParametersWithRequiredArgs(vararg additionalArgs: String): DoormanParameters {
return parseParameters(*(requiredArgs + additionalArgs))
}
}

View File

@ -20,6 +20,8 @@ import net.corda.node.serialization.KryoServerSerializationScheme
import net.corda.node.utilities.CertificateType
import net.corda.node.utilities.X509Utilities
import net.corda.nodeapi.internal.serialization.*
import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme
import net.corda.testing.common.internal.testNetworkParameters
import org.bouncycastle.asn1.x500.X500Name
import org.codehaus.jackson.map.ObjectMapper
import org.junit.BeforeClass
@ -72,7 +74,8 @@ class NodeInfoWebServiceTest {
on { getCertificatePath(any()) }.thenReturn(certPath)
}
DoormanServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage)).use {
DoormanServer(NetworkHostAndPort("localhost", 0),
NodeInfoWebService(nodeInfoStorage, testNetworkParameters(emptyList()))).use {
it.start()
val registerURL = URL("http://${it.hostAndPort}/api/${NodeInfoWebService.networkMapPath}/register")
val nodeInfoAndSignature = SignedData(nodeInfo.serialize(), digitalSignature).serialize().bytes
@ -98,7 +101,8 @@ class NodeInfoWebServiceTest {
on { getCertificatePath(any()) }.thenReturn(certPath)
}
DoormanServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage)).use {
DoormanServer(NetworkHostAndPort("localhost", 0),
NodeInfoWebService(nodeInfoStorage, testNetworkParameters(emptyList()))).use {
it.start()
val registerURL = URL("http://${it.hostAndPort}/api/${NodeInfoWebService.networkMapPath}/register")
val nodeInfoAndSignature = SignedData(nodeInfo.serialize(), digitalSignature).serialize().bytes
@ -116,7 +120,8 @@ class NodeInfoWebServiceTest {
val nodeInfoStorage: NodeInfoStorage = mock {
on { getNodeInfoHashes() }.thenReturn(networkMapList)
}
DoormanServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage)).use {
DoormanServer(NetworkHostAndPort("localhost", 0),
NodeInfoWebService(nodeInfoStorage, testNetworkParameters(emptyList()))).use {
it.start()
val conn = URL("http://${it.hostAndPort}/api/${NodeInfoWebService.networkMapPath}").openConnection() as HttpURLConnection
val response = conn.inputStream.bufferedReader().use { it.readLine() }
@ -139,7 +144,8 @@ class NodeInfoWebServiceTest {
on { getNodeInfo(nodeInfoHash) }.thenReturn(nodeInfo)
}
DoormanServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage)).use {
DoormanServer(NetworkHostAndPort("localhost", 0),
NodeInfoWebService(nodeInfoStorage, testNetworkParameters(emptyList()))).use {
it.start()
val nodeInfoURL = URL("http://${it.hostAndPort}/api/${NodeInfoWebService.networkMapPath}/$nodeInfoHash")
val conn = nodeInfoURL.openConnection()

View File

@ -0,0 +1,13 @@
notaries : [{
name: "O=Notary A, L=Port Louis, C=MU, OU=Org Unit, CN=Service Name"
key: "GfHq2tTVk9z4eXgyWmExBB3JfHpeuYrk9jUc4zaVVSXpnW8FdCUNDhw6GRGN"
validating: true
}, {
name: "O=Notary B, L=Bali, C=ID, OU=Org Unit, CN=Service Name"
key: "GfHq2tTVk9z4eXgyEshv6vtBDjp7n76QZH5hk6VXLhk3vRTAmKcP9F9tRfPj"
validating: false
}]
eventHorizonDays = 100
minimumPlatformVersion = 1
maxMessageSize = 100
maxTransactionSize = 100