Tanya S 6a1af006b1
feat: attach and refresh RLN proofs in the send service (Stream B) (#4069)
* feat: attach RLN proofs at the SendService transmission stage

The relay send path published without an RLN proof: proof generation
lived client-side in (legacy)lightpushPublish, so messages dispatched
through SendService -> RelaySendProcessor reached the network unproven
and would be rejected by an RLN-enforcing relay.

Adds Waku.attachRlnProof in the waku/api publish surface and calls it
from SendService immediately after admission, in both send() and the
retry loop. Placement is load-bearing:

- After admit(), so a message rejected by the rate limiter never draws
  a nonce.
- At transmission rather than API entry, because a proof binds to the
  epoch current when the message goes out, and a task can be retried
  for up to MaxTimeInCache after send() returns.

attachRlnProof is a no-op without RLN mounted (message passes through
unproven, as today) and short-circuits on a message that already
carries a proof, so retrying a task neither redraws a nonce nor
changes the bytes. It uses generateRLNProofWithRootRefresh rather than
the plain generator: a task can wait in the task cache while the group
root moves on chain, so the proof is validated against the
acceptable-root window and regenerated once against a refetched merkle
path if it went stale.

Proof-generation failure parks the task as NextRoundRetry rather than
failing it, matching the admission path: the dominant failure is
NonceLimitReached (RLN's own per-epoch budget exhausted), which the
service loop resolves as the epoch rolls over.

Adds tests/messaging/test_rln_proof_attach.nim covering the unmounted
pass-through, attach when mounted, and the idempotency contract that
the retry loop depends on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: handle RLN publish rejections in the send service retry loop

An RLN-invalid publish rejection now recovers through the send
service's existing retry loop instead of an inline retry in the kernel.

When a relay or lightpush publish is rejected as RLN-invalid, the
processor clears the message's stale proof, schedules a background
merkle-proof refresh, and parks the task as NextRoundRetry. The next
loop round re-admits the task and regenerates the proof against the
refreshed path. Clearing the proof is required: attachRlnProof
short-circuits on a message that already carries one, so without the
clear the task would resend the rejected proof until it ages out. The
relay processor previously failed such tasks outright, with no
recovery.

Kernel changes supporting this:

- Remove runRlnRefreshRetry from legacyLightpushPublish. The legacy
  path now schedules the refresh and returns the error tagged with
  RlnProofRefreshScheduledMsg, matching the non-legacy path; retrying
  is the caller's decision. Drops the now-unused
  RlnMerkleProofRefreshTimeout.
- generateRLNProofWithRootRefresh reuses the nonce drawn for the first
  attempt when it regenerates after a stale root, rather than drawing a
  second. Only the merkle path differs between the two attempts, so a
  redraw would spend two message ids from the epoch budget on a single
  message and drift the rate limit manager's accounting away from the
  nonce manager's.

Adds Waku.isRlnRejection / Waku.onRlnProofRejected as the messaging
layer's handle on the kernel's RLN rejection detection and background
refresh. Updates the legacy lightpush tests to the schedule-refresh
contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: cover currentRlnEpochQuota (mounted and unmounted)

currentRlnEpochQuota ships in the enforcement PR, but its mounted-RLN
assertion needs the anvil-backed group-manager scaffolding that lives in
this file, so the coverage rides along here: none when RLN is unmounted,
and the epoch index + userMessageLimit when it is.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor: consolidate the RLN-rejection parking into parkForRlnProofRefresh

The relay and lightpush processors duplicated the RLN-rejection recovery
(schedule background refresh, clear the stale proof, reset admission,
park as NextRoundRetry); both now call parkForRlnProofRefresh in
send_processor, so the proof-clear the retry contract depends on cannot
drift between the two processors. Also resets firstAdmittedTime so the
regenerated proof's fresh nonce is re-admitted rather than sent uncharged.

No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: clarify that lightpush reuses an already-attached RLN proof

The old wording ("attaches an RLN proof per attempt") reads as if every
retry redraws a nonce. The flow proves a message only when it carries no
proof, so a task admitted once reuses its proof and nonce across retries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: retry once on scheduled RLN proof refresh in the lightpush REST handlers

The kernel lightpush publish paths no longer retry an RLN-invalid publish
inline. On a stale merkle root they schedule a background cache refresh and
return early, tagging the error with RlnProofRefreshScheduledMsg — retrying
is the caller's decision, so the send service recovers through its own loop
and the kernel exposes mechanism only.

The synchronous REST endpoints have no such loop: they call publish once and
map the result to an HTTP status. Left unchanged, a transient stale-root
rejection that the kernel previously absorbed via runRlnRefreshRetry would
now surface to the HTTP client as a 503. Restore the transparent retry where
it belongs under this layering — at the caller — instead of back in the
kernel where it would re-nest inside the send service's retry.

Both the legacy and v3 handlers now retry the publish exactly once when the
first result carries RlnProofRefreshScheduledMsg, under the same
FutTimeoutForPushRequestProcessing bound. The handler's message carries no
proof, so the retry regenerates against the refreshed merkle path. Any other
error, and any error on the retry itself, maps to its response as before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: retry RLN proof attach on a charged-but-unproven task

admitAndProve set firstAdmittedTime before attaching the proof, then
guarded its whole body on firstAdmittedTime.isSome(). A transient proof
attach failure (e.g. NonceLimitReached) left the task charged but with an
empty proof, and the next round's early-return skipped the attach entirely
and shipped the message bare. The docstring's own invariant — "once
admitted, a task keeps its slot and its proof" — was violated: it kept the
slot but not the proof.

Guard only the rate-limit charge on firstAdmittedTime, not the attach.
attachRlnProof is already idempotent (short-circuits when RLN is unmounted
or a proof is present), so it is safe to call every round: a charged-but-
unproven task retries the attach until it sticks, then short-circuits. The
ordering invariant holds (charge strictly before attach, so an over-budget
message never draws a nonce), the charge stays once-per-task, and NO_PEERS
retries remain free.

This also removes a latent relay double-charge: previously a bare message
reached the relay, was rejected as RLN-invalid, and parkForRlnProofRefresh
reset firstAdmittedTime — re-charging a slot on the next round. The message
now never leaves unproven.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-03 10:15:43 +02:00
..

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-discovery is used to enable DNS discovery on the node. Waku DNS discovery is disabled by default.
  • --dns-discovery-url is mandatory if DNS discovery is enabled. It contains the URL for the node list. The URL must be in the format enrtree://<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>