Files
zeekayandHanzo Dev fb16ece6a3 feat(zapdb): native ZAP replication — CDC, physical snapshots, PQ, streaming
Replicates a zapdb to S3 via hanzoai/vfs with no keyspace scan in steady state
and post-quantum encryption out of the box.

- CDC incrementals via db.Subscribe (the WAL tail), not Backup(since)'s O(total)
  scan — a 100-key delta out of 200k keys: 2.76us/19KB vs 20ms/1GB (~7000x).
- Physical SST-copy snapshots (sparse, hole-aware): 134ms/25MB vs logical
  158ms/1.25GB/2.2M allocs. Hydrated before Badger opens; incrementals replay on top.
- Streaming codec: Backup -> zstd -> age into a pooled buffer (peak mem = compressed).
- Post-quantum (ML-KEM-768 + X25519 via luxfi/age v1.5.0), client-side before S3.
  Out of the box via REPLICATE_AGE_KEY_FILE (generate+persist, recipient derived).
- Per-DB streams (REPLICATE_AUTO_ON_OPEN) for subprocess VM plugins; backer/restorer
  split (REPLICATE_BACKUP / REPLICATE_SOURCE_ORDINAL) for multi-node.
- Public imports only (hanzoai/vfs v0.4.1, no replace). Verified live on an EVM
  C-chain end-to-end incl. disaster-recovery exact-match. Canonical KV schema
  (zapdb/zapdb.zap) + zero-copy codec (zapkv.go). Renames engine alias badger->zdb.

Co-authored-by: Hanzo Dev <[email protected]>
2026-06-16 02:00:03 +00:00

85 lines
3.3 KiB
Go

// Copyright (C) 2020-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package zapdb
// Canonical ZAP encoding for a KV record — the wire format defined in the
// colocated schema ./zapdb.zap (written in the ZAP format whose layout rules
// live in github.com/zap-proto/zap-spec). Unlike luxfi/zapdb's pb.marshal_zap
// (a sequential [u32 len][bytes]… layout that is "zap" in name only), this is a
// true ZAP struct: a fixed 48-byte header of scalars and (offset:u32, length:u32)
// pointer pairs, with payloads in the heap area that follows. Any field resolves
// in O(1) from its compile-time offset, and the byte accessors return slices that
// ALIAS the source buffer — zero-copy reads, no per-field allocation.
//
// This is the prototype runtime for moving the ZapDB backup codec onto the
// canonical schema. See zapkv_test.go for byte-stability + zero-copy proof and
// the package doc note on why this is not yet wired into luxfi/zapdb's backup.
import (
"encoding/binary"
"fmt"
)
// kvHeaderSize is the fixed-area size of a canonical ZAP KV: two u64 scalars
// (Version, ExpiresAt) + four (offset,length) pointer pairs (Key, Value,
// UserMeta, Meta), 8 bytes each = 48 bytes. Offsets are from the message start.
const kvHeaderSize = 48
func leU32(b []byte, off int) uint32 { return binary.LittleEndian.Uint32(b[off:]) }
func leU64(b []byte, off int) uint64 { return binary.LittleEndian.Uint64(b[off:]) }
// zapKV is a zero-copy view over a canonical ZAP KV record.
type zapKV struct{ b []byte }
func (k zapKV) Version() uint64 { return leU64(k.b, 0) }
func (k zapKV) ExpiresAt() uint64 { return leU64(k.b, 8) }
// ptr resolves an (offset,length) pointer at header offset hoff to a slice that
// aliases the underlying message — no copy.
func (k zapKV) ptr(hoff int) []byte {
off, length := leU32(k.b, hoff), leU32(k.b, hoff+4)
if length == 0 {
return nil
}
return k.b[off : off+length]
}
func (k zapKV) Key() []byte { return k.ptr(16) }
func (k zapKV) Value() []byte { return k.ptr(24) }
func (k zapKV) UserMeta() []byte { return k.ptr(32) }
func (k zapKV) Meta() []byte { return k.ptr(40) }
// encodeZapKV writes the canonical fixed-header + heap encoding of one KV record.
func encodeZapKV(version, expiresAt uint64, key, value, userMeta, meta []byte) []byte {
b := make([]byte, kvHeaderSize+len(key)+len(value)+len(userMeta)+len(meta))
binary.LittleEndian.PutUint64(b[0:], version)
binary.LittleEndian.PutUint64(b[8:], expiresAt)
off := kvHeaderSize
put := func(hoff int, data []byte) {
binary.LittleEndian.PutUint32(b[hoff:], uint32(off))
binary.LittleEndian.PutUint32(b[hoff+4:], uint32(len(data)))
off += copy(b[off:], data)
}
put(16, key)
put(24, value)
put(32, userMeta)
put(40, meta)
return b
}
// decodeZapKV returns a zero-copy view after bounds-checking every pointer, so an
// adversarial/truncated buffer can't make an accessor read out of range.
func decodeZapKV(b []byte) (zapKV, error) {
if len(b) < kvHeaderSize {
return zapKV{}, fmt.Errorf("zapkv: record %d bytes < header %d", len(b), kvHeaderSize)
}
for _, hoff := range []int{16, 24, 32, 40} {
off, length := leU32(b, hoff), leU32(b, hoff+4)
if uint64(off)+uint64(length) > uint64(len(b)) {
return zapKV{}, fmt.Errorf("zapkv: pointer @%d (off=%d len=%d) exceeds buffer %d", hoff, off, length, len(b))
}
}
return zapKV{b}, nil
}