From 373d99435c5444c176dd0d082a04c5d4c8a4d5cc Mon Sep 17 00:00:00 2001 From: Oliver Knowles Date: Tue, 20 Nov 2018 09:50:42 +0000 Subject: [PATCH] CORDA-1965 Auto-accept network parameter changes (#4222) * add auto acceptance of certain network parameters * Remove incorrect nullification of newNetworkParameters object within NetworkMapUpdater * Automatically update network parameters if update accepted and flag day occured * Comment cleanup * Add node configuration for auto accepting network parameter changes * Remove hot swapping of network parameters * Add docs for auto accept config flag * Minor change to log line * Remove unrelated fix that was corrected on master * Minor name change within NetworkParameters class * Minor doc rewording * Fix typo in docs * Address PR comments * Add node config option to turn off network param auto-accept on a per param basis * Address PR comments * Fix failing Network Map update integration test --- .../net/corda/core/node/AutoAcceptable.kt | 10 +++ .../net/corda/core/node/NetworkParameters.kt | 54 ++++++++++-- docs/source/changelog.rst | 3 + docs/source/corda-configuration-file.rst | 3 + docs/source/network-map.rst | 53 +++++++++-- docs/source/release-notes.rst | 4 + .../node/services/network/NetworkMapTest.kt | 10 ++- .../net/corda/node/internal/AbstractNode.kt | 8 +- .../node/services/config/NodeConfiguration.kt | 15 ++++ .../services/config/NodeConfigurationImpl.kt | 4 +- .../services/network/NetworkMapUpdater.kt | 43 +++++++-- .../node/internal/NetworkParametersTest.kt | 36 ++++++++ .../services/network/NetworkMapUpdaterTest.kt | 88 +++++++++++++++++-- .../node/internal/InternalMockNetwork.kt | 2 + .../testing/internal/TestNodeInfoBuilder.kt | 5 +- 15 files changed, 301 insertions(+), 37 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/node/AutoAcceptable.kt diff --git a/core/src/main/kotlin/net/corda/core/node/AutoAcceptable.kt b/core/src/main/kotlin/net/corda/core/node/AutoAcceptable.kt new file mode 100644 index 0000000000..bf868cbf19 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/node/AutoAcceptable.kt @@ -0,0 +1,10 @@ +package net.corda.core.node + +/** + * Tagging a network parameter with this annotation signifies that any update which modifies the parameter could be accepted without + * the need for the node operator to run a manual accept command. If the update contains changes for any non-auto-acceptable parameter + * or the behaviour is switched off via configuration then it will not be auto-accepted + */ +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +annotation class AutoAcceptable diff --git a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt index 2c1c807633..9a71d84f90 100644 --- a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt +++ b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt @@ -8,10 +8,16 @@ import net.corda.core.node.services.AttachmentId import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.DeprecatedConstructorForDeserialization import net.corda.core.utilities.days +import java.lang.reflect.Method import java.security.PublicKey import java.time.Duration import java.time.Instant +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.declaredMemberProperties +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.jvm.javaGetter +// DOCSTART 1 /** * 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. @@ -19,12 +25,14 @@ import java.time.Instant * @property notaries List of well known and trusted notary identities with information on validation type. * @property maxMessageSize This is currently ignored. However, it will be wired up in a future release. * @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 + * @property modifiedTime ([AutoAcceptable]) Last modification time of network parameters set. + * @property epoch ([AutoAcceptable]) Version number of the network parameters. Starting from 1, this will always increment on each new set * of parameters. - * @property whitelistedContractImplementations List of whitelisted jars containing contract code for each contract class. - * This will be used by [net.corda.core.contracts.WhitelistedByZoneAttachmentConstraint]. [You can learn more about contract constraints here](https://docs.corda.net/api-contract-constraints.html). - * @property packageOwnership List of the network-wide java packages that were successfully claimed by their owners. Any CorDapp JAR that offers contracts and states in any of these packages must be signed by the owner. + * @property whitelistedContractImplementations ([AutoAcceptable]) List of whitelisted jars containing contract code for each contract class. + * This will be used by [net.corda.core.contracts.WhitelistedByZoneAttachmentConstraint]. + * [You can learn more about contract constraints here](https://docs.corda.net/api-contract-constraints.html). + * @property packageOwnership ([AutoAcceptable]) List of the network-wide java packages that were successfully claimed by their owners. + * Any CorDapp JAR that offers contracts and states in any of these packages must be signed by the owner. * @property eventHorizon Time after which nodes will be removed from the network map if they have not been seen * during this period */ @@ -35,12 +43,13 @@ data class NetworkParameters( val notaries: List, val maxMessageSize: Int, val maxTransactionSize: Int, - val modifiedTime: Instant, - val epoch: Int, - val whitelistedContractImplementations: Map>, + @AutoAcceptable val modifiedTime: Instant, + @AutoAcceptable val epoch: Int, + @AutoAcceptable val whitelistedContractImplementations: Map>, val eventHorizon: Duration, - val packageOwnership: Map + @AutoAcceptable val packageOwnership: Map ) { + // DOCEND 1 @DeprecatedConstructorForDeserialization(1) constructor (minimumPlatformVersion: Int, notaries: List, @@ -80,6 +89,14 @@ data class NetworkParameters( emptyMap() ) + companion object { + private val memberPropertyPartition = NetworkParameters::class.declaredMemberProperties.asSequence() + .partition { it.isAutoAcceptable() } + private val autoAcceptableNamesAndGetters = memberPropertyPartition.first.associateBy({it.name}, {it.javaGetter}) + private val nonAutoAcceptableGetters = memberPropertyPartition.second.map { it.javaGetter } + val autoAcceptablePropertyNames = autoAcceptableNamesAndGetters.keys + } + init { require(minimumPlatformVersion > 0) { "minimumPlatformVersion must be at least 1" } require(notaries.distinctBy { it.identity } == notaries) { "Duplicate notary identities" } @@ -149,6 +166,21 @@ data class NetworkParameters( * Returns the public key of the package owner of the [contractClassName], or null if not owned. */ fun getOwnerOf(contractClassName: String): PublicKey? = this.packageOwnership.filterKeys { it.owns(contractClassName) }.values.singleOrNull() + + /** + * Returns true if the only properties changed in [newNetworkParameters] are [AutoAcceptable] and not + * included in the [excludedParameterNames] + */ + fun canAutoAccept(newNetworkParameters: NetworkParameters, excludedParameterNames: Set): Boolean { + return nonAutoAcceptableGetters.none { valueChanged(newNetworkParameters, it) } && + autoAcceptableNamesAndGetters.none { excludedParameterNames.contains(it.key) && valueChanged(newNetworkParameters, it.value) } + } + + private fun valueChanged(newNetworkParameters: NetworkParameters, getter: Method?): Boolean { + val propertyValue = getter?.invoke(this) + val newPropertyValue = getter?.invoke(newNetworkParameters) + return propertyValue != newPropertyValue + } } /** @@ -196,3 +228,7 @@ private fun isPackageValid(packageName: String): Boolean = packageName.isNotEmpt private fun noOverlap(packages: Collection) = packages.all { currentPackage -> packages.none { otherPackage -> otherPackage != currentPackage && otherPackage.name.startsWith("${currentPackage.name}.") } } + +private fun KProperty1.isAutoAcceptable(): Boolean { + return this.findAnnotation() != null +} \ No newline at end of file diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 602d57ab8a..2b965c6969 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -7,6 +7,9 @@ release, see :doc:`upgrade-notes`. Unreleased ---------- +* Added auto-acceptance of network parameters for network updates. This behaviour is available for a subset of the network parameters + and is configurable via the node config. See :doc:`network-map` for more information. + * Deprecated `SerializationContext.withAttachmentsClassLoader`. This functionality has always been disabled by flags and there is no reason for a CorDapp developer to use it. It is just an internal implementation detail of Corda. diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index c5db669868..c3a13ee732 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -248,6 +248,9 @@ The available config fields are listed below. The option takes effect only in production mode and defaults to Corda development keys (``["56CA54E803CB87C8472EBD3FBC6A2F1876E814CEEBF74860BD46997F40729367", "83088052AF16700457AE2C978A7D8AC38DD6A7C713539D00B897CD03A5E5D31D"]``), in development mode any key is allowed to sign Cordpapp JARs. +:autoAcceptNetworkParameterChanges: This flag toggles auto accepting of network parameter changes and is enabled by default. If a network operator issues a network parameter change which modifies + only auto-acceptable options and this behaviour is enabled then the changes will be accepted without any manual intervention from the node operator. See + :doc:`network-map` for more information on the update process and current auto-acceptable parameters. Set to ``false`` to disable. Examples -------- diff --git a/docs/source/network-map.rst b/docs/source/network-map.rst index 918ba7301a..d51f777d29 100644 --- a/docs/source/network-map.rst +++ b/docs/source/network-map.rst @@ -159,14 +159,55 @@ The fact a new set of parameters is being advertised shows up in the node logs w the ``CordaRPCOps.networkParametersFeed`` method. Typically a zone operator would also email node operators to let them know about the details of the impending change, along with the justification, how to object, deadlines and so on. -.. container:: codeset +.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 - .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt - :language: kotlin - :start-after: DOCSTART 1 - :end-before: DOCEND 1 +Auto Acceptance +``````````````` -The node administrator can review the change and decide if they are going to accept it. The approval should be do +If the only changes between the current and new parameters are for auto-acceptable parameters then, unless configured otherwise, the new +parameters will be accepted without user input. The following parameters with the ``@AutoAcceptable`` annotation are auto-acceptable: + +.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + +This behaviour can be turned off by setting the optional node configuration property ``NetworkParameterAcceptanceSettings.autoAcceptEnabled`` +to ``false``. For example: + +.. sourcecode:: guess + + ... + NetworkParameterAcceptanceSettings { + autoAcceptEnabled = false + } + ... + +It is also possible to switch off this behaviour at a more granular parameter level. This can be achieved by specifying the set of +``@AutoAcceptable`` parameters that should not be auto-acceptable in the optional +``NetworkParameterAcceptanceSettings.excludedAutoAcceptableParameters`` node configuration property. + +For example, auto-acceptance can be switched off for any updates that change the ``packageOwnership`` map by adding the following to the +node configuration: + +.. sourcecode:: guess + + ... + NetworkParameterAcceptanceSettings { + excludedAutoAcceptableParameters: ["packageOwnership"] + } + ... + +Manual Acceptance +````````````````` + +If the auto-acceptance behaviour is turned off via the configuration or the network parameters change involves parameters that are +not auto-acceptable then manual approval is required. + +In this case the node administrator can review the change and decide if they are going to accept it. The approval should be done before the update Deadline. Nodes that don't approve before the deadline will likely be removed from the network map by the zone operator, but that is a decision that is left to the operator's discretion. For example the operator might also choose to change the deadline instead. diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index b1db04faf5..71033c1c65 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -21,6 +21,10 @@ Significant Changes in 4.0 for "current-ness". In other words, the contract logic isn't run for the referencing transaction only. It's still a normal state when it occurs in an input or output position. +* **Added auto-acceptance for network parameters updates** + Added auto-accepting for a subset of network parameters, negating the need for a node operator to manually run an accept + command on every parameter update. This behaviour can be turned off via the node configuration. + << MORE TO COME >> .. _release_notes_v3_3: 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 747827cea7..a466ab5310 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 @@ -94,7 +94,10 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP notarySpecs = emptyList() ) { val alice = startNode(providedName = ALICE_NAME, devMode = false).getOrThrow() as NodeHandleInternal - val nextParams = networkMapServer.networkParameters.copy(epoch = 3, modifiedTime = Instant.ofEpochMilli(random63BitValue())) + val nextParams = networkMapServer.networkParameters.copy( + epoch = 3, + modifiedTime = Instant.ofEpochMilli(random63BitValue()), + maxMessageSize = networkMapServer.networkParameters.maxMessageSize + 1) val nextHash = nextParams.serialize().hash val snapshot = alice.rpc.networkParametersFeed().snapshot val updates = alice.rpc.networkParametersFeed().updates.bufferUntilSubscribed() @@ -103,7 +106,10 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP networkMapServer.scheduleParametersUpdate(nextParams, "Next parameters", Instant.ofEpochMilli(random63BitValue())) // Wait for network map client to poll for the next update. Thread.sleep(cacheTimeout.toMillis() * 2) - val laterParams = networkMapServer.networkParameters.copy(epoch = 4, modifiedTime = Instant.ofEpochMilli(random63BitValue())) + val laterParams = networkMapServer.networkParameters.copy( + epoch = 4, + modifiedTime = Instant.ofEpochMilli(random63BitValue()), + maxMessageSize = nextParams.maxMessageSize + 1) val laterHash = laterParams.serialize().hash networkMapServer.scheduleParametersUpdate(laterParams, "Another update", Instant.ofEpochMilli(random63BitValue())) Thread.sleep(cacheTimeout.toMillis() * 2) 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 bb788f2a72..df72f6410c 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -345,7 +345,13 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val (nodeInfo, signedNodeInfo) = nodeInfoAndSigned identityService.ourNames = nodeInfo.legalIdentities.map { it.name }.toSet() services.start(nodeInfo, netParams) - networkMapUpdater.start(trustRoot, signedNetParams.raw.hash, signedNodeInfo.raw.hash) + networkMapUpdater.start( + trustRoot, + signedNetParams.raw.hash, + signedNodeInfo, + netParams, + keyManagementService, + configuration.networkParameterAcceptanceSettings) startMessagingService(rpcOps, nodeInfo, myNotaryIdentity, netParams) // Do all of this in a database transaction so anything that might need a connection has one. diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index c75a7eb268..1e43d25104 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -85,6 +85,8 @@ interface NodeConfiguration { val cryptoServiceName: SupportedCryptoServices? val cryptoServiceConf: String? // Location for the cryptoService conf file. + val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings + companion object { // default to at least 8MB and a bit extra for larger heap sizes internal val defaultTransactionCacheSize: Long = 8.MB + getAdditionalCacheMemory() @@ -173,6 +175,19 @@ data class NetworkServicesConfig( val inferred: Boolean = false ) +/** + * Specifies the auto-acceptance behaviour for network parameter updates + * + * @property autoAcceptEnabled Specifies whether network parameter auto-accepting is enabled. Only parameters annotated with the + * AutoAcceptable annotation can be auto-accepted. + * @property excludedAutoAcceptableParameters Set of parameters to explicitly exclude from auto-accepting. Note that if [autoAcceptEnabled] + * is false then this parameter is redundant. + */ +data class NetworkParameterAcceptanceSettings( + val autoAcceptEnabled: Boolean = true, + val excludedAutoAcceptableParameters: Set = emptySet() +) + /** * Currently only used for notarisation requests. * diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt index 54fda39e5a..599ed07673 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfigurationImpl.kt @@ -76,7 +76,8 @@ data class NodeConfigurationImpl( override val flowOverrides: FlowOverrideConfig?, override val cordappSignerKeyFingerprintBlacklist: List = Defaults.cordappSignerKeyFingerprintBlacklist, override val cryptoServiceName: SupportedCryptoServices? = null, - override val cryptoServiceConf: String? = null + override val cryptoServiceConf: String? = null, + override val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings = Defaults.networkParameterAcceptanceSettings ) : NodeConfiguration { internal object Defaults { val jmxMonitoringHttpPort: Int? = null @@ -108,6 +109,7 @@ data class NodeConfigurationImpl( val flowMonitorSuspensionLoggingThresholdMillis: Duration = NodeConfiguration.DEFAULT_FLOW_MONITOR_SUSPENSION_LOGGING_THRESHOLD_MILLIS val jmxReporterType: JmxReporterType = NodeConfiguration.defaultJmxReporterType val cordappSignerKeyFingerprintBlacklist: List = DEV_PUB_KEY_HASHES.map { it.toString() } + val networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings = NetworkParameterAcceptanceSettings() fun cordappsDirectories(baseDirectory: Path) = listOf(baseDirectory / CORDAPPS_DIR_NAME_DEFAULT) 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 6536f5869c..66355758c7 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 @@ -4,18 +4,19 @@ import com.google.common.util.concurrent.MoreExecutors import net.corda.core.CordaRuntimeException import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SignedData -import net.corda.core.internal.copyTo -import net.corda.core.internal.div -import net.corda.core.internal.exists -import net.corda.core.internal.readObject +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.services.KeyManagementService import net.corda.core.serialization.serialize import net.corda.core.utilities.contextLogger import net.corda.core.utilities.minutes import net.corda.node.services.api.NetworkMapCacheInternal +import net.corda.node.services.config.NetworkParameterAcceptanceSettings import net.corda.node.utilities.NamedThreadFactory import net.corda.nodeapi.exceptions.OutdatedNetworkParameterHashException +import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.network.* import rx.Subscription import rx.subjects.PublishSubject @@ -45,20 +46,40 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, } private var newNetworkParameters: Pair? = null private var fileWatcherSubscription: Subscription? = null + private var autoAcceptNetworkParameters: Boolean = true private lateinit var trustRoot: X509Certificate 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 override fun close() { fileWatcherSubscription?.unsubscribe() MoreExecutors.shutdownAndAwaitTermination(networkMapPoller, 50, TimeUnit.SECONDS) } - fun start(trustRoot: X509Certificate, currentParametersHash: SecureHash, ourNodeInfoHash: SecureHash) { + fun start(trustRoot: X509Certificate, + currentParametersHash: SecureHash, + ourNodeInfo: SignedNodeInfo, + networkParameters: NetworkParameters, + keyManagementService: KeyManagementService, + networkParameterAcceptanceSettings: NetworkParameterAcceptanceSettings) { require(fileWatcherSubscription == null) { "Should not call this method twice." } this.trustRoot = trustRoot this.currentParametersHash = currentParametersHash - this.ourNodeInfoHash = ourNodeInfoHash + this.ourNodeInfo = ourNodeInfo + this.ourNodeInfoHash = ourNodeInfo.raw.hash + this.networkParameters = networkParameters + this.keyManagementService = keyManagementService + this.autoAcceptNetworkParameters = networkParameterAcceptanceSettings.autoAcceptEnabled + this.excludedAutoAcceptNetworkParameters = networkParameterAcceptanceSettings.excludedAutoAcceptableParameters + + val autoAcceptNetworkParametersNames = NetworkParameters.autoAcceptablePropertyNames - excludedAutoAcceptNetworkParameters + if (autoAcceptNetworkParameters && autoAcceptNetworkParametersNames.isNotEmpty()) { + logger.info("Auto-accept enabled for network parameter changes which modify only: $autoAcceptNetworkParametersNames") + } watchForNodeInfoFiles() if (networkMapClient != null) { watchHttpNetworkMap() @@ -195,7 +216,15 @@ The node will shutdown now.""") newNetParams, update.description, update.updateDeadline) - parametersUpdatesTrack.onNext(updateInfo) + + if (autoAcceptNetworkParameters && networkParameters.canAutoAccept(newNetParams, excludedAutoAcceptNetworkParameters)) { + logger.info("Auto-accepting network parameter update ${update.newParametersHash}") + acceptNewNetworkParameters(update.newParametersHash) { hash -> + hash.serialize().sign { keyManagementService.sign(it.bytes, ourNodeInfo.verified().legalIdentities[0].owningKey) } + } + } else { + parametersUpdatesTrack.onNext(updateInfo) + } } fun acceptNewNetworkParameters(parametersHash: SecureHash, sign: (SecureHash) -> SignedData) { diff --git a/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt b/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt index 09faa02886..4be3c41682 100644 --- a/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/NetworkParametersTest.kt @@ -4,6 +4,7 @@ import net.corda.core.crypto.generateKeyPair import net.corda.core.node.JavaPackageName import net.corda.core.node.NetworkParameters import net.corda.core.node.NotaryInfo +import net.corda.core.node.services.AttachmentId import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.days import net.corda.core.utilities.getOrThrow @@ -129,7 +130,42 @@ class NetworkParametersTest { assert(JavaPackageName("com.example").owns("com.example.something.MyClass")) assert(!JavaPackageName("com.example").owns("com.examplesomething.MyClass")) assert(!JavaPackageName("com.exam").owns("com.example.something.MyClass")) + } + @Test + fun `auto acceptance checks are correct`() { + val packageOwnership = mapOf( + JavaPackageName("com.example1") to generateKeyPair().public, + JavaPackageName("com.example2") to generateKeyPair().public) + val whitelistedContractImplementations = mapOf( + "example1" to listOf(AttachmentId.randomSHA256()), + "example2" to listOf(AttachmentId.randomSHA256())) + + val netParams = testNetworkParameters() + val netParamsAutoAcceptable = netParams.copy( + packageOwnership = packageOwnership, + whitelistedContractImplementations = whitelistedContractImplementations) + val netParamsNotAutoAcceptable = netParamsAutoAcceptable.copy( + maxMessageSize = netParams.maxMessageSize + 1) + + assert(netParams.canAutoAccept(netParams, emptySet())) { + "auto-acceptable if identical" + } + assert(netParams.canAutoAccept(netParams, NetworkParameters.autoAcceptablePropertyNames)) { + "auto acceptable if identical regardless of exclusions" + } + assert(netParams.canAutoAccept(netParamsAutoAcceptable, emptySet())) { + "auto-acceptable if only AutoAcceptable params have changed" + } + assert(netParams.canAutoAccept(netParamsAutoAcceptable, setOf("modifiedTime"))) { + "auto-acceptable if only AutoAcceptable params have changed and excluded param has not changed" + } + assert(!netParams.canAutoAccept(netParamsNotAutoAcceptable, emptySet())) { + "not auto-acceptable if non-AutoAcceptable param has changed" + } + assert(!netParams.canAutoAccept(netParamsAutoAcceptable, setOf("whitelistedContractImplementations"))) { + "not auto-acceptable if only AutoAcceptable params have changed but one has been added to the exclusion set" + } } // Helpers 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 bbe4812c8d..b6451f3217 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 @@ -11,13 +11,17 @@ import net.corda.core.identity.Party import net.corda.core.internal.* import net.corda.core.internal.concurrent.openFuture 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.millis import net.corda.node.VersionInfo import net.corda.node.services.api.NetworkMapCacheInternal +import net.corda.node.services.config.NetworkParameterAcceptanceSettings import net.corda.core.internal.NODE_INFO_DIRECTORY import net.corda.nodeapi.internal.NodeInfoAndSigned +import net.corda.nodeapi.internal.SignedNodeInfo +import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME import net.corda.nodeapi.internal.network.NodeInfoFilesCopier import net.corda.nodeapi.internal.network.SignedNetworkParameters @@ -27,7 +31,9 @@ import net.corda.testing.core.* import net.corda.testing.internal.DEV_ROOT_CA import net.corda.testing.internal.TestNodeInfoBuilder import net.corda.testing.internal.createNodeInfoAndSigned +import net.corda.testing.node.internal.MockKeyManagementService import net.corda.testing.node.internal.network.NetworkMapServer +import net.corda.testing.node.makeTestIdentityService import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.After @@ -37,6 +43,7 @@ import org.junit.Test import rx.schedulers.TestScheduler import java.io.IOException import java.net.URL +import java.security.KeyPair import java.time.Instant import java.time.temporal.ChronoUnit import java.util.* @@ -58,12 +65,16 @@ class NetworkMapUpdaterTest { private val fileWatcher = NodeInfoWatcher(baseDir, scheduler) private val nodeReadyFuture = openFuture() private val networkMapCache = createMockNetworkMapCache() + private lateinit var ourKeyPair: KeyPair + private lateinit var ourNodeInfo: SignedNodeInfo private lateinit var server: NetworkMapServer private lateinit var networkMapClient: NetworkMapClient private lateinit var updater: NetworkMapUpdater @Before fun setUp() { + ourKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + ourNodeInfo = createNodeInfoAndSigned("Our info", ourKeyPair).signed server = NetworkMapServer(cacheExpiryMs.millis) val address = server.start() networkMapClient = NetworkMapClient(URL("http://$address"), @@ -81,8 +92,16 @@ class NetworkMapUpdaterTest { updater = NetworkMapUpdater(networkMapCache, fileWatcher, netMapClient, baseDir, extraNetworkMapKeys) } - private fun startUpdater(ourNodeHash: SecureHash = SecureHash.randomSHA256()) { - updater.start(DEV_ROOT_CA.certificate, server.networkParameters.serialize().hash, ourNodeHash) + private fun startUpdater(ourNodeInfo: SignedNodeInfo = this.ourNodeInfo, + 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)) } @Test @@ -192,7 +211,7 @@ class NetworkMapUpdaterTest { val snapshot = paramsFeed.snapshot val updates = paramsFeed.updates.bufferUntilSubscribed() assertEquals(null, snapshot) - val newParameters = testNetworkParameters(epoch = 2) + val newParameters = testNetworkParameters(epoch = 2, maxMessageSize = 10485761) val updateDeadline = Instant.now().plus(1, ChronoUnit.DAYS) server.scheduleParametersUpdate(newParameters, "Test update", updateDeadline) startUpdater() @@ -211,18 +230,65 @@ class NetworkMapUpdaterTest { @Test fun `ack network parameters update`() { setUpdater() - val newParameters = testNetworkParameters(epoch = 314) + val newParameters = testNetworkParameters(epoch = 314, maxMessageSize = 10485761) + server.scheduleParametersUpdate(newParameters, "Test update", Instant.MIN) + startUpdater() + // TODO: Remove sleep in unit test. + Thread.sleep(2L * cacheExpiryMs) + val newHash = newParameters.serialize().hash + val updateFile = baseDir / NETWORK_PARAMS_UPDATE_FILE_NAME + assert(!updateFile.exists()) { "network parameters should not be auto accepted" } + updater.acceptNewNetworkParameters(newHash) { it.serialize().sign(ourKeyPair) } + val signedNetworkParams = updateFile.readObject() + val paramsFromFile = signedNetworkParams.verifiedNetworkMapCert(DEV_ROOT_CA.certificate) + assertEquals(newParameters, paramsFromFile) + assertEquals(newHash, server.latestParametersAccepted(ourKeyPair.public)) + } + + @Test + fun `network parameters auto-accepted when update only changes whitelist`() { + setUpdater() + val newParameters = testNetworkParameters( + epoch = 314, + whitelistedContractImplementations = mapOf("key" to listOf(SecureHash.randomSHA256()))) server.scheduleParametersUpdate(newParameters, "Test update", Instant.MIN) startUpdater() // TODO: Remove sleep in unit test. Thread.sleep(2L * cacheExpiryMs) val newHash = newParameters.serialize().hash - val keyPair = Crypto.generateKeyPair() - updater.acceptNewNetworkParameters(newHash) { it.serialize().sign(keyPair) } val updateFile = baseDir / NETWORK_PARAMS_UPDATE_FILE_NAME val signedNetworkParams = updateFile.readObject() val paramsFromFile = signedNetworkParams.verifiedNetworkMapCert(DEV_ROOT_CA.certificate) assertEquals(newParameters, paramsFromFile) + assertEquals(newHash, server.latestParametersAccepted(ourKeyPair.public)) + } + + @Test + fun `network parameters not auto-accepted when update only changes whitelist but parameter included in exclusion`() { + setUpdater() + val newParameters = testNetworkParameters( + epoch = 314, + whitelistedContractImplementations = mapOf("key" to listOf(SecureHash.randomSHA256()))) + server.scheduleParametersUpdate(newParameters, "Test update", Instant.MIN) + startUpdater(excludedAutoAcceptNetworkParameters = setOf("whitelistedContractImplementations")) + // TODO: Remove sleep in unit test. + Thread.sleep(2L * cacheExpiryMs) + val updateFile = baseDir / NETWORK_PARAMS_UPDATE_FILE_NAME + assert(!updateFile.exists()) { "network parameters should not be auto accepted" } + } + + @Test + fun `network parameters not auto-accepted when update only changes whitelist but auto accept configured to be false`() { + setUpdater() + val newParameters = testNetworkParameters( + epoch = 314, + whitelistedContractImplementations = mapOf("key" to listOf(SecureHash.randomSHA256()))) + server.scheduleParametersUpdate(newParameters, "Test update", Instant.MIN) + startUpdater(autoAcceptNetworkParameters = false) + // TODO: Remove sleep in unit test. + Thread.sleep(2L * cacheExpiryMs) + val updateFile = baseDir / NETWORK_PARAMS_UPDATE_FILE_NAME + assert(!updateFile.exists()) { "network parameters should not be auto accepted" } } @Test @@ -306,7 +372,7 @@ class NetworkMapUpdaterTest { setUpdater() networkMapCache.addNode(myInfo) // Simulate behaviour on node startup when our node info is added to cache networkMapClient.publish(signedOtherInfo) - startUpdater(ourNodeHash = signedMyInfo.raw.hash) + startUpdater(ourNodeInfo = signedMyInfo) Thread.sleep(2L * cacheExpiryMs) verify(networkMapCache, never()).removeNode(myInfo) assertThat(server.networkMapHashes()).containsOnly(signedOtherInfo.raw.hash) @@ -363,8 +429,12 @@ class NetworkMapUpdaterTest { } } - private fun createNodeInfoAndSigned(org: String): NodeInfoAndSigned { - return createNodeInfoAndSigned(CordaX500Name(org, "London", "GB")) + private fun createNodeInfoAndSigned(org: String, keyPair: KeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)): NodeInfoAndSigned { + val serial: Long = 1 + val platformVersion = 1 + val nodeInfoBuilder = TestNodeInfoBuilder() + nodeInfoBuilder.addLegalIdentity(CordaX500Name(org, "London", "GB"), keyPair, keyPair) + return nodeInfoBuilder.buildWithSigned(serial, platformVersion) } private fun advanceTime() { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index c48522506e..f2b9315920 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -35,6 +35,7 @@ import net.corda.node.services.api.FlowStarter import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.api.StartedNodeServices import net.corda.node.services.config.FlowTimeoutConfiguration +import net.corda.node.services.config.NetworkParameterAcceptanceSettings import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.VerifierType import net.corda.node.services.identity.PersistentIdentityService @@ -618,6 +619,7 @@ private fun mockNodeConfiguration(certificatesDirectory: Path): NodeConfiguratio doReturn(null).whenever(it).devModeOptions doReturn(null).whenever(it).cryptoServiceName doReturn(null).whenever(it).cryptoServiceConf + doReturn(NetworkParameterAcceptanceSettings()).whenever(it).networkParameterAcceptanceSettings } } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt index 63194080fc..e7f16ad0ab 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt @@ -20,9 +20,10 @@ import java.security.cert.X509Certificate class TestNodeInfoBuilder(private val intermediateAndRoot: Pair = DEV_INTERMEDIATE_CA to DEV_ROOT_CA.certificate) { private val identitiesAndPrivateKeys = ArrayList>() - fun addLegalIdentity(name: CordaX500Name, nodeKeyPair: KeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)): Pair { + fun addLegalIdentity(name: CordaX500Name, nodeKeyPair: + KeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME), + identityKeyPair: KeyPair = Crypto.generateKeyPair()): Pair { val nodeCertificateAndKeyPair = createDevNodeCa(intermediateAndRoot.first, name, nodeKeyPair) - val identityKeyPair = Crypto.generateKeyPair() val identityCert = X509Utilities.createCertificate( CertificateType.LEGAL_IDENTITY, nodeCertificateAndKeyPair.certificate,