Runtime & SVM — How Vexor Executes Transactions¶
Docs-site note (2026-06-25): Part of the planned Vexor documentation site, modeled on the structure and feel of docs.anza.xyz / anza.xyz. Written now so the runtime — the heart of any validator — is documented while it is fresh. Style: clean headings, plain language, every claim grounded in source, honest about what is live vs. dormant.
Overview¶
The runtime is the part of a validator that actually runs Solana transactions: it decodes each instruction, executes the target program (a native builtin like the Vote program, or a deployed sBPF program), applies the resulting account changes, and folds them into the slot's bank hash. If the runtime is even one byte off from the rest of the cluster, the node computes a different bank hash, votes a different fork, and falls out of consensus. The runtime is therefore the single most correctness-critical subsystem in the client.
Vexor runs Solana transactions with its own, 100% Zig, clean-room runtime — its own sBPF virtual machine, its own ELF loader and verifier, its own syscall layer (including the heavy cryptography), and its own native programs. It does not embed Agave's Rust runtime or Firedancer's C runtime. The contract it holds itself to is strict: same inputs → same bank hash as the cluster, byte-for-byte. On testnet today the node votes bank-exact and CURRENT with this runtime, which is the empirical proof that the execution path reproduces the canonical result.
The sBPF virtual machine¶
Solana programs are compiled to sBPF — Solana's variant of eBPF bytecode. Executing them correctly means implementing the instruction set, the four sBPF version dialects (v0/v1/v2/v3), the memory model, the call/stack discipline, and the compute-unit metering, all exactly as the canonical VM does.
Vexor has its own sBPF interpreter, written from the specification in Zig. It is not a binding to, or a fork of, anybody else's VM:
- The current production interpreter is
src/vex_bpf/interpreter.zig(the "v1" interpreter — the one that runs real deployed programs on the live node today). - A parallel, clean-room spec-for-spec rebuild lives in
src/vex_bpf2/interpreter.zig. Its module header documents it as a "clean-room mirror ofsolana-sbpf v0.14.4src/interpreter.rs+src/ebpf.rs" — the canonical Anza VM — written against the Rust source line-by-line, with the Zig clientsigused only for idiom guidance, never as a copy source. Vexor's consensus anchor is re-pinned to the newest Agave testnet release as it ships — currently v4.2.0-beta.0 — and the vendored v0.14.4 sBPF snapshot has been checked to carry byte-identical r10/stack/version semantics against that anchor.
The rebuild mirrors the hard, easy-to-get-wrong details verbatim — for example the per-version stack-pointer
math (the V0 frame-pointer advance is stack_frame_size * 2, V3 is * 1), MAX_CALL_DEPTH = 64, and the
exact compute-metering rule (+1 compute unit per instruction, OutOfCompute checked at the top of each step)
— and carries inline tests that prove the math byte-for-byte. The per-sBPF-version feature gates it implements
(SIMD-0166 dynamic stack frames, 0173 encoding, 0174 arithmetic, 0178 static syscalls, 0189 stricter ELF
headers, 0377 JMP32) are documented against their SIMDs in the source header.
Why two interpreters?
vex_bpf2is the next-generation, audited rebuild. Its native/builtin path is already live (see Native programs below), but its general BPF-over-Bank path is not yet wired —src/vex_svm/v2_dispatch.zigexplicitly returnsM5_BankBackedBpfNotPlumbedand routes non-builtin programs back to the proven v1 interpreter. So today the v1 interpreter is the production executor for ordinary deployed programs; v2 is taking over surface by surface. Both are Vexor's own code.
Honest framing: interpreter ≠ JIT, and that is parity, not a gap¶
It is sometimes claimed that "Vexor has no JIT" is a shortcoming. It is not. Firedancer also interprets sBPF — it does not ship a JIT compiler either. The JIT is an Agave-only feature. An interpreter is the correct, canonical, and Firedancer-equivalent way to execute sBPF; calling its absence a "Vexor gap" or a "Firedancer gap" is simply wrong. Vexor's interpreter is at Firedancer parity on this axis by design. (A computed-goto interpreter variant was prototyped and measured to give no win — a deliberate negative result, not an open item.)
ELF loader and verifier¶
Before a program can run, its on-chain ELF must be parsed, validated, and its bytecode statically verified. Vexor implements both halves itself:
- Loader (
src/vex_bpf2/elf.zig,loader.zig): parses the sBPF ELF, resolves the v0/v1/v2/v3 layout differences, lays out the program's memory regions, and builds the function registry the VM dispatches through. - Verifier (
src/vex_bpf2/verifier.zig): a "spec-for-spec rebuild" ofsolana-sbpf v0.14.4'sverifier.rs, mapped function-for-function (check_prog_len,check_jmp_offset,check_registers,check_callx_register, and theRequisiteVerifier::verifyentry point). It rejects malformed bytecode — bad opcodes, out-of-range jumps, illegal registers, misaligned immediates — before a single instruction executes, exactly where the canonical VM rejects it. Every divergence from the Rust source (e.g. a shorter error-enum taxonomy that re-routes to the closest canonical variant) is documented and justified inline so the behavior stays observably identical.
Syscalls¶
Programs reach the runtime through syscalls — logging, memory ops, hashing, sysvar access, cross-program
invocation, and cryptography. Vexor registers the canonical syscall set (mirroring Agave's
create_program_runtime_environment_v1 registration block) and implements each handler in Zig. The common ones
(sol_log*, sol_memcpy/memmove/memset/memcmp, sol_sha256/keccak256/blake3/sha512,
sol_secp256k1_recover, the sysvar getters and the generic sol_get_sysvar, return-data, stack-height,
PDA derivation) map onto Zig's standard crypto and Vexor's own helpers.
The interesting work is the heavy cryptographic syscalls, which Vexor has ported to byte-faithful Zig.
These run entirely, and unconditionally, on Vexor's own src/vex_crypto/ — a from-scratch Zig
reimplementation, KAT-verified byte-exact and carrying no Firedancer code at runtime. An earlier backend, over
Firedancer's ballet leaf-crypto (AVX-512 C primitives, byte-for-byte equal to Agave), has been removed
from the tree outright (2026-07-12) — there is no build configuration left that links it:
| Syscall surface | Implementation | Status |
|---|---|---|
sol_alt_bn128_group_op (BN254 G1/G2 add, mul, pairing) |
src/vex_crypto/bn254.zig — pure-Zig, unconditional |
Ported + active in every build |
sol_alt_bn128_compression (G1/G2 compress/decompress) |
same BN254 backend | Ported + active in every build |
sol_poseidon (BN254 Poseidon hash) |
same BN254 backend, endian-corrected | Ported + active in every build |
ZK ElGamal proof program (13 Verify* proofs) |
byte-faithful Zig port under src/vex_bpf2/zksdk/ (ed25519 MSM, Merlin transcript, sigma + range proofs) |
Ported + byte-faithful, validated vs. real Agave 5.0.1 prover vectors; deployed and confirmed bank-exact on the live node |
sol_big_mod_exp, curve25519/Ristretto ops |
Vexor crypto modules | Ported |
Two grounding notes, so this stays honest:
- The alt_bn128 / poseidon handlers are unconditionally wired to the pure-Zig backend — there is no longer
a build flag that could leave them
unported. The deploy pipeline still self-checks for the backend's presence (see Consensus guards), since a binary missing it entirely would indicate a broken build, not a deliberate configuration choice. - The ZK-ElGamal proof program replaced a permanent silent stub with a real, byte-faithful verifier
(closing the SIMD-0153 confidential-transfer exposure). The real verifier (
executeReal) is exercised by the e2e tests (12/12 real Agave proofs verify, 12/12 corrupt proofs reject) and is enabled on the live path via a monotonic re-enable. It was deployed and confirmed bank-exact on the live node.
Compute-unit metering. Alongside the crypto rewrite, the syscall and CPI cost-accounting surface has been audited and fixed for byte-parity against the Agave 4.2 cost model — including per-CPI invoke-unit charges, hash-syscall byte costs, heap cost, loader entry costs, and the loaded-accounts-data-size gate (SIMD-0186, 64 MiB). This matters because compute-unit metering is itself consensus-relevant: an under- or over-charged transaction can execute differently from the cluster's accounting of it. The audit and fixes are verified byte-parity against the anchored Agave 4.2 reference; see Accomplished for status.
A historical artifact worth flagging for anyone reading the source: the docstring header of
src/vex_bpf2/syscalls.zigstill lists the crypto syscalls as…NotImplementedplaceholders. That comment is stale — the handler bodies below it callbn254.g1Add,bn254.pairing,poseidon(…), etc. The code is ported; only the header comment lags.
Native programs (builtins)¶
Some programs are not sBPF at all — they are native builtins baked into the runtime: System, Vote, Stake,
Config, Compute Budget, Address Lookup Table, BPF Loader, and the ZK-ElGamal proof program. Vexor implements
each in Zig under src/vex_svm/native/ (v1) and src/vex_bpf2/builtins/ (v2 rebuild). These are the programs
that move lamports and delegate stake — the consensus-critical core — so most of them are held to exact
byte-fidelity against Vexor's current Agave anchor (v4.2.0-beta.0) and covered by golden-vector KATs (e.g.
nonce, SIMD-0291).
The Vote program is the one deliberate exception, and it's worth stating precisely. It is not a byte-fidelity port of Agave's implementation — it is a Vexor-authored rewrite (landed July 2026, built to Agave 4.2 vote semantics) and is the sole, unconditional live vote executor on the node today — no build flag selects it, and no alternate path exists to select instead. During development, a Sig-derived vote component ran alongside it as the differential-test oracle — never on the live vote path — to prove the rewrite byte-identical before it took over; that oracle component has been fully removed from the tree (Stage 8, 2026-07-12, −31,181 lines). Across 990k+ live vote instructions compared against the oracle, results matched byte-for-byte with zero mismatches, and the rewrite executes a vote instruction in ~1.9–2.0 µs versus ~8.9 µs for the retired transplant — 4.7× faster.
The v2 dispatch path: v2DispatchInternal → commitV2Mutations¶
Native execution on the new path flows through two functions in replay_stage.zig / v2_dispatch.zig:
v2DispatchInternalbuilds anInvokeContextover the real(Bank, AccountsDb)substrate, runs the v2 builtin (vex_bpf2.builtins.dispatch), and returns the account deltas as a uniform[]AccountMutation.commitV2Mutationsapplies those mutations throughbank.pending_writes, which is the same proven commit path the rest of replay uses, so the changes land in account state and feed the bank hash.
The v2 path is live for builtins today; for the handful of program variants not yet fully ported on v2, the dispatcher falls back — fail-loud, never silent — to the proven v1 native handler (System, Vote, Stake, ALT) through an explicit fallback vtable. This is how Vexor migrates the runtime surface-by-surface without ever risking a silent wrong answer: an un-ported variant routes to known-good v1 code or returns a named error, it never guesses.
Determinism: same inputs → same bank hash¶
The whole runtime exists to satisfy one property: given the same parent bank, the same transactions, and the same feature set, Vexor must produce the same bank hash the rest of the cluster produces. Several design choices serve that directly:
- Serial, deterministic execution. Transactions execute in a fixed order with the canonical compute-metering and cost rules. (Per Agave and Firedancer, every transaction executes; the block cost is an after-the-fact whole-block aggregate, not a per-transaction gate that could skip a mutation — a subtle rule Vexor follows exactly.) A parallel execution path exists, is build-gated, and is validated bank-exact against the serial path before it is ever allowed to affect a vote; it is a throughput option, not a correctness shortcut.
- Feature gates resolved against the live feature set, at the correct activation slot, so behavior flips on the exact boundary the cluster flips it.
- Byte-faithful native + crypto, as above, so vote state, stake state, and confidential-transfer proofs serialize and verify identically.
- The empirical backstop: the deploy gate is offline replay reproducing the cluster's bank hash plus at-tip cluster-attested parity. A change reaches the voting node only after it has been shown to compute the cluster's exact bank hash. Weeks of bank-exact voting are the running proof that the runtime is deterministic against the canonical implementation.
What is live vs. dormant (honest status, 2026-07-11)¶
The runtime is done and canonical for what the live cluster exercises today. Several items are deliberately ported-but-dormant — built and tested, but gated off because the feature that activates them is not yet live on the cluster. Listing them precisely matters:
| Item | State |
|---|---|
| sBPF interpreter (v1, production) + verifier + ELF loader | Done / canonical — runs real programs, bank-exact, Firedancer-parity (interpreter, not JIT). |
| v2 clean-room interpreter — builtin/native path | Live via v2DispatchInternal/commitV2Mutations. |
| Vote program (native builtin) | Live — Vexor-authored rewrite is the sole, unconditional vote executor; the Sig-derived differential-test oracle has been fully removed (Stage 8, 2026-07-12). 990k+ live instructions byte-identical, 4.7× faster than the retired transplant. |
| v2 general BPF-over-Bank path | Not yet wired (M5_BankBackedBpfNotPlumbed); non-builtin programs run on the v1 interpreter. |
| alt_bn128 / poseidon syscalls | Ported + active, unconditionally Vexor's own pure-Zig backend (src/vex_crypto/) — the Firedancer-backed Ballet alternative has been removed from the tree outright (2026-07-12); -Dpure_zig is kept on the build command but is now a no-op. |
| ZK-ElGamal proof program | Ported + byte-faithful; deployed and confirmed bank-exact on the live node. |
| SIMD-0268 (raise CPI nesting limit 4→8) | Ported + wired on the v2 dispatch path (max_stack_depth threaded per-dispatch; constant = 9). |
| SIMD-0449 (direct account pointers in the serializer) | Built but dormant — gated on direct_account_pointers, inactive on testnet; only the =false path is taken today. |
SIMD-0464 (Vote InitializeAccountV2), SIMD-0437 (rent lamports-per-byte) |
Ported with KATs, dormant — gated, not yet activated on the cluster. |
| Nonce-CPI Tier-2 (System 4/5/6 via CPI) | Not done — latent future feature, no live exposure. |
None of the dormant items affect the current bank hash (the gates are off), and none of the not-done items are active on the cluster today — they are forward-looking ports staged for the activation slots, not live divergences.
Where to look in the source¶
| File | Role |
|---|---|
src/vex_bpf/interpreter.zig |
Production v1 sBPF interpreter (live program execution). |
src/vex_bpf2/interpreter.zig |
Clean-room v2 interpreter rebuild (mirrors solana-sbpf v0.14.4). |
src/vex_bpf2/verifier.zig |
sBPF bytecode verifier (spec-for-spec rebuild). |
src/vex_bpf2/elf.zig, loader.zig |
ELF parse + program memory/layout. |
src/vex_bpf2/syscalls.zig |
Syscall registry + handlers (incl. the crypto syscalls). |
src/vex_bpf2/zksdk/ |
ZK-ElGamal proof verifier (ed25519 MSM, Merlin, sigma + range proofs). |
src/vex_crypto/bn254.zig |
BN254 (alt_bn128) + Poseidon — pure-Zig under -Dpure_zig (canonical); FD ballet is the alternate backend. |
src/vex_bpf2/builtins/, src/vex_svm/native/ |
Native programs (System, Vote, Stake, Config, ALT, …). |
src/vex_svm/v2_dispatch.zig, src/vex_svm/replay_stage.zig |
v2DispatchInternal + commitV2Mutations; native dispatch + fallback. |