Improve config

* Soft-deprecate --cluster-id=N triggering the associated preset selection
* Rewrite applyNetworkConf to apply user-set fields over preset fields
* Add createNode(preset, mode, overrides, additions) nim api
* Generate WakuNodeConfOverlay (all Option fields) from WakuNodeConf
* New parser for configJson handles new messaging shape and full conf shape
* Change all confbuilder defaults from literal values to DefaultXXX consts
* Change int/bool WakuNodeConf fields to Option to get user intent w/o sentinels
* Make Option CLI default-value help mention defaults now owned by confbuilder
* Misc refactors, fixes
* Add tests
This commit is contained in:
Fabiana Cecin 2026-05-08 01:19:39 -03:00
parent 689ef70de9
commit 098c6f2a6d
No known key found for this signature in database
GPG Key ID: BCAB8A55CB51B6C7
29 changed files with 987 additions and 292 deletions

View File

@ -96,7 +96,7 @@ when isMainModule:
wakuNodeConf.shards = @[conf.shard] wakuNodeConf.shards = @[conf.shard]
wakuNodeConf.contentTopics = conf.contentTopics 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... ## TODO: Depending on the tester needs we might extend here with shards, clusterId, etc...
wakuNodeConf.metricsServer = true wakuNodeConf.metricsServer = true

View File

@ -30,18 +30,18 @@ proc setup*(): Waku =
# Override configuration # Override configuration
conf.maxMessageSize = twnNetworkConf.maxMessageSize conf.maxMessageSize = twnNetworkConf.maxMessageSize
conf.clusterId = twnNetworkConf.clusterId conf.clusterId = some(twnNetworkConf.clusterId)
conf.rlnRelayEthContractAddress = twnNetworkConf.rlnRelayEthContractAddress conf.rlnRelayEthContractAddress = twnNetworkConf.rlnRelayEthContractAddress
conf.rlnRelayDynamic = twnNetworkConf.rlnRelayDynamic conf.rlnRelayDynamic = some(twnNetworkConf.rlnRelayDynamic)
conf.discv5Discovery = twnNetworkConf.discv5Discovery conf.discv5Discovery = some(twnNetworkConf.discv5Discovery)
conf.discv5BootstrapNodes = conf.discv5BootstrapNodes =
conf.discv5BootstrapNodes & twnNetworkConf.discv5BootstrapNodes conf.discv5BootstrapNodes & twnNetworkConf.discv5BootstrapNodes
conf.rlnEpochSizeSec = twnNetworkConf.rlnEpochSizeSec conf.rlnEpochSizeSec = some(twnNetworkConf.rlnEpochSizeSec)
conf.rlnRelayUserMessageLimit = twnNetworkConf.rlnRelayUserMessageLimit conf.rlnRelayUserMessageLimit = some(twnNetworkConf.rlnRelayUserMessageLimit)
# Only set rlnRelay to true if relay is configured # Only set rlnRelay to true if relay is configured
if conf.relay: if conf.relay:
conf.rlnRelay = twnNetworkConf.rlnRelay conf.rlnRelay = some(twnNetworkConf.rlnRelay)
info "Starting node" info "Starting node"
var waku = (waitFor Waku.new(conf)).valueOr: var waku = (waitFor Waku.new(conf)).valueOr:

View File

@ -1,11 +1,11 @@
import std/[json, strutils, tables] import std/json
import chronos, chronicles, results, confutils, confutils/std/net, ffi import chronos, chronicles, results, ffi
import import
waku/factory/waku, waku/factory/waku,
waku/node/waku_node, waku/node/waku_node,
waku/api/[api, types], waku/api/[api, types],
waku/events/[message_events, health_events], waku/events/[message_events, health_events],
tools/confutils/cli_args, tools/confutils/conf_from_json,
../declare_lib, ../declare_lib,
../json_event ../json_event
@ -15,59 +15,11 @@ proc `%`*(id: RequestId): JsonNode =
registerReqFFI(CreateNodeRequest, ctx: ptr FFIContext[Waku]): registerReqFFI(CreateNodeRequest, ctx: ptr FFIContext[Waku]):
proc(configJson: cstring): Future[Result[string, string]] {.async.} = proc(configJson: cstring): Future[Result[string, string]] {.async.} =
## Parse the JSON configuration using fieldPairs approach (WakuNodeConf) let conf = parseConfJson($configJson).valueOr:
var conf = defaultWakuNodeConf().valueOr: error "Failed to assemble WakuNodeConf from JSON",
return err("Failed creating default conf: " & error) 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: ctx.myLib[] = (await api.createNode(conf)).valueOr:
let errMsg = $error let errMsg = $error
chronicles.error "CreateNodeRequest failed", err = errMsg chronicles.error "CreateNodeRequest failed", err = errMsg
@ -96,7 +48,7 @@ proc logosdelivery_create_node(
): pointer {.dynlib, exportc, cdecl.} = ): pointer {.dynlib, exportc, cdecl.} =
initializeLibrary() initializeLibrary()
if isNil(callback): if callback.isNil():
echo "error: missing callback in logosdelivery_create_node" echo "error: missing callback in logosdelivery_create_node"
return nil return nil

View File

@ -3,6 +3,7 @@
import import
./test_entry_nodes, ./test_entry_nodes,
./test_node_conf, ./test_node_conf,
./test_messaging_conf,
./test_api_send, ./test_api_send,
./test_api_subscription, ./test_api_subscription,
./test_api_receive, ./test_api_receive,

View File

@ -96,7 +96,7 @@ suite "LM API health checking":
conf.listenAddress = parseIpAddress("0.0.0.0") conf.listenAddress = parseIpAddress("0.0.0.0")
conf.tcpPort = Port(0) conf.tcpPort = Port(0)
conf.discv5UdpPort = Port(0) conf.discv5UdpPort = Port(0)
conf.clusterId = 3'u16 conf.clusterId = some(3'u16)
conf.numShardsInNetwork = 1 conf.numShardsInNetwork = 1
conf.rest = false conf.rest = false
@ -273,7 +273,7 @@ suite "LM API health checking":
edgeConf.listenAddress = parseIpAddress("0.0.0.0") edgeConf.listenAddress = parseIpAddress("0.0.0.0")
edgeConf.tcpPort = Port(0) edgeConf.tcpPort = Port(0)
edgeConf.discv5UdpPort = Port(0) edgeConf.discv5UdpPort = Port(0)
edgeConf.clusterId = 3'u16 edgeConf.clusterId = some(3'u16)
edgeConf.maxMessageSize = "150 KiB" edgeConf.maxMessageSize = "150 KiB"
edgeConf.rest = false edgeConf.rest = false

View File

@ -67,9 +67,9 @@ proc createApiNodeConf(numShards: uint16 = 1): WakuNodeConf =
conf.listenAddress = parseIpAddress("0.0.0.0") conf.listenAddress = parseIpAddress("0.0.0.0")
conf.tcpPort = Port(0) conf.tcpPort = Port(0)
conf.discv5UdpPort = Port(0) conf.discv5UdpPort = Port(0)
conf.clusterId = 3'u16 conf.clusterId = some(3'u16)
conf.numShardsInNetwork = numShards conf.numShardsInNetwork = numShards
conf.reliabilityEnabled = true conf.reliabilityEnabled = some(true)
conf.rest = false conf.rest = false
result = conf result = conf

View File

@ -124,9 +124,9 @@ proc createApiNodeConf(mode: cli_args.WakuMode = cli_args.WakuMode.Core): WakuNo
conf.listenAddress = parseIpAddress("0.0.0.0") conf.listenAddress = parseIpAddress("0.0.0.0")
conf.tcpPort = Port(0) conf.tcpPort = Port(0)
conf.discv5UdpPort = Port(0) conf.discv5UdpPort = Port(0)
conf.clusterId = 3'u16 conf.clusterId = some(3'u16)
conf.numShardsInNetwork = 1 conf.numShardsInNetwork = 1
conf.reliabilityEnabled = true conf.reliabilityEnabled = some(true)
conf.rest = false conf.rest = false
result = conf result = conf

View File

@ -75,9 +75,9 @@ proc createApiNodeConf(
conf.listenAddress = parseIpAddress("0.0.0.0") conf.listenAddress = parseIpAddress("0.0.0.0")
conf.tcpPort = Port(0) conf.tcpPort = Port(0)
conf.discv5UdpPort = Port(0) conf.discv5UdpPort = Port(0)
conf.clusterId = 3'u16 conf.clusterId = some(3'u16)
conf.numShardsInNetwork = numShards conf.numShardsInNetwork = numShards
conf.reliabilityEnabled = true conf.reliabilityEnabled = some(true)
conf.rest = false conf.rest = false
result = conf result = conf

View File

@ -0,0 +1,179 @@
{.used.}
import std/[algorithm, json, options, sequtils], results, testutils/unittests
import tools/confutils/conf_from_json, tools/confutils/cli_args
import tools/confutils/messaging_conf
suite "Messaging conf JSON parser":
test "Routes to messaging shape when mode and overrides are present":
let res = parseConfJson("""{"mode": "Core", "overrides": {}}""")
require res.isOk()
let conf = res.get()
check conf.mode == cli_args.WakuMode.Core
test "Routes to full conf shape when only mode key is present":
let res = parseConfJson("""{"mode": "Edge"}""")
require res.isOk()
let conf = res.get()
check conf.mode == cli_args.WakuMode.Edge
test "Messaging shape applies overrides":
let res = parseConfJson(
"""{"mode": "Core", "overrides": {"clusterId": 42, "tcpPort": 12345}}"""
)
require res.isOk()
let conf = res.get()
check:
conf.clusterId == some(42'u16)
conf.tcpPort == Port(12345)
test "Messaging shape applies preset":
let res = parseConfJson("""{"mode": "Core", "preset": "twn", "overrides": {}}""")
require res.isOk()
let conf = res.get()
check conf.preset == "twn"
test "Messaging shape applies additions to list fields":
let res = parseConfJson(
"""{"mode": "Core", "overrides": {}, "additions": {"staticnodes": ["/ip4/1.2.3.4/tcp/60000/p2p/16Uiu2HAmTUbnxLGT9JvV6mu9oPyDjqHK4Phs1VDJNUgESgNSkuby"]}}"""
)
require res.isOk()
let conf = res.get()
check conf.staticnodes.len == 1
test "Messaging shape: additions concat after overrides on same list field":
let res = parseConfJson(
"""{"mode": "Core", "additions": {"staticnodes": ["/ip4/1.2.3.4/tcp/60000/p2p/16Uiu2HAmTUbnxLGT9JvV6mu9oPyDjqHK4Phs1VDJNUgESgNSkuby"]}, "overrides": {"staticnodes": ["/ip4/5.6.7.8/tcp/60000/p2p/16Uiu2HAmTUbnxLGT9JvV6mu9oPyDjqHK4Phs1VDJNUgESgNSkuby"]}}"""
)
require res.isOk()
let conf = res.get()
check:
conf.staticnodes.len == 2
conf.staticnodes[0] ==
"/ip4/5.6.7.8/tcp/60000/p2p/16Uiu2HAmTUbnxLGT9JvV6mu9oPyDjqHK4Phs1VDJNUgESgNSkuby"
conf.staticnodes[1] ==
"/ip4/1.2.3.4/tcp/60000/p2p/16Uiu2HAmTUbnxLGT9JvV6mu9oPyDjqHK4Phs1VDJNUgESgNSkuby"
test "Messaging shape rejects missing mode":
let res = parseConfJson("""{"overrides": {}}""")
check res.isErr()
test "Messaging shape rejects unknown override field":
let res = parseConfJson("""{"mode": "Core", "overrides": {"bogusField": 1}}""")
check res.isErr()
test "Messaging shape rejects addition on non-list field":
let res = parseConfJson(
"""{"mode": "Core", "overrides": {}, "additions": {"clusterId": [1]}}"""
)
check res.isErr()
test "Messaging shape rejects unknown top-level key":
let res = parseConfJson("""{"mode": "Core", "overrides": {}, "garbage": 1}""")
check res.isErr()
test "Full conf shape parses arbitrary WakuNodeConf fields":
let res = parseConfJson("""{"clusterId": 7, "tcpPort": 22222}""")
require res.isOk()
let conf = res.get()
check:
conf.clusterId == some(7'u16)
conf.tcpPort == Port(22222)
test "Full conf shape rejects unknown field":
let res = parseConfJson("""{"completelyMadeUp": 1}""")
check res.isErr()
test "Malformed JSON returns error":
let res = parseConfJson("{ not json }")
check res.isErr()
test "Rejects top-level JSON array":
let res = parseConfJson("""[1, 2]""")
check res.isErr()
test "Rejects top-level scalar":
let res = parseConfJson("""42""")
check res.isErr()
test "Rejects top-level null":
let res = parseConfJson("""null""")
check res.isErr()
test "Messaging shape rejects 'mode' inside 'overrides'":
let res = parseConfJson("""{"mode": "Core", "overrides": {"mode": "Edge"}}""")
check res.isErr()
test "Messaging shape rejects 'preset' inside 'overrides'":
let res = parseConfJson(
"""{"mode": "Core", "preset": "twn", "overrides": {"preset": "logos.dev"}}"""
)
check res.isErr()
test "Messaging shape rejects 'mode' inside 'additions'":
let res = parseConfJson(
"""{"mode": "Core", "overrides": {}, "additions": {"mode": "Edge"}}"""
)
check res.isErr()
test "Messaging shape rejects 'preset' inside 'additions'":
let res = parseConfJson(
"""{"mode": "Core", "overrides": {}, "additions": {"preset": "twn"}}"""
)
check res.isErr()
test "Rejects duplicate normalized keys":
let res = parseConfJson("""{"clusterId": 1, "ClusterId": 2}""")
check res.isErr()
test "Case-insensitive override matching":
let res = parseConfJson("""{"mode": "Core", "overrides": {"CLUSTERID": 99}}""")
require res.isOk()
let conf = res.get()
check conf.clusterId == some(99'u16)
test "Rejects 'overrides' that isn't a JSON object":
let res = parseConfJson("""{"mode": "Core", "overrides": "not an object"}""")
check res.isErr()
test "Rejects 'additions' that isn't a JSON object":
let res = parseConfJson(
"""{"mode": "Core", "overrides": {}, "additions": ["not an object"]}"""
)
check res.isErr()
test "JBool maps to Option[bool] field":
let res = parseConfJson("""{"mode": "Core", "overrides": {"rlnRelay": true}}""")
require res.isOk()
let conf = res.get()
check conf.rlnRelay == some(true)
suite "WakuNodeConfOverlay structure":
proc fieldNamesOfWakuNodeConf(): seq[string] =
var c: WakuNodeConf
for name, _ in fieldPairs(c):
result.add(name)
proc fieldNamesOfOverlay(): seq[string] =
var o: WakuNodeConfOverlay
for name, _ in fieldPairs(o):
result.add(name)
test "Overlay field names match WakuNodeConf minus excludes":
let expected =
fieldNamesOfWakuNodeConf().filterIt(it notin WakuNodeConfOverlayExcludes)
let actual = fieldNamesOfOverlay()
check sorted(actual) == sorted(expected)
test "Every overlay field is Option-typed":
var o: WakuNodeConfOverlay
var allOption = true
for _, value in fieldPairs(o):
when typeof(value) isnot Option:
allOption = false
check allOption
test "Excluded names are absent from overlay":
let actual = fieldNamesOfOverlay()
for excluded in WakuNodeConfOverlayExcludes:
check excluded notin actual

View File

@ -37,7 +37,7 @@ suite "WakuNodeConf - mode-driven toWakuConf":
var conf = defaultWakuNodeConf().valueOr: var conf = defaultWakuNodeConf().valueOr:
raiseAssert error raiseAssert error
conf.mode = Core conf.mode = Core
conf.clusterId = 1 conf.clusterId = some(1'u16)
## When ## When
let wakuConfRes = conf.toWakuConf() let wakuConfRes = conf.toWakuConf()
@ -58,7 +58,7 @@ suite "WakuNodeConf - mode-driven toWakuConf":
var conf = defaultWakuNodeConf().valueOr: var conf = defaultWakuNodeConf().valueOr:
raiseAssert error raiseAssert error
conf.mode = Edge conf.mode = Edge
conf.clusterId = 1 conf.clusterId = some(1'u16)
## When ## When
let wakuConfRes = conf.toWakuConf() let wakuConfRes = conf.toWakuConf()
@ -81,7 +81,7 @@ suite "WakuNodeConf - mode-driven toWakuConf":
conf.mode = cli_args.WakuMode.noMode conf.mode = cli_args.WakuMode.noMode
conf.relay = true conf.relay = true
conf.lightpush = false conf.lightpush = false
conf.clusterId = 5 conf.clusterId = some(5'u16)
## When ## When
let wakuConfRes = conf.toWakuConf() let wakuConfRes = conf.toWakuConf()
@ -122,7 +122,7 @@ suite "WakuNodeConf - JSON parsing with fieldPairs":
let conf = confRes.get() let conf = confRes.get()
check: check:
conf.mode == cli_args.WakuMode.noMode conf.mode == cli_args.WakuMode.noMode
conf.clusterId == 0 conf.clusterId.isNone()
conf.logLevel == logging.LogLevel.INFO conf.logLevel == logging.LogLevel.INFO
test "JSON with mode and clusterId": test "JSON with mode and clusterId":
@ -134,7 +134,7 @@ suite "WakuNodeConf - JSON parsing with fieldPairs":
let conf = confRes.get() let conf = confRes.get()
check: check:
conf.mode == Core conf.mode == Core
conf.clusterId == 42 conf.clusterId == some(42'u16)
test "JSON with Edge mode": test "JSON with Edge mode":
## Given / When ## Given / When
@ -165,7 +165,7 @@ suite "WakuNodeConf - JSON parsing with fieldPairs":
require confRes.isOk() require confRes.isOk()
let conf = confRes.get() let conf = confRes.get()
check: check:
conf.clusterId == 99 conf.clusterId == some(99'u16)
conf.numShardsInNetwork == 16 conf.numShardsInNetwork == 16
test "JSON with unknown fields is silently ignored": test "JSON with unknown fields is silently ignored":
@ -177,7 +177,7 @@ suite "WakuNodeConf - JSON parsing with fieldPairs":
require confRes.isOk() require confRes.isOk()
let conf = confRes.get() let conf = confRes.get()
check: check:
conf.clusterId == 5 conf.clusterId == some(5'u16)
test "Invalid JSON syntax returns error": test "Invalid JSON syntax returns error":
## Given / When ## Given / When

View File

@ -206,8 +206,9 @@ suite "Waku Conf - build with cluster conf":
assert conf.rlnRelayConf.isSome assert conf.rlnRelayConf.isSome
let rlnRelayConf = conf.rlnRelayConf.get() let rlnRelayConf = conf.rlnRelayConf.get()
check rlnRelayConf.ethContractAddress.string == # actually match the explicit contractAddress, which is the value set on the builder above
networkConf.rlnRelayEthContractAddress # 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.dynamic == networkConf.rlnRelayDynamic
check rlnRelayConf.chainId == networkConf.rlnRelayChainId check rlnRelayConf.chainId == networkConf.rlnRelayChainId
check rlnRelayConf.epochSizeSec == networkConf.rlnEpochSizeSec check rlnRelayConf.epochSizeSec == networkConf.rlnEpochSizeSec
@ -247,10 +248,6 @@ suite "Waku Conf - build with cluster conf":
let networkConf = NetworkConf.LogosDevConf() let networkConf = NetworkConf.LogosDevConf()
var builder = WakuConfBuilder.init() var builder = WakuConfBuilder.init()
builder.withNetworkConf(networkConf) 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 ## When
let conf = builder.build().expect("build should succeed") let conf = builder.build().expect("build should succeed")

View File

@ -4,21 +4,22 @@ import chronos, testutils/unittests, std/options
import waku import waku
import tools/confutils/cli_args import tools/confutils/cli_args
import waku/factory/networks_config
suite "Waku API - Create node": suite "Waku API - Create node":
asyncTest "Create node with minimal configuration": asyncTest "Create node with minimal configuration":
## Given ## Given
var nodeConf = defaultWakuNodeConf().valueOr: var nodeConf = defaultWakuNodeConf().valueOr:
raiseAssert error raiseAssert "defaultWakuNodeConf failed: " & error
nodeConf.mode = Core nodeConf.mode = Core
nodeConf.clusterId = 3'u16 nodeConf.clusterId = some(3'u16)
nodeConf.rest = false nodeConf.rest = false
# This is the actual minimal config but as the node auto-start, it is not suitable for tests # This is the actual minimal config but as the node auto-start, it is not suitable for tests
## When ## When
let node = (await createNode(nodeConf)).valueOr: let node = (await createNode(nodeConf)).valueOr:
raiseAssert error raiseAssert "createNode (minimal config) failed: " & error
## Then ## Then
check: check:
@ -29,9 +30,9 @@ suite "Waku API - Create node":
asyncTest "Create node with full configuration": asyncTest "Create node with full configuration":
## Given ## Given
var nodeConf = defaultWakuNodeConf().valueOr: var nodeConf = defaultWakuNodeConf().valueOr:
raiseAssert error raiseAssert "defaultWakuNodeConf failed: " & error
nodeConf.mode = Core nodeConf.mode = Core
nodeConf.clusterId = 99'u16 nodeConf.clusterId = some(99'u16)
nodeConf.rest = false nodeConf.rest = false
nodeConf.numShardsInNetwork = 16 nodeConf.numShardsInNetwork = 16
nodeConf.maxMessageSize = "1024 KiB" nodeConf.maxMessageSize = "1024 KiB"
@ -44,7 +45,7 @@ suite "Waku API - Create node":
## When ## When
let node = (await createNode(nodeConf)).valueOr: let node = (await createNode(nodeConf)).valueOr:
raiseAssert error raiseAssert "createNode (full config) failed: " & error
## Then ## Then
check: check:
@ -61,9 +62,9 @@ suite "Waku API - Create node":
asyncTest "Create node with mixed entry nodes (enrtree, multiaddr)": asyncTest "Create node with mixed entry nodes (enrtree, multiaddr)":
## Given ## Given
var nodeConf = defaultWakuNodeConf().valueOr: var nodeConf = defaultWakuNodeConf().valueOr:
raiseAssert error raiseAssert "defaultWakuNodeConf failed: " & error
nodeConf.mode = Core nodeConf.mode = Core
nodeConf.clusterId = 42'u16 nodeConf.clusterId = some(42'u16)
nodeConf.rest = false nodeConf.rest = false
nodeConf.entryNodes = @[ nodeConf.entryNodes = @[
"enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im",
@ -72,7 +73,7 @@ suite "Waku API - Create node":
## When ## When
let node = (await createNode(nodeConf)).valueOr: let node = (await createNode(nodeConf)).valueOr:
raiseAssert error raiseAssert "createNode (mixed entry nodes) failed: " & error
## Then ## Then
check: check:
@ -86,3 +87,89 @@ suite "Waku API - Create node":
node.conf.staticNodes.len == 1 node.conf.staticNodes.len == 1
node.conf.staticNodes[0] == node.conf.staticNodes[0] ==
"/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc" "/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)

View File

@ -43,7 +43,7 @@ logScope:
topics = "waku cli args" topics = "waku cli args"
# Git version in git describe format (defined at compile time) # Git version in git describe format (defined at compile time)
const git_version* {.strdefine.} = "n/a" const git_version* {.strdefine.} = "(unknown)"
type ConfResult*[T] = Result[T, string] type ConfResult*[T] = Result[T, string]
@ -117,20 +117,23 @@ type WakuNodeConf* = object
name: "rln-relay-eth-private-key" name: "rln-relay-eth-private-key"
.}: string .}: 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* {. rlnRelayUserMessageLimit* {.
desc: desc:
"Set a user message limit for the rln membership registration. Must be a positive integer. Default is 1.", "Set a user message limit for the rln membership registration. Must be a positive integer. Default is " &
defaultValue: 1, $DefaultRlnRelayUserMessageLimit & ".",
defaultValue: none(uint64),
name: "rln-relay-user-message-limit" 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* {. rlnEpochSizeSec* {.
desc: desc:
"Epoch size in seconds used to rate limit RLN memberships. Default is 1 second.", "Epoch size in seconds used to rate limit RLN memberships. Default is " &
defaultValue: 1, $DefaultRlnRelayEpochSizeSec & " second.",
defaultValue: none(uint64),
name: "rln-relay-epoch-sec" name: "rln-relay-epoch-sec"
.}: uint64 .}: Option[uint64]
maxMessageSize* {. maxMessageSize* {.
desc: desc:
@ -170,15 +173,18 @@ type WakuNodeConf* = object
name: "preset" name: "preset"
.}: string .}: string
# Option-typed; desc states the default since the CLI can't auto-show it for none().
clusterId* {. clusterId* {.
desc: desc: static(
"Cluster id that the node is running in. Node in a different cluster id is disconnected.", "Cluster id that the node is running in. Node in a different cluster id is disconnected. Default is " &
defaultValue: 0, $DefaultClusterId & "."
),
defaultValue: none(uint16),
name: "cluster-id" name: "cluster-id"
.}: uint16 .}: Option[uint16]
agentString* {. agentString* {.
defaultValue: "logos-delivery-" & cli_args.git_version, defaultValue: "logos-delivery " & cli_args.git_version,
desc: "Node agent string which is used as identifier in network", desc: "Node agent string which is used as identifier in network",
name: "agent-string" name: "agent-string"
.}: string .}: string
@ -291,11 +297,14 @@ hence would have reachability issues.""",
name: "relay-shard-manager" name: "relay-shard-manager"
.}: bool .}: bool
# Option-typed; desc states the default since the CLI can't auto-show it for none().
rlnRelay* {. rlnRelay* {.
desc: "Enable spam protection through rln-relay: true|false.", desc:
defaultValue: false, "Enable spam protection through rln-relay: true|false. Default is " &
$DefaultRlnRelayEnabled & ".",
defaultValue: none(bool),
name: "rln-relay" name: "rln-relay"
.}: bool .}: Option[bool]
rlnRelayCredIndex* {. rlnRelayCredIndex* {.
desc: "the index of the onchain commitment to use", desc: "the index of the onchain commitment to use",
@ -304,9 +313,9 @@ hence would have reachability issues.""",
rlnRelayDynamic* {. rlnRelayDynamic* {.
desc: "Enable waku-rln-relay with on-chain dynamic group management: true|false.", desc: "Enable waku-rln-relay with on-chain dynamic group management: true|false.",
defaultValue: false, defaultValue: none(bool),
name: "rln-relay-dynamic" name: "rln-relay-dynamic"
.}: bool .}: Option[bool]
entryNodes* {. entryNodes* {.
desc: desc:
@ -466,13 +475,14 @@ hence would have reachability issues.""",
.}: string .}: string
## Reliability config ## Reliability config
# Option-typed; desc states the default since the CLI can't auto-show it for none().
reliabilityEnabled* {. reliabilityEnabled* {.
desc: desc:
"""Adds an extra effort in the delivery/reception of messages by leveraging store-v3 requests. """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 """ &
with the drawback of consuming some more bandwidth.""", $DefaultP2pReliability & ".",
defaultValue: true, defaultValue: none(bool),
name: "reliability" name: "reliability"
.}: bool .}: Option[bool]
## REST HTTP config ## REST HTTP config
rest* {. rest* {.
@ -557,8 +567,11 @@ with the drawback of consuming some more bandwidth.""",
.}: string .}: string
## Discovery v5 config ## Discovery v5 config
# Option-typed; desc states the default since the CLI can't auto-show it for none().
discv5Discovery* {. discv5Discovery* {.
desc: "Enable discovering nodes via Node Discovery v5.", desc:
"Enable discovering nodes via Node Discovery v5. Default is " &
$DefaultDiscv5Enabled & ".",
defaultValue: none(bool), defaultValue: none(bool),
name: "discv5-discovery" name: "discv5-discovery"
.}: Option[bool] .}: Option[bool]
@ -627,8 +640,12 @@ with the drawback of consuming some more bandwidth.""",
.}: bool .}: bool
#Mix config #Mix config
mix* {.desc: "Enable mix protocol: true|false", defaultValue: false, name: "mix".}: # Option-typed; desc states the default since the CLI can't auto-show it for none().
bool mix* {.
desc: "Enable mix protocol: true|false. Default is " & $DefaultMix & ".",
defaultValue: none(bool),
name: "mix"
.}: Option[bool]
mixkey* {. mixkey* {.
desc: desc:
@ -643,12 +660,14 @@ with the drawback of consuming some more bandwidth.""",
.}: seq[MixNodePubInfo] .}: seq[MixNodePubInfo]
# Kademlia Discovery config # Kademlia Discovery config
# Option-typed; desc states the default since the CLI can't auto-show it for none().
enableKadDiscovery* {. enableKadDiscovery* {.
desc: desc:
"Enable extended kademlia discovery. Can be enabled without bootstrap nodes for the first node in the network.", "Enable extended kademlia discovery. Can be enabled without bootstrap nodes for the first node in the network. Default is " &
defaultValue: false, $DefaultKadEnabled & ".",
defaultValue: none(bool),
name: "enable-kad-discovery" name: "enable-kad-discovery"
.}: bool .}: Option[bool]
kadBootstrapNodes* {. kadBootstrapNodes* {.
desc: desc:
@ -913,7 +932,7 @@ proc toKeystoreGeneratorConf*(n: WakuNodeConf): RlnKeystoreGeneratorConf =
chainId: UInt256.fromBytesBE(n.rlnRelayChainId.toBytesBE()), chainId: UInt256.fromBytesBE(n.rlnRelayChainId.toBytesBE()),
ethClientUrls: n.ethClientUrls.mapIt(string(it)), ethClientUrls: n.ethClientUrls.mapIt(string(it)),
ethContractAddress: n.rlnRelayEthContractAddress, ethContractAddress: n.rlnRelayEthContractAddress,
userMessageLimit: n.rlnRelayUserMessageLimit, userMessageLimit: n.rlnRelayUserMessageLimit.get(DefaultRlnRelayUserMessageLimit),
ethPrivateKey: n.rlnRelayEthPrivateKey, ethPrivateKey: n.rlnRelayEthPrivateKey,
credPath: n.rlnRelayCredPath, credPath: n.rlnRelayCredPath,
credPassword: n.rlnRelayCredPassword, credPassword: n.rlnRelayCredPassword,
@ -947,7 +966,7 @@ proc toNetworkConf(
proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] =
var b = WakuConfBuilder.init() 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) return err("Error determining cluster from preset: " & $error)
if networkConf.isSome(): if networkConf.isSome():
@ -956,7 +975,8 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] =
b.withLogLevel(n.logLevel) b.withLogLevel(n.logLevel)
b.withLogFormat(n.logFormat) b.withLogFormat(n.logFormat)
b.rlnRelayConf.withEnabled(n.rlnRelay) if n.rlnRelay.isSome():
b.rlnRelayConf.withEnabled(n.rlnRelay.get())
if n.rlnRelayCredPath != "": if n.rlnRelayCredPath != "":
b.rlnRelayConf.withCredPath(n.rlnRelayCredPath) b.rlnRelayConf.withCredPath(n.rlnRelayCredPath)
if n.rlnRelayCredPassword != "": if n.rlnRelayCredPassword != "":
@ -968,18 +988,22 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] =
if n.rlnRelayChainId != 0: if n.rlnRelayChainId != 0:
b.rlnRelayConf.withChainId(n.rlnRelayChainId) b.rlnRelayConf.withChainId(n.rlnRelayChainId)
b.rlnRelayConf.withUserMessageLimit(n.rlnRelayUserMessageLimit) if n.rlnRelayUserMessageLimit.isSome():
b.rlnRelayConf.withEpochSizeSec(n.rlnEpochSizeSec) b.rlnRelayConf.withUserMessageLimit(n.rlnRelayUserMessageLimit.get())
if n.rlnEpochSizeSec.isSome():
b.rlnRelayConf.withEpochSizeSec(n.rlnEpochSizeSec.get())
if n.rlnRelayCredIndex.isSome(): if n.rlnRelayCredIndex.isSome():
b.rlnRelayConf.withCredIndex(n.rlnRelayCredIndex.get()) b.rlnRelayConf.withCredIndex(n.rlnRelayCredIndex.get())
b.rlnRelayConf.withDynamic(n.rlnRelayDynamic) if n.rlnRelayDynamic.isSome():
b.rlnRelayConf.withDynamic(n.rlnRelayDynamic.get())
if n.maxMessageSize != "": if n.maxMessageSize != "":
b.withMaxMessageSize(n.maxMessageSize) b.withMaxMessageSize(n.maxMessageSize)
b.withProtectedShards(n.protectedShards) b.withProtectedShards(n.protectedShards)
b.withClusterId(n.clusterId) if n.clusterId.isSome():
b.withClusterId(n.clusterId.get())
b.withAgentString(n.agentString) b.withAgentString(n.agentString)
@ -1033,7 +1057,7 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] =
if n.numShardsInNetwork != 0: if n.numShardsInNetwork != 0:
b.withNumShardsInCluster(n.numShardsInNetwork) b.withNumShardsInCluster(n.numShardsInNetwork)
b.withShardingConf(AutoSharding) b.withShardingConf(AutoSharding)
else: elif networkConf.isNone():
b.withShardingConf(StaticSharding) b.withShardingConf(StaticSharding)
# It is not possible to pass an empty sequence on the CLI # 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.withRangeSec(n.storeSyncRange)
b.storeServiceConf.storeSyncConf.withRelayJitterSec(n.storeSyncRelayJitter) 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.mixConf.withMixNodes(n.mixnodes)
b.withMix(n.mix)
if n.mixkey.isSome(): if n.mixkey.isSome():
b.mixConf.withMixKey(n.mixkey.get()) b.mixConf.withMixKey(n.mixkey.get())
@ -1078,7 +1103,8 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] =
b.filterServiceConf.withMaxCriteria(n.filterMaxCriteria) b.filterServiceConf.withMaxCriteria(n.filterMaxCriteria)
b.withLightPush(n.lightpush) b.withLightPush(n.lightpush)
b.withP2pReliability(n.reliabilityEnabled) if n.reliabilityEnabled.isSome():
b.withP2pReliability(n.reliabilityEnabled.get())
b.restServerConf.withEnabled(n.rest) b.restServerConf.withEnabled(n.rest)
b.restServerConf.withListenAddress(n.restAddress) b.restServerConf.withListenAddress(n.restAddress)
@ -1119,7 +1145,8 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] =
if n.rateLimits.len > 0: if n.rateLimits.len > 0:
b.rateLimitConf.withRateLimits(n.rateLimits) 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) b.kademliaDiscoveryConf.withBootstrapNodes(n.kadBootstrapNodes)
# Mode-driven configuration overrides # Mode-driven configuration overrides

View File

@ -0,0 +1,243 @@
import std/[json, strutils, tables]
import confutils, confutils/std/net, results
import ./cli_args
const
KeyMode = "mode"
KeyPreset = "preset"
KeyOverrides = "overrides"
KeyAdditions = "additions"
const TopLevelOnlyKeys = [KeyMode, KeyPreset]
## Keys that the messaging shape requires at the top-level of the JSON input.
## They must not appear inside `overrides` or `additions`.
proc collectJsonFields*(
jsonNode: JsonNode
): Result[Table[string, (string, JsonNode)], string] =
## Walk the top-level JSON object and key it by lowercased names.
if jsonNode.kind != JObject:
return err("config JSON must be a JSON object, got " & $jsonNode.kind)
var jsonFields: Table[string, (string, JsonNode)]
for key, value in jsonNode:
let lowerKey = key.toLowerAscii()
if jsonFields.hasKey(lowerKey):
let firstKey = jsonFields[lowerKey][0]
return err(
"Duplicate configuration option (case-insensitive): '" & firstKey & "' and '" &
key & "'"
)
jsonFields[lowerKey] = (key, value)
return ok(jsonFields)
proc unknownKeysError(
jsonFields: Table[string, (string, JsonNode)], prefix: string
): string =
## Format leftover JSON keys as an error message.
var keys = newSeq[string]()
for _, (jsonKey, _) in pairs(jsonFields):
keys.add(jsonKey)
return prefix & ": " & $keys
proc rejectTopLevelOnlyKeysInside(
node: JsonNode, blockName: string
): Result[void, string] =
## Error if `node` contains any key from `TopLevelOnlyKeys`.
for k, _ in node:
if k.toLowerAscii() in TopLevelOnlyKeys:
return err("'" & k & "' must be a top-level key, not inside '" & blockName & "'")
return ok()
proc jsonScalarToString(node: JsonNode): Result[string, string] =
## Convert a scalar JSON value to its string form.
case node.kind
of JString:
return ok(node.getStr())
of JInt:
return ok($node.getInt())
of JFloat:
return ok($node.getFloat())
of JBool:
return ok($node.getBool())
of JNull:
return ok("")
else:
return err("expected scalar JSON value, got " & $node.kind)
proc applyJsonFieldsToConf(
conf: var WakuNodeConf,
jsonFields: var Table[string, (string, JsonNode)],
parseErrPrefix: string,
unknownErrPrefix: string,
): Result[void, string] =
## Walk `conf`'s fields and write each one matched (case-insensitive) by
## `jsonFields`. seq fields take a JArray (full replace); scalar fields
## take any scalar JSON kind. Errors on leftover unknown keys.
for confField, confValue in fieldPairs(conf):
let lowerField = confField.toLowerAscii()
if jsonFields.hasKey(lowerField):
let (jsonKey, jsonValue) = jsonFields[lowerField]
when confValue is seq:
if jsonValue.kind != JArray:
return err(
parseErrPrefix & " '" & confField & "' from JSON key '" & jsonKey &
"' must be a JSON array"
)
var newSeq: typeof(confValue) = @[]
for item in jsonValue:
let formattedItem = jsonScalarToString(item).valueOr:
return err(
parseErrPrefix & " '" & confField & "' from JSON key '" & jsonKey & "': " &
error
)
try:
type ElemType = typeof(confValue[0])
newSeq.add(parseCmdArg(ElemType, formattedItem))
except CatchableError as e:
return err(
parseErrPrefix & " '" & confField & "' from JSON key '" & jsonKey & "': " &
e.msg & ". Value: " & formattedItem
)
confValue = newSeq
else:
let formattedString = jsonScalarToString(jsonValue).valueOr:
return err(
parseErrPrefix & " '" & confField & "' from JSON key '" & jsonKey & "': " &
error
)
try:
confValue = parseCmdArg(typeof(confValue), formattedString)
except CatchableError as e:
return err(
parseErrPrefix & " '" & confField & "' from JSON key '" & jsonKey & "': " &
e.msg & ". Value: " & formattedString
)
jsonFields.del(lowerField)
if jsonFields.len > 0:
return err(unknownKeysError(jsonFields, unknownErrPrefix))
return ok()
proc applyJsonAsOverride*(
conf: var WakuNodeConf, overrides: JsonNode
): Result[void, string] =
## Apply `overrides` JSON onto `conf` with replace semantics for both scalars and lists.
var jsonFields = ?collectJsonFields(overrides)
return applyJsonFieldsToConf(
conf, jsonFields, "Failed to parse override field",
"Unrecognized override field(s) found",
)
proc applyJsonAsAddition*(
conf: var WakuNodeConf, additions: JsonNode
): Result[void, string] =
## Append JSON array in `additions` to `conf` seq fields.
var jsonFields = ?collectJsonFields(additions)
for confField, confValue in fieldPairs(conf):
let lowerField = confField.toLowerAscii()
if jsonFields.hasKey(lowerField):
let (jsonKey, jsonValue) = jsonFields[lowerField]
when confValue is seq:
if jsonValue.kind != JArray:
return err(
"Addition field '" & confField & "' from JSON key '" & jsonKey &
"' must be a JSON array"
)
for item in jsonValue:
let formattedItem = jsonScalarToString(item).valueOr:
return err(
"Failed to parse addition item for field '" & confField & "': " & error
)
try:
type ElemType = typeof(confValue[0])
confValue.add(parseCmdArg(ElemType, formattedItem))
except CatchableError as e:
return err(
"Failed to parse addition item for field '" & confField & "': " & e.msg &
". Value: " & formattedItem
)
else:
return err(
"Field '" & confField & "' from JSON key '" & jsonKey &
"' is not a list and cannot be in additions"
)
jsonFields.del(lowerField)
if jsonFields.len > 0:
return err(unknownKeysError(jsonFields, "Unrecognized addition field(s) found"))
return ok()
proc assembleMessagingConf*(
jsonFields: Table[string, (string, JsonNode)]
): Result[WakuNodeConf, string] =
## Build a WakuNodeConf from the messaging shape
## `{mode, overrides, preset?, additions?}`. `mode` and `overrides` are
## required. Order: overrides applied first, then additions concat.
var conf = ?defaultWakuNodeConf()
var fields = jsonFields
if not fields.hasKey(KeyMode):
return err("messaging shape requires '" & KeyMode & "' key")
if not fields.hasKey(KeyOverrides):
return err("messaging shape requires '" & KeyOverrides & "' key")
let modeStr = jsonScalarToString(fields[KeyMode][1]).valueOr:
return err("Failed to parse '" & KeyMode & "': " & error)
try:
conf.mode = parseCmdArg(WakuMode, modeStr)
except CatchableError as e:
return err("Failed to parse '" & KeyMode & "': " & e.msg & ". Value: " & modeStr)
fields.del(KeyMode)
if fields.hasKey(KeyPreset):
let presetStr = jsonScalarToString(fields[KeyPreset][1]).valueOr:
return err("Failed to parse '" & KeyPreset & "': " & error)
conf.preset = presetStr
fields.del(KeyPreset)
let overridesNode = fields[KeyOverrides][1]
if overridesNode.kind != JObject:
return err("'" & KeyOverrides & "' must be a JSON object")
?rejectTopLevelOnlyKeysInside(overridesNode, KeyOverrides)
?applyJsonAsOverride(conf, overridesNode)
fields.del(KeyOverrides)
if fields.hasKey(KeyAdditions):
let additionsNode = fields[KeyAdditions][1]
if additionsNode.kind != JObject:
return err("'" & KeyAdditions & "' must be a JSON object")
?rejectTopLevelOnlyKeysInside(additionsNode, KeyAdditions)
?applyJsonAsAddition(conf, additionsNode)
fields.del(KeyAdditions)
if fields.len > 0:
return
err(unknownKeysError(fields, "Unrecognized top-level key(s) in messaging shape"))
return ok(conf)
proc assembleFullConf*(
jsonFields: Table[string, (string, JsonNode)]
): Result[WakuNodeConf, string] =
## Build a WakuNodeConf from a flat JSON object whose keys are WakuNodeConf field names.
var conf = ?defaultWakuNodeConf()
var fields = jsonFields
?applyJsonFieldsToConf(
conf, fields, "Failed to parse field", "Unrecognized configuration option(s) found"
)
return ok(conf)
proc parseConfJson*(jsonStr: string): Result[WakuNodeConf, string] =
## Parse a JSON config, route to messaging or full-config shape based on
## whether `overrides` or `additions` fields are in the config object top-level.
var jsonNode: JsonNode
try:
jsonNode = parseJson(jsonStr)
except CatchableError as e:
return err("Failed to parse config JSON: " & e.msg)
let jsonFields = ?collectJsonFields(jsonNode)
let isMessagingShape =
jsonFields.hasKey(KeyOverrides) or jsonFields.hasKey(KeyAdditions)
if isMessagingShape:
return assembleMessagingConf(jsonFields)
else:
return assembleFullConf(jsonFields)

View File

@ -0,0 +1,37 @@
{.push raises: [].}
import std/options
import results
import ./cli_args
import ./optionalize
const WakuNodeConfOverlayExcludes* = ["cmd", "execute", "mode", "preset"]
# Generates the WakuNodeConfOverlay type from the WakuNodeConf type.
# The generated type converts fields from type T to Option[T] if T != Option.
# Skips fields that are in WakuNodeConfOverlayExcludes.
optionalizeType(WakuNodeConfOverlay, WakuNodeConf, WakuNodeConfOverlayExcludes)
proc init*(T: type WakuNodeConfOverlay): WakuNodeConfOverlay =
## Default config overlay where every field is `none`.
return WakuNodeConfOverlay()
proc applyAsOverride*(conf: var WakuNodeConf, overlay: WakuNodeConfOverlay) =
## For all fields, overlay.some() overrides field of same name in conf.
for confName, confValue in fieldPairs(conf):
for ovName, ovValue in fieldPairs(overlay):
when confName == ovName:
if ovValue.isSome():
when typeof(confValue) is Option:
confValue = ovValue
else:
confValue = ovValue.get()
proc applyAsAddition*(conf: var WakuNodeConf, overlay: WakuNodeConfOverlay) =
## For all seq fields, overlay.some() concats to field of same name in conf.
for confName, confValue in fieldPairs(conf):
for ovName, ovValue in fieldPairs(overlay):
when confName == ovName:
when typeof(confValue) is seq:
if ovValue.isSome() and ovValue.get().len > 0:
confValue = confValue & ovValue.get()

View File

@ -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)

View File

@ -1,4 +1,4 @@
import chronicles, chronos, results import chronicles, chronos, results, std/options
import waku/factory/waku import waku/factory/waku
import waku/[requests/health_requests, waku_core, waku_node] import waku/[requests/health_requests, waku_core, waku_node]
@ -6,9 +6,10 @@ import waku/node/delivery_service/send_service
import waku/node/delivery_service/subscription_manager import waku/node/delivery_service/subscription_manager
import libp2p/peerid import libp2p/peerid
import ../../tools/confutils/cli_args import ../../tools/confutils/cli_args
import ../../tools/confutils/messaging_conf
import ./[api_conf, types] import ./[api_conf, types]
export cli_args export cli_args, messaging_conf
logScope: logScope:
topics = "api" topics = "api"
@ -24,6 +25,20 @@ proc createNode*(conf: WakuNodeConf): Future[Result[Waku, string]] {.async.} =
return ok(wakuRes) return ok(wakuRes)
proc createNode*(
preset = "",
mode = cli_args.WakuMode.Core,
overrides = WakuNodeConfOverlay.init(),
additions = WakuNodeConfOverlay.init(),
): Future[Result[Waku, string]] {.async.} =
var conf = defaultWakuNodeConf().valueOr:
return err("Failed creating default conf: " & error)
conf.mode = mode
conf.preset = preset
applyAsOverride(conf, overrides)
applyAsAddition(conf, additions)
return await createNode(conf)
proc checkApiAvailability(w: Waku): Result[void, string] = proc checkApiAvailability(w: Waku): Result[void, string] =
if w.isNil(): if w.isNil():
return err("Waku node is not initialized") return err("Waku node is not initialized")

View File

@ -4,6 +4,14 @@ import ../waku_conf
logScope: logScope:
topics = "waku conf builder discv5" topics = "waku conf builder discv5"
const
DefaultDiscv5Enabled*: bool = false
DefaultDiscv5BitsPerHop*: int = 1
DefaultDiscv5BucketIpLimit*: uint = 2
DefaultDiscv5EnrAutoUpdate*: bool = true
DefaultDiscv5TableIpLimit*: uint = 10
DefaultDiscv5UdpPort*: Port = Port(0)
########################### ###########################
## Discv5 Config Builder ## ## Discv5 Config Builder ##
########################### ###########################
@ -46,18 +54,18 @@ proc withBootstrapNodes*(b: var Discv5ConfBuilder, bootstrapNodes: seq[string])
b.bootstrapNodes = concat(b.bootstrapNodes, bootstrapNodes) b.bootstrapNodes = concat(b.bootstrapNodes, bootstrapNodes)
proc build*(b: Discv5ConfBuilder): Result[Option[Discv5Conf], string] = 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(none(Discv5Conf))
return ok( return ok(
some( some(
Discv5Conf( Discv5Conf(
bootstrapNodes: b.bootstrapNodes, bootstrapNodes: b.bootstrapNodes,
bitsPerHop: b.bitsPerHop.get(1), bitsPerHop: b.bitsPerHop.get(DefaultDiscv5BitsPerHop),
bucketIpLimit: b.bucketIpLimit.get(2), bucketIpLimit: b.bucketIpLimit.get(DefaultDiscv5BucketIpLimit),
enrAutoUpdate: b.enrAutoUpdate.get(true), enrAutoUpdate: b.enrAutoUpdate.get(DefaultDiscv5EnrAutoUpdate),
tableIpLimit: b.tableIpLimit.get(10), tableIpLimit: b.tableIpLimit.get(DefaultDiscv5TableIpLimit),
udpPort: b.udpPort.get(Port(0)), udpPort: b.udpPort.get(DefaultDiscv5UdpPort),
) )
) )
) )

View File

@ -4,6 +4,12 @@ import ../waku_conf
logScope: logScope:
topics = "waku conf builder filter service" topics = "waku conf builder filter service"
const
DefaultFilterEnabled*: bool = false
DefaultFilterMaxPeersToServe*: uint32 = 500
DefaultFilterSubscriptionTimeout*: uint16 = 300
DefaultFilterMaxCriteria*: uint32 = 1000
################################### ###################################
## Filter Service Config Builder ## ## Filter Service Config Builder ##
################################### ###################################
@ -37,15 +43,15 @@ proc withMaxCriteria*(b: var FilterServiceConfBuilder, maxCriteria: uint32) =
b.maxCriteria = some(maxCriteria) b.maxCriteria = some(maxCriteria)
proc build*(b: FilterServiceConfBuilder): Result[Option[FilterServiceConf], string] = 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(none(FilterServiceConf))
return ok( return ok(
some( some(
FilterServiceConf( FilterServiceConf(
maxPeersToServe: b.maxPeersToServe.get(500), maxPeersToServe: b.maxPeersToServe.get(DefaultFilterMaxPeersToServe),
subscriptionTimeout: b.subscriptionTimeout.get(300), subscriptionTimeout: b.subscriptionTimeout.get(DefaultFilterSubscriptionTimeout),
maxCriteria: b.maxCriteria.get(1000), maxCriteria: b.maxCriteria.get(DefaultFilterMaxCriteria),
) )
) )
) )

View File

@ -5,18 +5,20 @@ import waku/factory/waku_conf
logScope: logScope:
topics = "waku conf builder kademlia discovery" topics = "waku conf builder kademlia discovery"
const DefaultKadEnabled*: bool = false
####################################### #######################################
## Kademlia Discovery Config Builder ## ## Kademlia Discovery Config Builder ##
####################################### #######################################
type KademliaDiscoveryConfBuilder* = object type KademliaDiscoveryConfBuilder* = object
enabled*: bool enabled*: Option[bool]
bootstrapNodes*: seq[string] bootstrapNodes*: seq[string]
proc init*(T: type KademliaDiscoveryConfBuilder): KademliaDiscoveryConfBuilder = proc init*(T: type KademliaDiscoveryConfBuilder): KademliaDiscoveryConfBuilder =
KademliaDiscoveryConfBuilder() KademliaDiscoveryConfBuilder()
proc withEnabled*(b: var KademliaDiscoveryConfBuilder, enabled: bool) = proc withEnabled*(b: var KademliaDiscoveryConfBuilder, enabled: bool) =
b.enabled = enabled b.enabled = some(enabled)
proc withBootstrapNodes*( proc withBootstrapNodes*(
b: var KademliaDiscoveryConfBuilder, bootstrapNodes: seq[string] b: var KademliaDiscoveryConfBuilder, bootstrapNodes: seq[string]
@ -27,7 +29,7 @@ proc build*(
b: KademliaDiscoveryConfBuilder b: KademliaDiscoveryConfBuilder
): Result[Option[KademliaDiscoveryConf], string] = ): Result[Option[KademliaDiscoveryConf], string] =
# Kademlia is enabled if explicitly enabled OR if bootstrap nodes are provided # 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: if not enabled:
return ok(none(KademliaDiscoveryConf)) return ok(none(KademliaDiscoveryConf))

View File

@ -4,6 +4,12 @@ import ../waku_conf
logScope: logScope:
topics = "waku conf builder metrics server" topics = "waku conf builder metrics server"
const
DefaultMetricsEnabled*: bool = false
DefaultMetricsHttpAddress*: IpAddress = static parseIpAddress("127.0.0.1")
DefaultMetricsHttpPort*: Port = Port(0)
DefaultMetricsLogging*: bool = false
################################### ###################################
## Metrics Server Config Builder ## ## Metrics Server Config Builder ##
################################### ###################################
@ -33,15 +39,15 @@ proc withLogging*(b: var MetricsServerConfBuilder, logging: bool) =
b.logging = some(logging) b.logging = some(logging)
proc build*(b: MetricsServerConfBuilder): Result[Option[MetricsServerConf], string] = 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(none(MetricsServerConf))
return ok( return ok(
some( some(
MetricsServerConf( MetricsServerConf(
httpAddress: b.httpAddress.get(static parseIpAddress("127.0.0.1")), httpAddress: b.httpAddress.get(DefaultMetricsHttpAddress),
httpPort: b.httpPort.get(Port(0)), httpPort: b.httpPort.get(DefaultMetricsHttpPort),
logging: b.logging.get(false), logging: b.logging.get(DefaultMetricsLogging),
) )
) )
) )

View File

@ -5,6 +5,8 @@ import ../waku_conf, waku/waku_mix
logScope: logScope:
topics = "waku conf builder mix" topics = "waku conf builder mix"
const DefaultMixEnabled*: bool = false
################################## ##################################
## Mix Config Builder ## ## Mix Config Builder ##
################################## ##################################
@ -26,7 +28,7 @@ proc withMixNodes*(b: var MixConfBuilder, mixNodes: seq[MixNodePubInfo]) =
b.mixNodes = mixNodes b.mixNodes = mixNodes
proc build*(b: MixConfBuilder): Result[Option[MixConf], string] = proc build*(b: MixConfBuilder): Result[Option[MixConf], string] =
if not b.enabled.get(false): if not b.enabled.get(DefaultMixEnabled):
return ok(none[MixConf]()) return ok(none[MixConf]())
else: else:
if b.mixKey.isSome(): if b.mixKey.isSome():

View File

@ -4,6 +4,11 @@ import ../waku_conf
logScope: logScope:
topics = "waku conf builder rest server" topics = "waku conf builder rest server"
const
DefaultRestEnabled*: bool = false
DefaultRestPort*: Port = Port(0)
DefaultRestAdmin*: bool = false
################################ ################################
## REST Server Config Builder ## ## REST Server Config Builder ##
################################ ################################
@ -41,7 +46,7 @@ proc withRelayCacheCapacity*(b: var RestServerConfBuilder, relayCacheCapacity: u
b.relayCacheCapacity = some(relayCacheCapacity) b.relayCacheCapacity = some(relayCacheCapacity)
proc build*(b: RestServerConfBuilder): Result[Option[RestServerConf], string] = proc build*(b: RestServerConfBuilder): Result[Option[RestServerConf], string] =
if not b.enabled.get(false): if not b.enabled.get(DefaultRestEnabled):
return ok(none(RestServerConf)) return ok(none(RestServerConf))
if b.listenAddress.isNone(): if b.listenAddress.isNone():
@ -54,8 +59,8 @@ proc build*(b: RestServerConfBuilder): Result[Option[RestServerConf], string] =
RestServerConf( RestServerConf(
allowOrigin: b.allowOrigin, allowOrigin: b.allowOrigin,
listenAddress: b.listenAddress.get(), listenAddress: b.listenAddress.get(),
port: b.port.get(Port(0)), port: b.port.get(DefaultRestPort),
admin: b.admin.get(false), admin: b.admin.get(DefaultRestAdmin),
relayCacheCapacity: b.relayCacheCapacity.get(), relayCacheCapacity: b.relayCacheCapacity.get(),
) )
) )

View File

@ -4,6 +4,11 @@ import ../waku_conf
logScope: logScope:
topics = "waku conf builder rln relay" topics = "waku conf builder rln relay"
const
DefaultRlnRelayEnabled*: bool = false
DefaultRlnRelayEpochSizeSec*: uint64 = 1
DefaultRlnRelayUserMessageLimit*: uint64 = 1
############################## ##############################
## RLN Relay Config Builder ## ## RLN Relay Config Builder ##
############################## ##############################
@ -56,7 +61,7 @@ proc withUserMessageLimit*(b: var RlnRelayConfBuilder, userMessageLimit: uint64)
b.userMessageLimit = some(userMessageLimit) b.userMessageLimit = some(userMessageLimit)
proc build*(b: RlnRelayConfBuilder): Result[Option[RlnRelayConf], string] = proc build*(b: RlnRelayConfBuilder): Result[Option[RlnRelayConf], string] =
if not b.enabled.get(false): if not b.enabled.get(DefaultRlnRelayEnabled):
return ok(none(RlnRelayConf)) return ok(none(RlnRelayConf))
if b.chainId.isNone(): if b.chainId.isNone():
@ -78,11 +83,6 @@ proc build*(b: RlnRelayConfBuilder): Result[Option[RlnRelayConf], string] =
return err("rlnRelay.ethClientUrls is not specified") return err("rlnRelay.ethClientUrls is not specified")
if b.ethContractAddress.get("") == "": if b.ethContractAddress.get("") == "":
return err("rlnRelay.ethContractAddress is not specified") 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( return ok(
some( some(
RlnRelayConf( RlnRelayConf(
@ -92,8 +92,8 @@ proc build*(b: RlnRelayConfBuilder): Result[Option[RlnRelayConf], string] =
dynamic: b.dynamic.get(), dynamic: b.dynamic.get(),
ethClientUrls: b.ethClientUrls.get(), ethClientUrls: b.ethClientUrls.get(),
ethContractAddress: b.ethContractAddress.get(), ethContractAddress: b.ethContractAddress.get(),
epochSizeSec: b.epochSizeSec.get(), epochSizeSec: b.epochSizeSec.get(DefaultRlnRelayEpochSizeSec),
userMessageLimit: b.userMessageLimit.get(), userMessageLimit: b.userMessageLimit.get(DefaultRlnRelayUserMessageLimit),
) )
) )
) )

View File

@ -5,6 +5,14 @@ import ../waku_conf, ./store_sync_conf_builder
logScope: logScope:
topics = "waku conf builder store service" 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 ## ## Store Service Config Builder ##
################################## ##################################
@ -77,7 +85,7 @@ proc validateRetentionPolicies(policies: seq[string]): Result[void, string] =
return ok() return ok()
proc build*(b: StoreServiceConfBuilder): Result[Option[StoreServiceConf], string] = proc build*(b: StoreServiceConfBuilder): Result[Option[StoreServiceConf], string] =
if not b.enabled.get(false): if not b.enabled.get(DefaultStoreEnabled):
return ok(none(StoreServiceConf)) return ok(none(StoreServiceConf))
if b.dbUrl.get("") == "": if b.dbUrl.get("") == "":
@ -88,7 +96,7 @@ proc build*(b: StoreServiceConfBuilder): Result[Option[StoreServiceConf], string
let retentionPolicies = let retentionPolicies =
if b.retentionPolicies.len == 0: if b.retentionPolicies.len == 0:
@["time:" & $2.days.seconds] @[DefaultStoreRetentionPolicy]
else: else:
validateRetentionPolicies(b.retentionPolicies).isOkOr: validateRetentionPolicies(b.retentionPolicies).isOkOr:
return err("invalid retention policies: " & error) return err("invalid retention policies: " & error)
@ -97,12 +105,12 @@ proc build*(b: StoreServiceConfBuilder): Result[Option[StoreServiceConf], string
return ok( return ok(
some( some(
StoreServiceConf( StoreServiceConf(
dbMigration: b.dbMigration.get(true), dbMigration: b.dbMigration.get(DefaultStoreDbMigration),
dbURl: b.dbUrl.get(), dbURl: b.dbUrl.get(),
dbVacuum: b.dbVacuum.get(false), dbVacuum: b.dbVacuum.get(DefaultStoreDbVacuum),
maxNumDbConnections: b.maxNumDbConnections.get(50), maxNumDbConnections: b.maxNumDbConnections.get(DefaultStoreMaxNumDbConnections),
retentionPolicies: retentionPolicies, retentionPolicies: retentionPolicies,
resume: b.resume.get(false), resume: b.resume.get(DefaultStoreResume),
storeSyncConf: storeSyncConf, storeSyncConf: storeSyncConf,
) )
) )

View File

@ -4,6 +4,8 @@ import ../waku_conf
logScope: logScope:
topics = "waku conf builder store sync" topics = "waku conf builder store sync"
const DefaultStoreSyncEnabled*: bool = false
################################## ##################################
## Store Sync Config Builder ## ## Store Sync Config Builder ##
################################## ##################################
@ -30,7 +32,7 @@ proc withRelayJitterSec*(b: var StoreSyncConfBuilder, relayJitterSec: uint32) =
b.relayJitterSec = some(relayJitterSec) b.relayJitterSec = some(relayJitterSec)
proc build*(b: StoreSyncConfBuilder): Result[Option[StoreSyncConf], string] = proc build*(b: StoreSyncConfBuilder): Result[Option[StoreSyncConf], string] =
if not b.enabled.get(false): if not b.enabled.get(DefaultStoreSyncEnabled):
return ok(none(StoreSyncConf)) return ok(none(StoreSyncConf))
if b.rangeSec.isNone(): if b.rangeSec.isNone():

View File

@ -13,6 +13,9 @@ import
factory/networks_config, factory/networks_config,
common/logging, common/logging,
common/utils/parse_size_units, common/utils/parse_size_units,
node/peer_manager,
waku_core/message/default_values,
waku_core/topics/pubsub_topic,
waku_enr/capabilities, waku_enr/capabilities,
], ],
tools/confutils/entry_nodes tools/confutils/entry_nodes
@ -34,7 +37,35 @@ import
logScope: logScope:
topics = "waku conf builder" topics = "waku conf builder"
const DefaultMaxConnections* = 150 # Picks up the same -d:git_version=... build flag that cli_args.nim defines.
const git_version {.strdefine.} = "(unknown)"
const
DefaultMaxConnections* = 150
DefaultRelay*: bool = false
DefaultLightPush*: bool = false
DefaultPeerExchange*: bool = false
DefaultStoreSyncMount*: bool = false
DefaultRendezvous*: bool = false
DefaultMix*: bool = false
DefaultRelayPeerExchange*: bool = false
DefaultLogLevel*: logging.LogLevel = logging.LogLevel.INFO
DefaultLogFormat*: logging.LogFormat = logging.LogFormat.TEXT
DefaultNatStrategy*: string = "none"
DefaultP2pTcpPort*: Port = Port(0)
DefaultP2pListenAddress*: IpAddress = static parseIpAddress("0.0.0.0")
DefaultPortsShift*: uint16 = 0
DefaultExtMultiAddrsOnly*: bool = false
DefaultDnsAddrsNameServers*: seq[IpAddress] =
@[static parseIpAddress("1.1.1.1"), static parseIpAddress("1.0.0.1")]
DefaultPeerPersistence*: bool = false
DefaultAgentString*: string = "logos-delivery " & git_version
DefaultRelayShardedPeerManagement*: bool = false
DefaultRelayServiceRatio*: string = "50:50"
DefaultCircuitRelayClient*: bool = false
DefaultP2pReliability*: bool = true
DefaultNumShardsInCluster*: uint16 = 1
DefaultShardingConfKind*: ShardingConfKind = AutoSharding
type MaxMessageSizeKind* = enum type MaxMessageSizeKind* = enum
mmskNone mmskNone
@ -301,117 +332,124 @@ proc buildShardingConf(
bNumShardsInCluster: Option[uint16], bNumShardsInCluster: Option[uint16],
bSubscribeShards: Option[seq[uint16]], bSubscribeShards: Option[seq[uint16]],
): (ShardingConf, seq[uint16]) = ): (ShardingConf, seq[uint16]) =
case bShardingConfKind.get(AutoSharding) case bShardingConfKind.get(DefaultShardingConfKind)
of StaticSharding: of StaticSharding:
(ShardingConf(kind: StaticSharding), bSubscribeShards.get(@[])) (ShardingConf(kind: StaticSharding), bSubscribeShards.get(@[]))
of AutoSharding: of AutoSharding:
let numShardsInCluster = bNumShardsInCluster.get(1) let numShardsInCluster = bNumShardsInCluster.get(DefaultNumShardsInCluster)
let shardingConf = let shardingConf =
ShardingConf(kind: AutoSharding, numShardsInCluster: numShardsInCluster) ShardingConf(kind: AutoSharding, numShardsInCluster: numShardsInCluster)
let upperShard = uint16(numShardsInCluster - 1) let upperShard = uint16(numShardsInCluster - 1)
(shardingConf, bSubscribeShards.get(toSeq(0.uint16 .. upperShard))) (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) = proc applyNetworkConf(builder: var WakuConfBuilder) =
# Apply network conf, overrides most values passed individually ## NetworkConf = network presets.
# If you want to tweak values, don't use networkConf ## Cascade the chosen preset's values onto builder fields the user hasn't set.
# TODO: networkconf should be one field of the conf builder so that this function becomes unnecessary ## 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(): if builder.networkConf.isNone():
return return # If there is no preset given, then nothing to do.
let networkConf = builder.networkConf.get() let networkConf = builder.networkConf.get()
if builder.clusterId.isSome(): checkSetPresetValueToField(
warn "Cluster id was provided alongside a network conf", builder.clusterId, networkConf.clusterId,
used = networkConf.clusterId, discarded = builder.clusterId.get() "Cluster id was provided alongside a network conf",
builder.clusterId = some(networkConf.clusterId) )
# Apply relay parameters # Apply relay parameters
if builder.relay.get(false) and networkConf.rlnRelay: if builder.relay.get(DefaultRelay) and networkConf.rlnRelay:
if builder.rlnRelayConf.enabled.isSome(): checkSetPresetValueToField(
warn "RLN Relay was provided alongside a network conf", builder.rlnRelayConf.enabled,
used = networkConf.rlnRelay, discarded = builder.rlnRelayConf.enabled networkConf.rlnRelay, # true
builder.rlnRelayConf.withEnabled(true) "RLN Relay was provided alongside a network conf",
)
if builder.rlnRelayConf.ethContractAddress.get("") != "": checkSetPresetValueToField(
warn "RLN Relay ETH Contract Address was provided alongside a network conf", builder.rlnRelayConf.ethContractAddress, networkConf.rlnRelayEthContractAddress,
used = networkConf.rlnRelayEthContractAddress.string, "RLN Relay ETH Contract Address was provided alongside a network conf",
discarded = builder.rlnRelayConf.ethContractAddress.get().string )
builder.rlnRelayConf.withEthContractAddress(networkConf.rlnRelayEthContractAddress) checkSetPresetValueToField(
builder.rlnRelayConf.chainId, networkConf.rlnRelayChainId,
if builder.rlnRelayConf.chainId.isSome(): "RLN Relay Chain Id was provided alongside a network conf",
warn "RLN Relay Chain Id was provided alongside a network conf", )
used = networkConf.rlnRelayChainId, discarded = builder.rlnRelayConf.chainId checkSetPresetValueToField(
builder.rlnRelayConf.withChainId(networkConf.rlnRelayChainId) builder.rlnRelayConf.dynamic, networkConf.rlnRelayDynamic,
"RLN Relay Dynamic was provided alongside a network conf",
if builder.rlnRelayConf.dynamic.isSome(): )
warn "RLN Relay Dynamic was provided alongside a network conf", checkSetPresetValueToField(
used = networkConf.rlnRelayDynamic, discarded = builder.rlnRelayConf.dynamic builder.rlnRelayConf.epochSizeSec, networkConf.rlnEpochSizeSec,
builder.rlnRelayConf.withDynamic(networkConf.rlnRelayDynamic) "RLN Epoch Size in Seconds was provided alongside a network conf",
)
if builder.rlnRelayConf.epochSizeSec.isSome(): checkSetPresetValueToField(
warn "RLN Epoch Size in Seconds was provided alongside a network conf", builder.rlnRelayConf.userMessageLimit, networkConf.rlnRelayUserMessageLimit,
used = networkConf.rlnEpochSizeSec, "RLN Relay User Message Limit was provided alongside a network conf",
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)
# End Apply relay parameters # End Apply relay parameters
case builder.maxMessageSize.kind case builder.maxMessageSize.kind
of mmskNone: of mmskNone:
discard builder.withMaxMessageSize(parseCorrectMsgSize(networkConf.maxMessageSize))
of mmskStr, mmskInt: of mmskStr, mmskInt:
warn "Max Message Size was provided alongside a network conf", warn "Max Message Size was provided alongside a network conf",
used = networkConf.maxMessageSize, discarded = $builder.maxMessageSize used = $builder.maxMessageSize, discarded = networkConf.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
checkSetPresetValueToField(
builder.shardingConf, networkConf.shardingConf.kind,
"Sharding Conf was provided alongside a network conf",
)
case networkConf.shardingConf.kind case networkConf.shardingConf.kind
of StaticSharding:
builder.shardingConf = some(StaticSharding)
of AutoSharding: of AutoSharding:
builder.shardingConf = some(AutoSharding) checkSetPresetValueToField(
if builder.numShardsInCluster.isSome(): builder.numShardsInCluster, networkConf.shardingConf.numShardsInCluster,
warn "Num Shards In Cluster overrides network conf preset", "Num Shards In Cluster overrides network conf preset",
used = builder.numShardsInCluster.get(), )
ignored = networkConf.shardingConf.numShardsInCluster of StaticSharding:
else: discard
builder.numShardsInCluster = some(networkConf.shardingConf.numShardsInCluster)
if networkConf.discv5Discovery: checkSetPresetValueToField(
if builder.discv5Conf.enabled.isNone: builder.discv5Conf.enabled, networkConf.discv5Discovery,
builder.discv5Conf.withEnabled(networkConf.discv5Discovery) "Discv5 Discovery was provided alongside a network conf",
)
checkAddPresetValueToField(
builder.discv5Conf.bootstrapNodes, networkConf.discv5BootstrapNodes
)
if builder.discv5Conf.bootstrapNodes.len == 0 and checkSetPresetValueToField(
networkConf.discv5BootstrapNodes.len > 0: builder.kademliaDiscoveryConf.enabled, networkConf.enableKadDiscovery,
warn "Discv5 Bootstrap nodes were provided alongside a network conf", "Kademlia Discovery was provided alongside a network conf",
used = networkConf.discv5BootstrapNodes, )
discarded = builder.discv5Conf.bootstrapNodes checkAddPresetValueToField(
builder.discv5Conf.withBootstrapNodes(networkConf.discv5BootstrapNodes) builder.kademliaDiscoveryConf.bootstrapNodes, networkConf.kadBootstrapNodes
)
if networkConf.enableKadDiscovery: checkSetPresetValueToField(
if not builder.kademliaDiscoveryConf.enabled: builder.mix, networkConf.mix, "Mix was provided alongside a network conf"
builder.kademliaDiscoveryConf.withEnabled(networkConf.enableKadDiscovery) )
checkSetPresetValueToField(
if builder.kademliaDiscoveryConf.bootstrapNodes.len == 0 and builder.p2pReliability, networkConf.p2pReliability,
networkConf.kadBootstrapNodes.len > 0: "P2P Reliability was provided alongside a network conf",
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)
# Process entry nodes from network config - classify and distribute # Process entry nodes from network config - classify and distribute
if networkConf.entryNodes.len > 0: if networkConf.entryNodes.len > 0:
@ -449,44 +487,44 @@ proc build*(
builder.relay.get() builder.relay.get()
else: else:
warn "whether to mount relay is not specified, defaulting to not mounting" warn "whether to mount relay is not specified, defaulting to not mounting"
false DefaultRelay
let lightPush = let lightPush =
if builder.lightPush.isSome(): if builder.lightPush.isSome():
builder.lightPush.get() builder.lightPush.get()
else: else:
warn "whether to mount lightPush is not specified, defaulting to not mounting" warn "whether to mount lightPush is not specified, defaulting to not mounting"
false DefaultLightPush
let peerExchange = let peerExchange =
if builder.peerExchange.isSome(): if builder.peerExchange.isSome():
builder.peerExchange.get() builder.peerExchange.get()
else: else:
warn "whether to mount peerExchange is not specified, defaulting to not mounting" warn "whether to mount peerExchange is not specified, defaulting to not mounting"
false DefaultPeerExchange
let storeSync = let storeSync =
if builder.storeSync.isSome(): if builder.storeSync.isSome():
builder.storeSync.get() builder.storeSync.get()
else: else:
warn "whether to mount storeSync is not specified, defaulting to not mounting" warn "whether to mount storeSync is not specified, defaulting to not mounting"
false DefaultStoreSyncMount
let rendezvous = let rendezvous =
if builder.rendezvous.isSome(): if builder.rendezvous.isSome():
builder.rendezvous.get() builder.rendezvous.get()
else: else:
warn "whether to mount rendezvous is not specified, defaulting to not mounting" warn "whether to mount rendezvous is not specified, defaulting to not mounting"
false DefaultRendezvous
let mix = let mix =
if builder.mix.isSome(): if builder.mix.isSome():
builder.mix.get() builder.mix.get()
else: else:
warn "whether to mount mix is not specified, defaulting to not mounting" 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) let nodeKey = ?nodeKey(builder, rng)
@ -495,7 +533,7 @@ proc build*(
# TODO: ClusterId should never be defaulted, instead, presets # TODO: ClusterId should never be defaulted, instead, presets
# should be defined and used # should be defined and used
warn("Cluster Id was not specified, defaulting to 0") warn("Cluster Id was not specified, defaulting to 0")
0.uint16 DefaultClusterId
else: else:
builder.clusterId.get().uint16 builder.clusterId.get().uint16
@ -514,8 +552,9 @@ proc build*(
of mmskStr: of mmskStr:
?parseMsgSize(builder.maxMessageSize.str) ?parseMsgSize(builder.maxMessageSize.str)
else: else:
warn "Max Message Size not specified, defaulting to 150KiB" warn "Max Message Size not specified, defaulting to DefaultMaxWakuMessageSize",
parseCorrectMsgSize("150KiB") default = DefaultMaxWakuMessageSizeStr
DefaultMaxWakuMessageSize
let contentTopics = builder.contentTopics.get(@[]) let contentTopics = builder.contentTopics.get(@[])
@ -560,37 +599,37 @@ proc build*(
builder.logLevel.get() builder.logLevel.get()
else: else:
warn "Log Level not specified, defaulting to INFO" warn "Log Level not specified, defaulting to INFO"
logging.LogLevel.INFO DefaultLogLevel
let logFormat = let logFormat =
if builder.logFormat.isSome(): if builder.logFormat.isSome():
builder.logFormat.get() builder.logFormat.get()
else: else:
warn "Log Format not specified, defaulting to TEXT" warn "Log Format not specified, defaulting to TEXT"
logging.LogFormat.TEXT DefaultLogFormat
let natStrategy = let natStrategy =
if builder.natStrategy.isSome(): if builder.natStrategy.isSome():
builder.natStrategy.get() builder.natStrategy.get()
else: else:
warn "Nat Strategy is not specified, defaulting to none" warn "Nat Strategy is not specified, defaulting to none"
"none" DefaultNatStrategy
let p2pTcpPort = builder.p2pTcpPort.get(Port(0)) let p2pTcpPort = builder.p2pTcpPort.get(DefaultP2pTcpPort)
let p2pListenAddress = let p2pListenAddress =
if builder.p2pListenAddress.isSome(): if builder.p2pListenAddress.isSome():
builder.p2pListenAddress.get() builder.p2pListenAddress.get()
else: else:
warn "P2P listening address not specified, listening on 0.0.0.0" warn "P2P listening address not specified, listening on 0.0.0.0"
(static parseIpAddress("0.0.0.0")) DefaultP2pListenAddress
let portsShift = let portsShift =
if builder.portsShift.isSome(): if builder.portsShift.isSome():
builder.portsShift.get() builder.portsShift.get()
else: else:
warn "Ports Shift is not specified, defaulting to 0" warn "Ports Shift is not specified, defaulting to 0"
0.uint16 DefaultPortsShift
let dns4DomainName = let dns4DomainName =
if builder.dns4DomainName.isSome(): if builder.dns4DomainName.isSome():
@ -613,21 +652,21 @@ proc build*(
builder.extMultiAddrsOnly.get() builder.extMultiAddrsOnly.get()
else: else:
warn "Whether to only announce external multiaddresses is not specified, defaulting to false" warn "Whether to only announce external multiaddresses is not specified, defaulting to false"
false DefaultExtMultiAddrsOnly
let dnsAddrsNameServers = let dnsAddrsNameServers =
if builder.dnsAddrsNameServers.len != 0: if builder.dnsAddrsNameServers.len != 0:
builder.dnsAddrsNameServers builder.dnsAddrsNameServers
else: else:
warn "DNS name servers IPs not provided, defaulting to Cloudflare's." 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 = let peerPersistence =
if builder.peerPersistence.isSome(): if builder.peerPersistence.isSome():
builder.peerPersistence.get() builder.peerPersistence.get()
else: else:
warn "Peer persistence not specified, defaulting to false" warn "Peer persistence not specified, defaulting to false"
false DefaultPeerPersistence
let maxConnections = let maxConnections =
if builder.maxConnections.isSome(): if builder.maxConnections.isSome():
@ -641,15 +680,13 @@ proc build*(
warn "max-connections less than DefaultMaxConnections; we suggest using DefaultMaxConnections or more for better connectivity", warn "max-connections less than DefaultMaxConnections; we suggest using DefaultMaxConnections or more for better connectivity",
provided = maxConnections, recommended = DefaultMaxConnections provided = maxConnections, recommended = DefaultMaxConnections
# TODO: Do the git version thing here let agentString = builder.agentString.get(DefaultAgentString)
let agentString = builder.agentString.get("logos-delivery")
# TODO: use `DefaultColocationLimit`. the user of this value should let colocationLimit = builder.colocationLimit.get(DefaultColocationLimit)
# probably be defining a config object
let colocationLimit = builder.colocationLimit.get(5)
# TODO: is there a strategy for experimental features? delete vs promote # 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( let wakuFlags = CapabilitiesBitfield.init(
lightpush = lightPush and relay, lightpush = lightPush and relay,
@ -710,12 +747,12 @@ proc build*(
agentString: agentString, agentString: agentString,
colocationLimit: colocationLimit, colocationLimit: colocationLimit,
maxRelayPeers: builder.maxRelayPeers, maxRelayPeers: builder.maxRelayPeers,
relayServiceRatio: builder.relayServiceRatio.get("50:50"), relayServiceRatio: builder.relayServiceRatio.get(DefaultRelayServiceRatio),
rateLimit: rateLimit, rateLimit: rateLimit,
circuitRelayClient: builder.circuitRelayClient.get(false), circuitRelayClient: builder.circuitRelayClient.get(DefaultCircuitRelayClient),
staticNodes: builder.staticNodes, staticNodes: builder.staticNodes,
relayShardedPeerManagement: relayShardedPeerManagement, relayShardedPeerManagement: relayShardedPeerManagement,
p2pReliability: builder.p2pReliability.get(false), p2pReliability: builder.p2pReliability.get(DefaultP2pReliability),
wakuFlags: wakuFlags, wakuFlags: wakuFlags,
) )

View File

@ -4,6 +4,11 @@ import waku/factory/waku_conf
logScope: logScope:
topics = "waku conf builder websocket" topics = "waku conf builder websocket"
const
DefaultWebSocketEnabled*: bool = false
DefaultWebSocketSecureEnabled*: bool = false
DefaultWebSocketPort*: Port = Port(0)
############################## ##############################
## WebSocket Config Builder ## ## WebSocket Config Builder ##
############################## ##############################
@ -38,14 +43,15 @@ proc withCertPath*(b: var WebSocketConfBuilder, certPath: string) =
b.certPath = some(certPath) b.certPath = some(certPath)
proc build*(b: WebSocketConfBuilder): Result[Option[WebSocketConf], string] = proc build*(b: WebSocketConfBuilder): Result[Option[WebSocketConf], string] =
if not b.enabled.get(false): if not b.enabled.get(DefaultWebSocketEnabled):
return ok(none(WebSocketConf)) return ok(none(WebSocketConf))
if not b.secureEnabled.get(false): if not b.secureEnabled.get(DefaultWebSocketSecureEnabled):
return ok( return ok(
some( some(
WebSocketConf( WebSocketConf(
port: b.webSocketPort.get(Port(0)), secureConf: none(WebSocketSecureConf) port: b.webSocketPort.get(DefaultWebSocketPort),
secureConf: none(WebSocketSecureConf),
) )
) )
) )
@ -58,7 +64,7 @@ proc build*(b: WebSocketConfBuilder): Result[Option[WebSocketConf], string] =
return ok( return ok(
some( some(
WebSocketConf( WebSocketConf(
port: b.webSocketPort.get(Port(0)), port: b.webSocketPort.get(DefaultWebSocketPort),
secureConf: some( secureConf: some(
WebSocketSecureConf(keyPath: b.keyPath.get(), certPath: b.certPath.get()) WebSocketSecureConf(keyPath: b.keyPath.get(), certPath: b.certPath.get())
), ),

View File

@ -1,6 +1,7 @@
{.push raises: [].} {.push raises: [].}
import chronicles, results, stint import chronicles, results, stint
import waku/waku_core/message/default_values
logScope: logScope:
topics = "waku networks conf" topics = "waku networks conf"
@ -17,7 +18,7 @@ type
of StaticSharding: of StaticSharding:
discard discard
type NetworkConf* = object type NetworkConf* = object ## A network "preset" (--preset=twn, --preset=logos.dev).
maxMessageSize*: string # TODO: static convert to a uint64 maxMessageSize*: string # TODO: static convert to a uint64
clusterId*: uint16 clusterId*: uint16
rlnRelay*: bool rlnRelay*: bool
@ -41,7 +42,7 @@ type NetworkConf* = object
proc TheWakuNetworkConf*(T: type NetworkConf): NetworkConf = proc TheWakuNetworkConf*(T: type NetworkConf): NetworkConf =
const RelayChainId = 59141'u256 const RelayChainId = 59141'u256
return NetworkConf( return NetworkConf(
maxMessageSize: "150KiB", maxMessageSize: DefaultMaxWakuMessageSizeStr,
clusterId: 1, clusterId: 1,
rlnRelay: true, rlnRelay: true,
rlnRelayEthContractAddress: "0xB9cd878C90E49F797B4431fBF4fb333108CB90e6", rlnRelayEthContractAddress: "0xB9cd878C90E49F797B4431fBF4fb333108CB90e6",
@ -68,7 +69,7 @@ proc TheWakuNetworkConf*(T: type NetworkConf): NetworkConf =
proc LogosDevConf*(T: type NetworkConf): NetworkConf = proc LogosDevConf*(T: type NetworkConf): NetworkConf =
const ZeroChainId = 0'u256 const ZeroChainId = 0'u256
return NetworkConf( return NetworkConf(
maxMessageSize: "150KiB", maxMessageSize: DefaultMaxWakuMessageSizeStr,
clusterId: 2, clusterId: 2,
rlnRelay: false, rlnRelay: false,
rlnRelayEthContractAddress: "", rlnRelayEthContractAddress: "",