Skip to content

Consensus — How Vexor Agrees With the Cluster

Docs-site note (2026-07-11): Part of the Vexor documentation site, modeled on the structure and feel of docs.anza.xyz / anza.xyz. Vexor is testnet-only, pre-production (0.9.x); this page documents the consensus path as it runs today, not a mainnet-ready client. Style: clean headings, plain language, claims grounded in source + live measurement. Companion pages: VexLedger (the blockstore + offline replay) and The Vote Program (the vote execution rewrite referenced in §4).


Overview

Consensus is how a validator agrees, byte-for-byte, with the rest of the cluster about what happened in every slot. A Solana validator does not "trust" the network — it independently re-executes every block, computes a 32-byte fingerprint of the resulting state called the bank hash, and votes for the fork whose bank hashes match the supermajority. If a node computes even one wrong byte anywhere in that pipeline, its bank hash diverges from the cluster, its votes land on a fork no one else is on, and it falls out of consensus.

Vexor is an independent Solana validator client implemented from scratch in Zig, currently testnet-only. Its consensus path is validated byte-for-byte against the Agave reference implementation — currently anchored to the newest testnet release, v4.2.0-beta.0, re-anchored as the cluster upgrades — with Firedancer and Sig used as engineering references. The result, measured live: Vexor votes bank-exact and CURRENT in-cluster as a 100%-Zig validator, and has proven block production (its produced blocks were accepted by the cluster at a real leader window — see Status).

This page walks the consensus path in order: replay → bank hash → accounts lt-hash → voting → fork choice → the carrier discipline that keeps it all honest.


The consensus path, end to end

shreds in → reassemble entries → REPLAY (execute every tx) → FREEZE (compute bank_hash)
                                                  ┌───────────────────┘
                                    bank_hash matches cluster?
                                       yes → VOTE for this fork (TowerBFT)
                                       no  → divergence → carrier forensics

Every box below is a real component in the deployed binary; the file references are to the canonical working tree (src/vex_svm/…).


1. Replay — execute every block, freeze a bank

The replay stage (src/vex_svm/replay_stage.zig) is the heart of the node. For each slot it reassembles the block's entries from shreds, then executes every transaction in the block against the parent's account state — system transfers, vote and stake instructions, and arbitrary on-chain programs through Vexor's own sBPF virtual machine. Transactions run on the replay path either serially or, when enabled, across a parallel wave pool; both paths execute every transaction (a deliberate, canonical-matching choice — the block's compute cost is an after-the-fact statistic, never a per-transaction gate that could silently skip execution and drop a state mutation).

When the last entry of a slot has been executed, the node freezes the bank (Bank.freeze, bank.zig:4245). Freezing applies the slot's sysvar updates, folds every account write into the running state accumulator, and computes the slot's bank hash — the consensus fingerprint. A frozen bank is immutable; its hash is what the node will vote on.

Because Vexor persists shreds in VexLedger, any rooted slot can also be re-replayed offline, with no network, and reproduces the cluster's bank hash exactly. That offline-replay capability is the backbone of the forensics discipline at the end of this page.


2. Bank hash — the consensus fingerprint

The bank hash is a two-step SHA-256 over four inputs. This is the single most consensus-critical computation in the validator: it is Bank.computeBankHash (bank.zig:1109), a byte-exact port of Firedancer's fd_hashes_hash_bank() (which is itself byte-identical to Agave).

step1     = SHA256( parent_bank_hash ‖ signature_count (u64 LE) ‖ poh_hash )
bank_hash = SHA256( step1 ‖ accounts_lt_hash (2048 bytes, u16 LE) )

The four inputs, exactly:

Input What it is Source in bank.zig
parent bank hash The 32-byte bank hash of this slot's parent — this is what chains slots into a fork. A wrong parent hash poisons every descendant. self.parent_hash
signature count The total number of transaction signatures in the block, as a little-endian u64. self.signature_count
PoH hash The last entry's Proof-of-History hash for the slot — not any transaction's recent-blockhash (a real trap; the comment at bank.zig:1112 flags it). self.poh_hash
accounts lt-hash The 2048-byte homomorphic lattice hash of the entire account set after the slot — see the next section. self.accounts_lthash

There is an optional third step that folds in an 8-byte hard-fork mixin (getHashData, bank.zig:1080), but it is dormant on the normal/testnet path — the mixin is null on every ordinary slot, so the hash is byte-identical to the two-step form. It only fires when replaying the exact slot a hard fork is scheduled at, which this node never recomputes (it loads the fork-anchor hash from the snapshot).

Every frozen slot emits a [BANK-FROZEN] log line carrying the slot, parent hash, signature count, PoH hash, the final bank hash, and a checksum of the lt-hash. That line is the node's per-slot consensus black box — the parity tooling and the VexLedger flight recorder consume it to localize any divergence to the exact diverging input.


3. Accounts lt-hash — folding all account state into one leaf

The fourth bank-hash input deserves its own section because it is where all account state enters consensus. Solana's modern hashing (the accounts-lt-hash feature) replaces an expensive full Merkle re-hash of every account with a homomorphic lattice hash — a structure you can update incrementally as accounts change, without ever re-hashing the whole 80M+ account set.

Vexor's lt-hash (src/vex_crypto/lthash.zig) is 1024 × 16-bit lanes = 2048 bytes, matching Firedancer's fd_lthash_t. Two properties make it work:

  • Per-account hash. Each account's contribution is a BLAKE3 extended-output hash of lamports ‖ data ‖ executable ‖ owner ‖ pubkey, read out as 1024 little-endian u16 lanes (Bank.accountLtHash, bank.zig:982). A zero-lamport account contributes nothing (the identity hash) — exactly as Firedancer's fd_hashes.c excludes it.
  • Homomorphic accumulation. The slot keeps a running accumulator inherited from the parent. Each account write applies accumulator = accumulator − ltHash(old_state) + ltHash(new_state) via per-lane wrapping u16 add/subtract (applyLtHashDelta, bank.zig:1025). Because add and subtract are commutative and associative mod 2¹⁶, the final accumulator is independent of write order and never requires re-reading untouched accounts. On Zen4 the add/subtract compile to single AVX-512 vpaddw/vpsubw instructions over 32 lanes at a time.

At freeze, the 2048-byte accumulator is fed raw, as little-endian bytes, into step 2 of the bank hash. This is the mechanism by which "a wrong lamport balance on one account" or "one wrong byte of one account's data" becomes "a wrong bank hash for the whole slot." The lt-hash is also where the majority of historical bank-hash divergences ("carriers") were ultimately localized — a single account whose state Vexor computed differently from Agave.


4. Voting — TowerBFT, the vote program, vote landing

Once a bank is frozen and its hash matches what the node expects, the validator votes for that slot. A Solana vote is itself an on-chain transaction submitted to the vote program, and Vexor's vote program is Vexor-authored — a July 2026 rewrite, derived directly from Agave 4.2 semantics and shaped for zero-allocation execution, that is now the sole, unconditional live executor of every vote transaction. A Sig-derived component served as the differential-test oracle during the rewrite and has been fully removed from the tree (Stage 8, 2026-07-12). See The Vote Program for the full architecture, methodology, and measured performance.

The vote program parses and applies the full TowerBFT instruction set — including the modern TowerSync / TowerSyncSwitch variants (instruction tags 14/15) used today, alongside the legacy vote and vote-state-update forms. The core data structure is the tower of lockouts:

  • A Lockout is { slot, confirmation_count }. Its lockout period is 2^confirmation_count slots, and its expiration is slot + lockout_period (existing implementation, vote_program.zig:98).
  • The tower holds at most MAX_LOCKOUT_HISTORY = 31 lockouts (existing implementation, vote_program.zig:39) — the canonical Solana bound. Each new vote roughly doubles the lockout on the slots beneath it, which is what makes a vote a real commitment: the deeper a slot is in the tower, the longer the validator is locked out from voting against it.

Applying a vote mutates the on-chain vote account's state, and that mutated state is itself an account write — which means the vote state bytes feed back into the lt-hash and therefore the bank hash. Vote-state serialization must be byte-exact for the same reason everything else must: a wrong vote-state byte is a bank-hash divergence.

Getting the vote to land. A vote that is computed correctly still has to reach the current leader's TPU. Vexor sends votes over native QUIC (src/vex_network/solana_quic.zig). Getting QUIC votes to land required fixing a real protocol bug: Solana leaders grant unidirectional-stream credit via coalesced MAX_STREAMS_UNI QUIC frames and omit the initial transport-parameter grant, so a naive client never learns it is allowed to open a stream and falls back to UDP forever. Vexor's frame parser reads that credit byte-exact and gates stream opening on it, with a UDP relay as a safe fallback. The fix was validated against live leaders (credit correctly extracted, zero unhandled frames) before deployment.


5. Tower, lockouts, and fork choice — picking the heaviest fork

When the network has competing forks, the validator must pick one to vote on, and it must pick the same one everybody else will — otherwise it strands its own stake on a minority branch. Solana's rule is heaviest-fork: follow the fork carrying the most stake-weighted votes, subject to the tower lockouts above (you cannot vote against a slot you are still locked out on).

Vexor implements this as HeaviestSubtreeForkChoice (src/vex_consensus/fork_choice.zig), a port of Agave's heaviest-subtree fork choice. Its design:

  • Forks are keyed by SlotHashKey = (slot, bank_hash), not slot alone — so two equivocating blocks at the same slot (a duplicate/forked leader) are distinct nodes in the tree and never silently merged.
  • Each fork node tracks stake_voted_at (stake that voted for exactly this slot) and stake_voted_subtree (that plus all descendants), and caches its best_slot — the heaviest valid descendant. Invalid forks (e.g. a slot marked dead because its block failed replay) are excluded from best_slot selection via the candidacy flag, so the node never roots toward a known-bad subtree.
  • When replay freezes a slot and when cluster votes arrive, the tree's stake aggregates update and the heaviest descendant is recomputed; that is the slot the node builds its next vote on.

Mark-fork-invalid is wired into the dead-slot path (replay_stage.zig): when a block is marked dead, fork choice down-weights that subtree so the node re-points at a sibling instead of stranding behind the dead branch. This closed a real gap where a dead subtree retained full weight and widened the gap to the tip.

Scope note. The fork-choice port is the stake-aggregation + (slot, hash) keying + vote-migration core that drives day-to-day testnet voting. The full switch-threshold / tower-lockout fork-selection refinements, checked against the current Agave reference (v4.2.0-beta.0), are tracked as a follow-on phase (noted in the module header). Alpenglow (SIMD-0387 / the new votor consensus) is present only as a feature-gated scaffold, default off (-Dalpenglow=false) — no votor logic exists anywhere in the client yet; it is not active on testnet and not on the live voting path.


6. The proven result

Two consensus milestones are real and measured, not aspirational:

  • Bank-exact, CURRENT voting. Vexor runs as a 100%-Zig validator on the live testnet cluster, freezes each slot bank-exact (its [BANK-FROZEN] bank hashes match the cluster's), and stays CURRENT (not delinquent) with its votes landing at ~98.7% of maximum vote credits (measured against the theoretical 16/slot ceiling, on par with the Agave reference node run alongside it). This has held across weeks of voting under load — which is itself a proof that cross-slot account state is carried correctly, because the bank hash is a function of all account state and would diverge instantly if a prior slot's writes hadn't reached the next slot.
  • Proven block production. With the turbine real-stakes fix (broadcasting over the stake-weighted epoch set rather than a uniform-stake stub), Vexor's produced blocks are accepted by the cluster at a sustained ~97%+ of leader slots (first proven 4/4 at a real leader window, epoch 977, confirmed three independent ways via the public cluster oracle: getBlocks, getBlock, getBlockProduction). The pre-fix node produced blocks the cluster skipped 100% of the time; post-fix they land. The produced blocks are currently empty — transaction-bearing production exists behind a gate and is not yet enabled; the consensus-critical achievement so far is that Vexor's blocks are cluster-valid.

These two together mean Vexor participates in consensus as a peer: it agrees on history (bank-exact voting) and it can contribute to history (accepted block production).


Why one wrong byte matters — the carrier discipline

Everything above composes into a single brittle invariant: bank hash is a pure function of (parent hash, signature count, PoH, accounts lt-hash), and the lt-hash is a pure function of every account's exact bytes. There is no fuzzy matching and no quorum-by-similarity. One wrong lamport, one wrong vote-state byte, one wrong sysvar field → a different lt-hash → a different bank hash → a vote on a fork no one else is on → the node falls out of consensus (often surfacing first as a delinquency, which is usually a symptom downstream of a divergence, not the root cause).

Vexor treats a divergence as a forensic event with a fixed procedure, the "carrier" discipline:

  1. Localize the slot. Find the first slot where Vexor's bank hash differs from the cluster's (the per-slot [BANK-FROZEN] line is the evidence).
  2. Localize the input. The bank hash's four inputs are logged separately, so the first divergence is attributed to parent / signatures / PoH / lt-hash before any code is touched.
  3. Reproduce offline. Re-replay the carrier slot offline, with no network, from VexLedger's persisted shreds — deterministic and repeatable.
  4. Localize the account. When the input is the lt-hash, per-account analysis narrows it to the exact diverging account + field + owner program.
  5. Gate the fix offline. A candidate fix must make the offline replay reproduce the cluster's bank hash before it is ever deployed to the live voting path.

The same persistence-plus-replay machinery powers VexLedger's real-time divergence alarm, which is now armed live: the flight recorder captures each slot's consensus fingerprint, a non-blocking background thread compares it against a cluster oracle, and on the first divergence it automatically re-replays the suspect slot offline and narrows the cause to one of the four bank-hash inputs above (steps 1–3 of this playbook, automated). When the culprit is the lt-hash, pinpointing the exact diverging account is the same per-account analysis this playbook already uses (step 4) — collapsing what used to be a fully manual, multi-day investigation into a mostly-automated one. See the VexLedger page for that mechanism, and Reliability & Conformance for its current status.


Status (2026-07-11)

  • Voting: bank-exact and CURRENT in-cluster, sustained under load, ~98.7% of maximum vote credits. 100%-Zig consensus path.
  • Bank hash: two-step SHA-256, byte-exact vs the Agave reference (anchored to v4.2.0-beta.0, the newest testnet release) and Firedancer; hard-fork mixin dormant on testnet.
  • Accounts lt-hash: 2048-byte homomorphic lattice hash, AVX-512 accelerated, BLAKE3 per-account leaf, KAT-gated at boot.
  • Voting / vote program: native TowerBFT (TowerSync + legacy forms), 31-deep lockout tower, native QUIC votes (max-streams-uni RX fix) with UDP relay fallback. The vote program itself is Vexor-authored (July 2026 rewrite, Agave 4.2 semantics) and is the sole, unconditional live executor; a Sig-derived component served as the differential-test oracle during development and has been fully removed from the tree (Stage 8, 2026-07-12) — see The Vote Program.
  • Fork choice: HeaviestSubtreeForkChoice — stake-weighted, (slot, hash)-keyed, dead-fork down-weighting; switch-threshold/lockout refinements are a tracked follow-on phase.
  • Block production: turbine real-stakes fix proven — blocks produced and cluster-accepted at ~97%+ of leader slots. Blocks are currently empty; transaction-bearing production is gated and not yet enabled.
  • Known open items (per the canonical status doc): gossip CRDS signature verification is a security/ citizenship hardening item — crds.verify() exists but is not yet wired into cluster-info ingest; it is not an active bank-hash divergence. Snapshot-trust enforcement has one remaining gated call site. Alpenglow is a default-off scaffold, not on the live path.