Skip to content

Commit 1425088

Browse files
stubbstaclaude
andcommitted
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>
1 parent d9aec62 commit 1425088

4 files changed

Lines changed: 148 additions & 4 deletions

File tree

logos_delivery/messaging/delivery_service/send_service/send_service.nim

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,15 @@ proc trySendMessages(self: SendService) {.async.} =
260260
## `NextRoundRetry` and are retried as the epoch rolls over.
261261
(await self.rateLimit.admit(task.msg.payload)).isOkOr:
262262
continue
263+
264+
## Strictly after admission, so a rejected message never draws a nonce.
265+
## A no-op when RLN is not mounted, or when a prior round already
266+
## attached a proof.
267+
task.msg = (await self.waku.attachRlnProof(task.msg)).valueOr:
268+
error "SendService: failed to attach RLN proof, retrying next round",
269+
requestId = task.requestId, error = error
270+
continue
271+
263272
await self.sendProcessor.process(task)
264273

265274
proc serviceLoop(self: SendService) {.async.} =
@@ -296,6 +305,15 @@ proc send*(self: SendService, task: DeliveryTask) {.async.} =
296305
self.addTask(task)
297306
return
298307

308+
## Strictly after admission, so a rejected message never draws a nonce.
309+
## A no-op when RLN is not mounted.
310+
task.msg = (await self.waku.attachRlnProof(task.msg)).valueOr:
311+
error "SendService.send: failed to attach RLN proof, parking task",
312+
requestId = task.requestId, error = error
313+
task.state = DeliveryState.NextRoundRetry
314+
self.addTask(task)
315+
return
316+
299317
await self.sendProcessor.process(task)
300318
reportTaskResult(self, task)
301319
if task.state != DeliveryState.FailedToDeliver:

logos_delivery/waku/api/publish.nim

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import logos_delivery/waku/compat/option_valueor
99
{.push raises: [].}
1010

11-
import std/options
11+
import std/[options, times]
1212
import results, chronos
1313

1414
import logos_delivery/waku/waku
@@ -40,10 +40,34 @@ proc hasLightpush*(self: Waku): bool =
4040

4141
proc relayPushHandler*(self: Waku): PushMessageHandler =
4242
## Builds the relay publish handler used by the send pipeline. Caller
43-
## ensures relay is mounted. RLN proof generation is handled client-side
44-
## in (legacy)lightpushPublish; this handler only validates and republishes.
43+
## ensures relay is mounted. The handler validates and republishes; the
44+
## proof is attached by the messaging layer via `attachRlnProof`.
4545
return getRelayPushHandler(self.node.wakuRelay)
4646

47+
proc attachRlnProof*(
48+
self: Waku, message: WakuMessage
49+
): Future[Result[WakuMessage, string]] {.async.} =
50+
## Returns `message` carrying an RLN proof. A message that already has one is
51+
## returned untouched, so retrying a task neither redraws a nonce nor changes
52+
## the bytes. Without RLN mounted the message passes through unproven.
53+
##
54+
## Uses the root-refreshing generator: a message can wait in the send
55+
## service's task cache while the group root moves on chain, so the proof is
56+
## validated against the acceptable-root window and regenerated once against a
57+
## refetched merkle path if it went stale.
58+
if self.node.rln.isNil() or message.proof.len > 0:
59+
return ok(message)
60+
61+
var msgWithProof = message
62+
msgWithProof.proof = (
63+
await self.node.rln.generateRLNProofWithRootRefresh(
64+
message.toRLNSignal(), float64(getTime().toUnix())
65+
)
66+
).valueOr:
67+
return err("failed to attach RLN proof: " & error)
68+
69+
return ok(msgWithProof)
70+
4771
proc lightpushPeerAvailable*(self: Waku, shard: PubsubTopic): bool =
4872
## True if a lightpush service peer is available for `shard`.
4973
return self.node.peerManager.selectPeer(WakuLightPushCodec, some(shard)).isSome()

tests/messaging/test_all.nim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{.used.}
22

3-
import ./test_rate_limit_manager
3+
import ./test_rate_limit_manager, ./test_rln_proof_attach
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
{.used.}
2+
3+
import std/[options, net, osproc]
4+
import chronos, testutils/unittests, results, stew/byteutils
5+
import
6+
logos_delivery/waku/[waku, waku_core, rln],
7+
logos_delivery/waku/node/waku_node,
8+
logos_delivery/waku/node/waku_node/relay,
9+
logos_delivery/waku/api/publish,
10+
logos_delivery/api/conf/messaging_conf,
11+
logos_delivery/waku/factory/waku_conf
12+
import
13+
../testlib/testasync,
14+
../waku_rln_relay/utils_onchain,
15+
../waku_rln_relay/rln/waku_rln_relay_utils
16+
17+
proc testConf(): WakuConf =
18+
var conf = MessagingClientConf()
19+
.toWakuNodeConf(messaging_conf.LogosDeliveryMode.Core).valueOr:
20+
raiseAssert error
21+
conf.listenAddress = parseIpAddress("0.0.0.0")
22+
conf.tcpPort = Port(0)
23+
conf.discv5UdpPort = Port(0)
24+
conf.clusterId = some(3'u16)
25+
conf.numShardsInNetwork = 1
26+
conf.rest = false
27+
return conf.toWakuConf().valueOr:
28+
raiseAssert error
29+
30+
proc testMessage(): WakuMessage =
31+
WakuMessage(
32+
payload: "hello".toBytes(),
33+
contentTopic: "/test/1/attach/proto",
34+
timestamp: 1_700_000_000_000_000_000,
35+
)
36+
37+
suite "SendService RLN proof attach":
38+
asyncTest "passes the message through unproven when RLN is not mounted":
39+
## The default (no-RLN) configuration must be unaffected: no proof is
40+
## attached and the message reaches the send processors unchanged.
41+
let waku = (await Waku.new(testConf())).expect("Waku.new")
42+
let msg = testMessage()
43+
44+
let attached = (await waku.attachRlnProof(msg)).expect("attachRlnProof")
45+
46+
check:
47+
attached.proof.len == 0
48+
attached.payload == msg.payload
49+
attached.contentTopic == msg.contentTopic
50+
51+
suite "SendService RLN proof attach - RLN mounted":
52+
var
53+
waku {.threadvar.}: Waku
54+
anvilProc {.threadvar.}: Process
55+
manager {.threadvar.}: OnchainGroupManager
56+
57+
asyncSetup:
58+
anvilProc = runAnvil(stateFile = some(DEFAULT_ANVIL_STATE_PATH))
59+
manager = waitFor setupOnchainGroupManager(deployContracts = false)
60+
61+
waku = (await Waku.new(testConf())).expect("Waku.new")
62+
await waku.node.setRlnValidator(
63+
getWakuRlnConfig(
64+
manager = manager,
65+
userMessageLimit = 20,
66+
index = MembershipIndex(1),
67+
epochSizeSec = 600,
68+
)
69+
)
70+
71+
let credentials = generateCredentials()
72+
(
73+
waitFor cast[OnchainGroupManager](waku.node.rln.groupManager).register(
74+
credentials, UserMessageLimit(20)
75+
)
76+
).isOkOr:
77+
assert false, "failed to register RLN credentials: " & error
78+
79+
asyncTeardown:
80+
## The RLN proof-generator provider is registered on the global broker
81+
## context; without stopping RLN it leaks into the next test's setup.
82+
try:
83+
await waku.node.rln.stop()
84+
except Exception:
85+
assert false, "failed to stop RLN: " & getCurrentExceptionMsg()
86+
stopAnvil(anvilProc)
87+
88+
asyncTest "attaches a proof":
89+
let attached = (await waku.attachRlnProof(testMessage())).expect("attachRlnProof")
90+
91+
check attached.proof.len > 0
92+
93+
asyncTest "is idempotent: a message that already carries a proof is untouched":
94+
## Pins the retry contract: the send service re-attaches on every round, so
95+
## re-attaching must neither draw a fresh nonce nor change the bytes —
96+
## otherwise a retried task would resend under a new nullifier.
97+
let first = (await waku.attachRlnProof(testMessage())).expect("first attach")
98+
let second = (await waku.attachRlnProof(first)).expect("second attach")
99+
100+
check:
101+
first.proof.len > 0
102+
second.proof == first.proof

0 commit comments

Comments
 (0)