stubbsta ab668aafe7
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>
2026-07-21 10:07:15 +02:00

87 lines
3.3 KiB
Nim

## Waku layer API — message publish primitives used by the messaging send
## pipeline.
##
## Unlike `relay.nim`/`lightpush.nim`, these preserve the rich
## `WakuLightPushResult` (status code + description) that the send processors
## branch on for their retry decisions, and expose relay/lightpush availability
## so the messaging layer never inspects `waku.node` directly.
{.push raises: [].}
import std/times
import results, chronos
import logos_delivery/waku/waku
import
logos_delivery/waku/[
waku_core,
node/waku_node,
node/waku_node/lightpush,
node/peer_manager,
waku_relay/protocol,
rln,
waku_lightpush/common,
waku_lightpush/rpc,
waku_lightpush/client,
waku_lightpush/callbacks,
]
# WakuLightPushResult, PushMessageHandler, LightPushErrorCode (common) plus the
# LightPushStatusCode `$`/`==` the send processors branch on (rpc).
export common, rpc
proc hasRelay*(self: Waku): bool =
## True if relay (gossipsub publishing) is mounted.
return not self.node.wakuRelay.isNil()
proc hasLightpush*(self: Waku): bool =
## True if a lightpush client is mounted.
return not self.node.wakuLightpushClient.isNil()
proc relayPushHandler*(self: Waku): PushMessageHandler =
## Builds the relay publish handler used by the send pipeline. Caller
## ensures relay is mounted. The handler validates and republishes; the
## proof is attached by the messaging layer via `attachRlnProof`.
return getRelayPushHandler(self.node.wakuRelay)
proc attachRlnProof*(
self: Waku, message: WakuMessage
): Future[Result[WakuMessage, string]] {.async.} =
## Returns `message` carrying an RLN proof. A message that already has one is
## returned untouched, so retrying a task neither redraws a nonce nor changes
## the bytes. Without RLN mounted the message passes through unproven.
##
## Uses the root-refreshing generator: a message can wait in the send
## service's 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.
if self.node.rln.isNil() or message.proof.len > 0:
return ok(message)
var msgWithProof = message
msgWithProof.proof = (
await self.node.rln.generateRLNProofWithRootRefresh(
message.toRLNSignal(), float64(getTime().toUnix())
)
).valueOr:
return err("failed to attach RLN proof: " & error)
return ok(msgWithProof)
proc lightpushPeerAvailable*(self: Waku, shard: PubsubTopic): bool =
## True if a lightpush service peer is available for `shard`.
return self.node.peerManager.selectPeer(WakuLightPushCodec, Opt.some(shard)).isSome()
proc lightpushPublishToAny*(
self: Waku, shard: PubsubTopic, message: WakuMessage
): Future[WakuLightPushResult] {.async.} =
## Selects a lightpush service peer for `shard` and publishes `message`
## through the node's lightpush flow, which attaches an RLN proof per
## attempt when RLN is mounted. Returns SERVICE_NOT_AVAILABLE when no peer
## is available.
let peer = self.node.peerManager.selectPeer(WakuLightPushCodec, Opt.some(shard)).valueOr:
return lightpushResultServiceUnavailable("no lightpush peer available for shard")
try:
return await self.node.lightpushPublish(Opt.some(shard), message, Opt.some(peer))
except CatchableError as e:
return lightpushResultInternalError(e.msg)