Tanya S c873f74b4b
feat: enforce the per-epoch rate limit at the send service (RLN-sourced) (#4062)
* 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>
2026-07-30 13:10:41 +02:00
2022-11-21 09:31:03 +01:00
2026-05-07 18:19:34 +02:00
2026-01-30 11:52:25 +00:00
2026-05-27 10:40:54 +02:00

Logos Messaging Nim

Introduction

This repository implements a set of libp2p protocols aimed to bring private communications.

  • Nim implementation of these specs.
  • C library that exposes the implemented protocols.
  • CLI application that allows you to run a logos-delivery node.
  • Examples.
  • Various tests of above.

For more details see the source code

How to Build & Run ( Linux, MacOS & WSL )

These instructions are generic. For more detailed instructions, see the source code above.

Recommended and tested toolchain versions (these are installed when you follow the build instructions below):

  • Nim 2.2.4
  • Nimble 0.22.3

Prerequisites

The standard developer tools, including a C compiler, GNU Make, Bash, and Git.

In some distributions (Fedora linux for example), you may need to install which utility separately. Nimbus build system is relying on it.

You'll also need an installation of Rust and its toolchain (specifically rustc and cargo). The easiest way to install these, is using rustup:

Rust:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Wakunode

# The first `make` invocation will initialize the local dependency state.
make wakunode2

# Build with custom compilation flags. Do not use NIM_PARAMS unless you know what you are doing.
# Replace with your own flags
make wakunode2 NIMFLAGS="-d:chronicles_colors:none -d:disableMarchNative"

# Run with DNS bootstrapping
./build/wakunode2 --dns-discovery --dns-discovery-url=DNS_BOOTSTRAP_NODE_URL

# Run with the QUIC transport enabled
./build/wakunode2 --quic-support=true

# See available command line options
./build/wakunode2 --help

To join the network, you need to know the address of at least one bootstrap node. Please refer to the Waku README for more information.

For more on how to run wakunode2, refer to:

Issues

WSL

If you encounter difficulties building the project on WSL, consider placing the project within WSL's filesystem, avoiding the /mnt/ directory.

How to Build & Run ( Windows )

Windows Build Instructions

1. Install Required Tools

  • Git Bash Terminal: Download and install from https://git-scm.com/download/win
  • MSYS2:
    a. Download installer from https://www.msys2.org
    b. Install at "C:" (default location). Remove/rename the msys folder in case of previous installation. c. Use the mingw64 terminal from msys64 directory for package installation.

2. Install Dependencies

Open MSYS2 mingw64 terminal and run the following one-by-one :

pacman -Syu --noconfirm  
pacman -S --noconfirm --needed mingw-w64-x86_64-toolchain  
pacman -S --noconfirm --needed base-devel make cmake upx  
pacman -S --noconfirm --needed mingw-w64-x86_64-rust  
pacman -S --noconfirm --needed mingw-w64-x86_64-postgresql  
pacman -S --noconfirm --needed mingw-w64-x86_64-gcc  
pacman -S --noconfirm --needed mingw-w64-x86_64-gcc-libs  
pacman -S --noconfirm --needed mingw-w64-x86_64-libwinpthread-git  
pacman -S --noconfirm --needed mingw-w64-x86_64-zlib  
pacman -S --noconfirm --needed mingw-w64-x86_64-openssl  
pacman -S --noconfirm --needed mingw-w64-x86_64-python

3. Build Wakunode

  • Open Git Bash as administrator
  • clone nwaku and cd nwaku
  • Execute: ./scripts/build_windows.sh

4. Troubleshooting

If wakunode2.exe isn't generated:

  • Missing Dependencies: Verify with:
    which make cmake gcc g++ rustc cargo python3 upx
    If missing, revisit Step 2 or ensure MSYS2 is at C:\
  • Installation Conflicts: Remove existing MinGW/MSYS2/Git Bash installations and perform fresh install

Developing

Nim Runtime

This repository is bundled with a Nim runtime that includes the necessary dependencies for the project.

Before you can utilize the runtime you'll need to build the project, as detailed in a previous section. This will generate a nimbledeps/pkgs2 directory containing various dependencies.

If everything went well, you should see your prompt suffixed with [SuccessX]. Now you can run nim commands as usual.

Test Suite

# Run all the Waku tests
make test

# Run a specific test file
make test <test_file_path>
# e.g. : make test tests/wakunode2/test_all.nim

# Run a specific test name from a specific test file
make test <test_file_path> <test_name>
# e.g. : make test tests/wakunode2/test_all.nim "node setup is successful with default configuration"

Building single test files

During development it is helpful to build and run a single test file. To support this make has a specific target:

targets:

  • build/<relative path to your test file.nim>
  • test/<relative path to your test file.nim>

Binary will be created as <path to your test file.nim>.bin under the build directory .

# Build and run your test file separately
make test/tests/common/test_enr_builder.nim

Testing against js-waku

Refer to logos-delivery-js repo for instructions.

Formatting

Nim files are expected to be formatted using the nph version present in vendor/nph.

You can easily format file with the make nph/<relative path to nim> file command. For example:

make nph/waku/waku_core.nim

A convenient git hook is provided to automatically format file at commit time. Run the following command to install it:

make install-nph

Examples

Examples can be found in the examples folder. This includes a fully featured chat example.

Tools

Different tools and their corresponding how-to guides can be found in the tools folder.

Bugs, Questions & Features

For an inquiry, or if you would like to propose new features, feel free to open a general issue.

For bug reports, please tag your issue with the bug label.

If you believe the reported issue requires critical attention, please use the critical label to assist with triaging.

To get help, or participate in the conversation, join the Logos Discord server.

Docs

Description
Logos Messaging protocols implemented in Nim
Readme
Languages
Nim 88.9%
Python 6.8%
Shell 1.7%
C 0.8%
Nix 0.7%
Other 1%