diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt index feacf2c228..d03a1cc478 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt @@ -1,8 +1,10 @@ package net.corda.node.services.network import net.corda.core.crypto.random63BitValue +import net.corda.core.identity.Party 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.serialize import net.corda.core.utilities.getOrThrow @@ -11,6 +13,7 @@ import net.corda.nodeapi.internal.SignedNodeInfo 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.testing.common.internal.addNotary import net.corda.testing.common.internal.eventually import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.* @@ -74,7 +77,6 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP ) } - @Before fun start() { networkMapServer = NetworkMapServer(cacheTimeout, portAllocation.nextHostAndPort()) @@ -141,6 +143,102 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP } } + @Test(timeout = 300_000) + fun `Can hotload parameters if the notary changes`() { + internalDriver( + portAllocation = portAllocation, + compatibilityZone = compatibilityZone, + notarySpecs = emptyList() + ) { + + val notary: Party = TestIdentity.fresh("test notary").party + val oldParams = networkMapServer.networkParameters + val paramsWithNewNotary = oldParams.copy( + epoch = 3, + modifiedTime = Instant.ofEpochMilli(random63BitValue())).addNotary(notary) + + val alice = startNodeAndRunFlagDay(paramsWithNewNotary) + eventually { assertEquals(paramsWithNewNotary, alice.rpc.networkParameters) } + + } + } + + @Test(timeout = 300_000) + fun `If only the notary changes but parameters were not accepted, the node will still shut down on the flag day`() { + internalDriver( + portAllocation = portAllocation, + compatibilityZone = compatibilityZone, + notarySpecs = emptyList() + ) { + + val notary: Party = TestIdentity.fresh("test notary").party + val oldParams = networkMapServer.networkParameters + val paramsWithNewNotary = oldParams.copy( + epoch = 3, + modifiedTime = Instant.ofEpochMilli(random63BitValue())).addNotary(notary) + + val alice = startNode(providedName = ALICE_NAME, devMode = false).getOrThrow() as NodeHandleInternal + networkMapServer.scheduleParametersUpdate(paramsWithNewNotary, "Next parameters", Instant.ofEpochMilli(random63BitValue())) + // Wait for network map client to poll for the next update. + Thread.sleep(cacheTimeout.toMillis() * 2) + networkMapServer.advertiseNewParameters() + eventually { assertThatThrownBy { alice.rpc.networkParameters }.hasMessageContaining("Connection failure detected") } + + } + } + + @Test(timeout = 300_000) + fun `Can not hotload parameters if non-hotloadable parameter changes and the node will shut down`() { + internalDriver( + portAllocation = portAllocation, + compatibilityZone = compatibilityZone, + notarySpecs = emptyList() + ) { + + val oldParams = networkMapServer.networkParameters + val paramsWithUpdatedMaxMessageSize = oldParams.copy( + epoch = 3, + modifiedTime = Instant.ofEpochMilli(random63BitValue()), + maxMessageSize = oldParams.maxMessageSize + 1) + val alice = startNodeAndRunFlagDay(paramsWithUpdatedMaxMessageSize) + eventually { assertThatThrownBy { alice.rpc.networkParameters }.hasMessageContaining("Connection failure detected") } + } + } + + @Test(timeout = 300_000) + fun `Can not hotload parameters if notary and a non-hotloadable parameter changes and the node will shut down`() { + internalDriver( + portAllocation = portAllocation, + compatibilityZone = compatibilityZone, + notarySpecs = emptyList() + ) { + + val oldParams = networkMapServer.networkParameters + val notary: Party = TestIdentity.fresh("test notary").party + val paramsWithUpdatedMaxMessageSizeAndNotary = oldParams.copy( + epoch = 3, + modifiedTime = Instant.ofEpochMilli(random63BitValue()), + maxMessageSize = oldParams.maxMessageSize + 1).addNotary(notary) + val alice = startNodeAndRunFlagDay(paramsWithUpdatedMaxMessageSizeAndNotary) + eventually { assertThatThrownBy { alice.rpc.networkParameters }.hasMessageContaining("Connection failure detected") } + } + } + + private fun DriverDSLImpl.startNodeAndRunFlagDay(newParams: NetworkParameters): NodeHandleInternal { + + val alice = startNode(providedName = ALICE_NAME, devMode = false).getOrThrow() as NodeHandleInternal + val nextHash = newParams.serialize().hash + + networkMapServer.scheduleParametersUpdate(newParams, "Next parameters", Instant.ofEpochMilli(random63BitValue())) + // Wait for network map client to poll for the next update. + Thread.sleep(cacheTimeout.toMillis() * 2) + alice.rpc.acceptNewNetworkParameters(nextHash) + assertEquals(nextHash, networkMapServer.latestParametersAccepted(alice.nodeInfo.legalIdentities.first().owningKey)) + assertEquals(networkMapServer.networkParameters, alice.rpc.networkParameters) + networkMapServer.advertiseNewParameters() + return alice + } + @Test(timeout=300_000) fun `nodes process additions and removals from the network map correctly (and also download the network parameters)`() { internalDriver( diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 2c1c9563fa..c1fb4750d5 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -109,6 +109,8 @@ import net.corda.node.services.messaging.DeduplicationHandler import net.corda.node.services.messaging.MessagingService import net.corda.node.services.network.NetworkMapClient import net.corda.node.services.network.NetworkMapUpdater +import net.corda.node.services.network.NetworkParameterUpdateListener +import net.corda.node.services.network.NetworkParametersHotloader import net.corda.node.services.network.NodeInfoWatcher import net.corda.node.services.network.PersistentNetworkMapCache import net.corda.node.services.persistence.AbstractPartyDescriptor @@ -461,6 +463,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } + @Suppress("ComplexMethod") open fun start(): S { check(started == null) { "Node has already been started" } @@ -486,7 +489,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, startShell() networkMapClient?.start(trustRoot) - val (netParams, signedNetParams) = NetworkParametersReader(trustRoot, networkMapClient, configuration.baseDirectory).read() + val networkParametersReader = NetworkParametersReader(trustRoot, networkMapClient, configuration.baseDirectory) + val (netParams, signedNetParams) = networkParametersReader.read() log.info("Loaded network parameters: $netParams") check(netParams.minimumPlatformVersion <= versionInfo.platformVersion) { "Node's platform version is lower than network's required minimumPlatformVersion" @@ -507,13 +511,27 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val (nodeInfo, signedNodeInfo) = nodeInfoAndSigned identityService.ourNames = nodeInfo.legalIdentities.map { it.name }.toSet() services.start(nodeInfo, netParams) + + val networkParametersHotloader = if (networkMapClient == null) { + null + } else { + NetworkParametersHotloader(networkMapClient, trustRoot, netParams, networkParametersReader, networkParametersStorage).also { + it.addNotaryUpdateListener(networkMapCache) + it.addNotaryUpdateListener(identityService) + it.addNetworkParametersChangedListeners(services) + it.addNetworkParametersChangedListeners(networkMapUpdater) + } + } + networkMapUpdater.start( trustRoot, signedNetParams.raw.hash, signedNodeInfo, netParams, keyManagementService, - configuration.networkParameterAcceptanceSettings!!) + configuration.networkParameterAcceptanceSettings!!, + networkParametersHotloader) + try { startMessagingService(rpcOps, nodeInfo, myNotaryIdentity, netParams) } catch (e: Exception) { @@ -1153,7 +1171,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } - inner class ServiceHubInternalImpl : SingletonSerializeAsToken(), ServiceHubInternal, ServicesForResolution by servicesForResolution { + inner class ServiceHubInternalImpl : SingletonSerializeAsToken(), ServiceHubInternal, ServicesForResolution by servicesForResolution, NetworkParameterUpdateListener { override val rpcFlows = ArrayList>>() override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage(database) override val identityService: IdentityService get() = this@AbstractNode.identityService @@ -1186,6 +1204,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, override val attachmentsClassLoaderCache: AttachmentsClassLoaderCache get() = this@AbstractNode.attachmentsClassLoaderCache + @Volatile private lateinit var _networkParameters: NetworkParameters override val networkParameters: NetworkParameters get() = _networkParameters @@ -1272,6 +1291,10 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val ledgerTransaction = servicesForResolution.specialise(ltx) return verifierFactoryService.apply(ledgerTransaction) } + + override fun onNewNetworkParameters(networkParameters: NetworkParameters) { + this._networkParameters = networkParameters + } } } diff --git a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt index ac91bdec68..d9d8906861 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt @@ -12,6 +12,7 @@ import net.corda.core.internal.CertRole import net.corda.core.internal.NamedCacheFactory import net.corda.core.internal.hash import net.corda.core.internal.toSet +import net.corda.core.node.NotaryInfo import net.corda.core.node.services.UnknownAnonymousPartyException import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.MAX_HASH_HEX_SIZE @@ -19,6 +20,7 @@ import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.node.services.api.IdentityServiceInternal import net.corda.node.services.keys.BasicHSMKeyManagementService +import net.corda.node.services.network.NotaryUpdateListener import net.corda.node.services.persistence.PublicKeyHashToExternalId import net.corda.node.services.persistence.WritablePublicKeyToOwningIdentityCache import net.corda.node.utilities.AppendOnlyPersistentMap @@ -53,7 +55,8 @@ import kotlin.streams.toList * cached for efficient lookup. */ @ThreadSafe -class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSerializeAsToken(), IdentityServiceInternal { +@Suppress("TooManyFunctions") +class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSerializeAsToken(), IdentityServiceInternal, NotaryUpdateListener { companion object { private val log = contextLogger() @@ -197,7 +200,8 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri override val trustAnchor: TrustAnchor get() = _trustAnchor /** Stores notary identities obtained from the network parameters, for which we don't need to perform a database lookup. */ - private val notaryIdentityCache = HashSet() + @Volatile + private var notaryIdentityCache = HashSet() // CordaPersistence is not a c'tor parameter to work around the cyclic dependency lateinit var database: CordaPersistence @@ -453,4 +457,8 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri keys } } + + override fun onNewNotaryList(notaries: List) { + notaryIdentityCache = HashSet(notaries.map { it.identity }) + } } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt index 712efce341..b36f34d632 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapUpdater.kt @@ -1,6 +1,7 @@ package net.corda.node.services.network import com.google.common.util.concurrent.MoreExecutors +import net.corda.cliutils.ExitCodes import net.corda.core.CordaRuntimeException import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SignedData @@ -62,7 +63,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, private val baseDirectory: Path, private val extraNetworkMapKeys: List, private val networkParametersStorage: NetworkParametersStorage -) : AutoCloseable { +) : AutoCloseable, NetworkParameterUpdateListener { companion object { private val logger = contextLogger() private val defaultRetryInterval = 1.minutes @@ -77,12 +78,15 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, private val fileWatcherSubscription = AtomicReference() private var autoAcceptNetworkParameters: Boolean = true private lateinit var trustRoot: X509Certificate + @Volatile private lateinit var currentParametersHash: SecureHash private lateinit var ourNodeInfo: SignedNodeInfo private lateinit var ourNodeInfoHash: SecureHash + private lateinit var networkParameters: NetworkParameters private lateinit var keyManagementService: KeyManagementService private lateinit var excludedAutoAcceptNetworkParameters: Set + private var networkParametersHotloader: NetworkParametersHotloader? = null override fun close() { fileWatcherSubscription.updateAndGet { subscription -> @@ -95,13 +99,15 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, } MoreExecutors.shutdownAndAwaitTermination(networkMapPoller, 50, TimeUnit.SECONDS) } - + @Suppress("LongParameterList") fun start(trustRoot: X509Certificate, currentParametersHash: SecureHash, ourNodeInfo: SignedNodeInfo, networkParameters: NetworkParameters, keyManagementService: KeyManagementService, - networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings) { + networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings, + networkParametersHotloader: NetworkParametersHotloader? + ) { fileWatcherSubscription.updateAndGet { subscription -> require(subscription == null) { "Should not call this method twice" } this.trustRoot = trustRoot @@ -112,6 +118,8 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, this.keyManagementService = keyManagementService this.autoAcceptNetworkParameters = networkParameterAcceptanceSettings.autoAcceptEnabled this.excludedAutoAcceptNetworkParameters = networkParameterAcceptanceSettings.excludedAutoAcceptableParameters + this.networkParametersHotloader = networkParametersHotloader + val autoAcceptNetworkParametersNames = autoAcceptablePropertyNames - excludedAutoAcceptNetworkParameters if (autoAcceptNetworkParameters && autoAcceptNetworkParametersNames.isNotEmpty()) { @@ -180,7 +188,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, val additionalHashes = getPrivateNetworkNodeHashes(version) val allHashesFromNetworkMap = (globalNetworkMap.nodeInfoHashes + additionalHashes).toSet() if (currentParametersHash != globalNetworkMap.networkParameterHash) { - exitOnParametersMismatch(globalNetworkMap) + hotloadOrExitOnParametersMismatch(globalNetworkMap) } // Calculate any nodes that are now gone and remove _only_ them from the cache // NOTE: We won't remove them until after the add/update cycle as only then will we definitely know which nodes are no longer @@ -276,22 +284,26 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, } } - private fun exitOnParametersMismatch(networkMap: NetworkMap) { + private fun hotloadOrExitOnParametersMismatch(networkMap: NetworkMap) { val updatesFile = baseDirectory / NETWORK_PARAMS_UPDATE_FILE_NAME - val acceptedHash = if (updatesFile.exists()) updatesFile.readObject().raw.hash else null - val exitCode = if (acceptedHash == networkMap.networkParameterHash) { - logger.info("Flag day occurred. Network map switched to the new network parameters: " + - "${networkMap.networkParameterHash}. Node will shutdown now and needs to be started again.") - 0 - } else { - // TODO This needs special handling (node omitted update process or didn't accept new parameters) + val newParameterHash = networkMap.networkParameterHash + val nodeAcceptedNewParameters = updatesFile.exists() && newParameterHash == updatesFile.readObject().raw.hash + + if (!nodeAcceptedNewParameters) { logger.error( """Node is using network parameters with hash $currentParametersHash but the network map is advertising ${networkMap.networkParameterHash}. To resolve this mismatch, and move to the current parameters, delete the $NETWORK_PARAMS_FILE_NAME file from the node's directory and restart. The node will shutdown now.""") - 1 + exitProcess(ExitCodes.FAILURE) } - exitProcess(exitCode) + + val hotloadSucceeded = networkParametersHotloader!!.attemptHotload(newParameterHash) + if (!hotloadSucceeded) { + logger.info("Flag day occurred. Network map switched to the new network parameters: " + + "${networkMap.networkParameterHash}. Node will shutdown now and needs to be started again.") + exitProcess(ExitCodes.SUCCESS) + } + currentParametersHash = newParameterHash } private fun handleUpdateNetworkParameters(networkMapClient: NetworkMapClient, update: ParametersUpdate) { @@ -340,6 +352,10 @@ The node will shutdown now.""") throw OutdatedNetworkParameterHashException(parametersHash, newParametersHash) } } + + override fun onNewNetworkParameters(networkParameters: NetworkParameters) { + this.networkParameters = networkParameters + } } private val memberPropertyPartition = NetworkParameters::class.declaredMemberProperties.partition { it.isAutoAcceptable() } @@ -360,8 +376,8 @@ internal fun NetworkParameters.canAutoAccept(newNetworkParameters: NetworkParame private fun KProperty1.isAutoAcceptable(): Boolean = findAnnotation() != null -private fun NetworkParameters.valueChanged(newNetworkParameters: NetworkParameters, getter: Method?): Boolean { +internal fun NetworkParameters.valueChanged(newNetworkParameters: NetworkParameters, getter: Method?): Boolean { val propertyValue = getter?.invoke(this) val newPropertyValue = getter?.invoke(newNetworkParameters) return propertyValue != newPropertyValue -} \ No newline at end of file +} diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkParameterUpdateListener.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkParameterUpdateListener.kt new file mode 100644 index 0000000000..bce669e919 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkParameterUpdateListener.kt @@ -0,0 +1,11 @@ +package net.corda.node.services.network + +import net.corda.core.node.NetworkParameters + +/** + * When network parameters change on a flag day, onNewNetworkParameters will be invoked with the new parameters. + * Used inside {@link net.corda.node.services.network.NetworkParametersUpdater} + */ +interface NetworkParameterUpdateListener { + fun onNewNetworkParameters(networkParameters: NetworkParameters) +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkParametersHotloader.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkParametersHotloader.kt new file mode 100644 index 0000000000..5268e4f641 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkParametersHotloader.kt @@ -0,0 +1,88 @@ +package net.corda.node.services.network + +import net.corda.core.crypto.SecureHash +import net.corda.core.internal.NetworkParametersStorage +import net.corda.core.node.NetworkParameters +import net.corda.core.node.NotaryInfo +import net.corda.core.utilities.contextLogger +import net.corda.node.internal.NetworkParametersReader +import net.corda.nodeapi.internal.network.verifiedNetworkParametersCert +import java.security.cert.X509Certificate +import kotlin.reflect.full.declaredMemberProperties +import kotlin.reflect.jvm.javaGetter + +/** + * This class is responsible for hotloading new network parameters or shut down the node if it's not possible. + * Currently only hotloading notary changes are supported. + */ +class NetworkParametersHotloader(private val networkMapClient: NetworkMapClient, + private val trustRoot: X509Certificate, + @Volatile private var networkParameters: NetworkParameters, + private val networkParametersReader: NetworkParametersReader, + private val networkParametersStorage: NetworkParametersStorage) { + companion object { + private val logger = contextLogger() + private val alwaysHotloadable = listOf(NetworkParameters::epoch, NetworkParameters::modifiedTime) + } + + private val networkParameterUpdateListeners = mutableListOf() + private val notaryUpdateListeners = mutableListOf() + + fun addNetworkParametersChangedListeners(listener: NetworkParameterUpdateListener) { + networkParameterUpdateListeners.add(listener) + } + + fun addNotaryUpdateListener(listener: NotaryUpdateListener) { + notaryUpdateListeners.add(listener) + } + + private fun notifyListenersFor(notaries: List) = notaryUpdateListeners.forEach { it.onNewNotaryList(notaries) } + private fun notifyListenersFor(networkParameters: NetworkParameters) = networkParameterUpdateListeners.forEach { it.onNewNetworkParameters(networkParameters) } + + fun attemptHotload(newNetworkParameterHash: SecureHash): Boolean { + + val newSignedNetParams = networkMapClient.getNetworkParameters(newNetworkParameterHash) + val newNetParams = newSignedNetParams.verifiedNetworkParametersCert(trustRoot) + + if (canHotload(newNetParams)) { + logger.info("All changed parameters are hotloadable") + hotloadParameters(newNetParams) + return true + } else { + return false + } + } + + /** + * Ignoring always hotloadable properties (epoch, modifiedTime) return true if the notary is the only property that is different in the new network parameters + */ + private fun canHotload(newNetworkParameters: NetworkParameters): Boolean { + + val propertiesChanged = NetworkParameters::class.declaredMemberProperties + .minus(alwaysHotloadable) + .filter { networkParameters.valueChanged(newNetworkParameters, it.javaGetter) } + + logger.info("Updated NetworkParameters properties: $propertiesChanged") + + val noPropertiesChanged = propertiesChanged.isEmpty() + val onlyNotariesChanged = propertiesChanged == listOf(NetworkParameters::notaries) + return when { + noPropertiesChanged -> true + onlyNotariesChanged -> true + else -> false + } + } + + /** + * Update local networkParameters and currentParametersHash with new values. + * Notify all listeners for network parameter changes + */ + private fun hotloadParameters(newNetworkParameters: NetworkParameters) { + + networkParameters = newNetworkParameters + val networkParametersAndSigned = networkParametersReader.read() + networkParametersStorage.setCurrentParameters(networkParametersAndSigned.signed, trustRoot) + notifyListenersFor(newNetworkParameters) + notifyListenersFor(newNetworkParameters.notaries) + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/network/NotaryUpdateListener.kt b/node/src/main/kotlin/net/corda/node/services/network/NotaryUpdateListener.kt new file mode 100644 index 0000000000..6cd4570b71 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/network/NotaryUpdateListener.kt @@ -0,0 +1,11 @@ +package net.corda.node.services.network + +import net.corda.core.node.NotaryInfo + +/** + * When notaries inside network parameters change on a flag day, onNewNotaryList will be invoked with the new notary list. + * Used inside {@link net.corda.node.services.network.NetworkParametersUpdater} + */ +interface NotaryUpdateListener { + fun onNewNotaryList(notaries: List) +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt index f09d9aa8f6..709e415cdd 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt @@ -38,9 +38,10 @@ import javax.persistence.PersistenceException /** Database-based network map cache. */ @ThreadSafe +@Suppress("TooManyFunctions") open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory, private val database: CordaPersistence, - private val identityService: IdentityService) : NetworkMapCacheInternal, SingletonSerializeAsToken() { + private val identityService: IdentityService) : NetworkMapCacheInternal, SingletonSerializeAsToken(), NotaryUpdateListener { companion object { private val logger = contextLogger() @@ -53,6 +54,7 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory, override val nodeReady: OpenFuture = openFuture() + @Volatile private lateinit var notaries: List override val notaryIdentities: List get() = notaries.map { it.identity } @@ -386,4 +388,8 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory, for (nodeInfo in result) session.remove(nodeInfo) } } + + override fun onNewNotaryList(notaries: List) { + this.notaries = notaries + } } diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt index a406bd9be6..8e36a97de9 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt @@ -78,7 +78,6 @@ class NetworkMapUpdaterTest { @Rule @JvmField val testSerialization = SerializationEnvironmentRule(true) - private val cacheExpiryMs = 1000 private val privateNetUUID = UUID.randomUUID() private val fs = Jimfs.newFileSystem(unix()) @@ -120,12 +119,13 @@ class NetworkMapUpdaterTest { networkParameters: NetworkParameters = server.networkParameters, autoAcceptNetworkParameters: Boolean = true, excludedAutoAcceptNetworkParameters: Set = emptySet()) { + updater!!.start(DEV_ROOT_CA.certificate, server.networkParameters.serialize().hash, ourNodeInfo, networkParameters, MockKeyManagementService(makeTestIdentityService(), ourKeyPair), - NetworkParameterAcceptanceSettings(autoAcceptNetworkParameters, excludedAutoAcceptNetworkParameters)) + NetworkParameterAcceptanceSettings(autoAcceptNetworkParameters, excludedAutoAcceptNetworkParameters), null) } @Test(timeout=300_000) diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersHotloaderTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersHotloaderTest.kt new file mode 100644 index 0000000000..1fcd40cf42 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersHotloaderTest.kt @@ -0,0 +1,125 @@ +package net.corda.node.services.network + +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.never +import com.nhaarman.mockito_kotlin.verify +import net.corda.core.identity.Party +import net.corda.core.internal.NetworkParametersStorage +import net.corda.core.node.NetworkParameters +import net.corda.core.node.NotaryInfo +import net.corda.core.serialization.serialize +import net.corda.coretesting.internal.DEV_ROOT_CA +import net.corda.node.internal.NetworkParametersReader +import net.corda.nodeapi.internal.createDevNetworkMapCa +import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair +import net.corda.testing.common.internal.addNotary +import net.corda.testing.common.internal.testNetworkParameters +import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.core.TestIdentity +import org.junit.Assert +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito + +class NetworkParametersHotloaderTest { + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule(true) + private val networkMapCertAndKeyPair: CertificateAndKeyPair = createDevNetworkMapCa() + private val trustRoot = DEV_ROOT_CA.certificate + + private val originalNetworkParameters = testNetworkParameters() + private val notary: Party = TestIdentity.fresh("test notary").party + private val networkParametersWithNotary = originalNetworkParameters.addNotary(notary) + private val networkParametersStorage = Mockito.mock(NetworkParametersStorage::class.java) + + @Test(timeout = 300_000) + fun `can hotload if notary changes`() { + `can hotload`(networkParametersWithNotary) + } + + @Test(timeout = 300_000) + fun `can not hotload if notary changes but another non-hotloadable property also changes`() { + + val newnetParamsWithNewNotaryAndMaxMsgSize = networkParametersWithNotary.copy(maxMessageSize = networkParametersWithNotary.maxMessageSize + 1) + `can not hotload`(newnetParamsWithNewNotaryAndMaxMsgSize) + } + + @Test(timeout = 300_000) + fun `can hotload if only always hotloadable properties change`() { + + val newParametersWithAlwaysHotloadableProperties = originalNetworkParameters.copy(epoch = originalNetworkParameters.epoch + 1, modifiedTime = originalNetworkParameters.modifiedTime.plusSeconds(60)) + `can hotload`(newParametersWithAlwaysHotloadableProperties) + } + + @Test(timeout = 300_000) + fun `can not hotload if maxMessageSize changes`() { + + val parametersWithNewMaxMessageSize = originalNetworkParameters.copy(maxMessageSize = originalNetworkParameters.maxMessageSize + 1) + `can not hotload`(parametersWithNewMaxMessageSize) + } + + @Test(timeout = 300_000) + fun `can not hotload if maxTransactionSize changes`() { + + val parametersWithNewMaxTransactionSize = originalNetworkParameters.copy(maxTransactionSize = originalNetworkParameters.maxMessageSize + 1) + `can not hotload`(parametersWithNewMaxTransactionSize) + } + + @Test(timeout = 300_000) + fun `can not hotload if minimumPlatformVersion changes`() { + + val parametersWithNewMinimumPlatformVersion = originalNetworkParameters.copy(minimumPlatformVersion = originalNetworkParameters.minimumPlatformVersion + 1) + `can not hotload`(parametersWithNewMinimumPlatformVersion) + } + + private fun `can hotload`(newNetworkParameters: NetworkParameters) { + val notaryUpdateListener = Mockito.spy(object : NotaryUpdateListener { + override fun onNewNotaryList(notaries: List) { + } + }) + + val networkParametersChangedListener = Mockito.spy(object : NetworkParameterUpdateListener { + override fun onNewNetworkParameters(networkParameters: NetworkParameters) { + } + }) + val networkParametersHotloader = createHotloaderWithMockedServices(newNetworkParameters).also { + it.addNotaryUpdateListener(notaryUpdateListener) + it.addNetworkParametersChangedListeners(networkParametersChangedListener) + } + + Assert.assertTrue(networkParametersHotloader.attemptHotload(newNetworkParameters.serialize().hash)) + verify(notaryUpdateListener).onNewNotaryList(newNetworkParameters.notaries) + verify(networkParametersChangedListener).onNewNetworkParameters(newNetworkParameters) + } + + private fun `can not hotload`(newNetworkParameters: NetworkParameters) { + val notaryUpdateListener = Mockito.spy(object : NotaryUpdateListener { + override fun onNewNotaryList(notaries: List) { + } + }) + + val networkParametersChangedListener = Mockito.spy(object : NetworkParameterUpdateListener { + override fun onNewNetworkParameters(networkParameters: NetworkParameters) { + } + }) + val networkParametersHotloader = createHotloaderWithMockedServices(newNetworkParameters).also { + it.addNotaryUpdateListener(notaryUpdateListener) + it.addNetworkParametersChangedListeners(networkParametersChangedListener) + } + Assert.assertFalse(networkParametersHotloader.attemptHotload(newNetworkParameters.serialize().hash)) + verify(notaryUpdateListener, never()).onNewNotaryList(any()); + verify(networkParametersChangedListener, never()).onNewNetworkParameters(any()); + } + + private fun createHotloaderWithMockedServices(newNetworkParameters: NetworkParameters): NetworkParametersHotloader { + val signedNetworkParameters = networkMapCertAndKeyPair.sign(newNetworkParameters) + val networkMapClient = Mockito.mock(NetworkMapClient::class.java) + Mockito.`when`(networkMapClient.getNetworkParameters(newNetworkParameters.serialize().hash)).thenReturn(signedNetworkParameters) + val networkParametersReader = Mockito.mock(NetworkParametersReader::class.java) + Mockito.`when`(networkParametersReader.read()) + .thenReturn(NetworkParametersReader.NetworkParametersAndSigned(signedNetworkParameters, trustRoot)) + return NetworkParametersHotloader(networkMapClient, trustRoot, originalNetworkParameters, networkParametersReader, networkParametersStorage) + } +} + diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/network/NetworkMapServer.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/network/NetworkMapServer.kt index 88620bb1d7..6e3f5797a2 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/network/NetworkMapServer.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/network/NetworkMapServer.kt @@ -117,7 +117,7 @@ class NetworkMapServer(private val pollInterval: Duration, // Mapping from the UUID of the network (null for global one) to hashes of the nodes in network private val networkMaps = mutableMapOf>() val latestAcceptedParametersMap = mutableMapOf() - private val signedNetParams by lazy { networkMapCertAndKeyPair.sign(networkParameters) } + private val signedNetParams get() = networkMapCertAndKeyPair.sign(networkParameters) @POST @Path("publish")