mirror of
https://github.com/logos-messaging/logos-delivery.git
synced 2026-08-01 09:23:12 +00:00
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>
384 lines
15 KiB
Nim
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()
|