docs: document the three createNode config shapes (#79)

* docs: document createNode config shapes for the three user roles

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: drop port defaulting, make the storage-path default shape-aware

The injected top-level keys forced every config through the legacy flat
parser and broke the layered shapes ('kernel entry layer takes only
kernelConf'). Ports now follow upstream defaults; the per-instance
storage path goes inside kernelConf when the config carries one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: place the storage-path default in messagingOverrides for structured configs

Requires logos-delivery#4083 (localStoragePath on MessagingClientConf).
Pins logos-delivery to that branch; re-lock to master once it merges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: tighten config-defaults comments

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: re-lock logos-delivery to master with #4083 and #4084

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: route wrapper-less layered configs through messagingOverrides

A plain {mode, preset} config previously fell to the legacy flat parser
because the injected top-level storage path counts as a bare kernel
field. Creating messagingOverrides for it keeps the storage path set and
gives layered configs the structured defaults (ephemeral p2p ports).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Igor Sirotin
2026-07-31 09:33:17 +01:00
committed by GitHub
co-authored by Claude Fable 5
parent ee8d231fa3
commit 531e61e11c
4 changed files with 124 additions and 119 deletions
+34 -34
View File
@@ -103,50 +103,50 @@ The delivery module provides the following API methods (all synchronous, all ret
### Node Configuration (`createNode`)
`createNode` accepts a **flat** JSON object whose keys correspond to `WakuNodeConf`
field names (camelCase) from
[logos-delivery](https://github.com/logos-messaging/logos-delivery).
Unknown keys are silently ignored. Every field has a built-in default, so only
values that differ from defaults need to be supplied.
The JSON config is passed verbatim to
[logos-delivery](https://github.com/logos-messaging/logos-delivery), which owns
the grammar (`parseLogosDeliveryConf`). `entryLayer` selects how much of the
stack is mounted: `"kernel"` (transport node only), `"messaging"` (+ messaging
client), `"channels"` (+ reliable channels, the default).
#### Commonly used keys
Three typical shapes:
| Key | Type | Default | Description |
|----------------------|------------------|------------|------------------------------------------|
| `mode` | string | `"noMode"` | `"Core"`, `"Edge"`, or `"noMode"` |
| `preset` | string | `""` | Network preset (`"logos.test"`, `"logos.dev"`, `"twn"`) |
| `clusterId` | number (uint16) | `0` | Cluster identifier |
| `entryNodes` | array of string | `[]` | Bootstrap peers (enrtree / multiaddress) |
| `relay` | boolean | `false` | Enable relay protocol |
| `rlnRelay` | boolean | `false` | Enable RLN rate-limit nullifier |
| `tcpPort` | number (uint16) | `60000` | P2P TCP listen port |
| `numShardsInNetwork` | number (uint16) | `1` | Auto-sharding shard count |
| `logLevel` | string | `"INFO"` | `"TRACE"`, `"DEBUG"`, `"INFO"`, `"WARN"` |
| `logFormat` | string | `"TEXT"` | `"TEXT"` or `"JSON"` |
| `maxMessageSize` | string | `"150KiB"` | Maximum message payload size |
**App developer** — full stack (default `entryLayer`). `preset` picks the
network (`"logos.test"`, `"logos.dev"`, `"twn"`), `mode` picks the protocol
flags (`"Core"` = relay node, `"Edge"` = light node). Optional
`messagingOverrides` / `channelsOverrides` objects override per-layer defaults:
#### Presets
```json
{ "mode": "Core", "preset": "logos.test" }
```
Using a `preset` populates cluster ID, entry nodes, sharding, RLN, and other
network-specific defaults automatically. Individual keys supplied alongside a
preset override the preset values.
- `"logos.test"` Logos Test fleet (the default for running a node; mix
enabled, p2pReliability on, auto-shards, built-in bootstrap nodes).
- `"logos.dev"` Logos Dev Network (cluster 2, mix enabled, p2pReliability on,
8 auto-shards, built-in bootstrap nodes).
- `"twn"` The RLN-protected Waku Network (cluster 1).
Minimal example using the default `logos.test` preset:
**Node operator** — kernel-only service node on a public network. `mode` is not
applied on this layer, so protocol flags are set explicitly in `kernelConf`:
```json
{
"logLevel": "INFO",
"mode": "Core",
"preset": "logos.test"
"entryLayer": "kernel",
"kernelConf": { "preset": "logos.test", "relay": true }
}
```
**Network hoster** — kernel-only node on a self-hosted network; `kernelConf` is
a raw `WakuNodeConf` used as-is:
```json
{
"entryLayer": "kernel",
"kernelConf": { "clusterId": 42, "relay": true, "entryNodes": ["/dns4/…"] }
}
```
On kernel-only nodes `send` / `subscribe` / `channel*` fail with "node has no
messaging client" / "no reliable channel manager"; `getNodeInfo`, `storeQuery`
and metrics keep working.
The pre-layered flat shape (bare `WakuNodeConf` keys at top level) still parses
and boots the full stack.
### Content Topics
Content topics identify message channels for publishing and subscribing. Use a
Generated
+4 -4
View File
@@ -3824,11 +3824,11 @@
"zerokit": "zerokit"
},
"locked": {
"lastModified": 1785353753,
"narHash": "sha256-+hMflwMeA5aRhNV+XHhWqeXCYfpjHbcmXFFlfks7tnw=",
"lastModified": 1785443689,
"narHash": "sha256-MHPA2HSHjK6xeo+a1GrDQK80IBMmBShv7W10jZmeykI=",
"ref": "refs/heads/master",
"rev": "ed8e881c1d36af2d033de49289fa392fc6b7a092",
"revCount": 2419,
"rev": "f8b036594ea2a36b529e10b584b7d2851a3ac5c8",
"revCount": 2425,
"submodules": true,
"type": "git",
"url": "https://github.com/logos-messaging/logos-delivery"
+54 -37
View File
@@ -207,37 +207,46 @@ void DeliveryModuleImpl::event_callback(int callerRet, const char* msg, size_t l
}
}
// True when cfgObj already carries one of `names`. The upstream JSON conf
// parser keys fields case-insensitively and matches either the Nim field name
// or its CLI `name:` pragma, so a caller may legitimately spell a key several
// ways; matching the same way keeps us from overriding their value.
static bool containsAnyKey(const nlohmann::json& cfgObj,
std::initializer_list<const char*> names)
static std::string toLowerCopy(std::string s)
{
for (auto& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
return s;
}
// Case-insensitive key lookup, matching keys the same way as the upstream
// conf parser. Returns the key as spelled in the config.
static std::optional<std::string> findKey(const nlohmann::json& cfgObj,
std::initializer_list<const char*> names)
{
for (const auto& entry : cfgObj.items()) {
std::string key = entry.key();
for (auto& c : key) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
const std::string key = toLowerCopy(entry.key());
for (const char* name : names) {
if (key == name) return true;
if (key == name) return entry.key();
}
}
return std::nullopt;
}
// True when the config is the legacy flat shape: any top-level key besides the
// ones the layered parser consumes marks a bare WakuNodeConf field.
static bool isFlatShape(const nlohmann::json& cfgObj)
{
for (const auto& entry : cfgObj.items()) {
const std::string key = toLowerCopy(entry.key());
if (key != "entrylayer" && key != "mode" && key != "preset"
&& key != "kernelconf" && key != "messagingoverrides"
&& key != "channelsoverrides") {
return true;
}
}
return false;
}
// Default every listening port (tcpPort, discv5UdpPort, restPort,
// metricsServerPort, websocketPort) to 0 so the OS assigns an ephemeral port
// when the caller did not pin a specific value. Caller-supplied ports are
// preserved so fleet configs that pin ports keep working. logos-delivery now
// accepts port 0 (status-im/nim-confutils#146), which makes this work.
// See logos-delivery-module#18.
//
// Also default the node's storage directory to the per-instance path the host
// provisions for this module. logos-delivery otherwise falls back to "./data"
// (persistency.nim DefaultStoragePath), which is relative to the process
// working directory and therefore identical for every instance launched from
// it — side-by-side instances would share one SQLite file. The path is empty
// when the module runs outside a host that provisions persistence (unit tests
// constructing the impl directly), in which case upstream's default stands.
// Defaults the node's storage directory to the host's per-instance path, so
// side-by-side instances don't share upstream's cwd-relative "./data". The
// path goes where each config shape accepts it: kernelConf when present,
// messagingOverrides (created if needed) for the layered shapes, top level
// for the legacy flat shape.
static std::optional<std::string> applyConfigDefaults(const std::string& cfg,
const std::string& persistencePath)
{
@@ -254,21 +263,29 @@ static std::optional<std::string> applyConfigDefaults(const std::string& cfg,
return std::nullopt;
}
for (const char* portKey : {
"tcpPort",
"discv5UdpPort",
"restPort",
"metricsServerPort",
"websocketPort",
}) {
if (!cfgObj.contains(portKey)) {
cfgObj[portKey] = 0;
if (!persistencePath.empty()) {
nlohmann::json* target = &cfgObj;
const auto entryLayerKey = findKey(cfgObj, {"entrylayer"});
const bool kernelEntry = entryLayerKey && cfgObj[*entryLayerKey].is_string()
&& toLowerCopy(cfgObj[*entryLayerKey].get<std::string>()) == "kernel";
if (auto kernelConfKey = findKey(cfgObj, {"kernelconf"});
kernelConfKey && cfgObj[*kernelConfKey].is_object()) {
target = &cfgObj[*kernelConfKey];
} else if (kernelEntry) {
// Kernel entry without a kernelConf object: leave the config
// untouched for the parser to reject.
target = nullptr;
} else if (!isFlatShape(cfgObj)) {
auto overridesKey = findKey(cfgObj, {"messagingoverrides"});
if (!overridesKey) {
cfgObj["messagingOverrides"] = nlohmann::json::object();
overridesKey = "messagingOverrides";
}
target = cfgObj[*overridesKey].is_object() ? &cfgObj[*overridesKey] : nullptr;
}
if (target && !findKey(*target, {"localstoragepath", "local-storage-path"})) {
(*target)["localStoragePath"] = persistencePath + "/data";
}
}
if (!persistencePath.empty()
&& !containsAnyKey(cfgObj, {"localstoragepath", "local-storage-path"})) {
cfgObj["localStoragePath"] = persistencePath + "/data";
}
return cfgObj.dump();
+32 -44
View File
@@ -50,64 +50,52 @@ public:
~DeliveryModuleImpl();
/**
* @brief Creates a liblogosdelivery node from a WakuNodeConf JSON document.
* @brief Creates a liblogosdelivery node from a JSON configuration.
*
* The JSON is parsed by logos-delivery (liblogosdelivery folder) side and maps to
* `WakuNodeConf` from `tools/confutils/cli_args.nim`
* (https://github.com/logos-messaging/logos-delivery).
* The JSON passes through to logos-delivery verbatim; `parseLogosDeliveryConf`
* (https://github.com/logos-messaging/logos-delivery) owns the grammar.
* `entryLayer` selects how much of the stack is mounted:
* - `"kernel"` — transport node only
* - `"messaging"` — kernel + messaging client
* - `"channels"` — kernel + messaging + reliable channels (default)
*
* The configuration is a **flat** JSON object whose keys correspond to
* `WakuNodeConf` Nim field names (camelCase). Unknown keys are silently
* ignored. Every field has a built-in default, so only the values that
* differ from defaults need to be supplied.
* Three typical shapes:
*
* ## Commonly used keys
* | Key | Type | Default | Description |
* |----------------------|------------------|------------|---------------------------------------------|
* | `mode` | string | `"noMode"` | `"Core"`, `"Edge"`, or `"noMode"` |
* | `preset` | string | `""` | Network preset (`"twn"`, `"logos.dev"`, …) |
* | `clusterId` | number (uint16) | `0` | Cluster identifier |
* | `entryNodes` | array of string | `[]` | Bootstrap peers (enrtree / multiaddress) |
* | `relay` | boolean | `false` | Enable relay protocol |
* | `rlnRelay` | boolean | `false` | Enable RLN rate-limit nullifier |
* | `tcpPort` | number (uint16) | `60000` | P2P TCP listen port |
* | `numShardsInNetwork` | number (uint16) | `1` | Auto-sharding shard count |
* | `logLevel` | string | `"INFO"` | `"TRACE"`, `"DEBUG"`, `"INFO"`, `"WARN"`, … |
* | `logFormat` | string | `"TEXT"` | `"TEXT"` or `"JSON"` |
* | `maxMessageSize` | string | `"150KiB"` | Maximum message payload size |
* **App developer** — full stack (default `entryLayer`). `preset` picks the
* network (`"logos.test"`, `"logos.dev"`, `"twn"`), `mode` picks the protocol
* flags (`"Core"` = relay node, `"Edge"` = light node). Optional
* `messagingOverrides` / `channelsOverrides` objects override per-layer
* defaults:
* @code{.json}
* { "mode": "Core", "preset": "logos.test" }
* @endcode
*
* ## Presets
* Using a `preset` populates cluster ID, entry nodes, sharding, RLN, and
* other network-specific defaults automatically. Individual keys supplied
* alongside a preset override the preset values.
* - `"twn"` The RLN-protected Waku Network (cluster 1).
* - `"logos.dev"` Logos Dev Network (cluster 2, mix enabled,
* p2pReliability on, 8 auto-shards, built-in bootstrap nodes).
*
* Minimal `logos.dev` example:
* **Node operator** — kernel-only service node on a public network. `mode`
* is not applied on this layer, so protocol flags are set explicitly in
* `kernelConf`:
* @code{.json}
* {
* "logLevel": "INFO",
* "mode": "Core",
* "preset": "logos.dev"
* "entryLayer": "kernel",
* "kernelConf": { "preset": "logos.test", "relay": true }
* }
* @endcode
*
* Full override example:
* **Network hoster** — kernel-only node on a self-hosted network;
* `kernelConf` is a raw `WakuNodeConf` used as-is:
* @code{.json}
* {
* "mode": "Core",
* "clusterId": 42,
* "entryNodes": ["enrtree://TREE@nodes.example.com"],
* "relay": true,
* "tcpPort": 60000,
* "numShardsInNetwork": 8,
* "maxMessageSize": "150KiB",
* "logLevel": "INFO",
* "logFormat": "TEXT"
* "entryLayer": "kernel",
* "kernelConf": { "clusterId": 42, "relay": true, "entryNodes": ["/dns4/…"] }
* }
* @endcode
*
* On kernel-only nodes `send` / `subscribe` / `channel*` fail with "node has
* no messaging client" / "no reliable channel manager"; `getNodeInfo`,
* `storeQuery` and metrics keep working.
*
* The pre-layered flat shape (bare `WakuNodeConf` keys at top level) still
* parses and boots the full stack.
*
* @param cfg UTF-8 JSON payload string.
* @return `true` if context creation succeeds and callback returns `RET_OK`,
* otherwise `false`.