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 byte-for-byte conformant with the Solana protocol's canonical ledger history — currently anchored to the network's v4.2.0-beta.0 protocol baseline, re-anchored as the cluster upgrades. 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.

Transaction signature verification on replay

Before a block's transactions are executed, replay runs a whole-slot transaction signature verification pre-pass (src/vex_svm/replay_sigverify.zig): every transaction signature in the block is verified before any transaction in that block executes. A block containing an invalid transaction signature is invalid in its entirety — if any transaction in the block carries an invalid signature, the entire slot is marked dead rather than executed.

Verification runs as a parallel pre-pass across a configurable worker pool, completing before any execution-relevant bank state is mutated, and uses strict, non-cofactored Ed25519 verification per the network's signature-verification rules (SIMD-0152). The failure policy is deliberately asymmetric: an invalid signature kills the slot, but a transaction the parser cannot decode is counted rather than treated as fatal — rejecting a block the rest of the cluster accepts would itself cause a fork.

Transaction well-formedness is enforced as part of sanitization: a transaction's signature count must match its declared signer count exactly, on both the produce/ingest path and replay.

Three log lines mark this path at runtime: - [REPLAY-SIGVERIFY] whole-slot parallel pre-pass LIVE: N workers on cores {...} — emitted once at boot. - [REPLAY-SIGVERIFY] ARMED txs=N unparseable=N blocks_rejected=N — emitted periodically; blocks_rejected staying at 0 is the healthy state, non-zero means either an attack or a bug and warrants investigation. - [REPLAY-SIGVERIFY-DEAD] slot=N tx_i=N ... — emitted the moment a slot is killed for an invalid signature.


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 implementation of the network's canonical bank-hash algorithm.

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, per the network's accounts-lt-hash specification. 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), per the protocol's exclusion rule.
  • 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."


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 — implemented directly to the Solana vote-program specification and shaped for zero-allocation execution — and is the sole, unconditional live executor of every vote transaction. 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). Solana leaders grant unidirectional-stream credit via coalesced MAX_STREAMS_UNI QUIC frames rather than the initial transport-parameter grant; Vexor's frame parser reads that credit byte-exact and gates stream opening on it, with a UDP relay as a safe fallback.


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), an implementation of the protocol's heaviest-subtree fork-choice algorithm. 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.

Scope note. The fork-choice implementation 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 network protocol version (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). 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. Turbine broadcasts each produced block over the stake-weighted epoch set, and Vexor's produced blocks are accepted by the cluster at a sustained ~97%+ of leader slots (confirmed three independent ways via the public cluster oracle: getBlocks, getBlock, getBlockProduction). 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 (designed): the flight recorder captures each slot's consensus fingerprint, and on the first divergence it auto-replays and emits the exact diverging account — collapsing a normally multi-day investigation into one notification. See the VexLedger page for that mechanism, and the offline-replay carrier playbook it builds on.


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 per the network's canonical bank-hash algorithm (anchored to the v4.2.0-beta.0 protocol baseline); 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 handling) with UDP relay fallback. The vote program itself is Vexor-authored and is the sole, unconditional live executor — 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's stake-weighted broadcast is proven — blocks produced and cluster-accepted at ~97%+ of leader slots. Blocks are currently empty; transaction-bearing production is gated and not yet enabled.
  • Replay transaction-signature verification: live, via a whole-slot parallel pre-pass (see above); an invalid signature marks the slot dead, per the protocol's whole-block validity rule. Confirmed armed at boot, with zero rejected blocks observed under normal operation.
  • Known open items: gossip message signature verification is a planned hardening item, not yet wired into cluster-info ingest; it does not affect bank-hash consensus. Snapshot-trust enforcement has one remaining item tracked. Alpenglow is a default-off scaffold, not on the live path.