SOLVIZ

How Solana Works

From block production to propagation, consensus, and ingestion — traced to the agave v4.2.0 source that runs it.

anza-xyz/agave · tag v4.2.0

Written by Kyungmin Jeon · Published

01

How a Block is Built

A block is not written all at once. The leader executes transactions and stamps each batch into a running clock.

For its turn, one validator becomes the leader and gets to write the block. It does not process transactions one by one. The banking stage hands non-conflicting transactions to parallel worker threads, so transfers that touch different accounts run at the same time.

Every executed batch is then recorded into Proof of History. This is what makes Solana time-ordered before consensus even looks at the block: the order is already baked into the hash chain as the leader produces it.

SchedulerBanking workersPoH recorderPoH chainno shared accountsame accountPoH hash
Recorded in PoH
0 / 4
1The scheduler splits by account
2Execute in parallel, then record
3The conflicting one runs after

Figure 1. Transactions are scheduled across banking workers; conflicting accounts serialize, and each executed batch becomes an entry in the PoH chain.

In the code, `process_and_record_transactions` first runs age and fee checks, then executes the batch and hands the result to the recorder. The recorder calls `record_batches`, which mixes each batch's hash into the PoH chain and appends an entry. Ticks keep the clock moving even when there is nothing to execute, so the slot always advances at a steady rate.

agave v4.2.0core/src/banking_stage/consumer.rs:124
124pub fn process_and_record_transactions(
125 &self,
126 bank: &Bank,
127 txs: &[impl TransactionWithMeta],
128) -> ProcessTransactionBatchOutput {
129 let mut error_counters = TransactionErrorMetrics::default();
130 let pre_results = vec![Ok(()); txs.len()];
131 let check_results = bank.check_transactions(
132 txs, &pre_results, bank.max_processing_age(),
133 true, &mut error_counters,
134 );
135 // ...
136 let mut output = self.process_and_record_transactions_with_pre_results(
137 bank, txs, check_results, /* flags */
138 );

process_and_record_transactionsThe batch is age/QoS-checked, then executed and recorded — the "run it, then stamp it" handoff.

agave v4.2.0poh/src/poh_recorder.rs:349
349pub fn record(
350 &mut self,
351 bank_id: BankId,
352 mixins: Vec<Hash>,
353 transaction_batches: Vec<Vec<VersionedTransaction>>,
354) -> Result<RecordSummary> {
355 // ...
356 let tick_height = self.tick_height();
357 let (mut poh_lock, _) = measure_us!(self.poh.lock().unwrap());
358 let (mixed_in, _) =
359 measure_us!(poh_lock.record_batches(&mixins, &mut self.entries));

recordrecord_batches mixes each batch hash into the PoH chain and appends the entry — this is the moment a transaction is fixed in time.

Key Concept

The leader interleaves execution and timekeeping. By the time a batch is recorded, its position in time is already fixed — ordering is a byproduct of production, not a separate consensus step.

02

How a Block Spreads

One leader cannot send a full block to thousands of validators. So it does not try.

The block must reach the whole cluster fast, but a single leader does not have the bandwidth to send it to everyone. Turbine solves this the way a content-delivery network does.

First the block is broken into shreds. Then, instead of broadcasting to all, the leader sends shreds to a small anchor set, and each of those nodes forwards to the next layer. No node ever forwards to more than a fixed number of peers.

LeaderLayer 1Layer 2data shredcoding shred
1Split the block into shreds
2The leader sends only to layer 1
3Layer 1 forwards to layer 2

Figure 2. The block is split into data and coding shreds, then fanned out through a tree — the leader sends to an anchor set, which retransmits onward.

Shreds come in fixed-size erasure batches: `DATA_SHREDS_PER_FEC_BLOCK = 32` data shreds plus 32 coding shreds, so up to a third can be lost and still recovered. The tree width is set by `DATA_PLANE_FANOUT = 200`: each node retransmits to up to 200 peers. `get_retransmit_peers` computes exactly which peers a node owns from its position in a deterministic, stake-weighted shuffle — so every validator agrees on the tree without coordinating.

agave v4.2.0ledger/src/shred.rs:118
118pub const DATA_SHREDS_PER_FEC_BLOCK: usize = 32;
119pub const CODING_SHREDS_PER_FEC_BLOCK: usize = 32;
120pub const SHREDS_PER_FEC_BLOCK: usize =
121 DATA_SHREDS_PER_FEC_BLOCK + CODING_SHREDS_PER_FEC_BLOCK;

FEC block geometry32 data + 32 coding shreds per erasure batch — lose up to a third and the block still reconstructs.

agave v4.2.0turbine/src/cluster_nodes.rs:47
47pub(crate) const DATA_PLANE_FANOUT: usize = 200;
48pub(crate) const MAX_NUM_TURBINE_HOPS: usize = 4;
49// ...
50fn get_retransmit_peers<T>(
51 fanout: usize,
52 pred: impl Fn(T) -> bool, // identifies this node
53 nodes: impl IntoIterator<Item = T>,
54) -> (usize, impl Iterator<Item = T>) {
55 let index = /* this node's position in the shuffle */;
56 let offset = index.saturating_sub(1) % fanout;
57 let anchor = index - offset;
58 let peers = (anchor * fanout + offset + 1..).step_by(fanout).take(fanout);
59 (index, peers)
60}

DATA_PLANE_FANOUT / get_retransmit_peersEach node forwards to up to 200 peers; its exact peers fall out of its index in a deterministic shuffle, so the tree needs no coordination.

Key Concept

Turbine trades a little redundancy (coding shreds) and a couple of hops of latency for bandwidth that scales with the network. No single node is a bottleneck.

03

How the Network Agrees

Validators do not stop to hold a vote. They vote as the blocks stream past, and each vote costs more to take back.

Once a validator receives and replays a block, it checks the result and votes. Solana's consensus, Tower BFT, does not stop the chain to run voting rounds.

Each vote commits the validator more deeply to that fork through lockout. The deeper the lockout, the more a validator would lose by switching forks — so agreement becomes self-reinforcing. There is no slashing that burns stake; irreversibility comes from the growing cost of contradicting your own votes.

26%22%16%20%16%2/3Voted stakeLockout of one validator (slots)processedconfirmedfinalizedvotetwo-thirds line
Voted stake
0%
1Votes pile up as stake
2Past two-thirds it is confirmed
3Each vote doubles the lockout

Figure 3. Validators vote until a supermajority of stake agrees; lockout doubles with depth, and the block passes from processed to confirmed to finalized.

A slot becomes confirmed once voted stake passes two-thirds of total stake — that is exactly what `is_slot_confirmed` tests. Before voting, `is_locked_out` simulates adding the vote and refuses it if any surviving vote sits on a different fork. Tower checks stake at two depths, `VOTE_THRESHOLD_DEPTH_SHALLOW = 4` and `VOTE_THRESHOLD_DEPTH = 8`; once enough lockout piles onto one fork, reversing it becomes practically impossible, which is what users see as finalized at roughly 12–13 seconds.

agave v4.2.0core/src/consensus.rs:585
585fn is_slot_confirmed(
586 &self,
587 slot: Slot,
588 voted_stakes: &VotedStakes,
589 total_stake: Stake,
590) -> bool {
591 voted_stakes
592 .get(&slot)
593 .map(|stake| (*stake as f64 / total_stake as f64) > self.threshold_size)
594 .unwrap_or(false)
595}

is_slot_confirmedA slot is confirmed when voted stake exceeds threshold_size (two-thirds) of total stake.

agave v4.2.0core/src/consensus.rs:827
827pub fn is_locked_out(&self, slot: Slot, ancestors: &HashSet<Slot>) -> bool {
828 if !self.is_recent(slot) {
829 return true;
830 }
831 let mut vote_state = self.vote_state.clone();
832 vote_state.process_next_vote_slot(slot);
833 for vote in &vote_state.votes {
834 if slot != vote.slot() && !ancestors.contains(&vote.slot()) {
835 return true;
836 }
837 }
838 false
839}

is_locked_outSimulate the vote; if any surviving vote is on a different fork, this slot is still locked out. This is what makes reversal costly.

Key Concept

Confirmation is a stake threshold; finality is accumulated lockout. Solana buys fast agreement by making reversal expensive over time rather than by pausing for explicit checkpoints.

04

How Your Transaction Gets In

Before any of that, your transaction has to reach the leader — and prove it is really yours.

A signed transaction arrives at the leader as a network packet, and enters the TPU. There is no public mempool to sit in — the packet goes straight into the pipeline.

Three stages run in order. FetchStage collects packet batches from the sockets. SigVerifyStage verifies signatures and throws out forgeries. Only what survives reaches BankingStage.

UDP socketsFetchStageSigVerifyStageBankingStageDroppednot yet verifiedvalid signaturebad signature
Passed signature check
0 / 6
1FetchStage gathers packets
2Signatures are checked as a batch
3Only survivors reach banking

Figure 4. Packets flow FetchStage → SigVerifyStage → BankingStage; transactions with bad signatures are dropped before they reach execution.

The stages are wired together with channels: FetchStage's sender feeds SigVerifyStage, whose verified output feeds BankingStage. The real signature check is `ed25519_verify_serial`, run over a whole batch at once rather than per transaction — batching is how the leader keeps up at network speed. Only after this gate do transactions reach the scheduler from the first section, closing the loop back to block production.

agave v4.2.0core/src/tpu.rs:184
184let fetch_stage = FetchStage::new_with_sender(
185 tpu_vote_sockets, exit.clone(), &packet_sender, /* ... */
186);
187// ...
188let (sigverify_stage, _) = SigVerifyStage::new(
189 packet_receiver, vote_packet_receiver,
190 non_vote_sender, tpu_vote_sender, /* ... */
191);
192// ...
193let banking_stage = BankingStage::new_num_threads(
194 block_production_method, poh_recorder.clone(),
195 transaction_recorder, non_vote_receiver, /* ... */
196);

Fetch → SigVerify → BankingThe three stages are constructed in order and threaded together with channels — the leader's inbound assembly line.

agave v4.2.0core/src/sigverify.rs:322
322let (_, verify_time_us) = measure_us!(sigverify::ed25519_verify_serial(
323 &mut batch,
324 reject_non_vote,
325 enable_tx_v1,
326));
327let num_valid_packets = sigverify::count_valid_packets(std::iter::once(&batch));
328// ...
329let banking_packet_batch = BankingPacketBatch::new(vec![batch]);

ed25519_verify_serialThe real crypto gate: a whole packet batch is signature-checked at once, and invalid packets never reach banking.

Key Concept

No mempool means no public waiting room: a transaction is routed straight to the leader, verified in a batch, and either executed or dropped. The pipeline is the whole life of the transaction, compressed into one node.

Every snippet above is quoted verbatim from anza-xyz/agave at tag v4.2.0.