From 4cb4d297a2a917177208e35788319b85364da806 Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Fri, 8 May 2026 01:19:39 -0300 Subject: [PATCH 01/12] Improve config * Add createNode(preset, mode, overrides, additions) nim api * Set p2pTcp/discv5Udp/websocket ports to 0 (auto-bind) in new createNode() * Soft-deprecate --cluster-id=N triggering the associated preset selection * Rewrite applyNetworkConf to apply user-set fields over preset fields * 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 | 199 ++++++++++++++ tests/api/test_node_conf.nim | 14 +- tests/factory/test_waku_conf.nim | 9 +- tests/test_waku.nim | 140 +++++++++- tools/confutils/cli_args.nim | 103 ++++--- tools/confutils/conf_from_json.nim | 257 +++++++++++++++++ tools/confutils/messaging_conf.nim | 40 +++ tools/confutils/optionalize.nim | 74 +++++ waku/api/api.nim | 32 ++- .../conf_builder/discv5_conf_builder.nim | 18 +- .../filter_service_conf_builder.nim | 14 +- .../kademlia_discovery_conf_builder.nim | 8 +- .../metrics_server_conf_builder.nim | 12 +- .../factory/conf_builder/mix_conf_builder.nim | 4 +- .../conf_builder/rest_server_conf_builder.nim | 9 +- .../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 | 259 ++++++++++-------- .../conf_builder/web_socket_conf_builder.nim | 9 +- waku/factory/networks_config.nim | 7 +- 29 files changed, 1054 insertions(+), 289 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..6244ca55a --- /dev/null +++ b/tests/api/test_messaging_conf.nim @@ -0,0 +1,199 @@ +{.used.} + +import std/[algorithm, json, options, sequtils] + +import 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 + + test "Overlay inner types match WakuNodeConf field types": + var c: WakuNodeConf + var o: WakuNodeConfOverlay + for oname, ovalue in fieldPairs(o): + for cname, cvalue in fieldPairs(c): + when oname == cname: + when typeof(cvalue) is Option: + ovalue = cvalue + else: + ovalue = some(cvalue) + + test "Overlay default-constructs every field as none": + var o: WakuNodeConfOverlay + for _, value in fieldPairs(o): + when typeof(value) is Option: + check value.isNone() 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..aa1229dda 100644 --- a/tests/test_waku.nim +++ b/tests/test_waku.nim @@ -1,24 +1,28 @@ {.used.} -import chronos, testutils/unittests, std/options +import std/[net, options] + +import chronos, testutils/unittests import waku import tools/confutils/cli_args +import waku/factory/networks_config +import waku/factory/conf_builder/conf_builder 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 +33,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 +48,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 +65,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 +76,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 +90,119 @@ 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) + + asyncTest "Messaging API seeds 3 user-ports to 0; metrics/REST keep concrete defaults": + let overrides = WakuNodeConfOverlay( + discv5Discovery: some(true), + websocketSupport: some(true), + rest: some(true), + restRelayCacheCapacity: some(50'u32), + metricsServer: some(true), + ) + let node = (await createNode(mode = cli_args.WakuMode.Core, overrides = overrides)).valueOr: + raiseAssert "createNode (port-seeding check) failed: " & error + + check: + node.conf.endpointConf.p2pTcpPort == Port(0) + node.conf.discv5Conf.get().udpPort == Port(0) + node.conf.webSocketConf.get().port == Port(0) + node.conf.restServerConf.get().port == DefaultRestPort + node.conf.metricsServerConf.get().httpPort == DefaultMetricsHttpPort + + asyncTest "Messaging API: explicit user overrides win over developer-profile seeding": + # Caller's explicit tcpPort must take precedence over seedDeveloperProfile's 0. + let overrides = WakuNodeConfOverlay(tcpPort: some(Port(12345))) + let node = (await createNode(mode = cli_args.WakuMode.Core, overrides = overrides)).valueOr: + raiseAssert "createNode (override-wins check) failed: " & error + check node.conf.endpointConf.p2pTcpPort == Port(12345) + + asyncTest "Messaging API with no overrides: p2pTcpPort seeded to 0": + let node = (await createNode(mode = cli_args.WakuMode.Core)).valueOr: + raiseAssert "createNode (no overrides) failed: " & error + check node.conf.endpointConf.p2pTcpPort == Port(0) diff --git a/tools/confutils/cli_args.nim b/tools/confutils/cli_args.nim index d63b5880c..bcb9e851f 100644 --- a/tools/confutils/cli_args.nim +++ b/tools/confutils/cli_args.nim @@ -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,12 +173,15 @@ 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, @@ -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..ed95f4040 --- /dev/null +++ b/tools/confutils/conf_from_json.nim @@ -0,0 +1,257 @@ +import std/[json, strutils, tables] +import confutils, confutils/std/net, results +import ./cli_args +import ./messaging_conf + +const + KeyMode = "mode" + KeyPreset = "preset" + KeyOverrides = "overrides" + KeyAdditions = "additions" + +const CreateNodeWithOverridesExplicitKeys = [KeyMode, KeyPreset] + ## Keys that map to explicit parameters of `createNode(preset, mode, ...)`, + ## hence parsed at the messaging shape's top level and rejected inside + ## `overrides`/`additions` to avoid ambiguity. + +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 rejectOverridesExplicitKeys( + node: JsonNode, blockName: string +): Result[void, string] = + ## Error if `node` contains any key from `CreateNodeWithOverridesExplicitKeys`. + for k, _ in node: + if k.toLowerAscii() in CreateNodeWithOverridesExplicitKeys: + return err("'" & k & "' must be a top-level key, not inside '" & blockName & "'") + return ok() + +proc rejectOverlayExcludes(node: JsonNode): Result[void, string] = + ## Error if `node` contains any key from `WakuNodeConfOverlayExcludes`. + for k, _ in node: + if k.toLowerAscii() in WakuNodeConfOverlayExcludes: + return err("'" & k & "' is not settable via JSON configuration") + 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") + ?rejectOverlayExcludes(overridesNode) + ?rejectOverridesExplicitKeys(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") + ?rejectOverlayExcludes(additionsNode) + ?rejectOverridesExplicitKeys(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) + + if jsonNode.kind == JObject: + ?rejectOverlayExcludes(jsonNode) + + 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..42c98106a --- /dev/null +++ b/tools/confutils/messaging_conf.nim @@ -0,0 +1,40 @@ +{.push raises: [].} + +import std/options +import results +import ./cli_args +import ./optionalize + +const WakuNodeConfOverlayExcludes* = ["cmd", "execute"] + ## Variant-safety: `cmd` is the CLI subcommand discriminator (not dispatched + ## on by the library) and `execute` lives in its inactive branch. Excluded + ## from the overlay AND used as the JSON parser's hard-reject list. + +# 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..f724ff6e1 100644 --- a/waku/api/api.nim +++ b/waku/api/api.nim @@ -1,14 +1,16 @@ -import chronicles, chronos, results +import std/[net, options] + +import chronicles, chronos, libp2p/peerid, results import waku/factory/waku import waku/[requests/health_requests, waku_core, waku_node] 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 +26,30 @@ proc createNode*(conf: WakuNodeConf): Future[Result[Waku, string]] {.async.} = return ok(wakuRes) +proc seedDeveloperProfile(conf: var WakuNodeConf) = + # TODO: Remember to add QUIC port here as well when that is added. + var devPorts = WakuNodeConfOverlay.init() + devPorts.tcpPort = some(Port(0)) + devPorts.discv5UdpPort = some(Port(0)) + devPorts.websocketPort = some(Port(0)) + applyAsOverride(conf, devPorts) + +proc createNode*( + preset = "", + mode = cli_args.WakuMode.Core, + overrides = WakuNodeConfOverlay.init(), + additions = WakuNodeConfOverlay.init(), +): Future[Result[Waku, string]] {.async.} = + ## Create a Waku node from messaging-API parameters. + var conf = defaultWakuNodeConf().valueOr: + return err("Failed creating default conf: " & error) + conf.mode = mode + conf.preset = preset + seedDeveloperProfile(conf) + 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 5dd269d23..c2a40132f 100644 --- a/waku/factory/conf_builder/discv5_conf_builder.nim +++ b/waku/factory/conf_builder/discv5_conf_builder.nim @@ -4,7 +4,13 @@ import ../waku_conf logScope: topics = "waku conf builder discv5" -const DefaultDiscv5UdpPort*: Port = Port(9000) +const + DefaultDiscv5Enabled*: bool = false + DefaultDiscv5BitsPerHop*: int = 1 + DefaultDiscv5BucketIpLimit*: uint = 2 + DefaultDiscv5EnrAutoUpdate*: bool = true + DefaultDiscv5TableIpLimit*: uint = 10 + DefaultDiscv5UdpPort*: Port = Port(9000) ########################### ## Discv5 Config Builder ## @@ -48,17 +54,17 @@ 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), + 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 8b2ea4eb8..473b04d9e 100644 --- a/waku/factory/conf_builder/metrics_server_conf_builder.nim +++ b/waku/factory/conf_builder/metrics_server_conf_builder.nim @@ -4,7 +4,11 @@ import ../waku_conf logScope: topics = "waku conf builder metrics server" -const DefaultMetricsHttpPort*: Port = Port(8008) +const + DefaultMetricsEnabled*: bool = false + DefaultMetricsHttpAddress*: IpAddress = static parseIpAddress("127.0.0.1") + DefaultMetricsHttpPort*: Port = Port(8008) + DefaultMetricsLogging*: bool = false ################################### ## Metrics Server Config Builder ## @@ -35,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")), + httpAddress: b.httpAddress.get(DefaultMetricsHttpAddress), httpPort: b.httpPort.get(DefaultMetricsHttpPort), - logging: b.logging.get(false), + 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 dcafbb56a..88446c08b 100644 --- a/waku/factory/conf_builder/rest_server_conf_builder.nim +++ b/waku/factory/conf_builder/rest_server_conf_builder.nim @@ -4,7 +4,10 @@ import ../waku_conf logScope: topics = "waku conf builder rest server" -const DefaultRestPort*: Port = Port(8645) +const + DefaultRestEnabled*: bool = false + DefaultRestPort*: Port = Port(8645) + DefaultRestAdmin*: bool = false ################################ ## REST Server Config Builder ## @@ -43,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(): @@ -57,7 +60,7 @@ proc build*(b: RestServerConfBuilder): Result[Option[RestServerConf], string] = allowOrigin: b.allowOrigin, listenAddress: b.listenAddress.get(), port: b.port.get(DefaultRestPort), - admin: b.admin.get(false), + 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 5954bbe58..b13910ea3 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,9 +37,35 @@ import logScope: topics = "waku conf builder" +# 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(60000) + 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 @@ -303,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: @@ -451,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) @@ -497,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 @@ -516,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(@[]) @@ -562,21 +599,21 @@ 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(DefaultP2pTcpPort) @@ -585,14 +622,14 @@ proc build*( 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(): @@ -615,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(): @@ -643,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, @@ -712,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 61334d958..797c6d036 100644 --- a/waku/factory/conf_builder/web_socket_conf_builder.nim +++ b/waku/factory/conf_builder/web_socket_conf_builder.nim @@ -4,7 +4,10 @@ import waku/factory/waku_conf logScope: topics = "waku conf builder websocket" -const DefaultWebSocketPort*: Port = Port(8000) +const + DefaultWebSocketEnabled*: bool = false + DefaultWebSocketSecureEnabled*: bool = false + DefaultWebSocketPort*: Port = Port(8000) ############################## ## WebSocket Config Builder ## @@ -40,10 +43,10 @@ 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( 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: "", From b451b94085cb340f85d178c0efd694ae724623ac Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Fri, 29 May 2026 11:40:31 -0300 Subject: [PATCH 02/12] Clean separation between MessagingClient and kernel/core * Convert DeliveryService into optionally mountable MessagingClient * Move SubscriptionManager to core layer (WakuNode) * Ensure libwaku kernel_api/ still works (deprecated; removal pending) * Create node_types.nim to allow WakuNode to compose subsystems cleanly * Create node_telemetry.nim to centralize Prometheus types * Remove unnecessary "ptr Waku" / "addr waku" indirection * Rename Waku.startWaku -> Waku.start for upcoming Waku rename * Write complete proc surface for SubscriptionManager (all intents expressible) * Rename edgeFilterHealthLoop -> edgeFilterConnectionLoop ("Health" means monitoring) * logosdelivery_start_node calls mountMessagingClient then starts * libwaku and wakunode2 do not mount messagingClient * misc refactors/moves, improvements, fixes --- .../liteprotocoltester/liteprotocoltester.nim | 2 +- apps/wakunode2/wakunode2.nim | 2 +- examples/api_example/api_example.nim | 6 +- examples/wakustealthcommitments/node_spec.nim | 2 +- .../logos_delivery_api/node_api.nim | 7 +- library/kernel_api/node_lifecycle_api.nim | 2 +- tests/api/test_api_health.nim | 8 +- tests/api/test_api_receive.nim | 7 +- tests/api/test_api_send.nim | 24 +- tests/api/test_api_subscription.nim | 69 +-- tests/node/test_wakunode_health_monitor.nim | 20 +- tests/waku_discv5/test_waku_discv5.nim | 6 +- tests/waku_relay/test_wakunode_relay.nim | 7 + tests/wakunode2/test_app.nim | 6 +- waku/api/api.nim | 13 +- waku/factory/waku.nim | 135 ++--- waku/messaging_client.nim | 31 ++ .../delivery_service/delivery_service.nim | 44 -- .../recv_service/recv_service.nim | 12 +- .../send_service/send_service.nim | 7 +- .../health_monitor/node_health_monitor.nim | 1 + waku/node/kernel_api/filter.nim | 1 + waku/node/kernel_api/peer_exchange.nim | 1 + waku/node/kernel_api/relay.nim | 107 +--- waku/node/node_telemetry.nim | 27 + waku/node/node_types.nim | 113 ++++ .../subscription_manager.nim | 512 ++++++++++-------- waku/node/waku_metrics.nim | 1 + waku/node/waku_node.nim | 71 +-- waku/requests/health_requests.nim | 4 +- 30 files changed, 673 insertions(+), 575 deletions(-) create mode 100644 waku/messaging_client.nim delete mode 100644 waku/node/delivery_service/delivery_service.nim create mode 100644 waku/node/node_telemetry.nim create mode 100644 waku/node/node_types.nim rename waku/node/{delivery_service => }/subscription_manager.nim (56%) diff --git a/apps/liteprotocoltester/liteprotocoltester.nim b/apps/liteprotocoltester/liteprotocoltester.nim index 46c85e910..1877b8477 100644 --- a/apps/liteprotocoltester/liteprotocoltester.nim +++ b/apps/liteprotocoltester/liteprotocoltester.nim @@ -123,7 +123,7 @@ when isMainModule: error "Waku initialization failed", error = error quit(QuitFailure) - (waitFor startWaku(addr waku)).isOkOr: + (waitFor waku.start()).isOkOr: error "Starting waku failed", error = error quit(QuitFailure) diff --git a/apps/wakunode2/wakunode2.nim b/apps/wakunode2/wakunode2.nim index 484adf68f..be3a83f57 100644 --- a/apps/wakunode2/wakunode2.nim +++ b/apps/wakunode2/wakunode2.nim @@ -55,7 +55,7 @@ when isMainModule: error "Waku initialization failed", error = error quit(QuitFailure) - (waitFor startWaku(addr waku)).isOkOr: + (waitFor waku.start()).isOkOr: error "Starting waku failed", error = error quit(QuitFailure) diff --git a/examples/api_example/api_example.nim b/examples/api_example/api_example.nim index 2093a81c0..207e83429 100644 --- a/examples/api_example/api_example.nim +++ b/examples/api_example/api_example.nim @@ -82,8 +82,12 @@ when isMainModule: echo("Waku node created successfully!") + node.mountMessagingClient().isOkOr: + echo "Failed to mount messaging: ", error + quit(QuitFailure) + # Start the node - (waitFor startWaku(addr node)).isOkOr: + (waitFor node.start()).isOkOr: echo "Failed to start node: ", error quit(QuitFailure) diff --git a/examples/wakustealthcommitments/node_spec.nim b/examples/wakustealthcommitments/node_spec.nim index d85e83a5b..7751878ca 100644 --- a/examples/wakustealthcommitments/node_spec.nim +++ b/examples/wakustealthcommitments/node_spec.nim @@ -48,7 +48,7 @@ proc setup*(): Waku = error "Waku initialization failed", error = error quit(QuitFailure) - (waitFor startWaku(addr waku)).isOkOr: + (waitFor waku.start()).isOkOr: error "Starting waku failed", error = error quit(QuitFailure) diff --git a/liblogosdelivery/logos_delivery_api/node_api.nim b/liblogosdelivery/logos_delivery_api/node_api.nim index 2e30d1b43..3d9442726 100644 --- a/liblogosdelivery/logos_delivery_api/node_api.nim +++ b/liblogosdelivery/logos_delivery_api/node_api.nim @@ -172,7 +172,12 @@ proc logosdelivery_start_node( chronicles.error "ConnectionStatusChange.listen failed", err = $error return err("ConnectionStatusChange.listen failed: " & $error) - (await startWaku(addr ctx.myLib[])).isOkOr: + ctx.myLib[].mountMessagingClient().isOkOr: + let errMsg = $error + chronicles.error "mountMessagingClient failed", err = errMsg + return err("failed to mount messaging: " & errMsg) + + (await ctx.myLib[].start()).isOkOr: let errMsg = $error chronicles.error "START_NODE failed", err = errMsg return err("failed to start: " & errMsg) diff --git a/library/kernel_api/node_lifecycle_api.nim b/library/kernel_api/node_lifecycle_api.nim index 8f3e99b24..55dd7cd55 100644 --- a/library/kernel_api/node_lifecycle_api.nim +++ b/library/kernel_api/node_lifecycle_api.nim @@ -71,7 +71,7 @@ registerReqFFI(CreateNodeRequest, ctx: ptr FFIContext[Waku]): proc waku_start( ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer ) {.ffi.} = - (await startWaku(ctx[].myLib)).isOkOr: + (await ctx.myLib[].start()).isOkOr: error "START_NODE failed", error = error return err("failed to start: " & $error) return ok("") diff --git a/tests/api/test_api_health.nim b/tests/api/test_api_health.nim index d949db24f..62fe39b9e 100644 --- a/tests/api/test_api_health.nim +++ b/tests/api/test_api_health.nim @@ -103,7 +103,9 @@ suite "LM API health checking": client = (await createNode(conf)).valueOr: raiseAssert error - (await startWaku(addr client)).isOkOr: + client.mountMessagingClient().isOkOr: + raiseAssert error + (await client.start()).isOkOr: raiseAssert error asyncTeardown: @@ -281,7 +283,9 @@ suite "LM API health checking": edgeWaku = (await createNode(edgeConf)).valueOr: raiseAssert "Failed to create edge node: " & error - (await startWaku(addr edgeWaku)).isOkOr: + edgeWaku.mountMessagingClient().isOkOr: + raiseAssert "Failed to mount edge messaging: " & error + (await edgeWaku.start()).isOkOr: raiseAssert "Failed to start edge waku: " & error let relayReq = await RequestProtocolHealth.request( diff --git a/tests/api/test_api_receive.nim b/tests/api/test_api_receive.nim index d6aa954a4..85e522afc 100644 --- a/tests/api/test_api_receive.nim +++ b/tests/api/test_api_receive.nim @@ -6,6 +6,7 @@ import libp2p/[peerid, peerinfo, crypto/crypto] import brokers/broker_context import ../testlib/[common, wakucore, wakunode, testasync] import ../waku_archive/archive_utils +import waku/messaging_client import waku, @@ -16,7 +17,6 @@ import waku_relay/protocol, waku_archive, waku_archive/common as archive_common, - node/delivery_service/delivery_service, node/delivery_service/recv_service, ] import waku/factory/waku_conf @@ -147,7 +147,8 @@ suite "Messaging API, Receive Service (store recovery)": subscriber = (await createNode(createApiNodeConf(numShards))).expect( "Failed to create subscriber" ) - (await startWaku(addr subscriber)).expect("Failed to start subscriber") + subscriber.mountMessagingClient().expect("Failed to mount messaging") + (await subscriber.start()).expect("Failed to start subscriber") # publish after the subscriber exists but before it connects to the # store; the message reaches the archive but the subscriber doesn't @@ -185,7 +186,7 @@ suite "Messaging API, Receive Service (store recovery)": await eventManager.teardown() # trigger store check, should recover and deliver via MessageReceivedEvent - await subscriber.deliveryService.recvService.checkStore() + await subscriber.messagingClient.recvService.checkStore() let received = await eventManager.waitForEvents(TestTimeout) check received diff --git a/tests/api/test_api_send.nim b/tests/api/test_api_send.nim index 084119041..679d6a419 100644 --- a/tests/api/test_api_send.nim +++ b/tests/api/test_api_send.nim @@ -241,7 +241,9 @@ suite "Waku API - Send": lockNewGlobalBrokerContext: node = (await createNode(createApiNodeConf())).valueOr: raiseAssert error - (await startWaku(addr node)).isOkOr: + node.mountMessagingClient().isOkOr: + raiseAssert "Failed to mount messaging: " & error + (await node.start()).isOkOr: raiseAssert "Failed to start Waku node: " & error # node is not connected ! @@ -263,7 +265,9 @@ suite "Waku API - Send": lockNewGlobalBrokerContext: node = (await createNode(createApiNodeConf())).valueOr: raiseAssert error - (await startWaku(addr node)).isOkOr: + node.mountMessagingClient().isOkOr: + raiseAssert "Failed to mount messaging: " & error + (await node.start()).isOkOr: raiseAssert "Failed to start Waku node: " & error await node.node.connectToNodes( @@ -297,7 +301,9 @@ suite "Waku API - Send": lockNewGlobalBrokerContext: node = (await createNode(createApiNodeConf())).valueOr: raiseAssert error - (await startWaku(addr node)).isOkOr: + node.mountMessagingClient().isOkOr: + raiseAssert "Failed to mount messaging: " & error + (await node.start()).isOkOr: raiseAssert "Failed to start Waku node: " & error await node.node.connectToNodes(@[relayNode1PeerInfo]) @@ -327,7 +333,9 @@ suite "Waku API - Send": lockNewGlobalBrokerContext: node = (await createNode(createApiNodeConf())).valueOr: raiseAssert error - (await startWaku(addr node)).isOkOr: + node.mountMessagingClient().isOkOr: + raiseAssert "Failed to mount messaging: " & error + (await node.start()).isOkOr: raiseAssert "Failed to start Waku node: " & error await node.node.connectToNodes(@[lightpushNodePeerInfo]) @@ -357,7 +365,9 @@ suite "Waku API - Send": lockNewGlobalBrokerContext: node = (await createNode(createApiNodeConf())).valueOr: raiseAssert error - (await startWaku(addr node)).isOkOr: + node.mountMessagingClient().isOkOr: + raiseAssert "Failed to mount messaging: " & error + (await node.start()).isOkOr: raiseAssert "Failed to start Waku node: " & error await node.node.connectToNodes(@[lightpushNodePeerInfo, storeNodePeerInfo]) @@ -411,7 +421,9 @@ suite "Waku API - Send": lockNewGlobalBrokerContext: node = (await createNode(createApiNodeConf(cli_args.WakuMode.Edge))).valueOr: raiseAssert error - (await startWaku(addr node)).isOkOr: + node.mountMessagingClient().isOkOr: + raiseAssert "Failed to mount messaging: " & error + (await node.start()).isOkOr: raiseAssert "Failed to start Waku node: " & error await node.node.connectToNodes(@[fakeLightpushNodePeerInfo]) diff --git a/tests/api/test_api_subscription.nim b/tests/api/test_api_subscription.nim index 32d4e742f..23936776d 100644 --- a/tests/api/test_api_subscription.nim +++ b/tests/api/test_api_subscription.nim @@ -5,6 +5,7 @@ import chronos, testutils/unittests, stew/byteutils import libp2p/[peerid, peerinfo, multiaddress, crypto/crypto] import brokers/broker_context import ../testlib/[common, wakucore, wakunode, testasync] +import waku/messaging_client import waku, @@ -14,13 +15,14 @@ import events/message_events, waku_relay/protocol, node/kernel_api/filter, - node/delivery_service/subscription_manager, + node/subscription_manager, ] import waku/factory/waku_conf import tools/confutils/cli_args const TestTimeout = chronos.seconds(10) const NegativeTestTimeout = chronos.seconds(2) +const EdgeWaitTimeout = chronos.seconds(60) type ReceiveEventListenerManager = ref object brokerCtx: BrokerContext @@ -85,7 +87,8 @@ proc setupSubscriberNode(conf: WakuNodeConf): Future[Waku] {.async.} = var node: Waku lockNewGlobalBrokerContext: node = (await createNode(conf)).expect("Failed to create subscriber node") - (await startWaku(addr node)).expect("Failed to start subscriber node") + node.mountMessagingClient().expect("Failed to mount messaging") + (await node.start()).expect("Failed to start subscriber node") return node proc setupNetwork( @@ -161,20 +164,37 @@ proc getRelayShard(node: WakuNode, contentTopic: ContentTopic): PubsubTopic = return PubsubTopic($shardObj) proc waitForMesh(node: WakuNode, shard: PubsubTopic) {.async.} = - for _ in 0 ..< 50: + let deadline = Moment.now() + EdgeWaitTimeout + while Moment.now() < deadline: if node.wakuRelay.getNumPeersInMesh(shard).valueOr(0) > 0: return await sleepAsync(100.milliseconds) raise newException(ValueError, "GossipSub Mesh failed to stabilize on " & shard) proc waitForEdgeSubs(w: Waku, shard: PubsubTopic) {.async.} = - let sm = w.deliveryService.subscriptionManager - for _ in 0 ..< 50: - if sm.edgeFilterPeerCount(shard) > 0: + let deadline = Moment.now() + EdgeWaitTimeout + while Moment.now() < deadline: + if w.node.subscriptionManager.edgeFilterPeerCount(shard) > 0: return await sleepAsync(100.milliseconds) raise newException(ValueError, "Edge filter subscription failed on " & shard) +proc edgePeersReached(w: Waku, shard: PubsubTopic, n: int): Future[bool] {.async.} = + let deadline = Moment.now() + EdgeWaitTimeout + while Moment.now() < deadline: + if w.node.subscriptionManager.edgeFilterPeerCount(shard) >= n: + return true + await sleepAsync(100.milliseconds) + return false + +proc edgePeersDroppedBelow(w: Waku, shard: PubsubTopic, n: int): Future[bool] {.async.} = + let deadline = Moment.now() + EdgeWaitTimeout + while Moment.now() < deadline: + if w.node.subscriptionManager.edgeFilterPeerCount(shard) < n: + return true + await sleepAsync(100.milliseconds) + return false + proc publishToMesh( net: TestNetwork, contentTopic: ContentTopic, payload: seq[byte] ): Future[Result[int, string]] {.async.} = @@ -621,7 +641,8 @@ suite "Messaging API, SubscriptionManager": var subscriber: Waku lockNewGlobalBrokerContext: subscriber = (await createNode(conf)).expect("Failed to create edge subscriber") - (await startWaku(addr subscriber)).expect("Failed to start edge subscriber") + subscriber.mountMessagingClient().expect("Failed to mount messaging") + (await subscriber.start()).expect("Failed to start edge subscriber") # Connect edge subscriber to both filter servers so selectPeers finds both await subscriber.node.connectToNodes(@[publisherPeerInfo, meshBuddyPeerInfo]) @@ -632,12 +653,7 @@ suite "Messaging API, SubscriptionManager": (await subscriber.subscribe(testTopic)).expect("Failed to subscribe") # Wait for dialing both filter servers (HealthyThreshold = 2) - for _ in 0 ..< 100: - if subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 2: - break - await sleepAsync(100.milliseconds) - - check subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 2 + check await edgePeersReached(subscriber, shard, 2) # Verify message delivery with both servers alive await waitForMesh(publisher, shard) @@ -659,12 +675,8 @@ suite "Messaging API, SubscriptionManager": await subscriber.node.disconnectNode(meshBuddyPeerInfo) # Wait for the dead peer to be pruned - for _ in 0 ..< 50: - if subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) < 2: - break - await sleepAsync(100.milliseconds) - - check subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 1 + check await edgePeersDroppedBelow(subscriber, shard, 2) + check subscriber.node.subscriptionManager.edgeFilterPeerCount(shard) >= 1 # Verify messages still arrive through the surviving filter server (publisher) eventManager = newReceiveEventListenerManager(subscriber.brokerCtx, 1) @@ -758,7 +770,8 @@ suite "Messaging API, SubscriptionManager": var subscriber: Waku lockNewGlobalBrokerContext: subscriber = (await createNode(conf)).expect("Failed to create edge subscriber") - (await startWaku(addr subscriber)).expect("Failed to start edge subscriber") + subscriber.mountMessagingClient().expect("Failed to mount messaging") + (await subscriber.start()).expect("Failed to start edge subscriber") await subscriber.node.connectToNodes( @[publisherPeerInfo, meshBuddyPeerInfo, sparePeerInfo] @@ -770,23 +783,13 @@ suite "Messaging API, SubscriptionManager": (await subscriber.subscribe(testTopic)).expect("Failed to subscribe") # Wait for 2 confirmed peers (HealthyThreshold). The 3rd is available but not dialed. - for _ in 0 ..< 100: - if subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 2: - break - await sleepAsync(100.milliseconds) - - require subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) == - 2 + check await edgePeersReached(subscriber, shard, 2) + require subscriber.node.subscriptionManager.edgeFilterPeerCount(shard) == 2 await subscriber.node.disconnectNode(meshBuddyPeerInfo) # Wait for the sub loop to detect the loss and dial a replacement - for _ in 0 ..< 100: - if subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 2: - break - await sleepAsync(100.milliseconds) - - check subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 2 + check await edgePeersReached(subscriber, shard, 2) await waitForMesh(publisher, shard) diff --git a/tests/node/test_wakunode_health_monitor.nim b/tests/node/test_wakunode_health_monitor.nim index 08f641a75..a85056d51 100644 --- a/tests/node/test_wakunode_health_monitor.nim +++ b/tests/node/test_wakunode_health_monitor.nim @@ -15,8 +15,7 @@ import node/health_monitor/protocol_health, node/health_monitor/topic_health, node/health_monitor/node_health_monitor, - node/delivery_service/delivery_service, - node/delivery_service/subscription_manager, + messaging_client, node/kernel_api/relay, node/kernel_api/store, node/kernel_api/lightpush, @@ -27,6 +26,7 @@ import ] import ../testlib/[wakunode, wakucore], ../waku_archive/archive_utils +import waku/node/subscription_manager const MockDLow = 4 # Mocked GossipSub DLow value @@ -229,8 +229,8 @@ suite "Health Monitor - events": await nodeA.start() let ds = - DeliveryService.new(false, nodeA).expect("Failed to create DeliveryService") - ds.startDeliveryService().expect("Failed to start DeliveryService") + MessagingClient.new(false, nodeA).expect("Failed to create MessagingClient") + ds.start().expect("Failed to start MessagingClient") let monitorA = NodeHealthMonitor.new(nodeA) @@ -317,7 +317,7 @@ suite "Health Monitor - events": lastStatus == ConnectionStatus.Disconnected await monitorA.stopHealthMonitor() - await ds.stopDeliveryService() + await ds.stop() await nodeA.stop() asyncTest "Edge health driven by confirmed filter subscriptions": @@ -333,9 +333,9 @@ suite "Health Monitor - events": await nodeA.start() let ds = - DeliveryService.new(false, nodeA).expect("Failed to create DeliveryService") - ds.startDeliveryService().expect("Failed to start DeliveryService") - let subMgr = ds.subscriptionManager + MessagingClient.new(false, nodeA).expect("Failed to create MessagingClient") + ds.start().expect("Failed to start MessagingClient") + let subMgr = nodeA.subscriptionManager var nodeB: WakuNode lockNewGlobalBrokerContext: @@ -416,7 +416,7 @@ suite "Health Monitor - events": await EventShardTopicHealthChange.dropListener(nodeA.brokerCtx, shardHealthLis) check shardHealthOk == true - check subMgr.edgeFilterSubStates.len > 0 + check nodeA.subscriptionManager.edgeFilterSubStates.len > 0 healthSignal.clear() deadline = Moment.now() + TestConnectivityTimeLimit @@ -428,7 +428,7 @@ suite "Health Monitor - events": check lastStatus == ConnectionStatus.PartiallyConnected - await ds.stopDeliveryService() + await ds.stop() await monitorA.stopHealthMonitor() await nodeB.stop() await nodeA.stop() diff --git a/tests/waku_discv5/test_waku_discv5.nim b/tests/waku_discv5/test_waku_discv5.nim index 936c01826..36d34058c 100644 --- a/tests/waku_discv5/test_waku_discv5.nim +++ b/tests/waku_discv5/test_waku_discv5.nim @@ -431,7 +431,7 @@ suite "Waku Discovery v5": let waku0 = (await Waku.new(conf)).valueOr: raiseAssert error - (waitFor startWaku(addr waku0)).isOkOr: + (waitFor waku0.start()).isOkOr: raiseAssert error confBuilder.withNodeKey(crypto.PrivateKey.random(Secp256k1, myRng[])[]) @@ -445,7 +445,7 @@ suite "Waku Discovery v5": let waku1 = (await Waku.new(conf1)).valueOr: raiseAssert error - (waitFor startWaku(addr waku1)).isOkOr: + (waitFor waku1.start()).isOkOr: raiseAssert error await waku1.node.mountPeerExchange() @@ -461,7 +461,7 @@ suite "Waku Discovery v5": let waku2 = (await Waku.new(conf2)).valueOr: raiseAssert error - (waitFor startWaku(addr waku2)).isOkOr: + (waitFor waku2.start()).isOkOr: raiseAssert error # leave some time for discv5 to act diff --git a/tests/waku_relay/test_wakunode_relay.nim b/tests/waku_relay/test_wakunode_relay.nim index a687119bd..da79e2a09 100644 --- a/tests/waku_relay/test_wakunode_relay.nim +++ b/tests/waku_relay/test_wakunode_relay.nim @@ -698,6 +698,13 @@ suite "WakuNode - Relay": node.unsubscribe((kind: ContentUnsub, topic: contentTopicB)).isOkOr: assert false, "Failed to unsubscribe to topic: " & $error + check node.wakuRelay.isSubscribed(shard) + + node.unsubscribe((kind: ContentUnsub, topic: contentTopicA)).isOkOr: + assert false, "Failed to unsubscribe to topic: " & $error + node.unsubscribe((kind: ContentUnsub, topic: contentTopicC)).isOkOr: + assert false, "Failed to unsubscribe to topic: " & $error + ## After unsubcription, the node should not be subscribed to the shard anymore check not node.wakuRelay.isSubscribed(shard) diff --git a/tests/wakunode2/test_app.nim b/tests/wakunode2/test_app.nim index 7621ab1e7..8dc9e3582 100644 --- a/tests/wakunode2/test_app.nim +++ b/tests/wakunode2/test_app.nim @@ -46,7 +46,7 @@ suite "Wakunode2 - Waku initialization": var waku = (waitFor Waku.new(conf)).valueOr: raiseAssert error - (waitFor startWaku(addr waku)).isOkOr: + (waitFor waku.start()).isOkOr: raiseAssert error ## Then @@ -71,7 +71,7 @@ suite "Wakunode2 - Waku initialization": var waku = (waitFor Waku.new(conf)).valueOr: raiseAssert error - (waitFor startWaku(addr waku)).isOkOr: + (waitFor waku.start()).isOkOr: raiseAssert error ## Then @@ -128,7 +128,7 @@ suite "Wakunode2 - Waku initialization": (waitFor waku.stop()).isOkOr: raiseAssert error - (waitFor startWaku(addr waku)).isOkOr: + (waitFor waku.start()).isOkOr: raiseAssert error let portsJson = waku.stateInfo.getNodeInfoItem(NodeInfoId.MyBoundPorts) diff --git a/waku/api/api.nim b/waku/api/api.nim index 1eee982fd..56ab198e7 100644 --- a/waku/api/api.nim +++ b/waku/api/api.nim @@ -1,9 +1,10 @@ import chronicles, chronos, results import waku/factory/waku +import waku/messaging_client import waku/[requests/health_requests, waku_core, waku_node] import waku/node/delivery_service/send_service -import waku/node/delivery_service/subscription_manager +import waku/node/subscription_manager import libp2p/peerid import ../../tools/confutils/cli_args import ./[api_conf, types] @@ -38,24 +39,24 @@ proc subscribe*( ): Future[Result[void, string]] {.async.} = ?checkApiAvailability(w) - return w.deliveryService.subscriptionManager.subscribe(contentTopic) + return w.node.subscriptionManager.subscribe(contentTopic) proc unsubscribe*(w: Waku, contentTopic: ContentTopic): Result[void, string] = ?checkApiAvailability(w) - return w.deliveryService.subscriptionManager.unsubscribe(contentTopic) + return w.node.subscriptionManager.unsubscribe(contentTopic) proc send*( w: Waku, envelope: MessageEnvelope ): Future[Result[RequestId, string]] {.async.} = ?checkApiAvailability(w) - let isSubbed = w.deliveryService.subscriptionManager + let isSubbed = w.node.subscriptionManager .isSubscribed(envelope.contentTopic) .valueOr(false) if not isSubbed: info "Auto-subscribing to topic on send", contentTopic = envelope.contentTopic - w.deliveryService.subscriptionManager.subscribe(envelope.contentTopic).isOkOr: + w.node.subscriptionManager.subscribe(envelope.contentTopic).isOkOr: warn "Failed to auto-subscribe", error = error return err("Failed to auto-subscribe before sending: " & error) @@ -71,6 +72,6 @@ proc send*( msgHash = deliveryTask.msgHash.to0xHex(), myPeerId = w.node.peerId() - asyncSpawn w.deliveryService.sendService.send(deliveryTask) + asyncSpawn w.messagingClient.sendService.send(deliveryTask) return ok(requestId) diff --git a/waku/factory/waku.nim b/waku/factory/waku.nim index 6a5567f8c..49225996c 100644 --- a/waku/factory/waku.nim +++ b/waku/factory/waku.nim @@ -30,12 +30,12 @@ import waku_enr/sharding, waku_enr/multiaddr, api/types, + messaging_client, common/logging, node/peer_manager, node/health_monitor, node/waku_metrics, - node/delivery_service/delivery_service, - node/delivery_service/subscription_manager, + node/subscription_manager, rest_api/message_cache, rest_api/endpoint/server, rest_api/endpoint/builder as rest_server_builder, @@ -73,7 +73,7 @@ type Waku* = ref object healthMonitor*: NodeHealthMonitor - deliveryService*: DeliveryService + messagingClient*: MessagingClient restServer*: WakuRestServerRef metricsServer*: MetricsHttpServerRef @@ -215,10 +215,6 @@ proc new*( error "Failed setting up app callbacks", error = error return err("Failed setting up app callbacks: " & $error) - ## Delivery Monitor - let deliveryService = DeliveryService.new(wakuConf.p2pReliability, node).valueOr: - return err("could not create delivery service: " & $error) - var waku = Waku( stateInfo: WakuStateInfo.init(node), conf: wakuConf, @@ -226,7 +222,6 @@ proc new*( key: wakuConf.nodeKey, node: node, healthMonitor: healthMonitor, - deliveryService: deliveryService, appCallbacks: appCallbacks, restServer: restServer, brokerCtx: brokerCtx, @@ -254,9 +249,9 @@ proc getPorts( return ok((tcpPort: tcpPort, websocketPort: websocketPort)) -proc getRunningNetConfig(waku: ptr Waku): Future[Result[NetConfig, string]] {.async.} = - let conf = waku[].conf - let (tcpPort, websocketPort) = getPorts(waku[].node.switch.peerInfo.listenAddrs).valueOr: +proc getRunningNetConfig(waku: Waku): Future[Result[NetConfig, string]] {.async.} = + let conf = waku.conf + let (tcpPort, websocketPort) = getPorts(waku.node.switch.peerInfo.listenAddrs).valueOr: return err("Could not retrieve ports: " & error) if tcpPort.isSome(): @@ -276,67 +271,67 @@ proc getRunningNetConfig(waku: ptr Waku): Future[Result[NetConfig, string]] {.as return ok(netConf) -proc updateEnr(waku: ptr Waku): Future[Result[void, string]] {.async.} = +proc updateEnr(waku: Waku): Future[Result[void, string]] {.async.} = let netConf: NetConfig = (await getRunningNetConfig(waku)).valueOr: return err("error calling updateNetConfig: " & $error) - let record = enrConfiguration(waku[].conf, netConf).valueOr: + let record = enrConfiguration(waku.conf, netConf).valueOr: return err("ENR setup failed: " & error) - if isClusterMismatched(record, waku[].conf.clusterId): + if isClusterMismatched(record, waku.conf.clusterId): return err("cluster-id mismatch configured shards") - waku[].node.enr = record + waku.node.enr = record # If TCP/WS was configured with port 0, node.announcedAddresses was built # pre-bind with a port value of 0. In any case, the resync is harmless. - waku[].node.announcedAddresses = netConf.announcedAddresses + waku.node.announcedAddresses = netConf.announcedAddresses return ok() -proc updateAddressInENR(waku: ptr Waku): Result[void, string] = - let addresses: seq[MultiAddress] = waku[].node.announcedAddresses +proc updateAddressInENR(waku: Waku): Result[void, string] = + let addresses: seq[MultiAddress] = waku.node.announcedAddresses let encodedAddrs = multiaddr.encodeMultiaddrs(addresses) ## First update the enr info contained in WakuNode - let keyBytes = waku[].key.getRawBytes().valueOr: + let keyBytes = waku.key.getRawBytes().valueOr: return err("failed to retrieve raw bytes from waku key: " & $error) let parsedPk = keys.PrivateKey.fromHex(keyBytes.toHex()).valueOr: return err("failed to parse the private key: " & $error) let enrFields = @[toFieldPair(MultiaddrEnrField, encodedAddrs)] - waku[].node.enr.update(parsedPk, extraFields = enrFields).isOkOr: + waku.node.enr.update(parsedPk, extraFields = enrFields).isOkOr: return err("failed to update multiaddress in ENR updateAddressInENR: " & $error) info "Waku node ENR updated successfully with new multiaddress", - enr = waku[].node.enr.toUri(), record = $(waku[].node.enr) + enr = waku.node.enr.toUri(), record = $(waku.node.enr) ## Now update the ENR infor in discv5 - if not waku[].wakuDiscv5.isNil(): - waku[].wakuDiscv5.protocol.localNode.record = waku[].node.enr - let enr = waku[].wakuDiscv5.protocol.localNode.record + if not waku.wakuDiscv5.isNil(): + waku.wakuDiscv5.protocol.localNode.record = waku.node.enr + let enr = waku.wakuDiscv5.protocol.localNode.record info "Waku discv5 ENR updated successfully with new multiaddress", enr = enr.toUri(), record = $(enr) return ok() -proc updateWaku(waku: ptr Waku): Future[Result[void, string]] {.async.} = +proc updateWaku(waku: Waku): Future[Result[void, string]] {.async.} = (await updateEnr(waku)).isOkOr: return err("error calling updateEnr: " & $error) - ?updateAnnouncedAddrWithPrimaryIpAddr(waku[].node) + ?updateAnnouncedAddrWithPrimaryIpAddr(waku.node) ?updateAddressInENR(waku) return ok() -proc startDnsDiscoveryRetryLoop(waku: ptr Waku): Future[void] {.async.} = +proc startDnsDiscoveryRetryLoop(waku: Waku): Future[void] {.async.} = while true: await sleepAsync(30.seconds) if waku.conf.dnsDiscoveryConf.isSome(): let dnsDiscoveryConf = waku.conf.dnsDiscoveryConf.get() - waku[].dynamicBootstrapNodes = ( + waku.dynamicBootstrapNodes = ( await waku_dnsdisc.retrieveDynamicBootstrapNodes( dnsDiscoveryConf.enrTreeUrl, dnsDiscoveryConf.nameServers ) @@ -344,8 +339,8 @@ proc startDnsDiscoveryRetryLoop(waku: ptr Waku): Future[void] {.async.} = error "Retrieving dynamic bootstrap nodes failed", error = error continue - if not waku[].wakuDiscv5.isNil(): - let dynamicBootstrapEnrs = waku[].dynamicBootstrapNodes + if not waku.wakuDiscv5.isNil(): + let dynamicBootstrapEnrs = waku.dynamicBootstrapNodes .filterIt(it.hasUdpPort()) .mapIt(it.enr.get().toUri()) var discv5BootstrapEnrs: seq[enr.Record] @@ -353,26 +348,35 @@ proc startDnsDiscoveryRetryLoop(waku: ptr Waku): Future[void] {.async.} = for enrUri in dynamicBootstrapEnrs: addBootstrapNode(enrUri, discv5BootstrapEnrs) - waku[].wakuDiscv5.updateBootstrapRecords( - waku[].wakuDiscv5.protocol.bootstrapRecords & discv5BootstrapEnrs + waku.wakuDiscv5.updateBootstrapRecords( + waku.wakuDiscv5.protocol.bootstrapRecords & discv5BootstrapEnrs ) info "Connecting to dynamic bootstrap peers" try: await connectToNodes( - waku[].node, waku[].dynamicBootstrapNodes, "dynamic bootstrap" + waku.node, waku.dynamicBootstrapNodes, "dynamic bootstrap" ) except CatchableError: error "failed to connect to dynamic bootstrap nodes: " & getCurrentExceptionMsg() return -proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises: []).} = - if waku[].node.started: - warn "startWaku: waku node already started" +proc mountMessagingClient*(waku: Waku): Result[void, string] = + if not waku.messagingClient.isNil(): + return err("messaging client already mounted") + if waku.node.started: + return err("cannot mount messaging client on a started node") + waku.messagingClient = MessagingClient.new(waku.conf.p2pReliability, waku.node).valueOr: + return err("could not create messaging client: " & $error) + return ok() + +proc start*(waku: Waku): Future[Result[void, string]] {.async: (raises: []).} = + if waku.node.started: + warn "start: waku node already started" return ok() info "Retrieve dynamic bootstrap nodes" - let conf = waku[].conf + let conf = waku.conf if conf.dnsDiscoveryConf.isSome(): let dnsDiscoveryConf = waku.conf.dnsDiscoveryConf.get() @@ -390,9 +394,9 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises: error "Retrieving dynamic bootstrap nodes failed", error = dynamicBootstrapNodesRes.error # Start Dns Discovery retry loop - waku[].dnsRetryLoopHandle = waku.startDnsDiscoveryRetryLoop() + waku.dnsRetryLoopHandle = waku.startDnsDiscoveryRetryLoop() else: - waku[].dynamicBootstrapNodes = dynamicBootstrapNodesRes.get() + waku.dynamicBootstrapNodes = dynamicBootstrapNodesRes.get() ## Initialize persistency singleton instance - we don't need the instance itself here, ## but this ensures it's initialized before any store job starts. @@ -405,12 +409,12 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises: let bound = getPorts(waku.node.switch.peerInfo.listenAddrs).valueOr: return err("failed to read bound ports from switch: " & $error) - waku[].node.ports.tcp = bound.tcpPort.get(Port(0)).uint16 - waku[].node.ports.webSocket = bound.websocketPort.get(Port(0)).uint16 + waku.node.ports.tcp = bound.tcpPort.get(Port(0)).uint16 + waku.node.ports.webSocket = bound.websocketPort.get(Port(0)).uint16 ## Discv5 if conf.discv5Conf.isSome(): - waku[].wakuDiscV5 = ( + waku.wakuDiscV5 = ( await waku_discv5.setupAndStartDiscv5( waku.node.enr, waku.node.peerManager, @@ -425,23 +429,21 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises: ).valueOr: return err("failed to start waku discovery v5: " & error) - waku[].node.ports.discv5Udp = waku[].wakuDiscV5.udpPort.uint16 - waku[].conf.discv5Conf.get().udpPort = waku[].wakuDiscV5.udpPort + waku.node.ports.discv5Udp = waku.wakuDiscV5.udpPort.uint16 + waku.conf.discv5Conf.get().udpPort = waku.wakuDiscV5.udpPort ## Update waku data that is set dynamically on node start try: (await updateWaku(waku)).isOkOr: - return err("Error in startWaku: " & $error) + return err("Error in start: " & $error) except CatchableError: - return err("Caught exception in startWaku: " & getCurrentExceptionMsg()) + return err("Caught exception in start: " & getCurrentExceptionMsg()) - ## Reliability - if not waku[].deliveryService.isNil(): - waku[].deliveryService.startDeliveryService().isOkOr: - return err("failed to start delivery service: " & $error) + waku.node.subscriptionManager.subscribeAllAutoshards().isOkOr: + return err("failed to auto-subscribe autosharding shards: " & $error) ## Health Monitor - waku[].healthMonitor.startHealthMonitor().isOkOr: + waku.healthMonitor.startHealthMonitor().isOkOr: return err("failed to start health monitor: " & $error) ## Setup RequestConnectionStatus provider @@ -450,7 +452,7 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises: globalBrokerContext(), proc(): Result[RequestConnectionStatus, string] = try: - let healthReport = waku[].healthMonitor.getSyncNodeHealthReport() + let healthReport = waku.healthMonitor.getSyncNodeHealthReport() return ok(RequestConnectionStatus(connectionStatus: healthReport.connectionStatus)) except CatchableError: @@ -467,7 +469,7 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises: ): Future[Result[RequestProtocolHealth, string]] {.async.} = try: let protocolHealthStatus = - await waku[].healthMonitor.getProtocolHealthInfo(protocol) + await waku.healthMonitor.getProtocolHealthInfo(protocol) return ok(RequestProtocolHealth(healthStatus: protocolHealthStatus)) except CatchableError: return err("Failed to get protocol health: " & getCurrentExceptionMsg()), @@ -480,7 +482,7 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises: globalBrokerContext(), proc(): Future[Result[RequestHealthReport, string]] {.async.} = try: - let report = await waku[].healthMonitor.getNodeHealthReport() + let report = await waku.healthMonitor.getNodeHealthReport() return ok(RequestHealthReport(healthReport: report)) except CatchableError: return err("Failed to get health report: " & getCurrentExceptionMsg()), @@ -489,9 +491,9 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises: if conf.restServerConf.isSome(): rest_server_builder.startRestServerProtocolSupport( - waku[].restServer, - waku[].node, - waku[].wakuDiscv5, + waku.restServer, + waku.node, + waku.wakuDiscv5, conf.restServerConf.get(), conf.relay, conf.lightPush, @@ -509,21 +511,23 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises: ) ).valueOr: return err("Starting monitoring and external interfaces failed: " & error) - waku[].metricsServer = server - waku[].node.ports.metrics = port.uint16 - waku[].conf.metricsServerConf.get().httpPort = port + waku.metricsServer = server + waku.node.ports.metrics = port.uint16 + waku.conf.metricsServerConf.get().httpPort = port except CatchableError: return err( "Caught exception starting monitoring and external interfaces failed: " & getCurrentExceptionMsg() ) - waku[].healthMonitor.setOverallHealth(HealthStatus.READY) + waku.healthMonitor.setOverallHealth(HealthStatus.READY) + + if not waku.messagingClient.isNil(): + waku.messagingClient.start().isOkOr: + return err("failed to start messaging client: " & $error) return ok() proc stop*(waku: Waku): Future[Result[void, string]] {.async: (raises: []).} = - ## Waku shutdown - if not waku.node.started: warn "stop: attempting to stop node that isn't running" @@ -538,9 +542,8 @@ proc stop*(waku: Waku): Future[Result[void, string]] {.async: (raises: []).} = if not waku.wakuDiscv5.isNil(): await waku.wakuDiscv5.stop() - if not waku.deliveryService.isNil(): - await waku.deliveryService.stopDeliveryService() - waku.deliveryService = nil + if not waku.messagingClient.isNil(): + await waku.messagingClient.stop() if not waku.node.isNil(): await waku.node.stop() diff --git a/waku/messaging_client.nim b/waku/messaging_client.nim new file mode 100644 index 000000000..c5e2f307f --- /dev/null +++ b/waku/messaging_client.nim @@ -0,0 +1,31 @@ +import results, chronos +import + ./node/waku_node, + ./node/delivery_service/[recv_service, send_service] + +type MessagingClient* = ref object + sendService*: SendService + recvService*: RecvService + started: bool + +proc new*( + T: type MessagingClient, useP2PReliability: bool, node: WakuNode +): Result[T, string] = + let sendService = ?SendService.new(useP2PReliability, node) + let recvService = RecvService.new(node) + ok(T(sendService: sendService, recvService: recvService)) + +proc start*(self: MessagingClient): Result[void, string] = + if self.started: + return ok() + self.recvService.startRecvService() + self.sendService.startSendService() + self.started = true + ok() + +proc stop*(self: MessagingClient) {.async.} = + if not self.started: + return + await self.sendService.stopSendService() + await self.recvService.stopRecvService() + self.started = false diff --git a/waku/node/delivery_service/delivery_service.nim b/waku/node/delivery_service/delivery_service.nim deleted file mode 100644 index f3d78d98e..000000000 --- a/waku/node/delivery_service/delivery_service.nim +++ /dev/null @@ -1,44 +0,0 @@ -## This module helps to ensure the correct transmission and reception of messages - -import results -import chronos, chronicles -import - ./recv_service, - ./send_service, - ./subscription_manager, - waku/[ - waku_core, waku_node, waku_store/client, waku_relay/protocol, waku_lightpush/client - ] - -type DeliveryService* = ref object - sendService*: SendService - recvService*: RecvService - subscriptionManager*: SubscriptionManager - -proc new*( - T: type DeliveryService, useP2PReliability: bool, w: WakuNode -): Result[T, string] = - ## storeClient is needed to give store visitility to DeliveryService - ## wakuRelay and wakuLightpushClient are needed to give a mechanism to SendService to re-publish - let subscriptionManager = SubscriptionManager.new(w) - let sendService = ?SendService.new(useP2PReliability, w, subscriptionManager) - let recvService = RecvService.new(w, subscriptionManager) - - return ok( - DeliveryService( - sendService: sendService, - recvService: recvService, - subscriptionManager: subscriptionManager, - ) - ) - -proc startDeliveryService*(self: DeliveryService): Result[void, string] = - ?self.subscriptionManager.startSubscriptionManager() - self.recvService.startRecvService() - self.sendService.startSendService() - return ok() - -proc stopDeliveryService*(self: DeliveryService) {.async.} = - await self.sendService.stopSendService() - await self.recvService.stopRecvService() - await self.subscriptionManager.stopSubscriptionManager() diff --git a/waku/node/delivery_service/recv_service/recv_service.nim b/waku/node/delivery_service/recv_service/recv_service.nim index 899f80f71..6cb448114 100644 --- a/waku/node/delivery_service/recv_service/recv_service.nim +++ b/waku/node/delivery_service/recv_service/recv_service.nim @@ -4,17 +4,17 @@ import std/[tables, sequtils, options, sets] import chronos, chronicles, libp2p/utility -import ../[subscription_manager] import brokers/broker_context import waku/[ waku_core, + waku_core/topics, waku_store/client, waku_store/common, waku_filter_v2/client, - waku_core/topics, events/message_events, waku_node, + node/subscription_manager, ] const StoreCheckPeriod = chronos.minutes(5) ## How often to perform store queries @@ -38,7 +38,6 @@ type RecvService* = ref object of RootObj brokerCtx: BrokerContext node: WakuNode seenMsgListener: MessageSeenEventListener - subscriptionManager: SubscriptionManager recentReceivedMsgs: seq[RecvMessage] @@ -77,7 +76,7 @@ proc processIncomingMessage( ## or if the message is a duplicate (recently-seen). Otherwise, save it as ## recently-seen, emit a MessageReceivedEvent, and return true. - if not self.subscriptionManager.isSubscribed(pubsubTopic, message.contentTopic): + if not self.node.subscriptionManager.isContentSubscribed(pubsubTopic, message.contentTopic): trace "skipping message as I am not subscribed", shard = pubsubTopic, contentTopic = message.contentTopic return false @@ -101,7 +100,7 @@ proc checkStore*(self: RecvService) {.async.} = self.endTimeToCheck = getNowInNanosecondTime() ## query store and deliver new recovered messages per subscribed topic - for pubsubTopic, contentTopics in self.subscriptionManager.subscribedTopics: + for pubsubTopic, contentTopics in self.node.subscriptionManager.subscribedContentTopics: let storeResp: StoreQueryResponse = ( await self.node.wakuStoreClient.queryToAny( StoreQueryRequest( @@ -146,7 +145,7 @@ proc msgChecker(self: RecvService) {.async.} = await sleepAsync(StoreCheckPeriod) await self.checkStore() -proc new*(T: typedesc[RecvService], node: WakuNode, s: SubscriptionManager): T = +proc new*(T: typedesc[RecvService], node: WakuNode): T = ## The storeClient will help to acquire any possible missed messages let now = getNowInNanosecondTime() @@ -154,7 +153,6 @@ proc new*(T: typedesc[RecvService], node: WakuNode, s: SubscriptionManager): T = node: node, startTimeToCheck: now, brokerCtx: node.brokerCtx, - subscriptionManager: s, recentReceivedMsgs: @[], ) diff --git a/waku/node/delivery_service/send_service/send_service.nim b/waku/node/delivery_service/send_service/send_service.nim index 902f3aa1c..23e7d6397 100644 --- a/waku/node/delivery_service/send_service/send_service.nim +++ b/waku/node/delivery_service/send_service/send_service.nim @@ -6,10 +6,10 @@ import chronos, chronicles, libp2p/utility import brokers/broker_context import ./[send_processor, relay_processor, lightpush_processor, delivery_task], - ../[subscription_manager], waku/[ waku_core, node/waku_node, + node/subscription_manager, node/peer_manager, waku_store/client, waku_store/common, @@ -58,7 +58,6 @@ type SendService* = ref object of RootObj node: WakuNode checkStoreForMessages: bool - subscriptionManager: SubscriptionManager proc setupSendProcessorChain( peerManager: PeerManager, @@ -99,7 +98,6 @@ proc new*( T: typedesc[SendService], preferP2PReliability: bool, w: WakuNode, - s: SubscriptionManager, ): Result[T, string] = if w.wakuRelay.isNil() and w.wakuLightpushClient.isNil(): return err( @@ -120,7 +118,6 @@ proc new*( sendProcessor: sendProcessorChain, node: w, checkStoreForMessages: checkStoreForMessages, - subscriptionManager: s, ) return ok(sendService) @@ -263,7 +260,7 @@ proc send*(self: SendService, task: DeliveryTask) {.async.} = info "SendService.send: processing delivery task", requestId = task.requestId, msgHash = task.msgHash.to0xHex() - self.subscriptionManager.subscribe(task.msg.contentTopic).isOkOr: + self.node.subscriptionManager.subscribe(task.msg.contentTopic).isOkOr: error "SendService.send: failed to subscribe to content topic", contentTopic = task.msg.contentTopic, error = error diff --git a/waku/node/health_monitor/node_health_monitor.nim b/waku/node/health_monitor/node_health_monitor.nim index c652f7cea..98c0f6c7a 100644 --- a/waku/node/health_monitor/node_health_monitor.nim +++ b/waku/node/health_monitor/node_health_monitor.nim @@ -14,6 +14,7 @@ import events/health_events, events/peer_events, node/waku_node, + node/node_telemetry, node/peer_manager, node/kernel_api, node/health_monitor/online_monitor, diff --git a/waku/node/kernel_api/filter.nim b/waku/node/kernel_api/filter.nim index 948035f14..0db4875b0 100644 --- a/waku/node/kernel_api/filter.nim +++ b/waku/node/kernel_api/filter.nim @@ -21,6 +21,7 @@ import import ../waku_node, + ../node_telemetry, ../../waku_core, ../../waku_core/topics/sharding, ../../waku_filter_v2, diff --git a/waku/node/kernel_api/peer_exchange.nim b/waku/node/kernel_api/peer_exchange.nim index a4bec727b..1cb6bd3bb 100644 --- a/waku/node/kernel_api/peer_exchange.nim +++ b/waku/node/kernel_api/peer_exchange.nim @@ -19,6 +19,7 @@ import import ../waku_node, + ../node_telemetry, ../../waku_peer_exchange, ../../waku_core, ../peer_manager, diff --git a/waku/node/kernel_api/relay.nim b/waku/node/kernel_api/relay.nim index f1b80cf19..6fcced9e5 100644 --- a/waku/node/kernel_api/relay.nim +++ b/waku/node/kernel_api/relay.nim @@ -29,90 +29,18 @@ import waku_store_sync, waku_rln_relay, node/waku_node, + node/subscription_manager, node/peer_manager, events/message_events, ] export waku_relay.WakuRelayHandler -declarePublicHistogram waku_histogram_message_size, - "message size histogram in kB", - buckets = [ - 0.0, 1.0, 3.0, 5.0, 15.0, 50.0, 75.0, 100.0, 125.0, 150.0, 500.0, 700.0, 1000.0, Inf - ] - logScope: topics = "waku node relay api" ## Waku relay -proc registerRelayHandler( - node: WakuNode, topic: PubsubTopic, appHandler: WakuRelayHandler = nil -): bool = - ## Registers the only handler for the given topic. - ## Notice that this handler internally calls other handlers, such as filter, - ## archive, etc, plus the handler provided by the application. - ## Returns `true` if a mesh subscription was created or `false` if the relay - ## was already subscribed to the topic. - - let alreadySubscribed = node.wakuRelay.isSubscribed(topic) - - if not appHandler.isNil(): - if not alreadySubscribed or not node.legacyAppHandlers.hasKey(topic): - node.legacyAppHandlers[topic] = appHandler - else: - debug "Legacy appHandler already exists for active PubsubTopic, ignoring new handler", - topic = topic - - if alreadySubscribed: - return false - - proc traceHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = - let msgSizeKB = msg.payload.len / 1000 - - waku_node_messages.inc(labelValues = ["relay"]) - waku_histogram_message_size.observe(msgSizeKB) - - proc filterHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = - if node.wakuFilter.isNil(): - return - - await node.wakuFilter.handleMessage(topic, msg) - - proc archiveHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = - if node.wakuArchive.isNil(): - return - - await node.wakuArchive.handleMessage(topic, msg) - - proc syncHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = - if node.wakuStoreReconciliation.isNil(): - return - - node.wakuStoreReconciliation.messageIngress(topic, msg) - - proc internalHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = - MessageSeenEvent.emit(node.brokerCtx, topic, msg) - - let uniqueTopicHandler = proc( - topic: PubsubTopic, msg: WakuMessage - ): Future[void] {.async, gcsafe.} = - await traceHandler(topic, msg) - await filterHandler(topic, msg) - await archiveHandler(topic, msg) - await syncHandler(topic, msg) - await internalHandler(topic, msg) - - # Call the legacy (kernel API) app handler if it exists. - # Normally, hasKey is false and the MessageSeenEvent bus (new API) is used instead. - # But we need to support legacy behavior (kernel API use), hence this. - # NOTE: We can delete `legacyAppHandlers` if instead we refactor WakuRelay to support multiple - # PubsubTopic handlers, since that's actually supported by libp2p PubSub (bigger refactor...) - if node.legacyAppHandlers.hasKey(topic) and not node.legacyAppHandlers[topic].isNil(): - await node.legacyAppHandlers[topic](topic, msg) - - node.wakuRelay.subscribe(topic, uniqueTopicHandler) - proc getTopicOfSubscriptionEvent( node: WakuNode, subscription: SubscriptionEvent ): Result[(PubsubTopic, Option[ContentTopic]), string] = @@ -147,17 +75,9 @@ proc subscribe*( error "Failed to decode subscription event", error = error return err("Failed to decode subscription event: " & error) - if node.registerRelayHandler(pubsubTopic, handler): - info "subscribe", pubsubTopic, contentTopicOp - node.topicSubscriptionQueue.emit((kind: PubsubSub, topic: pubsubTopic)) - else: - if isNil(handler): - warn "No-effect API call to subscribe. Already subscribed to topic", pubsubTopic - else: - info "subscribe (was already subscribed in the mesh; appHandler set)", - pubsubTopic = pubsubTopic - - return ok() + if contentTopicOp.isSome(): + return node.subscriptionManager.subscribe(pubsubTopic, contentTopicOp.get(), handler) + return node.subscriptionManager.subscribeShard(pubsubTopic, handler) proc unsubscribe*( node: WakuNode, subscription: SubscriptionEvent @@ -174,22 +94,9 @@ proc unsubscribe*( error "Failed to decode unsubscribe event", error = error return err("Failed to decode unsubscribe event: " & error) - let hadHandler = node.legacyAppHandlers.hasKey(pubsubTopic) - if hadHandler: - node.legacyAppHandlers.del(pubsubTopic) - - if node.wakuRelay.isSubscribed(pubsubTopic): - info "unsubscribe", pubsubTopic, contentTopicOp - node.wakuRelay.unsubscribe(pubsubTopic) - node.topicSubscriptionQueue.emit((kind: PubsubUnsub, topic: pubsubTopic)) - else: - if not hadHandler: - warn "No-effect API call to `unsubscribe`. Was not subscribed", pubsubTopic - else: - info "unsubscribe (was not subscribed in the mesh; appHandler removed)", - pubsubTopic = pubsubTopic - - return ok() + if contentTopicOp.isSome(): + return node.subscriptionManager.unsubscribe(pubsubTopic, contentTopicOp.get()) + return node.subscriptionManager.unsubscribeAll(pubsubTopic) proc isSubscribed*( node: WakuNode, subscription: SubscriptionEvent diff --git a/waku/node/node_telemetry.nim b/waku/node/node_telemetry.nim new file mode 100644 index 000000000..cd214969c --- /dev/null +++ b/waku/node/node_telemetry.nim @@ -0,0 +1,27 @@ +{.push raises: [].} + +import metrics + +declarePublicGauge waku_version, + "Waku version info (in git describe format)", ["version"] + +declarePublicCounter waku_node_errors, "number of wakunode errors", ["type"] + +declarePublicGauge waku_lightpush_peers, "number of lightpush peers" + +declarePublicGauge waku_filter_peers, "number of filter peers" + +declarePublicGauge waku_store_peers, "number of store peers" + +declarePublicGauge waku_px_peers, + "number of peers (in the node's peerManager) supporting the peer exchange protocol" + +declarePublicCounter waku_node_messages, "number of messages received", ["type"] + +declarePublicHistogram waku_histogram_message_size, + "message size histogram in kB", + buckets = [ + 0.0, 1.0, 3.0, 5.0, 15.0, 50.0, 75.0, 100.0, 125.0, 150.0, 500.0, 700.0, 1000.0, Inf + ] + +{.pop.} diff --git a/waku/node/node_types.nim b/waku/node/node_types.nim new file mode 100644 index 000000000..3a23993c7 --- /dev/null +++ b/waku/node/node_types.nim @@ -0,0 +1,113 @@ +{.push raises: [].} + +import + std/[options, tables, sets], + chronos, + results, + eth/keys, + bearssl/rand, + eth/p2p/discoveryv5/enr, + libp2p/crypto/crypto, + libp2p/[multiaddress, multicodec], + libp2p/protocols/ping, + libp2p/protocols/mix/mix_protocol, + brokers/broker_context + +import + waku/[ + waku_core, + waku_relay, + waku_archive, + waku_store/protocol as store, + waku_store/client as store_client, + waku_store/resume, + waku_store_sync, + waku_filter_v2, + waku_filter_v2/client as filter_client, + waku_metadata, + waku_rendezvous/protocol, + waku_rendezvous/client as rendezvous_client, + waku_lightpush_legacy/client as legacy_lightpush_client, + waku_lightpush_legacy as legacy_lightpush_protocol, + waku_lightpush/client as lightpush_client, + waku_lightpush as lightpush_protocol, + waku_peer_exchange, + waku_rln_relay, + waku_mix, + common/rate_limit/setting, + discovery/waku_kademlia, + net/bound_ports, + events/peer_events, + ], + ./peer_manager, + ./health_monitor/topic_health + +# key and crypto modules different +type + # TODO: Move to application instance (e.g., `WakuNode2`) + WakuInfo* = object # NOTE One for simplicity, can extend later as needed + listenAddresses*: seq[string] + enrUri*: string #multiaddrStrings*: seq[string] + mixPubKey*: Option[string] + + # NOTE based on Eth2Node in NBC eth2_network.nim + WakuNode* = ref object + peerManager*: PeerManager + switch*: Switch + wakuRelay*: WakuRelay + wakuArchive*: waku_archive.WakuArchive + wakuStore*: store.WakuStore + wakuStoreClient*: store_client.WakuStoreClient + wakuStoreResume*: StoreResume + wakuStoreReconciliation*: SyncReconciliation + wakuStoreTransfer*: SyncTransfer + wakuFilter*: waku_filter_v2.WakuFilter + wakuFilterClient*: filter_client.WakuFilterClient + wakuRlnRelay*: WakuRLNRelay + wakuLegacyLightPush*: WakuLegacyLightPush + wakuLegacyLightpushClient*: WakuLegacyLightPushClient + wakuLightPush*: WakuLightPush + wakuLightpushClient*: WakuLightPushClient + wakuPeerExchange*: WakuPeerExchange + wakuPeerExchangeClient*: WakuPeerExchangeClient + wakuMetadata*: WakuMetadata + wakuAutoSharding*: Option[Sharding] + enr*: enr.Record + libp2pPing*: Ping + rng*: ref rand.HmacDrbgContext + brokerCtx*: BrokerContext + wakuRendezvous*: WakuRendezVous + wakuRendezvousClient*: rendezvous_client.WakuRendezVousClient + announcedAddresses*: seq[MultiAddress] + extMultiAddrsOnly*: bool # When true, skip automatic IP address replacement + started*: bool # Indicates that node has started listening + topicSubscriptionQueue*: AsyncEventQueue[SubscriptionEvent] + rateLimitSettings*: ProtocolRateLimitSettings + legacyAppHandlers*: Table[PubsubTopic, WakuRelayHandler] + ## Kernel API Relay appHandlers (if any) + subscriptionManager*: SubscriptionManager + wakuMix*: WakuMix + kademliaDiscoveryLoop*: Future[void] + wakuKademlia*: WakuKademlia + ports*: BoundPorts + + ShardSubscription* = object + contentTopics*: HashSet[ContentTopic] + directShardSub*: bool ## shard subscribed directly (PubsubSub), independent of content-topic interest + + EdgeFilterSubState* = object + peers*: seq[RemotePeerInfo] + pending*: seq[Future[void]] + pendingPeers*: HashSet[PeerId] + currentHealth*: TopicHealth + + SubscriptionManager* = ref object of RootObj + node*: WakuNode + shards*: Table[PubsubTopic, ShardSubscription] + edgeFilterSubStates*: Table[PubsubTopic, EdgeFilterSubState] + edgeFilterWakeup*: AsyncEvent + edgeFilterSubLoopFut*: Future[void] + edgeFilterConnectionLoopFut*: Future[void] + peerEventListener*: WakuPeerEventListener + +{.pop.} diff --git a/waku/node/delivery_service/subscription_manager.nim b/waku/node/subscription_manager.nim similarity index 56% rename from waku/node/delivery_service/subscription_manager.nim rename to waku/node/subscription_manager.nim index 393a61eae..597c0f48e 100644 --- a/waku/node/delivery_service/subscription_manager.nim +++ b/waku/node/subscription_manager.nim @@ -1,18 +1,21 @@ -import std/[sequtils, sets, tables, options, strutils], chronos, chronicles, results +import std/[sequtils, sets, tables, options], chronos, chronicles, metrics, results import libp2p/[peerid, peerinfo] import brokers/broker_context import waku/[ waku_core, - waku_core/topics, waku_core/topics/sharding, - waku_node, + node/node_types, + node/node_telemetry, waku_relay, + waku_archive, + waku_store_sync, waku_filter_v2/common as filter_common, waku_filter_v2/client as filter_client, waku_filter_v2/protocol as filter_protocol, events/health_events, + events/message_events, events/peer_events, requests/health_requests, node/peer_manager, @@ -20,126 +23,114 @@ import node/health_monitor/connection_status, ] -# --------------------------------------------------------------------------- -# Logos Messaging API SubscriptionManager -# -# Maps all topic subscription intent and centralizes all consistency -# maintenance of the pubsub and content topic subscription model across -# the various network drivers that handle topics (Edge/Filter and Core/Relay). -# --------------------------------------------------------------------------- +{.push raises: [].} -type EdgeFilterSubState* = object - peers: seq[RemotePeerInfo] - ## Filter service peers with confirmed subscriptions on this shard. - pending: seq[Future[void]] ## In-flight dial futures for peers not yet confirmed. - pendingPeers: HashSet[PeerId] ## PeerIds of peers currently being dialed. - currentHealth: TopicHealth - ## Cached health derived from peers.len; updated on every peer set change. +proc doRelaySubscribe( + node: WakuNode, shard: PubsubTopic, appHandler: WakuRelayHandler = nil +): bool = + let alreadySubscribed = node.wakuRelay.isSubscribed(shard) + + if not appHandler.isNil(): + if not alreadySubscribed or not node.legacyAppHandlers.hasKey(shard): + node.legacyAppHandlers[shard] = appHandler + else: + debug "Legacy appHandler already exists for active PubsubTopic, ignoring new handler", + topic = shard + + if alreadySubscribed: + return false + + proc traceHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = + let msgSizeKB = msg.payload.len / 1000 + + waku_node_messages.inc(labelValues = ["relay"]) + waku_histogram_message_size.observe(msgSizeKB) + + proc filterHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = + if node.wakuFilter.isNil(): + return + + await node.wakuFilter.handleMessage(topic, msg) + + proc archiveHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = + if node.wakuArchive.isNil(): + return + + await node.wakuArchive.handleMessage(topic, msg) + + proc syncHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = + if node.wakuStoreReconciliation.isNil(): + return + + node.wakuStoreReconciliation.messageIngress(topic, msg) + + proc internalHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = + MessageSeenEvent.emit(node.brokerCtx, topic, msg) + + let uniqueTopicHandler = proc( + topic: PubsubTopic, msg: WakuMessage + ): Future[void] {.async, gcsafe.} = + await traceHandler(topic, msg) + await filterHandler(topic, msg) + await archiveHandler(topic, msg) + await syncHandler(topic, msg) + await internalHandler(topic, msg) + + if node.legacyAppHandlers.hasKey(topic) and not node.legacyAppHandlers[topic].isNil(): + await node.legacyAppHandlers[topic](topic, msg) + + node.wakuRelay.subscribe(shard, uniqueTopicHandler) + node.topicSubscriptionQueue.emit((kind: PubsubSub, topic: shard)) + return true + +proc doRelayUnsubscribe(node: WakuNode, shard: PubsubTopic) = + if node.legacyAppHandlers.hasKey(shard): + node.legacyAppHandlers.del(shard) + + if node.wakuRelay.isSubscribed(shard): + node.wakuRelay.unsubscribe(shard) + node.topicSubscriptionQueue.emit((kind: PubsubUnsub, topic: shard)) + +proc new*(T: type SubscriptionManager, node: WakuNode): T = + T( + node: node, + shards: initTable[PubsubTopic, ShardSubscription](), + edgeFilterSubStates: initTable[PubsubTopic, EdgeFilterSubState](), + edgeFilterWakeup: newAsyncEvent(), + ) + +func wanted(entry: ShardSubscription): bool = + ## True if the shard has content-topic interest or a direct subscription. + return entry.contentTopics.len > 0 or entry.directShardSub + +proc isContentSubscribed*( + self: SubscriptionManager, shard: PubsubTopic, contentTopic: ContentTopic +): bool = + self.shards.withValue(shard, sub): + return contentTopic in sub.contentTopics + return false + +iterator subscribedContentTopics*( + self: SubscriptionManager +): (PubsubTopic, HashSet[ContentTopic]) = + ## Yields each shard with its non-empty content-topic set. + for shard, sub in self.shards.pairs: + if sub.contentTopics.len > 0: + yield (shard, sub.contentTopics) func toTopicHealth*(peersCount: int): TopicHealth = if peersCount >= HealthyThreshold: - TopicHealth.SUFFICIENTLY_HEALTHY + return TopicHealth.SUFFICIENTLY_HEALTHY elif peersCount > 0: - TopicHealth.MINIMALLY_HEALTHY + return TopicHealth.MINIMALLY_HEALTHY else: - TopicHealth.UNHEALTHY + return TopicHealth.UNHEALTHY -type SubscriptionManager* = ref object of RootObj - node: WakuNode - contentTopicSubs: Table[PubsubTopic, HashSet[ContentTopic]] - ## Map of Shard to ContentTopic needed because e.g. WakuRelay is PubsubTopic only. - ## A present key with an empty HashSet value means pubsubtopic already subscribed - ## (via subscribePubsubTopics()) but there's no specific content topic interest yet. - edgeFilterSubStates*: Table[PubsubTopic, EdgeFilterSubState] - ## Per-shard filter subscription state for edge mode. - edgeFilterWakeup: AsyncEvent - ## Signalled when the edge filter sub loop should re-reconcile. - edgeFilterSubLoopFut: Future[void] - edgeFilterHealthLoopFut: Future[void] - peerEventListener: WakuPeerEventListener - ## Listener for peer connect/disconnect events (edge filter wakeup). - -iterator subscribedTopics*( - self: SubscriptionManager -): (PubsubTopic, HashSet[ContentTopic]) = - ## Iterate over all subscribed content topics, batched per shard. - ## This is guaranteed to return a non-empty `topics` (content topics) list on iteration. - - for pubsub, topics in self.contentTopicSubs.pairs: - # We are iterating over subscribed content topics; if we are subscribed to - # a shard but have no subscription (interest) for any content topic in that - # shard, then avoid triggering an iteration that doesn't advance the intent - # to iterate over content topic subscriptions. - if topics.len == 0: - continue - yield (pubsub, topics) - -proc edgeFilterPeerCount*(sm: SubscriptionManager, shard: PubsubTopic): int = - sm.edgeFilterSubStates.withValue(shard, state): +proc edgeFilterPeerCount*(self: SubscriptionManager, shard: PubsubTopic): int = + self.edgeFilterSubStates.withValue(shard, state): return state.peers.len return 0 -proc new*(T: typedesc[SubscriptionManager], node: WakuNode): T = - SubscriptionManager( - node: node, contentTopicSubs: initTable[PubsubTopic, HashSet[ContentTopic]]() - ) - -proc addContentTopicInterest( - self: SubscriptionManager, shard: PubsubTopic, topic: ContentTopic -): Result[void, string] = - var changed = false - if not self.contentTopicSubs.hasKey(shard): - self.contentTopicSubs[shard] = initHashSet[ContentTopic]() - changed = true - - self.contentTopicSubs.withValue(shard, cTopics): - if not cTopics[].contains(topic): - cTopics[].incl(topic) - changed = true - - if changed and not isNil(self.edgeFilterWakeup): - self.edgeFilterWakeup.fire() - - return ok() - -proc removeContentTopicInterest( - self: SubscriptionManager, shard: PubsubTopic, topic: ContentTopic -): Result[void, string] = - var changed = false - self.contentTopicSubs.withValue(shard, cTopics): - if cTopics[].contains(topic): - cTopics[].excl(topic) - changed = true - - if cTopics[].len == 0 and isNil(self.node.wakuRelay): - self.contentTopicSubs.del(shard) # We're done with cTopics here - - if changed and not isNil(self.edgeFilterWakeup): - self.edgeFilterWakeup.fire() - - return ok() - -proc subscribePubsubTopics( - self: SubscriptionManager, shards: seq[PubsubTopic] -): Result[void, string] = - if isNil(self.node.wakuRelay): - return err("subscribePubsubTopics requires a Relay") - - var errors: seq[string] - - for shard in shards: - if not self.contentTopicSubs.hasKey(shard): - self.node.subscribe((kind: PubsubSub, topic: shard), nil).isOkOr: - errors.add("shard " & shard & ": " & error) - continue - - self.contentTopicSubs[shard] = initHashSet[ContentTopic]() - - if errors.len > 0: - return err("subscribeShard errors: " & errors.join("; ")) - - return ok() - proc getShardForContentTopic( self: SubscriptionManager, topic: ContentTopic ): Result[PubsubTopic, string] = @@ -147,55 +138,138 @@ proc getShardForContentTopic( let shardObj = ?self.node.wakuAutoSharding.get().getShard(topic) return ok($shardObj) - return err("SubscriptionManager requires AutoSharding") + return err("autosharding is not configured; pass an explicit shard") + +proc subscribeShard*( + self: SubscriptionManager, shard: PubsubTopic, handler: WakuRelayHandler = nil +): Result[void, string] = + ## Subscribes to the shard directly and joins the relay mesh. + var added = false + self.shards.withValue(shard, entry): + if not entry.directShardSub: + entry.directShardSub = true + added = true + do: + self.shards[shard] = + ShardSubscription(contentTopics: initHashSet[ContentTopic](), directShardSub: true) + added = true + if added: + self.edgeFilterWakeup.fire() + if not isNil(self.node.wakuRelay): + discard self.node.doRelaySubscribe(shard, handler) + return ok() + +proc unsubscribeShard*( + self: SubscriptionManager, shard: PubsubTopic +): Result[void, string] = + ## Drops the direct shard subscription; unsubscribes the mesh if no content topic wants it. + var removed = false + var shardEmpty = false + self.shards.withValue(shard, entry): + if entry.directShardSub: + entry.directShardSub = false + removed = true + shardEmpty = not entry[].wanted() + if removed: + self.edgeFilterWakeup.fire() + if shardEmpty: + self.shards.del(shard) + if not isNil(self.node.wakuRelay): + self.node.doRelayUnsubscribe(shard) + return ok() + +proc subscribe*( + self: SubscriptionManager, + shard: PubsubTopic, + contentTopic: ContentTopic, + handler: WakuRelayHandler = nil, +): Result[void, string] = + ## Adds content-topic interest on the shard and joins the relay mesh. + var added = false + self.shards.withValue(shard, entry): + if contentTopic notin entry.contentTopics: + entry.contentTopics.incl(contentTopic) + added = true + do: + var entry = ShardSubscription(contentTopics: initHashSet[ContentTopic]()) + entry.contentTopics.incl(contentTopic) + self.shards[shard] = entry + added = true + if added: + self.edgeFilterWakeup.fire() + if not isNil(self.node.wakuRelay): + discard self.node.doRelaySubscribe(shard, handler) + return ok() + +proc unsubscribe*( + self: SubscriptionManager, shard: PubsubTopic, contentTopic: ContentTopic +): Result[void, string] = + ## Drops content-topic interest on the shard; unsubscribes the mesh if nothing else wants it. + var removed = false + var shardEmpty = false + self.shards.withValue(shard, entry): + if contentTopic in entry.contentTopics: + entry.contentTopics.excl(contentTopic) + removed = true + shardEmpty = not entry[].wanted() + if removed: + self.edgeFilterWakeup.fire() + if shardEmpty: + self.shards.del(shard) + if not isNil(self.node.wakuRelay): + self.node.doRelayUnsubscribe(shard) + return ok() + +proc subscribe*( + self: SubscriptionManager, topic: ContentTopic +): Result[void, string] = + ## Subscribes to a content topic, resolving its shard via autosharding. + let shard = ?self.getShardForContentTopic(topic) + return self.subscribe(shard, topic) + +proc unsubscribe*( + self: SubscriptionManager, topic: ContentTopic +): Result[void, string] = + ## Unsubscribes from a content topic, resolving its shard via autosharding. + let shard = ?self.getShardForContentTopic(topic) + return self.unsubscribe(shard, topic) + +proc unsubscribeAll*( + self: SubscriptionManager, shard: PubsubTopic +): Result[void, string] = + ## Drops every content topic on the shard, then the direct subscription. + var snapshot: seq[ContentTopic] + self.shards.withValue(shard, sub): + snapshot = toSeq(sub.contentTopics) + for contentTopic in snapshot: + ?self.unsubscribe(shard, contentTopic) + return self.unsubscribeShard(shard) proc isSubscribed*( self: SubscriptionManager, topic: ContentTopic ): Result[bool, string] = let shard = ?self.getShardForContentTopic(topic) - return ok( - self.contentTopicSubs.hasKey(shard) and self.contentTopicSubs[shard].contains(topic) - ) + return ok(self.isContentSubscribed(shard, topic)) -proc isSubscribed*( - self: SubscriptionManager, shard: PubsubTopic, contentTopic: ContentTopic -): bool {.raises: [].} = - self.contentTopicSubs.withValue(shard, cTopics): - return cTopics[].contains(contentTopic) - return false +proc subscribeAllAutoshards*(self: SubscriptionManager): Result[void, string] = + ## Subscribes the relay to every shard in the configured autosharding cluster. + if self.node.wakuRelay.isNil() or self.node.wakuAutoSharding.isNone(): + return ok() -proc subscribe*(self: SubscriptionManager, topic: ContentTopic): Result[void, string] = - if isNil(self.node.wakuRelay) and isNil(self.node.wakuFilterClient): - return err("SubscriptionManager requires either Relay or Filter Client.") + let autoSharding = self.node.wakuAutoSharding.get() + let numShards = autoSharding.shardCountGenZero + if numShards == 0: + return ok() - let shard = ?self.getShardForContentTopic(topic) + for i in 0'u32 ..< numShards: + let shardObj = RelayShard(clusterId: autoSharding.clusterId, shardId: uint16(i)) + self.subscribeShard(PubsubTopic($shardObj)).isOkOr: + error "failed to auto-subscribe relay to cluster shard", + shard = $shardObj, error = error - if not isNil(self.node.wakuRelay) and not self.contentTopicSubs.hasKey(shard): - ?self.subscribePubsubTopics(@[shard]) + ok() - ?self.addContentTopicInterest(shard, topic) - - return ok() - -proc unsubscribe*( - self: SubscriptionManager, topic: ContentTopic -): Result[void, string] = - if isNil(self.node.wakuRelay) and isNil(self.node.wakuFilterClient): - return err("SubscriptionManager requires either Relay or Filter Client.") - - let shard = ?self.getShardForContentTopic(topic) - - if self.isSubscribed(shard, topic): - ?self.removeContentTopicInterest(shard, topic) - - return ok() - -# --------------------------------------------------------------------------- -# Edge Filter driver for the Logos Messaging API -# -# The SubscriptionManager absorbs natively the responsibility of using the -# Edge Filter protocol to effect subscriptions and message receipt for edge. -# --------------------------------------------------------------------------- +{.pop.} const EdgeFilterSubscribeTimeout = chronos.seconds(15) ## Timeout for a single filter subscribe/unsubscribe RPC to a service peer. @@ -225,23 +299,22 @@ proc removePeer(self: SubscriptionManager, shard: PubsubTopic, peerId: PeerId) = ## update health, and wake the sub loop to dial a replacement. ## Best-effort unsubscribe so the service peer stops pushing to us. self.edgeFilterSubStates.withValue(shard, state): - var peer: RemotePeerInfo - var found = false - for p in state.peers: + var idx = -1 + for i, p in state.peers: if p.peerId == peerId: - peer = p - found = true + idx = i break - if not found: + if idx < 0: return - state.peers.keepItIf(it.peerId != peerId) + let peer = state.peers[idx] + state.peers.del(idx) self.updateShardHealth(shard, state[]) self.edgeFilterWakeup.fire() if not self.node.wakuFilterClient.isNil(): - self.contentTopicSubs.withValue(shard, topics): - let ct = toSeq(topics[]) + self.shards.withValue(shard, sub): + let ct = toSeq(sub.contentTopics) if ct.len > 0: proc doUnsubscribe() {.async.} = discard await self.node.wakuFilterClient.unsubscribe(peer, shard, ct) @@ -331,14 +404,14 @@ proc dialFilterPeer( self.edgeFilterSubStates.withValue(shard, state): state.pendingPeers.excl(peer.peerId) -proc edgeFilterHealthLoop*(self: SubscriptionManager) {.async.} = - ## Periodically pings all connected filter service peers to verify they are +proc edgeFilterConnectionLoop(self: SubscriptionManager) {.async.} = + ## Periodically pings all tracked filter service peers to verify they are ## still alive at the application layer. Peers that fail the ping are removed. while true: await sleepAsync(EdgeFilterLoopInterval) if self.node.wakuFilterClient.isNil(): - warn "filter client is nil within edge filter health loop" + warn "filter client is nil within edge filter connection loop" continue var connected = initTable[PeerId, RemotePeerInfo]() @@ -356,7 +429,6 @@ proc edgeFilterHealthLoop*(self: SubscriptionManager) {.async.} = (peer.peerId, self.node.wakuFilterClient.ping(peer, EdgeFilterPingTimeout)) ) - # extract future tasks from (PeerId, Future) tuples and await them await allFutures(pingTasks.mapIt(it[1])) for (peerId, task) in pingTasks: @@ -407,7 +479,7 @@ proc selectFilterCandidates( candidates.setLen(needed) return candidates -proc edgeFilterSubLoop*(self: SubscriptionManager) {.async.} = +proc edgeFilterSubLoop(self: SubscriptionManager) {.async.} = ## Reconciles filter subscriptions with the desired state from SubscriptionManager. var lastSynced = initTable[PubsubTopic, HashSet[ContentTopic]]() @@ -421,11 +493,16 @@ proc edgeFilterSubLoop*(self: SubscriptionManager) {.async.} = trace "edgeFilterSubLoop: wakuFilterClient is nil, skipping" continue - let desired = self.contentTopicSubs + var newSynced = initTable[PubsubTopic, HashSet[ContentTopic]]() + var allShards: HashSet[PubsubTopic] + for shard, sub in self.shards.pairs: + if sub.contentTopics.len > 0: + newSynced[shard] = sub.contentTopics + allShards.incl(shard) + for shard in lastSynced.keys: + allShards.incl(shard) - trace "edgeFilterSubLoop: desired state", numShards = desired.len - - let allShards = toHashSet(toSeq(desired.keys)) + toHashSet(toSeq(lastSynced.keys)) + trace "edgeFilterSubLoop: desired state", numShards = newSynced.len # Step 1: read state across all shards at once and # create a list of peer dial tasks and shard tracking to delete. @@ -434,15 +511,28 @@ proc edgeFilterSubLoop*(self: SubscriptionManager) {.async.} = var shardsToDelete: seq[PubsubTopic] for shard in allShards: - let currTopics = desired.getOrDefault(shard) - let prevTopics = lastSynced.getOrDefault(shard) + # Compute added/removed deltas via direct iteration; no HashSet copies. + var addedTopics: seq[ContentTopic] + var removedTopics: seq[ContentTopic] + newSynced.withValue(shard, curr): + lastSynced.withValue(shard, prev): + for t in curr[]: + if t notin prev[]: + addedTopics.add(t) + for t in prev[]: + if t notin curr[]: + removedTopics.add(t) + do: + for t in curr[]: + addedTopics.add(t) + do: + lastSynced.withValue(shard, prev): + for t in prev[]: + removedTopics.add(t) - if shard notin self.edgeFilterSubStates: - self.edgeFilterSubStates[shard] = - EdgeFilterSubState(currentHealth: TopicHealth.UNHEALTHY) - - let addedTopics = toSeq(currTopics - prevTopics) - let removedTopics = toSeq(prevTopics - currTopics) + discard self.edgeFilterSubStates.mgetOrPut( + shard, EdgeFilterSubState(currentHealth: TopicHealth.UNHEALTHY) + ) self.edgeFilterSubStates.withValue(shard, state): state.peers.keepItIf( @@ -454,7 +544,7 @@ proc edgeFilterSubLoop*(self: SubscriptionManager) {.async.} = for peer in state.peers: asyncSpawn self.syncFilterDeltas(peer, shard, addedTopics, removedTopics) - if currTopics.len == 0: + if shard notin newSynced: shardsToDelete.add(shard) else: self.updateShardHealth(shard, state[]) @@ -462,7 +552,11 @@ proc edgeFilterSubLoop*(self: SubscriptionManager) {.async.} = let needed = max(0, HealthyThreshold - state.peers.len - state.pending.len) if needed > 0: - let tracked = state.peers.mapIt(it.peerId).toHashSet() + state.pendingPeers + var tracked: HashSet[PeerId] + for p in state.peers: + tracked.incl(p.peerId) + for p in state.pendingPeers: + tracked.incl(p) let candidates = self.selectFilterCandidates(shard, tracked, needed) let toDial = min(needed, candidates.len) @@ -474,11 +568,13 @@ proc edgeFilterSubLoop*(self: SubscriptionManager) {.async.} = num_available = candidates.len, toDial = toDial + var dialTopics: seq[ContentTopic] + newSynced.withValue(shard, curr): + dialTopics = toSeq(curr[]) + for i in 0 ..< toDial: dialTasks.add( - EdgeDialTask( - peer: candidates[i], shard: shard, topics: toSeq(currTopics) - ) + EdgeDialTask(peer: candidates[i], shard: shard, topics: dialTopics) ) # Step 2: execute deferred shard tracking deletion and dial tasks. @@ -495,13 +591,11 @@ proc edgeFilterSubLoop*(self: SubscriptionManager) {.async.} = self.edgeFilterSubStates.withValue(task.shard, state): state.pending.add(fut) - lastSynced = desired + lastSynced = newSynced proc startEdgeFilterLoops(self: SubscriptionManager): Result[void, string] = ## Start the edge filter orchestration loops. ## Caller must ensure this is only called in edge mode (relay nil, filter client present). - self.edgeFilterWakeup = newAsyncEvent() - self.peerEventListener = WakuPeerEvent.listen( self.node.brokerCtx, proc(evt: WakuPeerEvent) {.async: (raises: []), gcsafe.} = @@ -513,7 +607,7 @@ proc startEdgeFilterLoops(self: SubscriptionManager): Result[void, string] = return err("Failed to listen to peer events for edge filter: " & error) self.edgeFilterSubLoopFut = self.edgeFilterSubLoop() - self.edgeFilterHealthLoopFut = self.edgeFilterHealthLoop() + self.edgeFilterConnectionLoopFut = self.edgeFilterConnectionLoop() return ok() proc stopEdgeFilterLoops(self: SubscriptionManager) {.async: (raises: []).} = @@ -522,9 +616,9 @@ proc stopEdgeFilterLoops(self: SubscriptionManager) {.async: (raises: []).} = await self.edgeFilterSubLoopFut.cancelAndWait() self.edgeFilterSubLoopFut = nil - if not isNil(self.edgeFilterHealthLoopFut): - await self.edgeFilterHealthLoopFut.cancelAndWait() - self.edgeFilterHealthLoopFut = nil + if not isNil(self.edgeFilterConnectionLoopFut): + await self.edgeFilterConnectionLoopFut.cancelAndWait() + self.edgeFilterConnectionLoopFut = nil for shard, state in self.edgeFilterSubStates: for fut in state.pending: @@ -533,18 +627,7 @@ proc stopEdgeFilterLoops(self: SubscriptionManager) {.async: (raises: []).} = await WakuPeerEvent.dropListener(self.node.brokerCtx, self.peerEventListener) -# --------------------------------------------------------------------------- -# SubscriptionManager Lifecycle (calls Edge behavior above) -# -# startSubscriptionManager and stopSubscriptionManager orchestrate both the -# core (relay) and edge (filter) paths, and register/clear broker providers. -# --------------------------------------------------------------------------- - -proc startSubscriptionManager*(self: SubscriptionManager): Result[void, string] = - # Register edge filter broker providers. The shard/content health providers - # in WakuNode query these via the broker as a fallback when relay health is - # not available. If edge mode is not active, these providers simply return - # NOT_SUBSCRIBED / strength 0, which is harmless. +proc start*(self: SubscriptionManager): Result[void, string] = RequestEdgeShardHealth.setProvider( self.node.brokerCtx, proc(shard: PubsubTopic): Result[RequestEdgeShardHealth, string] = @@ -566,31 +649,18 @@ proc startSubscriptionManager*(self: SubscriptionManager): Result[void, string] ).isOkOr: error "Can't set provider for RequestEdgeFilterPeerCount", error = error + # Start Edge workers if node is in Edge mode (which is + # currently mutually-exclusive with relay being mounted). if self.node.wakuRelay.isNil(): return self.startEdgeFilterLoops() - # Core mode: auto-subscribe relay to all shards in autosharding. - if self.node.wakuAutoSharding.isSome(): - let autoSharding = self.node.wakuAutoSharding.get() - let clusterId = autoSharding.clusterId - let numShards = autoSharding.shardCountGenZero - - if numShards > 0: - var clusterPubsubTopics = newSeqOfCap[PubsubTopic](numShards) - - for i in 0 ..< numShards: - let shardObj = RelayShard(clusterId: clusterId, shardId: uint16(i)) - clusterPubsubTopics.add(PubsubTopic($shardObj)) - - self.subscribePubsubTopics(clusterPubsubTopics).isOkOr: - error "Failed to auto-subscribe Relay to cluster shards: ", error = error - else: - info "SubscriptionManager has no AutoSharding configured; skipping auto-subscribe." - return ok() -proc stopSubscriptionManager*(self: SubscriptionManager) {.async: (raises: []).} = +proc stop*(self: SubscriptionManager) {.async: (raises: []).} = + # Stop Edge workers if node is in Edge mode (which is + # currently mutually-exclusive with relay being mounted). if self.node.wakuRelay.isNil(): await self.stopEdgeFilterLoops() + RequestEdgeShardHealth.clearProvider(self.node.brokerCtx) RequestEdgeFilterPeerCount.clearProvider(self.node.brokerCtx) diff --git a/waku/node/waku_metrics.nim b/waku/node/waku_metrics.nim index af74b1532..bb4c10fff 100644 --- a/waku/node/waku_metrics.nim +++ b/waku/node/waku_metrics.nim @@ -4,6 +4,7 @@ import chronicles, chronos, metrics, metrics/chronos_httpserver import waku/[net/auto_port, waku_rln_relay/protocol_metrics as rln_metrics, utils/collector], ./peer_manager, + ./node_telemetry, ./waku_node const LogInterval = 10.minutes diff --git a/waku/node/waku_node.nim b/waku/node/waku_node.nim index 26a2b5a57..9ac3c5d00 100644 --- a/waku/node/waku_node.nim +++ b/waku/node/waku_node.nim @@ -60,23 +60,14 @@ import requests/health_requests, events/health_events, events/message_events, + events/peer_events, ], waku/discovery/waku_kademlia, waku/net/[bound_ports, net_config], ./peer_manager, ./health_monitor/health_status, - ./health_monitor/topic_health - -declarePublicCounter waku_node_messages, "number of messages received", ["type"] - -declarePublicGauge waku_version, - "Waku version info (in git describe format)", ["version"] -declarePublicCounter waku_node_errors, "number of wakunode errors", ["type"] -declarePublicGauge waku_lightpush_peers, "number of lightpush peers" -declarePublicGauge waku_filter_peers, "number of filter peers" -declarePublicGauge waku_store_peers, "number of store peers" -declarePublicGauge waku_px_peers, - "number of peers (in the node's peerManager) supporting the peer exchange protocol" + ./health_monitor/topic_health, + ./node_telemetry logScope: topics = "waku node" @@ -94,53 +85,10 @@ const clientId* = "Nimbus Waku v2 node" const WakuNodeVersionString* = "version / git commit hash: " & git_version -# key and crypto modules different -type - # TODO: Move to application instance (e.g., `WakuNode2`) - WakuInfo* = object # NOTE One for simplicity, can extend later as needed - listenAddresses*: seq[string] - enrUri*: string #multiaddrStrings*: seq[string] - mixPubKey*: Option[string] +import ./node_types +export node_types - # NOTE based on Eth2Node in NBC eth2_network.nim - WakuNode* = ref object - peerManager*: PeerManager - switch*: Switch - wakuRelay*: WakuRelay - wakuArchive*: waku_archive.WakuArchive - wakuStore*: store.WakuStore - wakuStoreClient*: store_client.WakuStoreClient - wakuStoreResume*: StoreResume - wakuStoreReconciliation*: SyncReconciliation - wakuStoreTransfer*: SyncTransfer - wakuFilter*: waku_filter_v2.WakuFilter - wakuFilterClient*: filter_client.WakuFilterClient - wakuRlnRelay*: WakuRLNRelay - wakuLegacyLightPush*: WakuLegacyLightPush - wakuLegacyLightpushClient*: WakuLegacyLightPushClient - wakuLightPush*: WakuLightPush - wakuLightpushClient*: WakuLightPushClient - wakuPeerExchange*: WakuPeerExchange - wakuPeerExchangeClient*: WakuPeerExchangeClient - wakuMetadata*: WakuMetadata - wakuAutoSharding*: Option[Sharding] - enr*: enr.Record - libp2pPing*: Ping - rng*: ref rand.HmacDrbgContext - brokerCtx*: BrokerContext - wakuRendezvous*: WakuRendezVous - wakuRendezvousClient*: rendezvous_client.WakuRendezVousClient - announcedAddresses*: seq[MultiAddress] - extMultiAddrsOnly*: bool # When true, skip automatic IP address replacement - started*: bool # Indicates that node has started listening - topicSubscriptionQueue*: AsyncEventQueue[SubscriptionEvent] - rateLimitSettings*: ProtocolRateLimitSettings - legacyAppHandlers*: Table[PubsubTopic, WakuRelayHandler] - ## Kernel API Relay appHandlers (if any) - wakuMix*: WakuMix - kademliaDiscoveryLoop*: Future[void] - wakuKademlia*: WakuKademlia - ports*: BoundPorts +import ./subscription_manager proc deduceRelayShard( node: WakuNode, @@ -230,6 +178,8 @@ proc new*( peerManager.setShardGetter(node.getShardsGetter(@[])) + node.subscriptionManager = SubscriptionManager.new(node) + return node proc peerInfo*(node: WakuNode): PeerInfo = @@ -600,6 +550,9 @@ proc start*(node: WakuNode) {.async.} = node.startProvidersAndListeners() + node.subscriptionManager.start().isOkOr: + error "failed to start subscription manager", error = error + if not zeroPortPresent: updateAnnouncedAddrWithPrimaryIpAddr(node).isOkOr: error "failed update announced addr", error = $error @@ -611,6 +564,8 @@ proc start*(node: WakuNode) {.async.} = proc stop*(node: WakuNode) {.async.} = ## By stopping the switch we are stopping all the underlying mounted protocols + await node.subscriptionManager.stop() + node.stopProvidersAndListeners() ## NOTE: This will dispatch gossipsub stop to the WakuRelay.stop method override diff --git a/waku/requests/health_requests.nim b/waku/requests/health_requests.nim index d48b3278f..ccf08f83d 100644 --- a/waku/requests/health_requests.nim +++ b/waku/requests/health_requests.nim @@ -38,14 +38,14 @@ RequestBroker: proc signature(protocol: WakuProtocol): Future[Result[RequestProtocolHealth, string]] -# Get edge filter health for a single shard (set by DeliveryService when edge mode is active) +# Get edge filter health for a single shard (set when edge mode is active) RequestBroker(sync): type RequestEdgeShardHealth* = object health*: TopicHealth proc signature(shard: PubsubTopic): Result[RequestEdgeShardHealth, string] -# Get edge filter confirmed peer count (set by DeliveryService when edge mode is active) +# Get edge filter confirmed peer count (set when edge mode is active) RequestBroker(sync): type RequestEdgeFilterPeerCount* = object peerCount*: int From 5538df0ff92876bc0d20c4e545dffb065b122380 Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Fri, 29 May 2026 13:13:14 -0300 Subject: [PATCH 03/12] Fix lint --- tests/api/test_api_subscription.nim | 4 +++- waku/api/api.nim | 5 ++--- waku/factory/waku.nim | 9 +++------ waku/messaging_client.nim | 4 +--- waku/node/delivery_service/recv_service/recv_service.nim | 4 +++- waku/node/delivery_service/send_service/send_service.nim | 4 +--- waku/node/kernel_api/relay.nim | 3 ++- waku/node/node_types.nim | 3 ++- waku/node/subscription_manager.nim | 9 ++++----- 9 files changed, 21 insertions(+), 24 deletions(-) diff --git a/tests/api/test_api_subscription.nim b/tests/api/test_api_subscription.nim index 23936776d..8f587b535 100644 --- a/tests/api/test_api_subscription.nim +++ b/tests/api/test_api_subscription.nim @@ -187,7 +187,9 @@ proc edgePeersReached(w: Waku, shard: PubsubTopic, n: int): Future[bool] {.async await sleepAsync(100.milliseconds) return false -proc edgePeersDroppedBelow(w: Waku, shard: PubsubTopic, n: int): Future[bool] {.async.} = +proc edgePeersDroppedBelow( + w: Waku, shard: PubsubTopic, n: int +): Future[bool] {.async.} = let deadline = Moment.now() + EdgeWaitTimeout while Moment.now() < deadline: if w.node.subscriptionManager.edgeFilterPeerCount(shard) < n: diff --git a/waku/api/api.nim b/waku/api/api.nim index 56ab198e7..e0e7d4d7b 100644 --- a/waku/api/api.nim +++ b/waku/api/api.nim @@ -51,9 +51,8 @@ proc send*( ): Future[Result[RequestId, string]] {.async.} = ?checkApiAvailability(w) - let isSubbed = w.node.subscriptionManager - .isSubscribed(envelope.contentTopic) - .valueOr(false) + let isSubbed = + w.node.subscriptionManager.isSubscribed(envelope.contentTopic).valueOr(false) if not isSubbed: info "Auto-subscribing to topic on send", contentTopic = envelope.contentTopic w.node.subscriptionManager.subscribe(envelope.contentTopic).isOkOr: diff --git a/waku/factory/waku.nim b/waku/factory/waku.nim index 49225996c..9f1493fe5 100644 --- a/waku/factory/waku.nim +++ b/waku/factory/waku.nim @@ -340,9 +340,8 @@ proc startDnsDiscoveryRetryLoop(waku: Waku): Future[void] {.async.} = continue if not waku.wakuDiscv5.isNil(): - let dynamicBootstrapEnrs = waku.dynamicBootstrapNodes - .filterIt(it.hasUdpPort()) - .mapIt(it.enr.get().toUri()) + let dynamicBootstrapEnrs = + waku.dynamicBootstrapNodes.filterIt(it.hasUdpPort()).mapIt(it.enr.get().toUri()) var discv5BootstrapEnrs: seq[enr.Record] # parse enrURIs from the configuration and add the resulting ENRs to the discv5BootstrapEnrs seq for enrUri in dynamicBootstrapEnrs: @@ -354,9 +353,7 @@ proc startDnsDiscoveryRetryLoop(waku: Waku): Future[void] {.async.} = info "Connecting to dynamic bootstrap peers" try: - await connectToNodes( - waku.node, waku.dynamicBootstrapNodes, "dynamic bootstrap" - ) + await connectToNodes(waku.node, waku.dynamicBootstrapNodes, "dynamic bootstrap") except CatchableError: error "failed to connect to dynamic bootstrap nodes: " & getCurrentExceptionMsg() return diff --git a/waku/messaging_client.nim b/waku/messaging_client.nim index c5e2f307f..32cb64a9e 100644 --- a/waku/messaging_client.nim +++ b/waku/messaging_client.nim @@ -1,7 +1,5 @@ import results, chronos -import - ./node/waku_node, - ./node/delivery_service/[recv_service, send_service] +import ./node/waku_node, ./node/delivery_service/[recv_service, send_service] type MessagingClient* = ref object sendService*: SendService diff --git a/waku/node/delivery_service/recv_service/recv_service.nim b/waku/node/delivery_service/recv_service/recv_service.nim index 6cb448114..500926cc7 100644 --- a/waku/node/delivery_service/recv_service/recv_service.nim +++ b/waku/node/delivery_service/recv_service/recv_service.nim @@ -76,7 +76,9 @@ proc processIncomingMessage( ## or if the message is a duplicate (recently-seen). Otherwise, save it as ## recently-seen, emit a MessageReceivedEvent, and return true. - if not self.node.subscriptionManager.isContentSubscribed(pubsubTopic, message.contentTopic): + if not self.node.subscriptionManager.isContentSubscribed( + pubsubTopic, message.contentTopic + ): trace "skipping message as I am not subscribed", shard = pubsubTopic, contentTopic = message.contentTopic return false diff --git a/waku/node/delivery_service/send_service/send_service.nim b/waku/node/delivery_service/send_service/send_service.nim index 23e7d6397..242dfc111 100644 --- a/waku/node/delivery_service/send_service/send_service.nim +++ b/waku/node/delivery_service/send_service/send_service.nim @@ -95,9 +95,7 @@ proc setupSendProcessorChain( return ok(processors[0]) proc new*( - T: typedesc[SendService], - preferP2PReliability: bool, - w: WakuNode, + T: typedesc[SendService], preferP2PReliability: bool, w: WakuNode ): Result[T, string] = if w.wakuRelay.isNil() and w.wakuLightpushClient.isNil(): return err( diff --git a/waku/node/kernel_api/relay.nim b/waku/node/kernel_api/relay.nim index 6fcced9e5..ef2c999ce 100644 --- a/waku/node/kernel_api/relay.nim +++ b/waku/node/kernel_api/relay.nim @@ -76,7 +76,8 @@ proc subscribe*( return err("Failed to decode subscription event: " & error) if contentTopicOp.isSome(): - return node.subscriptionManager.subscribe(pubsubTopic, contentTopicOp.get(), handler) + return + node.subscriptionManager.subscribe(pubsubTopic, contentTopicOp.get(), handler) return node.subscriptionManager.subscribeShard(pubsubTopic, handler) proc unsubscribe*( diff --git a/waku/node/node_types.nim b/waku/node/node_types.nim index 3a23993c7..1d41a1c97 100644 --- a/waku/node/node_types.nim +++ b/waku/node/node_types.nim @@ -93,7 +93,8 @@ type ShardSubscription* = object contentTopics*: HashSet[ContentTopic] - directShardSub*: bool ## shard subscribed directly (PubsubSub), independent of content-topic interest + directShardSub*: bool + ## shard subscribed directly (PubsubSub), independent of content-topic interest EdgeFilterSubState* = object peers*: seq[RemotePeerInfo] diff --git a/waku/node/subscription_manager.nim b/waku/node/subscription_manager.nim index 597c0f48e..4ac215acf 100644 --- a/waku/node/subscription_manager.nim +++ b/waku/node/subscription_manager.nim @@ -150,8 +150,9 @@ proc subscribeShard*( entry.directShardSub = true added = true do: - self.shards[shard] = - ShardSubscription(contentTopics: initHashSet[ContentTopic](), directShardSub: true) + self.shards[shard] = ShardSubscription( + contentTopics: initHashSet[ContentTopic](), directShardSub: true + ) added = true if added: self.edgeFilterWakeup.fire() @@ -220,9 +221,7 @@ proc unsubscribe*( self.node.doRelayUnsubscribe(shard) return ok() -proc subscribe*( - self: SubscriptionManager, topic: ContentTopic -): Result[void, string] = +proc subscribe*(self: SubscriptionManager, topic: ContentTopic): Result[void, string] = ## Subscribes to a content topic, resolving its shard via autosharding. let shard = ?self.getShardForContentTopic(topic) return self.subscribe(shard, topic) From 1ad877fc23428328447fb826f4dd329fed5a7f3c Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Fri, 29 May 2026 17:45:31 -0300 Subject: [PATCH 04/12] Restore libwaku kernel_api semantics for interop tests --- waku/node/kernel_api/relay.nim | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/waku/node/kernel_api/relay.nim b/waku/node/kernel_api/relay.nim index ef2c999ce..30fc22ec3 100644 --- a/waku/node/kernel_api/relay.nim +++ b/waku/node/kernel_api/relay.nim @@ -71,13 +71,14 @@ proc subscribe*( error "Invalid API call to `subscribe`. WakuRelay not mounted." return err("Invalid API call to `subscribe`. WakuRelay not mounted.") - let (pubsubTopic, contentTopicOp) = getTopicOfSubscriptionEvent(node, subscription).valueOr: + let (pubsubTopic, _) = getTopicOfSubscriptionEvent(node, subscription).valueOr: error "Failed to decode subscription event", error = error return err("Failed to decode subscription event: " & error) - if contentTopicOp.isSome(): - return - node.subscriptionManager.subscribe(pubsubTopic, contentTopicOp.get(), handler) + # strict version + #if contentTopicOp.isSome(): + # return + # node.subscriptionManager.subscribe(pubsubTopic, contentTopicOp.get(), handler) return node.subscriptionManager.subscribeShard(pubsubTopic, handler) proc unsubscribe*( @@ -91,12 +92,13 @@ proc unsubscribe*( error "Invalid API call to `unsubscribe`. WakuRelay not mounted." return err("Invalid API call to `unsubscribe`. WakuRelay not mounted.") - let (pubsubTopic, contentTopicOp) = getTopicOfSubscriptionEvent(node, subscription).valueOr: + let (pubsubTopic, _) = getTopicOfSubscriptionEvent(node, subscription).valueOr: error "Failed to decode unsubscribe event", error = error return err("Failed to decode unsubscribe event: " & error) - if contentTopicOp.isSome(): - return node.subscriptionManager.unsubscribe(pubsubTopic, contentTopicOp.get()) + # strict version + #if contentTopicOp.isSome(): + # return node.subscriptionManager.unsubscribe(pubsubTopic, contentTopicOp.get()) return node.subscriptionManager.unsubscribeAll(pubsubTopic) proc isSubscribed*( From 6baefddf471ab6e4fcb2a58f8cc36ef08da01210 Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Fri, 29 May 2026 20:09:20 -0300 Subject: [PATCH 05/12] Restore libwaku kernel_api semantics for interop tests (2) --- waku/node/subscription_manager.nim | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/waku/node/subscription_manager.nim b/waku/node/subscription_manager.nim index 98afe2fae..661c57004 100644 --- a/waku/node/subscription_manager.nim +++ b/waku/node/subscription_manager.nim @@ -156,8 +156,8 @@ proc subscribeShard*( added = true if added: self.edgeFilterWakeup.fire() - if not isNil(self.node.wakuRelay): - discard self.node.doRelaySubscribe(shard, handler) + if not isNil(self.node.wakuRelay): + discard self.node.doRelaySubscribe(shard, handler) return ok() proc unsubscribeShard*( @@ -198,8 +198,8 @@ proc subscribe*( added = true if added: self.edgeFilterWakeup.fire() - if not isNil(self.node.wakuRelay): - discard self.node.doRelaySubscribe(shard, handler) + if not isNil(self.node.wakuRelay): + discard self.node.doRelaySubscribe(shard, handler) return ok() proc unsubscribe*( From bef86e44bb04cc1904b6c1cb18ce7179cb1bd4fd Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Fri, 29 May 2026 21:36:14 -0300 Subject: [PATCH 06/12] Improve edge filter peer cleanup on disconnect --- waku/node/subscription_manager.nim | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/waku/node/subscription_manager.nim b/waku/node/subscription_manager.nim index 661c57004..4b34f8dcb 100644 --- a/waku/node/subscription_manager.nim +++ b/waku/node/subscription_manager.nim @@ -598,8 +598,17 @@ proc startEdgeFilterLoops(self: SubscriptionManager): Result[void, string] = self.peerEventListener = WakuPeerEvent.listen( self.node.brokerCtx, proc(evt: WakuPeerEvent) {.async: (raises: []), gcsafe.} = - if evt.kind == WakuPeerEventKind.EventDisconnected or - evt.kind == WakuPeerEventKind.EventMetadataUpdated: + if evt.kind == WakuPeerEventKind.EventDisconnected: + # We know a peer is gone, so if it was a service filter peer for this + # edge node, remove it from the list of service filter peers for each + # shard it served and re-evaluate shard health for the affected shards. + for shard, state in self.edgeFilterSubStates.mpairs: + let oldLen = state.peers.len + state.peers.keepItIf(it.peerId != evt.peerId) + if state.peers.len < oldLen: + self.updateShardHealth(shard, state) + self.edgeFilterWakeup.fire() + elif evt.kind == WakuPeerEventKind.EventMetadataUpdated: self.edgeFilterWakeup.fire() , ).valueOr: From dcca369c1241e625ebae463ed083a47c63327a7c Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Fri, 29 May 2026 22:12:17 -0300 Subject: [PATCH 07/12] Fix nix and lint * Fix nix path heuristic missing sds srcDir * Fix lint --- nix/default.nix | 2 +- waku/node/subscription_manager.nim | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/nix/default.nix b/nix/default.nix index ec9e0542c..dfe537f24 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -30,7 +30,7 @@ let # while others use the repo root. Pass both so the compiler finds either layout. pathArgs = builtins.concatStringsSep " " - (builtins.concatMap (p: [ "--path:${p}" "--path:${p}/src" ]) + (builtins.concatMap (p: [ "--path:${p}" "--path:${p}/src" "--path:${p}/sds" ]) (builtins.attrValues otherDeps)); libExt = diff --git a/waku/node/subscription_manager.nim b/waku/node/subscription_manager.nim index 4b34f8dcb..f954f7449 100644 --- a/waku/node/subscription_manager.nim +++ b/waku/node/subscription_manager.nim @@ -609,8 +609,7 @@ proc startEdgeFilterLoops(self: SubscriptionManager): Result[void, string] = self.updateShardHealth(shard, state) self.edgeFilterWakeup.fire() elif evt.kind == WakuPeerEventKind.EventMetadataUpdated: - self.edgeFilterWakeup.fire() - , + self.edgeFilterWakeup.fire(), ).valueOr: return err("Failed to listen to peer events for edge filter: " & error) From 6db9793ef2a5e3463e894ce0541f3a510f5e7523 Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Sat, 30 May 2026 09:33:16 -0300 Subject: [PATCH 08/12] Fix style Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> --- channels/reliable_channel_manager.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/channels/reliable_channel_manager.nim b/channels/reliable_channel_manager.nim index 020bf9b2d..68ae82388 100644 --- a/channels/reliable_channel_manager.nim +++ b/channels/reliable_channel_manager.nim @@ -41,7 +41,7 @@ proc new*( return err("messaging client is required") if sendHandler.isNil(): return err("sendHandler is required") - ok( + return ok( T( channels: initTable[ChannelId, ReliableChannel](), messagingClient: messagingClient, From 019f93485309073af1e9e587fde351784a818c78 Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Sat, 30 May 2026 09:33:41 -0300 Subject: [PATCH 09/12] Fix style Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> --- liblogosdelivery/logos_delivery_api/node_api.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/liblogosdelivery/logos_delivery_api/node_api.nim b/liblogosdelivery/logos_delivery_api/node_api.nim index 9818626cc..042bdb3a8 100644 --- a/liblogosdelivery/logos_delivery_api/node_api.nim +++ b/liblogosdelivery/logos_delivery_api/node_api.nim @@ -174,7 +174,7 @@ proc logosdelivery_start_node( ctx.myLib[].mountMessagingClient().isOkOr: let errMsg = $error - chronicles.error "mountMessagingClient failed", err = errMsg + chronicles.error "mountMessagingClient failed", error = errMsg return err("failed to mount messaging: " & errMsg) ctx.myLib[].mountReliableChannelManager().isOkOr: From d195b5522cc4df93ac8f3bc5a534b74d5ad44ac8 Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Sat, 30 May 2026 10:49:03 -0300 Subject: [PATCH 10/12] Restore registerRelayHandler --- waku/node/subscription_manager.nim | 32 +++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/waku/node/subscription_manager.nim b/waku/node/subscription_manager.nim index f954f7449..0cac87073 100644 --- a/waku/node/subscription_manager.nim +++ b/waku/node/subscription_manager.nim @@ -25,9 +25,10 @@ import {.push raises: [].} -proc doRelaySubscribe( +proc registerRelayHandler( node: WakuNode, shard: PubsubTopic, appHandler: WakuRelayHandler = nil ): bool = + ## Returns true iff we did a new (and only) subscription for this shard in GossipSub. let alreadySubscribed = node.wakuRelay.isSubscribed(shard) if not appHandler.isNil(): @@ -80,16 +81,37 @@ proc doRelaySubscribe( await node.legacyAppHandlers[topic](topic, msg) node.wakuRelay.subscribe(shard, uniqueTopicHandler) - node.topicSubscriptionQueue.emit((kind: PubsubSub, topic: shard)) return true -proc doRelayUnsubscribe(node: WakuNode, shard: PubsubTopic) = +proc unregisterRelayHandler(node: WakuNode, shard: PubsubTopic): bool = + ## Returns true iff we had a subscription for this shard in GossipSub and it was removed. if node.legacyAppHandlers.hasKey(shard): node.legacyAppHandlers.del(shard) if node.wakuRelay.isSubscribed(shard): node.wakuRelay.unsubscribe(shard) + return true + return false + +proc doRelaySubscribe( + node: WakuNode, shard: PubsubTopic, appHandler: WakuRelayHandler = nil +): bool = + ## Subscribes the node to a shard. + ## Returns true if we actually subscribed (transitioned from unsubscribed to subscribed). + ## Emit the shard subscription event if we actually subscribed. + let installed = node.registerRelayHandler(shard, appHandler) + if installed: + node.topicSubscriptionQueue.emit((kind: PubsubSub, topic: shard)) + return installed + +proc doRelayUnsubscribe(node: WakuNode, shard: PubsubTopic): bool = + ## Unsubscribes the node from a shard. + ## Returns true if we actually unsubscribed (transitioned from subscribed to unsubscribed). + ## Emit the shard unsubscription event if we actually unsubscribed. + let unsubscribed = node.unregisterRelayHandler(shard) + if unsubscribed: node.topicSubscriptionQueue.emit((kind: PubsubUnsub, topic: shard)) + return unsubscribed proc new*(T: type SubscriptionManager, node: WakuNode): T = T( @@ -176,7 +198,7 @@ proc unsubscribeShard*( if shardEmpty: self.shards.del(shard) if not isNil(self.node.wakuRelay): - self.node.doRelayUnsubscribe(shard) + discard self.node.doRelayUnsubscribe(shard) return ok() proc subscribe*( @@ -218,7 +240,7 @@ proc unsubscribe*( if shardEmpty: self.shards.del(shard) if not isNil(self.node.wakuRelay): - self.node.doRelayUnsubscribe(shard) + discard self.node.doRelayUnsubscribe(shard) return ok() proc subscribe*(self: SubscriptionManager, topic: ContentTopic): Result[void, string] = From 4b2456fbbee70b7a0f8c5fea540aaf1578b427bb Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Mon, 1 Jun 2026 22:02:43 -0300 Subject: [PATCH 11/12] Fix broken main merge --- tests/channels/test_reliable_channel_send_receive.nim | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/channels/test_reliable_channel_send_receive.nim b/tests/channels/test_reliable_channel_send_receive.nim index 2f49182a2..11d6c9a9f 100644 --- a/tests/channels/test_reliable_channel_send_receive.nim +++ b/tests/channels/test_reliable_channel_send_receive.nim @@ -1,6 +1,6 @@ {.used.} -import std/[net] +import std/[net, options] import chronos, testutils/unittests, stew/byteutils import brokers/broker_context @@ -24,9 +24,9 @@ proc createApiNodeConf(): 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 = 1 - conf.reliabilityEnabled = true + conf.reliabilityEnabled = some(true) conf.rest = false return conf From 37ee2d53eeebda9310b6f424fbc59fa548702c8e Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Mon, 1 Jun 2026 23:04:55 -0300 Subject: [PATCH 12/12] Simplify config improvement PR * Remove WakuNodeConfOverlay + optionalize macro * Remove createNode(preset/mode/overrides/additions) and seedDeveloperProfile * conf_from_json: drop messaging-shape parser, keep flat-conf only * Tests: drop messaging-API/Overlay tests --- tests/api/test_all.nim | 1 - tests/api/test_messaging_conf.nim | 199 ----------------------------- tests/test_waku.nim | 116 ----------------- tools/confutils/conf_from_json.nim | 140 +------------------- tools/confutils/messaging_conf.nim | 40 ------ tools/confutils/optionalize.nim | 74 ----------- waku/api/api.nim | 27 +--- 7 files changed, 3 insertions(+), 594 deletions(-) delete mode 100644 tests/api/test_messaging_conf.nim delete mode 100644 tools/confutils/messaging_conf.nim delete mode 100644 tools/confutils/optionalize.nim diff --git a/tests/api/test_all.nim b/tests/api/test_all.nim index ab4b18f77..56be19c27 100644 --- a/tests/api/test_all.nim +++ b/tests/api/test_all.nim @@ -3,7 +3,6 @@ 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_messaging_conf.nim b/tests/api/test_messaging_conf.nim deleted file mode 100644 index 6244ca55a..000000000 --- a/tests/api/test_messaging_conf.nim +++ /dev/null @@ -1,199 +0,0 @@ -{.used.} - -import std/[algorithm, json, options, sequtils] - -import 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 - - test "Overlay inner types match WakuNodeConf field types": - var c: WakuNodeConf - var o: WakuNodeConfOverlay - for oname, ovalue in fieldPairs(o): - for cname, cvalue in fieldPairs(c): - when oname == cname: - when typeof(cvalue) is Option: - ovalue = cvalue - else: - ovalue = some(cvalue) - - test "Overlay default-constructs every field as none": - var o: WakuNodeConfOverlay - for _, value in fieldPairs(o): - when typeof(value) is Option: - check value.isNone() diff --git a/tests/test_waku.nim b/tests/test_waku.nim index aa1229dda..65a15199d 100644 --- a/tests/test_waku.nim +++ b/tests/test_waku.nim @@ -90,119 +90,3 @@ 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) - - asyncTest "Messaging API seeds 3 user-ports to 0; metrics/REST keep concrete defaults": - let overrides = WakuNodeConfOverlay( - discv5Discovery: some(true), - websocketSupport: some(true), - rest: some(true), - restRelayCacheCapacity: some(50'u32), - metricsServer: some(true), - ) - let node = (await createNode(mode = cli_args.WakuMode.Core, overrides = overrides)).valueOr: - raiseAssert "createNode (port-seeding check) failed: " & error - - check: - node.conf.endpointConf.p2pTcpPort == Port(0) - node.conf.discv5Conf.get().udpPort == Port(0) - node.conf.webSocketConf.get().port == Port(0) - node.conf.restServerConf.get().port == DefaultRestPort - node.conf.metricsServerConf.get().httpPort == DefaultMetricsHttpPort - - asyncTest "Messaging API: explicit user overrides win over developer-profile seeding": - # Caller's explicit tcpPort must take precedence over seedDeveloperProfile's 0. - let overrides = WakuNodeConfOverlay(tcpPort: some(Port(12345))) - let node = (await createNode(mode = cli_args.WakuMode.Core, overrides = overrides)).valueOr: - raiseAssert "createNode (override-wins check) failed: " & error - check node.conf.endpointConf.p2pTcpPort == Port(12345) - - asyncTest "Messaging API with no overrides: p2pTcpPort seeded to 0": - let node = (await createNode(mode = cli_args.WakuMode.Core)).valueOr: - raiseAssert "createNode (no overrides) failed: " & error - check node.conf.endpointConf.p2pTcpPort == Port(0) diff --git a/tools/confutils/conf_from_json.nim b/tools/confutils/conf_from_json.nim index ed95f4040..d269faadc 100644 --- a/tools/confutils/conf_from_json.nim +++ b/tools/confutils/conf_from_json.nim @@ -1,18 +1,6 @@ import std/[json, strutils, tables] import confutils, confutils/std/net, results import ./cli_args -import ./messaging_conf - -const - KeyMode = "mode" - KeyPreset = "preset" - KeyOverrides = "overrides" - KeyAdditions = "additions" - -const CreateNodeWithOverridesExplicitKeys = [KeyMode, KeyPreset] - ## Keys that map to explicit parameters of `createNode(preset, mode, ...)`, - ## hence parsed at the messaging shape's top level and rejected inside - ## `overrides`/`additions` to avoid ambiguity. proc collectJsonFields*( jsonNode: JsonNode @@ -41,22 +29,6 @@ proc unknownKeysError( keys.add(jsonKey) return prefix & ": " & $keys -proc rejectOverridesExplicitKeys( - node: JsonNode, blockName: string -): Result[void, string] = - ## Error if `node` contains any key from `CreateNodeWithOverridesExplicitKeys`. - for k, _ in node: - if k.toLowerAscii() in CreateNodeWithOverridesExplicitKeys: - return err("'" & k & "' must be a top-level key, not inside '" & blockName & "'") - return ok() - -proc rejectOverlayExcludes(node: JsonNode): Result[void, string] = - ## Error if `node` contains any key from `WakuNodeConfOverlayExcludes`. - for k, _ in node: - if k.toLowerAscii() in WakuNodeConfOverlayExcludes: - return err("'" & k & "' is not settable via JSON configuration") - return ok() - proc jsonScalarToString(node: JsonNode): Result[string, string] = ## Convert a scalar JSON value to its string form. case node.kind @@ -126,105 +98,6 @@ proc applyJsonFieldsToConf( 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") - ?rejectOverlayExcludes(overridesNode) - ?rejectOverridesExplicitKeys(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") - ?rejectOverlayExcludes(additionsNode) - ?rejectOverridesExplicitKeys(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] = @@ -237,21 +110,12 @@ proc assembleFullConf*( 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. + ## Parse a flat JSON config whose keys are WakuNodeConf field names. var jsonNode: JsonNode try: jsonNode = parseJson(jsonStr) except CatchableError as e: return err("Failed to parse config JSON: " & e.msg) - if jsonNode.kind == JObject: - ?rejectOverlayExcludes(jsonNode) - let jsonFields = ?collectJsonFields(jsonNode) - let isMessagingShape = - jsonFields.hasKey(KeyOverrides) or jsonFields.hasKey(KeyAdditions) - if isMessagingShape: - return assembleMessagingConf(jsonFields) - else: - return assembleFullConf(jsonFields) + return assembleFullConf(jsonFields) diff --git a/tools/confutils/messaging_conf.nim b/tools/confutils/messaging_conf.nim deleted file mode 100644 index 42c98106a..000000000 --- a/tools/confutils/messaging_conf.nim +++ /dev/null @@ -1,40 +0,0 @@ -{.push raises: [].} - -import std/options -import results -import ./cli_args -import ./optionalize - -const WakuNodeConfOverlayExcludes* = ["cmd", "execute"] - ## Variant-safety: `cmd` is the CLI subcommand discriminator (not dispatched - ## on by the library) and `execute` lives in its inactive branch. Excluded - ## from the overlay AND used as the JSON parser's hard-reject list. - -# 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 deleted file mode 100644 index 8ad6ffdec..000000000 --- a/tools/confutils/optionalize.nim +++ /dev/null @@ -1,74 +0,0 @@ -{.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 20b7eb3b9..03f176a72 100644 --- a/waku/api/api.nim +++ b/waku/api/api.nim @@ -8,10 +8,9 @@ import waku/[requests/health_requests, waku_core, waku_node] import waku/node/delivery_service/send_service import waku/node/subscription_manager import ../../tools/confutils/cli_args -import ../../tools/confutils/messaging_conf import ./[api_conf, types] -export cli_args, messaging_conf +export cli_args logScope: topics = "api" @@ -27,30 +26,6 @@ proc createNode*(conf: WakuNodeConf): Future[Result[Waku, string]] {.async.} = return ok(wakuRes) -proc seedDeveloperProfile(conf: var WakuNodeConf) = - # TODO: Remember to add QUIC port here as well when that is added. - var devPorts = WakuNodeConfOverlay.init() - devPorts.tcpPort = some(Port(0)) - devPorts.discv5UdpPort = some(Port(0)) - devPorts.websocketPort = some(Port(0)) - applyAsOverride(conf, devPorts) - -proc createNode*( - preset = "", - mode = cli_args.WakuMode.Core, - overrides = WakuNodeConfOverlay.init(), - additions = WakuNodeConfOverlay.init(), -): Future[Result[Waku, string]] {.async.} = - ## Create a Waku node from messaging-API parameters. - var conf = defaultWakuNodeConf().valueOr: - return err("Failed creating default conf: " & error) - conf.mode = mode - conf.preset = preset - seedDeveloperProfile(conf) - 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")