Building Vexor¶
Docs-site note (2026-07-11): Part of the Vexor documentation site (Anza-style). This page documents the exact, current way to build the Vexor validator binary, grounded in
build.zigand the deploy pipeline that gates it. There is one build command for a normal deploy; everything else here explains why.
The toolchain — Zig 0.15.2, exactly¶
Vexor is written in Zig 0.15.2. The build does not produce a correct binary on any other version, and this is not a "probably fine" version pin — it is load-bearing:
- Zig 0.15.2 uses the
--release=fast|safe|smallflag form; it rejects the older-Doptimize=…syntax. - The
std.fs.FileReader/Writeroverhaul andArrayListbecoming unmanaged-by-default both landed between 0.14 and 0.15, and Vexor's code depends on the 0.15.2 shape of both. - An older compiler can still link and run a binary — it just silently miscompiles parts of it. That is a worse failure mode than a build error, because nothing tells you it happened.
Check zig version before building. If it doesn't print 0.15.2, install the pinned toolchain rather than
using whatever zig resolves to on $PATH.
The one command¶
Run from the repo root with the pinned Zig 0.15.2. -Dprod is the bundling switch: it turns on all
8 canonical production feature flags at once (the table below). -Dpure_zig is retained on the command
line for compatibility: crypto is unconditionally pure-Zig (see
why below), so the flag is a no-op,
but passing it costs nothing and keeps the build command self-documenting. Every flag in
build.zig defaults to off, so a bare zig build is a deliberately minimal, fast-iterating binary that is
not what you want to run a validator with — -Dprod -Dpure_zig is what turns a checkout into a deployable
production binary. The bundle is golden-replay-gated as behaviorally identical to enabling all 8 flags
explicitly, and each flag remains individually overridable on top of it (e.g. -Dprod -Dpure_zig
-Dfec_dedup=false) for forensic or bisect builds that need a narrower flag surface.
-Dcpu=znver4 matches Vexor's reference box (an EPYC 9374F) — see Hardware for why. If your
hardware is a different CPU, set -Dcpu= to match (Zig's standard target-CPU option, not something
Vexor-specific); getting it wrong doesn't break consensus, but it can silently disable ISA extensions
(AVX-512, sha_ni, vaes) that sig-verify and hashing lean on for throughput.
The resulting binary lands at zig-out/bin/vexor. The deploy tooling additionally self-checks the binary's
compiled-in feature markers before launch, so a mis-built binary fails the deploy rather than silently
shipping — see Deploying Vexor.
What -Dprod bundles¶
-Dprod turns on 8 flags, each of which is individually a -D option in build.zig and defaults to
off there:
| Flag | What it does | Why it matters |
|---|---|---|
leader_mode |
Block-production tick loop | Required to produce blocks as leader (separate from whether broadcast is armed). |
repair_stake_weighting |
Stake-weighted repair-peer selection | Faster, more correct repair versus round-robin peer choice. |
parallel_exec |
Wave-barrier parallel transaction execution | Throughput — parallelizes non-conflicting transactions across a worker pool. |
fec_dedup |
Ed25519 signature dedup across a FEC set | Throughput — avoids re-verifying signatures already checked once. |
watchdog |
In-process liveness-observing thread | Operational — see supervision. |
status_cache |
Cross-block already-processed cache | Needed to safely support transaction-bearing block production. |
use_native_quic_votes |
Route votes over QUIC instead of UDP | Networking — reduces reliance on the UDP+relay vote path. |
Unconditional behaviors. Vote execution and BN254/Poseidon leaf crypto are not gated by any build flag: Vexor's own vote program (marker:
voteforge) is the sole, unconditional vote executor, and crypto is unconditionally pure-Zig — see below.
-Dvex_ledger (included in -Dprod) adds the persistent blockstore on top of this set:
| Flag | What it does | Why it matters |
|---|---|---|
vex_ledger |
Vexor's own Zig-native blockstore | Enables the VexLedger storage path. |
Two other feature flags default on in build.zig regardless of which script you use and don't need to be
passed: jemalloc (the allocator) and two_tier (the accounts-DB read/commit design). Everything outside
these lists is either a default-on baseline or an unvetted, experimental feature gated behind its own flag —
The -Dprod option's doc comment in build.zig enumerates what it deliberately still omits and why. Turning one of
those on outside of a deliberate test is not part of a normal build.
A handful of the bundled flags are also gated behind a runtime environment variable at deploy time — building the capability in and arming it at runtime are two separate switches, so a production binary with no env set behaves identically to a binary built without that flag at all. See the Environment Variable Reference for which ones.
The crypto backend: Firedancer Ballet vs. pure-Zig¶
Crypto is unconditionally pure-Zig — Vexor's own from-scratch implementation of the leaf-level crypto
primitives (Ed25519, BLAKE3, BN254/alt_bn128, Poseidon) under src/vex_crypto/ — the binary links zero
Firedancer symbols at runtime, always. There is no build configuration that links Firedancer crypto code;
ballet_* are not valid -D flags on any current checkout. An alternate backend, over Firedancer's ballet
AVX-512 C libraries, exists as a reference configuration, but production never ships it.
-Dpure_zig is kept as an accepted flag on the build command for compatibility and self-documentation, but
it is a no-op: zig build -Dprod alone produces the same pure-Zig binary as -Dprod -Dpure_zig. The
pure-Zig crypto is verified byte-exact against known-answer test vectors (including published solana-bn254 /
EIP-197 / go-ethereum vectors for BN254) and against live cluster parity. See
NOTICE and PROVENANCE for the attribution story and
The Vote Program for how the crypto implementation fits into Vexor's broader
Firedancer-independence.
Every binary still needs one runtime guard: VEX_ALLOW_NO_BN254=1. The BN254 boot guard is designed around
a linked Ballet BN254 library and refuses to boot without either that library or this explicit flag; since no
production build links that library, this environment variable is required on every deploy, unconditionally.
See the Environment Variable Reference for details.
Why --release=fast¶
Zig's optimize modes trade off speed, size, and runtime safety checks. ReleaseFast is the canonical production mode, for performance. A Debug build is roughly 30× slower — a validator in Debug mode falls behind the cluster tip and cannot keep up with replay, signature verification, or PoH hashing in real time.
The --release= value on the command line does not choose the mode
build.zig pins a preferred optimize mode, and when that is set Zig returns the preferred mode for
any --release= value — fast, safe, and small are all equivalent. What is not equivalent
is omitting --release entirely, which yields a Debug build.
To actually change the optimize mode, edit preferred_optimize_mode in build.zig. Changing the
command line will not do it.
Verifying the optimize mode¶
Check the artifact, never the flag. ReleaseFast strips Zig's safety-check panic strings, so:
strings <binary> | grep -cE "reached unreachable code|index out of bounds"
# 0 → ReleaseFast ✅
# >0 → ReleaseSafe or Debug
The deploy pipeline enforces exactly this check and refuses to launch a binary that fails it.
Binary size cannot detect a bad build
Earlier guidance here said a correct binary is 15–17 MB and that anything over 50 MB is an accidental Debug build. Both halves are wrong. Measured on the current tree, a Debug binary is 32.7 MB — under the 50 MB threshold, so it passes that check — and it is smaller than the 36.8 MB ReleaseFast binary. A size threshold would pass the Debug build and flag the correct one. Use the safety-string check above instead.
Verifying the flags landed¶
Because dropping a single flag produces a binary that runs and votes but silently diverges from the cluster under specific conditions, don't just trust that the build command was typed correctly — verify it landed. The deploy tooling already does this for you: it runs a self-check after every build that greps the output binary for each flag's compiled-in marker string and fails the build if any is missing, rather than letting a silently-incomplete binary reach the deploy step. The deploy pipeline then runs its own, narrower version of the same check before it will launch a binary (see Consensus guards). You can run the same kind of check manually at any time:
strings -n6 zig-out/bin/vexor | grep -c voteforge # Vexor vote program present?
strings -n6 zig-out/bin/vexor | grep -c fd_bn254 # expect 0 — no Firedancer BN254 linked, ever
A nonzero voteforge count confirms the vote program is in the binary you're about to run — it is the only
vote-execution path there is now, so this should always be nonzero. The fd_bn254 count should always read
zero: every current build is crypto-unconditionally pure-Zig, so there is no configuration that links
Firedancer's Ballet BN254 library anymore.
Test targets¶
Beyond the production build, zig build exposes named test steps that exercise specific subsystems in
isolation. The ones relevant to the replay path added most recently:
zig build test-replay-stage # replay-stage unit coverage
zig build test-replay-fuzz-gate # malformed-input fuzz gate for replay framing + signature verification
zig build test-replay-fuzz-gate-framing # framing half only
zig build test-replay-fuzz-gate-sigverify # signature-verification half only
zig build test-slot-params # SIMD-0525 slot-parameter KATs
test-slot-params is wired into the test-migrated aggregate, so it already runs as part of standard
CI. test-replay-stage and test-replay-fuzz-gate (and its two halves) are not wired into any
aggregate yet — a deliberate choice to keep a brand-new test surface visible on its own before folding
it into the aggregate run. Run them explicitly if you're touching the replay or signature-verification
path; don't assume the standard CI run covers them yet.
Build while the node is stopped¶
Building a release binary is CPU-intensive across every core on the box. If a validator is currently voting on the same machine, build after stopping it, not concurrently — this removes any question of the build contending with the consensus-critical replay/vote path for CPU or I/O. See the deploy contract for the stop → build → deploy sequence.
Quick reference¶
# Confirm the toolchain
zig version # must print 0.15.2
# Build the production binary (set -Dcpu= to your target CPU)
zig build -Dprod -Dpure_zig -Dcpu=znver4 --release=fast # the bundle is golden-gated; deploy self-checks the markers
# Sanity-check the output
strings zig-out/bin/vexor | grep -cE "reached unreachable code|index out of bounds" # 0 = ReleaseFast
# Do NOT sanity-check by size: a Debug binary here is smaller than a ReleaseFast one.
strings -n6 zig-out/bin/vexor | grep -c voteforge # Vexor vote program compiled in (expect >0)
strings -n6 zig-out/bin/vexor | grep -c fd_bn254 # Firedancer BN254 linked (expect 0 for -Dpure_zig)
# Then deploy — see the Deploy page (pure-Zig binaries need VEX_ALLOW_NO_BN254=1 at launch)