logos-delivery/docs/analysis/plan_phase3_wakuenvelope.md
NagyZoltanPeter 05ac82d454
docs: async copy analysis and 3-phase elimination plan
Analysis of chronos/refc deep-copy behavior on the WakuMessage path
(~19+F payload copies, 4-6 SHA-256 passes per inbound relayed message)
plus executor plans for: perf harness, WakuMessage ref object, WakuEnvelope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 04:07:59 +02:00

9.1 KiB
Raw Blame History

Phase 3 — WakuEnvelope API break: decode once, hash once (executor plan)

Self-contained implementation plan. Context: async_copy_analysis.md, async_copy_fix_plan.md. Prerequisites: Phases 1 and 2 merged (WakuMessage is a ref object; benchmark + counters exist).

Objective: introduce WakuEnvelope (message + pubsubTopic + msgHash, one ref) as the unit that flows through internal dispatch, eliminate redundant proto decodes and computeMessageHash recomputation, and stop copying the filter push buffer per peer. Target, verified by Phase 1 counters: ≤ 2 decodes and 1 hash per inbound relayed message (from ≥ 4 and 46), plus 1 encode + 1 hash on the outbound publish leg.

This is a deliberate internal API break: WakuRelayHandler changes shape, and every registered handler follows.

Constraints

Same as previous phases (refc, nph, gitnexus impact/detect_changes, no push, verify by running). Run gitnexus_impact({target: "WakuRelayHandler", direction: "upstream"}) and report before editing.

Design decisions (already made — do not re-litigate)

  1. No validator→handler cache. The gossipsub validator and the topic handler have no shared side channel, and keying a handoff cache by a non-cryptographic hash of the raw bytes would allow attacker-crafted collisions to substitute messages. We accept 2 decodes (validator + topicHandler); the envelope kills the other 23 decodes and all re-hashing. A keyed cache can be a later optimization with its own security review.
  2. The onRecv observer decode is deleted outright — its decoded message is unused (see Step 2).
  3. WakuEnvelope is a ref object and immutable by convention, same as WakuMessage.

Step 1 — The type

New file logos_delivery/waku/waku_core/message/envelope.nim (~50 LOC), exported from waku_core:

type WakuEnvelope* = ref object
  msg*: WakuMessage
  pubsubTopic*: PubsubTopic
  hash*: WakuMessageHash

proc init*(T: type WakuEnvelope, pubsubTopic: PubsubTopic, msg: WakuMessage): T =
  WakuEnvelope(msg: msg, pubsubTopic: pubsubTopic,
               hash: computeMessageHash(pubsubTopic, msg))

Plus shortLog/$ helper for chronicles fields (hash as 0x-hex).

Step 2 — WakuRelay (logos_delivery/waku/waku_relay/protocol.nim)

Current inbound anatomy (line refs at time of writing):

  • ordered validator decodes (generateOrderedValidator, :536-565, decode at :543, hash-on-reject at :553)
  • observers (initRelayObservers, :255-353): onRecv :301 decodes via decodeRpcMessageInfo (:256) but updateMetrics (:283) never uses the decoded message — only sizes; onValidated :324 decodes only to log; onSend :339 decodes to log + metrics
  • topicHandler wrapper (subscribe, :596-634, decode at :603) → calls the WakuRelayHandler
  • publish (:674-695): timestamp fix (Phase 2 made it ensureTimestampSet), encode, hash, gossipsub publish

Changes:

  1. WakuRelayHandler (type in this module; 19 src + 5 test references repo-wide) becomes proc(envelope: WakuEnvelope): Future[void] {.gcsafe, raises: [].} (keep the existing pragma set).
  2. topicHandler wrapper (:600-616): after the (kept) decode, build let envelope = WakuEnvelope.init(pubsubTopic, decMsg) — the single hash computation for the inbound path — and call handler(envelope).
  3. Ordered validator (:536-565): decode stays (validators need the message). The reject-path hash (:553) may stay — it only runs on rejected messages.
  4. Observers:
    • onRecv (:301-322): delete the decodeRpcMessageInfo call; keep control-message handling and byte metrics computed from msg.data.len + msg.topic.len directly.
    • onValidated (:324-337): decodes only for logMessageInfo. Wrap in a compile-time or log-level gate (chronicles logLevel >= some threshold pattern used elsewhere in the repo) so production INFO builds do not pay a decode per message; alternatively downgrade to logging msgId + topic only (no decode). Pick whichever keeps current log content at DEBUG/TRACE.
    • onSend (:339-348): same treatment as onValidated.
  5. publish (:674-695): after encode, computeMessageHash stays (outbound leg, 1 hash) — no structural change beyond what Phase 2 did.
  6. validateMessage (:567-594, lightpush entry): currently re-encodes just for the size check and re-hashes for logging. Add an overload/param taking the already-encoded buffer length and an optional precomputed hash so the lightpush path (which has the buffer) stops double-encoding. Small, contained.

Step 3 — Dispatch (logos_delivery/waku/node/subscription_manager.nim:29-85)

registerRelayHandler: traceHandler/filterHandler/archiveHandler/syncHandler/ internalHandler (:45-70), uniqueTopicHandler (:72-84), and the legacyAppHandlers: Table[PubsubTopic, WakuRelayHandler] all switch to the envelope signature. Inside, replace (topic, msg) argument pairs with envelope and use envelope.msg.payload.len etc. MessageSeenEvent.emit(node.brokerCtx, topic, msg) (:70): change the event to carry the envelope (see Step 5).

Step 4 — Consumers (each loses its own computeMessageHash)

28 src computeMessageHash call sites exist; ~1014 die here. For each consumer, change the entry point signature and use envelope.hash:

Consumer Entry point Redundant hash removed
Archive waku_archive/archive.nim:98 handleMessage :101
Filter waku_filter_v2/protocol.nim:243 handleMessage :246 and :195
Store-sync waku_store_sync/reconciliation.nim messageIngress (called from subscription_manager:67) :78
Lightpush→relay waku_lightpush*/callbacks.nim:16-24 (builds the relay publish) :24 (hash once when building the envelope/publish result)
API/REST relay api/relay.nim (:30) and any REST messaging handler registering a WakuRelayHandler (this branch's messaging REST endpoints) :30
library/FFI events library/json_message_event.nim (:88) via MessageSeenEvent :88

Keep computeMessageHash call sites that hash other messages (store queries hashing stored rows, sync transfer :171, filter rpc :95 on the client side, etc.) — only remove recomputation of an inbound message's own hash where an envelope is in hand.

Step 5 — Broker event

MessageSeenEvent (logos_delivery/api/events/kernel_events): change payload from (topic, msg) to the envelope (or add hash alongside, whichever keeps broker codegen simplest). Update its listeners — notably the library/JSON event path, which then uses envelope.hash instead of recomputing (:88) — and any messaging-REST event cache on this branch that listens to MessageSeenEvent.

Step 6 — Filter per-peer buffer share (waku_filter_v2/protocol.nim)

handleMessagepushToPeers (:213-216) encodes MessagePush once (good) but then pushToPeer(peerId, buffer) (:170) is {.async.} with a by-value seq[byte] — one buffer deep-copy per subscribed peer. Fix: make pushToPeer {.async: (raw: true).} or pass ref seq[byte]. One proc + call site, ~25 LOC.

Step 7 — Tests

  • Update every handler construction to the envelope signature (grep WakuRelayHandler in tests, plus tests calling handleMessage(topic, msg) on archive/filter and messageIngress directly): wrap with WakuEnvelope.init(topic, msg).
  • Add toEnvelope(topic, msg) convenience to tests/testlib/wakucore.nim if it reduces churn (thin alias of WakuEnvelope.init).
  • New unit tests (~40 LOC): envelope init computes the same hash as computeMessageHash; relay topicHandler delivers an envelope whose hash matches an independently computed one.
  • Affected suites to run individually while iterating: tests/waku_relay, tests/node, tests/waku_archive, tests/waku_filter_v2, tests/waku_lightpush, tests/waku_store_sync, plus the REST/API suites on this branch.

Step 8 — Verification (mandatory)

  1. Full make test green.
  2. Phase 1 macro benchmark: counter assertion is the acceptance gatedecodes_per_msg ≤ 2 and hashes_per_msg == 1 on the receiving node (observer logging gated per Step 2.4; if the gate is log-level-based, run the bench at INFO). Micro bench: expect ~7580 % alloc-volume reduction vs bench_baseline.md and the throughput gain in the analysis tables (~4× headroom at the 10/50/150 kB profile).
  3. Record the after-numbers in docs/analysis/bench_baseline.md (append a "Phase 3" section with commit hash).
  4. gitnexus_detect_changes() reviewed — the affected set must be: waku_core (new file), waku_relay, node/subscription_manager, waku_archive, waku_filter_v2, waku_lightpush, waku_store_sync, api/events + api/relay, library, tests. Anything outside that list is scope creep — stop and reassess.
  5. Commit (no push).

Estimated diff

~150 new LOC, ~640990 changed LOC (tests ~250400 of that). Budget: 46 days. The only genuinely fiddly part is Step 2 (relay observers + handler wrapper); do it first, get tests/waku_relay green, then fan out to consumers mechanically.