Files
zap/LLM.md
T
Hanzo Dev 8a09d8eaf3 zap: locality-adaptive Router (P1) — one Send API, in-process Cost-0 fast path
Decouples WHO (Destination) from HOW (Interface) from the message (Payload):
Send picks the cheapest Interface whose CanReach(dst) is true. When the dst
lives in this binary, InProcessInterface (Cost 0) delivers the LIVE Value to a
local handler — zero serialize, zero copy, zero socket; Encode runs only on a
network hop. NodeInterface adapts the existing *Node wire (unchanged) as the
Cost-10 fallback. Named Router to stay orthogonal to the Transport wire enum
and the transport/ GPU subpackage. First consumer: hanzo/cloud o11y telemetry.

- router.go: Router, Destination, Payload, Interface, LocalHandler, ErrNoRoute
- interface.go: InProcessInterface (Cost 0) + NodeInterface (Cost 10, ErrNotEncodable)
- router_test.go: 7 tests incl. zero-serialize proof (Encode never called in-process)
2026-07-09 16:48:57 -07:00

11 KiB

Hanzo Zap

Overview

Go module: github.com/luxfi/zap

Tech Stack

  • Language: Go

Build & Run

go build ./...
go test ./...

Structure

zap/
  LICENSE
  README.md
  bench/
  benchmark_test.go
  builder.go
  consensus_test.go
  evm.go
  examples/
  go.mod
  go.sum
  grpc_comparison_test.go
  local_consensus_test.go
  mcp/
  mcp_bench_test.go
  memory_bench_test.go

Key Files

  • README.md -- Project documentation
  • go.mod -- Go module definition

PQ-TLS Support

Set NodeConfig.TLS to a *tls.Config to wrap all TCP connections (listener, getOrConnect, ConnectDirect) with TLS. Supports PQ-TLS 1.3 when the Go runtime provides post-quantum key exchange (X25519Kyber768). When TLS is nil (the default), connections are plaintext -- fully backward compatible.

QUIC transport (TLS 1.3 + X25519MLKEM768 by default)

github.com/luxfi/zap/quic provides a QUIC transport with TLS 1.3 and the IANA-registered hybrid post-quantum key exchange X25519MLKEM768 (NamedGroup 0x11ec) preferred by default. Opt in with:

import _ "github.com/luxfi/zap/quic" // registers the QUIC factory

n := zap.NewNode(zap.NodeConfig{
    NodeID:    "node-a",
    Port:      9999,
    TLS:       tlsCfg,            // server Certificates required
    Transport: zap.TransportQUIC, // default stays TransportTCP
})

The QUIC transport adds, beyond what TCP+TLS gives:

  • Multiplexed bidi/uni streams (one ZAP message exchange per stream).
  • Connection migration on local-IP changes.
  • 0-RTT resumption via TLS 1.3 session tickets (set quic.Config{RejectEarlyData: true} to force 1-RTT for non-idempotent handlers).
  • ALPN allowlist zap/1 only.

See quic/README.md for details, defaults, deployment notes, and the threat-model discussion.

GPU-aware transport/ subpackage (LP-203 zero-copy)

github.com/luxfi/zap/transport registers four implementations behind the same Transport interface:

Name Build tags Wire GPU-resident bytes
default always in-proc / TCP no
uma cgo,linux,cuda or cgo,darwin NIC → managed yes (cudaMallocManaged on linux, MTLBuffer on darwin)
gpudirect cgo,linux,gpudirect,cuda NIC → GPU VRAM yes (DMA-buf MR)
dpdk cgo,linux,dpdk + pkg-config libdpdk NIC poll loop no (CPU hugepage)

transport.Pick("") selects in order gpudirect > dpdk > uma > default. An operator override via ZAP_TRANSPORT={default,uma,gpudirect, dpdk} returns ErrNotAvailable on a host that can't provide it (no silent fallback when the operator was explicit).

Transports that can hand out GPU-resident buffers also implement BufferAllocator:

tr, _ := transport.Pick("uma")
alloc, ok := tr.(transport.BufferAllocator)
if !ok { /* default transport — heap allocate */ }
buf, _ := alloc.AllocBuffer(1 << 20)        // 1 MiB managed slab
copy(buf.Bytes(), payload)                  // CPU writes
gpuKernel.launch(buf.DevicePtr(), 1<<20)    // GPU reads same bytes
buf.Release()                                // back to slab pool

UMA pool is slab-allocated (slabClasses = 256B, 1KiB, 4KiB, 16KiB, 64KiB, 1MiB). Pool budget defaults to 4 GiB via ZAP_UMA_POOL_BYTES. Sizes above 1 MiB take a cold-path cudaMallocManaged per call.

Boot emits one slog.Info line listing the chosen transport + probe errors for the others. Silence with ZAP_TRANSPORT_QUIET=1.

Probe gaps are reported honestly: on a host with libibverbs but no Mellanox HCA visible, Pick("gpudirect") returns missing prereq(s): [ibverbs-device raw-packet-cap nvidia-peermem] — never a fake success.

Read-buffer pool (bufpool.go)

The TCP dispatch read path sources frame buffers from a quantized sync.Pool (64B / 256B / 1 KiB / 4 KiB / 16 KiB / 64 KiB) via readMessageRawPooled + refcounted *bufRef. Message carries an optional refs *bufRef; Message.Release() decrements the refcount and (at zero) returns the slab. Message.Retain() extends lifetime across goroutine boundaries (e.g. when a Call response is delivered via channel).

Wire format is byte-identical — only the buffer lifecycle changed. Frames over 64 KiB fall back to a one-off heap alloc transparent to the caller. Message.Release() on a Builder-built or unpooled message is a no-op so existing callers keep working unmodified.

Read benchmarks (Apple M1 Max, go test -bench='BenchmarkRead' -benchtime=2s -count=2):

Size Legacy ns/op Pooled ns/op Speedup Allocs (legacy → pooled)
64B 38 40 1.0x 2 → 1, 68 → 4 B/op
256B 68 43 1.6x 2 → 1, 260 → 4 B/op
1KB 284 54 5.3x 2 → 1, 1028 → 4 B/op
4KB 716 125 5.7x 2 → 1, 4100 → 4 B/op
16KB 2244 355 6.3x 2 → 1, 16388 → 4 B/op
64KB 4802 1142 4.2x 2 → 1, 65540 → 4 B/op

Per-Call QUIC streams (TransportStreamer)

Node.Call over QUIC opens a fresh per-Call bidirectional stream via TransportStreamer.OpenCallStream instead of serializing on the shared control stream's ctrlMu. The server-side acceptCallStreams goroutine spawns one handler goroutine per accepted stream so concurrent Calls execute in parallel up to QUIC's stream limit (1024 by default).

Wire format on each per-Call stream is one length-prefixed ZAP frame in each direction — no correlation header needed since each stream carries exactly one request + one response.

Per-stream is the only Call path on QUIC; there is no fallback. The control stream carries one-way Sends only.

End-to-end Call throughput (2 ms simulated handler):

Concurrency Per-stream Reference Speedup
1 worker 3.19 ms/op 3.17 ms/op 1.0x
10 workers 1.24 ms/op 3.01 ms/op 2.4x
100 workers 1.06 ms/op 3.01 ms/op 2.85x

Reference = handler latency on a serialized control-stream path (pre-optimization measurement, retained for context). Per-stream overlaps handlers in flight (peak inflight = 32 observed in TestQUICCallConcurrent).

Locality-adaptive Router (P1) — router.go, interface.go

The one-Send-API router that decouples WHO (Destination) from HOW (Interface) from the message (Payload), so a caller never names a port or a transport:

r := zap.NewRouter()
r.Register(zap.NewNodeInterface(node))     // network fallback, Cost 10 (conn_pq wire)
inproc := zap.NewInProcessInterface()      // Cost 0
inproc.Register("hanzo.o11y.traces", handler)
r.Register(inproc)

r.Send(ctx, "hanzo.o11y.traces", zap.Payload{Value: spans, Encode: encodeFn})

The routing rule is a Cost table, not a caller branch. Send picks the cheapest Interface whose CanReach(dst) is true. When the destination lives in THIS binary, InProcessInterface (Cost 0) wins and the handler is called with the LIVE Payload.Value — zero serialization, zero copy, zero socket. Encode (the zero-copy wire form) is invoked ONLY when a network Interface is chosen, and only then. "If in-process skip TCP; else use the wire" falls out for free.

Three "transport"-ish names, kept orthogonal — do not conflate:

  • zap.Transport (int enum TransportTCP/TransportQUIC) — which wire ONE Node speaks. NodeInterface adapts such a Node into the router.
  • zap.Router — this P1 locality-adaptive router over many Interfaces.
  • github.com/luxfi/zap/transport — the GPU-aware buffer subpackage (LP-203).

This is Reticulum's model made post-quantum + zero-copy: Destination is a capability aspect today; the announce-routed PQ-identity destination hash (multi-hop, mDNS/K8s-SRV discovery interfaces, UDP/QUIC/radio interfaces) refines it in later phases WITHOUT moving the Send call site. Node is unchanged; NodeInterface adapts it. First consumer: hanzo/cloud o11y telemetry (cloud's own spans → in-process → ClickHouse sink, never the wire). See router_test.go (7 tests: cost-preference, zero-serialize proof, fallback, no-route, sort, re-register, non-encodable-refused).

DexSession orchestration (dexsession/)

ZAP/Cap'n-Proto orchestration layer for the Lux DEX: capability transport + promise pipelining that makes the native C↔D atomic settlement flow FEEL synchronous, while the value boundary stays 100% native.

THE HARD INVARIANT: a ZAP response is NEVER sufficient to move money. Money moves ONLY when C consumes a real D→C atomic export object, or D consumes a real C→D object. The orchestration layer holds no StateDB/AtomicState/shared-memory and has NO compile edge to precompile/dex or the EVM — it cannot import the C Verify path and Verify cannot import it. It traffics in VALUES ([20]byte accounts, [32]byte ids, uint64 amounts) and bytes only:

  • prepareSwapIntent → 0x9999 CALLDATA the USER signs (no funds reserved, no enforceable amountOut — the floor rides MinAmountOut in the signed tx).
  • notifyIntent → tells D to scan/import the C→D object the signed tx created; cannot make a D match valid without a committed D block; returns a watch.
  • importSettlement → finalization CALLDATA / a tx that POINTS at a DExportRef; the chain re-reads the real D→C object and binds recipient/asset/amount to it.

DExportRef is a POINTER {sourceChainID, sourceTxID, outputIndex, intentID}, NOT a DFillReceipt. "No capability moves money" is provable by EXHAUSTION: the surface has no creditBalance/settleFill/overrideMatch/adminWithdraw method. Capabilities (QuoteCap/IntentCap/WatchCap/SettlementCap/AdminCap) are unforgeable tokens scoped to a bootstrap grant; a session cannot widen its own authority.

The 60-byte atomic object wire, DeriveIntentID, DeriveUTXOID, the V4 swap selector (0xF3CD914C), and the Phase-A/B hookData tags are REPRODUCED as pure values (not imported) and pinned byte-identical to precompile/dex + chains/dexvm by parity_test.go — the "three homes" pattern, keeping the transport module free of the EVM/cgo dep graph.

Promise pipelining (promise.go/pipeline.go): dependent calls dispatch the instant their inputs resolve (thenAsync), so quote→prepare→notify→ onCommitted→import overlaps network latency; SwapFlow/SwapFlowWithSender wire the declarative end-to-end path. The watch streams D's result (poll/push) and resolves to a pointer on commit.

Build/test (pure Go): CGO_ENABLED=0 GOWORK=off go test ./dexsession/. The critical adversarial proof is TestRED_ZAP_ResponseAloneCannotMoveMoney.