Consensus Protocols

Deep dive into Avalanche's Snow* family of consensus protocols including Snowball, Snowman, and Avalanche consensus.

Avalanche uses a novel family of consensus protocols collectively known as the Snow protocols*. These protocols achieve consensus through repeated random sampling, providing probabilistic safety guarantees with sub-second finality.

Consensus Overview

Traditional consensus protocols (like PBFT) require all-to-all communication, limiting scalability. Avalanche's approach is fundamentally different:

PropertyTraditional (PBFT)Avalanche Snow*
CommunicationAll-to-all (O(n²))Random sampling (O(k log n))
FinalityDeterministicProbabilistic (tunable)
Scalability~100 nodesThousands of nodes
LatencySecondsSub-second

The Snow* protocols are named after their "snowball" effect - once a preference starts forming, it quickly avalanches to a decision.

The Snow* Protocol Family

Snowball: Binary Consensus

Snowball is the foundational protocol for deciding between two conflicting options:

Key Parameters:

  • k (sample size): Number of validators to query (default 20)
  • αₚ (preference threshold): Votes needed to switch preference (default 15)
  • α꜀ (confidence threshold): Votes needed to increase confidence (default 15)
  • β (finalization threshold): Consecutive successful rounds (default 20)
  • Concurrent polls: Parallel polls while processing (default 4)
  • Optimal processing: Soft cap on in-flight items (default 10)

Snowman: Linear Chain Consensus

Snowman extends Snowball to decide on a linear sequence of blocks. It's used by:

  • P-Chain (Platform Chain)
  • C-Chain (Contract Chain)
  • X-Chain (Exchange Chain) - linearized in the Cortina upgrade (April 2023)
  • Most Avalanche L1s

View source on GitHub

snow/consensus/snowman/consensus.go
type Consensus interface {
    // Initialize with last accepted block
    Initialize(
        ctx *snow.ConsensusContext,
        params snowball.Parameters,
        lastAcceptedID ids.ID,
        lastAcceptedHeight uint64,
        lastAcceptedTime time.Time,
    ) error

    // Tracking & liveness
    NumProcessing() int
    Processing(ids.ID) bool
    IsPreferred(ids.ID) bool

    // Add a new block to consensus
    Add(Block) error

    // Get the preferred blocks
    Preference() ids.ID
    PreferenceAtHeight(height uint64) (ids.ID, bool)

    // Get the last accepted block
    LastAccepted() (ids.ID, uint64)

    // Record poll results from network sampling
    RecordPoll(context.Context, bag.Bag[ids.ID]) error

    // Lightweight ancestry lookup
    GetParent(id ids.ID) (ids.ID, bool)
}

Block Lifecycle in Snowman:

Avalanche DAG Consensus (Historical)

The Avalanche DAG consensus engine is no longer used on the Primary Network. The X-Chain was linearized in the Cortina upgrade (April 2023 on Mainnet) and now uses Snowman consensus. The DAG engine code remains in the codebase for historical compatibility only.

Historically, Avalanche consensus operated on a Directed Acyclic Graph (DAG) of transactions, where non-conflicting transactions could be processed in parallel:

       ┌───┐
       │ G │  Genesis
       └─┬─┘
      ┌──┴──┐
    ┌─┴─┐ ┌─┴─┐
    │ A │ │ B │  Vertices could have
    └─┬─┘ └─┬─┘  multiple parents
      │  ╲╱  │
      │  ╱╲  │
    ┌─┴─┐ ┌─┴─┐
    │ C │ │ D │
    └───┘ └───┘

The linearization was implemented via the LinearizableVMWithEngine interface, which allows a DAG-based VM to transition to linear block production after a designated "stop vertex."

Consensus Engine Architecture

The consensus engine sits between the VM and the network:

Engine States

The consensus engine progresses through several states (snow/state.go):

type State uint8

const (
    Initializing State = iota  // 0
    StateSyncing               // 1
    Bootstrapping              // 2
    NormalOp                   // 3
)
StateDescription
InitializingInitial setup before sync begins
StateSyncingFast catch-up using state summaries
BootstrappingCatching up with network state via block replay
NormalOpParticipating in consensus

The Snowman Engine

View source on GitHub

snow/engine/snowman/engine.go
type Engine struct {
    Config

    // Consensus instance
    Consensus smcon.Consensus

    // VM interface
    VM block.ChainVM

    // Network communication
    Sender common.Sender

    // Block management
    pending   map[ids.ID]snowman.Block
    blocked   map[ids.ID][]snowman.Block
}

Engine Responsibilities:

  1. Block fetching: Request missing blocks from peers
  2. Block verification: Validate blocks via the VM
  3. Consensus voting: Query peers and record votes
  4. Block finalization: Accept or reject blocks based on consensus

Block Processing Flow

1. Receiving a Block

func (e *Engine) Put(ctx context.Context, nodeID ids.NodeID, requestID uint32, blkBytes []byte) error {
    // Parse the block
    blk, err := e.VM.ParseBlock(ctx, blkBytes)

    // Verify ancestry exists
    if !e.hasAncestry(blk) {
        // Request missing ancestors
        e.requestAncestors(blk.Parent())
        return nil
    }

    // Issue to consensus
    return e.issue(ctx, blk)
}

2. Issuing to Consensus

func (e *Engine) issue(ctx context.Context, blk snowman.Block) error {
    // Verify the block
    if err := blk.Verify(ctx); err != nil {
        return err
    }

    // Add to consensus
    if err := e.Consensus.Add(blk); err != nil {
        return err
    }

    // Start voting
    e.sendQuery(ctx, blk.ID())
    return nil
}

3. Recording Votes

func (e *Engine) Chits(ctx context.Context, nodeID ids.NodeID, requestID uint32, preferredID ids.ID, ...) error {
    // Collect votes in a bag
    votes := bag.Of(preferredID)

    // When enough votes collected, record the poll
    if e.polls.Finished() {
        return e.Consensus.RecordPoll(ctx, e.polls.Result())
    }
    return nil
}

Snowman++ (ProposerVM)

Snowman++ adds soft proposer windows on top of Snowman to pace block production. It is implemented by wrapping a ChainVM in the ProposerVM and is enabled on the P-Chain and C-Chain.

vms/proposervm/vm.go
// ProposerVM wraps a ChainVM to add proposer selection
type VM struct {
    inner block.ChainVM

    // Proposer selection
    windower Windower
}

How it works:

  1. Validators are sampled (by stake) to form a proposer list for the next block.
  2. Each proposer gets a 5s window; up to 6 windows are scheduled from the parent timestamp.
  3. Within their window, only the designated proposer can build a valid block.
  4. After the final window, any validator may propose, which preserves liveness if proposers are offline.

Benefits:

  • Predictable pacing: Prevents multiple validators from racing the same height.
  • Stake-weighted fairness: Windows are derived from the subnet validator set.
  • Graceful fallback: Production opens to everyone after the final window.

Consensus Parameters

Consensus parameters live in snow/consensus/snowball/parameters.go:

type Parameters struct {
    // Sample size for each poll
    K int `json:"k"`

    // Switch preference threshold
    AlphaPreference int `json:"alphaPreference"`

    // Increase confidence threshold
    AlphaConfidence int `json:"alphaConfidence"`

    // Finalization threshold
    Beta int `json:"beta"`

    // Concurrent polls
    ConcurrentRepolls int `json:"concurrentRepolls"`

    // Congestion control
    OptimalProcessing     int `json:"optimalProcessing"`
    MaxOutstandingItems   int `json:"maxOutstandingItems"`
    MaxItemProcessingTime time.Duration `json:"maxItemProcessingTime"`
}
ParameterDefaultDescription
K20Validators sampled per round
AlphaPreference15Votes needed to change preference
AlphaConfidence15Votes needed to increase confidence
Beta20Consecutive successful polls to finalize
ConcurrentRepolls4Parallel polls while processing
OptimalProcessing10Soft target for in-flight vertices/blocks
MaxOutstandingItems256Health threshold for queued items
MaxItemProcessingTime30sHealth threshold for a single item

These parameters are network-wide and cannot be changed for individual nodes. Modifying them would cause consensus failures.

Security Properties

Probabilistic Safety

The probability of a safety violation (accepting conflicting blocks) is:

P(safety violation)<(1αconfidencek)βP(\text{safety violation}) < \left(1 - \frac{\alpha_{confidence}}{k}\right)^\beta

With default parameters: P<(11520)201012P < \left(1 - \frac{15}{20}\right)^{20} \approx 10^{-12}

Liveness

Avalanche guarantees liveness as long as:

  • More than α/k (75%) of stake is honest
  • Network is eventually synchronous

Next Steps

Is this guide helpful?