docs: describe the three implementations, and how to build and check each

Lux is one protocol realised in Go, Rust and C++, and the docs described only
the Go one — so a reader had no way to tell which implementation a statement
was about, or that two of them are measured against the third rather than
against the spec.

Three new pages, one concern each:

  /implementations  what exists in each language across the six planes
  /build            native build recipes for all three
  /conformance      the corpus, the pinned oracle, the harness and its gaps

Refreshed where the existing pages were wrong or ambiguous:

  /zap          the version word is 2, not 1; the header rules a decoder must
                enforce, including the refusals, which are part of the protocol
  /zap/family   four layers answer to the name ZAP and two of them share no
                bytes; naming them apart is the fix, not a version field
  /gpu          one C++ implementation with bindings, not three; CUDA is not
                cleared for anything entering a state root
  /nodes        a node with no --http-port listens on 9630, not 9650

Every recipe and every port here was run or read out of the source, and the
one failing conformance check is recorded as failing rather than asserted to
be correct.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-08-29 01:36:13 -07:00
co-authored by Hanzo Dev
parent b2b217bdcb
commit 14aa0fb805
12 changed files with 797 additions and 17 deletions
+9
View File
@@ -7,6 +7,15 @@ description: Multi-layer architecture of Lux Network
Lux Network employs a multi-layer architecture designed for scalability, security, and flexibility.
<Callout>
**One protocol, three implementations.** The design on this page is realised in
Go, Rust and C++. `luxfi/node` (Go) is what runs on every live network and is
the answer the other two are measured against. See
[Implementations](/implementations) for what exists in each language,
[Build from source](/build) for how to build them, and
[Conformance](/conformance) for how they are held to one answer.
</Callout>
## Network Layers
### P-Chain (Platform Chain)
+194
View File
@@ -0,0 +1,194 @@
---
title: Build from source
description: Native build recipes for the Go, Rust and C++ implementations
---
Every recipe on this page is the one the repository itself uses. If you only
want to *run* a node, take a release instead — [Run Nodes](/nodes) starts there.
Build from source when you are changing an implementation or checking one
against [the corpus](/conformance).
<Callout>
Toolchain versions below are the ones these recipes are known to work on, not
minimums discovered by bisection. Older ones may work; nobody has checked.
</Callout>
| Stack | Toolchain | Build | Test |
|---|---|---|---|
| Go | Go 1.26 | `./scripts/build.sh` | `go test ./...` |
| Rust | rustc 1.97, cargo 1.97 | `cargo build --release` | `cargo test` |
| C++ | CMake 3.28, Ninja 1.11, GCC 13 or Clang 18 | `cmake --build build` | `ctest --test-dir build` |
## Go — `luxd`
```bash
git clone https://github.com/luxfi/node
cd node
./scripts/build.sh
./build/luxd --version
```
The build takes its Go experiment flags and its FIPS settings from
`scripts/constants.sh`, and the `Makefile` sources the same file. That is
deliberate: `make` and the scripts cannot compile different things, and a flag
combination that is known to break gets refused in one place rather than
producing a binary that fails at startup.
`make build` is the same build. Profiles trade size for symbols:
| Profile | Command | Result |
|---|---|---|
| minimal | `./scripts/build.sh` | ZAP transport, all VMs, stripped |
| core | `./scripts/build.sh -p core` | P/X/C only, stripped, plugin-ready |
| dev | `./scripts/build.sh -p dev` | all VMs with debug symbols |
| tiny | `./scripts/build.sh -p tiny` | all VMs, compressed |
| race | `./scripts/build.sh -r` | minimal with the race detector |
**cgo matters here.** `CGO_ENABLED=1` is the default and selects the assembly
BLS implementation; `CGO_ENABLED=0` selects a pure-Go one. They are two
different implementations of a consensus-critical primitive, so a suite run
with one flag has not exercised the other. The race detector also needs cgo, so
`CGO_ENABLED=0` silently disables `-race` as well.
### Consensus and the EVM on their own
```bash
git clone https://github.com/luxfi/consensus && cd consensus
go build ./... && go test ./protocol/...
```
```bash
git clone https://github.com/luxfi/evmgpu && cd evmgpu
CGO_ENABLED=0 go build ./... # pure Go
go build ./... # needs luxgpu on PKG_CONFIG_PATH
```
`evmgpu` links the C++ GPU stack through pkg-config. Without `luxgpu.pc` on
`PKG_CONFIG_PATH` the cgo build fails at configure time; build with
`CGO_ENABLED=0` or [build `luxcpp/gpu` first](#the-gpu-stack).
### Verifying the node you built
A node serves everything under `/v1/`. There is no `/ext/` and no `/api/`. The
default ports are **9630** for HTTP and **9631** for staking.
```bash
./build/luxd &
curl -s -X POST -H 'content-type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"info.getNodeVersion","params":{}}' \
http://127.0.0.1:9630/v1/info
curl -s http://127.0.0.1:9630/v1/health
curl -s -X POST -H 'content-type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}' \
http://127.0.0.1:9630/v1/bc/C/rpc
```
`info.getNodeVersion` returns the version **and the git commit** the binary was
built from. That commit is what pins a corpus, so record it whenever you capture
one.
To serve the same handler chain over the ZAP transport instead of HTTP/1.1, set
a bind address — it is off unless you ask for it:
```bash
ZAP_RPC_LISTEN=:9651 ./build/luxd
```
## Rust — `hanzod` and the crates
```bash
cargo build --release --bin hanzod
./target/release/hanzod --version
```
The consensus crate builds and tests standalone, and is the crate the harness
puts questions to:
```bash
cd consensus/pkg/rust # the lux-consensus crate lives inside luxfi/consensus
cargo test
```
<Callout type="warn">
`hanzod`'s consensus crate currently depends on `lux-consensus` by a **relative
path** into a sibling checkout of `luxfi/consensus`, not by a published version.
A clean checkout of `hanzo/node` on its own will not resolve it. Until that is a
version dependency, clone both repositories side by side.
</Callout>
Building is not participating. `hanzod` starts and runs the Quasar engine; it
has no block source and no vote transport, so it does not yet take part in a
network. Treat a successful build as a successful build.
## C++ — the daemon and the finality gate
Both C++ repositories are out-of-source CMake builds and both find their
dependencies through `LUXCPP_ROOT`.
### `consensus2` — the finality gate
```bash
git clone https://github.com/luxfi/consensus2 && cd consensus2
cmake -S . -B build \
-DLUXCPP_ROOT=/path/to/luxcpp \
-DZAP_DIR=/path/to/luxcpp/zap-cpp-core/include \
-DCMAKE_BUILD_TYPE=Release
cmake --build build -j"$(nproc)"
ctest --test-dir build --output-on-failure
```
Sanitizers instrument `consensus2`'s own code and leave the reused assembly
crypto alone: `-DCONSENSUS2_SANITIZE=address,undefined` or `=thread`.
ThreadSanitizer aborts with `unexpected memory mapping` on kernels that hand out
32 bits of mmap randomness — its shadow mapping does not fit. That is the
loader, not the code. Run it with randomization off:
```bash
setarch -R ctest --test-dir build
```
### `node2` — the daemon
```bash
git clone https://github.com/luxfi/node2 && cd node2
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j"$(nproc)"
ctest --test-dir build --output-on-failure
```
`node2` finds `consensus2` and `luxcpp` automatically when they sit beside it;
`-DCONSENSUS2_DIR` and `-DLUXCPP_ROOT` override. Its suite includes the reused
`consensus2` tests, so a green `node2` run covers both.
A real multi-process cluster over loopback TCP, rather than threads in one
process:
```bash
./scripts/cluster.sh build/node2d 19310 5 # five validators
./scripts/cluster.sh build/node2d 19310 5 4 # ...with validator 4 held down
```
### `cevm` — the C++ EVM
```bash
cmake -S . -B build/release -DCMAKE_BUILD_TYPE=Release
cmake --build build/release
ctest --test-dir build/release --output-on-failure
```
### The GPU stack
`luxcpp/gpu` picks its backend at compile time or at runtime, in the order
CUDA → Metal → WebGPU → CPU. `LUX_GPU_BACKEND` overrides the choice. The Go
bindings in `luxfi/gpu` link it through pkg-config; see [GPU](/gpu).
## Cross-checking what you built
Building three implementations proves three compilers agree with themselves. It
proves nothing about whether they agree with each other — that is what the
[conformance harness](/conformance) is for, and it is the gate an
implementation passes before it goes anywhere near a live network.
+153
View File
@@ -0,0 +1,153 @@
---
title: Conformance
description: The corpus that holds three implementations to one answer, and the harness that checks them
---
Three implementations of one protocol will agree on the easy cases by accident
and disagree on the hard ones silently. Conformance is the machinery that makes
the disagreement loud and early instead of quiet and on-chain.
The corpus lives in [`luxfi/conformance`](https://github.com/luxfi/conformance).
It holds canonical outputs captured from a live Go node. Every other
implementation must reproduce them before it is allowed near a network.
## The contract
> For every vector, an implementation is conformant when the same input
> produces output **equal** to the recorded result. Equality is exact: same hex,
> same field values, same canonical JSON.
"Close" is a fork. There is no tolerance because there is nothing for a
tolerance to mean — a block hash that is nearly right identifies a different
block, and a quorum threshold that is off by one accepts a block the rest of the
network refuses.
## What is recorded
Vectors are grouped by plane. Each plane is a place two implementations can
disagree and produce two chains.
| Vector | Plane | What it pins |
|---|---|---|
| `evm_chainid`, `evm_genesis_block` | EVM | The chain id and genesis block as the node reports them |
| `evm_header` | EVM | The exact bytes the genesis hash is taken over |
| `evm_transaction` | EVM | Signing hash, wire bytes, recovered sender and list root per transaction type, plus the intrinsic gas floor and both contract-address rules |
| `fpc_theta` | Consensus | θ per phase, and the α it yields at each committee size |
| `luminance` | Consensus | The weight committees are sampled on, at every step of a trajectory |
| `pchain_validators` | Consensus | The validator set that defines the committee |
| `zap_handshake` | Wire | The first frame two peers exchange, and the frames Go refuses |
| `zap_arena` | Wire | Every scalar, all four list kinds, the null shapes, nested objects through a signed pointer, and seven malformed frames with Go's answer to each |
| `zap_frame` | Wire | The *other* ZAP — the [frame RPC](/zap/family), recorded so the shared name stops being an argument |
| `network_id`, `node_version` | Provenance | What the corpus was captured against |
Two details in that table carry more weight than they look like they do.
**Refusals are recorded, not just acceptances.** `zap_handshake` and `zap_arena`
both record frames Go's decoder *rejects*. An implementation must reject them
too. Accepting a frame the network rejects is not leniency — it is a second
protocol, and the peer that admits the message sees a history nobody else does.
**Floats are recorded as bits.** θ and luminance are stored as IEEE-754 bit
patterns, because a float is not its decimal rendering and two implementations
that print the same sixteen digits can still be two different numbers.
## The oracle is pinned
`node_version.json` records the exact Go build the corpus came from — version
and git commit. Re-capturing against a different build is a deliberate act: bump
the pin, re-run the capture, review the diff.
> A vector that changes without a version bump is a regression in the oracle,
> not a new truth.
The current pin is `luxd/1.36.133` at commit `f81aad95`. A node built from
`luxfi/node` HEAD today reports a higher version, so re-capturing without
bumping would silently move the ground everything else is measured against.
## Reproducing the corpus
```bash
scripts/capture.sh http://127.0.0.1:9630
```
One command, because the corpus is asked for in two ways and both halves must
come from the same build:
- Over `/v1/`, it asks a **running node** what state it is in.
- Through `oracle/`, a Go program linked against `luxfi/consensus`,
`luxfi/geth` and `luxfi/zap`, it asks the **packages** what their functions do.
Vectors are never hand-edited. The corpus is always something Go actually said.
The oracle checks its own work before writing. The genesis header vector is
published only if keccak256 of the recorded bytes equals what `Header.Hash()`
returns *and* equals the hash the node reported — three ways of getting the same
number, or no vector.
## Running the harness
One harness exists today, in Rust. It carries **no vectors of its own** — a
second copy of the truth is a second truth — and it puts the corpus's questions
to the crates that would answer them on a live network: `lux-consensus` for
consensus, `hanzo-zap` for the wire, `alloy` for the EVM.
```bash
cd lux-rs/conformance
LUX_VECTORS=/path/to/luxfi/conformance/vectors cargo test
```
`LUX_VECTORS` defaults to a sibling checkout. **A missing corpus fails; it never
skips.** A suite that passes when it checked nothing is worse than one that
fails, because it will be believed.
### Current result
Twelve checks across four planes. Eleven pass:
| Plane | Checks | Result |
|---|---|---|
| `consensus_fpc` | θ and α match the oracle | pass |
| `consensus_luminance` | the whole trajectory matches | pass |
| `evm_header` | fields read back; hashes to the block hash | pass |
| `evm_transaction` | signed bytes decode; signing hash, id, sender and list root match | pass |
| `zap_handshake` | the frames Go refuses are refused | pass |
| `zap_handshake` | the frame bytes match the oracle | **fail** |
The one failure is a single 16-bit word: Rust writes version `1` where Go writes
`2`, on all six recorded node ids. It is a [known and deliberate
divergence](/zap#the-version-word) — a reader in the fleet has not been widened
yet, and readers are widened before emitters flip. It is left failing rather
than asserted-as-correct, because a test that locks in a divergence will keep
passing after the divergence is fixed.
### Reading a failure
A failure prints the id it disagreed on, what Go said, and what the
implementation said. That is enough to fix it, and deliberately not enough to
argue about — one of the two byte strings is on the network and the other is
not.
## What is not covered
Stated plainly, because a gap that is not written down gets mistaken for a pass.
- **No C++ harness against this corpus.** `consensus2` has its own conformance
test with its own captured vectors, and it passes; it is not pointed at this
corpus. Two corpora agreeing today is luck, not design.
- **No end-to-end consensus sequence.** The corpus covers the functions a
decision is made of, not yet a sequence of accept, reject and reorg decisions.
- **No Rust EVM building a header** from a genesis specification, and no
execution to a state root. Those need a chain with transactions in it, and the
C-Chain sits at block zero.
- **No execution-plane vectors from the C++ EVM.** `cevm`'s opcode state roots
are checked only against `cevm`'s own goldens, so a divergence in gas
accounting is invisible to every other implementation.
## The rule
> A harness that is not green does not ship. A network is not migrated to an
> implementation until its harness is green against the corpus captured from
> that network's own Go node.
Green against a devnet corpus says nothing about mainnet: a corpus records what
one node said, not what the protocol permits.
+27
View File
@@ -171,6 +171,33 @@ without degrading finality or cert size.
| M-Chain | MPC ceremonies (CGGMP21 / FROST) | Bridge custody for external wallets (per LP-134) |
| F-Chain | FHE compute + TFHE bootstrap-key generation | Encrypted EVM, encrypted ML inference (per LP-134) |
## Implementations
Quasar exists in three languages. `luxfi/consensus` (Go) is the one that runs
on every live network, and the other two conform to it rather than to the spec
independently — a spec has room for two readings, a running chain does not.
| Language | Module | State |
|---|---|---|
| Go | `luxfi/consensus` | The network. `protocol/` covers Photon, Wave, Focus, Prism, Horizon, Flare, Ray, Field, Quasar and Pulsar. |
| Rust | `lux-consensus` (in `luxfi/consensus/pkg/rust`) | The FPC threshold and luminance planes are byte-exact to Go, checked against the corpus. Used by `hanzod`. |
| C++ | `luxfi/consensus2` | A leaderless finality gate with real BLS12-381 verification, embedded by `node2d`. Its canonical vote message is byte-identical to Go's. |
Two properties every implementation has to preserve, because getting either
wrong looks like a rounding difference and is not one:
- **α is derived, never assumed.** FPC picks θ per phase and α is `ceil(θ·k)`.
Two implementations that derive θ with hashes that merely resemble each other
will disagree on how many votes accept a round — one finalizes what the other
refuses.
- **The stake floor and the signer floor are one definition.** The verifier
re-derives both from the live validator set rather than reading a
certificate's claim about itself, so a set of votes relabelled to a higher
tier dies on the floor it did not meet.
See [Implementations](/implementations) for the full picture and
[Conformance](/conformance) for how these are checked.
## Further reading
- [Post-Quantum Security](/post-quantum) — Pulsar (TALUS) and Corona (Ringtail) in depth
+35
View File
@@ -6,6 +6,29 @@ icon: Cpu
Go bindings for `luxcpp/gpu`, providing GPU-accelerated array operations for crypto, ZK, and ML workloads. Used by `github.com/luxfi/accel` for high-level blockchain acceleration.
## Where the GPU stack lives
Unlike the other planes, this one is **not** three implementations of one thing.
There is one implementation — C++ — and everything else is a binding to it.
| Language | Role |
|---|---|
| C++ (`luxcpp/gpu`, `luxcpp/accel`) | The kernels and the launchers. The whole stack. |
| Go (`luxfi/gpu`, `luxfi/accel`) | cgo bindings. Passes Go-heap pointers across the boundary. |
| Rust | None. |
Two consequences follow, and both matter more here than for a pure library:
- **The cgo boundary is a consensus surface.** A launcher that reads a length a
peer supplied, or that adopts a stale error from an unrelated caller sharing
its thread, does not crash — it hashes something else. Two nodes on different
hardware then compute different state roots from identical input, which is a
fork rather than an outage.
- **A launcher without a numeric oracle is untested.** Checking that a kernel
file exists and starts with the right magic number is a file test. The test
that counts compares the launcher's output against a CPU computation of the
same thing.
## Backend Support
| Backend | Platform | Dependencies | Priority |
@@ -17,6 +40,14 @@ Go bindings for `luxcpp/gpu`, providing GPU-accelerated array operations for cry
Backend is auto-detected at runtime in priority order. Override with `LUX_GPU_BACKEND` env var (`cuda`, `metal`, `webgpu`, `cpu`).
<Callout type="warn">
**CUDA is not cleared for consensus work.** Its launchers do not yet observe the
same host/device pointer contract as HIP, and the Go binding calls them through a
signature they do not all declare. Use `LUX_GPU_BACKEND=cpu` for anything whose
output enters a state root, until the CUDA launchers are ported and have CPU
oracles of their own.
</Callout>
## Build Modes
| Mode | Command | GPU Support |
@@ -24,6 +55,10 @@ Backend is auto-detected at runtime in priority order. Override with `LUX_GPU_BA
| **CPU only** | `CGO_ENABLED=0 go build ./...` | No (pure Go fallback) |
| **GPU enabled** | `CGO_ENABLED=1 go build ./...` | Yes (requires luxcpp/gpu) |
The GPU build needs `luxgpu.pc` on `PKG_CONFIG_PATH`; without it the cgo build
fails at configure time rather than falling back. See
[Build from source](/build#the-gpu-stack).
## Data Types
| Type | Description |
+145
View File
@@ -0,0 +1,145 @@
---
title: Implementations
description: One protocol, three implementations — Go is the network, Rust and C++ conform to it
---
Lux is one protocol with three implementations. They are not three products.
They are three programs that must produce the same answer to the same question,
and one of them is the answer.
**`luxfi/node` (Go) is the network.** It is what runs on mainnet, testnet and
devnet. When Rust or C++ disagrees with it, Rust or C++ is wrong — not because
Go is better written, but because a chain has exactly one history and Go is
already keeping it. An implementation is correct only insofar as it reproduces,
byte for byte, what the Go node produces.
<Callout>
Nothing here is a roadmap. Each row below is a state you can check today by
building the thing and running the [conformance harness](/conformance) against
the corpus. A claim with no vector behind it is not a claim.
</Callout>
## The three
| | Language | Repository | Binary | What it is |
|---|---|---|---|---|
| **luxd** | Go | `luxfi/node` | `luxd` | The network. Every live chain. The oracle every other row is measured against. |
| **hanzod** | Rust | `hanzo/node` | `hanzod` | The Rust node. Runs the Quasar engine through the `lux-consensus` crate. |
| **node2** | C++ | `luxfi/node2` | `node2d` | The C++ daemon. Embeds `luxfi/consensus2` as its finality gate. |
Supporting crates and libraries, one concern each:
| Plane | Go | Rust | C++ |
|---|---|---|---|
| Consensus | `luxfi/consensus` | `lux-consensus` (`luxfi/consensus/pkg/rust`) | `luxfi/consensus2` |
| EVM | `luxfi/geth`, `luxfi/evm`, `luxfi/evmgpu` | `lux-evm` — `revm` as the machine, `alloy` for the encodings | `cevm` |
| Wire — arena codec | `luxfi/zap` | `hanzo-zap` | — |
| Wire — frame RPC | `luxfi/api/zap` | — | `luxcpp/zap-cpp-core` |
| HTTP over ZAP | `zap-proto/http` | — | — |
| Service framework | `zap-proto/zip` | — | — |
| GPU | `luxfi/gpu` (bindings) | — | `luxcpp/gpu`, `luxcpp/accel` |
## The six planes
A "plane" is a place where two implementations can disagree and produce two
chains. There are six, and each has its own page.
<Cards>
<Card title="Node" href="/nodes">
Bootstrapping, P2P, the RPC surface, the VM registry. Every path is under `/v1/`.
</Card>
<Card title="Consensus" href="/consensus">
Quasar — Photon, Wave, Focus, and the compact-certificate envelope.
</Card>
<Card title="EVM" href="/chains/cchain">
The C-Chain. Header preimage, transaction signing, state root, gas.
</Card>
<Card title="ZAP" href="/zap">
The arena codec. Sixteen-byte header, zero-copy field access, pinned to Go's bytes.
</Card>
<Card title="zip and the ZAP family" href="/zap/family">
Four layers answer to one name. `zip` declares an operation once and projects it onto every transport.
</Card>
<Card title="GPU" href="/gpu">
Kernels and launchers. Only C++ has a full stack.
</Card>
</Cards>
## Where each stack stands
Building and passing your own tests is the easy half. What the tables below
record is the harder half — whether the thing agrees with Go. `—` means it does
not exist in that language and is not meant to.
### Go — `luxfi/*`
| Plane | State |
|---|---|
| node | The oracle. `./scripts/build.sh` on `luxfi/node` produces `luxd`; the local devnet answers `/v1/info` and `/v1/health`. |
| consensus | The oracle. `go test ./protocol/...` green across Photon, Wave, Focus, Prism, Horizon, Flare, Ray, Field, Quasar, Pulsar. |
| EVM | The oracle, with a **split to settle**: `luxfi/geth`'s `Hash19` and `luxfi/evm`'s `HeaderSerializable` encode the same header to different byte lengths. One block, one hash — one of the two shapes has to go, and until it does no other language can conform to "the" preimage. |
| ZAP | The oracle. `luxfi/zap` emits version word `2` and accepts `1` and `2`. |
| zip | `zap-proto/zip`. Go only — it is the service framework, not a wire format. |
| GPU | `luxfi/gpu` is a cgo binding over the C++ stack, not an independent implementation. |
### Rust — `hanzo/node`, `lux-rs/*`
| Plane | State |
|---|---|
| node | `hanzod` builds and runs. It is **not yet a network participant**: the Quasar engine is wired, the block source and vote transport are not. Do not read "runs" as "validates". |
| consensus | `lux-consensus` builds, tests green, and the FPC threshold and luminance planes are byte-exact to Go. The aggregate-certificate path is being **removed**, not fixed: Go's `QuorumCert` carries no aggregate field, so there is nothing for an aggregate verifier to conform to. |
| EVM | Genesis hash, the signing hash for every transaction type, sender recovery and the transaction-list root are byte-exact to Go. Header *sealing* is not: the header type in use cannot hold `blockGasCost` or the `extData` fields, and Lux sets `blockGasCost` on every produced header. |
| ZAP | `hanzo-zap` reads and writes the arena codec and refuses the frames Go refuses. It emits version word `1` where Go emits `2` — see [the ordering rule](/zap#the-version-word). |
| zip | — |
| GPU | — |
### C++ — `luxfi/node2`, `luxfi/consensus2`, `cevm`, `luxcpp/*`
| Plane | State |
|---|---|
| node | `node2d` builds; 17 of 17 tests pass; a five-process cluster finalizes over real loopback TCP. |
| consensus | `consensus2` builds; 11 of 11 tests pass, including its conformance test against the Go corpus. |
| EVM | `cevm`'s trie and interpreter match `geth` across every opcode vector in its suite. The shipped state transition does not read the EVM's refund output, so a storage clear costs more gas than `geth` charges — the correct transition already exists in the repository and has to be promoted over the shipped one. |
| ZAP | `luxcpp/zap-cpp-core` implements the **frame RPC**, and its constants match Go's exactly. It does not implement the arena codec, and is not supposed to. |
| zip | — |
| GPU | The only full GPU stack, so there is nothing to conform *to* — correctness is against a CPU computation of the same thing. Where a launcher has such an oracle, HIP matches it; many launchers do not have one yet, and CUDA is [not cleared](/gpu) for anything entering a state root. |
## The migration rule
`luxd` → `hanzod` is a real end-state, and it passes through the corpus rather
than around it.
> An implementation does not replace another on a live network until its harness
> is green against the corpus captured from **that network's own Go node**.
Two consequences worth stating plainly:
- **Green somewhere else does not count.** A harness green against a devnet
corpus says nothing about mainnet, because the corpus records what one node
said, not what the protocol permits.
- **A harness that skips is a harness that failed.** The Rust harness fails on a
missing corpus rather than passing on zero checks. A suite that passes when it
checked nothing is worse than one that fails, because it will be believed.
## Duplicates to collapse
Two things doing one job is a defect. These are known and named so they are not
rediscovered:
- **`cevm` has two homes.** Both remotes share a root commit and have since
diverged. One of them stops receiving consensus-critical fixes the moment
someone picks the wrong one.
- **`hanzo-zap` has more than one copy.** They were identical and have drifted —
one is now nearly twice the length of the other. The same wire question has to
be answered twice, and can be answered differently.
- **The GPU ABI header has three copies.** The drifted copy also defines the
version constant its own compile-time check reads, so the check against
exactly this cannot fire.
- **`evmgpu` pins an older module set than `node`** — a different `geth` and a
different `precompile` — which is a state-root divergence waiting for a
precompile to be called.
## See also
- [Build from source](/build) — verified native recipes for all three.
- [Conformance](/conformance) — the corpus, the pinned oracle, the harness.
+11
View File
@@ -14,6 +14,17 @@ description: Post-quantum secure, multi-consensus blockchain infrastructure
| [Nodes](/nodes) | Validators, full nodes, archive nodes |
| [Staking](/staking) | Delegation and liquid staking |
## Implementations
One protocol in three languages. Go is what runs on every live network; Rust and
C++ conform to it.
| Name | Description |
|------|-------------|
| [Implementations](/implementations) | Go, Rust and C++ across the six planes |
| [Build from source](/build) | Native build recipes for all three |
| [Conformance](/conformance) | The corpus, the pinned oracle, the harness |
## Chains
| Name | Description |
+4
View File
@@ -7,6 +7,10 @@
"consensus",
"post-quantum",
"tokenomics",
"---Implementations---",
"implementations",
"build",
"conformance",
"---Nodes---",
"nodes",
"staking",
+11 -4
View File
@@ -66,8 +66,15 @@ lux validator add \
## Monitoring
Access the node metrics and monitoring:
A node with no `--http-port` set listens on **9630**, and stakes on **9631**.
Every path is under `/v1/`.
- **Metrics endpoint**: `http://localhost:9650/v1/metrics`
- **Health check**: `http://localhost:9650/v1/health`
- **Info API**: `http://localhost:9650/v1/info`
- **Metrics endpoint**: `http://localhost:9630/v1/metrics`
- **Health check**: `http://localhost:9630/v1/health`
- **Info API**: `http://localhost:9630/v1/info`
## Building one
The pages above run a node from a release. To build any of the three
implementations from source — Go, Rust or C++ — see
[Build from source](/build).
+116
View File
@@ -0,0 +1,116 @@
---
title: The ZAP family
description: Four layers answer to the name ZAP — the arena codec, the frame RPC, HTTP over ZAP, and zip
---
Four different things in this stack answer to "ZAP". They are layers, not
versions: none of them is an upgrade of another, and two of them cannot read a
single byte the other writes. Saying "the ZAP format" identifies nothing.
This page names them apart. Everywhere else in these docs, the unqualified word
**ZAP** means the arena codec — layer 1.
| Layer | Module | Header | What it does |
|---|---|---|---|
| 1 · **arena codec** | `luxfi/zap` | 16 bytes, little-endian, magic `ZAP\0` | Turns a struct into bytes you can read fields out of without parsing |
| 2 · **frame RPC** | `luxfi/api/zap` · `luxcpp/zap-cpp-core` | 5 bytes, big-endian, + a 4-byte request id | Multiplexes request/response over one connection |
| 3 · **HTTP over ZAP** | `zap-proto/http` | layer 2 | `net/http` semantics on the ZAP transport |
| 4 · **zip** | `zap-proto/zip` | layer 3 | Declares an operation once and projects it onto every interface |
A fifth framing, unrelated to all of these, carries the post-quantum peer
handshake: a bare 4-byte big-endian length prefix in `luxfi/node`'s
`network/peer`, capped at 16 KiB. It has no type byte, no magic and no request
id. It is not part of this family and only looks like layer 2 from a distance.
## Layer 1 — the arena codec
Zero-copy: a field read is a pointer offset, not a parse. Every multi-byte
integer is little-endian and offsets are relative to the offset field itself.
Full detail, including the header rules a decoder must enforce, is on the
[ZAP page](/zap).
Implementations: Go (`luxfi/zap`), Rust (`hanzo-zap`), Python, TypeScript.
**There is no C++ arena codec.** A C++ service speaks layer 2.
## Layer 2 — the frame RPC
A different protocol with the same name, and the one C++ implements.
```
[ 4-byte BE length ][ 1-byte type ][ 4-byte BE request id ][ body ]
```
The type byte carries two flags in its high bits, so a service id has to fit in
the low six:
| Field | Value |
|---|---|
| Header size | 5 bytes |
| Request-id size | 4 bytes |
| Maximum message | 16 MiB |
| Response flag | `0x80` |
| Error flag | `0x40` |
| Type mask | `0x3F` |
A service id at or above `0x40` sets a flag bit, and both ends will read it as
a flag and dispatch to the wrong handler. Ids stay in `1..63`.
Payload scalars here are **big-endian** — the opposite of layer 1 — and bytes
and strings carry a `u32` length prefix. This is the clearest reason the two
layers cannot be treated as one protocol with a version field.
Implementations: Go (`luxfi/api/zap`), C++ (`luxcpp/zap-cpp-core`). The C++
constants — header size, request-id size, message cap, both flags, the mask —
match Go's exactly. **There is no Rust frame RPC.**
## Layer 3 — HTTP over ZAP
`zap-proto/http` gives `net/http` request/response semantics over the ZAP
transport, so an existing handler and an existing client work unchanged while
the wire underneath changes. Confidentiality comes from an X-Wing hybrid key
exchange (X25519 + ML-KEM-768) at the transport layer rather than from a bearer
token at the application layer.
`luxd` serves its entire `/v1/*` handler chain — the same CORS, host filter and
router as the HTTP listener — over this layer, so a gateway can reach a node
over native ZAP instead of HTTP/1.1. It is opt-in and additive: the listener
starts only when `ZAP_RPC_LISTEN` names a bind address, and a failure to start
it is a warning, never fatal. A node upgrade never silently opens a port.
```bash
ZAP_RPC_LISTEN=:9651 ./build/luxd
```
## Layer 4 — zip
`zap-proto/zip` is the ZAP-native Go service framework, built on Fiber v3 and
fasthttp. It is **not a serialization format** — that is layer 1, and the
confusion is the reason this page exists.
The idea is one declaration and no second copy of it. A route is declared once
as a typed operation, with its input and output types as the contract, and zip
projects that one declaration into:
- a REST route,
- an OpenAPI description,
- an MCP tool,
- a CLI verb,
- a by-name call plane,
with ZAP as the primary transport and HTTP as a secondary view of the same
operations. Nothing in that list is written twice, so none of them can drift
from the others.
`luxd` does not use zip today; it serves `/v1/` through its own router over
layer 3. zip is where Lux services that are not the node itself declare their
surface.
## Why the names have to be fixed
The corpus records both layer 1 and layer 2 side by side, not because an
implementation needs both, but so the name stops being an argument in review. A
sentence like "our ZAP decoder rejects that frame" is true of one layer and
false of the other, and there is no way to tell which from the sentence.
One name per format. Until the modules are renamed, qualify it: *arena codec*
or *frame RPC*, never bare "ZAP" in a place where it could be either.
+91 -13
View File
@@ -11,6 +11,13 @@ ZAP (**Z**ero-copy **A**pplication **P**rotocol) is a high-performance binary pr
import "github.com/luxfi/zap"
```
<Callout>
This page is the **arena codec** — the 16-byte-header format `luxfi/zap`
encodes. Three other layers also answer to the name ZAP, including one whose
header is five big-endian bytes and shares nothing with this one. See
[the ZAP family](/zap/family) before assuming which is meant.
</Callout>
## Performance
| Metric | MCP JSON-RPC | ZAP | Improvement |
@@ -20,21 +27,72 @@ import "github.com/luxfi/zap"
| Allocations | 58/op | 2/op | 29x fewer |
| Parse time | ~1,000 ns | 2.9 ns | 345x faster |
## Wire Format
## Wire format
Every ZAP message has a 16-byte header followed by a variable-length data segment:
Every message is a 16-byte header followed by a variable-length data segment.
All multi-byte integers are little-endian, and an offset is relative to the
position of the offset field itself. Data segments are 8-byte aligned.
```
Header (16 bytes)
Magic: "ZAP\x00" (4 bytes)
Version: 1 (2 bytes)
Flags: compression, etc. (2 bytes)
Root Offset: offset to root object (4 bytes)
Size: total message size (4 bytes)
Data Segment (variable)
Magic "ZAP\x00" bytes 0..4
Version 2 (see below) bytes 4..6
Flags compressed / encrypted / signed bytes 6..8
Root offset offset to the root object bytes 8..12
Size total message size bytes 12..16
Data segment (variable)
Structs, lists, text, bytes...
```
### The version word
Two schemas are defined, and the number is a **schema** discriminator, not a
release number:
| Value | Schema |
|---|---|
| `1` | Legacy platform-chain schema — `NetworkID` at byte 0, no transaction-kind discriminator |
| `2` | Current schema — transaction kind at byte 0, every other field shifted by one |
Go's builder **emits `2`** and its reader **accepts `1` and `2`**. The overlap
is not free: a version-1-shaped transaction with `NetworkID = 11` has `0x0B` at
byte 0, which is also a valid transaction-kind value, so a version-1 header over
version-2 bytes would pass a discriminator check and then misread the rest of
the buffer. Wrapping code requires version 2.
A reader is widened before an emitter is flipped, never the other way round:
every reader in the fleet must accept the new value before the first writer
produces it. Rust (`hanzo-zap`) emits `1` today for exactly this reason — one
reader has not been widened yet — and its handshake bytes therefore differ from
Go's in one 16-bit word. That difference is [recorded in the corpus](/conformance)
as a known, deliberate divergence rather than hidden behind a passing test.
### What a decoder must refuse
Refusal is part of the protocol. Accepting a frame the network rejects is not
leniency, it is a second protocol — one peer admits a message the rest of the
network will not, and the two see different histories.
Go's reader enforces three rules and a conformant decoder enforces all three:
1. The magic is exactly `ZAP\0`.
2. The version is `1` or `2`.
3. The declared size at bytes 12..16 is **at least** the 16-byte header and **at
most** the number of bytes actually present. The message is then truncated to
that declared size.
Rule 3 is the one that gets left out, because a decoder that checks only magic
and version still reads every well-formed message correctly, and every test
written from the happy path passes. What it also does is read past the end of a
short frame and return values Go never saw. That is a **state** divergence, not
a transport one: these are the bytes transactions and predicate results are
decoded from, so the two nodes do not fail to talk — they agree on different
data.
The corpus records Go's verdict for each malformed frame, so a decoder can be
held to all three rules rather than to the two that are easy to remember. If
you are writing one, that is the check to run first.
## Supported Types
| Type | Size | Description |
@@ -68,10 +126,30 @@ node := zap.NewNode(zap.NodeConfig{
Zero-copy semantics are preserved under TLS. The TLS layer operates on the same buffer without additional heap allocations.
## Use Cases
## Port
- **AI Agent Orchestration**: 20-tool orchestrator runs at 150K ops/sec vs 10K with JSON-RPC
- **VM-to-VM Communication**: Cross-VM message routing on the Lux network
- **GPU Cluster / FHE Kernels**: Zero-copy ciphertext transfer between compute nodes
`9999` is ZAP the way `80` is HTTP and `443` is HTTPS. Every ZAP-hosting service
binds it; the DNS name says which service answers.
## Use cases
- **AI agent orchestration**: 20-tool orchestrator runs at 150K ops/sec vs 10K with JSON-RPC
- **VM-to-VM communication**: cross-VM message routing on the Lux network
- **GPU cluster / FHE kernels**: zero-copy ciphertext transfer between compute nodes
- **Consensus**: 5-node consensus reaching agreement in ~450 microseconds
- **MCP Bridge**: Auto-discover and accelerate existing MCP servers with ZAP transport
- **MCP bridge**: auto-discover and accelerate existing MCP servers with ZAP transport
## Implementations
| Language | Module | Notes |
|---|---|---|
| Go | `luxfi/zap` | The oracle. Emits version `2`, accepts `1` and `2`. |
| Rust | `hanzo-zap` | Used by `hanzod` and by the conformance harness. Emits version `1` — see [the version word](#the-version-word). |
| Python | `hanzo-zap` | Reader and writer. |
| TypeScript | `@zap-proto/zap` | Reader and writer. |
| C++ | — | None. A C++ service speaks the [frame RPC](/zap/family) instead. |
A decoder is conformant when it agrees with [the corpus](/conformance) — which
records the bytes Go writes **and** the malformed frames Go refuses — not when
it round-trips its own output. Round-tripping your own output only proves the
encoder and decoder were written by the same person.
+1
View File
@@ -1,6 +1,7 @@
{
"pages": [
"index",
"family",
"api",
"---Package reference---",
"reference"