An audit read the code and found the proposal that specifies it. Go reaches
all 122 capability proposals and 64 of the 121 other Standards Track ones;
Rust has 35, mostly the operator and the CLI; C++ has three, because the
complete original C++ in the estate is a ZAP handler, a bot nothing specifies,
and authored kernels inside forked CUDA and Metal crates.
| | shipped | partial | none | not assessed |
| Go | 146 | 33 | 1 | 80 |
| C++ | 1 | 2 | 0 | 257 |
| Rust | 10 | 25 | 1 | 224 |
The large "not assessed" columns are the point rather than a gap. The model and
training specs are Python, the frontend and design-system proposals are
TypeScript, and recording `none` against those would read as a search that
came up empty when the honest statement is that the runtime was never the
question. Governance, Meta and Process are not graded at all.
Six proposals are Final with nothing found in Go and are left unwritten rather
than demoted: 0065, 0089, 0098, 0103, 0521 and 0902. HIP-0521 answers itself,
its own section reading "Not yet implemented". Three of the others name a
canonical Go file in consensus/protocol/auth that nobody wrote, in a package
where the four neighbouring proposals' files all exist with tests beside them.
Co-authored-by: Hanzo Dev <[email protected]>
19 KiB
hip, title, author, type, category, status, implementation-go, created, requires
| hip | title | author | type | category | status | implementation-go | created | requires |
|---|---|---|---|---|---|---|---|---|
| 0107 | Streaming Replication over VFS | Hanzo AI Team | Standards Track | Infrastructure | Final | partial | 2026-05-18 | HIP-0026, HIP-0027, HIP-0302 |
HIP-0107: Streaming Replication over VFS
Abstract
This proposal defines the unified streaming replication contract for every durable state in Hanzo. All sources of incremental updates — SQLite WAL, ZapDB log, blockchain state, generic append-only logs — ship frames through a single pipeline built from three orthogonal pieces:
- One source-adapter interface (
replicate.Source) — pulls frames from a backing engine. - One sink interface (
hanzo/vfs.Backend) — the only object-store abstraction in Hanzo. All sink-side bytes arevfs.Writecalls. - One encryption layer (
luxfi/age) — frames are age-encrypted at the boundary; the master key lives in KMS per HIP-0027.
The standalone hanzoai/zapdb-replicator sidecar is deprecated and
folded into hanzoai/replicate as a source adapter. The per-backend
sink code in replicate/s3, replicate/gs, replicate/abs,
replicate/nats, replicate/oss, replicate/sftp, replicate/webdav
collapses into hanzo/vfs. One way to do everything.
This HIP does not supersede HIP-0302; HIP-0302 stays Final as the spec for the SQLite-WAL and ZapDB substrate (encryption keys, recovery objectives, K8s sidecar pattern, PQ-safe age v1.3.0+ recipients). HIP-0107 sits on top: it unifies the transport so the SQLite backend, the ZapDB backend, and any future backend route bytes through the same pipeline.
Two mechanisms, one substrate (the reconciliation). Hanzo grew TWO
SQLite-HA implementations: hanzoai/replicate (a Litestream fork —
continuous streaming WAL → object store, files stored as age-encrypted
.zap.age) and hanzoai/vfs/replica (whole-object VACUUM INTO
snapshots plus the piece Litestream lacks: a coordinator-free single-writer
election). That overlap is a one-and-one-way violation. This HIP resolves
it, and names the winners — one home per concern: streaming replication
(hanzoai/replicate) is the canonical durability/replication transport; HRW
single-writer election is hanzoai/ha (extracted from vfs/replica, which
had kept storage-agnostic election inside a SQLite library — its own
one-and-one-way violation); the VACUUM INTO snapshot / hydrate / adopt
primitives stay in hanzoai/vfs/replica. ha decides WHO writes;
vfs/replica decides HOW state ships. The whole-object snapshot path is
superseded by streaming for the replication path; election (now ha) and
the hydrate/adopt primitives stay. See
"HA-SQLite substrate" and "Relationship to HIP-0302 and hanzoai/replicate"
below.
Specification
Architecture
┌───────────────────────────────────────────┐
│ hanzoai/replicate │
│ │
┌─────────┐ │ ┌──────────────┐ ┌─────────────────┐ │ ┌────────────┐
│ Source │──┼─▶│ frame-pump │───▶│ age-encrypt │──┼─▶│ vfs.Backend│
│ adapter │ │ │ (one impl) │ │ (luxfi/age) │ │ │ (sink) │
└─────────┘ │ └──────────────┘ └─────────────────┘ │ └────────────┘
└───────────────────────────────────────────┘
Four canonical source adapters ship with hanzoai/replicate:
| Source | Engine | Frame definition |
|---|---|---|
source/sqlite-wal |
SQLite WAL (the original Litestream surface) | WAL frames, header per Litestream protocol |
source/zapdb-log |
ZapDB incremental backup | ZAP frame format (magic 0x5A415001, defined in HIP-0302 §"ZapDB Streaming Replication") |
source/chain-state |
Lux primary network, regulated EVM L1, Z-Chain, Hanzo's L1 | per-block state delta + snapshot frames |
source/generic-log |
Any append-only log file or Unix socket | length-prefixed records |
All four implement:
package replicate
type Source interface {
Name() string
// PullFrames yields frames as the source produces them.
// The returned channel closes when the source quiesces.
PullFrames(ctx context.Context) (<-chan Frame, error)
// Restore reads frames in order and applies them to the destination.
Restore(ctx context.Context, frames <-chan Frame, dst string) error
}
type Frame struct {
ID uint64 // monotonic per-source
Kind FrameKind // snapshot | delta | tombstone
Payload []byte // engine-specific (WAL bytes, ZAP frame, block delta, log record)
Checksum uint32 // CRC-32 over Payload
Timestamp time.Time
}
Sink: hanzo/vfs is the only object store
The per-backend code currently in hanzoai/replicate/{s3,gs,abs,nats, oss,sftp,webdav} moves to hanzo/vfs as standard
vfs.Backend implementations. After this HIP lands:
hanzoai/replicatehas zero direct cloud-SDK imports. The only storage interaction isvfs.Open(uri)+vfs.Backend.Write(...).- Adding a new sink backend is one PR to
hanzo/vfs. The replication pipeline picks it up for free. - A deployment configures replication via a single
vfs://URI:vfs://gs/hanzo-replicate-mainnet/iam/<instance>/....
Encryption boundary
Age encryption happens once, at the frame boundary, before the sink call. The recipient set per HIP-0302:
- ML-KEM-768 + X25519 hybrid (
age1pqprefix), age v1.3.0+ - Per-service keypair derived from a KMS-stored master key via HKDF
with
hanzo:replicate:domain separator - HIP-0302 recovery objectives apply unchanged (RPO 1s SQLite / 500ms ZapDB; RTO 30s / 15s)
The sink layer sees opaque ciphertext. The sink layer must not inspect frame contents.
Frame format (canonical)
The on-disk frame format is the ZAP frame already defined in HIP-0302 §"ZapDB Streaming Replication":
Frame header (16 bytes):
[0:4] magic = 0x5A415001 ("ZAP\x01")
[4:8] frame_id = monotonic uint32
[8:12] length = payload length in bytes
[12:14] checksum = CRC-16 of payload
[14:15] flags = 0x01=snapshot, 0x02=delta, 0x04=compressed
[15:16] reserved = 0x00
The SQLite-WAL source wraps WAL frames in this envelope. The chain-state source wraps block deltas. The generic-log source wraps records. One on-disk format across all sources.
Multi-tenant bucket layout
The KMS-derived master key is per-deployment. Each tenant org gets a distinct key path:
vfs://<backend>/<bucket>/replicate/<org>/<service>/<source>/
├── snapshots/<frame_id>.snap.zap.age
├── deltas/<start>_<end>.delta.zap.age
└── latest
<org>comes from JWTownerclaim (HIP-0026 unified identity).<service>is the subsystem name (iam,base,kms, ...).<source>is one ofsqlite-wal,zapdb-log,chain-state,generic-log.latestis a small pointer object naming the most recent frame.
Restore protocol
On pod startup, the K8s init container per HIP-0302 invokes:
hanzo-replicate restore \
--source=sqlite-wal \
--vfs=vfs://gs/hanzo-replicate-mainnet \
--org=hanzo \
--service=iam \
--dst=/data/iam.db \
--age-key-ref=kms://hanzo/superbase/master
The same binary handles all four source types via the --source flag.
Restore drains the latest snapshot frame, then applies delta frames in
monotonic order, then resumes streaming.
HA-SQLite substrate: single-writer election + adoption contract (shipped)
The streaming pipeline above answers "how do bytes get durable." It does
NOT answer "which replica may write." Litestream — and therefore
hanzoai/replicate — assumes a single writer and does not elect one.
hanzoai/ha supplies exactly that missing half — coordinator-free
single-writer election — and hanzoai/vfs/replica supplies the primitives a
service needs to adopt HA without changing its own storage library. Together
they are the shared substrate every Hanzo service imports instead of
re-inventing durability: ha decides WHO writes, vfs/replica decides HOW
state ships.
Single-writer election (Rendezvous / HRW), no coordinator — hanzoai/ha
hanzoai/ha answers, identically on every replica and with no network
round-trip, "who is the single writer for key K?" via Highest-Random-Weight
hashing over the live membership set (K is any ownership unit — an org id, a
shard, a singleton job name):
Owner(key, members)— deterministic winner;weight = SHA-256(key ‖ 0x00 ‖ memberID)[:8]as a big-endian uint64, tie-broken on member ID so the result is order-independent.IsOwner(key, selfID, members)— the per-write hot-path gate.Replicas(key, members, n)— owner first, then ordered failover successors, so a reader can pre-warm the key it is next in line to own.Membership(seam) +Static— the live-set source: a cluster impl (ha/k8s, roadmap) or single-processStaticfor dev and tests.- Fail-closed: empty membership yields
ok=false— never a wrong writer. No election protocol, no lock service, no service discovery, no SQL, no KV. Pure Go, stdlib only.
Ownership is PER-ORG: the owning replica writes ALL of an org's DBs (root + every per-project + per-user file) so an org's data has locality and moves as one unit on failover. Reads are served from any replica's vfs-synced copy.
Adoption contract
Every adopter wires the same three lines at its per-org store seam:
- hydrate-on-open —
Pull()(orRestoreFile) the latest durable copy before first use of an org DB, so a fresh or replaced pod starts from committed state. - single-writer —
IsOwner(org, self, members)gates writes; a non-owner serves stale-tolerant reads (Pull-then-read) and forwards writes to the owner. - post-commit ship/stream — the owner ships after a write burst (the
shipped
Replicator.PushLoop), superseded on the streaming path byreplicatecontinuous WAL shipping.
Shipped primitives (hanzoai/vfs/replica)
SQLiteDB(sqlite.go) — the real per-org handle:Snapshot=VACUUM INTOa temp (forces a WAL checkpoint, one transaction-consistent copy, no torn pages, no-wal/-shmto ship);Restore= write-temp → close → drop-wal/-shm→ atomic rename → reopen (a crash mid-restore leaves the old DB intact). CGO-free (modernc.org/sqlite), same durability pragmas (WAL / NORMAL / foreign_keys / busy_timeout) as base and visor.SnapshotFile/RestoreFile— handle-less adoption primitives so an xorm / GORM service adopts HA without a handle conflict (restore runs BEFORE the service opens its own engine).BackendStore(store.go) — adapts anyvfsbackend (file://dev,s3://SeaweedFS prod) to theStore, with a cheap<key>.vercontent-version sidecar so a reader skips redundant pulls;Getrecomputes the version authoritatively, so a stale sidecar degrades to a redundant pull, never a wrong restore.- Per-org envelope encryption —
WithEncryption(Cipher, org)seals every pushed snapshot with the org's KMS-derived key, orgID bound as AAD so a blob cannot be replayed under another org (HIP-0302 recipients). DBPath(org, scope, service)— the canonical HIP-0302 object layout (orgs/<org>/[scope/]<service>.db), computed identically on every replica;Version(data)— the one content hash (SHA-256) both theReplicatorand anyStoreuse.
Live adoption
visor is the reference adopter and it is LIVE. object/store_replica.go
opts in via REPLICA_STORE (s3://… SeaweedFS in prod, file:// in test;
unset = local-only, nil replicator, today's behavior with zero regression).
It hydrates-on-open before xorm opens each org DB and runs a per-org ship
loop; the success path logs, verbatim (store_replica.go:128):
visor HA: org "_global" shipping to object store (durable)
with explicit push-FAILED / push-recovered logs so a durability feature can
never fail invisibly. hanzoai/cloud consolidated its own former
internal/org replicator + election onto this same package (#11a3ae6 —
re-exported as thin aliases in internal/org/shared.go), so cloud and
visor now run ONE implementation; election has since been extracted again
into hanzoai/ha (aliases repointed there; vfs/replica is now
replication-only). Rolling the same adoption to iam / commerce / base is the
remaining work.
Relationship to HIP-0302 and hanzoai/replicate
HIP-0302 stays Final: it owns the encryption recipients, recovery objectives, and the sidecar / emptyDir pattern. HIP-0107 owns the transport and the write-coordination on top. The two SQLite-HA mechanisms reconcile as follows — this is the decision, stated plainly:
| Concern | Winner | Why |
|---|---|---|
| Durability / replication transport | hanzoai/replicate (Litestream fork) |
Continuous streaming WAL → object store (converted to immutable LTX, shipped as age-encrypted .zap.age) is strictly better than whole-object snapshots: lower RPO, incremental bytes, no periodic full-DB rewrite. CGO-free (modernc.org/sqlite). Canonical replication path. |
| Single-writer coordination | hanzoai/ha |
Litestream / replicate do NOT elect a writer. HRW / Rendezvous election is the genuinely-novel piece — coordinator-free, fail-closed, no SQL/KV. Extracted from vfs/replica so a non-SQLite singleton (cron, queue drain) elects without importing a storage library. |
| Adoption / hydrate / restore | hanzoai/vfs/replica (SnapshotFile / RestoreFile) |
Handle-less primitives let any service hydrate-on-open and adopt HA regardless of its own DB library. |
Whole-object VACUUM INTO snapshot |
superseded for replication | Kept as the consistent-copy primitive behind SnapshotFile and as the currently-shipped ship path; replaced by streaming as the durability transport as services migrate. |
Honest status. The election, the adoption primitives, and the visor
live path are shipped. The convergence — every adopter shipping via
replicate streaming rather than the Replicator whole-object push, and
folding zapdb-replicator + the per-backend sinks into the one
source-adapter → age → vfs.Backend pipeline (the four-source design
above) — is the reconciliation roadmap this HIP ratifies, not a claim that
it is already uniform. The one-and-one-way violation (two overlapping
SQLite-HA libraries) is closed by DECISION here; the code convergence
follows the phases below.
Implementation phases
| Phase | Work | Effort |
|---|---|---|
| 1 | Move replicate/{s3,gs,abs,nats,oss,sftp,webdav} per-backend code into hanzo/vfs. Existing replicate.Sink interface deletes; callers switch to vfs.Backend. |
3–4 days |
| 2 | Fold hanzoai/zapdb-replicator into hanzoai/replicate as source/zapdb-log. Deprecate ghcr.io/hanzoai/zapdb-replicator (publish a final vN+1.0.0-deprecated tag pointing deployers at ghcr.io/hanzoai/replicate). |
2 days |
| 3 | Implement source/chain-state for Lux primary network. Reuse for regulated EVM L1 and any other Lux-derived L1. |
3–5 days for Lux primary; ~1–2 days per derived L1. |
| 4 | Implement source/generic-log for arbitrary append-only logs (Unix socket or file). |
1 day |
| 5 | Update K8s manifests across ~/work/hanzo/universe/infra/k8s/ to swap zapdb-replicator sidecars for replicate --source=zapdb-log. Bump hanzoai/replicate to the version that ships all four sources. |
1 day per service touched |
Non-goals
hanzoai/datastore(Hanzo Datastore fork). Datastore already uses ReplicatedMergeTree + S3-disk for its own replication. HIP-0107 does not touch it. The two systems share an S3 bucket viavfsprefix separation:s3://bucket/datastore/...is Datastore-owned;s3://bucket/replicate/...is HIP-0107-owned. No collision.- Cross-region replication patterns. Active-active, geo-failover,
read replicas across regions — separate concern. HIP-0107 ships
frames to one
vfs.Backend; what the operator chooses for the region/zone topology of that backend is outside scope. - Real-time CDC for non-WAL sources. Hanzo Base's per-record hooks (HIP-0105) carry their own change-data-capture stream. That is a logical-event pipeline; HIP-0107 is a byte-level frame pipeline. They do not converge.
- Snapshot retention / GC policy. HIP-0302 already defines RPO, RTO, and retention windows. HIP-0107 inherits them unchanged.
- Replacing HIP-0302's encryption + recovery contract. HIP-0302 stays Final. HIP-0107 adds the unified transport on top.
Open questions
- vfs URI scheme for non-S3 backends.
vfs://gs/...,vfs://abs/...,vfs://sftp/...— confirm the URI prefix matches the existinghanzo/vfsconvention before Phase 1. - Chain-state delta granularity. Per-block is the obvious default. Some Lux-derived L1s have 200ms block times — frames at 5 Hz per chain may stress the sink. Suggest configurable batching, default = 1 frame per N blocks where N is tuned per chain.
- Generic-log fan-in. A single replicate process pumping N
generic logs sharing one
Source— likely fine; benchmark before committing to a goroutine-per-source model. - Backpressure when vfs.Backend is slow. Today
zapdb-replicatordrops oldest unsent frame on bounded-queue overflow. Confirm this is the policy we want across all four sources, or whetherchain-state(consensus-critical) needs block-on-full instead.
References
- HIP-0027 — Secrets Management Standard (the KMS that holds the age master key per service/per org)
- HIP-0302 — Hanzo Replicate: Encrypted SQLite + ZapDB Durability for Base Services (the predecessor for the SQLite/ZapDB substrate; HIP-0107 adds the unified pipeline on top — HIP-0302 stays Final)
- HIP-0405 — S3 CRD (the
hanzoai/s3workload behind thes3://sink this pipeline writes to in production) - Reference implementations:
~/work/hanzo/ha(SHIPPED) —github.com/hanzoai/ha, HRW single-writer election (Owner/IsOwner/Replicas/Member) + theMembershipseam andStatic. Pure Go, stdlib only; election with no storage dependency.~/work/hanzo/vfs/replica(SHIPPED) — theReplicator(replica.go), the realSQLiteDBsnapshot/restore +SnapshotFile/RestoreFileadoption primitives (sqlite.go), andBackendStoreover avfsbackend (store.go). Replication only; election lives inhanzoai/ha.~/work/hanzo/replicate—github.com/hanzoai/replicate, the Litestream fork: streaming WAL → immutable LTX → object store, files stored as age-encrypted.zap.age; the canonical replication transport.~/work/hanzo/vfs— the sink backends (vfs.Backend).
- Litestream protocol (SQLite WAL framing): https://litestream.io
- age v1.3.0+ PQ-hybrid recipients: https://age-encryption.org