logos-delivery/tests/messaging/test_rln_proof_attach.nim
stubbsta ef06f67a2a
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-21 13:40:00 +02:00

117 lines
4.1 KiB
Nim

{.used.}
import std/[options, net, osproc]
import chronos, testutils/unittests, results, stew/byteutils
import
logos_delivery/waku/[waku, waku_core, rln],
logos_delivery/waku/node/waku_node,
logos_delivery/waku/node/waku_node/relay,
logos_delivery/waku/api/publish,
logos_delivery/api/conf/messaging_conf,
logos_delivery/waku/factory/waku_conf
import
../testlib/testasync,
../waku_rln_relay/utils_onchain,
../waku_rln_relay/rln/waku_rln_relay_utils
proc testConf(): WakuConf =
var conf = MessagingClientConf()
.toWakuNodeConf(messaging_conf.LogosDeliveryMode.Core).valueOr:
raiseAssert error
conf.listenAddress = parseIpAddress("0.0.0.0")
conf.tcpPort = Port(0)
conf.discv5UdpPort = Port(0)
conf.clusterId = Opt.some(3'u16)
conf.numShardsInNetwork = 1
conf.rest = false
return conf.toWakuConf().valueOr:
raiseAssert error
proc testMessage(): WakuMessage =
WakuMessage(
payload: "hello".toBytes(),
contentTopic: "/test/1/attach/proto",
timestamp: 1_700_000_000_000_000_000,
)
suite "SendService RLN proof attach":
asyncTest "passes the message through unproven when RLN is not mounted":
## The default (no-RLN) configuration must be unaffected: no proof is
## attached and the message reaches the send processors unchanged.
let waku = (await Waku.new(testConf())).expect("Waku.new")
let msg = testMessage()
let attached = (await waku.attachRlnProof(msg)).expect("attachRlnProof")
check:
attached.proof.len == 0
attached.payload == msg.payload
attached.contentTopic == msg.contentTopic
asyncTest "currentRlnEpochQuota is none when RLN is not mounted":
## The rate limit manager reads `none` as "use the wall-clock fallback".
let waku = (await Waku.new(testConf())).expect("Waku.new")
check waku.currentRlnEpochQuota().isNone()
suite "SendService RLN proof attach - RLN mounted":
var
waku {.threadvar.}: Waku
anvilProc {.threadvar.}: Process
manager {.threadvar.}: OnchainGroupManager
asyncSetup:
anvilProc = runAnvil(stateFile = Opt.some(DEFAULT_ANVIL_STATE_PATH))
manager = waitFor setupOnchainGroupManager(deployContracts = false)
waku = (await Waku.new(testConf())).expect("Waku.new")
await waku.node.setRlnValidator(
getWakuRlnConfig(
manager = manager,
userMessageLimit = 20,
index = MembershipIndex(1),
epochSizeSec = 600,
)
)
let credentials = generateCredentials()
(
waitFor cast[OnchainGroupManager](waku.node.rln.groupManager).register(
credentials, UserMessageLimit(20)
)
).isOkOr:
assert false, "failed to register RLN credentials: " & error
asyncTeardown:
## The RLN proof-generator provider is registered on the global broker
## context; without stopping RLN it leaks into the next test's setup.
try:
await waku.node.rln.stop()
except Exception:
assert false, "failed to stop RLN: " & getCurrentExceptionMsg()
stopAnvil(anvilProc)
asyncTest "attaches a proof":
let attached = (await waku.attachRlnProof(testMessage())).expect("attachRlnProof")
check attached.proof.len > 0
asyncTest "currentRlnEpochQuota reports RLN's epoch and user message limit":
## Wires the rate limit manager to RLN: the manager clamps its configured
## cap to `messageLimit` and rolls on `epochIndex`.
let quota = waku.currentRlnEpochQuota()
check:
quota.isSome()
quota.get().messageLimit == 20'u64 # the mounted userMessageLimit
quota.get().epochIndex > 0'u64 # unixTime div epochSize, far from zero
asyncTest "is idempotent: a message that already carries a proof is untouched":
## Pins the retry contract: the send service re-attaches on every round, so
## re-attaching must neither draw a fresh nonce nor change the bytes —
## otherwise a retried task would resend under a new nullifier.
let first = (await waku.attachRlnProof(testMessage())).expect("first attach")
let second = (await waku.attachRlnProof(first)).expect("second attach")
check:
first.proof.len > 0
second.proof == first.proof