mirror of
https://github.com/logos-messaging/logos-delivery.git
synced 2026-07-23 21:13:14 +00:00
The forceMerkleProofRefresh bool was threaded through five procs (checkAndGenerateRLNProof → generateRLNProof → generateProof base + on-chain → ensureFreshMerkleProofPath) purely so a caller could ask the merkle path to be refetched from chain on a stale-RLN retry. Reading any one of those signatures required a mental stack trace back up to the retry caller to know what "force" meant. Flip the contract: expose invalidateMerkleProofCache on the group manager; callers invoke it on the retry path to empty the cache, and the normal proof-gen flow then sees an empty cache and refetches on its own. No flag crosses procs. Removed forceMerkleProofRefresh from: - group_manager_base.generateProof (+ on_chain override) - ensureFreshMerkleProofPath (its `force` param) - generateRLNProof Renamed on checkAndGenerateRLNProof: forceMerkleProofRefresh → regenerate. It now controls one thing — bypassing the "message already has a proof" short-circuit — and never propagates downstream. Callers pair it with invalidateMerkleProofCache when they need the refetch semantics too. Retry callers updated in lockstep: generateRLNProofWithRootRefresh, REST attachRlnProofValidateWithRetry, and both legacy + modern lightpush retry paths now call groupManager.invalidateMerkleProofCache() before regenerating. The primitive test in test_wakunode_lightpush.nim now exercises the pair directly, and the onchain ensureFreshMerkleProofPath test is rewritten to exercise invalidate → ensureFresh as one flow (retiring its `force = true` variant). Pure refactor; behavior unchanged. Verified against: tests/waku_rln_relay/test_rln_group_manager_onchain.nim (29/29) tests/wakunode_rest/test_rest_relay.nim (13/13) tests/waku_rln_relay/test_wakunode_rln_relay.nim (5/5, 1 skip) tests/node/test_wakunode_lightpush.nim (11/11) tests/node/test_wakunode_legacy_lightpush.nim (9/9) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
390 lines
15 KiB
Nim
390 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,
|
|
../../rln/nonce_manager
|
|
|
|
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,
|
|
drawnMessageId: Option[Nonce],
|
|
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, reuseMessageId = drawnMessageId
|
|
)
|
|
).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, drawnMessageId) = (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, drawnMessageId, 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, drawnMessageId) = (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, reuseMessageId = drawnMessageId
|
|
)
|
|
).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()
|