* feat: enforce per-epoch budget in RateLimitManager.admit Replaces the pass-through skeleton with a lazily rolled fixed window: admit() charges one message against the current epoch, resets the counter once epochPeriodSec has elapsed, and rejects with OverBudget when messagesPerEpoch is exhausted. Disabled or non-positive configurations admit everything, so the default-constructed MessagingClientConf (enabled = false) keeps today's behaviour. Parking of over-budget messages stays with the SendService scheduler (NextRoundRetry); the manager only answers whether one more transmission fits. The queue / dequeueReady stubs that anticipated manager-side parking are removed accordingly. Extends tests/messaging/test_rate_limit_manager.nim with budget boundary, epoch rollover, resetEpoch, and degenerate-config cases, replacing the enabled-pass-through placeholder test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: split rate limit manager into config, quota source, and enforcement modules Decomposes logos_delivery/messaging/rate_limit_manager/ into three modules with one responsibility each: - rate_limit_config: the configuration vocabulary (RateLimitConfig, RateLimitError, defaults, isEnforcing). The API conf layer now imports this alone instead of the enforcement engine. - quota_source: the RLN seam. A single QuotaProvider callback returns EpochQuota (epoch index + user message limit) so the two are read atomically and a read cannot straddle an epoch boundary. Returns none when RLN is unavailable, selecting the wall-clock fallback. The callback shape keeps the manager free of any dependency on the Waku kernel; the RLN-backed provider is built one layer up. - rate_limit_manager: enforcement only; re-exports the other two so existing single-import call sites are unchanged. Config field names now mirror RLN exactly, per the requirement that the rate limit share RLN Relay's format: epochSizeSec (was epochPeriodSec) and userMessageLimit (was messagesPerEpoch), both uint64 to match RlnConf. Renaming is contained to this branch: the config type moved into the new rate_limit_config module here. admit() now works in epoch-index terms: the epoch comes from the provider when set (RLN's calcEpoch value), else from an absolute wall-clock window (unixTime div epochSizeSec — absolute rather than anchored at first use, matching RLN's derivation). The effective limit is min(config.userMessageLimit, RLN's) — RLN can only tighten the configured limit, since exceeding it would fail at proof generation once the epoch's message ids are exhausted. With no provider wired (this commit), behaviour is the wall-clock window as before; RLN-backed provider wiring follows separately. Tests rewritten against injected fake providers: limit boundary, epoch rollover without sleeps, RLN-clamps-config, config-tightens-below-RLN, and the wall-clock fallback (7 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: charge admission once per task and exempt budget-parked tasks from the reaper Two defects the send-service seam had once admission actually enforces, both fixed by a single DeliveryTask.firstAdmittedTime field: - The retry loop gated admission on firstPropagatedTime.isNone(), so a task that failed to propagate (e.g. no peers) was re-admitted on every 1s tick, re-charging the epoch budget and — with the shipped default of one message per epoch — starving all other traffic. Admission is now gated on firstAdmittedTime.isNone(): a task charges one slot / draws one nonce for its lifetime, and retries reuse it. - reportTaskResult reaped any never-propagated task older than MaxTimeInCache (60s) measured from message creation, so an over-budget task parked for a 600s epoch was hard-failed with a misleading "Unable to send within retry time window" long before the epoch could roll (issue #4049). The reaper now runs only for admitted tasks and measures from admission, so a task waiting for epoch budget is exempt and gets a full delivery window once it is finally admitted. The RLN-rejection branch in both processors resets firstAdmittedTime along with clearing the proof, so the regenerated proof's fresh nonce is re-admitted rather than sent uncharged. The reap decision is extracted to DeliveryTask.isDeliveryTimedOut and unit-tested (tests/messaging/test_delivery_task_reaping.nim); a scheduler-level integration test of park-and-release is a follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: source the rate limit manager's epoch and budget from RLN Wires the quota seam to its producer, so enforcement tracks RLN rather than only the wall clock. - Waku.currentRlnEpochQuota (waku/api/publish) reads RLN's current epoch index and the epoch's user message limit together, returning none when RLN is not mounted (or its limit is unset). - MessagingClient.new builds a QuotaProvider closure over that accessor and hands it to the RateLimitManager. The closure is late-binding: it queries the kernel on each admission, so a node whose RLN mounts after construction upgrades from the wall-clock fallback to RLN's epoch and limit automatically, with no reconstruction. With this, admit() rolls its window on RLN's epoch and clamps the configured cap to RLN's user message limit; without RLN it still falls back to the absolute wall-clock window and the configured limit. Also switches the quota seam from std/options to results `Opt`, matching the kernel surface it now bridges (`groupManager.userMessageLimit` is `Opt`) and the rest of the messaging layer post-Opt migration. Tests: currentRlnEpochQuota is none unmounted and reports epoch + the configured userMessageLimit when mounted (anvil-backed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: cover admit-once and park/release at the send service scheduler The rate limit manager's budget logic and the delivery-timeout reaper are unit-covered, but the send service's use of them across the service loop was not. Drive the scheduler a tick at a time against a scripted fake processor and a fixed-epoch quota provider — no network, no sleeps — asserting: - a task is charged against the budget exactly once however many rounds delivery takes (firstAdmittedTime guards re-admission); - an over-budget task parks as NextRoundRetry without reaching the processor, then is admitted and delivered on the first tick after the epoch rolls. Two testability seams keep this deterministic without a live relay: an optional sendProcessor override on SendService.new injects the fake, and trySendMessages is exported to drive one loop tick. The task is built directly (like test_delivery_task_reaping) since DeliveryTask.new resolves its shard through a broker provider only registered once the node starts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: note rateLimit config is settable only programmatically MessagingClientConf.rateLimit cannot be set through the JSON config or a CLI flag: it carries no name pragma, and RateLimitConfig is a nested object with no parseCmdArg, so applyJsonFieldsToConf rejects it with "cannot be set via JSON". Record the limitation and the fix path on the field. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor: consolidate the admission gate into admitOnce send() and the retry loop duplicated the admit-then-stamp-firstAdmittedTime sequence; both now call SendService.admitOnce, keeping the charge-once invariant in one place. No behavior change; messaging tests pass (14/14). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * use explicit return statement * Make comments mroe concise * refactor: give each rate-limit config field a single responsibility `isEnforcing` folded three fields into one "should I enforce" answer, so a zeroed `messagesPerEpoch` or `epochPeriodSec` silently disabled an enabled config. Split the responsibilities: - `enabled` alone gates enforcement; `admit` reads it directly and `isEnforcing` is removed. - a zero `messagesPerEpoch` now means what it says — admit nothing — which `admit` already yields (`0 >= 0` -> OverBudget), no special case. - `RateLimitManager.new` returns a Result and rejects an enabled config with `epochPeriodSec == 0`, the only value that could crash the wall-clock fallback (`unixTime div epochPeriodSec`). Callers thread the Result through; a zero-init `RateLimitConfig` stays valid (disabled), so default construction is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Waku
This folder contains code related to Waku, both as a node and as a protocol.
Introduction
This is an implementation in Nim of the Waku suite of protocols.
See specifications.
How to Build & Run
Prerequisites
- GNU Make, Bash and the usual POSIX utilities. Git 2.9.4 or newer.
Wakunode binary
# The first `make` invocation will update all Git submodules.
# You'll run `make update` after each `git pull`, in the future, to keep those submodules up to date.
make wakunode2
# See available command line options
./build/wakunode2 --help
# Connect the client directly with the Status test fleet
# TODO NYI
#./build/wakunode2 --log-level:debug --discovery:off --fleet:test --log-metrics
Note: building wakunode2 requires 2GB of RAM. The build will fail on systems not fulfilling this requirement.
Setting up a wakunode2 on the smallest digital ocean droplet, you can either
- compile on a stronger droplet featuring the same CPU architecture and downgrade after compiling, or
- activate swap on the smallest droplet, or
- use Docker.
Waku Protocol Test Suite
# Run all the Waku tests
make test
To run a specific test file or test case:
# Run all tests in a specific file
make test tests/waku_filter_v2/test_waku_filter.nim
# Run a specific test case within a file
make test tests/waku_filter_v2/test_waku_filter.nim "specific test name"
Alternatively, you can invoke the Nim compiler directly. For more on available flags, refer to the compiler flags and chronicles documentation.
nim c -r -d:chronicles_log_level=WARN --verbosity=0 --hints=off ./tests/waku_filter_v2/test_waku_filter.nim
You may also want to change the outdir to a folder ignored by git.
nim c -r -d:chronicles_log_level=WARN --verbosity=0 --hints=off --outdir=build ./tests/waku_filter_v2/test_waku_filter.nim
Waku Protocol Example
There are basic examples of both publishing and subscribing,
more limited in features and configuration than the wakunode2 binary,
located in examples/.
There is also a more full featured example in apps/chat2/.
Using Metrics
Metrics are available for Waku nodes.
make wakunode2
./build/wakunode2 --metrics-server
Ensure your Prometheus config prometheus.yml contains the targets you care about, e.g.:
scrape_configs:
- job_name: "waku"
static_configs:
- targets: ['localhost:8008', 'localhost:8009', 'localhost:8010']
For visualisation, similar steps can be used as is written down for Nimbus here.
There is a similar example dashboard that includes visualisation of the
envelopes available at metrics/waku-grafana-dashboard.json.
Spec support
All Waku RFCs reside at rfc.vac.dev.
Note that Waku specs are titled WAKU2-XXX
to differentiate them from a previous legacy version of Waku with RFC titles in the format WAKU-XXX.
The legacy Waku protocols are stable, but not under active development.
Generating and configuring a private key
By default a node will generate a new, random key pair each time it boots,
resulting in a different public libp2p multiaddrs after each restart.
To maintain consistent addressing across restarts,
it is possible to configure the node with a previously generated private key using the --nodekey option.
wakunode2 --nodekey=<64_char_hex>
This option takes a Secp256k1 private key in 64 char hexstring format.
To generate such a key on Linux systems,
use the openssl rand command to generate a pseudo-random 32 byte hexstring.
openssl rand -hex 32
Example output:
$ openssl rand -hex 32
6a29e767c96a2a380bb66b9a6ffcd6eb54049e14d796a1d866307b8beb7aee58
where the key 6a29e767c96a2a380bb66b9a6ffcd6eb54049e14d796a1d866307b8beb7aee58 can be used as nodekey.
To create a reusable keyfile on Linux using openssl,
use the ecparam command coupled with some standard utilities
whenever you want to extract the 32 byte private key in hex format.
# Generate keyfile
openssl ecparam -genkey -name secp256k1 -out my_private_key.pem
# Extract 32 byte private key
openssl ec -in my_private_key.pem -outform DER | tail -c +8 | head -c 32| xxd -p -c 32
Example output:
read EC key
writing EC key
0c687bb8a7984c770b566eae08520c67f53d302f24b8d4e5e47cc479a1e1ce23
where the key 0c687bb8a7984c770b566eae08520c67f53d302f24b8d4e5e47cc479a1e1ce23 can be used as nodekey.
wakunode2 --nodekey=0c687bb8a7984c770b566eae08520c67f53d302f24b8d4e5e47cc479a1e1ce23
Configuring a domain name
It is possible to configure an IPv4 DNS domain name that resolves to the node's public IPv4 address.
wakunode2 --dns4-domain-name=mynode.example.com
This allows for the node's publicly announced multiaddrs to use the /dns4 scheme.
In addition, nodes with domain name and secure websocket configured,
will generate a discoverable ENR containing the /wss multiaddr with /dns4 domain name.
This is necessary to verify domain certificates when connecting to this node over secure websocket.
Using DNS discovery to connect to existing nodes
A node can discover other nodes to connect to using DNS-based discovery. The following command line options are available:
--dns-discovery Enable DNS Discovery
--dns-discovery-url URL for DNS node list in format 'enrtree://<key>@<fqdn>'
--dns-addrs-name-server DNS name server IPs to query. Argument may be repeated.
--dns-discoveryis used to enable DNS discovery on the node. Waku DNS discovery is disabled by default.--dns-discovery-urlis mandatory if DNS discovery is enabled. It contains the URL for the node list. The URL must be in the formatenrtree://<key>@<fqdn>where<fqdn>is the fully qualified domain name and<key>is the base32 encoding of the compressed 32-byte public key that signed the list at that location.
A node will attempt connection to all discovered nodes.
This can be used, for example, to connect to one of the existing fleets. Current URLs for the published fleet lists:
- production fleet:
enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im - test fleet:
enrtree://AOGYWMBYOUIMOENHXCHILPKY3ZRFEULMFI4DOM442QSZ73TT2A7VI@test.waku.nodes.status.im
See the separate tutorial for a complete guide to DNS discovery.
Enabling Websocket
Websocket is currently the only Waku transport supported by browser nodes that uses js-waku. Setting up websocket enables your node to directly serve browser peers.
A valid certificate is necessary to serve browser nodes,
you can use letsencrypt:
sudo letsencrypt -d <your.domain.name>
You will need the privkey.pem and fullchain.pem files.
To enable secure websocket, pass the generated files to wakunode2:
Note, the default port for websocket is 8000.
wakunode2 --websocket-secure-support=true --websocket-secure-key-path="<letsencrypt cert dir>/privkey.pem" --websocket-secure-cert-path="<letsencrypt cert dir>/fullchain.pem"
Self-signed certificates
Self-signed certificates are not recommended for production setups because:
- Browsers do not accept self-signed certificates
- Browsers do not display an error when rejecting a certificate for websocket.
However, they can be used for local testing purposes:
mkdir -p ./ssl_dir/
openssl req -x509 -newkey rsa:4096 -keyout ./ssl_dir/key.pem -out ./ssl_dir/cert.pem -sha256 -nodes
wakunode2 --websocket-secure-support=true --websocket-secure-key-path="./ssl_dir/key.pem" --websocket-secure-cert-path="./ssl_dir/cert.pem"
Enabling QUIC
QUIC is a UDP-based transport that peers can use to connect to your node.
The default port for QUIC is 60000.
wakunode2 --quic-support=true
To listen on a different UDP port, use --quic-port:
wakunode2 --quic-support=true --quic-port=<port>