ENT-1433: Implementation of NetworkParameters updates (#477)

* Implementation of NetworkParameters updates

Implementation of NetworkParameters updates for network-management
server side of the process.
Change the way network parameters are read up on network-management
startup. Introduced 3 flags to clearly communicate intentions:
initial-network-parameters, update-network-parameters and flag-day.

* Address Shams comments

* Address comments

* NetworkMapStorage refactor

getLatestNetworkParameters returns NetworkParametersEntity

* TODO Exit after parameters update

Started implementing feature requested by devops. TODO: Fix
NodeRegistrationTest

* Code cleanup

* Merged the two cmd line flags into one --set-network-parameters. The idea being to simply check if there are any active network parameters to determine if it's an update or not.

Applying network parameters causes the server to exit once done (ENT-1559).

* Merge fixes

* Minor cleanup

* Small fixes

* Merge fixes and cleanup

* TODO Implementation of cancelUpdate flag

Will slightly change after more manual testing

* Small cleanup

* Address comments

Add OneToOne relation between ParametersUpdate and NetworkParameters.

* Address comments fix tests
This commit is contained in:
Katarzyna Streich 2018-03-21 12:29:27 +00:00 committed by Shams Asari
parent 785891515e
commit 75b467e340
33 changed files with 1175 additions and 326 deletions

View File

@ -59,9 +59,12 @@ java -DdefaultLogLevel=TRACE -DconsoleLogLevel=TRACE -jar doorman-<version>.jar
#Configuring network management service #Configuring network management service
### Local signing ### Local signing
When `keystorePath` is provided in the config file, a signer will be created to handle all the signing periodically using the CA keys in the provided keystore. When `keystorePath` is provided in the config file, a signer will be created to handle all the signing periodically
using the CA keys in the provided keystore.
The network management service can be started without a signer, the signing will be delegated to external process (e.g. HSM) connecting to the same database, the server will poll the database periodically for newly signed data and update the statuses accordingly. The network management service can be started without a signer, the signing will be delegated to external process
(e.g. HSM) connecting to the same database, the server will poll the database periodically for newly signed data and
update the statuses accordingly.
Additional configuration needed for local signer: Additional configuration needed for local signer:
``` ```
@ -77,7 +80,8 @@ Doorman service can be started with the following options :
### JIRA ### JIRA
The doorman service can use JIRA to manage the certificate signing request approval workflow. This can be turned on by providing JIRA connection configuration in the config file. The doorman service can use JIRA to manage the certificate signing request approval workflow. This can be turned on by providing
JIRA connection configuration in the config file.
``` ```
doorman { doorman {
jira { jira {
@ -93,10 +97,12 @@ The doorman service can use JIRA to manage the certificate signing request appro
``` ```
#### JIRA project configuration #### JIRA project configuration
* The JIRA project should setup as "Business Project" with "Task" workflow. * The JIRA project should setup as "Business Project" with "Task" workflow.
* Custom text field input "Request ID", and "Reject Reason" should be created in JIRA, doorman will exit with error without these custom fields. * Custom text field input "Request ID", and "Reject Reason" should be created in JIRA, doorman will exit with error without
these custom fields.
### Auto approval ### Auto approval
When `approveAll` is set to `true`, the doorman will approve all requests on receive. (*This should only be enabled in a test environment) When `approveAll` is set to `true`, the doorman will approve all requests on receive. (*This should only be enabled in a
test environment)
### Network map service ### Network map service
Network map service can be enabled by providing the following config: Network map service can be enabled by providing the following config:
@ -107,7 +113,6 @@ The doorman service can use JIRA to manage the certificate signing request appro
} }
``` ```
`cacheTimeout`(ms) determines how often the server should poll the database for a newly signed network map and also how often nodes should poll for a new network map (by including this value in the HTTP response header). **This is not how often changes to the network map are signed, which is a different process.** `cacheTimeout`(ms) determines how often the server should poll the database for a newly signed network map and also how often nodes should poll for a new network map (by including this value in the HTTP response header). **This is not how often changes to the network map are signed, which is a different process.**
`signInterval`(ms) this is only relevant when local signer is enabled. The signer poll the database according to the `signInterval`, and create a new network map if the collection of node info hashes is different from the current network map. `signInterval`(ms) this is only relevant when local signer is enabled. The signer poll the database according to the `signInterval`, and create a new network map if the collection of node info hashes is different from the current network map.
## Example config file ## Example config file
@ -214,11 +219,17 @@ networkMap {
Save the parameters to `network-parameters.conf` Save the parameters to `network-parameters.conf`
### 6. Load initial network parameters file for network map service ### 6. Load the initial network parameters
A network parameters file is required to start the network map service for the first time. The initial network parameters file can be loaded using the `--update-network-parameters` flag. A network parameters file is required to start the network map service for the first time. The initial network parameters
We can now restart the network management server with both doorman and network map service. file can be loaded using the `--set-network-parameters` flag. We can now restart the network management server with
both doorman and network map service.
``` ```
java -jar doorman-<version>.jar --config-file <config file> --update-network-parameters network-parameters.conf java -jar doorman-<version>.jar --config-file <config file> --set-network-parameters network-parameters.conf
```
The server will terminate once this process is complete. Start it back up again with both the doorman and network map service.
```
java -jar doorman-<version>.jar --config-file <config file>
``` ```
### 7. Archive policy ### 7. Archive policy
@ -230,11 +241,42 @@ Run the following SQL script to archive the node info table (change the timestam
delect from node_info where is_current = false and published_at < '2018-03-12' delect from node_info where is_current = false and published_at < '2018-03-12'
``` ```
## Private Network Map # Updating the network parameters
The initial network parameters can be subsequently changed through an update process. However, these changes must first
be advertised to the entire network to allow nodes time to agree to the changes.
The server needs to be shutdown and started with the same `set-network-parameters` as before but this time the network
parameters file must have `parametersUpdate` config block:
parametersUpdate {
description = "Important update"
updateDeadline = "2017-08-31T05:10:36.297Z" # ISO-8601 time, substitute it with update deadline
}
`description` is a short description of the update that will be communicated to the nodes and `updateDeadline` is
the time (in ISO-8601 format) by which all nodes in the network must decide that they have accepted the new parameters.
NOTE: Currently only backwards compatible changes to the network parameters can be made, i.e. notaries can't be removed,
max transaction size can only increase, etc.
It is possible to poll the network map database to check how many network participants have accepted the new network parameters
- the information is stored in the `node-info.accepted_parameters_hash` column.
When the time for switching the parameters comes, doorman should be restarted again, this time only with `flag-day` flag
(no parameters file).
```
java -jar doorman-<version>.jar --flag-day
```
This will switch the parameters that were previously advertised as an update to be the current ones in the network map.
All nodes in the network need to restart to apply the new parameters. Any node which has not accepted the new parameters
will fail to start.
# Private Network Map
The private network is a tactical solution to provide temporary privacy to the initial network map. The private network is a tactical solution to provide temporary privacy to the initial network map.
### Creating a private network ## Creating a private network
To create a new private network, a entry has to be create in the ``private_network`` table manually. To create a new private network, an entry has to be created in the ``private_network`` table manually.
Run the following SQL script to create a new private network: Run the following SQL script to create a new private network:
@ -248,12 +290,12 @@ Then use the following SQL to retrieve the private network ID for the private ne
select id from private_network where name = 'Private Network Name' select id from private_network where name = 'Private Network Name'
``` ```
### Modify existing private network registration ## Modify existing private network registration
Since this is a tactical solution, any modification will require manual database changes. Since this is a tactical solution, any modification will require manual database changes.
**We should try to keep these changes to the minimal** **We should try to keep these changes to the minimal**
#### Add nodes to a private network ### Add nodes to a private network
``` ```
update certificate_signing_request update certificate_signing_request
@ -269,7 +311,7 @@ set private_network = '<<private_network_id>>'
where status = 'APPROVED' where status = 'APPROVED'
``` ```
#### Move a node from its private network and into the global network map** ### Move a node from its private network and into the global network map**
``` ```
update certificate_signing_request update certificate_signing_request

View File

@ -5,7 +5,6 @@ notaries : [{
notaryNodeInfoFile: "/Path/To/NodeInfo/File2" notaryNodeInfoFile: "/Path/To/NodeInfo/File2"
validating: false validating: false
}] }]
eventHorizonDays = 100
minimumPlatformVersion = 1 minimumPlatformVersion = 1
maxMessageSize = 10485760 maxMessageSize = 10485760
maxTransactionSize = 100 maxTransactionSize = 10485760

View File

@ -0,0 +1,168 @@
package com.r3.corda.networkmanage.doorman
import com.r3.corda.networkmanage.common.makeTestDataSourceProperties
import com.r3.corda.networkmanage.common.utils.CertPathAndKey
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
import net.corda.core.crypto.random63BitValue
import net.corda.core.internal.bufferUntilSubscribed
import net.corda.core.internal.concurrent.transpose
import net.corda.core.internal.div
import net.corda.core.internal.readObject
import net.corda.core.messaging.ParametersUpdateInfo
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.seconds
import net.corda.nodeapi.internal.createDevNetworkMapCa
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
import net.corda.nodeapi.internal.network.SignedNetworkParameters
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.core.*
import net.corda.testing.driver.PortAllocation
import net.corda.testing.driver.internal.NodeHandleInternal
import net.corda.testing.internal.createDevIntermediateCaCertPath
import net.corda.testing.node.internal.CompatibilityZoneParams
import net.corda.testing.node.internal.internalDriver
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.net.URL
import java.security.cert.X509Certificate
import java.time.Instant
class NetworkParametersUpdateTest {
companion object {
private val timeoutMillis = 5.seconds.toMillis()
}
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule(true)
private val portAllocation = PortAllocation.Incremental(10000)
private val serverAddress = portAllocation.nextHostAndPort()
private lateinit var rootCaCert: X509Certificate
private lateinit var doormanCa: CertificateAndKeyPair
private lateinit var networkMapCa: CertificateAndKeyPair
private lateinit var dbName: String
private var server: NetworkManagementServer? = null
@Before
fun init() {
dbName = random63BitValue().toString()
val (rootCa, doormanCa) = createDevIntermediateCaCertPath()
rootCaCert = rootCa.certificate
this.doormanCa = doormanCa
networkMapCa = createDevNetworkMapCa(rootCa)
}
@After
fun cleanUp() {
server?.close()
}
@Test
fun `network parameters update commnunicated to node`() {
// Initialise the server with some network parameters
val initialNetParams = NetworkParametersCmd.Set(
notaries = emptyList(),
minimumPlatformVersion = 1,
maxMessageSize = 1_000_000,
maxTransactionSize = 1_000_000,
parametersUpdate = null
)
applyNetworkParametersAndStart(initialNetParams)
val compatibilityZone = CompatibilityZoneParams(
URL("http://$serverAddress"),
publishNotaries = {},
rootCert = rootCaCert
)
internalDriver(
portAllocation = portAllocation,
compatibilityZone = compatibilityZone,
initialiseSerialization = false,
extraCordappPackagesToScan = listOf("net.corda.finance")
) {
var (alice) = listOf(
startNode(providedName = ALICE_NAME),
defaultNotaryNode
).transpose().getOrThrow()
alice as NodeHandleInternal
val snapshot = alice.rpc.networkParametersFeed().snapshot
val updates = alice.rpc.networkParametersFeed().updates.bufferUntilSubscribed()
assertThat(snapshot).isNull()
val updateDeadline = Instant.now() + 10.seconds
applyNetworkParametersAndStart(initialNetParams.copy(
maxTransactionSize = 1_000_001,
parametersUpdate = ParametersUpdateConfig(
description = "Very Important Update",
updateDeadline = updateDeadline
)
)
)
updates.expectEvents(isStrict = true) {
sequence(
expect { update: ParametersUpdateInfo ->
assertEquals(update.description, "Very Important Update")
assertEquals(update.parameters.maxTransactionSize, 1_000_001)
assertEquals(update.parameters.epoch, 2) // The epoch must increment automatically.
}
)
}
val paramUpdateInfo = alice.rpc.networkParametersFeed().snapshot!!
alice.rpc.acceptNewNetworkParameters(paramUpdateInfo.hash)
// Make sure we've passed the deadline
Thread.sleep(Math.max(updateDeadline.toEpochMilli() - System.currentTimeMillis(), 0))
applyNetworkParametersAndStart(NetworkParametersCmd.FlagDay)
alice.stop()
alice = startNode(providedName = ALICE_NAME).getOrThrow() as NodeHandleInternal
// TODO It is also possible to check what version of parameters node runs by writing flow that reads that value from ServiceHub
val networkParameters = (alice.configuration.baseDirectory / NETWORK_PARAMS_FILE_NAME)
.readObject<SignedNetworkParameters>().verified()
assertEquals(networkParameters, paramUpdateInfo.parameters)
assertThat(alice.rpc.networkParametersFeed().snapshot).isNull() // Check that NMS doesn't advertise updates anymore.
}
}
private fun startServer(startNetworkMap: Boolean = true): NetworkManagementServer {
val server = NetworkManagementServer(makeTestDataSourceProperties(dbName), DatabaseConfig(runMigration = true))
server.start(
serverAddress,
CertPathAndKey(listOf(doormanCa.certificate, rootCaCert), doormanCa.keyPair.private),
DoormanConfig(approveAll = true, jira = null, approveInterval = timeoutMillis),
if (startNetworkMap) {
NetworkMapStartParams(
LocalSigner(networkMapCa),
NetworkMapConfig(cacheTimeout = timeoutMillis, signInterval = timeoutMillis)
)
} else {
null
}
)
return server
}
private fun applyNetworkParametersAndStart(networkParametersCmd: NetworkParametersCmd) {
server?.close()
NetworkManagementServer(makeTestDataSourceProperties(dbName), DatabaseConfig(runMigration = true)).use {
it.processNetworkParameters(networkParametersCmd)
}
server = startServer(startNetworkMap = true)
// Wait for server to process the parameters update and for the nodes to poll again
Thread.sleep(timeoutMillis * 2)
}
}

View File

@ -11,17 +11,13 @@
package com.r3.corda.networkmanage.doorman package com.r3.corda.networkmanage.doorman
import com.r3.corda.networkmanage.common.makeTestDataSourceProperties import com.r3.corda.networkmanage.common.makeTestDataSourceProperties
import com.r3.corda.networkmanage.common.persistence.configureDatabase
import com.r3.corda.networkmanage.common.utils.CertPathAndKey import com.r3.corda.networkmanage.common.utils.CertPathAndKey
import com.r3.corda.networkmanage.doorman.signer.LocalSigner import com.r3.corda.networkmanage.doorman.signer.LocalSigner
import net.corda.cordform.CordformNode import net.corda.cordform.CordformNode
import net.corda.core.crypto.random63BitValue import net.corda.core.crypto.random63BitValue
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.div import net.corda.core.internal.*
import net.corda.core.internal.exists
import net.corda.core.internal.list
import net.corda.core.messaging.startFlow import net.corda.core.messaging.startFlow
import net.corda.core.node.NetworkParameters
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
@ -30,9 +26,7 @@ import net.corda.finance.flows.CashIssueAndPaymentFlow
import net.corda.nodeapi.internal.createDevNetworkMapCa import net.corda.nodeapi.internal.createDevNetworkMapCa
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.*
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.singleIdentity
import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.PortAllocation
import net.corda.testing.driver.internal.NodeHandleInternal import net.corda.testing.driver.internal.NodeHandleInternal
@ -55,12 +49,11 @@ class NodeRegistrationTest : IntegrationTest() {
private val notaryName = CordaX500Name("NotaryService", "Zurich", "CH") private val notaryName = CordaX500Name("NotaryService", "Zurich", "CH")
private val aliceName = CordaX500Name("Alice", "London", "GB") private val aliceName = CordaX500Name("Alice", "London", "GB")
private val genevieveName = CordaX500Name("Genevieve", "London", "GB") private val genevieveName = CordaX500Name("Genevieve", "London", "GB")
private val timeoutMillis = 5.seconds.toMillis()
@ClassRule @ClassRule
@JvmField @JvmField
val databaseSchemas = IntegrationTestSchemas(notaryName.organisation, aliceName.organisation, genevieveName.organisation) val databaseSchemas = IntegrationTestSchemas(notaryName.organisation, aliceName.organisation, genevieveName.organisation)
private val timeoutMillis = 5.seconds.toMillis()
} }
@Rule @Rule
@ -71,7 +64,7 @@ class NodeRegistrationTest : IntegrationTest() {
private val serverAddress = portAllocation.nextHostAndPort() private val serverAddress = portAllocation.nextHostAndPort()
private lateinit var rootCaCert: X509Certificate private lateinit var rootCaCert: X509Certificate
private lateinit var csrCa: CertificateAndKeyPair private lateinit var doormanCa: CertificateAndKeyPair
private lateinit var networkMapCa: CertificateAndKeyPair private lateinit var networkMapCa: CertificateAndKeyPair
private lateinit var dbName: String private lateinit var dbName: String
@ -82,7 +75,7 @@ class NodeRegistrationTest : IntegrationTest() {
dbName = random63BitValue().toString() dbName = random63BitValue().toString()
val (rootCa, doormanCa) = createDevIntermediateCaCertPath() val (rootCa, doormanCa) = createDevIntermediateCaCertPath()
rootCaCert = rootCa.certificate rootCaCert = rootCa.certificate
this.csrCa = doormanCa this.doormanCa = doormanCa
networkMapCa = createDevNetworkMapCa(rootCa) networkMapCa = createDevNetworkMapCa(rootCa)
} }
@ -96,17 +89,19 @@ class NodeRegistrationTest : IntegrationTest() {
// Start the server without the network parameters config which won't start the network map. Just the doorman // Start the server without the network parameters config which won't start the network map. Just the doorman
// registration process will start up, allowing us to register the notaries which will then be used in the network // registration process will start up, allowing us to register the notaries which will then be used in the network
// parameters. // parameters.
server = startNetworkManagementServer(networkParameters = null) server = startServer(startNetworkMap = false)
val compatibilityZone = CompatibilityZoneParams( val compatibilityZone = CompatibilityZoneParams(
URL("http://$serverAddress"), URL("http://$serverAddress"),
publishNotaries = { notaryInfos -> publishNotaries = { notaryInfos ->
val setNetParams = NetworkParametersCmd.Set(
notaries = notaryInfos,
minimumPlatformVersion = 1,
maxMessageSize = 10485760,
maxTransactionSize = 10485760,
parametersUpdate = null
)
// Restart the server once we're able to generate the network parameters // Restart the server once we're able to generate the network parameters
server!!.close() applyNetworkParametersAndStart(setNetParams)
server = startNetworkManagementServer(testNetworkParameters(notaryInfos))
// Once restarted we delay starting the nodes to make sure the network map server has processed the
// network parameters, otherwise the nodes will fail to start as the network parameters won't be
// available for them to download.
Thread.sleep(2 * timeoutMillis)
}, },
rootCert = rootCaCert rootCert = rootCaCert
) )
@ -149,25 +144,6 @@ class NodeRegistrationTest : IntegrationTest() {
} }
} }
private fun startNetworkManagementServer(networkParameters: NetworkParameters?): NetworkManagementServer {
return NetworkManagementServer().apply {
start(
serverAddress,
configureDatabase(makeTestDataSourceProperties(dbName), DatabaseConfig(runMigration = true)),
CertPathAndKey(listOf(csrCa.certificate, rootCaCert), csrCa.keyPair.private),
DoormanConfig(approveAll = true, jira = null, approveInterval = timeoutMillis),
networkParameters?.let {
NetworkMapStartParams(
LocalSigner(networkMapCa),
it,
NetworkMapConfig(cacheTimeout = timeoutMillis, signInterval = timeoutMillis)
)
}
)
}
}
private fun NodeHandleInternal.onlySeesFromNetworkMap(vararg nodes: NodeHandle) { private fun NodeHandleInternal.onlySeesFromNetworkMap(vararg nodes: NodeHandle) {
// Make sure the nodes aren't getting the node infos from their additional directories // Make sure the nodes aren't getting the node infos from their additional directories
val nodeInfosDir = configuration.baseDirectory / CordformNode.NODE_INFO_DIRECTORY val nodeInfosDir = configuration.baseDirectory / CordformNode.NODE_INFO_DIRECTORY
@ -176,4 +152,32 @@ class NodeRegistrationTest : IntegrationTest() {
} }
assertThat(rpc.networkMapSnapshot()).containsOnlyElementsOf(nodes.map { it.nodeInfo }) assertThat(rpc.networkMapSnapshot()).containsOnlyElementsOf(nodes.map { it.nodeInfo })
} }
private fun startServer(startNetworkMap: Boolean = true): NetworkManagementServer {
val server = NetworkManagementServer(makeTestDataSourceProperties(dbName), DatabaseConfig(runMigration = true))
server.start(
serverAddress,
CertPathAndKey(listOf(doormanCa.certificate, rootCaCert), doormanCa.keyPair.private),
DoormanConfig(approveAll = true, jira = null, approveInterval = timeoutMillis),
if (startNetworkMap) {
NetworkMapStartParams(
LocalSigner(networkMapCa),
NetworkMapConfig(cacheTimeout = timeoutMillis, signInterval = timeoutMillis)
)
} else {
null
}
)
return server
}
private fun applyNetworkParametersAndStart(networkParametersCmd: NetworkParametersCmd) {
server?.close()
NetworkManagementServer(makeTestDataSourceProperties(dbName), DatabaseConfig(runMigration = true)).use {
it.processNetworkParameters(networkParametersCmd)
}
server = startServer(startNetworkMap = true)
// Wait for server to process the parameters update and for the nodes to poll again
Thread.sleep(timeoutMillis * 2)
}
} }

View File

@ -96,14 +96,11 @@ class SigningServiceIntegrationTest : HsmBaseTest() {
@Test @Test
fun `Signing service signs approved CSRs`() { fun `Signing service signs approved CSRs`() {
//Start doorman server //Start doorman server
val database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true)) NetworkManagementServer(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true)).use { server ->
NetworkManagementServer().use { server ->
server.start( server.start(
hostAndPort = NetworkHostAndPort(HOST, 0), hostAndPort = NetworkHostAndPort(HOST, 0),
database = database,
csrCertPathAndKey = null, csrCertPathAndKey = null,
doormanServiceParameter = DoormanConfig(approveAll = true, approveInterval = 2.seconds.toMillis(), jira = null), doormanConfig = DoormanConfig(approveAll = true, approveInterval = 2.seconds.toMillis(), jira = null),
startNetworkMap = null) startNetworkMap = null)
val doormanHostAndPort = server.hostAndPort val doormanHostAndPort = server.hostAndPort
// Start Corda network registration. // Start Corda network registration.
@ -164,9 +161,6 @@ class SigningServiceIntegrationTest : HsmBaseTest() {
doReturn("trustpass").whenever(it).trustStorePassword doReturn("trustpass").whenever(it).trustStorePassword
doReturn("cordacadevpass").whenever(it).keyStorePassword doReturn("cordacadevpass").whenever(it).keyStorePassword
doReturn("iTest@R3.com").whenever(it).emailAddress doReturn("iTest@R3.com").whenever(it).emailAddress
// doReturn(X509KeyStore.fromFile(it.nodeKeystore, it.keyStorePassword, true)).whenever(it).loadNodeKeyStore(any())
// doReturn(X509KeyStore.fromFile(it.sslKeystore, it.keyStorePassword, true)).whenever(it).loadSslKeyStore(any())
// doReturn(trustStore).whenever(it).loadTrustStore(any())
} }
} }
} }

View File

@ -1,6 +1,5 @@
package com.r3.corda.networkmanage.common.persistence package com.r3.corda.networkmanage.common.persistence
import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequestStorage
import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.PKCS10CertificationRequest
/** /**

View File

@ -12,16 +12,20 @@ package com.r3.corda.networkmanage.common.persistence
import com.r3.corda.networkmanage.common.persistence.entity.NetworkMapEntity import com.r3.corda.networkmanage.common.persistence.entity.NetworkMapEntity
import com.r3.corda.networkmanage.common.persistence.entity.NetworkParametersEntity import com.r3.corda.networkmanage.common.persistence.entity.NetworkParametersEntity
import com.r3.corda.networkmanage.common.persistence.entity.ParametersUpdateEntity
import com.r3.corda.networkmanage.common.persistence.entity.NodeInfoEntity import com.r3.corda.networkmanage.common.persistence.entity.NodeInfoEntity
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.internal.DigitalSignatureWithCert import net.corda.core.internal.DigitalSignatureWithCert
import net.corda.core.node.NetworkParameters import net.corda.core.node.NetworkParameters
import net.corda.nodeapi.internal.network.NetworkMapAndSigned import net.corda.nodeapi.internal.network.NetworkMapAndSigned
import net.corda.nodeapi.internal.network.ParametersUpdate
import net.corda.nodeapi.internal.network.SignedNetworkParameters import net.corda.nodeapi.internal.network.SignedNetworkParameters
import java.time.Instant
/** /**
* Data access object interface for NetworkMap persistence layer * Data access object interface for NetworkMap persistence layer
*/ */
// TODO This storage abstraction should be removed. It results in less readable code when constructing network map in NetworkMapSigner
interface NetworkMapStorage { interface NetworkMapStorage {
/** /**
* Returns the active network map, or null * Returns the active network map, or null
@ -52,9 +56,31 @@ interface NetworkMapStorage {
fun saveNetworkParameters(networkParameters: NetworkParameters, signature: DigitalSignatureWithCert?): SecureHash fun saveNetworkParameters(networkParameters: NetworkParameters, signature: DigitalSignatureWithCert?): SecureHash
/** /**
* Retrieves the latest (i.e. most recently inserted) network parameters * Save new parameters update information with corresponding network parameters. Only one parameters update entity can be present at any time. Any existing
* parameters update is cleared and overwritten by this one.
*/
fun saveNewParametersUpdate(networkParameters: NetworkParameters, description: String, updateDeadline: Instant)
/**
* Indicate that it is time to switch network parameters in network map from active ones to the ones from update.
* @param parametersHash hash of the parameters from update
*/
fun setFlagDay(parametersHash: SecureHash)
/**
* Retrieves the latest (i.e. most recently inserted) network parameters entity
* Note that they may not have been signed up yet. * Note that they may not have been signed up yet.
* @return latest network parameters * @return latest network parameters entity
*/ */
fun getLatestNetworkParameters(): NetworkParametersEntity? fun getLatestNetworkParameters(): NetworkParametersEntity?
/**
* Retrieve any parameters update that may be active, null if none are present.
*/
fun getParametersUpdate(): ParametersUpdateEntity?
/**
* Removes any scheduled parameters updates.
*/
fun clearParametersUpdates()
} }

View File

@ -32,10 +32,24 @@ interface NodeInfoStorage {
*/ */
fun getNodeInfo(nodeInfoHash: SecureHash): SignedNodeInfo? fun getNodeInfo(nodeInfoHash: SecureHash): SignedNodeInfo?
/**
* Retrieve latest accepted parameters hash for nodeInfo with given hash.
* @return Hash of network parameters that the node has accepted or null if couldn't find node info with given hash or
* there is no information on accepted parameters hash stored for this entity
*/
fun getAcceptedParametersUpdateHash(nodeInfoHash: SecureHash): SecureHash?
/** /**
* The [nodeInfoAndSigned] is keyed by the public key, old node info with the same public key will be replaced by the new node info. * The [nodeInfoAndSigned] is keyed by the public key, old node info with the same public key will be replaced by the new node info.
* @param nodeInfoAndSigned signed node info data to be stored * @param nodeInfoAndSigned signed node info data to be stored
* @return hash for the newly created node info entry * @return hash for the newly created node info entry
*/ */
fun putNodeInfo(nodeInfoAndSigned: NodeInfoAndSigned): SecureHash fun putNodeInfo(nodeInfoAndSigned: NodeInfoAndSigned): SecureHash
/**
* Store information about latest accepted [NetworkParameters] hash.
* @param publicKeyHash Hash of public key that accepted network parameters. This public key should belong to [NodeInfo]
* @param acceptedParametersHash Hash of latest accepted network parameters.
*/
fun ackNodeInfoParametersUpdate(publicKeyHash: SecureHash, acceptedParametersHash: SecureHash)
} }

View File

@ -68,7 +68,8 @@ sealed class NetworkManagementSchemaServices {
CertificateRevocationListEntity::class.java, CertificateRevocationListEntity::class.java,
NodeInfoEntity::class.java, NodeInfoEntity::class.java,
NetworkParametersEntity::class.java, NetworkParametersEntity::class.java,
NetworkMapEntity::class.java)) { NetworkMapEntity::class.java,
ParametersUpdateEntity::class.java)) {
override val migrationResource = "network-manager.changelog-master" override val migrationResource = "network-manager.changelog-master"
} }
} }

View File

@ -15,9 +15,11 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.internal.DigitalSignatureWithCert import net.corda.core.internal.DigitalSignatureWithCert
import net.corda.core.node.NetworkParameters import net.corda.core.node.NetworkParameters
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.nodeapi.internal.network.ParametersUpdate
import net.corda.nodeapi.internal.network.NetworkMapAndSigned import net.corda.nodeapi.internal.network.NetworkMapAndSigned
import net.corda.nodeapi.internal.network.SignedNetworkParameters import net.corda.nodeapi.internal.network.SignedNetworkParameters
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
import java.time.Instant
import net.corda.nodeapi.internal.persistence.DatabaseTransaction import net.corda.nodeapi.internal.persistence.DatabaseTransaction
/** /**
@ -86,12 +88,21 @@ class PersistentNetworkMapStorage(private val database: CordaPersistence) : Netw
val serialised = networkParameters.serialize() val serialised = networkParameters.serialize()
val hash = serialised.hash val hash = serialised.hash
database.transaction { database.transaction {
session.saveOrUpdate(NetworkParametersEntity( val entity = getNetworkParametersEntity(hash)
val newNetworkParamsEntity = if (entity != null) {
entity.copy(
signature = signature?.bytes,
certificate = signature?.by?.encoded
)
} else {
NetworkParametersEntity(
parametersBytes = serialised.bytes, parametersBytes = serialised.bytes,
parametersHash = hash.toString(), parametersHash = hash.toString(),
signature = signature?.bytes, signature = signature?.bytes,
certificate = signature?.by?.encoded certificate = signature?.by?.encoded
)) )
}
session.merge(newNetworkParamsEntity)
} }
return hash return hash
} }
@ -110,6 +121,52 @@ class PersistentNetworkMapStorage(private val database: CordaPersistence) : Netw
} }
} }
override fun saveNewParametersUpdate(networkParameters: NetworkParameters, description: String, updateDeadline: Instant) {
database.transaction {
val hash = saveNetworkParameters(networkParameters, null)
val netParamsEntity = getNetworkParametersEntity(hash)!!
clearParametersUpdates()
session.save(ParametersUpdateEntity(0, netParamsEntity, description, updateDeadline))
}
}
override fun clearParametersUpdates() {
database.transaction {
val delete = "delete from ${ParametersUpdateEntity::class.java.name}"
session.createQuery(delete).executeUpdate()
}
}
override fun getParametersUpdate(): ParametersUpdateEntity? {
return database.transaction {
val currentParametersHash = getActiveNetworkMap()?.toNetworkMap()?.networkParameterHash
val latestParameters = getLatestNetworkParameters()
val criteria = session.criteriaBuilder.createQuery(ParametersUpdateEntity::class.java)
val root = criteria.from(ParametersUpdateEntity::class.java)
val query = criteria.select(root)
// We just want the last entry
val parametersUpdate = session.createQuery(query).setMaxResults(1).uniqueResult()
check(parametersUpdate == null || latestParameters == parametersUpdate.networkParameters) {
"ParametersUpdate doesn't correspond to latest network parameters"
}
// Highly unlikely, but...
check (parametersUpdate == null || latestParameters?.parametersHash != currentParametersHash?.toString()) {
"Having update for parameters that are already in network map"
}
parametersUpdate
}
}
override fun setFlagDay(parametersHash: SecureHash) {
database.transaction {
val parametersUpdateEntity = getParametersUpdate() ?: throw IllegalArgumentException("Setting flag day but no parameters update to switch to")
if (parametersHash.toString() != parametersUpdateEntity.networkParameters.parametersHash) {
throw IllegalArgumentException("Setting flag day for parameters: $parametersHash, but in database we have update for: ${parametersUpdateEntity.networkParameters.parametersHash}")
}
session.merge(parametersUpdateEntity.copy(flagDay = true))
}
}
private fun DatabaseTransaction.getNetworkParametersEntity(hash: SecureHash): NetworkParametersEntity? { private fun DatabaseTransaction.getNetworkParametersEntity(hash: SecureHash): NetworkParametersEntity? {
return uniqueEntityWhere { builder, path -> return uniqueEntityWhere { builder, path ->
builder.equal(path.get<String>(NetworkParametersEntity::parametersHash.name), hash.toString()) builder.equal(path.get<String>(NetworkParametersEntity::parametersHash.name), hash.toString())

View File

@ -13,6 +13,7 @@ package com.r3.corda.networkmanage.common.persistence
import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity
import com.r3.corda.networkmanage.common.persistence.entity.NodeInfoEntity import com.r3.corda.networkmanage.common.persistence.entity.NodeInfoEntity
import com.r3.corda.networkmanage.common.utils.buildCertPath import com.r3.corda.networkmanage.common.utils.buildCertPath
import com.r3.corda.networkmanage.common.utils.hashString
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256 import net.corda.core.crypto.sha256
import net.corda.core.internal.CertRole import net.corda.core.internal.CertRole
@ -54,6 +55,7 @@ class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeIn
val hash = signedNodeInfo.raw.hash val hash = signedNodeInfo.raw.hash
val hashedNodeInfo = NodeInfoEntity( val hashedNodeInfo = NodeInfoEntity(
nodeInfoHash = hash.toString(), nodeInfoHash = hash.toString(),
identityPkHash = nodeInfo.legalIdentities.first().owningKey.hashString(),
certificateSigningRequest = request, certificateSigningRequest = request,
signedNodeInfoBytes = signedNodeInfo.serialize().bytes, signedNodeInfoBytes = signedNodeInfo.serialize().bytes,
isCurrent = true) isCurrent = true)
@ -68,6 +70,13 @@ class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeIn
} }
} }
override fun getAcceptedParametersUpdateHash(nodeInfoHash: SecureHash): SecureHash? {
return database.transaction {
val hashString = session.find(NodeInfoEntity::class.java, nodeInfoHash.toString())?.acceptedParametersHash
if (hashString == null || hashString.isEmpty()) null else SecureHash.parse(hashString)
}
}
override fun getCertificatePath(publicKeyHash: SecureHash): CertPath? { override fun getCertificatePath(publicKeyHash: SecureHash): CertPath? {
return database.transaction { return database.transaction {
val request = getSignedRequestByPublicHash(publicKeyHash) val request = getSignedRequestByPublicHash(publicKeyHash)
@ -75,6 +84,20 @@ class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeIn
} }
} }
override fun ackNodeInfoParametersUpdate(publicKeyHash: SecureHash, acceptedParametersHash: SecureHash) {
return database.transaction {
val builder = session.criteriaBuilder
val query = builder.createQuery(NodeInfoEntity::class.java).run {
from(NodeInfoEntity::class.java).run {
where(builder.equal(get<NodeInfoEntity>(NodeInfoEntity::identityPkHash.name), publicKeyHash.toString()))
}
}
val nodeInfo = session.createQuery(query).setMaxResults(1).uniqueResult()
val newInfo = nodeInfo.copy(acceptedParametersHash = acceptedParametersHash.toString())
session.merge(newInfo)
}
}
private fun DatabaseTransaction.getSignedRequestByPublicHash(publicKeyHash: SecureHash): CertificateSigningRequestEntity? { private fun DatabaseTransaction.getSignedRequestByPublicHash(publicKeyHash: SecureHash): CertificateSigningRequestEntity? {
return uniqueEntityWhere { builder, path -> return uniqueEntityWhere { builder, path ->
val publicKeyEq = builder.equal(path.get<String>(CertificateSigningRequestEntity::publicKeyHash.name), publicKeyHash.toString()) val publicKeyEq = builder.equal(path.get<String>(CertificateSigningRequestEntity::publicKeyHash.name), publicKeyHash.toString())

View File

@ -55,4 +55,19 @@ class NetworkParametersEntity(
DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature) DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature)
) )
} }
fun copy(parametersHash: String = this.parametersHash,
created: Instant = this.created,
parametersBytes: ByteArray = this.parametersBytes,
signature: ByteArray? = this.signature,
certificate: ByteArray? = this.certificate
): NetworkParametersEntity {
return NetworkParametersEntity(
parametersHash = parametersHash,
created = created,
parametersBytes = parametersBytes,
signature = signature,
certificate = certificate
)
}
} }

View File

@ -23,6 +23,9 @@ data class NodeInfoEntity(
@Column(name = "node_info_hash", length = 64) @Column(name = "node_info_hash", length = 64)
val nodeInfoHash: String = "", val nodeInfoHash: String = "",
@Column(name = "public_key_hash", length = 64)
val identityPkHash: String = "",
@ManyToOne(optional = false, fetch = FetchType.LAZY) @ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "certificate_signing_request") @JoinColumn(name = "certificate_signing_request")
val certificateSigningRequest: CertificateSigningRequestEntity, val certificateSigningRequest: CertificateSigningRequestEntity,
@ -35,10 +38,30 @@ data class NodeInfoEntity(
val isCurrent: Boolean, val isCurrent: Boolean,
@Column(name = "published_at") @Column(name = "published_at")
val publishedAt: Instant = Instant.now() val publishedAt: Instant = Instant.now(),
@Column(name = "accepted_parameters_hash", length = 64)
val acceptedParametersHash: String = ""
) { ) {
/** /**
* Deserialize NodeInfoEntity.signedNodeInfoBytes into the [SignedNodeInfo] instance * Deserialize NodeInfoEntity.signedNodeInfoBytes into the [SignedNodeInfo] instance
*/ */
fun toSignedNodeInfo() = signedNodeInfoBytes.deserialize<SignedNodeInfo>() fun toSignedNodeInfo() = signedNodeInfoBytes.deserialize<SignedNodeInfo>()
fun copy(nodeInfoHash: String = this.nodeInfoHash,
certificateSigningRequest: CertificateSigningRequestEntity = this.certificateSigningRequest,
signedNodeInfoBytes: ByteArray = this.signedNodeInfoBytes,
isCurrent: Boolean = this.isCurrent,
publishedAt: Instant = this.publishedAt,
acceptedParametersHash: String = this.acceptedParametersHash
): NodeInfoEntity {
return NodeInfoEntity(
nodeInfoHash = nodeInfoHash,
certificateSigningRequest = certificateSigningRequest,
signedNodeInfoBytes = signedNodeInfoBytes,
isCurrent = isCurrent,
publishedAt = publishedAt,
acceptedParametersHash = acceptedParametersHash
)
}
} }

View File

@ -0,0 +1,47 @@
package com.r3.corda.networkmanage.common.persistence.entity
import net.corda.core.crypto.SecureHash
import net.corda.nodeapi.internal.network.ParametersUpdate
import java.time.Instant
import javax.persistence.*
@Entity
@Table(name = "parameters_update")
class ParametersUpdateEntity(
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
val id: Long? = null,
// TODO use @MapsId to get rid of additional sequence
@ManyToOne(fetch = FetchType.EAGER, optional = false)
@JoinColumn(name = "network_parameters", foreignKey = ForeignKey(name = "FK__param_up__net_param"))
val networkParameters: NetworkParametersEntity,
@Column(name = "description")
val description: String,
@Column(name = "update_deadline")
val updateDeadline: Instant,
// This boolean flag is used when we want to explicitly point that it's time to switch parameters in network map.
@Column(name = "flag_day")
val flagDay: Boolean = false
) {
fun toParametersUpdate(): ParametersUpdate = ParametersUpdate(SecureHash.parse(networkParameters.parametersHash), description, updateDeadline)
fun toNetMapUpdate(): ParametersUpdate? = if (!flagDay) ParametersUpdate(SecureHash.parse(networkParameters.parametersHash), description, updateDeadline) else null
fun copy(id: Long? = this.id,
networkParameters: NetworkParametersEntity = this.networkParameters,
description: String = this.description,
updateDeadline: Instant = this.updateDeadline,
flagDay: Boolean = this.flagDay): ParametersUpdateEntity {
return ParametersUpdateEntity(
id = id,
networkParameters = networkParameters,
description = description,
updateDeadline = updateDeadline,
flagDay = flagDay
)
}
}

View File

@ -12,12 +12,15 @@ package com.r3.corda.networkmanage.common.signer
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.nodeapi.internal.network.NetworkMap
import net.corda.core.node.NetworkParameters import net.corda.core.node.NetworkParameters
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.serialize
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug import net.corda.core.utilities.debug
import net.corda.nodeapi.internal.network.NetworkMap
import net.corda.nodeapi.internal.network.NetworkMapAndSigned import net.corda.nodeapi.internal.network.NetworkMapAndSigned
class NetworkMapSigner(private val networkMapStorage: NetworkMapStorage, private val signer: Signer) { class NetworkMapSigner(private val networkMapStorage: NetworkMapStorage, private val signer: Signer) {
private companion object { private companion object {
val logger = contextLogger() val logger = contextLogger()
@ -27,37 +30,39 @@ class NetworkMapSigner(private val networkMapStorage: NetworkMapStorage, private
* Signs the network map and latest network parameters if they haven't been signed yet. * Signs the network map and latest network parameters if they haven't been signed yet.
*/ */
fun signNetworkMap() { fun signNetworkMap() {
// TODO There is no network parameters update process in place yet. We assume that latest parameters are to be used logger.debug("Fetching current network map...")
// in current network map. val currentNetworkMap = networkMapStorage.getActiveNetworkMap()
val latestNetParamsEntity = networkMapStorage.getLatestNetworkParameters() if (currentNetworkMap == null) {
if (latestNetParamsEntity == null) { logger.info("There is currently no network map")
logger.warn("No network parameters present") } else {
logger.debug { "Current network map: $currentNetworkMap" }
}
val currentNetworkParameters = currentNetworkMap?.networkParameters
logger.debug("Current network map parameters: ${currentNetworkParameters?.toNetworkParameters()}")
logger.debug("Fetching node info hashes with VALID certificates...")
val nodeInfoHashes = networkMapStorage.getActiveNodeInfoHashes()
logger.debug("Retrieved node info hashes: $nodeInfoHashes")
val latestNetworkParameters = networkMapStorage.getLatestNetworkParameters()
if (latestNetworkParameters == null) {
logger.debug("No network parameters present")
return return
} }
logger.debug { "Retrieved latest network parameters: ${latestNetworkParameters.toNetworkParameters()}" }
val latestNetParams = latestNetParamsEntity.toNetworkParameters() val parametersUpdate = networkMapStorage.getParametersUpdate()
logger.debug { "Latest network parameters: $latestNetParams" } logger.debug { "Retrieved parameters update: $parametersUpdate" }
// We persist signed parameters only if they were not persisted before (they are not in currentSignedNetworkMap as normal parameters or as an update)
if (!latestNetParamsEntity.isSigned) { if (!latestNetworkParameters.isSigned) {
signAndPersistNetworkParameters(latestNetParams) signAndPersistNetworkParameters(latestNetworkParameters.toNetworkParameters())
} else { } else {
logger.debug("Network parameters are already signed") logger.debug { "No need to sign any network parameters as they're up-to-date" }
} }
val parametersToNetworkMap = if (parametersUpdate?.flagDay == true || currentNetworkParameters == null) {
val nodeInfoHashes = networkMapStorage.getActiveNodeInfoHashes() networkMapStorage.clearParametersUpdates()
logger.debug { "Active node-info hashes:\n${nodeInfoHashes.joinToString("\n")}" } latestNetworkParameters
} else currentNetworkParameters
val currentNetworkMap = networkMapStorage.getActiveNetworkMap()?.toNetworkMap() val newNetworkMap = NetworkMap(nodeInfoHashes, SecureHash.parse(parametersToNetworkMap.parametersHash), parametersUpdate?.toNetMapUpdate())
if (currentNetworkMap != null) {
logger.debug { "Current network map: $currentNetworkMap" }
} else {
logger.info("There is currently no network map")
}
val newNetworkMap = NetworkMap(nodeInfoHashes, SecureHash.parse(latestNetParamsEntity.parametersHash), null)
logger.debug { "Potential new network map: $newNetworkMap" } logger.debug { "Potential new network map: $newNetworkMap" }
if (currentNetworkMap?.toNetworkMap() != newNetworkMap) {
if (currentNetworkMap != newNetworkMap) {
val netNetworkMapAndSigned = NetworkMapAndSigned(newNetworkMap) { signer.signBytes(it.bytes) } val netNetworkMapAndSigned = NetworkMapAndSigned(newNetworkMap) { signer.signBytes(it.bytes) }
networkMapStorage.saveNewActiveNetworkMap(netNetworkMapAndSigned) networkMapStorage.saveNewActiveNetworkMap(netNetworkMapAndSigned)
logger.info("Signed new network map: $newNetworkMap") logger.info("Signed new network map: $newNetworkMap")

View File

@ -1,15 +1,18 @@
package com.r3.corda.networkmanage.doorman package com.r3.corda.networkmanage.doorman
import com.google.common.primitives.Booleans
import com.r3.corda.networkmanage.common.utils.ShowHelpException import com.r3.corda.networkmanage.common.utils.ShowHelpException
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigParseOptions import com.typesafe.config.ConfigParseOptions
import com.typesafe.config.ConfigRenderOptions
import joptsimple.OptionParser import joptsimple.OptionParser
import joptsimple.util.EnumConverter import joptsimple.util.EnumConverter
import joptsimple.util.PathConverter import joptsimple.util.PathConverter
import joptsimple.util.PathProperties import joptsimple.util.PathProperties
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NotaryInfo
import net.corda.nodeapi.internal.config.parseAs import net.corda.nodeapi.internal.config.parseAs
import java.nio.file.Path import java.nio.file.Path
import java.time.Instant
class DoormanArgsParser { class DoormanArgsParser {
private val optionParser = OptionParser() private val optionParser = OptionParser()
@ -24,12 +27,14 @@ class DoormanArgsParser {
.withRequiredArg() .withRequiredArg()
.withValuesConvertedBy(object : EnumConverter<Mode>(Mode::class.java) {}) .withValuesConvertedBy(object : EnumConverter<Mode>(Mode::class.java) {})
.defaultsTo(Mode.DOORMAN) .defaultsTo(Mode.DOORMAN)
private val updateNetworkParametersArg = optionParser private val setNetworkParametersArg = optionParser
.accepts("update-network-parameters", "Update network parameters file. Currently only network parameters initialisation is supported.") .accepts("set-network-parameters", "Set the network parameters using the given file. This can be for either the initial set or to schedule an update.")
.withRequiredArg() .withRequiredArg()
.withValuesConvertedBy(PathConverter(PathProperties.FILE_EXISTING)) .withValuesConvertedBy(PathConverter(PathProperties.FILE_EXISTING))
private val flagDayArg = optionParser.accepts("flag-day", "Roll over the scheduled network parameters to be the current.")
private val cancelUpdateArg = optionParser.accepts("cancel-network-parameters-update", "Cancel the scheduled update of the network parameters.")
private val trustStorePasswordArg = optionParser private val trustStorePasswordArg = optionParser
.accepts("trust-store-password", "Password for the generated network root trust store. Only required when operating in ${Mode.ROOT_KEYGEN} mode.") .accepts("trust-store-password", "Password for the generated network root trust store. Only applicable when operating in ${Mode.ROOT_KEYGEN} mode.")
.withRequiredArg() .withRequiredArg()
fun parse(vararg args: String): DoormanCmdLineOptions { fun parse(vararg args: String): DoormanCmdLineOptions {
@ -39,13 +44,27 @@ class DoormanArgsParser {
} }
val configFile = optionSet.valueOf(configFileArg) val configFile = optionSet.valueOf(configFileArg)
val mode = optionSet.valueOf(modeArg) val mode = optionSet.valueOf(modeArg)
val networkParametersFile = optionSet.valueOf(updateNetworkParametersArg) val setNetworkParametersFile = optionSet.valueOf(setNetworkParametersArg)
val flagDay = optionSet.has(flagDayArg)
val cancelUpdate = optionSet.has(cancelUpdateArg)
require(Booleans.countTrue(setNetworkParametersFile != null, flagDay, cancelUpdate) <= 1) {
"Only one of $setNetworkParametersArg, $flagDay and $cancelUpdate can be specified"
}
val networkParametersOption = when {
setNetworkParametersFile != null -> NetworkParametersCmd.Set.fromFile(setNetworkParametersFile)
flagDay -> NetworkParametersCmd.FlagDay
cancelUpdate -> NetworkParametersCmd.CancelUpdate
else -> null
}
val trustStorePassword = optionSet.valueOf(trustStorePasswordArg) val trustStorePassword = optionSet.valueOf(trustStorePasswordArg)
return DoormanCmdLineOptions(configFile, mode, networkParametersFile, trustStorePassword) return DoormanCmdLineOptions(configFile, mode, networkParametersOption, trustStorePassword)
} }
} }
data class DoormanCmdLineOptions(val configFile: Path, val mode: Mode, val networkParametersFile: Path?, val trustStorePassword: String?) { data class DoormanCmdLineOptions(val configFile: Path,
val mode: Mode,
val networkParametersCmd: NetworkParametersCmd?,
val trustStorePassword: String?) {
init { init {
// Make sure trust store password is only specified in root keygen mode. // Make sure trust store password is only specified in root keygen mode.
if (mode != Mode.ROOT_KEYGEN) { if (mode != Mode.ROOT_KEYGEN) {
@ -53,3 +72,58 @@ data class DoormanCmdLineOptions(val configFile: Path, val mode: Mode, val netwo
} }
} }
} }
sealed class NetworkParametersCmd {
/**
* This is the same as [NetworkParametersConfig] but instead of using [NotaryConfig] it uses the simpler to test with
* [NotaryInfo].
*/
data class Set(val minimumPlatformVersion: Int,
val notaries: List<NotaryInfo>,
val maxMessageSize: Int,
val maxTransactionSize: Int,
val parametersUpdate: ParametersUpdateConfig?) : NetworkParametersCmd() {
companion object {
fun fromFile(file: Path): Set {
return ConfigFactory.parseFile(file.toFile(), ConfigParseOptions.defaults())
.parseAs<NetworkParametersConfig>()
.let {
Set(
it.minimumPlatformVersion,
it.notaries.map { it.toNotaryInfo() },
it.maxMessageSize,
it.maxTransactionSize,
it.parametersUpdate
)
}
}
}
fun checkCompatibility(currentNetParams: NetworkParameters) {
// TODO Comment it out when maxMessageSize is properly wired
// require(previousParameters.maxMessageSize <= newParameters.maxMessageSize) { "maxMessageSize can only increase" }
require(maxTransactionSize >= currentNetParams.maxTransactionSize) { "maxTransactionSize can only increase" }
val removedNames = currentNetParams.notaries.map { it.identity.name } - notaries.map { it.identity.name }
require(removedNames.isEmpty()) { "notaries cannot be removed: $removedNames" }
val removedKeys = currentNetParams.notaries.map { it.identity.owningKey } - notaries.map { it.identity.owningKey }
require(removedKeys.isEmpty()) { "notaries cannot be removed: $removedKeys" }
}
fun toNetworkParameters(modifiedTime: Instant, epoch: Int): NetworkParameters {
return NetworkParameters(
minimumPlatformVersion,
notaries,
maxMessageSize,
maxTransactionSize,
modifiedTime,
epoch,
// TODO: Tudor, Michal - pass the actual network parameters where we figure out how
emptyMap()
)
}
}
object FlagDay : NetworkParametersCmd()
object CancelUpdate : NetworkParametersCmd()
}

View File

@ -11,15 +11,12 @@
package com.r3.corda.networkmanage.doorman package com.r3.corda.networkmanage.doorman
import com.jcabi.manifests.Manifests import com.jcabi.manifests.Manifests
import com.r3.corda.networkmanage.common.persistence.configureDatabase
import com.r3.corda.networkmanage.common.utils.* import com.r3.corda.networkmanage.common.utils.*
import com.r3.corda.networkmanage.doorman.signer.LocalSigner import com.r3.corda.networkmanage.doorman.signer.LocalSigner
import net.corda.core.node.NetworkParameters
import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509KeyStore
import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.X509Utilities
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.time.Instant import java.time.Instant
import kotlin.concurrent.thread
import kotlin.system.exitProcess import kotlin.system.exitProcess
private val logger = LoggerFactory.getLogger("com.r3.corda.networkmanage.doorman") private val logger = LoggerFactory.getLogger("com.r3.corda.networkmanage.doorman")
@ -47,7 +44,7 @@ fun main(args: Array<String>) {
} }
} }
data class NetworkMapStartParams(val signer: LocalSigner?, val updateNetworkParameters: NetworkParameters?, val config: NetworkMapConfig) data class NetworkMapStartParams(val signer: LocalSigner?, val config: NetworkMapConfig)
data class NetworkManagementServerStatus(var serverStartTime: Instant = Instant.now(), var lastRequestCheckTime: Instant? = null) data class NetworkManagementServerStatus(var serverStartTime: Instant = Instant.now(), var lastRequestCheckTime: Instant? = null)
@ -85,28 +82,34 @@ private fun caKeyGenMode(config: NetworkManagementServerConfig) {
private fun doormanMode(cmdLineOptions: DoormanCmdLineOptions, config: NetworkManagementServerConfig) { private fun doormanMode(cmdLineOptions: DoormanCmdLineOptions, config: NetworkManagementServerConfig) {
initialiseSerialization() initialiseSerialization()
val persistence = configureDatabase(config.dataSourceProperties, config.database)
val networkManagementServer = NetworkManagementServer(config.dataSourceProperties, config.database)
if (cmdLineOptions.networkParametersCmd == null) {
// TODO: move signing to signing server. // TODO: move signing to signing server.
val csrAndNetworkMap = processKeyStore(config) val csrAndNetworkMap = processKeyStore(config)
if (csrAndNetworkMap != null) { if (csrAndNetworkMap != null) {
logger.info("Starting network management services with local signing") logger.info("Starting network management services with local signing")
} }
val networkManagementServer = NetworkManagementServer()
val networkParameters = cmdLineOptions.networkParametersFile?.let {
// TODO This check shouldn't be needed. Fix up the config design.
requireNotNull(config.networkMap) { "'networkMap' config is required for applying network parameters" }
logger.info("Parsing network parameters from '${it.toAbsolutePath()}'...")
parseNetworkParametersConfig(it).toNetworkParameters(modifiedTime = Instant.now(), epoch = 1)
}
val networkMapStartParams = config.networkMap?.let { val networkMapStartParams = config.networkMap?.let {
NetworkMapStartParams(csrAndNetworkMap?.second, networkParameters, it) NetworkMapStartParams(csrAndNetworkMap?.second, it)
} }
networkManagementServer.start(config.address, persistence, csrAndNetworkMap?.first, config.doorman, networkMapStartParams) networkManagementServer.start(
config.address,
csrAndNetworkMap?.first,
config.doorman,
networkMapStartParams)
Runtime.getRuntime().addShutdownHook(thread(start = false) { Runtime.getRuntime().addShutdownHook(object : Thread("ShutdownHook") {
override fun run() {
networkManagementServer.close() networkManagementServer.close()
}) }
})
} else {
networkManagementServer.use {
it.processNetworkParameters(cmdLineOptions.networkParametersCmd)
}
}
} }

View File

@ -11,10 +11,7 @@
package com.r3.corda.networkmanage.doorman package com.r3.corda.networkmanage.doorman
import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory
import com.r3.corda.networkmanage.common.persistence.ApproveAllCertificateSigningRequestStorage import com.r3.corda.networkmanage.common.persistence.*
import com.r3.corda.networkmanage.common.persistence.PersistentCertificateSigningRequestStorage
import com.r3.corda.networkmanage.common.persistence.PersistentNetworkMapStorage
import com.r3.corda.networkmanage.common.persistence.PersistentNodeInfoStorage
import com.r3.corda.networkmanage.common.signer.NetworkMapSigner import com.r3.corda.networkmanage.common.signer.NetworkMapSigner
import com.r3.corda.networkmanage.common.utils.CertPathAndKey import com.r3.corda.networkmanage.common.utils.CertPathAndKey
import com.r3.corda.networkmanage.doorman.signer.DefaultCsrHandler import com.r3.corda.networkmanage.doorman.signer.DefaultCsrHandler
@ -23,23 +20,31 @@ import com.r3.corda.networkmanage.doorman.signer.LocalSigner
import com.r3.corda.networkmanage.doorman.webservice.MonitoringWebService import com.r3.corda.networkmanage.doorman.webservice.MonitoringWebService
import com.r3.corda.networkmanage.doorman.webservice.NetworkMapWebService import com.r3.corda.networkmanage.doorman.webservice.NetworkMapWebService
import com.r3.corda.networkmanage.doorman.webservice.RegistrationWebService import com.r3.corda.networkmanage.doorman.webservice.RegistrationWebService
import net.corda.core.crypto.SecureHash
import net.corda.core.node.NetworkParameters import net.corda.core.node.NetworkParameters
import net.corda.core.serialization.serialize
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import java.io.Closeable import java.io.Closeable
import java.net.URI import java.net.URI
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
import java.util.*
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class NetworkManagementServer : Closeable { class NetworkManagementServer(dataSourceProperties: Properties, databaseConfig: DatabaseConfig) : Closeable {
companion object { companion object {
private val logger = contextLogger() private val logger = contextLogger()
} }
private val closeActions = mutableListOf<() -> Unit>() private val closeActions = mutableListOf<() -> Unit>()
private val database = configureDatabase(dataSourceProperties, databaseConfig).also { closeActions += it::close }
private val networkMapStorage = PersistentNetworkMapStorage(database)
private val nodeInfoStorage = PersistentNodeInfoStorage(database)
lateinit var hostAndPort: NetworkHostAndPort lateinit var hostAndPort: NetworkHostAndPort
override fun close() { override fun close() {
@ -53,23 +58,12 @@ class NetworkManagementServer : Closeable {
} }
} }
private fun getNetworkMapService(config: NetworkMapConfig, database: CordaPersistence, signer: LocalSigner?, newNetworkParameters: NetworkParameters?): NetworkMapWebService { private fun getNetworkMapService(config: NetworkMapConfig, signer: LocalSigner?): NetworkMapWebService {
val networkMapStorage = PersistentNetworkMapStorage(database)
val nodeInfoStorage = PersistentNodeInfoStorage(database)
val localNetworkMapSigner = signer?.let { NetworkMapSigner(networkMapStorage, it) } val localNetworkMapSigner = signer?.let { NetworkMapSigner(networkMapStorage, it) }
val latestParameters = networkMapStorage.getLatestNetworkParameters()?.toNetworkParameters() ?:
newNetworkParameters?.let {
val netParamsOfNetworkMap = networkMapStorage.getActiveNetworkMap()?.networkParameters
if (netParamsOfNetworkMap == null) {
localNetworkMapSigner?.signAndPersistNetworkParameters(it) ?: networkMapStorage.saveNetworkParameters(it, null)
} else {
throw UnsupportedOperationException("Network parameters already exist. Updating them is not supported yet.")
}
}
val latestParameters = networkMapStorage.getLatestNetworkParameters() ?:
throw IllegalStateException("No network parameters were found. Please upload new network parameters before starting network map service") throw IllegalStateException("No network parameters were found. Please upload new network parameters before starting network map service")
logger.info("Starting network map service with network parameters: $latestParameters") logger.info("Starting network map service with network parameters: $latestParameters")
localNetworkMapSigner?.signAndPersistNetworkParameters(latestParameters)
if (localNetworkMapSigner != null) { if (localNetworkMapSigner != null) {
logger.info("Starting background worker for signing the network map using the local key store") logger.info("Starting background worker for signing the network map using the local key store")
@ -88,7 +82,6 @@ class NetworkManagementServer : Closeable {
return NetworkMapWebService(nodeInfoStorage, networkMapStorage, config) return NetworkMapWebService(nodeInfoStorage, networkMapStorage, config)
} }
private fun getDoormanService(config: DoormanConfig, private fun getDoormanService(config: DoormanConfig,
database: CordaPersistence, database: CordaPersistence,
csrCertPathAndKey: CertPathAndKey?, csrCertPathAndKey: CertPathAndKey?,
@ -128,16 +121,15 @@ class NetworkManagementServer : Closeable {
} }
fun start(hostAndPort: NetworkHostAndPort, fun start(hostAndPort: NetworkHostAndPort,
database: CordaPersistence,
csrCertPathAndKey: CertPathAndKey?, csrCertPathAndKey: CertPathAndKey?,
doormanServiceParameter: DoormanConfig?, // TODO Doorman config shouldn't be optional as the doorman is always required to run doormanConfig: DoormanConfig?, // TODO Doorman config shouldn't be optional as the doorman is always required to run
startNetworkMap: NetworkMapStartParams? startNetworkMap: NetworkMapStartParams?
) { ) {
val services = mutableListOf<Any>() val services = mutableListOf<Any>()
val serverStatus = NetworkManagementServerStatus() val serverStatus = NetworkManagementServerStatus()
startNetworkMap?.let { services += getNetworkMapService(it.config, database, it.signer, it.updateNetworkParameters) } startNetworkMap?.let { services += getNetworkMapService(it.config, it.signer) }
doormanServiceParameter?.let { services += getDoormanService(it, database, csrCertPathAndKey, serverStatus) } doormanConfig?.let { services += getDoormanService(it, database, csrCertPathAndKey, serverStatus) }
require(services.isNotEmpty()) { "No service created, please provide at least one service config." } require(services.isNotEmpty()) { "No service created, please provide at least one service config." }
@ -150,4 +142,85 @@ class NetworkManagementServer : Closeable {
closeActions += webServer::close closeActions += webServer::close
this.hostAndPort = webServer.hostAndPort this.hostAndPort = webServer.hostAndPort
} }
fun processNetworkParameters(networkParametersCmd: NetworkParametersCmd) {
when (networkParametersCmd) {
is NetworkParametersCmd.Set -> handleSetNetworkParameters(networkParametersCmd)
NetworkParametersCmd.FlagDay -> handleFlagDay()
NetworkParametersCmd.CancelUpdate -> handleCancelUpdate()
}
}
private fun handleSetNetworkParameters(setNetParams: NetworkParametersCmd.Set) {
logger.info("maxMessageSize is not currently wired in the nodes")
val activeNetParams = networkMapStorage.getActiveNetworkMap()?.networkParameters?.toNetworkParameters()
if (activeNetParams == null) {
require(setNetParams.parametersUpdate == null) {
"'parametersUpdate' specified in network parameters file but there are no network parameters to update"
}
val initialNetParams = setNetParams.toNetworkParameters(modifiedTime = Instant.now(), epoch = 1)
logger.info("Saving initial network parameters to be signed:\n$initialNetParams")
networkMapStorage.saveNetworkParameters(initialNetParams, null)
} else {
val parametersUpdate = requireNotNull(setNetParams.parametersUpdate) {
"'parametersUpdate' not specified in network parameters file but there is already an active set of network parameters."
}
setNetParams.checkCompatibility(activeNetParams)
val latestNetParams = checkNotNull(networkMapStorage.getLatestNetworkParameters()?.toNetworkParameters()) {
"Something has gone wrong! We have an active set of network parameters ($activeNetParams) but apparently no latest network parameters!"
}
// It's not necessary that latestNetParams is the current active network parameters. It can be the network
// parameters from a previous update attempt which has't activated yet. We still take the epoch value for this
// new set from latestNetParams to make sure the advertised update attempts have incrementing epochs.
// This has the implication that active network parameters may have gaps in their epochs.
val newNetParams = setNetParams.toNetworkParameters(modifiedTime = Instant.now(), epoch = latestNetParams.epoch + 1)
logger.info("Enabling update to network parameters:\n$newNetParams\n$parametersUpdate")
require(!sameNetworkParameters(latestNetParams, newNetParams)) { "New network parameters are the same as the latest ones" }
networkMapStorage.saveNewParametersUpdate(newNetParams, parametersUpdate.description, parametersUpdate.updateDeadline)
logger.info("Update enabled")
}
}
private fun sameNetworkParameters(params1: NetworkParameters, params2: NetworkParameters): Boolean {
return params1.copy(epoch = 1, modifiedTime = Instant.MAX) == params2.copy(epoch = 1, modifiedTime = Instant.MAX)
}
private fun handleFlagDay() {
val parametersUpdate = checkNotNull(networkMapStorage.getParametersUpdate()) {
"No network parameters updates are scheduled"
}
check(Instant.now() >= parametersUpdate.updateDeadline) {
"Update deadline of ${parametersUpdate.updateDeadline} hasn't passed yet"
}
val activeNetParams = networkMapStorage.getActiveNetworkMap()?.networkParameters
val latestNetParamsEntity = networkMapStorage.getLatestNetworkParameters()!!
check(latestNetParamsEntity.isSigned) {
"Parameters we are trying to switch to haven't been signed yet"
}
// TODO This check is stil not good enough as when it comes to signing, the NetworkMapSigner will just accept
check(latestNetParamsEntity.parametersHash == parametersUpdate.networkParameters.parametersHash) {
"The latest network parameters is not the scheduled one:\n${latestNetParamsEntity.toNetworkParameters()}\n${parametersUpdate.toParametersUpdate()}"
}
logger.info("Flag day has occurred, however the new network parameters won't be active until the new network map is signed.\n" +
"Switching from: $activeNetParams\nTo: ${latestNetParamsEntity.toNetworkParameters()}")
networkMapStorage.setFlagDay(SecureHash.parse(parametersUpdate.networkParameters.parametersHash))
}
private fun handleCancelUpdate() {
val parametersUpdate = networkMapStorage.getParametersUpdate()
if (parametersUpdate == null) {
logger.info("Trying to cancel parameters update but no update is scheduled")
} else {
logger.info("Cancelling parameters update: ${parametersUpdate.toParametersUpdate()}")
// We leave parameters from that update in the database, for auditing reasons
networkMapStorage.clearParametersUpdates()
}
}
} }

View File

@ -11,13 +11,10 @@
package com.r3.corda.networkmanage.doorman package com.r3.corda.networkmanage.doorman
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigParseOptions
import net.corda.core.internal.exists
import net.corda.core.internal.readObject import net.corda.core.internal.readObject
import net.corda.core.node.NetworkParameters import net.corda.core.node.NetworkParameters
import net.corda.core.node.NotaryInfo import net.corda.core.node.NotaryInfo
import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.config.parseAs
import java.nio.file.Path import java.nio.file.Path
import java.time.Instant import java.time.Instant
@ -28,6 +25,7 @@ import java.time.Instant
*/ */
data class NotaryConfig(private val notaryNodeInfoFile: Path, data class NotaryConfig(private val notaryNodeInfoFile: Path,
private val validating: Boolean) { private val validating: Boolean) {
// TODO ENT-1608 - Check that the identity belongs to us
fun toNotaryInfo(): NotaryInfo { fun toNotaryInfo(): NotaryInfo {
val nodeInfo = notaryNodeInfoFile.readObject<SignedNodeInfo>().verified() val nodeInfo = notaryNodeInfoFile.readObject<SignedNodeInfo>().verified()
// It is always the last identity (in the list of identities) that corresponds to the notary identity. // It is always the last identity (in the list of identities) that corresponds to the notary identity.
@ -37,32 +35,15 @@ data class NotaryConfig(private val notaryNodeInfoFile: Path,
} }
} }
data class ParametersUpdateConfig(val description: String, val updateDeadline: Instant)
/** /**
* Data class containing the fields from [NetworkParameters] which can be read at start-up time from doorman. * 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 * It is a proper subset of [NetworkParameters] except for the [notaries] field which is replaced by a list of
* [NotaryConfig] which is parsable. * [NotaryConfig] which is parsable.
*
* This is public only because [parseAs] needs to be able to call its constructor.
*/ */
data class NetworkParametersConfig(val minimumPlatformVersion: Int, data class NetworkParametersConfig(val minimumPlatformVersion: Int,
val notaries: List<NotaryConfig>, val notaries: List<NotaryConfig>,
val maxMessageSize: Int, val maxMessageSize: Int,
val maxTransactionSize: Int) { val maxTransactionSize: Int,
fun toNetworkParameters(modifiedTime: Instant, epoch: Int): NetworkParameters { val parametersUpdate: ParametersUpdateConfig?)
return NetworkParameters(
minimumPlatformVersion,
notaries.map { it.toNotaryInfo() },
maxMessageSize,
maxTransactionSize,
modifiedTime,
epoch,
// TODO: Tudor, Michal - pass the actual network parameters where we figure out how
emptyMap()
)
}
}
fun parseNetworkParametersConfig(configFile: Path): NetworkParametersConfig {
check(configFile.exists()) { "File $configFile does not exist" }
return ConfigFactory.parseFile(configFile.toFile(), ConfigParseOptions.defaults()).parseAs()
}

View File

@ -17,6 +17,8 @@ import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage
import com.r3.corda.networkmanage.doorman.NetworkMapConfig import com.r3.corda.networkmanage.doorman.NetworkMapConfig
import com.r3.corda.networkmanage.doorman.webservice.NetworkMapWebService.Companion.NETWORK_MAP_PATH import com.r3.corda.networkmanage.doorman.webservice.NetworkMapWebService.Companion.NETWORK_MAP_PATH
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignedData
import net.corda.core.crypto.sha256
import net.corda.core.node.NetworkParameters import net.corda.core.node.NetworkParameters
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
@ -96,6 +98,25 @@ class NetworkMapWebService(private val nodeInfoStorage: NodeInfoStorage,
}.build() }.build()
} }
@POST
@Path("ack-parameters")
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
fun ackNetworkParameters(input: InputStream): Response {
return try {
val signedParametersHash = input.readBytes().deserialize<SignedData<SecureHash>>()
val hash = signedParametersHash.verified()
networkMapStorage.getSignedNetworkParameters(hash) ?: throw IllegalArgumentException("No network parameters with hash $hash")
logger.debug { "Received ack-parameters with $hash from ${signedParametersHash.sig.by}" }
nodeInfoStorage.ackNodeInfoParametersUpdate(signedParametersHash.sig.by.encoded.sha256(), hash)
ok()
} catch (e: Exception) {
when (e) {
is SignatureException -> status(Response.Status.FORBIDDEN).entity(e.message)
else -> status(Response.Status.INTERNAL_SERVER_ERROR)
}
}.build()
}
@GET @GET
fun getNetworkMap(): Response = createResponse(currentSignedNetworkMap, addCacheTimeout = true) fun getNetworkMap(): Response = createResponse(currentSignedNetworkMap, addCacheTimeout = true)

View File

@ -97,8 +97,8 @@
<column name="hash" type="NVARCHAR(64)"> <column name="hash" type="NVARCHAR(64)">
<constraints nullable="false"/> <constraints nullable="false"/>
</column> </column>
<column name="certificate" type="BLOB"/>
<column name="created" type="TIMESTAMP"/> <column name="created" type="TIMESTAMP"/>
<column name="certificate" type="BLOB"/>
<column name="parameters_bytes" type="BLOB"> <column name="parameters_bytes" type="BLOB">
<constraints nullable="false"/> <constraints nullable="false"/>
</column> </column>
@ -110,6 +110,7 @@
<column name="node_info_hash" type="NVARCHAR(64)"> <column name="node_info_hash" type="NVARCHAR(64)">
<constraints nullable="false"/> <constraints nullable="false"/>
</column> </column>
<column name="public_key_hash" type="NVARCHAR(64)"/>
<column name="signed_node_info_bytes" type="BLOB"> <column name="signed_node_info_bytes" type="BLOB">
<constraints nullable="false"/> <constraints nullable="false"/>
</column> </column>
@ -122,6 +123,7 @@
<column name="published_at" type="TIMESTAMP"> <column name="published_at" type="TIMESTAMP">
<constraints nullable="false"/> <constraints nullable="false"/>
</column> </column>
<column name="accepted_parameters_hash" type="NVARCHAR(64)"/>
</createTable> </createTable>
</changeSet> </changeSet>
<changeSet author="R3.Corda" id="1520338500424-8"> <changeSet author="R3.Corda" id="1520338500424-8">
@ -313,4 +315,21 @@
constraintName="FK_NM_NP" constraintName="FK_NM_NP"
referencedColumnNames="hash" referencedTableName="network_parameters"/> referencedColumnNames="hash" referencedTableName="network_parameters"/>
</changeSet> </changeSet>
<changeSet author="R3.Corda" id="update-net-params">
<createTable tableName="parameters_update">
<column name="description" type="VARCHAR(255)"/>
<column name="update_deadline" type="TIMESTAMP"/>
<column name="id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="network_parameters" type="NVARCHAR(64)">
<constraints nullable="false"/>
</column>
<column name="flag_day" type="BOOLEAN"/>
</createTable>
<addPrimaryKey columnNames="id" constraintName="CONSTRAINT_PARAMUPKEY" tableName="parameters_update"/>
<addForeignKeyConstraint baseTableName="parameters_update" baseColumnNames="network_parameters"
constraintName="FK_PU_NP"
referencedTableName="network_parameters" referencedColumnNames="hash"/>
</changeSet>
</databaseChangeLog> </databaseChangeLog>

View File

@ -1,76 +0,0 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
package com.r3.corda.networkmanage
import com.google.common.jimfs.Jimfs
import com.r3.corda.networkmanage.doorman.NetworkParametersConfig
import com.r3.corda.networkmanage.doorman.NotaryConfig
import net.corda.core.internal.copyTo
import net.corda.core.node.NotaryInfo
import net.corda.core.serialization.serialize
import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.internal.createNodeInfoAndSigned
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Rule
import org.junit.Test
import java.nio.file.Path
import java.time.Instant
import java.util.*
class NetworkParametersConfigTest {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
private val fs = Jimfs.newFileSystem()
@After
fun cleanUp() {
fs.close()
}
@Test
fun toNetworkParameters() {
val (aliceNodeInfo, aliceSignedNodeInfo) = createNodeInfoAndSigned(ALICE_NAME)
val (bobNodeInfo, bobSignedNodeInfo) = createNodeInfoAndSigned(BOB_NAME)
val networkParametersConfig = NetworkParametersConfig(
notaries = listOf(
NotaryConfig(aliceSignedNodeInfo.writeToFile(), true),
NotaryConfig(bobSignedNodeInfo.writeToFile(), false)
),
maxMessageSize = 100,
maxTransactionSize = 100,
minimumPlatformVersion = 3
)
val modifiedTime = Instant.now()
val networkParameters = networkParametersConfig.toNetworkParameters(modifiedTime = modifiedTime, epoch = 2)
assertThat(networkParameters.modifiedTime).isEqualTo(modifiedTime)
assertThat(networkParameters.epoch).isEqualTo(2)
assertThat(networkParameters.notaries).containsExactly(
NotaryInfo(aliceNodeInfo.legalIdentities[0], true),
NotaryInfo(bobNodeInfo.legalIdentities[0], false)
)
assertThat(networkParameters.maxMessageSize).isEqualTo(100)
assertThat(networkParameters.maxTransactionSize).isEqualTo(100)
assertThat(networkParameters.minimumPlatformVersion).isEqualTo(3)
}
private fun SignedNodeInfo.writeToFile(): Path {
val path = fs.getPath(UUID.randomUUID().toString())
serialize().open().copyTo(path)
return path
}
}

View File

@ -4,9 +4,11 @@ import com.r3.corda.networkmanage.common.persistence.entity.NetworkMapEntity
import com.r3.corda.networkmanage.common.persistence.entity.NetworkParametersEntity import com.r3.corda.networkmanage.common.persistence.entity.NetworkParametersEntity
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.node.NetworkParameters import net.corda.core.node.NetworkParameters
import net.corda.core.serialization.serialize
import net.corda.nodeapi.internal.createDevNetworkMapCa import net.corda.nodeapi.internal.createDevNetworkMapCa
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.nodeapi.internal.network.NetworkMap import net.corda.nodeapi.internal.network.NetworkMap
import net.corda.nodeapi.internal.network.ParametersUpdate
import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.common.internal.testNetworkParameters
fun createNetworkParametersEntity(signingCertAndKeyPair: CertificateAndKeyPair = createDevNetworkMapCa(), fun createNetworkParametersEntity(signingCertAndKeyPair: CertificateAndKeyPair = createDevNetworkMapCa(),
@ -20,10 +22,21 @@ fun createNetworkParametersEntity(signingCertAndKeyPair: CertificateAndKeyPair =
) )
} }
fun createNetworkParametersEntityUnsigned(networkParameters: NetworkParameters = testNetworkParameters()): NetworkParametersEntity {
val serialised = networkParameters.serialize()
return NetworkParametersEntity(
parametersHash = serialised.hash.toString(),
parametersBytes = serialised.bytes,
signature = null,
certificate = null
)
}
fun createNetworkMapEntity(signingCertAndKeyPair: CertificateAndKeyPair = createDevNetworkMapCa(), fun createNetworkMapEntity(signingCertAndKeyPair: CertificateAndKeyPair = createDevNetworkMapCa(),
netParamsEntity: NetworkParametersEntity, netParamsEntity: NetworkParametersEntity,
nodeInfoHashes: List<SecureHash> = emptyList()): NetworkMapEntity { nodeInfoHashes: List<SecureHash> = emptyList(),
val signedNetMap = signingCertAndKeyPair.sign(NetworkMap(nodeInfoHashes, SecureHash.parse(netParamsEntity.parametersHash), null)) parametersUpdate: ParametersUpdate? = null): NetworkMapEntity {
val signedNetMap = signingCertAndKeyPair.sign(NetworkMap(nodeInfoHashes, SecureHash.parse(netParamsEntity.parametersHash), parametersUpdate))
return NetworkMapEntity( return NetworkMapEntity(
networkMapBytes = signedNetMap.raw.bytes, networkMapBytes = signedNetMap.raw.bytes,
signature = signedNetMap.sig.bytes, signature = signedNetMap.sig.bytes,

View File

@ -13,10 +13,13 @@ package com.r3.corda.networkmanage.common.persistence
import com.r3.corda.networkmanage.TestBase import com.r3.corda.networkmanage.TestBase
import com.r3.corda.networkmanage.common.persistence.entity.NodeInfoEntity import com.r3.corda.networkmanage.common.persistence.entity.NodeInfoEntity
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.random63BitValue
import net.corda.core.serialization.serialize
import net.corda.nodeapi.internal.createDevNetworkMapCa import net.corda.nodeapi.internal.createDevNetworkMapCa
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.nodeapi.internal.network.NetworkMap import net.corda.nodeapi.internal.network.NetworkMap
import net.corda.nodeapi.internal.network.NetworkMapAndSigned import net.corda.nodeapi.internal.network.NetworkMapAndSigned
import net.corda.nodeapi.internal.network.ParametersUpdate
import net.corda.nodeapi.internal.network.verifiedNetworkMapCert import net.corda.nodeapi.internal.network.verifiedNetworkMapCert
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseConfig
@ -28,6 +31,7 @@ import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.time.Instant
class PersistentNetworkMapStorageTest : TestBase() { class PersistentNetworkMapStorageTest : TestBase() {
private lateinit var persistence: CordaPersistence private lateinit var persistence: CordaPersistence
@ -121,4 +125,29 @@ class PersistentNetworkMapStorageTest : TestBase() {
// then // then
assertThat(validNodeInfoHashes).containsOnly(nodeInfoHashB) assertThat(validNodeInfoHashes).containsOnly(nodeInfoHashB)
} }
@Test
fun `saveNewParametersUpdate clears the previous updates from database`() {
val testParameters1 = testNetworkParameters(epoch = 1)
val testParameters2 = testNetworkParameters(epoch = 2)
val hash1 = testParameters1.serialize().hash
val hash2 = testParameters2.serialize().hash
val updateDeadline1 = Instant.ofEpochMilli(random63BitValue())
val updateDeadline2 = Instant.ofEpochMilli(random63BitValue())
networkMapStorage.saveNewParametersUpdate(testParameters1, "Update 1", updateDeadline1)
networkMapStorage.saveNewParametersUpdate(testParameters1, "Update of update", updateDeadline1)
assertThat(networkMapStorage.getParametersUpdate()?.toParametersUpdate()).isEqualTo(ParametersUpdate(hash1, "Update of update", updateDeadline1))
networkMapStorage.saveNewParametersUpdate(testParameters2, "Update 3", updateDeadline2)
assertThat(networkMapStorage.getParametersUpdate()?.toParametersUpdate()).isEqualTo(ParametersUpdate(hash2, "Update 3", updateDeadline2))
}
@Test
fun `clear parameters update removes all parameters updates`() {
val params1 = testNetworkParameters(minimumPlatformVersion = 1)
val params2 = testNetworkParameters(minimumPlatformVersion = 2)
networkMapStorage.saveNewParametersUpdate(params1, "Update 1", Instant.ofEpochMilli(random63BitValue()))
networkMapStorage.saveNewParametersUpdate(params2, "Update 2", Instant.ofEpochMilli(random63BitValue()))
networkMapStorage.clearParametersUpdates()
assertThat(networkMapStorage.getParametersUpdate()).isNull()
}
} }

View File

@ -146,6 +146,21 @@ class PersistentNodeInfoStorageTest : TestBase() {
val persistedSignedNodeInfo = nodeInfoStorage.getNodeInfo(nodeInfoHash) val persistedSignedNodeInfo = nodeInfoStorage.getNodeInfo(nodeInfoHash)
assertThat(persistedSignedNodeInfo?.signatures).isEqualTo(nodeInfoWithSigned.signed.signatures) assertThat(persistedSignedNodeInfo?.signatures).isEqualTo(nodeInfoWithSigned.signed.signatures)
} }
@Test
fun `accept parameters updates node info correctly`() {
// given
val (nodeInfoWithSigned) = createValidSignedNodeInfo("Test", requestStorage)
// when
val paramsHash = SecureHash.randomSHA256()
val nodeInfoHash = nodeInfoStorage.putNodeInfo(nodeInfoWithSigned)
nodeInfoStorage.ackNodeInfoParametersUpdate(SecureHash.parse(nodeInfoWithSigned.nodeInfo.legalIdentities.first().owningKey.hashString()), paramsHash)
// then
val persistedParametersHash = nodeInfoStorage.getAcceptedParametersUpdateHash(nodeInfoHash)
assertThat(persistedParametersHash).isEqualTo(paramsHash)
}
} }
internal fun createValidSignedNodeInfo(organisation: String, internal fun createValidSignedNodeInfo(organisation: String,

View File

@ -13,11 +13,17 @@ package com.r3.corda.networkmanage.common.signer
import com.nhaarman.mockito_kotlin.* import com.nhaarman.mockito_kotlin.*
import com.r3.corda.networkmanage.TestBase import com.r3.corda.networkmanage.TestBase
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
import com.r3.corda.networkmanage.common.persistence.entity.ParametersUpdateEntity
import com.r3.corda.networkmanage.createNetworkMapEntity import com.r3.corda.networkmanage.createNetworkMapEntity
import com.r3.corda.networkmanage.createNetworkParametersEntity import com.r3.corda.networkmanage.createNetworkParametersEntity
import com.r3.corda.networkmanage.createNetworkParametersEntityUnsigned
import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.random63BitValue
import net.corda.core.internal.DigitalSignatureWithCert import net.corda.core.internal.DigitalSignatureWithCert
import net.corda.core.internal.uncheckedCast
import net.corda.core.node.NetworkParameters
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.nodeapi.internal.createDevNetworkMapCa import net.corda.nodeapi.internal.createDevNetworkMapCa
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
@ -29,6 +35,7 @@ import org.assertj.core.api.Assertions.assertThat
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.time.Instant
import kotlin.test.assertEquals import kotlin.test.assertEquals
class NetworkMapSignerTest : TestBase() { class NetworkMapSignerTest : TestBase() {
@ -47,27 +54,25 @@ class NetworkMapSignerTest : TestBase() {
signer = mock() signer = mock()
networkMapStorage = mock() networkMapStorage = mock()
networkMapSigner = NetworkMapSigner(networkMapStorage, signer) networkMapSigner = NetworkMapSigner(networkMapStorage, signer)
whenever(signer.signBytes(any())).then {
DigitalSignatureWithCert(signingCertAndKeyPair.certificate, Crypto.doSign(signingCertAndKeyPair.keyPair.private, it.arguments[0] as ByteArray))
}
whenever(signer.signObject(any<NetworkParameters>())).then {
val serialised: SerializedBytes<NetworkParameters> = uncheckedCast(it.arguments[0].serialize())
SignedNetworkParameters(serialised, signer.signBytes(serialised.bytes))
}
} }
@Test @Test
fun `signNetworkMap builds and signs network map and network parameters`() { fun `signNetworkMap builds and signs network map and network parameters`() {
// given // given
val nodeInfoHashes = listOf(SecureHash.randomSHA256(), SecureHash.randomSHA256()) val nodeInfoHashes = listOf(SecureHash.randomSHA256(), SecureHash.randomSHA256())
val activeNetParams = testNetworkParameters(minimumPlatformVersion = 1) val latestNetParams = testNetworkParameters(epoch = 3)
val latestNetParams = testNetworkParameters(minimumPlatformVersion = 2) val latestNetParamsEntity = createNetworkParametersEntityUnsigned(latestNetParams)
val activeNetParamsEntity = createNetworkParametersEntity(signingCertAndKeyPair, activeNetParams)
val latestNetParamsEntity = createNetworkParametersEntity(signingCertAndKeyPair, latestNetParams)
val netMapEntity = createNetworkMapEntity(signingCertAndKeyPair, activeNetParamsEntity, nodeInfoHashes)
whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(latestNetParamsEntity) whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(latestNetParamsEntity)
whenever(networkMapStorage.getActiveNodeInfoHashes()).thenReturn(nodeInfoHashes) whenever(networkMapStorage.getActiveNodeInfoHashes()).thenReturn(nodeInfoHashes)
whenever(networkMapStorage.getActiveNetworkMap()).thenReturn(netMapEntity) whenever(networkMapStorage.getActiveNetworkMap()).thenReturn(null)
whenever(signer.signBytes(any())).then { whenever(networkMapStorage.getParametersUpdate()).thenReturn(null)
DigitalSignatureWithCert(signingCertAndKeyPair.certificate, Crypto.doSign(signingCertAndKeyPair.keyPair.private, it.arguments[0] as ByteArray))
}
whenever(signer.signObject(latestNetParams)).then {
val serialised = latestNetParams.serialize()
SignedNetworkParameters(serialised, signer.signBytes(serialised.bytes))
}
// when // when
networkMapSigner.signNetworkMap() networkMapSigner.signNetworkMap()
@ -75,13 +80,21 @@ class NetworkMapSignerTest : TestBase() {
// then // then
// Verify networkMapStorage calls // Verify networkMapStorage calls
verify(networkMapStorage).getActiveNodeInfoHashes() verify(networkMapStorage).getActiveNodeInfoHashes()
verify(networkMapStorage).getActiveNetworkMap()
verify(networkMapStorage).getParametersUpdate()
verify(networkMapStorage).getLatestNetworkParameters() verify(networkMapStorage).getLatestNetworkParameters()
argumentCaptor<NetworkMapAndSigned>().apply { argumentCaptor<NetworkMapAndSigned>().apply {
verify(networkMapStorage).saveNewActiveNetworkMap(capture()) verify(networkMapStorage).saveNewActiveNetworkMap(capture())
val capturedNetworkMap = firstValue.networkMap val capturedNetworkMap = firstValue.networkMap
// Parameters in network map got swapped for latest ones.
assertEquals(latestNetParams.serialize().hash, capturedNetworkMap.networkParameterHash) assertEquals(latestNetParams.serialize().hash, capturedNetworkMap.networkParameterHash)
assertThat(capturedNetworkMap.nodeInfoHashes).isEqualTo(nodeInfoHashes) assertThat(capturedNetworkMap.nodeInfoHashes).isEqualTo(nodeInfoHashes)
} }
val paramsCaptor = argumentCaptor<NetworkParameters>()
val signatureCaptor = argumentCaptor<DigitalSignatureWithCert>()
verify(networkMapStorage).saveNetworkParameters(paramsCaptor.capture(), signatureCaptor.capture())
assertEquals(paramsCaptor.firstValue, latestNetParams)
assertThat(signatureCaptor.firstValue.verify(latestNetParams.serialize()))
} }
@Test @Test
@ -99,32 +112,128 @@ class NetworkMapSignerTest : TestBase() {
// then // then
// Verify networkMapStorage is not called // Verify networkMapStorage is not called
verify(networkMapStorage, never()).saveNewActiveNetworkMap(any()) verify(networkMapStorage, never()).saveNewActiveNetworkMap(any())
verify(networkMapStorage, never()).saveNetworkParameters(any(), any())
} }
@Test @Test
fun `signNetworkMap creates a new network map if there is no current network map`() { fun `signNetworkMap creates a new network map if there is no current network map`() {
// given // given
val netParams = testNetworkParameters() val netParams = testNetworkParameters()
whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(createNetworkParametersEntity(signingCertAndKeyPair, netParams)) whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(createNetworkParametersEntityUnsigned(netParams))
whenever(networkMapStorage.getActiveNodeInfoHashes()).thenReturn(emptyList()) whenever(networkMapStorage.getActiveNodeInfoHashes()).thenReturn(emptyList())
whenever(networkMapStorage.getActiveNetworkMap()).thenReturn(null) whenever(networkMapStorage.getActiveNetworkMap()).thenReturn(null)
whenever(signer.signBytes(any())).then { whenever(networkMapStorage.getParametersUpdate()).thenReturn(null)
DigitalSignatureWithCert(signingCertAndKeyPair.certificate, Crypto.doSign(signingCertAndKeyPair.keyPair.private, it.arguments[0] as ByteArray))
}
whenever(signer.signObject(netParams)).then {
val serialised = netParams.serialize()
SignedNetworkParameters(serialised, signer.signBytes(serialised.bytes))
}
// when // when
networkMapSigner.signNetworkMap() networkMapSigner.signNetworkMap()
// then // then
// Verify networkMapStorage calls // Verify networkMapStorage calls
verify(networkMapStorage).getActiveNodeInfoHashes() verify(networkMapStorage).getActiveNodeInfoHashes()
verify(networkMapStorage).getActiveNetworkMap()
verify(networkMapStorage).getLatestNetworkParameters() verify(networkMapStorage).getLatestNetworkParameters()
verify(networkMapStorage).getParametersUpdate()
argumentCaptor<NetworkMapAndSigned>().apply { argumentCaptor<NetworkMapAndSigned>().apply {
verify(networkMapStorage).saveNewActiveNetworkMap(capture()) verify(networkMapStorage).saveNewActiveNetworkMap(capture())
assertEquals(netParams.serialize().hash, firstValue.networkMap.networkParameterHash) assertEquals(netParams.serialize().hash, firstValue.networkMap.networkParameterHash)
} }
val paramsCaptor = argumentCaptor<NetworkParameters>()
val signatureCaptor = argumentCaptor<DigitalSignatureWithCert>()
verify(networkMapStorage).saveNetworkParameters(paramsCaptor.capture(), signatureCaptor.capture())
assertEquals(paramsCaptor.firstValue, netParams)
assertThat(signatureCaptor.firstValue.verify(netParams.serialize()))
}
@Test
fun `signNetworkMap signs new network parameters`() {
// given
val currentNetworkParameters = createNetworkParametersEntity(signingCertAndKeyPair)
val updateNetworkParameters = createNetworkParametersEntityUnsigned(testNetworkParameters(epoch = 2))
val updateParametersHash = SecureHash.parse(updateNetworkParameters.parametersHash)
val parametersUpdate = ParametersUpdateEntity(0, updateNetworkParameters,"Update time", Instant.ofEpochMilli(random63BitValue()))
val netMapEntity = createNetworkMapEntity(signingCertAndKeyPair, currentNetworkParameters, emptyList(), null)
whenever(networkMapStorage.getActiveNetworkMap()).thenReturn(netMapEntity)
whenever(networkMapStorage.getParametersUpdate()).thenReturn(parametersUpdate)
whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(updateNetworkParameters)
whenever(networkMapStorage.getActiveNodeInfoHashes()).thenReturn(emptyList())
// when
networkMapSigner.signNetworkMap()
// then
// Verify networkMapStorage calls
verify(networkMapStorage).getActiveNetworkMap()
verify(networkMapStorage).getActiveNodeInfoHashes()
verify(networkMapStorage).getLatestNetworkParameters()
verify(networkMapStorage).getParametersUpdate()
val paramsCaptor = argumentCaptor<NetworkParameters>()
val signatureCaptor = argumentCaptor<DigitalSignatureWithCert>()
verify(networkMapStorage, times(1)).saveNetworkParameters(paramsCaptor.capture(), signatureCaptor.capture())
assertEquals(paramsCaptor.firstValue, updateNetworkParameters.toNetworkParameters())
assertThat(signatureCaptor.firstValue.verify(updateNetworkParameters.parametersBytes))
}
@Test
fun `signNetworkMap fails if there is parameter update without relevant parameters stored`() {
val updateNetworkParameters = createNetworkParametersEntityUnsigned(testNetworkParameters(epoch = 2))
val parametersUpdate = ParametersUpdateEntity(0, updateNetworkParameters,"Update time", Instant.ofEpochMilli(random63BitValue()))
whenever(networkMapStorage.getActiveNetworkMap()).thenReturn(null)
whenever(networkMapStorage.getParametersUpdate()).thenReturn(parametersUpdate)
whenever(networkMapStorage.getActiveNodeInfoHashes()).thenReturn(emptyList())
whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(createNetworkParametersEntity())
verify(networkMapStorage, never()).saveNetworkParameters(any(), any())
verify(networkMapStorage, never()).saveNewActiveNetworkMap(any())
}
@Test
fun `setting flag day on parameters update changes parameters inside network map`() {
val activeNetworkParameters = createNetworkParametersEntity(signingCertAndKeyPair, testNetworkParameters(epoch = 1))
val updateNetworkParameters = createNetworkParametersEntity(signingCertAndKeyPair, testNetworkParameters(epoch = 2))
val parametersUpdate = ParametersUpdateEntity(0, updateNetworkParameters,"Update time", Instant.ofEpochMilli(random63BitValue()))
val activeNetworkMap = createNetworkMapEntity(signingCertAndKeyPair, activeNetworkParameters, emptyList(), parametersUpdate.toParametersUpdate())
whenever(networkMapStorage.getActiveNetworkMap()).thenReturn(activeNetworkMap)
whenever(networkMapStorage.getActiveNodeInfoHashes()).thenReturn(emptyList())
whenever(networkMapStorage.getParametersUpdate()).thenReturn(parametersUpdate.copy(flagDay = true))
whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(updateNetworkParameters)
// when
networkMapSigner.signNetworkMap()
//then
argumentCaptor<NetworkMapAndSigned>().apply {
verify(networkMapStorage).saveNewActiveNetworkMap(capture())
val netMap = firstValue.networkMap
assertEquals(SecureHash.parse(updateNetworkParameters.parametersHash), netMap.networkParameterHash)
assertEquals(emptyList(), netMap.nodeInfoHashes)
assertEquals(null, netMap.parametersUpdate)
}
}
@Test
fun `cancel update test`() {
val activeNetworkParameters = createNetworkParametersEntity(signingCertAndKeyPair, testNetworkParameters(epoch = 1))
val updateNetworkParameters = createNetworkParametersEntityUnsigned(testNetworkParameters(epoch = 2))
val parametersUpdate = ParametersUpdateEntity(0, updateNetworkParameters,"Update time", Instant.ofEpochMilli(random63BitValue()))
val activeNetworkMap = createNetworkMapEntity(signingCertAndKeyPair, activeNetworkParameters, emptyList(), parametersUpdate.toParametersUpdate())
whenever(networkMapStorage.getActiveNetworkMap()).thenReturn(activeNetworkMap)
whenever(networkMapStorage.getActiveNodeInfoHashes()).thenReturn(emptyList())
whenever(networkMapStorage.getParametersUpdate()).thenReturn(null)
whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(createNetworkParametersEntity())
// when
networkMapSigner.signNetworkMap()
//then
argumentCaptor<NetworkMapAndSigned>().apply {
verify(networkMapStorage).saveNewActiveNetworkMap(capture())
val netMap = firstValue.networkMap
assertEquals(SecureHash.parse(activeNetworkParameters.parametersHash), netMap.networkParameterHash)
assertEquals(emptyList(), netMap.nodeInfoHashes)
assertEquals(null, netMap.parametersUpdate)
}
} }
} }

View File

@ -26,7 +26,7 @@ class DoormanArgsParserTest {
@Test @Test
fun `should fail when network parameters file is missing`() { fun `should fail when network parameters file is missing`() {
assertThatThrownBy { assertThatThrownBy {
argsParser.parse("--config-file", validConfigPath, "--update-network-parameters", "not-here") argsParser.parse("--config-file", validConfigPath, "--set-network-parameters", "not-here")
}.hasMessageContaining("not-here") }.hasMessageContaining("not-here")
} }

View File

@ -0,0 +1,70 @@
package com.r3.corda.networkmanage.doorman
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NotaryInfo
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.TestIdentity
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.Test
class NetworkParametersCmdTest {
@Test
fun `maxTransactionSize cannot decrease`() {
val netParams = testNetworkParameters(maxTransactionSize = 100)
val netParamsCmd = netParams.toCmd()
assertThatExceptionOfType(IllegalArgumentException::class.java)
.isThrownBy { netParamsCmd.copy(maxTransactionSize = 99).checkCompatibility(netParams) }
.withMessageContaining("maxTransactionSize")
netParamsCmd.copy(maxTransactionSize = 100).checkCompatibility(netParams)
netParamsCmd.copy(maxTransactionSize = 101).checkCompatibility(netParams)
}
@Test
fun `notary cannot be removed`() {
val alice = NotaryInfo(TestIdentity(ALICE_NAME).party, true)
val bob = NotaryInfo(TestIdentity(BOB_NAME).party, true)
val netParams = testNetworkParameters(notaries = listOf(alice, bob))
val netParamsCmd = netParams.toCmd()
assertThatExceptionOfType(IllegalArgumentException::class.java)
.isThrownBy { netParamsCmd.copy(notaries = listOf(alice)).checkCompatibility(netParams) }
.withMessageContaining("notaries")
assertThatExceptionOfType(IllegalArgumentException::class.java)
.isThrownBy { netParamsCmd.copy(notaries = emptyList()).checkCompatibility(netParams) }
.withMessageContaining("notaries")
}
@Test
fun `notary identity key cannot change`() {
val netParams = testNetworkParameters(notaries = listOf(NotaryInfo(freshParty(ALICE_NAME), true)))
val notaryKeyChanged = netParams.toCmd().copy(notaries = listOf(NotaryInfo(freshParty(ALICE_NAME), true)))
assertThatExceptionOfType(IllegalArgumentException::class.java)
.isThrownBy { notaryKeyChanged.checkCompatibility(netParams) }
.withMessageContaining("notaries")
}
@Test
fun `notary name cannot change`() {
val identity = freshParty(ALICE_NAME)
val netParams = testNetworkParameters(notaries = listOf(NotaryInfo(identity, true)))
val notaryNameChanged = netParams.toCmd().copy(notaries = listOf(NotaryInfo(Party(BOB_NAME, identity.owningKey), true)))
assertThatExceptionOfType(IllegalArgumentException::class.java)
.isThrownBy { notaryNameChanged.checkCompatibility(netParams) }
.withMessageContaining("notaries")
}
private fun NetworkParameters.toCmd(): NetworkParametersCmd.Set {
return NetworkParametersCmd.Set(
minimumPlatformVersion = minimumPlatformVersion,
notaries = notaries,
maxMessageSize = maxMessageSize,
maxTransactionSize = maxTransactionSize,
parametersUpdate = null
)
}
private fun freshParty(name: CordaX500Name) = TestIdentity(name).party
}

View File

@ -0,0 +1,48 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
package com.r3.corda.networkmanage.doorman
import com.google.common.jimfs.Jimfs
import net.corda.core.internal.copyTo
import net.corda.core.serialization.serialize
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.internal.createNodeInfoAndSigned
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Rule
import org.junit.Test
import java.util.*
class NotaryConfigTest {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
private val fs = Jimfs.newFileSystem()
@After
fun cleanUp() {
fs.close()
}
@Test
fun toNotaryInfo() {
val (nodeInfo, signedNodeInfo) = createNodeInfoAndSigned(ALICE_NAME)
val file = fs.getPath(UUID.randomUUID().toString())
signedNodeInfo.serialize().open().copyTo(file)
val notaryInfo = NotaryConfig(file, true).toNotaryInfo()
assertThat(notaryInfo.identity).isEqualTo(nodeInfo.legalIdentities[0])
assertThat(notaryInfo.validating).isTrue()
}
}

View File

@ -10,21 +10,19 @@
package com.r3.corda.networkmanage.doorman.webservice package com.r3.corda.networkmanage.doorman.webservice
import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.*
import com.nhaarman.mockito_kotlin.times
import com.nhaarman.mockito_kotlin.verify
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage
import com.r3.corda.networkmanage.createNetworkMapEntity import com.r3.corda.networkmanage.createNetworkMapEntity
import com.r3.corda.networkmanage.doorman.NetworkManagementWebServer import com.r3.corda.networkmanage.doorman.NetworkManagementWebServer
import com.r3.corda.networkmanage.doorman.NetworkMapConfig import com.r3.corda.networkmanage.doorman.NetworkMapConfig
import net.corda.core.crypto.*
import net.corda.core.crypto.SecureHash.Companion.randomSHA256 import net.corda.core.crypto.SecureHash.Companion.randomSHA256
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.checkOkResponse import net.corda.core.internal.*
import net.corda.core.internal.openHttpConnection
import net.corda.core.internal.responseAs
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.createDevNetworkMapCa import net.corda.nodeapi.internal.createDevNetworkMapCa
@ -43,7 +41,6 @@ import org.junit.Test
import java.io.IOException import java.io.IOException
import java.net.URL import java.net.URL
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import javax.ws.rs.core.MediaType
import kotlin.test.assertEquals import kotlin.test.assertEquals
class NetworkMapWebServiceTest { class NetworkMapWebServiceTest {
@ -68,15 +65,13 @@ class NetworkMapWebServiceTest {
val networkMapStorage: NetworkMapStorage = mock { val networkMapStorage: NetworkMapStorage = mock {
on { getActiveNetworkMap() }.thenReturn(createNetworkMapEntity()) on { getActiveNetworkMap() }.thenReturn(createNetworkMapEntity())
} }
// Create node info. // Create node info.
val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB")) val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"))
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
it.start() it.start()
val nodeInfoAndSignature = signedNodeInfo.serialize().bytes
// Post node info and signature to doorman, this should pass without any exception. // Post node info and signature to doorman, this should pass without any exception.
it.doPost("publish", nodeInfoAndSignature) it.doPost("publish", signedNodeInfo.serialize())
} }
} }
@ -90,8 +85,7 @@ class NetworkMapWebServiceTest {
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
it.start() it.start()
val nodeInfoAndSignature = signedNodeInfo.serialize().bytes assertThatThrownBy { it.doPost("publish", signedNodeInfo.serialize()) }
assertThatThrownBy { it.doPost("publish", nodeInfoAndSignature) }
.hasMessageStartingWith("Response Code 400: Minimum platform version is 2") .hasMessageStartingWith("Response Code 400: Minimum platform version is 2")
} }
} }
@ -106,8 +100,7 @@ class NetworkMapWebServiceTest {
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
it.start() it.start()
val nodeInfoAndSignature = signedNodeInfo.serialize().bytes assertThatThrownBy { it.doPost("publish", signedNodeInfo.serialize()) }
assertThatThrownBy { it.doPost("publish", nodeInfoAndSignature) }
.hasMessageStartingWith("Response Code 503: Network parameters have not been initialised") .hasMessageStartingWith("Response Code 503: Network parameters have not been initialised")
} }
} }
@ -158,7 +151,7 @@ class NetworkMapWebServiceTest {
@Test @Test
fun `get network parameters`() { fun `get network parameters`() {
val networkParameters = testNetworkParameters(emptyList()) val networkParameters = testNetworkParameters()
val signedNetworkParameters = signingCertAndKeyPair.sign(networkParameters) val signedNetworkParameters = signingCertAndKeyPair.sign(networkParameters)
val networkParametersHash = signedNetworkParameters.raw.hash val networkParametersHash = signedNetworkParameters.raw.hash
@ -178,15 +171,33 @@ class NetworkMapWebServiceTest {
} }
} }
private fun NetworkManagementWebServer.doPost(path: String, payload: ByteArray) { @Test
val url = URL("http://$hostAndPort/network-map/$path") fun `ack network parameters update`() {
url.openHttpConnection().apply { val netParams = testNetworkParameters()
doOutput = true val hash = netParams.serialize().hash
requestMethod = "POST" val nodeInfoStorage: NodeInfoStorage = mock {
setRequestProperty("Content-Type", MediaType.APPLICATION_OCTET_STREAM) on { ackNodeInfoParametersUpdate(any(), eq(hash)) }.then { Unit }
outputStream.write(payload)
checkOkResponse()
} }
val networkMapStorage: NetworkMapStorage = mock {
on { getSignedNetworkParameters(hash) }.thenReturn(signingCertAndKeyPair.sign(netParams))
}
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(nodeInfoStorage, networkMapStorage, testNetworkMapConfig)).use {
it.start()
val keyPair = Crypto.generateKeyPair()
val signedHash = hash.serialize().sign { keyPair.sign(it) }
it.doPost("ack-parameters", signedHash.serialize())
verify(nodeInfoStorage).ackNodeInfoParametersUpdate(keyPair.public.encoded.sha256(), hash)
val randomSigned = SecureHash.randomSHA256().serialize().sign { keyPair.sign(it) }
assertThatThrownBy { it.doPost("ack-parameters", randomSigned.serialize()) }
.hasMessageContaining("HTTP ERROR 500")
val badSigned = SignedData(signedHash.raw, randomSigned.sig)
assertThatThrownBy { it.doPost("ack-parameters", badSigned.serialize()) }
.hasMessageStartingWith("Response Code 403: Signature Verification failed!")
}
}
private fun NetworkManagementWebServer.doPost(path: String, payload: OpaqueBytes) {
URL("http://$hostAndPort/network-map/$path").post(payload)
} }
private inline fun <reified T : Any> NetworkManagementWebServer.doGet(path: String): T { private inline fun <reified T : Any> NetworkManagementWebServer.doGet(path: String): T {

View File

@ -12,22 +12,20 @@ package net.corda.node.services.network
import net.corda.cordform.CordformNode import net.corda.cordform.CordformNode
import net.corda.core.crypto.random63BitValue import net.corda.core.crypto.random63BitValue
import net.corda.core.internal.*
import net.corda.core.internal.concurrent.transpose import net.corda.core.internal.concurrent.transpose
import net.corda.core.internal.div import net.corda.core.messaging.ParametersUpdateInfo
import net.corda.core.internal.exists
import net.corda.core.internal.list
import net.corda.core.internal.readObject
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
import net.corda.core.serialization.serialize
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME
import net.corda.nodeapi.internal.network.SignedNetworkParameters import net.corda.nodeapi.internal.network.SignedNetworkParameters
import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.*
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.internal.NodeHandleInternal
import net.corda.testing.driver.internal.RandomFree import net.corda.testing.driver.internal.RandomFree
import net.corda.testing.internal.IntegrationTest import net.corda.testing.internal.IntegrationTest
import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.IntegrationTestSchemas
@ -35,6 +33,7 @@ import net.corda.testing.internal.toDatabaseSchemaName
import net.corda.testing.node.internal.CompatibilityZoneParams import net.corda.testing.node.internal.CompatibilityZoneParams
import net.corda.testing.node.internal.internalDriver import net.corda.testing.node.internal.internalDriver
import net.corda.testing.node.internal.network.NetworkMapServer import net.corda.testing.node.internal.network.NetworkMapServer
import org.assertj.core.api.Assertions
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.* import org.junit.*
import java.net.URL import java.net.URL
@ -73,6 +72,53 @@ class NetworkMapTest : IntegrationTest() {
networkMapServer.close() networkMapServer.close()
} }
@Test
fun `parameters update test`() {
internalDriver(
portAllocation = portAllocation,
compatibilityZone = compatibilityZone,
initialiseSerialization = false,
notarySpecs = emptyList()
) {
val alice = startNode(providedName = ALICE_NAME).getOrThrow() as NodeHandleInternal
val nextParams = networkMapServer.networkParameters.copy(epoch = 3, modifiedTime = Instant.ofEpochMilli(random63BitValue()))
val nextHash = nextParams.serialize().hash
val snapshot = alice.rpc.networkParametersFeed().snapshot
val updates = alice.rpc.networkParametersFeed().updates.bufferUntilSubscribed()
assertEquals(null, snapshot)
assertThat(updates.isEmpty)
networkMapServer.scheduleParametersUpdate(nextParams, "Next parameters", Instant.ofEpochMilli(random63BitValue()))
// Wait for network map client to poll for the next update.
Thread.sleep(cacheTimeout.toMillis() * 2)
val laterParams = networkMapServer.networkParameters.copy(epoch = 4, modifiedTime = Instant.ofEpochMilli(random63BitValue()))
val laterHash = laterParams.serialize().hash
networkMapServer.scheduleParametersUpdate(laterParams, "Another update", Instant.ofEpochMilli(random63BitValue()))
Thread.sleep(cacheTimeout.toMillis() * 2)
updates.expectEvents(isStrict = false) {
sequence(
expect { update: ParametersUpdateInfo ->
assertEquals(update.description, "Next parameters")
assertEquals(update.hash, nextHash)
assertEquals(update.parameters, nextParams)
},
expect { update: ParametersUpdateInfo ->
assertEquals(update.description, "Another update")
assertEquals(update.hash, laterHash)
assertEquals(update.parameters, laterParams)
}
)
}
// This should throw, because the nextHash has been replaced by laterHash
Assertions.assertThatThrownBy { alice.rpc.acceptNewNetworkParameters(nextHash) }.hasMessageContaining("Refused to accept parameters with hash")
alice.rpc.acceptNewNetworkParameters(laterHash)
assertEquals(laterHash, networkMapServer.latestParametersAccepted(alice.nodeInfo.legalIdentities.first().owningKey))
networkMapServer.advertiseNewParameters()
val networkParameters = (alice.configuration.baseDirectory / NETWORK_PARAMS_UPDATE_FILE_NAME)
.readObject<SignedNetworkParameters>().verified()
Assert.assertEquals(networkParameters, laterParams)
}
}
@Test @Test
fun `node correctly downloads and saves network parameters file on startup`() { fun `node correctly downloads and saves network parameters file on startup`() {
internalDriver( internalDriver(

View File

@ -215,10 +215,10 @@ class NetworkMapUpdaterTest {
updates.expectEvents(isStrict = false) { updates.expectEvents(isStrict = false) {
sequence( sequence(
expect { update: ParametersUpdateInfo -> expect { update: ParametersUpdateInfo ->
assertThat(update.updateDeadline == updateDeadline) assertEquals(update.updateDeadline, updateDeadline)
assertThat(update.description == "Test update") assertEquals(update.description,"Test update")
assertThat(update.hash == newParameters.serialize().hash) assertEquals(update.hash, newParameters.serialize().hash)
assertThat(update.parameters == newParameters) assertEquals(update.parameters, newParameters)
} }
) )
} }

View File

@ -54,10 +54,6 @@ class NetworkMapServer(private val cacheTimeout: Duration,
private val server: Server private val server: Server
var networkParameters: NetworkParameters = stubNetworkParameters var networkParameters: NetworkParameters = stubNetworkParameters
set(networkParameters) {
check(field == stubNetworkParameters) { "Network parameters can be set only once" }
field = networkParameters
}
private val service = InMemoryNetworkMapService() private val service = InMemoryNetworkMapService()
private var parametersUpdate: ParametersUpdate? = null private var parametersUpdate: ParametersUpdate? = null
private var nextNetworkParameters: NetworkParameters? = null private var nextNetworkParameters: NetworkParameters? = null