stubbsta 571ed17c0f
Drop nonce reuse on RLN retry; consume a fresh nonce instead
The retry path threaded a rejected attempt's message id back so
NonceManager.rollbackNonce could CAS the counter and reclaim the slot.
This bought at most one nonce per retry (a rare event) at the cost of
an Option[Nonce] param, a tuple return, and a concurrent-draw race the
CAS had to defend against.

Simpler to optimistically waste the nonce: checkAndGenerateRLNProof
returns Result[WakuMessage, string] and callers drop the tuple
destructuring. NonceManager.rollbackNonce and its tests, now dead,
are removed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-10 12:46:01 +02:00

384 lines
15 KiB
Nim

import logos_delivery/waku/compat/option_valueor
{.push raises: [].}
import
std/[hashes, options, strutils, tables, net],
chronos,
chronicles,
metrics,
results,
stew/byteutils,
eth/keys,
eth/p2p/discoveryv5/enr,
libp2p/crypto/crypto,
libp2p/protocols/ping,
libp2p/protocols/pubsub/gossipsub,
libp2p/protocols/pubsub/rpc/messages,
libp2p/builders,
libp2p/transports/tcptransport,
libp2p/transports/wstransport,
libp2p/utility,
libp2p_mix
import
../waku_node,
../../waku_core,
../../waku_core/topics/sharding,
../../waku_lightpush_legacy/client as legacy_lightpush_client,
../../waku_lightpush_legacy as legacy_lightpush_protocol,
../../waku_lightpush/client as lightpush_client,
../../waku_lightpush as lightpush_protocol,
../peer_manager,
../../common/rate_limit/setting,
../../rln
logScope:
topics = "waku node lightpush api"
const MountWithoutRelayError* = "cannot mount lightpush because relay is not mounted"
## Waku lightpush
proc mountLegacyLightPush*(
node: WakuNode, rateLimit: RateLimitSetting = DefaultGlobalNonRelayRateLimit
): Future[Result[void, string]] {.async.} =
info "mounting legacy light push"
if node.wakuRelay.isNil():
return err(MountWithoutRelayError)
info "mounting legacy lightpush with relay"
let pushHandler = legacy_lightpush_protocol.getRelayPushHandler(node.wakuRelay)
node.wakuLegacyLightPush =
WakuLegacyLightPush.new(node.peerManager, node.rng, pushHandler, some(rateLimit))
if node.started:
# Node has started already. Let's start lightpush too.
await node.wakuLegacyLightPush.start()
node.switch.mount(node.wakuLegacyLightPush, protocolMatcher(WakuLegacyLightPushCodec))
info "legacy lightpush mounted successfully"
return ok()
proc mountLegacyLightPushClient*(node: WakuNode) =
info "mounting legacy light push client"
if node.wakuLegacyLightpushClient.isNil():
node.wakuLegacyLightpushClient =
WakuLegacyLightPushClient.new(node.peerManager, node.rng)
proc internalLegacyLightpushPublish(
node: WakuNode, pubsubTopic: PubsubTopic, message: WakuMessage, peer: RemotePeerInfo
): Future[legacy_lightpush_protocol.WakuLightPushResult[string]] {.async, gcsafe.} =
## Dispatches to the legacy lightpush client if mounted, otherwise to the
## self-hosted server. Callers guarantee at least one is mounted.
let msgHash = pubsubTopic.computeMessageHash(message).to0xHex()
if not node.wakuLegacyLightpushClient.isNil():
notice "publishing message with legacy lightpush",
pubsubTopic = pubsubTopic,
contentTopic = message.contentTopic,
target_peer_id = peer.peerId,
msg_hash = msgHash
return await node.wakuLegacyLightpushClient.publish(pubsubTopic, message, peer)
notice "publishing message with self hosted legacy lightpush",
pubsubTopic = pubsubTopic,
contentTopic = message.contentTopic,
target_peer_id = peer.peerId,
msg_hash = msgHash
return await node.wakuLegacyLightPush.handleSelfLightPushRequest(pubsubTopic, message)
proc resolveLegacyPubsubTopic(
node: WakuNode, pubsubTopic: Option[PubsubTopic], contentTopic: ContentTopic
): Result[PubsubTopic, string] =
## Returns the explicit pubsub topic if provided, otherwise derives it from
## `contentTopic` via autosharding.
if pubsubTopic.isSome():
return ok(pubsubTopic.get())
if node.wakuAutoSharding.isNone():
return err("Pubsub topic must be specified when static sharding is enabled")
let parsedTopic = NsContentTopic.parse(contentTopic).valueOr:
return err("Invalid content-topic: " & $error)
let shard = node.wakuAutoSharding.get().getShard(parsedTopic).valueOr:
return err("Autosharding error: " & error)
return ok($shard)
proc runRlnRefreshRetry(
node: WakuNode,
rln: Option[Rln],
msgWithProof: WakuMessage,
pubsubForPublish: PubsubTopic,
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.
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 checkAndGenerateRLNProof(rln, msgWithProof, regenerate = true)
).valueOr:
return err("failed call checkAndGenerateRLNProof 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"
return fallback
return retryFut.read()
proc legacyLightpushPublish*(
node: WakuNode,
pubsubTopic: Option[PubsubTopic],
message: WakuMessage,
peer: RemotePeerInfo,
): Future[legacy_lightpush_protocol.WakuLightPushResult[string]] {.async, gcsafe.} =
## Pushes a `WakuMessage` to a node which relays it further on PubSub topic.
## Returns whether relaying was successful or not.
## `WakuMessage` should contain a `contentTopic` field for light node
## functionality.
if node.wakuLegacyLightpushClient.isNil() and node.wakuLegacyLightPush.isNil():
error "failed to publish message as legacy lightpush not available"
return err("Waku lightpush not available")
# toRLNSignal includes the timestamp in the proof input, so it must be fixed
# before proof generation. The downstream ensureTimestampSet in the client
# publish becomes an idempotent no-op safety net.
let message = ensureTimestampSet(message)
let rln =
if node.rln.isNil():
none(Rln)
else:
some(node.rln)
let msgWithProof = (await checkAndGenerateRLNProof(rln, message)).valueOr:
return err("failed call checkAndGenerateRLNProof from lightpush: " & error)
try:
let pubsubForPublish = resolveLegacyPubsubTopic(
node, pubsubTopic, message.contentTopic
).valueOr:
return err(error)
let firstResult =
await internalLegacyLightpushPublish(node, pubsubForPublish, msgWithProof, peer)
# A publish error mentioning RLN can indicate a stale merkle proof path;
# refresh it and retry the publish once. Legacy lightpush has no status
# codes, so we string-match the RLN error for backward compatibility.
if firstResult.isOk() or rln.isNone() or
not firstResult.error.contains(RlnValidatorErrorMsg):
return firstResult
return await runRlnRefreshRetry(
node, rln, msgWithProof, pubsubForPublish, peer, firstResult
)
except CatchableError:
return err(getCurrentExceptionMsg())
# TODO: Move to application module (e.g., wakunode2.nim)
proc legacyLightpushPublish*(
node: WakuNode, pubsubTopic: Option[PubsubTopic], message: WakuMessage
): Future[legacy_lightpush_protocol.WakuLightPushResult[string]] {.
async, gcsafe, deprecated: "Use 'node.legacyLightpushPublish()' instead"
.} =
if node.wakuLegacyLightpushClient.isNil() and node.wakuLegacyLightPush.isNil():
error "failed to publish message as legacy lightpush not available"
return err("waku legacy lightpush not available")
var peerOpt: Option[RemotePeerInfo] = none(RemotePeerInfo)
if not node.wakuLegacyLightpushClient.isNil():
peerOpt = node.peerManager.selectPeer(WakuLegacyLightPushCodec)
if peerOpt.isNone():
let msg = "no suitable remote peers"
error "failed to publish message", err = msg
return err(msg)
elif not node.wakuLegacyLightPush.isNil():
peerOpt = some(RemotePeerInfo.init($node.switch.peerInfo.peerId))
return await node.legacyLightpushPublish(pubsubTopic, message, peer = peerOpt.get())
proc mountLightPush*(
node: WakuNode, rateLimit: RateLimitSetting = DefaultGlobalNonRelayRateLimit
): Future[Result[void, string]] {.async.} =
info "mounting light push"
if node.wakuRelay.isNil():
return err(MountWithoutRelayError)
info "mounting lightpush with relay"
let pushHandler = lightpush_protocol.getRelayPushHandler(node.wakuRelay)
node.wakuLightPush = WakuLightPush.new(
node.peerManager, node.rng, pushHandler, node.wakuAutoSharding, some(rateLimit)
)
if node.started:
# Node has started already. Let's start lightpush too.
await node.wakuLightPush.start()
node.switch.mount(node.wakuLightPush, protocolMatcher(WakuLightPushCodec))
info "lightpush mounted successfully"
return ok()
proc mountLightPushClient*(node: WakuNode) =
info "mounting light push client"
if node.wakuLightpushClient.isNil():
node.wakuLightpushClient = WakuLightPushClient.new(node.peerManager, node.rng)
proc lightpushPublishHandler(
node: WakuNode,
pubsubTopic: PubsubTopic,
message: WakuMessage,
peer: RemotePeerInfo | PeerInfo,
mixify: bool = false,
): Future[lightpush_protocol.WakuLightPushResult] {.async.} =
let msgHash = pubsubTopic.computeMessageHash(message).to0xHex()
if not node.wakuLightpushClient.isNil():
notice "publishing message with lightpush",
pubsubTopic = pubsubTopic,
contentTopic = message.contentTopic,
target_peer_id = peer.peerId,
msg_hash = msgHash,
mixify = mixify
if defined(libp2p_mix_experimental_exit_is_dest) and mixify:
#indicates we want to use mix to send the message
when defined(libp2p_mix_experimental_exit_is_dest):
#TODO: How to handle multiple addresses?
let conn = node.wakuMix.toConnection(
MixDestination.exitNode(peer.peerId),
WakuLightPushCodec,
MixParameters(expectReply: Opt.some(true), numSurbs: Opt.some(byte(1))),
# indicating we only want a single path to be used for reply hence numSurbs = 1
).valueOr:
error "could not create mix connection"
return lighpushErrorResult(
LightPushErrorCode.SERVICE_NOT_AVAILABLE,
"Waku lightpush with mix not available",
)
return await node.wakuLightpushClient.publish(some(pubsubTopic), message, conn)
else:
return await node.wakuLightpushClient.publish(some(pubsubTopic), message, peer)
if not node.wakuLightPush.isNil():
if mixify:
error "mixify is not supported with self hosted lightpush"
return lighpushErrorResult(
LightPushErrorCode.SERVICE_NOT_AVAILABLE,
"Waku lightpush with mix not available",
)
notice "publishing message with self hosted lightpush",
pubsubTopic = pubsubTopic,
contentTopic = message.contentTopic,
target_peer_id = peer.peerId,
msg_hash = msgHash
return
await node.wakuLightPush.handleSelfLightPushRequest(some(pubsubTopic), message)
proc lightpushPublish*(
node: WakuNode,
pubsubTopic: Option[PubsubTopic],
message: WakuMessage,
peerOpt: Option[RemotePeerInfo] = none(RemotePeerInfo),
mixify: bool = false,
): Future[lightpush_protocol.WakuLightPushResult] {.async.} =
if node.wakuLightpushClient.isNil() and node.wakuLightPush.isNil():
error "failed to publish message as lightpush not available"
return lighpushErrorResult(
LightPushErrorCode.SERVICE_NOT_AVAILABLE, "Waku lightpush not available"
)
if mixify and node.wakuMix.isNil():
error "failed to publish message using mix as mix protocol is not mounted"
return lighpushErrorResult(
LightPushErrorCode.SERVICE_NOT_AVAILABLE, "Waku lightpush with mix not available"
)
let toPeer: RemotePeerInfo = peerOpt.valueOr:
if not node.wakuLightPush.isNil():
RemotePeerInfo.init(node.peerId())
elif not node.wakuLightpushClient.isNil():
node.peerManager.selectPeer(WakuLightPushCodec).valueOr:
let msg = "no suitable remote peers"
error "failed to publish message", msg = msg
return lighpushErrorResult(LightPushErrorCode.NO_PEERS_TO_RELAY, msg)
else:
return lighpushErrorResult(
LightPushErrorCode.NO_PEERS_TO_RELAY, "no suitable remote peers"
)
let pubsubForPublish = pubsubTopic.valueOr:
if node.wakuAutoSharding.isNone():
let msg = "Pubsub topic must be specified when static sharding is enabled"
error "lightpush publish error", error = msg
return lighpushErrorResult(LightPushErrorCode.INVALID_MESSAGE, msg)
let parsedTopic = NsContentTopic.parse(message.contentTopic).valueOr:
let msg = "Invalid content-topic:" & $error
error "lightpush request handling error", error = msg
return lighpushErrorResult(LightPushErrorCode.INVALID_MESSAGE, msg)
node.wakuAutoSharding.get().getShard(parsedTopic).valueOr:
let msg = "Autosharding error: " & error
error "lightpush publish error", error = msg
return lighpushErrorResult(LightPushErrorCode.INTERNAL_SERVER_ERROR, msg)
# toRLNSignal includes the timestamp in the proof input, so the timestamp
# must be fixed before proof generation. The downstream ensureTimestampSet
# in the client publish becomes an idempotent no-op safety net.
let message = ensureTimestampSet(message)
let rln =
if node.rln.isNil():
none(Rln)
else:
some(node.rln)
let msgWithProof = (await checkAndGenerateRLNProof(rln, message)).valueOr:
return lighpushErrorResult(LightPushErrorCode.OUT_OF_RLN_PROOF, error)
let firstResult =
await lightpushPublishHandler(node, pubsubForPublish, msgWithProof, toPeer, mixify)
# A publish error can indicate a stale Merkle proof path; refresh it and
# retry the publish once. Gate only on unambiguously RLN-related failures:
# 504 (OUT_OF_RLN_PROOF) is always RLN-specific; 420 (INVALID_MESSAGE) is
# also returned for non-RLN rejections (e.g. oversized messages), so require
# the error description to contain RlnValidatorErrorMsg — matching the legacy
# lightpush path — to avoid unbounded on-chain RPCs on non-RLN errors.
if firstResult.isOk() or rln.isNone():
return firstResult
let isRlnRelatedFailure =
firstResult.error.code == LightPushErrorCode.OUT_OF_RLN_PROOF or (
firstResult.error.code == LightPushErrorCode.INVALID_MESSAGE and
firstResult.error.desc.get("").contains(RlnValidatorErrorMsg)
)
if not isRlnRelatedFailure:
return firstResult
info "lightpush send rejected; refreshing merkle proof and retrying once",
statusCode = $firstResult.error.code
rln.get().groupManager.invalidateMerkleProofCache()
proc runRetry(): Future[lightpush_protocol.WakuLightPushResult] {.async, gcsafe.} =
let retryMsg = (
await checkAndGenerateRLNProof(rln, msgWithProof, regenerate = true)
).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",
statusCode = $firstResult.error.code
return firstResult
return retryFut.read()