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>
9.1 KiB
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 andcomputeMessageHashrecomputation, 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 4–6), plus 1 encode + 1 hash on the outbound publish leg.This is a deliberate internal API break:
WakuRelayHandlerchanges 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)
- 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 2–3 decodes and all re-hashing. A keyed cache can be a later optimization with its own security review.
- The
onRecvobserver decode is deleted outright — its decoded message is unused (see Step 2). WakuEnvelopeis aref objectand 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 viadecodeRpcMessageInfo(:256) butupdateMetrics(: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 theWakuRelayHandler publish(:674-695): timestamp fix (Phase 2 made itensureTimestampSet), encode, hash, gossipsub publish
Changes:
WakuRelayHandler(type in this module; 19 src + 5 test references repo-wide) becomesproc(envelope: WakuEnvelope): Future[void] {.gcsafe, raises: [].}(keep the existing pragma set).- topicHandler wrapper (:600-616): after the (kept) decode, build
let envelope = WakuEnvelope.init(pubsubTopic, decMsg)— the single hash computation for the inbound path — and callhandler(envelope). - Ordered validator (:536-565): decode stays (validators need the message). The reject-path hash (:553) may stay — it only runs on rejected messages.
- Observers:
onRecv(:301-322): delete thedecodeRpcMessageInfocall; keep control-message handling and byte metrics computed frommsg.data.len + msg.topic.lendirectly.onValidated(:324-337): decodes only forlogMessageInfo. Wrap in a compile-time or log-level gate (chronicleslogLevel >= some thresholdpattern 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.
publish(:674-695): after encode,computeMessageHashstays (outbound leg, 1 hash) — no structural change beyond what Phase 2 did.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; ~10–14 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)
handleMessage → pushToPeers (: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
WakuRelayHandlerin tests, plus tests callinghandleMessage(topic, msg)on archive/filter andmessageIngressdirectly): wrap withWakuEnvelope.init(topic, msg). - Add
toEnvelope(topic, msg)convenience totests/testlib/wakucore.nimif it reduces churn (thin alias ofWakuEnvelope.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)
- Full
make testgreen. - Phase 1 macro benchmark: counter assertion is the acceptance gate —
decodes_per_msg ≤ 2andhashes_per_msg == 1on 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 ~75–80 % alloc-volume reduction vsbench_baseline.mdand the throughput gain in the analysis tables (~4× headroom at the 10/50/150 kB profile). - Record the after-numbers in
docs/analysis/bench_baseline.md(append a "Phase 3" section with commit hash). 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.- Commit (no push).
Estimated diff
~150 new LOC, ~640–990 changed LOC (tests ~250–400 of that). Budget: 4–6 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.