stubbsta e18e944113
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>
2026-07-24 10:17:43 +02:00

40 lines
1.9 KiB
Nim

## Epoch + user-message-limit source for the rate limit manager.
##
## "Quota" is the tracking issues' word (#3838: "we should know our remaining
## quota") for what RLN expresses as an epoch's user message limit: each epoch
## grants `userMessageLimit` message ids (nonces), and the epoch rolling over
## refills the allowance.
##
## The manager rate-limits per epoch, but must not know *where* the epoch and
## the limit come from. With RLN mounted both are RLN's; without it, a
## wall-clock window and the configured limit stand in. Epoch and limit are
## read together, through one provider, so a read cannot straddle an epoch
## boundary and pair a fresh epoch with a stale limit. The provider is a
## callback so the manager stays free of any dependency on the `Waku` kernel —
## the RLN-backed provider is built one layer up, where the kernel handle is in
## scope. It returns `none` when RLN is not mounted, which is the signal to
## fall back to the wall clock.
import std/times
import results
type
EpochQuota* = object
epochIndex*: uint64
## Current epoch as its numeric value (`timestamp div` the epoch length);
## the kernel's `Epoch` type is the same value serialized to 32 bytes.
userMessageLimit*: uint64
## Message ids the epoch grants — the hard ceiling on admissions before
## the epoch rolls over.
QuotaProvider* = proc(): Opt[EpochQuota] {.gcsafe, raises: [].}
## The epoch and its user message limit, or `none` when RLN is not mounted.
proc wallClockEpochIndex*(epochPeriodSec: uint64): uint64 =
## Absolute wall-clock epoch: `unixTime div epochPeriodSec`. Absolute (not a
## sliding window anchored at first use) so it matches RLN's `calcEpoch`, and
## so two managers started at different moments agree on the boundary.
## `epochPeriodSec` is assumed positive; the manager only calls this when
## enforcing.
uint64(getTime().toUnix()) div epochPeriodSec