From 098c6f2a6db2f4f0bb3dac361c0d0cf8dff50d1c Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Fri, 8 May 2026 01:19:39 -0300 Subject: [PATCH] Improve config * Soft-deprecate --cluster-id=N triggering the associated preset selection * Rewrite applyNetworkConf to apply user-set fields over preset fields * Add createNode(preset, mode, overrides, additions) nim api * Generate WakuNodeConfOverlay (all Option fields) from WakuNodeConf * New parser for configJson handles new messaging shape and full conf shape * Change all confbuilder defaults from literal values to DefaultXXX consts * Change int/bool WakuNodeConf fields to Option to get user intent w/o sentinels * Make Option CLI default-value help mention defaults now owned by confbuilder * Misc refactors, fixes * Add tests --- .../liteprotocoltester/liteprotocoltester.nim | 2 +- examples/wakustealthcommitments/node_spec.nim | 12 +- .../logos_delivery_api/node_api.nim | 64 +---- tests/api/test_all.nim | 1 + tests/api/test_api_health.nim | 4 +- tests/api/test_api_receive.nim | 4 +- tests/api/test_api_send.nim | 4 +- tests/api/test_api_subscription.nim | 4 +- tests/api/test_messaging_conf.nim | 179 ++++++++++++ tests/api/test_node_conf.nim | 14 +- tests/factory/test_waku_conf.nim | 9 +- tests/test_waku.nim | 105 ++++++- tools/confutils/cli_args.nim | 107 ++++--- tools/confutils/conf_from_json.nim | 243 ++++++++++++++++ tools/confutils/messaging_conf.nim | 37 +++ tools/confutils/optionalize.nim | 74 +++++ waku/api/api.nim | 19 +- .../conf_builder/discv5_conf_builder.nim | 20 +- .../filter_service_conf_builder.nim | 14 +- .../kademlia_discovery_conf_builder.nim | 8 +- .../metrics_server_conf_builder.nim | 14 +- .../factory/conf_builder/mix_conf_builder.nim | 4 +- .../conf_builder/rest_server_conf_builder.nim | 11 +- .../conf_builder/rln_relay_conf_builder.nim | 16 +- .../store_service_conf_builder.nim | 20 +- .../conf_builder/store_sync_conf_builder.nim | 4 +- .../conf_builder/waku_conf_builder.nim | 265 ++++++++++-------- .../conf_builder/web_socket_conf_builder.nim | 14 +- waku/factory/networks_config.nim | 7 +- 29 files changed, 987 insertions(+), 292 deletions(-) create mode 100644 tests/api/test_messaging_conf.nim create mode 100644 tools/confutils/conf_from_json.nim create mode 100644 tools/confutils/messaging_conf.nim create mode 100644 tools/confutils/optionalize.nim diff --git a/apps/liteprotocoltester/liteprotocoltester.nim b/apps/liteprotocoltester/liteprotocoltester.nim index 46c85e910..e54440245 100644 --- a/apps/liteprotocoltester/liteprotocoltester.nim +++ b/apps/liteprotocoltester/liteprotocoltester.nim @@ -96,7 +96,7 @@ when isMainModule: wakuNodeConf.shards = @[conf.shard] wakuNodeConf.contentTopics = conf.contentTopics - wakuNodeConf.clusterId = conf.clusterId + wakuNodeConf.clusterId = some(conf.clusterId) ## TODO: Depending on the tester needs we might extend here with shards, clusterId, etc... wakuNodeConf.metricsServer = true diff --git a/examples/wakustealthcommitments/node_spec.nim b/examples/wakustealthcommitments/node_spec.nim index d85e83a5b..3f999aae4 100644 --- a/examples/wakustealthcommitments/node_spec.nim +++ b/examples/wakustealthcommitments/node_spec.nim @@ -30,18 +30,18 @@ proc setup*(): Waku = # Override configuration conf.maxMessageSize = twnNetworkConf.maxMessageSize - conf.clusterId = twnNetworkConf.clusterId + conf.clusterId = some(twnNetworkConf.clusterId) conf.rlnRelayEthContractAddress = twnNetworkConf.rlnRelayEthContractAddress - conf.rlnRelayDynamic = twnNetworkConf.rlnRelayDynamic - conf.discv5Discovery = twnNetworkConf.discv5Discovery + conf.rlnRelayDynamic = some(twnNetworkConf.rlnRelayDynamic) + conf.discv5Discovery = some(twnNetworkConf.discv5Discovery) conf.discv5BootstrapNodes = conf.discv5BootstrapNodes & twnNetworkConf.discv5BootstrapNodes - conf.rlnEpochSizeSec = twnNetworkConf.rlnEpochSizeSec - conf.rlnRelayUserMessageLimit = twnNetworkConf.rlnRelayUserMessageLimit + conf.rlnEpochSizeSec = some(twnNetworkConf.rlnEpochSizeSec) + conf.rlnRelayUserMessageLimit = some(twnNetworkConf.rlnRelayUserMessageLimit) # Only set rlnRelay to true if relay is configured if conf.relay: - conf.rlnRelay = twnNetworkConf.rlnRelay + conf.rlnRelay = some(twnNetworkConf.rlnRelay) info "Starting node" var waku = (waitFor Waku.new(conf)).valueOr: diff --git a/liblogosdelivery/logos_delivery_api/node_api.nim b/liblogosdelivery/logos_delivery_api/node_api.nim index 90630717b..72d1c594f 100644 --- a/liblogosdelivery/logos_delivery_api/node_api.nim +++ b/liblogosdelivery/logos_delivery_api/node_api.nim @@ -1,11 +1,11 @@ -import std/[json, strutils, tables] -import chronos, chronicles, results, confutils, confutils/std/net, ffi +import std/json +import chronos, chronicles, results, ffi import waku/factory/waku, waku/node/waku_node, waku/api/[api, types], waku/events/[message_events, health_events], - tools/confutils/cli_args, + tools/confutils/conf_from_json, ../declare_lib, ../json_event @@ -15,59 +15,11 @@ proc `%`*(id: RequestId): JsonNode = registerReqFFI(CreateNodeRequest, ctx: ptr FFIContext[Waku]): proc(configJson: cstring): Future[Result[string, string]] {.async.} = - ## Parse the JSON configuration using fieldPairs approach (WakuNodeConf) - var conf = defaultWakuNodeConf().valueOr: - return err("Failed creating default conf: " & error) + let conf = parseConfJson($configJson).valueOr: + error "Failed to assemble WakuNodeConf from JSON", + error = error, configJson = $configJson + return err(error) - var jsonNode: JsonNode - try: - jsonNode = parseJson($configJson) - except Exception: - let exceptionMsg = getCurrentExceptionMsg() - error "Failed to parse config JSON", - error = exceptionMsg, configJson = $configJson - return err( - "Failed to parse config JSON: " & exceptionMsg & " configJson string: " & - $configJson - ) - - var jsonFields: Table[string, (string, JsonNode)] - for key, value in jsonNode: - let lowerKey = key.toLowerAscii() - - if jsonFields.hasKey(lowerKey): - error "Duplicate configuration option found when normalized to lowercase", - key = key - return err( - "Duplicate configuration option found when normalized to lowercase: '" & key & - "'" - ) - - jsonFields[lowerKey] = (key, value) - - for confField, confValue in fieldPairs(conf): - let lowerField = confField.toLowerAscii() - if jsonFields.hasKey(lowerField): - let (jsonKey, jsonValue) = jsonFields[lowerField] - let formattedString = ($jsonValue).strip(chars = {'\"'}) - try: - confValue = parseCmdArg(typeof(confValue), formattedString) - except Exception: - return err( - "Failed to parse field '" & confField & "' from JSON key '" & jsonKey & "': " & - getCurrentExceptionMsg() & ". Value: " & formattedString - ) - - jsonFields.del(lowerField) - - if jsonFields.len > 0: - var unknownKeys = newSeq[string]() - for _, (jsonKey, _) in pairs(jsonFields): - unknownKeys.add(jsonKey) - error "Unrecognized configuration option(s) found", option = unknownKeys - return err("Unrecognized configuration option(s) found: " & $unknownKeys) - - # Create the node ctx.myLib[] = (await api.createNode(conf)).valueOr: let errMsg = $error chronicles.error "CreateNodeRequest failed", err = errMsg @@ -96,7 +48,7 @@ proc logosdelivery_create_node( ): pointer {.dynlib, exportc, cdecl.} = initializeLibrary() - if isNil(callback): + if callback.isNil(): echo "error: missing callback in logosdelivery_create_node" return nil diff --git a/tests/api/test_all.nim b/tests/api/test_all.nim index 56be19c27..ab4b18f77 100644 --- a/tests/api/test_all.nim +++ b/tests/api/test_all.nim @@ -3,6 +3,7 @@ import ./test_entry_nodes, ./test_node_conf, + ./test_messaging_conf, ./test_api_send, ./test_api_subscription, ./test_api_receive, diff --git a/tests/api/test_api_health.nim b/tests/api/test_api_health.nim index f3dd340af..a83263492 100644 --- a/tests/api/test_api_health.nim +++ b/tests/api/test_api_health.nim @@ -96,7 +96,7 @@ suite "LM API health checking": conf.listenAddress = parseIpAddress("0.0.0.0") conf.tcpPort = Port(0) conf.discv5UdpPort = Port(0) - conf.clusterId = 3'u16 + conf.clusterId = some(3'u16) conf.numShardsInNetwork = 1 conf.rest = false @@ -273,7 +273,7 @@ suite "LM API health checking": edgeConf.listenAddress = parseIpAddress("0.0.0.0") edgeConf.tcpPort = Port(0) edgeConf.discv5UdpPort = Port(0) - edgeConf.clusterId = 3'u16 + edgeConf.clusterId = some(3'u16) edgeConf.maxMessageSize = "150 KiB" edgeConf.rest = false diff --git a/tests/api/test_api_receive.nim b/tests/api/test_api_receive.nim index 52f8713f9..e1bf155f3 100644 --- a/tests/api/test_api_receive.nim +++ b/tests/api/test_api_receive.nim @@ -67,9 +67,9 @@ proc createApiNodeConf(numShards: uint16 = 1): WakuNodeConf = conf.listenAddress = parseIpAddress("0.0.0.0") conf.tcpPort = Port(0) conf.discv5UdpPort = Port(0) - conf.clusterId = 3'u16 + conf.clusterId = some(3'u16) conf.numShardsInNetwork = numShards - conf.reliabilityEnabled = true + conf.reliabilityEnabled = some(true) conf.rest = false result = conf diff --git a/tests/api/test_api_send.nim b/tests/api/test_api_send.nim index 28f0ca2ff..43efe9555 100644 --- a/tests/api/test_api_send.nim +++ b/tests/api/test_api_send.nim @@ -124,9 +124,9 @@ proc createApiNodeConf(mode: cli_args.WakuMode = cli_args.WakuMode.Core): WakuNo conf.listenAddress = parseIpAddress("0.0.0.0") conf.tcpPort = Port(0) conf.discv5UdpPort = Port(0) - conf.clusterId = 3'u16 + conf.clusterId = some(3'u16) conf.numShardsInNetwork = 1 - conf.reliabilityEnabled = true + conf.reliabilityEnabled = some(true) conf.rest = false result = conf diff --git a/tests/api/test_api_subscription.nim b/tests/api/test_api_subscription.nim index e0ceb9226..f8f40218e 100644 --- a/tests/api/test_api_subscription.nim +++ b/tests/api/test_api_subscription.nim @@ -75,9 +75,9 @@ proc createApiNodeConf( conf.listenAddress = parseIpAddress("0.0.0.0") conf.tcpPort = Port(0) conf.discv5UdpPort = Port(0) - conf.clusterId = 3'u16 + conf.clusterId = some(3'u16) conf.numShardsInNetwork = numShards - conf.reliabilityEnabled = true + conf.reliabilityEnabled = some(true) conf.rest = false result = conf diff --git a/tests/api/test_messaging_conf.nim b/tests/api/test_messaging_conf.nim new file mode 100644 index 000000000..85312002c --- /dev/null +++ b/tests/api/test_messaging_conf.nim @@ -0,0 +1,179 @@ +{.used.} + +import std/[algorithm, json, options, sequtils], results, testutils/unittests +import tools/confutils/conf_from_json, tools/confutils/cli_args +import tools/confutils/messaging_conf + +suite "Messaging conf JSON parser": + test "Routes to messaging shape when mode and overrides are present": + let res = parseConfJson("""{"mode": "Core", "overrides": {}}""") + require res.isOk() + let conf = res.get() + check conf.mode == cli_args.WakuMode.Core + + test "Routes to full conf shape when only mode key is present": + let res = parseConfJson("""{"mode": "Edge"}""") + require res.isOk() + let conf = res.get() + check conf.mode == cli_args.WakuMode.Edge + + test "Messaging shape applies overrides": + let res = parseConfJson( + """{"mode": "Core", "overrides": {"clusterId": 42, "tcpPort": 12345}}""" + ) + require res.isOk() + let conf = res.get() + check: + conf.clusterId == some(42'u16) + conf.tcpPort == Port(12345) + + test "Messaging shape applies preset": + let res = parseConfJson("""{"mode": "Core", "preset": "twn", "overrides": {}}""") + require res.isOk() + let conf = res.get() + check conf.preset == "twn" + + test "Messaging shape applies additions to list fields": + let res = parseConfJson( + """{"mode": "Core", "overrides": {}, "additions": {"staticnodes": ["/ip4/1.2.3.4/tcp/60000/p2p/16Uiu2HAmTUbnxLGT9JvV6mu9oPyDjqHK4Phs1VDJNUgESgNSkuby"]}}""" + ) + require res.isOk() + let conf = res.get() + check conf.staticnodes.len == 1 + + test "Messaging shape: additions concat after overrides on same list field": + let res = parseConfJson( + """{"mode": "Core", "additions": {"staticnodes": ["/ip4/1.2.3.4/tcp/60000/p2p/16Uiu2HAmTUbnxLGT9JvV6mu9oPyDjqHK4Phs1VDJNUgESgNSkuby"]}, "overrides": {"staticnodes": ["/ip4/5.6.7.8/tcp/60000/p2p/16Uiu2HAmTUbnxLGT9JvV6mu9oPyDjqHK4Phs1VDJNUgESgNSkuby"]}}""" + ) + require res.isOk() + let conf = res.get() + check: + conf.staticnodes.len == 2 + conf.staticnodes[0] == + "/ip4/5.6.7.8/tcp/60000/p2p/16Uiu2HAmTUbnxLGT9JvV6mu9oPyDjqHK4Phs1VDJNUgESgNSkuby" + conf.staticnodes[1] == + "/ip4/1.2.3.4/tcp/60000/p2p/16Uiu2HAmTUbnxLGT9JvV6mu9oPyDjqHK4Phs1VDJNUgESgNSkuby" + + test "Messaging shape rejects missing mode": + let res = parseConfJson("""{"overrides": {}}""") + check res.isErr() + + test "Messaging shape rejects unknown override field": + let res = parseConfJson("""{"mode": "Core", "overrides": {"bogusField": 1}}""") + check res.isErr() + + test "Messaging shape rejects addition on non-list field": + let res = parseConfJson( + """{"mode": "Core", "overrides": {}, "additions": {"clusterId": [1]}}""" + ) + check res.isErr() + + test "Messaging shape rejects unknown top-level key": + let res = parseConfJson("""{"mode": "Core", "overrides": {}, "garbage": 1}""") + check res.isErr() + + test "Full conf shape parses arbitrary WakuNodeConf fields": + let res = parseConfJson("""{"clusterId": 7, "tcpPort": 22222}""") + require res.isOk() + let conf = res.get() + check: + conf.clusterId == some(7'u16) + conf.tcpPort == Port(22222) + + test "Full conf shape rejects unknown field": + let res = parseConfJson("""{"completelyMadeUp": 1}""") + check res.isErr() + + test "Malformed JSON returns error": + let res = parseConfJson("{ not json }") + check res.isErr() + + test "Rejects top-level JSON array": + let res = parseConfJson("""[1, 2]""") + check res.isErr() + + test "Rejects top-level scalar": + let res = parseConfJson("""42""") + check res.isErr() + + test "Rejects top-level null": + let res = parseConfJson("""null""") + check res.isErr() + + test "Messaging shape rejects 'mode' inside 'overrides'": + let res = parseConfJson("""{"mode": "Core", "overrides": {"mode": "Edge"}}""") + check res.isErr() + + test "Messaging shape rejects 'preset' inside 'overrides'": + let res = parseConfJson( + """{"mode": "Core", "preset": "twn", "overrides": {"preset": "logos.dev"}}""" + ) + check res.isErr() + + test "Messaging shape rejects 'mode' inside 'additions'": + let res = parseConfJson( + """{"mode": "Core", "overrides": {}, "additions": {"mode": "Edge"}}""" + ) + check res.isErr() + + test "Messaging shape rejects 'preset' inside 'additions'": + let res = parseConfJson( + """{"mode": "Core", "overrides": {}, "additions": {"preset": "twn"}}""" + ) + check res.isErr() + + test "Rejects duplicate normalized keys": + let res = parseConfJson("""{"clusterId": 1, "ClusterId": 2}""") + check res.isErr() + + test "Case-insensitive override matching": + let res = parseConfJson("""{"mode": "Core", "overrides": {"CLUSTERID": 99}}""") + require res.isOk() + let conf = res.get() + check conf.clusterId == some(99'u16) + + test "Rejects 'overrides' that isn't a JSON object": + let res = parseConfJson("""{"mode": "Core", "overrides": "not an object"}""") + check res.isErr() + + test "Rejects 'additions' that isn't a JSON object": + let res = parseConfJson( + """{"mode": "Core", "overrides": {}, "additions": ["not an object"]}""" + ) + check res.isErr() + + test "JBool maps to Option[bool] field": + let res = parseConfJson("""{"mode": "Core", "overrides": {"rlnRelay": true}}""") + require res.isOk() + let conf = res.get() + check conf.rlnRelay == some(true) + +suite "WakuNodeConfOverlay structure": + proc fieldNamesOfWakuNodeConf(): seq[string] = + var c: WakuNodeConf + for name, _ in fieldPairs(c): + result.add(name) + + proc fieldNamesOfOverlay(): seq[string] = + var o: WakuNodeConfOverlay + for name, _ in fieldPairs(o): + result.add(name) + + test "Overlay field names match WakuNodeConf minus excludes": + let expected = + fieldNamesOfWakuNodeConf().filterIt(it notin WakuNodeConfOverlayExcludes) + let actual = fieldNamesOfOverlay() + check sorted(actual) == sorted(expected) + + test "Every overlay field is Option-typed": + var o: WakuNodeConfOverlay + var allOption = true + for _, value in fieldPairs(o): + when typeof(value) isnot Option: + allOption = false + check allOption + + test "Excluded names are absent from overlay": + let actual = fieldNamesOfOverlay() + for excluded in WakuNodeConfOverlayExcludes: + check excluded notin actual diff --git a/tests/api/test_node_conf.nim b/tests/api/test_node_conf.nim index e171c5207..a29d9c96f 100644 --- a/tests/api/test_node_conf.nim +++ b/tests/api/test_node_conf.nim @@ -37,7 +37,7 @@ suite "WakuNodeConf - mode-driven toWakuConf": var conf = defaultWakuNodeConf().valueOr: raiseAssert error conf.mode = Core - conf.clusterId = 1 + conf.clusterId = some(1'u16) ## When let wakuConfRes = conf.toWakuConf() @@ -58,7 +58,7 @@ suite "WakuNodeConf - mode-driven toWakuConf": var conf = defaultWakuNodeConf().valueOr: raiseAssert error conf.mode = Edge - conf.clusterId = 1 + conf.clusterId = some(1'u16) ## When let wakuConfRes = conf.toWakuConf() @@ -81,7 +81,7 @@ suite "WakuNodeConf - mode-driven toWakuConf": conf.mode = cli_args.WakuMode.noMode conf.relay = true conf.lightpush = false - conf.clusterId = 5 + conf.clusterId = some(5'u16) ## When let wakuConfRes = conf.toWakuConf() @@ -122,7 +122,7 @@ suite "WakuNodeConf - JSON parsing with fieldPairs": let conf = confRes.get() check: conf.mode == cli_args.WakuMode.noMode - conf.clusterId == 0 + conf.clusterId.isNone() conf.logLevel == logging.LogLevel.INFO test "JSON with mode and clusterId": @@ -134,7 +134,7 @@ suite "WakuNodeConf - JSON parsing with fieldPairs": let conf = confRes.get() check: conf.mode == Core - conf.clusterId == 42 + conf.clusterId == some(42'u16) test "JSON with Edge mode": ## Given / When @@ -165,7 +165,7 @@ suite "WakuNodeConf - JSON parsing with fieldPairs": require confRes.isOk() let conf = confRes.get() check: - conf.clusterId == 99 + conf.clusterId == some(99'u16) conf.numShardsInNetwork == 16 test "JSON with unknown fields is silently ignored": @@ -177,7 +177,7 @@ suite "WakuNodeConf - JSON parsing with fieldPairs": require confRes.isOk() let conf = confRes.get() check: - conf.clusterId == 5 + conf.clusterId == some(5'u16) test "Invalid JSON syntax returns error": ## Given / When diff --git a/tests/factory/test_waku_conf.nim b/tests/factory/test_waku_conf.nim index 885e22867..a458e17db 100644 --- a/tests/factory/test_waku_conf.nim +++ b/tests/factory/test_waku_conf.nim @@ -206,8 +206,9 @@ suite "Waku Conf - build with cluster conf": assert conf.rlnRelayConf.isSome let rlnRelayConf = conf.rlnRelayConf.get() - check rlnRelayConf.ethContractAddress.string == - networkConf.rlnRelayEthContractAddress + # actually match the explicit contractAddress, which is the value set on the builder above + # this proves that an explicit builder call wins over the same field set via the preset + check rlnRelayConf.ethContractAddress.string == contractAddress check rlnRelayConf.dynamic == networkConf.rlnRelayDynamic check rlnRelayConf.chainId == networkConf.rlnRelayChainId check rlnRelayConf.epochSizeSec == networkConf.rlnEpochSizeSec @@ -247,10 +248,6 @@ suite "Waku Conf - build with cluster conf": let networkConf = NetworkConf.LogosDevConf() var builder = WakuConfBuilder.init() builder.withNetworkConf(networkConf) - # Note: builder.withNumShardsInCluster() is not called when the - # value that comes from the CLI path is 0 (which means it was - # either set to 0 or was left unset). - builder.withShardingConf(StaticSharding) ## When let conf = builder.build().expect("build should succeed") diff --git a/tests/test_waku.nim b/tests/test_waku.nim index cf5675716..f3df8295f 100644 --- a/tests/test_waku.nim +++ b/tests/test_waku.nim @@ -4,21 +4,22 @@ import chronos, testutils/unittests, std/options import waku import tools/confutils/cli_args +import waku/factory/networks_config suite "Waku API - Create node": asyncTest "Create node with minimal configuration": ## Given var nodeConf = defaultWakuNodeConf().valueOr: - raiseAssert error + raiseAssert "defaultWakuNodeConf failed: " & error nodeConf.mode = Core - nodeConf.clusterId = 3'u16 + nodeConf.clusterId = some(3'u16) nodeConf.rest = false # This is the actual minimal config but as the node auto-start, it is not suitable for tests ## When let node = (await createNode(nodeConf)).valueOr: - raiseAssert error + raiseAssert "createNode (minimal config) failed: " & error ## Then check: @@ -29,9 +30,9 @@ suite "Waku API - Create node": asyncTest "Create node with full configuration": ## Given var nodeConf = defaultWakuNodeConf().valueOr: - raiseAssert error + raiseAssert "defaultWakuNodeConf failed: " & error nodeConf.mode = Core - nodeConf.clusterId = 99'u16 + nodeConf.clusterId = some(99'u16) nodeConf.rest = false nodeConf.numShardsInNetwork = 16 nodeConf.maxMessageSize = "1024 KiB" @@ -44,7 +45,7 @@ suite "Waku API - Create node": ## When let node = (await createNode(nodeConf)).valueOr: - raiseAssert error + raiseAssert "createNode (full config) failed: " & error ## Then check: @@ -61,9 +62,9 @@ suite "Waku API - Create node": asyncTest "Create node with mixed entry nodes (enrtree, multiaddr)": ## Given var nodeConf = defaultWakuNodeConf().valueOr: - raiseAssert error + raiseAssert "defaultWakuNodeConf failed: " & error nodeConf.mode = Core - nodeConf.clusterId = 42'u16 + nodeConf.clusterId = some(42'u16) nodeConf.rest = false nodeConf.entryNodes = @[ "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", @@ -72,7 +73,7 @@ suite "Waku API - Create node": ## When let node = (await createNode(nodeConf)).valueOr: - raiseAssert error + raiseAssert "createNode (mixed entry nodes) failed: " & error ## Then check: @@ -86,3 +87,89 @@ suite "Waku API - Create node": node.conf.staticNodes.len == 1 node.conf.staticNodes[0] == "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc" + + asyncTest "Create node via messaging API with overrides": + let + clusterId = 3'u16 + numShards = 1'u16 + let overrides = WakuNodeConfOverlay( + clusterId: some(clusterId), rest: some(false), numShardsInNetwork: some(numShards) + ) + + let node = (await createNode(mode = cli_args.WakuMode.Core, overrides = overrides)).valueOr: + raiseAssert "createNode (overrides only) failed: " & error + + check: + not node.isNil() + node.conf.clusterId == clusterId + node.conf.shardingConf.numShardsInCluster == numShards + + asyncTest "Create node via messaging API with overrides + additions": + let + clusterId = 7'u16 + staticnode = + "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc" + let overrides = WakuNodeConfOverlay( + clusterId: some(clusterId), rest: some(false), numShardsInNetwork: some(1'u16) + ) + let additions = WakuNodeConfOverlay(staticnodes: some(@[staticnode])) + + let node = ( + await createNode( + mode = cli_args.WakuMode.Core, overrides = overrides, additions = additions + ) + ).valueOr: + raiseAssert "createNode (overrides + additions) failed: " & error + + check: + not node.isNil() + node.conf.clusterId == clusterId + node.conf.staticNodes.len == 1 + node.conf.staticNodes[0] == staticnode + + asyncTest "Create node via messaging API with preset": + let + preset = "twn" + twn = NetworkConf.TheWakuNetworkConf() + let overrides = WakuNodeConfOverlay(rest: some(false)) + + let node = ( + await createNode( + preset = preset, mode = cli_args.WakuMode.Edge, overrides = overrides + ) + ).valueOr: + raiseAssert "createNode (preset = " & preset & ") failed: " & error + + check: + not node.isNil() + node.conf.clusterId == twn.clusterId + node.conf.shardingConf.kind == twn.shardingConf.kind + node.conf.shardingConf.numShardsInCluster == twn.shardingConf.numShardsInCluster + node.conf.discv5Conf.isSome() + node.conf.discv5Conf.get().bootstrapNodes.len == twn.discv5BootstrapNodes.len + + asyncTest "Create node via messaging API: additions concat with preset's bootstrap nodes": + let + preset = "twn" + twn = NetworkConf.TheWakuNetworkConf() + addedBootstrapNode = + "enr:-QESuED0qW1BCmF-oH_ARGPr97Nv767bl_43uoy70vrbah3EaCAdK3Q0iRQ6wkSTTpdrg_dU_NC2ydO8leSlRpBX4pxiAYJpZIJ2NIJpcIRA4VDAim11bHRpYWRkcnO4XAArNiZub2RlLTAxLmRvLWFtczMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwAtNiZub2RlLTAxLmRvLWFtczMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQOTd-h5owwj-cx7xrmbvQKU8CV3Fomfdvcv1MBc-67T5oN0Y3CCdl-DdWRwgiMohXdha3UyDw" + let overrides = WakuNodeConfOverlay(rest: some(false)) + let additions = + WakuNodeConfOverlay(discv5BootstrapNodes: some(@[addedBootstrapNode])) + + let node = ( + await createNode( + preset = preset, + mode = cli_args.WakuMode.Edge, + overrides = overrides, + additions = additions, + ) + ).valueOr: + raiseAssert "createNode (preset = " & preset & " + additions) failed: " & error + + check: + not node.isNil() + node.conf.discv5Conf.isSome() + node.conf.discv5Conf.get().bootstrapNodes.len == twn.discv5BootstrapNodes.len + 1 + node.conf.discv5Conf.get().bootstrapNodes.contains(addedBootstrapNode) diff --git a/tools/confutils/cli_args.nim b/tools/confutils/cli_args.nim index d63b5880c..b6ed628b7 100644 --- a/tools/confutils/cli_args.nim +++ b/tools/confutils/cli_args.nim @@ -43,7 +43,7 @@ logScope: topics = "waku cli args" # Git version in git describe format (defined at compile time) -const git_version* {.strdefine.} = "n/a" +const git_version* {.strdefine.} = "(unknown)" type ConfResult*[T] = Result[T, string] @@ -117,20 +117,23 @@ type WakuNodeConf* = object name: "rln-relay-eth-private-key" .}: string - # TODO: Remove "Default is" when it's already visible on the CLI + # Option-typed; desc states the default since the CLI can't auto-show it for none(). rlnRelayUserMessageLimit* {. desc: - "Set a user message limit for the rln membership registration. Must be a positive integer. Default is 1.", - defaultValue: 1, + "Set a user message limit for the rln membership registration. Must be a positive integer. Default is " & + $DefaultRlnRelayUserMessageLimit & ".", + defaultValue: none(uint64), name: "rln-relay-user-message-limit" - .}: uint64 + .}: Option[uint64] + # Option-typed; desc states the default since the CLI can't auto-show it for none(). rlnEpochSizeSec* {. desc: - "Epoch size in seconds used to rate limit RLN memberships. Default is 1 second.", - defaultValue: 1, + "Epoch size in seconds used to rate limit RLN memberships. Default is " & + $DefaultRlnRelayEpochSizeSec & " second.", + defaultValue: none(uint64), name: "rln-relay-epoch-sec" - .}: uint64 + .}: Option[uint64] maxMessageSize* {. desc: @@ -170,15 +173,18 @@ type WakuNodeConf* = object name: "preset" .}: string + # Option-typed; desc states the default since the CLI can't auto-show it for none(). clusterId* {. - desc: - "Cluster id that the node is running in. Node in a different cluster id is disconnected.", - defaultValue: 0, + desc: static( + "Cluster id that the node is running in. Node in a different cluster id is disconnected. Default is " & + $DefaultClusterId & "." + ), + defaultValue: none(uint16), name: "cluster-id" - .}: uint16 + .}: Option[uint16] agentString* {. - defaultValue: "logos-delivery-" & cli_args.git_version, + defaultValue: "logos-delivery " & cli_args.git_version, desc: "Node agent string which is used as identifier in network", name: "agent-string" .}: string @@ -291,11 +297,14 @@ hence would have reachability issues.""", name: "relay-shard-manager" .}: bool + # Option-typed; desc states the default since the CLI can't auto-show it for none(). rlnRelay* {. - desc: "Enable spam protection through rln-relay: true|false.", - defaultValue: false, + desc: + "Enable spam protection through rln-relay: true|false. Default is " & + $DefaultRlnRelayEnabled & ".", + defaultValue: none(bool), name: "rln-relay" - .}: bool + .}: Option[bool] rlnRelayCredIndex* {. desc: "the index of the onchain commitment to use", @@ -304,9 +313,9 @@ hence would have reachability issues.""", rlnRelayDynamic* {. desc: "Enable waku-rln-relay with on-chain dynamic group management: true|false.", - defaultValue: false, + defaultValue: none(bool), name: "rln-relay-dynamic" - .}: bool + .}: Option[bool] entryNodes* {. desc: @@ -466,13 +475,14 @@ hence would have reachability issues.""", .}: string ## Reliability config + # Option-typed; desc states the default since the CLI can't auto-show it for none(). reliabilityEnabled* {. desc: - """Adds an extra effort in the delivery/reception of messages by leveraging store-v3 requests. -with the drawback of consuming some more bandwidth.""", - defaultValue: true, + """Adds an extra effort in the delivery/reception of messages by leveraging store-v3 requests, with the drawback of consuming some more bandwidth. Default is """ & + $DefaultP2pReliability & ".", + defaultValue: none(bool), name: "reliability" - .}: bool + .}: Option[bool] ## REST HTTP config rest* {. @@ -557,8 +567,11 @@ with the drawback of consuming some more bandwidth.""", .}: string ## Discovery v5 config + # Option-typed; desc states the default since the CLI can't auto-show it for none(). discv5Discovery* {. - desc: "Enable discovering nodes via Node Discovery v5.", + desc: + "Enable discovering nodes via Node Discovery v5. Default is " & + $DefaultDiscv5Enabled & ".", defaultValue: none(bool), name: "discv5-discovery" .}: Option[bool] @@ -627,8 +640,12 @@ with the drawback of consuming some more bandwidth.""", .}: bool #Mix config - mix* {.desc: "Enable mix protocol: true|false", defaultValue: false, name: "mix".}: - bool + # Option-typed; desc states the default since the CLI can't auto-show it for none(). + mix* {. + desc: "Enable mix protocol: true|false. Default is " & $DefaultMix & ".", + defaultValue: none(bool), + name: "mix" + .}: Option[bool] mixkey* {. desc: @@ -643,12 +660,14 @@ with the drawback of consuming some more bandwidth.""", .}: seq[MixNodePubInfo] # Kademlia Discovery config + # Option-typed; desc states the default since the CLI can't auto-show it for none(). enableKadDiscovery* {. desc: - "Enable extended kademlia discovery. Can be enabled without bootstrap nodes for the first node in the network.", - defaultValue: false, + "Enable extended kademlia discovery. Can be enabled without bootstrap nodes for the first node in the network. Default is " & + $DefaultKadEnabled & ".", + defaultValue: none(bool), name: "enable-kad-discovery" - .}: bool + .}: Option[bool] kadBootstrapNodes* {. desc: @@ -913,7 +932,7 @@ proc toKeystoreGeneratorConf*(n: WakuNodeConf): RlnKeystoreGeneratorConf = chainId: UInt256.fromBytesBE(n.rlnRelayChainId.toBytesBE()), ethClientUrls: n.ethClientUrls.mapIt(string(it)), ethContractAddress: n.rlnRelayEthContractAddress, - userMessageLimit: n.rlnRelayUserMessageLimit, + userMessageLimit: n.rlnRelayUserMessageLimit.get(DefaultRlnRelayUserMessageLimit), ethPrivateKey: n.rlnRelayEthPrivateKey, credPath: n.rlnRelayCredPath, credPassword: n.rlnRelayCredPassword, @@ -947,7 +966,7 @@ proc toNetworkConf( proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = var b = WakuConfBuilder.init() - let networkConf = toNetworkConf(n.preset, some(n.clusterId)).valueOr: + let networkConf = toNetworkConf(n.preset, n.clusterId).valueOr: return err("Error determining cluster from preset: " & $error) if networkConf.isSome(): @@ -956,7 +975,8 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = b.withLogLevel(n.logLevel) b.withLogFormat(n.logFormat) - b.rlnRelayConf.withEnabled(n.rlnRelay) + if n.rlnRelay.isSome(): + b.rlnRelayConf.withEnabled(n.rlnRelay.get()) if n.rlnRelayCredPath != "": b.rlnRelayConf.withCredPath(n.rlnRelayCredPath) if n.rlnRelayCredPassword != "": @@ -968,18 +988,22 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = if n.rlnRelayChainId != 0: b.rlnRelayConf.withChainId(n.rlnRelayChainId) - b.rlnRelayConf.withUserMessageLimit(n.rlnRelayUserMessageLimit) - b.rlnRelayConf.withEpochSizeSec(n.rlnEpochSizeSec) + if n.rlnRelayUserMessageLimit.isSome(): + b.rlnRelayConf.withUserMessageLimit(n.rlnRelayUserMessageLimit.get()) + if n.rlnEpochSizeSec.isSome(): + b.rlnRelayConf.withEpochSizeSec(n.rlnEpochSizeSec.get()) if n.rlnRelayCredIndex.isSome(): b.rlnRelayConf.withCredIndex(n.rlnRelayCredIndex.get()) - b.rlnRelayConf.withDynamic(n.rlnRelayDynamic) + if n.rlnRelayDynamic.isSome(): + b.rlnRelayConf.withDynamic(n.rlnRelayDynamic.get()) if n.maxMessageSize != "": b.withMaxMessageSize(n.maxMessageSize) b.withProtectedShards(n.protectedShards) - b.withClusterId(n.clusterId) + if n.clusterId.isSome(): + b.withClusterId(n.clusterId.get()) b.withAgentString(n.agentString) @@ -1033,7 +1057,7 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = if n.numShardsInNetwork != 0: b.withNumShardsInCluster(n.numShardsInNetwork) b.withShardingConf(AutoSharding) - else: + elif networkConf.isNone(): b.withShardingConf(StaticSharding) # It is not possible to pass an empty sequence on the CLI @@ -1066,9 +1090,10 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = b.storeServiceConf.storeSyncConf.withRangeSec(n.storeSyncRange) b.storeServiceConf.storeSyncConf.withRelayJitterSec(n.storeSyncRelayJitter) - b.mixConf.withEnabled(n.mix) + if n.mix.isSome(): + b.mixConf.withEnabled(n.mix.get()) + b.withMix(n.mix.get()) b.mixConf.withMixNodes(n.mixnodes) - b.withMix(n.mix) if n.mixkey.isSome(): b.mixConf.withMixKey(n.mixkey.get()) @@ -1078,7 +1103,8 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = b.filterServiceConf.withMaxCriteria(n.filterMaxCriteria) b.withLightPush(n.lightpush) - b.withP2pReliability(n.reliabilityEnabled) + if n.reliabilityEnabled.isSome(): + b.withP2pReliability(n.reliabilityEnabled.get()) b.restServerConf.withEnabled(n.rest) b.restServerConf.withListenAddress(n.restAddress) @@ -1119,7 +1145,8 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = if n.rateLimits.len > 0: b.rateLimitConf.withRateLimits(n.rateLimits) - b.kademliaDiscoveryConf.withEnabled(n.enableKadDiscovery) + if n.enableKadDiscovery.isSome(): + b.kademliaDiscoveryConf.withEnabled(n.enableKadDiscovery.get()) b.kademliaDiscoveryConf.withBootstrapNodes(n.kadBootstrapNodes) # Mode-driven configuration overrides diff --git a/tools/confutils/conf_from_json.nim b/tools/confutils/conf_from_json.nim new file mode 100644 index 000000000..2cc5976a1 --- /dev/null +++ b/tools/confutils/conf_from_json.nim @@ -0,0 +1,243 @@ +import std/[json, strutils, tables] +import confutils, confutils/std/net, results +import ./cli_args + +const + KeyMode = "mode" + KeyPreset = "preset" + KeyOverrides = "overrides" + KeyAdditions = "additions" + +const TopLevelOnlyKeys = [KeyMode, KeyPreset] + ## Keys that the messaging shape requires at the top-level of the JSON input. + ## They must not appear inside `overrides` or `additions`. + +proc collectJsonFields*( + jsonNode: JsonNode +): Result[Table[string, (string, JsonNode)], string] = + ## Walk the top-level JSON object and key it by lowercased names. + if jsonNode.kind != JObject: + return err("config JSON must be a JSON object, got " & $jsonNode.kind) + var jsonFields: Table[string, (string, JsonNode)] + for key, value in jsonNode: + let lowerKey = key.toLowerAscii() + if jsonFields.hasKey(lowerKey): + let firstKey = jsonFields[lowerKey][0] + return err( + "Duplicate configuration option (case-insensitive): '" & firstKey & "' and '" & + key & "'" + ) + jsonFields[lowerKey] = (key, value) + return ok(jsonFields) + +proc unknownKeysError( + jsonFields: Table[string, (string, JsonNode)], prefix: string +): string = + ## Format leftover JSON keys as an error message. + var keys = newSeq[string]() + for _, (jsonKey, _) in pairs(jsonFields): + keys.add(jsonKey) + return prefix & ": " & $keys + +proc rejectTopLevelOnlyKeysInside( + node: JsonNode, blockName: string +): Result[void, string] = + ## Error if `node` contains any key from `TopLevelOnlyKeys`. + for k, _ in node: + if k.toLowerAscii() in TopLevelOnlyKeys: + return err("'" & k & "' must be a top-level key, not inside '" & blockName & "'") + return ok() + +proc jsonScalarToString(node: JsonNode): Result[string, string] = + ## Convert a scalar JSON value to its string form. + case node.kind + of JString: + return ok(node.getStr()) + of JInt: + return ok($node.getInt()) + of JFloat: + return ok($node.getFloat()) + of JBool: + return ok($node.getBool()) + of JNull: + return ok("") + else: + return err("expected scalar JSON value, got " & $node.kind) + +proc applyJsonFieldsToConf( + conf: var WakuNodeConf, + jsonFields: var Table[string, (string, JsonNode)], + parseErrPrefix: string, + unknownErrPrefix: string, +): Result[void, string] = + ## Walk `conf`'s fields and write each one matched (case-insensitive) by + ## `jsonFields`. seq fields take a JArray (full replace); scalar fields + ## take any scalar JSON kind. Errors on leftover unknown keys. + for confField, confValue in fieldPairs(conf): + let lowerField = confField.toLowerAscii() + if jsonFields.hasKey(lowerField): + let (jsonKey, jsonValue) = jsonFields[lowerField] + when confValue is seq: + if jsonValue.kind != JArray: + return err( + parseErrPrefix & " '" & confField & "' from JSON key '" & jsonKey & + "' must be a JSON array" + ) + var newSeq: typeof(confValue) = @[] + for item in jsonValue: + let formattedItem = jsonScalarToString(item).valueOr: + return err( + parseErrPrefix & " '" & confField & "' from JSON key '" & jsonKey & "': " & + error + ) + try: + type ElemType = typeof(confValue[0]) + newSeq.add(parseCmdArg(ElemType, formattedItem)) + except CatchableError as e: + return err( + parseErrPrefix & " '" & confField & "' from JSON key '" & jsonKey & "': " & + e.msg & ". Value: " & formattedItem + ) + confValue = newSeq + else: + let formattedString = jsonScalarToString(jsonValue).valueOr: + return err( + parseErrPrefix & " '" & confField & "' from JSON key '" & jsonKey & "': " & + error + ) + try: + confValue = parseCmdArg(typeof(confValue), formattedString) + except CatchableError as e: + return err( + parseErrPrefix & " '" & confField & "' from JSON key '" & jsonKey & "': " & + e.msg & ". Value: " & formattedString + ) + jsonFields.del(lowerField) + if jsonFields.len > 0: + return err(unknownKeysError(jsonFields, unknownErrPrefix)) + return ok() + +proc applyJsonAsOverride*( + conf: var WakuNodeConf, overrides: JsonNode +): Result[void, string] = + ## Apply `overrides` JSON onto `conf` with replace semantics for both scalars and lists. + var jsonFields = ?collectJsonFields(overrides) + return applyJsonFieldsToConf( + conf, jsonFields, "Failed to parse override field", + "Unrecognized override field(s) found", + ) + +proc applyJsonAsAddition*( + conf: var WakuNodeConf, additions: JsonNode +): Result[void, string] = + ## Append JSON array in `additions` to `conf` seq fields. + var jsonFields = ?collectJsonFields(additions) + for confField, confValue in fieldPairs(conf): + let lowerField = confField.toLowerAscii() + if jsonFields.hasKey(lowerField): + let (jsonKey, jsonValue) = jsonFields[lowerField] + when confValue is seq: + if jsonValue.kind != JArray: + return err( + "Addition field '" & confField & "' from JSON key '" & jsonKey & + "' must be a JSON array" + ) + for item in jsonValue: + let formattedItem = jsonScalarToString(item).valueOr: + return err( + "Failed to parse addition item for field '" & confField & "': " & error + ) + try: + type ElemType = typeof(confValue[0]) + confValue.add(parseCmdArg(ElemType, formattedItem)) + except CatchableError as e: + return err( + "Failed to parse addition item for field '" & confField & "': " & e.msg & + ". Value: " & formattedItem + ) + else: + return err( + "Field '" & confField & "' from JSON key '" & jsonKey & + "' is not a list and cannot be in additions" + ) + jsonFields.del(lowerField) + if jsonFields.len > 0: + return err(unknownKeysError(jsonFields, "Unrecognized addition field(s) found")) + return ok() + +proc assembleMessagingConf*( + jsonFields: Table[string, (string, JsonNode)] +): Result[WakuNodeConf, string] = + ## Build a WakuNodeConf from the messaging shape + ## `{mode, overrides, preset?, additions?}`. `mode` and `overrides` are + ## required. Order: overrides applied first, then additions concat. + var conf = ?defaultWakuNodeConf() + var fields = jsonFields + + if not fields.hasKey(KeyMode): + return err("messaging shape requires '" & KeyMode & "' key") + if not fields.hasKey(KeyOverrides): + return err("messaging shape requires '" & KeyOverrides & "' key") + + let modeStr = jsonScalarToString(fields[KeyMode][1]).valueOr: + return err("Failed to parse '" & KeyMode & "': " & error) + try: + conf.mode = parseCmdArg(WakuMode, modeStr) + except CatchableError as e: + return err("Failed to parse '" & KeyMode & "': " & e.msg & ". Value: " & modeStr) + fields.del(KeyMode) + + if fields.hasKey(KeyPreset): + let presetStr = jsonScalarToString(fields[KeyPreset][1]).valueOr: + return err("Failed to parse '" & KeyPreset & "': " & error) + conf.preset = presetStr + fields.del(KeyPreset) + + let overridesNode = fields[KeyOverrides][1] + if overridesNode.kind != JObject: + return err("'" & KeyOverrides & "' must be a JSON object") + ?rejectTopLevelOnlyKeysInside(overridesNode, KeyOverrides) + ?applyJsonAsOverride(conf, overridesNode) + fields.del(KeyOverrides) + + if fields.hasKey(KeyAdditions): + let additionsNode = fields[KeyAdditions][1] + if additionsNode.kind != JObject: + return err("'" & KeyAdditions & "' must be a JSON object") + ?rejectTopLevelOnlyKeysInside(additionsNode, KeyAdditions) + ?applyJsonAsAddition(conf, additionsNode) + fields.del(KeyAdditions) + + if fields.len > 0: + return + err(unknownKeysError(fields, "Unrecognized top-level key(s) in messaging shape")) + + return ok(conf) + +proc assembleFullConf*( + jsonFields: Table[string, (string, JsonNode)] +): Result[WakuNodeConf, string] = + ## Build a WakuNodeConf from a flat JSON object whose keys are WakuNodeConf field names. + var conf = ?defaultWakuNodeConf() + var fields = jsonFields + ?applyJsonFieldsToConf( + conf, fields, "Failed to parse field", "Unrecognized configuration option(s) found" + ) + return ok(conf) + +proc parseConfJson*(jsonStr: string): Result[WakuNodeConf, string] = + ## Parse a JSON config, route to messaging or full-config shape based on + ## whether `overrides` or `additions` fields are in the config object top-level. + var jsonNode: JsonNode + try: + jsonNode = parseJson(jsonStr) + except CatchableError as e: + return err("Failed to parse config JSON: " & e.msg) + + let jsonFields = ?collectJsonFields(jsonNode) + let isMessagingShape = + jsonFields.hasKey(KeyOverrides) or jsonFields.hasKey(KeyAdditions) + if isMessagingShape: + return assembleMessagingConf(jsonFields) + else: + return assembleFullConf(jsonFields) diff --git a/tools/confutils/messaging_conf.nim b/tools/confutils/messaging_conf.nim new file mode 100644 index 000000000..5166801e8 --- /dev/null +++ b/tools/confutils/messaging_conf.nim @@ -0,0 +1,37 @@ +{.push raises: [].} + +import std/options +import results +import ./cli_args +import ./optionalize + +const WakuNodeConfOverlayExcludes* = ["cmd", "execute", "mode", "preset"] + +# Generates the WakuNodeConfOverlay type from the WakuNodeConf type. +# The generated type converts fields from type T to Option[T] if T != Option. +# Skips fields that are in WakuNodeConfOverlayExcludes. +optionalizeType(WakuNodeConfOverlay, WakuNodeConf, WakuNodeConfOverlayExcludes) + +proc init*(T: type WakuNodeConfOverlay): WakuNodeConfOverlay = + ## Default config overlay where every field is `none`. + return WakuNodeConfOverlay() + +proc applyAsOverride*(conf: var WakuNodeConf, overlay: WakuNodeConfOverlay) = + ## For all fields, overlay.some() overrides field of same name in conf. + for confName, confValue in fieldPairs(conf): + for ovName, ovValue in fieldPairs(overlay): + when confName == ovName: + if ovValue.isSome(): + when typeof(confValue) is Option: + confValue = ovValue + else: + confValue = ovValue.get() + +proc applyAsAddition*(conf: var WakuNodeConf, overlay: WakuNodeConfOverlay) = + ## For all seq fields, overlay.some() concats to field of same name in conf. + for confName, confValue in fieldPairs(conf): + for ovName, ovValue in fieldPairs(overlay): + when confName == ovName: + when typeof(confValue) is seq: + if ovValue.isSome() and ovValue.get().len > 0: + confValue = confValue & ovValue.get() diff --git a/tools/confutils/optionalize.nim b/tools/confutils/optionalize.nim new file mode 100644 index 000000000..8ad6ffdec --- /dev/null +++ b/tools/confutils/optionalize.nim @@ -0,0 +1,74 @@ +{.push raises: [].} + +import std/[macros, options] + +proc isOptionType(n: NimNode): bool = + if n.kind == nnkBracketExpr and n.len >= 1: + let head = n[0] + return head.eqIdent("Option") + return false + +proc unwrapName(n: NimNode): NimNode = + var cur = n + if cur.kind == nnkPragmaExpr: + cur = cur[0] + if cur.kind == nnkPostfix: + cur = cur[1] + return cur + +proc collectFields(rec: NimNode, target: NimNode, excluded: seq[string]) = + for child in rec: + case child.kind + of nnkIdentDefs: + let nameNode = child[0] + let fieldType = child[^2] + let plainName = unwrapName(nameNode) + if plainName.kind notin {nnkIdent, nnkSym}: + continue + if $plainName in excluded: + continue + let newType = + if isOptionType(fieldType): + fieldType + else: + nnkBracketExpr.newTree(ident("Option"), fieldType) + let exported = postfix(ident($plainName), "*") + target.add(newIdentDefs(exported, newType, newEmptyNode())) + of nnkRecCase: + for branch in child[1 ..^ 1]: + case branch.kind + of nnkOfBranch: + collectFields(branch[^1], target, excluded) + of nnkElse: + collectFields(branch[0], target, excluded) + else: + discard + of nnkRecList: + collectFields(child, target, excluded) + else: + discard + +macro optionalizeType*( + newName: untyped, source: typedesc, exclude: static[openArray[string]] = [] +): untyped = + var typImpl = source.getTypeImpl + if typImpl.kind == nnkBracketExpr and typImpl.len >= 2: + typImpl = typImpl[1].getTypeImpl + if typImpl.kind != nnkObjectTy: + error("optionalizeType: expected object type, got " & $typImpl.kind, source) + + var excluded: seq[string] = @[] + for e in exclude: + excluded.add(e) + + let recList = typImpl[2] + let newRecList = newNimNode(nnkRecList) + collectFields(recList, newRecList, excluded) + + let typeDef = nnkTypeDef.newTree( + postfix(newName, "*"), + newEmptyNode(), + nnkObjectTy.newTree(newEmptyNode(), newEmptyNode(), newRecList), + ) + + result = nnkTypeSection.newTree(typeDef) diff --git a/waku/api/api.nim b/waku/api/api.nim index 1eee982fd..15046f907 100644 --- a/waku/api/api.nim +++ b/waku/api/api.nim @@ -1,4 +1,4 @@ -import chronicles, chronos, results +import chronicles, chronos, results, std/options import waku/factory/waku import waku/[requests/health_requests, waku_core, waku_node] @@ -6,9 +6,10 @@ import waku/node/delivery_service/send_service import waku/node/delivery_service/subscription_manager import libp2p/peerid import ../../tools/confutils/cli_args +import ../../tools/confutils/messaging_conf import ./[api_conf, types] -export cli_args +export cli_args, messaging_conf logScope: topics = "api" @@ -24,6 +25,20 @@ proc createNode*(conf: WakuNodeConf): Future[Result[Waku, string]] {.async.} = return ok(wakuRes) +proc createNode*( + preset = "", + mode = cli_args.WakuMode.Core, + overrides = WakuNodeConfOverlay.init(), + additions = WakuNodeConfOverlay.init(), +): Future[Result[Waku, string]] {.async.} = + var conf = defaultWakuNodeConf().valueOr: + return err("Failed creating default conf: " & error) + conf.mode = mode + conf.preset = preset + applyAsOverride(conf, overrides) + applyAsAddition(conf, additions) + return await createNode(conf) + proc checkApiAvailability(w: Waku): Result[void, string] = if w.isNil(): return err("Waku node is not initialized") diff --git a/waku/factory/conf_builder/discv5_conf_builder.nim b/waku/factory/conf_builder/discv5_conf_builder.nim index c87901ea3..a8c6deeba 100644 --- a/waku/factory/conf_builder/discv5_conf_builder.nim +++ b/waku/factory/conf_builder/discv5_conf_builder.nim @@ -4,6 +4,14 @@ import ../waku_conf logScope: topics = "waku conf builder discv5" +const + DefaultDiscv5Enabled*: bool = false + DefaultDiscv5BitsPerHop*: int = 1 + DefaultDiscv5BucketIpLimit*: uint = 2 + DefaultDiscv5EnrAutoUpdate*: bool = true + DefaultDiscv5TableIpLimit*: uint = 10 + DefaultDiscv5UdpPort*: Port = Port(0) + ########################### ## Discv5 Config Builder ## ########################### @@ -46,18 +54,18 @@ proc withBootstrapNodes*(b: var Discv5ConfBuilder, bootstrapNodes: seq[string]) b.bootstrapNodes = concat(b.bootstrapNodes, bootstrapNodes) proc build*(b: Discv5ConfBuilder): Result[Option[Discv5Conf], string] = - if not b.enabled.get(false): + if not b.enabled.get(DefaultDiscv5Enabled): return ok(none(Discv5Conf)) return ok( some( Discv5Conf( bootstrapNodes: b.bootstrapNodes, - bitsPerHop: b.bitsPerHop.get(1), - bucketIpLimit: b.bucketIpLimit.get(2), - enrAutoUpdate: b.enrAutoUpdate.get(true), - tableIpLimit: b.tableIpLimit.get(10), - udpPort: b.udpPort.get(Port(0)), + bitsPerHop: b.bitsPerHop.get(DefaultDiscv5BitsPerHop), + bucketIpLimit: b.bucketIpLimit.get(DefaultDiscv5BucketIpLimit), + enrAutoUpdate: b.enrAutoUpdate.get(DefaultDiscv5EnrAutoUpdate), + tableIpLimit: b.tableIpLimit.get(DefaultDiscv5TableIpLimit), + udpPort: b.udpPort.get(DefaultDiscv5UdpPort), ) ) ) diff --git a/waku/factory/conf_builder/filter_service_conf_builder.nim b/waku/factory/conf_builder/filter_service_conf_builder.nim index 0a6617430..11efc84db 100644 --- a/waku/factory/conf_builder/filter_service_conf_builder.nim +++ b/waku/factory/conf_builder/filter_service_conf_builder.nim @@ -4,6 +4,12 @@ import ../waku_conf logScope: topics = "waku conf builder filter service" +const + DefaultFilterEnabled*: bool = false + DefaultFilterMaxPeersToServe*: uint32 = 500 + DefaultFilterSubscriptionTimeout*: uint16 = 300 + DefaultFilterMaxCriteria*: uint32 = 1000 + ################################### ## Filter Service Config Builder ## ################################### @@ -37,15 +43,15 @@ proc withMaxCriteria*(b: var FilterServiceConfBuilder, maxCriteria: uint32) = b.maxCriteria = some(maxCriteria) proc build*(b: FilterServiceConfBuilder): Result[Option[FilterServiceConf], string] = - if not b.enabled.get(false): + if not b.enabled.get(DefaultFilterEnabled): return ok(none(FilterServiceConf)) return ok( some( FilterServiceConf( - maxPeersToServe: b.maxPeersToServe.get(500), - subscriptionTimeout: b.subscriptionTimeout.get(300), - maxCriteria: b.maxCriteria.get(1000), + maxPeersToServe: b.maxPeersToServe.get(DefaultFilterMaxPeersToServe), + subscriptionTimeout: b.subscriptionTimeout.get(DefaultFilterSubscriptionTimeout), + maxCriteria: b.maxCriteria.get(DefaultFilterMaxCriteria), ) ) ) diff --git a/waku/factory/conf_builder/kademlia_discovery_conf_builder.nim b/waku/factory/conf_builder/kademlia_discovery_conf_builder.nim index 916d71be1..135663086 100644 --- a/waku/factory/conf_builder/kademlia_discovery_conf_builder.nim +++ b/waku/factory/conf_builder/kademlia_discovery_conf_builder.nim @@ -5,18 +5,20 @@ import waku/factory/waku_conf logScope: topics = "waku conf builder kademlia discovery" +const DefaultKadEnabled*: bool = false + ####################################### ## Kademlia Discovery Config Builder ## ####################################### type KademliaDiscoveryConfBuilder* = object - enabled*: bool + enabled*: Option[bool] bootstrapNodes*: seq[string] proc init*(T: type KademliaDiscoveryConfBuilder): KademliaDiscoveryConfBuilder = KademliaDiscoveryConfBuilder() proc withEnabled*(b: var KademliaDiscoveryConfBuilder, enabled: bool) = - b.enabled = enabled + b.enabled = some(enabled) proc withBootstrapNodes*( b: var KademliaDiscoveryConfBuilder, bootstrapNodes: seq[string] @@ -27,7 +29,7 @@ proc build*( b: KademliaDiscoveryConfBuilder ): Result[Option[KademliaDiscoveryConf], string] = # Kademlia is enabled if explicitly enabled OR if bootstrap nodes are provided - let enabled = b.enabled or b.bootstrapNodes.len > 0 + let enabled = b.enabled.get(DefaultKadEnabled) or b.bootstrapNodes.len > 0 if not enabled: return ok(none(KademliaDiscoveryConf)) diff --git a/waku/factory/conf_builder/metrics_server_conf_builder.nim b/waku/factory/conf_builder/metrics_server_conf_builder.nim index 29ba035a2..dfade602f 100644 --- a/waku/factory/conf_builder/metrics_server_conf_builder.nim +++ b/waku/factory/conf_builder/metrics_server_conf_builder.nim @@ -4,6 +4,12 @@ import ../waku_conf logScope: topics = "waku conf builder metrics server" +const + DefaultMetricsEnabled*: bool = false + DefaultMetricsHttpAddress*: IpAddress = static parseIpAddress("127.0.0.1") + DefaultMetricsHttpPort*: Port = Port(0) + DefaultMetricsLogging*: bool = false + ################################### ## Metrics Server Config Builder ## ################################### @@ -33,15 +39,15 @@ proc withLogging*(b: var MetricsServerConfBuilder, logging: bool) = b.logging = some(logging) proc build*(b: MetricsServerConfBuilder): Result[Option[MetricsServerConf], string] = - if not b.enabled.get(false): + if not b.enabled.get(DefaultMetricsEnabled): return ok(none(MetricsServerConf)) return ok( some( MetricsServerConf( - httpAddress: b.httpAddress.get(static parseIpAddress("127.0.0.1")), - httpPort: b.httpPort.get(Port(0)), - logging: b.logging.get(false), + httpAddress: b.httpAddress.get(DefaultMetricsHttpAddress), + httpPort: b.httpPort.get(DefaultMetricsHttpPort), + logging: b.logging.get(DefaultMetricsLogging), ) ) ) diff --git a/waku/factory/conf_builder/mix_conf_builder.nim b/waku/factory/conf_builder/mix_conf_builder.nim index 145ccb76e..1a832d352 100644 --- a/waku/factory/conf_builder/mix_conf_builder.nim +++ b/waku/factory/conf_builder/mix_conf_builder.nim @@ -5,6 +5,8 @@ import ../waku_conf, waku/waku_mix logScope: topics = "waku conf builder mix" +const DefaultMixEnabled*: bool = false + ################################## ## Mix Config Builder ## ################################## @@ -26,7 +28,7 @@ proc withMixNodes*(b: var MixConfBuilder, mixNodes: seq[MixNodePubInfo]) = b.mixNodes = mixNodes proc build*(b: MixConfBuilder): Result[Option[MixConf], string] = - if not b.enabled.get(false): + if not b.enabled.get(DefaultMixEnabled): return ok(none[MixConf]()) else: if b.mixKey.isSome(): diff --git a/waku/factory/conf_builder/rest_server_conf_builder.nim b/waku/factory/conf_builder/rest_server_conf_builder.nim index 5ad5b3a39..8c721c75d 100644 --- a/waku/factory/conf_builder/rest_server_conf_builder.nim +++ b/waku/factory/conf_builder/rest_server_conf_builder.nim @@ -4,6 +4,11 @@ import ../waku_conf logScope: topics = "waku conf builder rest server" +const + DefaultRestEnabled*: bool = false + DefaultRestPort*: Port = Port(0) + DefaultRestAdmin*: bool = false + ################################ ## REST Server Config Builder ## ################################ @@ -41,7 +46,7 @@ proc withRelayCacheCapacity*(b: var RestServerConfBuilder, relayCacheCapacity: u b.relayCacheCapacity = some(relayCacheCapacity) proc build*(b: RestServerConfBuilder): Result[Option[RestServerConf], string] = - if not b.enabled.get(false): + if not b.enabled.get(DefaultRestEnabled): return ok(none(RestServerConf)) if b.listenAddress.isNone(): @@ -54,8 +59,8 @@ proc build*(b: RestServerConfBuilder): Result[Option[RestServerConf], string] = RestServerConf( allowOrigin: b.allowOrigin, listenAddress: b.listenAddress.get(), - port: b.port.get(Port(0)), - admin: b.admin.get(false), + port: b.port.get(DefaultRestPort), + admin: b.admin.get(DefaultRestAdmin), relayCacheCapacity: b.relayCacheCapacity.get(), ) ) diff --git a/waku/factory/conf_builder/rln_relay_conf_builder.nim b/waku/factory/conf_builder/rln_relay_conf_builder.nim index 4cdcf8324..f4e70767e 100644 --- a/waku/factory/conf_builder/rln_relay_conf_builder.nim +++ b/waku/factory/conf_builder/rln_relay_conf_builder.nim @@ -4,6 +4,11 @@ import ../waku_conf logScope: topics = "waku conf builder rln relay" +const + DefaultRlnRelayEnabled*: bool = false + DefaultRlnRelayEpochSizeSec*: uint64 = 1 + DefaultRlnRelayUserMessageLimit*: uint64 = 1 + ############################## ## RLN Relay Config Builder ## ############################## @@ -56,7 +61,7 @@ proc withUserMessageLimit*(b: var RlnRelayConfBuilder, userMessageLimit: uint64) b.userMessageLimit = some(userMessageLimit) proc build*(b: RlnRelayConfBuilder): Result[Option[RlnRelayConf], string] = - if not b.enabled.get(false): + if not b.enabled.get(DefaultRlnRelayEnabled): return ok(none(RlnRelayConf)) if b.chainId.isNone(): @@ -78,11 +83,6 @@ proc build*(b: RlnRelayConfBuilder): Result[Option[RlnRelayConf], string] = return err("rlnRelay.ethClientUrls is not specified") if b.ethContractAddress.get("") == "": return err("rlnRelay.ethContractAddress is not specified") - if b.epochSizeSec.isNone(): - return err("rlnRelay.epochSizeSec is not specified") - if b.userMessageLimit.isNone(): - return err("rlnRelay.userMessageLimit is not specified") - return ok( some( RlnRelayConf( @@ -92,8 +92,8 @@ proc build*(b: RlnRelayConfBuilder): Result[Option[RlnRelayConf], string] = dynamic: b.dynamic.get(), ethClientUrls: b.ethClientUrls.get(), ethContractAddress: b.ethContractAddress.get(), - epochSizeSec: b.epochSizeSec.get(), - userMessageLimit: b.userMessageLimit.get(), + epochSizeSec: b.epochSizeSec.get(DefaultRlnRelayEpochSizeSec), + userMessageLimit: b.userMessageLimit.get(DefaultRlnRelayUserMessageLimit), ) ) ) diff --git a/waku/factory/conf_builder/store_service_conf_builder.nim b/waku/factory/conf_builder/store_service_conf_builder.nim index f1b0b1402..d1b51c9e5 100644 --- a/waku/factory/conf_builder/store_service_conf_builder.nim +++ b/waku/factory/conf_builder/store_service_conf_builder.nim @@ -5,6 +5,14 @@ import ../waku_conf, ./store_sync_conf_builder logScope: topics = "waku conf builder store service" +const + DefaultStoreEnabled*: bool = false + DefaultStoreDbMigration*: bool = true + DefaultStoreDbVacuum*: bool = false + DefaultStoreMaxNumDbConnections*: int = 50 + DefaultStoreResume*: bool = false + DefaultStoreRetentionPolicy*: string = "time:" & $2.days.seconds + ################################## ## Store Service Config Builder ## ################################## @@ -77,7 +85,7 @@ proc validateRetentionPolicies(policies: seq[string]): Result[void, string] = return ok() proc build*(b: StoreServiceConfBuilder): Result[Option[StoreServiceConf], string] = - if not b.enabled.get(false): + if not b.enabled.get(DefaultStoreEnabled): return ok(none(StoreServiceConf)) if b.dbUrl.get("") == "": @@ -88,7 +96,7 @@ proc build*(b: StoreServiceConfBuilder): Result[Option[StoreServiceConf], string let retentionPolicies = if b.retentionPolicies.len == 0: - @["time:" & $2.days.seconds] + @[DefaultStoreRetentionPolicy] else: validateRetentionPolicies(b.retentionPolicies).isOkOr: return err("invalid retention policies: " & error) @@ -97,12 +105,12 @@ proc build*(b: StoreServiceConfBuilder): Result[Option[StoreServiceConf], string return ok( some( StoreServiceConf( - dbMigration: b.dbMigration.get(true), + dbMigration: b.dbMigration.get(DefaultStoreDbMigration), dbURl: b.dbUrl.get(), - dbVacuum: b.dbVacuum.get(false), - maxNumDbConnections: b.maxNumDbConnections.get(50), + dbVacuum: b.dbVacuum.get(DefaultStoreDbVacuum), + maxNumDbConnections: b.maxNumDbConnections.get(DefaultStoreMaxNumDbConnections), retentionPolicies: retentionPolicies, - resume: b.resume.get(false), + resume: b.resume.get(DefaultStoreResume), storeSyncConf: storeSyncConf, ) ) diff --git a/waku/factory/conf_builder/store_sync_conf_builder.nim b/waku/factory/conf_builder/store_sync_conf_builder.nim index 4c7177b71..d47c199a4 100644 --- a/waku/factory/conf_builder/store_sync_conf_builder.nim +++ b/waku/factory/conf_builder/store_sync_conf_builder.nim @@ -4,6 +4,8 @@ import ../waku_conf logScope: topics = "waku conf builder store sync" +const DefaultStoreSyncEnabled*: bool = false + ################################## ## Store Sync Config Builder ## ################################## @@ -30,7 +32,7 @@ proc withRelayJitterSec*(b: var StoreSyncConfBuilder, relayJitterSec: uint32) = b.relayJitterSec = some(relayJitterSec) proc build*(b: StoreSyncConfBuilder): Result[Option[StoreSyncConf], string] = - if not b.enabled.get(false): + if not b.enabled.get(DefaultStoreSyncEnabled): return ok(none(StoreSyncConf)) if b.rangeSec.isNone(): diff --git a/waku/factory/conf_builder/waku_conf_builder.nim b/waku/factory/conf_builder/waku_conf_builder.nim index 40c6e5244..ef5da8b4a 100644 --- a/waku/factory/conf_builder/waku_conf_builder.nim +++ b/waku/factory/conf_builder/waku_conf_builder.nim @@ -13,6 +13,9 @@ import factory/networks_config, common/logging, common/utils/parse_size_units, + node/peer_manager, + waku_core/message/default_values, + waku_core/topics/pubsub_topic, waku_enr/capabilities, ], tools/confutils/entry_nodes @@ -34,7 +37,35 @@ import logScope: topics = "waku conf builder" -const DefaultMaxConnections* = 150 +# Picks up the same -d:git_version=... build flag that cli_args.nim defines. +const git_version {.strdefine.} = "(unknown)" + +const + DefaultMaxConnections* = 150 + DefaultRelay*: bool = false + DefaultLightPush*: bool = false + DefaultPeerExchange*: bool = false + DefaultStoreSyncMount*: bool = false + DefaultRendezvous*: bool = false + DefaultMix*: bool = false + DefaultRelayPeerExchange*: bool = false + DefaultLogLevel*: logging.LogLevel = logging.LogLevel.INFO + DefaultLogFormat*: logging.LogFormat = logging.LogFormat.TEXT + DefaultNatStrategy*: string = "none" + DefaultP2pTcpPort*: Port = Port(0) + DefaultP2pListenAddress*: IpAddress = static parseIpAddress("0.0.0.0") + DefaultPortsShift*: uint16 = 0 + DefaultExtMultiAddrsOnly*: bool = false + DefaultDnsAddrsNameServers*: seq[IpAddress] = + @[static parseIpAddress("1.1.1.1"), static parseIpAddress("1.0.0.1")] + DefaultPeerPersistence*: bool = false + DefaultAgentString*: string = "logos-delivery " & git_version + DefaultRelayShardedPeerManagement*: bool = false + DefaultRelayServiceRatio*: string = "50:50" + DefaultCircuitRelayClient*: bool = false + DefaultP2pReliability*: bool = true + DefaultNumShardsInCluster*: uint16 = 1 + DefaultShardingConfKind*: ShardingConfKind = AutoSharding type MaxMessageSizeKind* = enum mmskNone @@ -301,117 +332,124 @@ proc buildShardingConf( bNumShardsInCluster: Option[uint16], bSubscribeShards: Option[seq[uint16]], ): (ShardingConf, seq[uint16]) = - case bShardingConfKind.get(AutoSharding) + case bShardingConfKind.get(DefaultShardingConfKind) of StaticSharding: (ShardingConf(kind: StaticSharding), bSubscribeShards.get(@[])) of AutoSharding: - let numShardsInCluster = bNumShardsInCluster.get(1) + let numShardsInCluster = bNumShardsInCluster.get(DefaultNumShardsInCluster) let shardingConf = ShardingConf(kind: AutoSharding, numShardsInCluster: numShardsInCluster) let upperShard = uint16(numShardsInCluster - 1) (shardingConf, bSubscribeShards.get(toSeq(0.uint16 .. upperShard))) +template checkSetPresetValueToField[T]( + field: var Option[T], presetVal: T, msg: static string +) = + ## Set the field to the preset's value, unless the field is already set + ## (explicit wins). Warn iff the field's existing value differs from the + ## preset's. No-op if they agree. + + if field.isSome(): + if field.get() != presetVal: + warn msg, used = field.get(), discarded = presetVal + else: + field = some(presetVal) + +proc checkAddPresetValueToField[T](field: var seq[T], presetVals: seq[T]) = + ## Append the preset's list values to the field's existing list. Lists + ## concat rather than override; both the user's and the preset's entries + ## end up in the final list. + + field = field & presetVals + proc applyNetworkConf(builder: var WakuConfBuilder) = - # Apply network conf, overrides most values passed individually - # If you want to tweak values, don't use networkConf - # TODO: networkconf should be one field of the conf builder so that this function becomes unnecessary + ## NetworkConf = network presets. + ## Cascade the chosen preset's values onto builder fields the user hasn't set. + ## User-set fields stay; preset fills the gaps and warns on conflict (explicit wins). + ## List fields concat (preset's nodes appended to user's). + if builder.networkConf.isNone(): - return + return # If there is no preset given, then nothing to do. + let networkConf = builder.networkConf.get() - if builder.clusterId.isSome(): - warn "Cluster id was provided alongside a network conf", - used = networkConf.clusterId, discarded = builder.clusterId.get() - builder.clusterId = some(networkConf.clusterId) + checkSetPresetValueToField( + builder.clusterId, networkConf.clusterId, + "Cluster id was provided alongside a network conf", + ) # Apply relay parameters - if builder.relay.get(false) and networkConf.rlnRelay: - if builder.rlnRelayConf.enabled.isSome(): - warn "RLN Relay was provided alongside a network conf", - used = networkConf.rlnRelay, discarded = builder.rlnRelayConf.enabled - builder.rlnRelayConf.withEnabled(true) - - if builder.rlnRelayConf.ethContractAddress.get("") != "": - warn "RLN Relay ETH Contract Address was provided alongside a network conf", - used = networkConf.rlnRelayEthContractAddress.string, - discarded = builder.rlnRelayConf.ethContractAddress.get().string - builder.rlnRelayConf.withEthContractAddress(networkConf.rlnRelayEthContractAddress) - - if builder.rlnRelayConf.chainId.isSome(): - warn "RLN Relay Chain Id was provided alongside a network conf", - used = networkConf.rlnRelayChainId, discarded = builder.rlnRelayConf.chainId - builder.rlnRelayConf.withChainId(networkConf.rlnRelayChainId) - - if builder.rlnRelayConf.dynamic.isSome(): - warn "RLN Relay Dynamic was provided alongside a network conf", - used = networkConf.rlnRelayDynamic, discarded = builder.rlnRelayConf.dynamic - builder.rlnRelayConf.withDynamic(networkConf.rlnRelayDynamic) - - if builder.rlnRelayConf.epochSizeSec.isSome(): - warn "RLN Epoch Size in Seconds was provided alongside a network conf", - used = networkConf.rlnEpochSizeSec, - discarded = builder.rlnRelayConf.epochSizeSec - builder.rlnRelayConf.withEpochSizeSec(networkConf.rlnEpochSizeSec) - - if builder.rlnRelayConf.userMessageLimit.isSome(): - warn "RLN Relay User Message Limit was provided alongside a network conf", - used = networkConf.rlnRelayUserMessageLimit, - discarded = builder.rlnRelayConf.userMessageLimit - if builder.rlnRelayConf.userMessageLimit.get(0) == 0: - ## only override with the "preset" value if there was not explicit set value - builder.rlnRelayConf.withUserMessageLimit(networkConf.rlnRelayUserMessageLimit) - + if builder.relay.get(DefaultRelay) and networkConf.rlnRelay: + checkSetPresetValueToField( + builder.rlnRelayConf.enabled, + networkConf.rlnRelay, # true + "RLN Relay was provided alongside a network conf", + ) + checkSetPresetValueToField( + builder.rlnRelayConf.ethContractAddress, networkConf.rlnRelayEthContractAddress, + "RLN Relay ETH Contract Address was provided alongside a network conf", + ) + checkSetPresetValueToField( + builder.rlnRelayConf.chainId, networkConf.rlnRelayChainId, + "RLN Relay Chain Id was provided alongside a network conf", + ) + checkSetPresetValueToField( + builder.rlnRelayConf.dynamic, networkConf.rlnRelayDynamic, + "RLN Relay Dynamic was provided alongside a network conf", + ) + checkSetPresetValueToField( + builder.rlnRelayConf.epochSizeSec, networkConf.rlnEpochSizeSec, + "RLN Epoch Size in Seconds was provided alongside a network conf", + ) + checkSetPresetValueToField( + builder.rlnRelayConf.userMessageLimit, networkConf.rlnRelayUserMessageLimit, + "RLN Relay User Message Limit was provided alongside a network conf", + ) # End Apply relay parameters case builder.maxMessageSize.kind of mmskNone: - discard + builder.withMaxMessageSize(parseCorrectMsgSize(networkConf.maxMessageSize)) of mmskStr, mmskInt: warn "Max Message Size was provided alongside a network conf", - used = networkConf.maxMessageSize, discarded = $builder.maxMessageSize - builder.withMaxMessageSize(parseCorrectMsgSize(networkConf.maxMessageSize)) - - if builder.shardingConf.isSome(): - warn "Sharding Conf was provided alongside a network conf", - used = networkConf.shardingConf.kind, discarded = builder.shardingConf + used = $builder.maxMessageSize, discarded = networkConf.maxMessageSize + checkSetPresetValueToField( + builder.shardingConf, networkConf.shardingConf.kind, + "Sharding Conf was provided alongside a network conf", + ) case networkConf.shardingConf.kind - of StaticSharding: - builder.shardingConf = some(StaticSharding) of AutoSharding: - builder.shardingConf = some(AutoSharding) - if builder.numShardsInCluster.isSome(): - warn "Num Shards In Cluster overrides network conf preset", - used = builder.numShardsInCluster.get(), - ignored = networkConf.shardingConf.numShardsInCluster - else: - builder.numShardsInCluster = some(networkConf.shardingConf.numShardsInCluster) + checkSetPresetValueToField( + builder.numShardsInCluster, networkConf.shardingConf.numShardsInCluster, + "Num Shards In Cluster overrides network conf preset", + ) + of StaticSharding: + discard - if networkConf.discv5Discovery: - if builder.discv5Conf.enabled.isNone: - builder.discv5Conf.withEnabled(networkConf.discv5Discovery) + checkSetPresetValueToField( + builder.discv5Conf.enabled, networkConf.discv5Discovery, + "Discv5 Discovery was provided alongside a network conf", + ) + checkAddPresetValueToField( + builder.discv5Conf.bootstrapNodes, networkConf.discv5BootstrapNodes + ) - if builder.discv5Conf.bootstrapNodes.len == 0 and - networkConf.discv5BootstrapNodes.len > 0: - warn "Discv5 Bootstrap nodes were provided alongside a network conf", - used = networkConf.discv5BootstrapNodes, - discarded = builder.discv5Conf.bootstrapNodes - builder.discv5Conf.withBootstrapNodes(networkConf.discv5BootstrapNodes) + checkSetPresetValueToField( + builder.kademliaDiscoveryConf.enabled, networkConf.enableKadDiscovery, + "Kademlia Discovery was provided alongside a network conf", + ) + checkAddPresetValueToField( + builder.kademliaDiscoveryConf.bootstrapNodes, networkConf.kadBootstrapNodes + ) - if networkConf.enableKadDiscovery: - if not builder.kademliaDiscoveryConf.enabled: - builder.kademliaDiscoveryConf.withEnabled(networkConf.enableKadDiscovery) - - if builder.kademliaDiscoveryConf.bootstrapNodes.len == 0 and - networkConf.kadBootstrapNodes.len > 0: - builder.kademliaDiscoveryConf.withBootstrapNodes(networkConf.kadBootstrapNodes) - - if networkConf.mix: - if builder.mix.isNone: - builder.mix = some(networkConf.mix) - - if builder.p2pReliability.isNone: - builder.withP2pReliability(networkConf.p2pReliability) + checkSetPresetValueToField( + builder.mix, networkConf.mix, "Mix was provided alongside a network conf" + ) + checkSetPresetValueToField( + builder.p2pReliability, networkConf.p2pReliability, + "P2P Reliability was provided alongside a network conf", + ) # Process entry nodes from network config - classify and distribute if networkConf.entryNodes.len > 0: @@ -449,44 +487,44 @@ proc build*( builder.relay.get() else: warn "whether to mount relay is not specified, defaulting to not mounting" - false + DefaultRelay let lightPush = if builder.lightPush.isSome(): builder.lightPush.get() else: warn "whether to mount lightPush is not specified, defaulting to not mounting" - false + DefaultLightPush let peerExchange = if builder.peerExchange.isSome(): builder.peerExchange.get() else: warn "whether to mount peerExchange is not specified, defaulting to not mounting" - false + DefaultPeerExchange let storeSync = if builder.storeSync.isSome(): builder.storeSync.get() else: warn "whether to mount storeSync is not specified, defaulting to not mounting" - false + DefaultStoreSyncMount let rendezvous = if builder.rendezvous.isSome(): builder.rendezvous.get() else: warn "whether to mount rendezvous is not specified, defaulting to not mounting" - false + DefaultRendezvous let mix = if builder.mix.isSome(): builder.mix.get() else: warn "whether to mount mix is not specified, defaulting to not mounting" - false + DefaultMix - let relayPeerExchange = builder.relayPeerExchange.get(false) + let relayPeerExchange = builder.relayPeerExchange.get(DefaultRelayPeerExchange) let nodeKey = ?nodeKey(builder, rng) @@ -495,7 +533,7 @@ proc build*( # TODO: ClusterId should never be defaulted, instead, presets # should be defined and used warn("Cluster Id was not specified, defaulting to 0") - 0.uint16 + DefaultClusterId else: builder.clusterId.get().uint16 @@ -514,8 +552,9 @@ proc build*( of mmskStr: ?parseMsgSize(builder.maxMessageSize.str) else: - warn "Max Message Size not specified, defaulting to 150KiB" - parseCorrectMsgSize("150KiB") + warn "Max Message Size not specified, defaulting to DefaultMaxWakuMessageSize", + default = DefaultMaxWakuMessageSizeStr + DefaultMaxWakuMessageSize let contentTopics = builder.contentTopics.get(@[]) @@ -560,37 +599,37 @@ proc build*( builder.logLevel.get() else: warn "Log Level not specified, defaulting to INFO" - logging.LogLevel.INFO + DefaultLogLevel let logFormat = if builder.logFormat.isSome(): builder.logFormat.get() else: warn "Log Format not specified, defaulting to TEXT" - logging.LogFormat.TEXT + DefaultLogFormat let natStrategy = if builder.natStrategy.isSome(): builder.natStrategy.get() else: warn "Nat Strategy is not specified, defaulting to none" - "none" + DefaultNatStrategy - let p2pTcpPort = builder.p2pTcpPort.get(Port(0)) + let p2pTcpPort = builder.p2pTcpPort.get(DefaultP2pTcpPort) let p2pListenAddress = if builder.p2pListenAddress.isSome(): builder.p2pListenAddress.get() else: warn "P2P listening address not specified, listening on 0.0.0.0" - (static parseIpAddress("0.0.0.0")) + DefaultP2pListenAddress let portsShift = if builder.portsShift.isSome(): builder.portsShift.get() else: warn "Ports Shift is not specified, defaulting to 0" - 0.uint16 + DefaultPortsShift let dns4DomainName = if builder.dns4DomainName.isSome(): @@ -613,21 +652,21 @@ proc build*( builder.extMultiAddrsOnly.get() else: warn "Whether to only announce external multiaddresses is not specified, defaulting to false" - false + DefaultExtMultiAddrsOnly let dnsAddrsNameServers = if builder.dnsAddrsNameServers.len != 0: builder.dnsAddrsNameServers else: warn "DNS name servers IPs not provided, defaulting to Cloudflare's." - @[static parseIpAddress("1.1.1.1"), static parseIpAddress("1.0.0.1")] + DefaultDnsAddrsNameServers let peerPersistence = if builder.peerPersistence.isSome(): builder.peerPersistence.get() else: warn "Peer persistence not specified, defaulting to false" - false + DefaultPeerPersistence let maxConnections = if builder.maxConnections.isSome(): @@ -641,15 +680,13 @@ proc build*( warn "max-connections less than DefaultMaxConnections; we suggest using DefaultMaxConnections or more for better connectivity", provided = maxConnections, recommended = DefaultMaxConnections - # TODO: Do the git version thing here - let agentString = builder.agentString.get("logos-delivery") + let agentString = builder.agentString.get(DefaultAgentString) - # TODO: use `DefaultColocationLimit`. the user of this value should - # probably be defining a config object - let colocationLimit = builder.colocationLimit.get(5) + let colocationLimit = builder.colocationLimit.get(DefaultColocationLimit) # TODO: is there a strategy for experimental features? delete vs promote - let relayShardedPeerManagement = builder.relayShardedPeerManagement.get(false) + let relayShardedPeerManagement = + builder.relayShardedPeerManagement.get(DefaultRelayShardedPeerManagement) let wakuFlags = CapabilitiesBitfield.init( lightpush = lightPush and relay, @@ -710,12 +747,12 @@ proc build*( agentString: agentString, colocationLimit: colocationLimit, maxRelayPeers: builder.maxRelayPeers, - relayServiceRatio: builder.relayServiceRatio.get("50:50"), + relayServiceRatio: builder.relayServiceRatio.get(DefaultRelayServiceRatio), rateLimit: rateLimit, - circuitRelayClient: builder.circuitRelayClient.get(false), + circuitRelayClient: builder.circuitRelayClient.get(DefaultCircuitRelayClient), staticNodes: builder.staticNodes, relayShardedPeerManagement: relayShardedPeerManagement, - p2pReliability: builder.p2pReliability.get(false), + p2pReliability: builder.p2pReliability.get(DefaultP2pReliability), wakuFlags: wakuFlags, ) diff --git a/waku/factory/conf_builder/web_socket_conf_builder.nim b/waku/factory/conf_builder/web_socket_conf_builder.nim index 26af7cd4a..e9ddd3728 100644 --- a/waku/factory/conf_builder/web_socket_conf_builder.nim +++ b/waku/factory/conf_builder/web_socket_conf_builder.nim @@ -4,6 +4,11 @@ import waku/factory/waku_conf logScope: topics = "waku conf builder websocket" +const + DefaultWebSocketEnabled*: bool = false + DefaultWebSocketSecureEnabled*: bool = false + DefaultWebSocketPort*: Port = Port(0) + ############################## ## WebSocket Config Builder ## ############################## @@ -38,14 +43,15 @@ proc withCertPath*(b: var WebSocketConfBuilder, certPath: string) = b.certPath = some(certPath) proc build*(b: WebSocketConfBuilder): Result[Option[WebSocketConf], string] = - if not b.enabled.get(false): + if not b.enabled.get(DefaultWebSocketEnabled): return ok(none(WebSocketConf)) - if not b.secureEnabled.get(false): + if not b.secureEnabled.get(DefaultWebSocketSecureEnabled): return ok( some( WebSocketConf( - port: b.webSocketPort.get(Port(0)), secureConf: none(WebSocketSecureConf) + port: b.webSocketPort.get(DefaultWebSocketPort), + secureConf: none(WebSocketSecureConf), ) ) ) @@ -58,7 +64,7 @@ proc build*(b: WebSocketConfBuilder): Result[Option[WebSocketConf], string] = return ok( some( WebSocketConf( - port: b.webSocketPort.get(Port(0)), + port: b.webSocketPort.get(DefaultWebSocketPort), secureConf: some( WebSocketSecureConf(keyPath: b.keyPath.get(), certPath: b.certPath.get()) ), diff --git a/waku/factory/networks_config.nim b/waku/factory/networks_config.nim index d9c0cf879..731b5fd84 100644 --- a/waku/factory/networks_config.nim +++ b/waku/factory/networks_config.nim @@ -1,6 +1,7 @@ {.push raises: [].} import chronicles, results, stint +import waku/waku_core/message/default_values logScope: topics = "waku networks conf" @@ -17,7 +18,7 @@ type of StaticSharding: discard -type NetworkConf* = object +type NetworkConf* = object ## A network "preset" (--preset=twn, --preset=logos.dev). maxMessageSize*: string # TODO: static convert to a uint64 clusterId*: uint16 rlnRelay*: bool @@ -41,7 +42,7 @@ type NetworkConf* = object proc TheWakuNetworkConf*(T: type NetworkConf): NetworkConf = const RelayChainId = 59141'u256 return NetworkConf( - maxMessageSize: "150KiB", + maxMessageSize: DefaultMaxWakuMessageSizeStr, clusterId: 1, rlnRelay: true, rlnRelayEthContractAddress: "0xB9cd878C90E49F797B4431fBF4fb333108CB90e6", @@ -68,7 +69,7 @@ proc TheWakuNetworkConf*(T: type NetworkConf): NetworkConf = proc LogosDevConf*(T: type NetworkConf): NetworkConf = const ZeroChainId = 0'u256 return NetworkConf( - maxMessageSize: "150KiB", + maxMessageSize: DefaultMaxWakuMessageSizeStr, clusterId: 2, rlnRelay: false, rlnRelayEthContractAddress: "",