Deep dive into IOTA’s DAG-based consensus protocol and how validators reach agreement on transaction ordering.
IOTA implements a DAG-based consensus protocol that provides high throughput and low latency while maintaining Byzantine fault tolerance. This protocol is optimized for the common case where most objects are owned, requiring consensus only for shared objects.
Round progression: Validators propose blocks for the current round
Block broadcast: Each validator broadcasts their block to the network
Block validation: Validators verify incoming blocks and add them to their local DAG
Round advancement: Once a validator has enough blocks for round N, they can propose for round N+1
// Rounds are simple integerspub type Round = u32;// Genesis round is round 0const GENESIS_ROUND: Round = 0;// Validators build on top of each other's blockslet current_round = last_included_round + 1;
For each round, one validator is designated as the leader:
pub struct LeaderSchedule { // Determines leader for each round leader_swap_table: Arc<RwLock<LeaderSwapTable>>, // Number of leaders to elect per round num_leaders: usize,}// Get leader for a specific roundfn leader_at_round(&self, round: Round) -> AuthorityIndex { // Uses reputation scores and rotation to select leader}
Leaders are rotated among validators based on:
Stake weight
Reputation scores (derived from past performance)
Deterministic rotation to ensure fairness
Leader rotation ensures that no single validator can monopolize block production while giving higher-stake validators more opportunities proportional to their stake.
pub struct UniversalCommitter { // Consensus context context: Arc<Context>, // Leader selection leader_schedule: Arc<LeaderSchedule>, // DAG state dag_state: Arc<RwLock<DagState>>, // Number of leaders per round number_of_leaders: usize, // Whether to use pipelining pipelined: bool,}
The leader block has a quorum (2f+1) of validators referencing it
The references appear in blocks from subsequent rounds
No conflicting leaders have more support
pub struct CommittedSubDag { // The leader block being committed leader: VerifiedBlock, // All blocks that causally precede the leader blocks: Vec<VerifiedBlock>, // Timestamp of the commit timestamp_ms: BlockTimestampMs, // Commit index commit_index: CommitIndex,}
pub trait BlockVerifier { fn verify(&self, block: &SignedBlock) -> ConsensusResult<()>;}// Verification checks:// - Valid signature from claimed author// - Correct epoch and protocol version// - Round number is sequential// - Ancestors are from previous round// - Timestamp is not too far in past/future// - Transactions are valid// - Block size is within limits
pub struct CommitObserver { // Channel to send committed output sender: Sender<CommittedSubDag>, // Last processed commit index last_processed: CommitIndex,}// Process a committed sub-DAGpub fn handle_commit(&mut self, committed: CommittedSubDag) { // Extract transactions in commit order let transactions = self.order_transactions(&committed); // Send to execution layer self.sender.send(committed)?; // Update last processed index self.last_processed = committed.commit_index;}
Within a committed sub-DAG, transactions are ordered deterministically:
Block ordering: Blocks are ordered by causal relationships in the DAG
Transaction ordering: Within each block, transactions appear in the order specified by the proposer
Deterministic execution: All validators execute transactions in the same order
// Order blocks in a committed sub-DAGfn order_blocks(subdag: &CommittedSubDag) -> Vec<VerifiedBlock> { // Topological sort by round and authority subdag.blocks .iter() .sorted_by_key(|b| (b.round(), b.author())) .collect()}