V4 CRITICAL: forward.handle no longer panics on hostile method/path (validForwardLine→400 + http.NewRequestWithContext); node.safeHandle adds a recover() boundary across all 5 dispatch sites (TCP dispatchLoop x2, getOrConnect, QUIC handleCallStream + invokeHandlerOneWay) — a handler panic drops one conn, never crashes the node. Proven on TCP+QUIC. V2 HIGH: forward/identity.go single-source strip set (superset of base's StripIdentityHeaders + Cookie drop + X-Gateway-* sweep), applied on BOTH legs (serve.go backend + client.go gateway); Authorization preserved. V6: SSE responses → 501 (buffered-stream guard). + consolidation: zap_crosswire_test.go proves byte-identical wire vs zap-proto/go; MIGRATION.md roadmap. Red-reviewed SHIP (0 crit/high/med). Zero exported-API change — source-compatible.
6.2 KiB
ZAP-native storage migration
The canonical KV + transport for Lux is luxfi/zap (this repo) backed by
luxfi/zapdb for the on-disk KV. New deployments use ZAP for hot KV
and cold ancient stores. PebbleDB / leveldb remain only as legacy
in-place upgrade paths.
ZAP runtime consolidation (serialization core)
github.com/zap-proto/go is the canonical pure-stdlib runtime for the ZAP
wire format (Parse/Message/Object/List + Builder/ObjectBuilder/
ListBuilder + Schema). This repo (luxfi/zap) historically duplicated
that core in its own zap.go/builder.go/schema.go while adding the
network layer (Node mesh, QUIC/TCP, mDNS, PQ-TLS handshake, EVM types, the
forward HTTP-over-ZAP contract). The duplication is being collapsed so
there is ONE serialization runtime; zap-proto/go is the source of truth and
this repo will ride it.
Status (first safe step landed): the two cores are now proven to emit a
byte-identical data segment, and zap-proto/go has absorbed the two deltas
that previously diverged it from this repo's hardened core, so the readers now
agree on accept/reject for every input.
- Wire proof.
zap_crosswire_test.go(here) and the file of the same name inzap-proto/gopin the SAMEgoldenV1Hexconstant. This repo'sNewBuilderV1emits it byte-for-byte;zap-proto/go's defaultNewBuilderemits it too. The ONLY header difference between this repo's default (Version2) output and the v1 golden vector is byte 4 (the version). A live cross-encode in a throwawaygo.workjoining both modules confirmed every field round-trips both directions. - Deltas reconciled in
zap-proto/go(additive — no honest wire changed): (1) it now acceptsVersion1ANDVersion2on read (was v1-only → it would reject this repo's default v2 buffers); (2) its reader now applies the same hardening this repo already had — unsigned-forwardByteswithabsPos < HeaderSizerejection,Object/ListabsOffset < HeaderSizerejection, andListlength ≤ msgsizeclamp.
Remaining steps (see zap-proto/go's LLM.md for the canonical list):
schema.go is already byte-identical and is the first alias target; the
reader/builder alias follows once cmd/zapgen lands (the read-buffer pool —
Message.refs/Release/Retain — is network-local and stays here); the
StartObject pre-reserve discipline is a documented, test-covered delta to
unify when the builder is aliased (adopt zap-proto/go's pre-reserve — it is
strictly more reserving and byte-identical for every honest layout).
This doc enumerates every storage surface in luxd and the migration path per backend.
Backends in luxd today
| Surface | Today | Canonical | Migration |
|---|---|---|---|
| P-Chain DB | leveldb | ZAP KV | One-shot migrator (planned, #186) |
| X-Chain DB | leveldb | ZAP KV | Same as P-Chain |
| EVM chainData (per chain) | PebbleDB | ZAP KV | migrate-pebble-to-zap (planned, #187) |
| EVM ancient store | upstream freezer | ZAP ancient (rawdb.ZapAncientStore) |
luxfi/geth/cmd/migrate-ancient (shipped) |
| Plugin DBs (aivm, etc.) | mixed | ZAP KV | Per-plugin opt-in |
| ZAP wire protocol (peer + RPC) | ZAP | ZAP | No change |
EVM ancient store — shipped
See ~/work/lux/geth/core/rawdb/zap_ancient.go. Key layout:
['a'][kind byte][big-endian uint64 number] -> snappy-compressed value
['m']['h'][kind byte] -> big-endian uint64 head
['m']['t'][kind byte] -> big-endian uint64 tail
Single ZAP database per chain. Snappy on by default (~10× compression on
cold data); tables can opt out via freezerTableConfig{noSnappy: true}.
Headers + canonical hashes are non-prunable; bodies + receipts are
prunable so TruncateTail is real work, not a no-op.
Migrating an existing chain
# Stop luxd on the box.
kubectl scale -n lux-system sts/<network>-archive --replicas=0
# Run the migrator. Source = upstream freezer dir; dest = new ZAP dir.
migrate-ancient \
--src /data/db/<network>/chainData/<chainID>/ancient \
--dst /data/ancient/<chainID>
# Flip the NodeFleet CR to point at the new ZAP store. (The default is
# already zap; existing legacy fleets need an explicit edit.)
kubectl -n lux-system edit nodefleet <network>-fleet
# spec.archive.ancientStore.backend: zap
# spec.archive.ancientStore.path: /data/ancient
# Bring the archive back up.
kubectl scale -n lux-system sts/<network>-archive --replicas=1
Verification
The migrator copies head + tail pointers. After re-opening, the new
store reports the same Ancients() and Tail() values. The chain head
on disk does not change — block import resumes at the same number.
ZAP wire (unchanged)
The peer-to-peer transport and the JSON-RPC server already use ZAP. No migration required.
Hot KV (P/X/chainData/plugin DBs)
Planned as separate one-shot migrators per backend (issues #186, #187).
ZAP KV under the hood is luxfi/zapdb (the BadgerDB fork) — the same
backend the EVM ancient store uses, so a unified migration story is
reachable.
Why ZAP
- Append-friendly LSM tree fits the cold block history pattern (~10× snappy compression on bodies + receipts).
- One backend everywhere → one toolchain, one set of operational primitives (snapshots, backups, profiling).
- Transport layer (
luxfi/zap) already uses the same codecs — the wire format and the storage format can sharenode_codec.go.
Constraints
- ZAP records max 2 GB per key/value (BadgerDB limit). Our largest cold record (a body at the network's gas cap) is ~2 MB; comfortably within budget.
- ZAP files are not directly addressable by other tools (Pebble's sst-dump style debug tools). Use the migrator + JSON-RPC.