From 29dfaa12b6f7627187c66713aee693078f12b70b Mon Sep 17 00:00:00 2001 From: stubbsta Date: Sat, 11 Jul 2026 10:50:47 +0200 Subject: [PATCH] Bound only the proof refresh on lightpush RLN retry, not the republish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../waku/node/waku_node/lightpush.nim | 47 +++++++++---------- logos_delivery/waku/rln/constants.nim | 9 ++-- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/logos_delivery/waku/node/waku_node/lightpush.nim b/logos_delivery/waku/node/waku_node/lightpush.nim index cfaade900..d5ee5903a 100644 --- a/logos_delivery/waku/node/waku_node/lightpush.nim +++ b/logos_delivery/waku/node/waku_node/lightpush.nim @@ -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) diff --git a/logos_delivery/waku/rln/constants.nim b/logos_delivery/waku/rln/constants.nim index a2097ffc5..028c548c7 100644 --- a/logos_delivery/waku/rln/constants.nim +++ b/logos_delivery/waku/rln/constants.nim @@ -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