Bound only the proof refresh on lightpush RLN retry, not the republish

RlnRefreshRetryTimeout previously wrapped the whole retry leg: merkle
path refetch + proof regeneration + the second publish. Sharing one
clock meant a slow (but successful) refetch could leave no budget for
the publish, cancelling it mid-flight — burning the freshly drawn nonce
and, worse, reporting the original RLN rejection while the retried
request may already have reached the service node and been relayed.

Move the withTimeout to cover only attachRLNProof (the on-chain
eth_call + proof regeneration — the one step with an unbounded external
dependency). The retried publish runs unbounded after it, matching the
first attempt's semantics, so slow transports (e.g. an optional mix
route) cannot be cancelled by the refresh budget and the caller always
receives the publish's actual result.

On refresh timeout the retry publish never starts and the original
rejection is returned, so a "failed" answer now guarantees no second
request is in flight.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
stubbsta 2026-07-11 10:50:47 +02:00
parent 6bfcd1e43e
commit 29dfaa12b6
No known key found for this signature in database
2 changed files with 28 additions and 28 deletions

View File

@ -114,25 +114,23 @@ proc runRlnRefreshRetry(
peer: RemotePeerInfo,
fallback: legacy_lightpush_protocol.WakuLightPushResult[string],
): Future[legacy_lightpush_protocol.WakuLightPushResult[string]] {.async, gcsafe.} =
## Force-refreshes the RLN merkle proof path and retries the publish once,
## bounded by RlnRefreshRetryTimeout. Returns `fallback` on timeout so a
## hanging RPC/libp2p round-trip cannot stall the caller indefinitely.
## Force-refreshes the RLN merkle proof path and retries the publish once.
## Only the refresh (on-chain refetch + proof regeneration) is bounded by
## RlnRefreshRetryTimeout — a hanging RPC cannot stall the caller
## indefinitely and `fallback` (the original rejection) is returned instead.
## The retried publish itself runs unbounded, matching the first attempt.
info "legacy lightpush send rejected as RLN-invalid; " &
"refreshing merkle proof and retrying once"
rln.get().groupManager.invalidateMerkleProofCache()
proc runRetry(): Future[legacy_lightpush_protocol.WakuLightPushResult[string]] {.
async, gcsafe
.} =
let retryMsg = (await attachRLNProof(rln.get(), msgWithProof)).valueOr:
return err("failed call attachRLNProof from lightpush retry: " & error)
return await internalLegacyLightpushPublish(node, pubsubForPublish, retryMsg, peer)
let retryFut = runRetry()
if not (await retryFut.withTimeout(RlnRefreshRetryTimeout)):
warn "legacy lightpush RLN-refresh retry timed out; returning original error"
let refreshFut = attachRLNProof(rln.get(), msgWithProof)
if not (await refreshFut.withTimeout(RlnRefreshRetryTimeout)):
warn "legacy lightpush RLN proof refresh timed out; returning original error"
return fallback
return retryFut.read()
let retryMsg = refreshFut.read().valueOr:
return err("failed call attachRLNProof from lightpush retry: " & error)
return await internalLegacyLightpushPublish(node, pubsubForPublish, retryMsg, peer)
proc legacyLightpushPublish*(
node: WakuNode,
@ -367,15 +365,16 @@ proc lightpushPublish*(
statusCode = $firstResult.error.code
rln.get().groupManager.invalidateMerkleProofCache()
proc runRetry(): Future[lightpush_protocol.WakuLightPushResult] {.async, gcsafe.} =
let retryMsg = (await attachRLNProof(rln.get(), msgWithProof)).valueOr:
return lighpushErrorResult(LightPushErrorCode.OUT_OF_RLN_PROOF, error)
return
await lightpushPublishHandler(node, pubsubForPublish, retryMsg, toPeer, mixify)
let retryFut = runRetry()
if not (await retryFut.withTimeout(RlnRefreshRetryTimeout)):
warn "lightpush RLN-refresh retry timed out; returning original error",
# Only the refresh (on-chain refetch + proof regeneration) is bounded — a
# hanging RPC cannot stall the caller indefinitely and the original
# rejection is returned instead. The retried publish itself runs unbounded,
# matching the first attempt.
let refreshFut = attachRLNProof(rln.get(), msgWithProof)
if not (await refreshFut.withTimeout(RlnRefreshRetryTimeout)):
warn "lightpush RLN proof refresh timed out; returning original error",
statusCode = $firstResult.error.code
return firstResult
return retryFut.read()
let retryMsg = refreshFut.read().valueOr:
return lighpushErrorResult(LightPushErrorCode.OUT_OF_RLN_PROOF, error)
return await lightpushPublishHandler(node, pubsubForPublish, retryMsg, toPeer, mixify)

View File

@ -20,10 +20,11 @@ const RlnCredentialsFilename* = "rlnCredentials.txt"
# RLN Validator message rejection Error string, is used to trigger proof refresh and publish retry in the lightpush client
const RlnValidatorErrorMsg* = "RLN validation failed"
# Upper bound on the reactive refresh + retry after a stale-RLN rejection.
# The retry synchronously fetches a fresh merkle path from the RLN contract
# (eth_call round-trip) and republishes; without a bound a hanging RPC endpoint
# could stall the caller indefinitely. Matches the REST `futTimeout`.
# Upper bound on the reactive merkle proof refresh after a stale-RLN
# rejection: fetching a fresh merkle path from the RLN contract (eth_call
# round-trip) plus proof regeneration. Without a bound a hanging RPC endpoint
# could stall the caller indefinitely. The retried publish is not covered —
# it runs unbounded, matching the first attempt.
const RlnRefreshRetryTimeout* = 5.seconds
# inputs of the membership contract constructor