Skip to content

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.zig and 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|small flag form; it rejects the older -Doptimize=… syntax.
  • The std.fs.File Reader/Writer overhaul and ArrayList becoming 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

zig build -Dprod -Dpure_zig -Dcpu=znver4 --release=fast

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)