First approach to network parameters updates (#2412)

* Network parameters updates

Add two RPC methods networkParametersFeed and
acceptNewNetworkParameters. Implementation of client handling of network
parameters update event. Partial implementation of accepting new
parameters and installing them on the node as well as node startup with
updated parameters.

Move reading of network parameters on startup to separate
NetworkParametersReader class. Add tests.

Move NetworkParameters and NotaryInfo classes to core.

* Ignore evolvability test - to be fixed later

* Add documentation on update process
This commit is contained in:
Katarzyna Streich 2018-02-08 14:31:43 +00:00 committed by GitHub
parent cbe947694d
commit 6acff3a7df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 588 additions and 132 deletions

View File

@ -3,16 +3,16 @@
package net.corda.core.internal
import net.corda.core.cordapp.CordappProvider
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sha256
import net.corda.core.crypto.*
import net.corda.core.identity.CordaX500Name
import net.corda.core.node.ServicesForResolution
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.OpaqueBytes
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x500.X500NameBuilder
import org.bouncycastle.asn1.x500.style.BCStyle
@ -30,6 +30,7 @@ import java.nio.charset.Charset
import java.nio.charset.StandardCharsets.UTF_8
import java.nio.file.*
import java.nio.file.attribute.FileAttribute
import java.security.KeyPair
import java.security.PrivateKey
import java.security.cert.X509Certificate
import java.time.Duration
@ -307,6 +308,16 @@ val KClass<*>.packageName: String get() = java.`package`.name
fun URL.openHttpConnection(): HttpURLConnection = openConnection() as HttpURLConnection
fun URL.post(serializedData: OpaqueBytes) {
openHttpConnection().apply {
doOutput = true
requestMethod = "POST"
setRequestProperty("Content-Type", "application/octet-stream")
outputStream.use { serializedData.open().copyTo(it) }
checkOkResponse()
}
}
fun HttpURLConnection.checkOkResponse() {
if (responseCode != 200) {
val message = errorStream.use { it.reader().readText() }
@ -353,3 +364,11 @@ fun <T : Any> T.signWithCert(privateKey: PrivateKey, certificate: X509Certificat
val signature = Crypto.doSign(privateKey, serialised.bytes)
return SignedDataWithCert(serialised, DigitalSignatureWithCert(certificate, signature))
}
inline fun <T : Any> SerializedBytes<T>.sign(signer: (SerializedBytes<T>) -> DigitalSignature.WithKey): SignedData<T> {
return SignedData(this, signer(this))
}
inline fun <T : Any> SerializedBytes<T>.sign(keyPair: KeyPair): SignedData<T> {
return SignedData(this, keyPair.sign(this.bytes))
}

View File

@ -13,6 +13,7 @@ import net.corda.core.flows.StateMachineRunId
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NodeInfo
import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.NetworkMapCache
@ -23,6 +24,7 @@ import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.Try
import rx.Observable
import java.io.IOException
import java.io.InputStream
import java.security.PublicKey
import java.time.Instant
@ -72,6 +74,24 @@ sealed class StateMachineUpdate {
data class Removed(override val id: StateMachineRunId, val result: Try<*>) : StateMachineUpdate()
}
// DOCSTART 1
/**
* Data class containing information about the scheduled network parameters update. The info is emitted every time node
* receives network map with [ParametersUpdate] which wasn't seen before. For more information see: [CordaRPCOps.networkParametersFeed] and [CordaRPCOps.acceptNewNetworkParameters].
* @property hash new [NetworkParameters] hash
* @property parameters new [NetworkParameters] data structure
* @property description description of the update
* @property updateDeadline deadline for accepting this update using [CordaRPCOps.acceptNewNetworkParameters]
*/
@CordaSerializable
data class ParametersUpdateInfo(
val hash: SecureHash,
val parameters: NetworkParameters,
val description: String,
val updateDeadline: Instant
)
// DOCEND 1
@CordaSerializable
data class StateMachineTransactionMapping(val stateMachineRunId: StateMachineRunId, val transactionId: SecureHash)
@ -205,6 +225,29 @@ interface CordaRPCOps : RPCOps {
@RPCReturnsObservables
fun networkMapFeed(): DataFeed<List<NodeInfo>, NetworkMapCache.MapChange>
/**
* Returns [DataFeed] object containing information on currently scheduled parameters update (null if none are currently scheduled)
* and observable with future update events. Any update that occurs before the deadline automatically cancels the current one.
* Only the latest update can be accepted.
* Note: This operation may be restricted only to node administrators.
*/
// TODO This operation should be restricted to just node admins.
@RPCReturnsObservables
fun networkParametersFeed(): DataFeed<ParametersUpdateInfo?, ParametersUpdateInfo>
/**
* Accept network parameters with given hash, hash is obtained through [networkParametersFeed] method.
* Information is sent back to the zone operator that the node accepted the parameters update - this process cannot be
* undone.
* Only parameters that are scheduled for update can be accepted, if different hash is provided this method will fail.
* Note: This operation may be restricted only to node administrators.
* @param parametersHash hash of network parameters to accept
* @throws IllegalArgumentException if network map advertises update with different parameters hash then the one accepted by node's operator.
* @throws IOException if failed to send the approval to network map
*/
// TODO This operation should be restricted to just node admins.
fun acceptNewNetworkParameters(parametersHash: SecureHash)
/**
* Start the given flow with the given arguments. [logicType] must be annotated
* with [net.corda.core.flows.StartableByRPC].

View File

@ -0,0 +1,45 @@
package net.corda.core.node
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import java.time.Instant
/**
* Network parameters are a set of values that every node participating in the zone needs to agree on and use to
* correctly interoperate with each other.
* @property minimumPlatformVersion Minimum version of Corda platform that is required for nodes in the network.
* @property notaries List of well known and trusted notary identities with information on validation type.
* @property maxMessageSize Maximum P2P message sent over the wire in bytes.
* @property maxTransactionSize Maximum permitted transaction size in bytes.
* @property modifiedTime Last modification time of network parameters set.
* @property epoch Version number of the network parameters. Starting from 1, this will always increment on each new set
* of parameters.
*/
// TODO Add eventHorizon - how many days a node can be offline before being automatically ejected from the network.
// It needs separate design.
// TODO Currently maxTransactionSize is not wired.
@CordaSerializable
data class NetworkParameters(
val minimumPlatformVersion: Int,
val notaries: List<NotaryInfo>,
val maxMessageSize: Int,
val maxTransactionSize: Int,
val modifiedTime: Instant,
val epoch: Int
) {
init {
require(minimumPlatformVersion > 0) { "minimumPlatformVersion must be at least 1" }
require(notaries.distinctBy { it.identity } == notaries) { "Duplicate notary identities" }
require(epoch > 0) { "epoch must be at least 1" }
require(maxMessageSize > 0) { "maxMessageSize must be at least 1" }
require(maxTransactionSize > 0) { "maxTransactionSize must be at least 1" }
}
}
/**
* Data class storing information about notaries available in the network.
* @property identity Identity of the notary (note that it can be an identity of the distributed node).
* @property validating Indicates if the notary is validating.
*/
@CordaSerializable
data class NotaryInfo(val identity: Party, val validating: Boolean)

View File

@ -2,7 +2,6 @@ package net.corda.core.node.services
import net.corda.core.DoNotImplement
import net.corda.core.concurrent.CordaFuture
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party

View File

@ -22,6 +22,8 @@ The set of REST end-points for the network map service are as follows.
+================+=========================================+==============================================================================================================================================+
| POST | /network-map/publish | For the node to upload its signed ``NodeInfo`` object to the network map. |
+----------------+-----------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------+
| POST | /network-map/ack-parameters | For the node operator to acknowledge network map that new parameters were accepted for future update. |
+----------------+-----------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------+
| GET | /network-map | Retrieve the current signed network map object. The entire object is signed with the network map certificate which is also attached. |
+----------------+-----------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------+
| GET | /network-map/node-info/{hash} | Retrieve a signed ``NodeInfo`` as specified in the network map object. |
@ -77,3 +79,35 @@ More parameters will be added in future releases to regulate things like allowed
offline before it is evicted from the zone, whether or not IPv6 connectivity is required for zone members, required
cryptographic algorithms and rollout schedules (e.g. for moving to post quantum cryptography), parameters related to
SGX and so on.
Network parameters update process
---------------------------------
In case of the need to change network parameters Corda zone operator will start the update process. There are many reasons
that may lead to this decision: we discovered that some new fields have to be added to enable smooth network interoperability or change
of the existing compatibility constants is required due to upgrade or security reasons.
To synchronize all nodes in the compatibility zone to use the new set of the network parameters two RPC methods exist. The process
requires human interaction and approval of the change.
When the update is about to happen the network map service starts to advertise the additional information with the usual network map
data. It includes new network parameters hash, description of the change and the update deadline. Node queries network map server
for the new set of parameters and emits ``ParametersUpdateInfo`` via ``CordaRPCOps::networkParametersFeed`` method to inform
node operator about the event.
.. container:: codeset
.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt
:language: kotlin
:start-after: DOCSTART 1
:end-before: DOCEND 1
Node administrator can review the change and decide if is going to accept it. The approval should be done before ``updateDeadline``.
Nodes that don't approve before the deadline will be removed from the network map.
If the network operator starts advertising a different set of new parameters then that new set overrides the previous set. Only the latest update can be accepted.
To send back parameters approval to the zone operator RPC method ``fun acceptNewNetworkParameters(parametersHash: SecureHash)``
has to be called with ``parametersHash`` from update. Notice that the process cannot be undone.
Next time the node polls network map after the deadline the advertised network parameters will be the updated ones. Previous set
of parameters will no longer be valid. At this point the node will automatically shutdown and will require the node operator
to bring it back again.

View File

@ -7,6 +7,8 @@ import net.corda.core.node.NodeInfo
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import java.security.PublicKey
import java.security.SignatureException
/**
@ -44,3 +46,11 @@ class SignedNodeInfo(val raw: SerializedBytes<NodeInfo>, val signatures: List<Di
return nodeInfo
}
}
inline fun NodeInfo.sign(signer: (PublicKey, SerializedBytes<NodeInfo>) -> DigitalSignature): SignedNodeInfo {
// For now we exclude any composite identities, see [SignedNodeInfo]
val owningKeys = legalIdentities.map { it.owningKey }.filter { it !is CompositeKey }
val serialised = serialize()
val signatures = owningKeys.map { signer(it, serialised) }
return SignedNodeInfo(serialised, signatures)
}

View File

@ -4,10 +4,7 @@ import net.corda.core.CordaOID
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SignatureScheme
import net.corda.core.crypto.random63BitValue
import net.corda.core.internal.CertRole
import net.corda.core.internal.reader
import net.corda.core.internal.uncheckedCast
import net.corda.core.internal.writer
import net.corda.core.internal.*
import net.corda.core.utilities.days
import net.corda.core.utilities.millis
import org.bouncycastle.asn1.*

View File

@ -5,7 +5,9 @@ import net.corda.cordform.CordformNode
import net.corda.core.identity.Party
import net.corda.core.internal.*
import net.corda.core.internal.concurrent.fork
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NodeInfo
import net.corda.core.node.NotaryInfo
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
@ -167,7 +169,7 @@ class NetworkBootstrapper {
epoch = 1
), overwriteFile = true)
nodeDirs.forEach(copier::install)
nodeDirs.forEach { copier.install(it) }
}
private fun NotaryInfo.prettyPrint(): String = "${identity.name} (${if (validating) "" else "non-"}validating)"

View File

@ -1,58 +1,48 @@
package net.corda.nodeapi.internal.network
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party
import net.corda.core.internal.CertRole
import net.corda.core.internal.SignedDataWithCert
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.CordaSerializable
import net.corda.nodeapi.internal.crypto.X509Utilities
import java.security.cert.X509Certificate
import java.time.Instant
const val NETWORK_PARAMS_FILE_NAME = "network-parameters"
const val NETWORK_PARAMS_UPDATE_FILE_NAME = "network-parameters-update"
/**
* Data class containing hash of [NetworkParameters] and network participant's [NodeInfo] hashes.
* Data structure representing the network map available from the HTTP network map service as a serialised blob.
* @property nodeInfoHashes list of network participant's [NodeInfo] hashes
* @property networkParameterHash hash of the current active [NetworkParameters]
* @property parametersUpdate if present means that network operator has scheduled an update of the network parameters
*/
@CordaSerializable
data class NetworkMap(val nodeInfoHashes: List<SecureHash>, val networkParameterHash: SecureHash)
data class NetworkMap(
val nodeInfoHashes: List<SecureHash>,
val networkParameterHash: SecureHash,
val parametersUpdate: ParametersUpdate?
)
/**
* @property minimumPlatformVersion Minimum version of Corda platform that is required for nodes in the network.
* @property notaries List of well known and trusted notary identities with information on validation type.
* @property maxMessageSize Maximum P2P message sent over the wire in bytes.
* @property maxTransactionSize Maximum permitted transaction size in bytes.
* @property modifiedTime
* @property epoch Version number of the network parameters. Starting from 1, this will always increment on each new set
* of parameters.
* Data class representing scheduled network parameters update.
* @property newParametersHash Hash of the new [NetworkParameters] which can be requested from the network map
* @property description Short description of the update
* @property updateDeadline deadline by which new network parameters need to be accepted, after this date network operator
* can switch to new parameters which will result in getting nodes with old parameters out of the network
*/
// TODO Add eventHorizon - how many days a node can be offline before being automatically ejected from the network.
// It needs separate design.
// TODO Currently maxTransactionSize is not wired.
@CordaSerializable
data class NetworkParameters(
val minimumPlatformVersion: Int,
val notaries: List<NotaryInfo>,
val maxMessageSize: Int,
val maxTransactionSize: Int,
val modifiedTime: Instant,
val epoch: Int
) {
init {
require(minimumPlatformVersion > 0) { "minimumPlatformVersion must be at least 1" }
require(notaries.distinctBy { it.identity } == notaries) { "Duplicate notary identities" }
require(epoch > 0) { "epoch must be at least 1" }
require(maxMessageSize > 0) { "maxMessageSize must be at least 1" }
require(maxTransactionSize > 0) { "maxTransactionSize must be at least 1" }
}
}
@CordaSerializable
data class NotaryInfo(val identity: Party, val validating: Boolean)
data class ParametersUpdate(
val newParametersHash: SecureHash,
val description: String,
val updateDeadline: Instant
)
fun <T : Any> SignedDataWithCert<T>.verifiedNetworkMapCert(rootCert: X509Certificate): T {
require(CertRole.extract(sig.by) == CertRole.NETWORK_MAP) { "Incorrect cert role: ${CertRole.extract(sig.by)}" }
X509Utilities.validateCertificateChain(rootCert, sig.by, rootCert)
return verified()
}
}

View File

@ -1,8 +1,7 @@
package net.corda.nodeapi.internal.network
import net.corda.core.internal.copyTo
import net.corda.core.internal.div
import net.corda.core.internal.signWithCert
import net.corda.core.internal.*
import net.corda.core.node.NetworkParameters
import net.corda.core.serialization.serialize
import net.corda.nodeapi.internal.createDevNetworkMapCa
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
@ -13,7 +12,9 @@ import java.nio.file.StandardCopyOption
class NetworkParametersCopier(
networkParameters: NetworkParameters,
networkMapCa: CertificateAndKeyPair = createDevNetworkMapCa(),
overwriteFile: Boolean = false
overwriteFile: Boolean = false,
@VisibleForTesting
val update: Boolean = false
) {
private val copyOptions = if (overwriteFile) arrayOf(StandardCopyOption.REPLACE_EXISTING) else emptyArray()
private val serialisedSignedNetParams = networkParameters.signWithCert(
@ -22,8 +23,9 @@ class NetworkParametersCopier(
).serialize()
fun install(nodeDir: Path) {
val fileName = if (update) NETWORK_PARAMS_UPDATE_FILE_NAME else NETWORK_PARAMS_FILE_NAME
try {
serialisedSignedNetParams.open().copyTo(nodeDir / NETWORK_PARAMS_FILE_NAME, *copyOptions)
serialisedSignedNetParams.open().copyTo(nodeDir / fileName, *copyOptions)
} catch (e: FileAlreadyExistsException) {
// This is only thrown if the file already exists and we didn't specify to overwrite it. In that case we
// ignore this exception as we're happy with the existing file.

View File

@ -3,10 +3,10 @@ package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.crypto.Crypto.generateKeyPair
import net.corda.core.crypto.SignedData
import net.corda.core.crypto.sign
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NotaryInfo
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
import net.corda.core.serialization.SerializedBytes
import net.corda.nodeapi.internal.network.NetworkParameters
import net.corda.nodeapi.internal.network.NotaryInfo
import net.corda.testing.common.internal.ProjectStructure.projectRootDir
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.TestIdentity
@ -485,6 +485,7 @@ class EvolvabilityTests {
// the resulting file and add to the repo, changing the filename as appropriate
//
@Test
@Ignore("Test fails after moving NetworkParameters and NotaryInfo into core from node-api")
fun readBrokenNetworkParameters(){
val sf = testDefaultFactory()
sf.register(net.corda.nodeapi.internal.serialization.amqp.custom.InstantSerializer(sf))

View File

@ -25,7 +25,7 @@ import net.corda.node.services.transactions.minClusterSize
import net.corda.node.services.transactions.minCorrectReplicas
import net.corda.nodeapi.internal.DevIdentityGenerator
import net.corda.nodeapi.internal.network.NetworkParametersCopier
import net.corda.nodeapi.internal.network.NotaryInfo
import net.corda.core.node.NotaryInfo
import net.corda.testing.core.chooseIdentity
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.contracts.DummyContract

View File

@ -8,8 +8,8 @@ import net.corda.core.node.NodeInfo
import net.corda.core.serialization.deserialize
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.seconds
import net.corda.core.node.NetworkParameters
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
import net.corda.nodeapi.internal.network.NetworkParameters
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.SerializationEnvironmentRule
@ -45,7 +45,7 @@ class NetworkMapTest {
networkMapServer = NetworkMapServer(cacheTimeout, portAllocation.nextHostAndPort())
val address = networkMapServer.start()
compatibilityZone = CompatibilityZoneParams(URL("http://$address"), publishNotaries = {
networkMapServer.networkParameters = testNetworkParameters(it, modifiedTime = Instant.ofEpochMilli(random63BitValue()))
networkMapServer.networkParameters = testNetworkParameters(it, modifiedTime = Instant.ofEpochMilli(random63BitValue()), epoch = 2)
})
}

View File

@ -8,8 +8,6 @@ import net.corda.confidential.SwapIdentitiesHandler
import net.corda.core.CordaException
import net.corda.core.concurrent.CordaFuture
import net.corda.core.context.InvocationContext
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.sign
import net.corda.core.flows.*
import net.corda.core.identity.CordaX500Name
@ -54,14 +52,11 @@ import net.corda.node.services.vault.VaultSoftLockManager
import net.corda.node.shell.InteractiveShell
import net.corda.node.utilities.AffinityExecutor
import net.corda.nodeapi.internal.DevIdentityGenerator
import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
import net.corda.nodeapi.internal.network.NetworkParameters
import net.corda.nodeapi.internal.network.verifiedNetworkMapCert
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.HibernateConfiguration
import net.corda.nodeapi.internal.sign
import net.corda.nodeapi.internal.storeLegalIdentity
import org.apache.activemq.artemis.utils.ReusableLatch
import org.hibernate.type.descriptor.java.JavaTypeDescriptorRegistry
@ -137,7 +132,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
protected val runOnStop = ArrayList<() -> Any?>()
private val _nodeReadyFuture = openFuture<Unit>()
protected var networkMapClient: NetworkMapClient? = null
protected lateinit var networkMapUpdater: NetworkMapUpdater
lateinit var securityManager: RPCSecurityManager
/** Completes once the node has successfully registered with the network map service
@ -165,14 +160,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
validateKeystore()
}
private inline fun signNodeInfo(nodeInfo: NodeInfo, sign: (PublicKey, SerializedBytes<NodeInfo>) -> DigitalSignature): SignedNodeInfo {
// For now we exclude any composite identities, see [SignedNodeInfo]
val owningKeys = nodeInfo.legalIdentities.map { it.owningKey }.filter { it !is CompositeKey }
val serialised = nodeInfo.serialize()
val signatures = owningKeys.map { sign(it, serialised) }
return SignedNodeInfo(serialised, signatures)
}
open fun generateAndSaveNodeInfo(): NodeInfo {
check(started == null) { "Node has already been started" }
log.info("Generating nodeInfo ...")
@ -185,7 +172,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
val persistentNetworkMapCache = PersistentNetworkMapCache(database, notaries = emptyList())
persistentNetworkMapCache.start()
val (keyPairs, info) = initNodeInfo(persistentNetworkMapCache, identity, identityKeyPair)
val signedNodeInfo = signNodeInfo(info) { publicKey, serialised ->
val signedNodeInfo = info.sign { publicKey, serialised ->
val privateKey = keyPairs.single { it.public == publicKey }.private
privateKey.sign(serialised.bytes)
}
@ -202,7 +189,10 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null)
val identityService = makeIdentityService(identity.certificate)
networkMapClient = configuration.compatibilityZoneURL?.let { NetworkMapClient(it, identityService.trustRoot) }
retrieveNetworkParameters(identityService.trustRoot)
networkParameters = NetworkParametersReader(identityService.trustRoot, networkMapClient, configuration.baseDirectory).networkParameters
check(networkParameters.minimumPlatformVersion <= versionInfo.platformVersion) {
"Node's platform version is lower than network's required minimumPlatformVersion"
}
// Do all of this in a database transaction so anything that might need a connection has one.
val (startedImpl, schedulerService) = initialiseDatabasePersistence(schemaService, identityService) { database ->
val networkMapCache = NetworkMapCacheImpl(PersistentNetworkMapCache(database, networkParameters.notaries).start(), identityService)
@ -241,14 +231,15 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
startShell(rpcOps)
Pair(StartedNodeImpl(this, _services, info, checkpointStorage, smm, attachments, network, database, rpcOps, flowStarter, notaryService), schedulerService)
}
val networkMapUpdater = NetworkMapUpdater(services.networkMapCache,
networkMapUpdater = NetworkMapUpdater(services.networkMapCache,
NodeInfoWatcher(configuration.baseDirectory, getRxIoScheduler(), Duration.ofMillis(configuration.additionalNodeInfoPollingFrequencyMsec)),
networkMapClient,
networkParameters.serialize().hash)
networkParameters.serialize().hash,
configuration.baseDirectory)
runOnStop += networkMapUpdater::close
networkMapUpdater.updateNodeInfo(services.myInfo) {
signNodeInfo(it) { publicKey, serialised ->
it.sign { publicKey, serialised ->
services.keyManagementService.sign(serialised.bytes, publicKey).withoutKey()
}
}
@ -633,29 +624,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
return PersistentKeyManagementService(identityService, keyPairs)
}
private fun retrieveNetworkParameters(trustRoot: X509Certificate) {
val networkParamsFile = configuration.baseDirectory / NETWORK_PARAMS_FILE_NAME
networkParameters = if (networkParamsFile.exists()) {
networkParamsFile.readAll().deserialize<SignedDataWithCert<NetworkParameters>>().verifiedNetworkMapCert(trustRoot)
} else {
log.info("No network-parameters file found. Expecting network parameters to be available from the network map.")
val networkMapClient = checkNotNull(networkMapClient) {
"Node hasn't been configured to connect to a network map from which to get the network parameters"
}
val (networkMap, _) = networkMapClient.getNetworkMap()
val signedParams = networkMapClient.getNetworkParameters(networkMap.networkParameterHash)
val verifiedParams = signedParams.verifiedNetworkMapCert(trustRoot)
signedParams.serialize().open().copyTo(configuration.baseDirectory / NETWORK_PARAMS_FILE_NAME)
verifiedParams
}
log.info("Loaded network parameters: $networkParameters")
check(networkParameters.minimumPlatformVersion <= versionInfo.platformVersion) {
"Node's platform version is lower than network's required minimumPlatformVersion"
}
}
private fun makeCoreNotaryService(notaryConfig: NotaryConfig, database: CordaPersistence): NotaryService {
val notaryKey = myNotaryIdentity?.owningKey ?: throw IllegalArgumentException("No notary identity initialized when creating a notary service")
return notaryConfig.run {
@ -785,6 +753,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
override val networkService: MessagingService get() = network
override val clock: Clock get() = platformClock
override val configuration: NodeConfiguration get() = this@AbstractNode.configuration
override val networkMapUpdater: NetworkMapUpdater get() = this@AbstractNode.networkMapUpdater
override fun <T : SerializeAsToken> cordaService(type: Class<T>): T {
require(type.isAnnotationPresent(CordaService::class.java)) { "${type.name} is not a Corda service" }
return cordappServices.getInstance(type) ?: throw IllegalArgumentException("Corda service ${type.name} does not exist")

View File

@ -13,12 +13,14 @@ import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.sign
import net.corda.core.messaging.*
import net.corda.core.node.NodeInfo
import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.NetworkMapCache
import net.corda.core.node.services.Vault
import net.corda.core.node.services.vault.*
import net.corda.core.serialization.serialize
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.getOrThrow
@ -48,6 +50,18 @@ internal class CordaRPCOpsImpl(
return snapshot
}
override fun networkParametersFeed(): DataFeed<ParametersUpdateInfo?, ParametersUpdateInfo> {
return services.networkMapUpdater.track()
}
override fun acceptNewNetworkParameters(parametersHash: SecureHash) {
services.networkMapUpdater.acceptNewNetworkParameters(
parametersHash,
// TODO When multiple identities design will be better specified this should be signature from node operator.
{ hash -> hash.serialize().sign { services.keyManagementService.sign(it.bytes, services.myInfo.legalIdentities[0].owningKey) } }
)
}
override fun networkMapFeed(): DataFeed<List<NodeInfo>, NetworkMapCache.MapChange> {
return database.transaction {
services.networkMapCache.track()

View File

@ -0,0 +1,81 @@
package net.corda.node.internal
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.*
import net.corda.core.node.NetworkParameters
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.utilities.contextLogger
import net.corda.node.services.network.NetworkMapClient
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.verifiedNetworkMapCert
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.security.cert.X509Certificate
class NetworkParametersReader(private val trustRoot: X509Certificate,
private val networkMapClient: NetworkMapClient?,
private val baseDirectory: Path) {
companion object {
private val logger = contextLogger()
}
private val networkParamsFile = baseDirectory / NETWORK_PARAMS_FILE_NAME
private val parametersUpdateFile = baseDirectory / NETWORK_PARAMS_UPDATE_FILE_NAME
val networkParameters by lazy { retrieveNetworkParameters() }
private fun retrieveNetworkParameters(): NetworkParameters {
val advertisedParametersHash = networkMapClient?.getNetworkMap()?.networkMap?.networkParameterHash
val signedParametersFromFile = if (networkParamsFile.exists()) {
networkParamsFile.readAll().deserialize<SignedDataWithCert<NetworkParameters>>()
} else {
null
}
val parameters = if (advertisedParametersHash != null) {
// TODO On one hand we have node starting without parameters and just accepting them by default,
// on the other we have parameters update process - it needs to be unified. Say you start the node, you don't have matching parameters,
// you get them from network map, but you have to run the approval step.
if (signedParametersFromFile == null) { // Node joins for the first time.
downloadParameters(trustRoot, advertisedParametersHash)
}
else if (signedParametersFromFile.raw.hash == advertisedParametersHash) { // Restarted with the same parameters.
signedParametersFromFile.verifiedNetworkMapCert(trustRoot)
} else { // Update case.
readParametersUpdate(advertisedParametersHash, signedParametersFromFile.raw.hash).verifiedNetworkMapCert(trustRoot)
}
} else { // No compatibility zone configured. Node should proceed with parameters from file.
signedParametersFromFile?.verifiedNetworkMapCert(trustRoot) ?: throw IllegalArgumentException("Couldn't find network parameters file and compatibility zone wasn't configured")
}
logger.info("Loaded network parameters: $parameters")
return parameters
}
private fun readParametersUpdate(advertisedParametersHash: SecureHash, previousParametersHash: SecureHash): SignedDataWithCert<NetworkParameters> {
if (!parametersUpdateFile.exists()) {
throw IllegalArgumentException("Node uses parameters with hash: $previousParametersHash " +
"but network map is advertising: ${advertisedParametersHash}.\n" +
"Please update node to use correct network parameters file.")
}
val signedUpdatedParameters = parametersUpdateFile.readAll().deserialize<SignedDataWithCert<NetworkParameters>>()
if (signedUpdatedParameters.raw.hash != advertisedParametersHash) {
throw IllegalArgumentException("Both network parameters and network parameters update files don't match" +
"parameters advertised by network map.\n" +
"Please update node to use correct network parameters file.")
}
parametersUpdateFile.moveTo(networkParamsFile, StandardCopyOption.REPLACE_EXISTING)
return signedUpdatedParameters
}
// Used only when node joins for the first time.
private fun downloadParameters(trustRoot: X509Certificate, parametersHash: SecureHash): NetworkParameters {
logger.info("No network-parameters file found. Expecting network parameters to be available from the network map.")
val networkMapClient = checkNotNull(networkMapClient) {
"Node hasn't been configured to connect to a network map from which to get the network parameters"
}
val signedParams = networkMapClient.getNetworkParameters(parametersHash)
val verifiedParams = signedParams.verifiedNetworkMapCert(trustRoot)
signedParams.serialize().open().copyTo(baseDirectory / NETWORK_PARAMS_FILE_NAME)
return verifiedParams
}
}

View File

@ -9,6 +9,7 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.DataFeed
import net.corda.core.messaging.ParametersUpdateInfo
import net.corda.core.node.NodeInfo
import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.NetworkMapCache
@ -20,6 +21,13 @@ import java.security.PublicKey
// TODO change to KFunction reference after Kotlin fixes https://youtrack.jetbrains.com/issue/KT-12140
class RpcAuthorisationProxy(private val implementation: CordaRPCOps, private val context: () -> RpcAuthContext) : CordaRPCOps {
override fun networkParametersFeed(): DataFeed<ParametersUpdateInfo?, ParametersUpdateInfo> = guard("networkParametersFeed") {
implementation.networkParametersFeed()
}
override fun acceptNewNetworkParameters(parametersHash: SecureHash) = guard("acceptNewNetworkParameters") {
implementation.acceptNewNetworkParameters(parametersHash)
}
override fun uploadAttachmentWithMetadata(jar: InputStream, uploader: String, filename: String): SecureHash = guard("uploadAttachmentWithMetadata") {
implementation.uploadAttachmentWithMetadata(jar, uploader, filename)

View File

@ -20,6 +20,7 @@ import net.corda.node.internal.InitiatedFlowFactory
import net.corda.node.internal.cordapp.CordappProviderInternal
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.messaging.MessagingService
import net.corda.node.services.network.NetworkMapUpdater
import net.corda.node.services.statemachine.FlowStateMachineImpl
import net.corda.nodeapi.internal.persistence.CordaPersistence
@ -64,6 +65,7 @@ interface ServiceHubInternal : ServiceHub {
val networkService: MessagingService
val database: CordaPersistence
val configuration: NodeConfiguration
val networkMapUpdater: NetworkMapUpdater
override val cordappProvider: CordappProviderInternal
override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable<SignedTransaction>) {
require(txs.any()) { "No transactions passed in for recording" }

View File

@ -2,11 +2,13 @@ package net.corda.node.services.network
import com.google.common.util.concurrent.MoreExecutors
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.SignedDataWithCert
import net.corda.core.internal.checkOkResponse
import net.corda.core.internal.openHttpConnection
import net.corda.core.internal.responseAs
import net.corda.core.crypto.SignedData
import net.corda.core.internal.*
import net.corda.core.messaging.DataFeed
import net.corda.core.messaging.ParametersUpdateInfo
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.minutes
@ -16,40 +18,42 @@ import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.node.utilities.NamedThreadFactory
import net.corda.node.utilities.registration.cacheControl
import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME
import net.corda.nodeapi.internal.network.NetworkMap
import net.corda.nodeapi.internal.network.NetworkParameters
import net.corda.nodeapi.internal.network.ParametersUpdate
import net.corda.nodeapi.internal.network.verifiedNetworkMapCert
import okhttp3.CacheControl
import okhttp3.Headers
import rx.Subscription
import rx.subjects.PublishSubject
import java.io.BufferedReader
import java.io.Closeable
import java.net.URL
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.security.cert.X509Certificate
import java.time.Duration
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
class NetworkMapClient(compatibilityZoneURL: URL, private val trustedRoot: X509Certificate) {
class NetworkMapClient(compatibilityZoneURL: URL, val trustedRoot: X509Certificate) {
companion object {
private val logger = contextLogger()
}
private val networkMapUrl = URL("$compatibilityZoneURL/network-map")
fun publish(signedNodeInfo: SignedNodeInfo) {
val publishURL = URL("$networkMapUrl/publish")
logger.trace { "Publishing NodeInfo to $publishURL." }
publishURL.openHttpConnection().apply {
doOutput = true
requestMethod = "POST"
setRequestProperty("Content-Type", "application/octet-stream")
outputStream.use { signedNodeInfo.serialize().open().copyTo(it) }
checkOkResponse()
}
publishURL.post(signedNodeInfo.serialize())
logger.trace { "Published NodeInfo to $publishURL successfully." }
}
fun ackNetworkParametersUpdate(signedParametersHash: SignedData<SecureHash>) {
val ackURL = URL("$networkMapUrl/ack-parameters")
logger.trace { "Sending network parameters with hash ${signedParametersHash.raw.deserialize()} approval to $ackURL." }
ackURL.post(signedParametersHash.serialize())
logger.trace { "Sent network parameters approval to $ackURL successfully." }
}
fun getNetworkMap(): NetworkMapResponse {
logger.trace { "Fetching network map update from $networkMapUrl." }
val connection = networkMapUrl.openHttpConnection()
@ -90,12 +94,26 @@ data class NetworkMapResponse(val networkMap: NetworkMap, val cacheMaxAge: Durat
class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
private val fileWatcher: NodeInfoWatcher,
private val networkMapClient: NetworkMapClient?,
private val currentParametersHash: SecureHash) : Closeable {
private val currentParametersHash: SecureHash,
private val baseDirectory: Path) : Closeable {
companion object {
private val logger = contextLogger()
private val retryInterval = 1.minutes
}
private var newNetworkParameters: Pair<ParametersUpdate, SignedDataWithCert<NetworkParameters>>? = null
fun track(): DataFeed<ParametersUpdateInfo?, ParametersUpdateInfo> {
val currentUpdateInfo = newNetworkParameters?.let {
ParametersUpdateInfo(it.first.newParametersHash, it.second.verified(), it.first.description, it.first.updateDeadline)
}
return DataFeed(
currentUpdateInfo,
parametersUpdatesTrack
)
}
private val parametersUpdatesTrack: PublishSubject<ParametersUpdateInfo> = PublishSubject.create<ParametersUpdateInfo>()
private val executor = Executors.newSingleThreadScheduledExecutor(NamedThreadFactory("Network Map Updater Thread", Executors.defaultThreadFactory()))
private var fileWatcherSubscription: Subscription? = null
@ -130,8 +148,9 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
override fun run() {
val nextScheduleDelay = try {
val (networkMap, cacheTimeout) = networkMapClient.getNetworkMap()
// TODO NetworkParameters updates are not implemented yet. Every mismatch should result in node shutdown.
networkMap.parametersUpdate?.let { handleUpdateNetworkParameters(it) }
if (currentParametersHash != networkMap.networkParameterHash) {
// TODO This needs special handling (node omitted update process/didn't accept new parameters or didn't restart on updateDeadline)
logger.error("Node is using parameters with hash: $currentParametersHash but network map is advertising: ${networkMap.networkParameterHash}.\n" +
"Please update node to use correct network parameters file.\"")
System.exit(1)
@ -181,4 +200,32 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
}
executor.submit(task)
}
private fun handleUpdateNetworkParameters(update: ParametersUpdate) {
if (update.newParametersHash == newNetworkParameters?.first?.newParametersHash) { // This update was handled already.
return
}
val newParameters = networkMapClient?.getNetworkParameters(update.newParametersHash)
if (newParameters != null) {
logger.info("Downloaded new network parameters: $newParameters from the update: $update")
newNetworkParameters = Pair(update, newParameters)
parametersUpdatesTrack.onNext(ParametersUpdateInfo(update.newParametersHash, newParameters.verifiedNetworkMapCert(networkMapClient!!.trustedRoot), update.description, update.updateDeadline))
}
}
fun acceptNewNetworkParameters(parametersHash: SecureHash, sign: (SecureHash) -> SignedData<SecureHash>) {
networkMapClient ?: throw IllegalStateException("Network parameters updates are not support without compatibility zone configured")
// TODO This scenario will happen if node was restarted and didn't download parameters yet, but we accepted them. Add persisting of newest parameters from update.
val (_, newParams) = newNetworkParameters ?: throw IllegalArgumentException("Couldn't find parameters update for the hash: $parametersHash")
val newParametersHash = newParams.verifiedNetworkMapCert(networkMapClient.trustedRoot).serialize().hash // We should check that we sign the right data structure hash.
if (parametersHash == newParametersHash) {
// The latest parameters have priority.
newParams.serialize()
.open()
.copyTo(baseDirectory / NETWORK_PARAMS_UPDATE_FILE_NAME, StandardCopyOption.REPLACE_EXISTING)
networkMapClient.ackNetworkParametersUpdate(sign(parametersHash))
} else {
throw IllegalArgumentException("Refused to accept parameters with hash $parametersHash because network map advertises update with hash $newParametersHash. Please check newest version")
}
}
}

View File

@ -7,6 +7,7 @@ import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.node.NotaryInfo
import net.corda.core.internal.bufferUntilSubscribed
import net.corda.core.internal.concurrent.openFuture
import net.corda.core.internal.schemas.NodeInfoSchemaV1
@ -23,7 +24,6 @@ import net.corda.core.utilities.loggerFor
import net.corda.node.services.api.NetworkMapCacheBaseInternal
import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.node.utilities.NonInvalidatingCache
import net.corda.nodeapi.internal.network.NotaryInfo
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.bufferUntilDatabaseCommit
import net.corda.nodeapi.internal.persistence.wrapWithDatabaseTransaction

View File

@ -7,9 +7,9 @@ import net.corda.core.utilities.getOrThrow
import net.corda.finance.DOLLARS
import net.corda.finance.flows.CashIssueFlow
import net.corda.node.services.config.NotaryConfig
import net.corda.nodeapi.internal.network.NetworkParameters
import net.corda.core.node.NetworkParameters
import net.corda.nodeapi.internal.network.NetworkParametersCopier
import net.corda.nodeapi.internal.network.NotaryInfo
import net.corda.core.node.NotaryInfo
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME

View File

@ -1,12 +1,15 @@
package net.corda.node.services.network
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.sha256
import net.corda.core.internal.*
import net.corda.core.serialization.serialize
import net.corda.core.utilities.seconds
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.DEV_ROOT_CA
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.driver.PortAllocation
import net.corda.testing.internal.TestNodeInfoBuilder
import net.corda.testing.internal.createNodeInfoAndSigned
@ -20,6 +23,8 @@ import org.junit.Rule
import org.junit.Test
import java.io.IOException
import java.net.URL
import java.time.Instant
import java.time.temporal.ChronoUnit
import kotlin.test.assertEquals
class NetworkMapClientTest {
@ -90,4 +95,23 @@ class NetworkMapClientTest {
fun `get hostname string from http response correctly`() {
assertEquals("test.host.name", networkMapClient.myPublicHostname())
}
@Test
fun `handle parameters update`() {
val nextParameters = testNetworkParameters(emptyList(), epoch = 2)
val originalNetworkParameterHash = server.networkParameters.serialize().hash
val nextNetworkParameterHash = nextParameters.serialize().hash
val description = "Test parameters"
server.scheduleParametersUpdate(nextParameters, description, Instant.now().plus(1, ChronoUnit.DAYS))
val (networkMap) = networkMapClient.getNetworkMap()
assertEquals(networkMap.networkParameterHash, originalNetworkParameterHash)
assertEquals(networkMap.parametersUpdate?.description, description)
assertEquals(networkMap.parametersUpdate?.newParametersHash, nextNetworkParameterHash)
assertEquals(networkMapClient.getNetworkParameters(originalNetworkParameterHash).verified(), server.networkParameters)
assertEquals(networkMapClient.getNetworkParameters(nextNetworkParameterHash).verified(), nextParameters)
val keyPair = Crypto.generateKeyPair()
val signedHash = nextNetworkParameterHash.serialize().sign(keyPair)
networkMapClient.ackNetworkParametersUpdate(signedHash)
assertEquals(nextNetworkParameterHash, server.latestParametersAccepted(keyPair.public))
}
}

View File

@ -7,19 +7,27 @@ import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.times
import com.nhaarman.mockito_kotlin.verify
import net.corda.cordform.CordformNode.NODE_INFO_DIRECTORY
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.div
import net.corda.core.internal.uncheckedCast
import net.corda.core.internal.*
import net.corda.core.messaging.ParametersUpdateInfo
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.utilities.millis
import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.createDevNetworkMapCa
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME
import net.corda.nodeapi.internal.network.NetworkMap
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.nodeapi.internal.network.ParametersUpdate
import net.corda.nodeapi.internal.network.verifiedNetworkMapCert
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.*
import net.corda.testing.internal.TestNodeInfoBuilder
import net.corda.testing.internal.createNodeInfoAndSigned
import org.assertj.core.api.Assertions.assertThat
@ -27,8 +35,11 @@ import org.junit.After
import org.junit.Rule
import org.junit.Test
import rx.schedulers.TestScheduler
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
import kotlin.test.assertEquals
class NetworkMapUpdaterTest {
@Rule
@ -39,13 +50,16 @@ class NetworkMapUpdaterTest {
private val baseDir = fs.getPath("/node")
private val networkMapCache = createMockNetworkMapCache()
private val nodeInfoMap = ConcurrentHashMap<SecureHash, SignedNodeInfo>()
private val networkParamsMap = HashMap<SecureHash, NetworkParameters>()
private val networkMapCa: CertificateAndKeyPair = createDevNetworkMapCa()
private val cacheExpiryMs = 100
private val networkMapClient = createMockNetworkMapClient()
private val scheduler = TestScheduler()
private val networkParametersHash = SecureHash.randomSHA256()
private val fileWatcher = NodeInfoWatcher(baseDir, scheduler)
private val updater = NetworkMapUpdater(networkMapCache, fileWatcher, networkMapClient, networkParametersHash)
private val updater = NetworkMapUpdater(networkMapCache, fileWatcher, networkMapClient, networkParametersHash, baseDir)
private val nodeInfoBuilder = TestNodeInfoBuilder()
private var parametersUpdate: ParametersUpdate? = null
@After
fun cleanUp() {
@ -180,18 +194,73 @@ class NetworkMapUpdaterTest {
assertThat(networkMapCache.allNodeHashes).containsOnly(fileNodeInfo.serialize().hash)
}
@Test
fun `emit new parameters update info on parameters update from network map`() {
val paramsFeed = updater.track()
val snapshot = paramsFeed.snapshot
val updates = paramsFeed.updates.bufferUntilSubscribed()
assertEquals(null, snapshot)
val newParameters = testNetworkParameters(emptyList(), epoch = 2)
val updateDeadline = Instant.now().plus(1, ChronoUnit.DAYS)
scheduleParametersUpdate(newParameters, "Test update", updateDeadline)
updater.subscribeToNetworkMap()
updates.expectEvents(isStrict = false) {
sequence(
expect { update: ParametersUpdateInfo ->
assertThat(update.updateDeadline == updateDeadline)
assertThat(update.description == "Test update")
assertThat(update.hash == newParameters.serialize().hash)
assertThat(update.parameters == newParameters)
}
)
}
}
@Test
fun `ack network parameters update`() {
val newParameters = testNetworkParameters(emptyList(), epoch = 314)
scheduleParametersUpdate(newParameters, "Test update", Instant.MIN)
updater.subscribeToNetworkMap()
// TODO: Remove sleep in unit test.
Thread.sleep(2L * cacheExpiryMs)
val newHash = newParameters.serialize().hash
val keyPair = Crypto.generateKeyPair()
updater.acceptNewNetworkParameters(newHash, { hash -> hash.serialize().sign(keyPair)})
verify(networkMapClient).ackNetworkParametersUpdate(any())
val updateFile = baseDir / NETWORK_PARAMS_UPDATE_FILE_NAME
val signedNetworkParams = updateFile.readAll().deserialize<SignedDataWithCert<NetworkParameters>>()
val paramsFromFile = signedNetworkParams.verifiedNetworkMapCert(DEV_ROOT_CA.certificate)
assertEquals(newParameters, paramsFromFile)
}
private fun scheduleParametersUpdate(nextParameters: NetworkParameters, description: String, updateDeadline: Instant) {
val nextParamsHash = nextParameters.serialize().hash
networkParamsMap[nextParamsHash] = nextParameters
parametersUpdate = ParametersUpdate(nextParamsHash, description, updateDeadline)
}
private fun createMockNetworkMapClient(): NetworkMapClient {
return mock {
on { trustedRoot }.then {
DEV_ROOT_CA.certificate
}
on { publish(any()) }.then {
val signedNodeInfo: SignedNodeInfo = uncheckedCast(it.arguments[0])
nodeInfoMap.put(signedNodeInfo.verified().serialize().hash, signedNodeInfo)
}
on { getNetworkMap() }.then {
NetworkMapResponse(NetworkMap(nodeInfoMap.keys.toList(), networkParametersHash), cacheExpiryMs.millis)
NetworkMapResponse(NetworkMap(nodeInfoMap.keys.toList(), networkParametersHash, parametersUpdate), cacheExpiryMs.millis)
}
on { getNodeInfo(any()) }.then {
nodeInfoMap[it.arguments[0]]?.verified()
}
on { getNetworkParameters(any()) }.then {
val paramsHash: SecureHash = uncheckedCast(it.arguments[0])
networkParamsMap[paramsHash]?.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
}
on { ackNetworkParametersUpdate(any()) }.then {
Unit
}
}
}

View File

@ -0,0 +1,63 @@
package net.corda.node.services.network
import com.google.common.jimfs.Configuration
import com.google.common.jimfs.Jimfs
import net.corda.core.internal.*
import net.corda.core.node.NetworkParameters
import net.corda.core.serialization.deserialize
import net.corda.core.utilities.seconds
import net.corda.node.internal.NetworkParametersReader
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.NetworkParametersCopier
import net.corda.nodeapi.internal.network.verifiedNetworkMapCert
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.DEV_ROOT_CA
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.driver.PortAllocation
import net.corda.testing.node.internal.network.NetworkMapServer
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.net.URL
import kotlin.test.assertEquals
import kotlin.test.assertFalse
class NetworkParametersReaderTest {
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule(true)
private val cacheTimeout = 100000.seconds
private lateinit var server: NetworkMapServer
private lateinit var networkMapClient: NetworkMapClient
@Before
fun setUp() {
server = NetworkMapServer(cacheTimeout, PortAllocation.Incremental(10000).nextHostAndPort())
val hostAndPort = server.start()
networkMapClient = NetworkMapClient(URL("http://${hostAndPort.host}:${hostAndPort.port}"), DEV_ROOT_CA.certificate)
}
@After
fun tearDown() {
server.close()
}
@Test
fun `read correct set of parameters from file`() {
val fs = Jimfs.newFileSystem(Configuration.unix())
val baseDirectory = fs.getPath("/node").createDirectories()
val oldParameters = testNetworkParameters(emptyList(), epoch = 1)
NetworkParametersCopier(oldParameters).install(baseDirectory)
NetworkParametersCopier(server.networkParameters, update = true).install(baseDirectory) // Parameters update file.
val parameters = NetworkParametersReader(DEV_ROOT_CA.certificate, networkMapClient, baseDirectory).networkParameters
assertFalse((baseDirectory / NETWORK_PARAMS_UPDATE_FILE_NAME).exists())
assertEquals(server.networkParameters, parameters)
// Parameters from update should be moved to `network-parameters` file.
val parametersFromFile = (baseDirectory / NETWORK_PARAMS_FILE_NAME).readAll().deserialize<SignedDataWithCert<NetworkParameters>>().verifiedNetworkMapCert(DEV_ROOT_CA.certificate)
assertEquals(server.networkParameters, parametersFromFile)
}
}

View File

@ -42,7 +42,7 @@ import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor
import net.corda.nodeapi.internal.DevIdentityGenerator
import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.network.NetworkParametersCopier
import net.corda.nodeapi.internal.network.NotaryInfo
import net.corda.core.node.NotaryInfo
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.core.DUMMY_NOTARY_NAME

View File

@ -37,7 +37,7 @@ import net.corda.nodeapi.internal.crypto.X509KeyStore
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.nodeapi.internal.network.NetworkParametersCopier
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier
import net.corda.nodeapi.internal.network.NotaryInfo
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

View File

@ -1,6 +1,7 @@
package net.corda.testing.node.internal.network
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignedData
import net.corda.core.internal.signWithCert
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.deserialize
@ -9,8 +10,9 @@ import net.corda.core.utilities.NetworkHostAndPort
import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.createDevNetworkMapCa
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.core.node.NetworkParameters
import net.corda.nodeapi.internal.network.NetworkMap
import net.corda.nodeapi.internal.network.NetworkParameters
import net.corda.nodeapi.internal.network.ParametersUpdate
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.server.ServerConnector
import org.eclipse.jetty.server.handler.HandlerCollection
@ -21,6 +23,7 @@ import org.glassfish.jersey.servlet.ServletContainer
import java.io.Closeable
import java.io.InputStream
import java.net.InetSocketAddress
import java.security.PublicKey
import java.security.SignatureException
import java.time.Duration
import java.time.Instant
@ -46,6 +49,8 @@ class NetworkMapServer(private val cacheTimeout: Duration,
field = networkParameters
}
private val service = InMemoryNetworkMapService()
private var parametersUpdate: ParametersUpdate? = null
private var nextNetworkParameters: NetworkParameters? = null
init {
server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply {
@ -80,6 +85,21 @@ class NetworkMapServer(private val cacheTimeout: Duration,
service.removeNodeInfo(nodeInfo)
}
fun scheduleParametersUpdate(nextParameters: NetworkParameters, description: String, updateDeadline: Instant) {
nextNetworkParameters = nextParameters
parametersUpdate = ParametersUpdate(nextParameters.serialize().hash, description, updateDeadline)
}
fun advertiseNewParameters() {
networkParameters = checkNotNull(nextNetworkParameters) { "Schedule parameters update first" }
nextNetworkParameters = null
parametersUpdate = null
}
fun latestParametersAccepted(publicKey: PublicKey): SecureHash? {
return service.latestAcceptedParametersMap[publicKey]
}
override fun close() {
server.stop()
}
@ -87,6 +107,7 @@ class NetworkMapServer(private val cacheTimeout: Duration,
@Path("network-map")
inner class InMemoryNetworkMapService {
private val nodeInfoMap = mutableMapOf<SecureHash, SignedNodeInfo>()
val latestAcceptedParametersMap = mutableMapOf<PublicKey, SecureHash>()
private val signedNetParams by lazy {
networkParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
}
@ -108,10 +129,20 @@ class NetworkMapServer(private val cacheTimeout: Duration,
}.build()
}
@POST
@Path("ack-parameters")
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
fun ackNetworkParameters(input: InputStream): Response {
val signedParametersHash = input.readBytes().deserialize<SignedData<SecureHash>>()
val hash = signedParametersHash.verified()
latestAcceptedParametersMap[signedParametersHash.sig.by] = hash
return ok().build()
}
@GET
@Produces(MediaType.APPLICATION_OCTET_STREAM)
fun getNetworkMap(): Response {
val networkMap = NetworkMap(nodeInfoMap.keys.toList(), signedNetParams.raw.hash)
val networkMap = NetworkMap(nodeInfoMap.keys.toList(), signedNetParams.raw.hash, parametersUpdate)
val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
return Response.ok(signedNetworkMap.serialize().bytes).header("Cache-Control", "max-age=${cacheTimeout.seconds}").build()
}
@ -137,8 +168,14 @@ class NetworkMapServer(private val cacheTimeout: Duration,
@Path("network-parameters/{var}")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
fun getNetworkParameter(@PathParam("var") hash: String): Response {
require(signedNetParams.raw.hash == SecureHash.parse(hash))
return Response.ok(signedNetParams.serialize().bytes).build()
val requestedHash = SecureHash.parse(hash)
val requestedParameters = if (requestedHash == signedNetParams.raw.hash) {
signedNetParams
} else if (requestedHash == nextNetworkParameters?.serialize()?.hash) {
nextNetworkParameters?.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
} else null
requireNotNull(requestedParameters)
return Response.ok(requestedParameters!!.serialize().bytes).build()
}
@GET

View File

@ -1,7 +1,7 @@
package net.corda.testing.common.internal
import net.corda.nodeapi.internal.network.NetworkParameters
import net.corda.nodeapi.internal.network.NotaryInfo
import net.corda.core.node.NetworkParameters
import net.corda.core.node.NotaryInfo
import java.time.Instant
fun testNetworkParameters(

View File

@ -10,9 +10,9 @@ import net.corda.core.internal.noneOrSingle
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.demobench.plugin.CordappController
import net.corda.demobench.pty.R3Pty
import net.corda.nodeapi.internal.network.NetworkParameters
import net.corda.core.node.NetworkParameters
import net.corda.nodeapi.internal.network.NetworkParametersCopier
import net.corda.nodeapi.internal.network.NotaryInfo
import net.corda.core.node.NotaryInfo
import net.corda.nodeapi.internal.DevIdentityGenerator
import tornadofx.*
import java.io.IOException