Skip to main content
The Header Chain Circuit implements Bitcoin’s header chain verification logic within a zero-knowledge virtual machine (zkVM). It validates sequences of Bitcoin block headers, ensuring chain integrity, proof-of-work compliance, and difficulty adjustments while maintaining a compact verifiable state.

Purpose

The circuit serves as a cryptographic light client for Bitcoin, enabling:
  • Chain Continuity Verification: Ensures each block correctly references its predecessor
  • Proof-of-Work Validation: Verifies block hashes meet difficulty requirements
  • Difficulty Adjustment: Validates Bitcoin’s 2016-block retargeting mechanism
  • Accumulative Work Tracking: Maintains total accumulated proof-of-work for chain selection
  • Recursive Proving: Enables verification of arbitrarily long chains through proof composition

Core Verification Logic

The circuit performs comprehensive validation for each block header in the input sequence:

1. Method ID Consistency

assert_eq!(prev_proof.method_id, input.method_id, 
    "Method ID mismatch, the input method ID must match the previous proof's method ID");
guest.verify(input.method_id, &prev_proof);
Ensures the same circuit version is used throughout the chain, preventing proof mixing from incompatible network types.

2. Chain Continuity

assert_eq!(
    block_header.prev_block_hash, self.best_block_hash,
    "Previous block hash does not match the best block hash"
);
Verifies each block’s prev_block_hash field matches the hash of the previous block, ensuring no chain breaks.

3. Proof-of-Work Validation

let new_block_hash = block_header.compute_block_hash();
check_hash_valid(&new_block_hash, &target_to_use);
The circuit:
  1. Computes the double SHA-256 hash of the block header
  2. Converts the hash to a 256-bit integer (little-endian to big-endian)
  3. Verifies the hash is less than or equal to the difficulty target
pub fn compute_block_hash(&self) -> [u8; 32] {
    let mut hasher = Sha256::new();
    hasher.update(self.version.to_le_bytes());
    hasher.update(self.prev_block_hash);
    hasher.update(self.merkle_root);
    hasher.update(self.time.to_le_bytes());
    hasher.update(self.bits.to_le_bytes());
    hasher.update(self.nonce.to_le_bytes());
    let first_hash_result = hasher.finalize_reset();
    
    hasher.update(first_hash_result);
    let result: [u8; 32] = hasher.finalize().into();
    result
}
Bitcoin uses double SHA-256 (SHA-256(SHA-256(header))) for all block hashes.

4. Difficulty Adjustment

Bitcoin adjusts difficulty every 2016 blocks to maintain ~10 minute block times:
if !IS_REGTEST && self.block_height % BLOCKS_PER_EPOCH == BLOCKS_PER_EPOCH - 1 {
    current_target_bytes = calculate_new_difficulty(
        self.epoch_start_time,
        block_header.time,
        self.current_target_bits,
    );
    self.current_target_bits = target_to_bits(&current_target_bytes);
}
Adjustment Algorithm:
  1. Calculate actual timespan: last_block_time - epoch_start_time
  2. Clamp to [expected/4, expected*4] to limit adjustment range
  3. New target = old target × actual_timespan / expected_timespan
  4. Ensure new target doesn’t exceed network maximum
Mainnet
  • Standard 2-week adjustment (1,209,600 seconds)
  • Maximum target: 0x1D00FFFF
  • Adjustment range: ±75% per period
Testnet4
  • Standard rules plus emergency difficulty reduction
  • If block time > 20 minutes after previous block, maximum difficulty allowed
  • Prevents network stalls during low hash rate periods
Signet
  • Custom 10-second block time
  • Adjusted epoch timespan: 20,160 seconds
  • Maximum target: 0x1E0377AE
Regtest
  • Always uses maximum difficulty: 0x207FFFFF
  • No difficulty adjustments
  • For local testing only

5. Timestamp Validation

if !validate_timestamp(block_header.time, self.prev_11_timestamps) {
    panic!("Timestamp is not valid, it must be greater than the median of the last 11 timestamps");
}
Implements Bitcoin’s Median Time Past (MTP) rule:
  • Block timestamp must be strictly greater than the median of the previous 11 blocks
  • Prevents miners from manipulating timestamps to affect difficulty
  • The median is calculated by sorting the 11 timestamps and selecting the 6th value

6. MMR Integrity

self.block_hashes_mmr.append(new_block_hash);
Maintains a Merkle Mountain Range (MMR) for efficient block hash storage and verification:
  • Stores only subroots instead of all block hashes
  • Enables compact inclusion proofs for any historical block
  • Used by Bridge Circuit for SPV verification

Merkle Mountain Range (MMR)

The circuit uses two MMR implementations for different contexts:

MMR Guest (zkVM)

circuits-lib/src/header_chain/mmr_guest.rs Designed for constrained zkVM environment:
  • Efficiently verifies inclusion proofs without storing entire chain
  • Confirms a block hash is part of the verified chain
  • Optimized for proof generation inside zero-knowledge circuits

MMR Native (Host)

circuits-lib/src/header_chain/mmr_native.rs Used for off-chain data preparation:
  • Builds MMR from sequences of block headers
  • Generates inclusion proofs for specific blocks
  • Proofs are passed to MMRGuest for verification in zkVM
A Merkle Mountain Range is a binary tree-like structure optimized for append-only operations:
Height 2:          7
                  / \
Height 1:        3   6   
                / \ / \
Height 0:      1 2 4 5 8
  • Each number represents a subroot
  • Only the peaks (rightmost nodes at each height) need to be stored
  • New blocks are appended efficiently without rebuilding the entire tree
  • Inclusion proofs consist of the path from leaf to a peak

Chain State

The circuit maintains a ChainState struct with all data needed for the next block verification:
pub struct ChainState {
    pub block_height: u32,
    pub total_work: [u8; 32],
    pub best_block_hash: [u8; 32],
    pub current_target_bits: u32,
    pub epoch_start_time: u32,
    pub prev_11_timestamps: [u32; 11],
    pub block_hashes_mmr: MMRGuest,
}
Field Descriptions:
  • block_height: Current chain height (u32::MAX for uninitialized)
  • total_work: Cumulative proof-of-work as 256-bit big-endian integer
  • best_block_hash: Hash of most recently validated block
  • current_target_bits: Current difficulty in compact representation
  • epoch_start_time: Timestamp of first block in current difficulty epoch
  • prev_11_timestamps: Rolling array of previous 11 block timestamps for MTP validation
  • block_hashes_mmr: Merkle Mountain Range of all verified block hashes

Work Calculation

Accumulated work quantifies the computational effort required to produce the chain:
fn calculate_work(target: &[u8; 32]) -> U256 {
    let target = U256::from_be_slice(target);
    if target == U256::ZERO {
        return U256::MAX;
    }
    if target == U256::ONE {
        return U256::MAX;
    }
    if target == U256::MAX {
        return U256::ONE;
    }

    let comp = !target;
    let ret = comp.wrapping_div(&target.wrapping_add(&U256::ONE));
    ret.wrapping_add(&U256::ONE)
}
Follows Bitcoin Core’s formula: work = 2^256 / (target + 1) Using mathematical identity: 2^256 / (x + 1) == ~x / (x + 1) + 1 Lower targets (higher difficulty) produce more work per block.

Recursive Proving

The circuit supports recursive verification for arbitrary chain lengths:
let mut chain_state = match input.prev_proof {
    HeaderChainPrevProofType::GenesisBlock(genesis_state) => {
        genesis_state_hash = genesis_state.to_hash();
        genesis_state
    }
    HeaderChainPrevProofType::PrevProof(prev_proof) => {
        guest.verify(input.method_id, &prev_proof);
        genesis_state_hash = prev_proof.genesis_state_hash;
        prev_proof.chain_state
    }
};
1

Genesis State

First proof starts from a known genesis chain state (genesis block or checkpoint)
2

Block Processing

Circuit processes a batch of block headers and outputs updated chain state
3

Proof Verification

Next circuit run verifies the previous proof before processing more headers
4

State Continuation

Chain state from previous proof becomes starting point for new batch
5

Genesis Hash Preservation

The original genesis state hash is carried through all proofs
This enables verification of millions of blocks without proving all blocks in a single circuit execution.

Circuit Input/Output

Input Structure

pub struct HeaderChainCircuitInput {
    pub method_id: [u32; 8],
    pub prev_proof: HeaderChainPrevProofType,
    pub block_headers: Vec<CircuitBlockHeader>,
}

pub enum HeaderChainPrevProofType {
    GenesisBlock(ChainState),
    PrevProof(BlockHeaderCircuitOutput),
}

Output Structure

pub struct BlockHeaderCircuitOutput {
    pub method_id: [u32; 8],
    pub genesis_state_hash: [u8; 32],
    pub chain_state: ChainState,
}
The output serves as input to the next proof run, enabling chain composition.

Key Implementation Files

RISC Zero Guest

risc0-circuits/header-chain/guest/src/main.rs
fn main() {
    let zkvm_guest = circuits_lib::common::zkvm::Risc0Guest::new();
    circuits_lib::header_chain::header_chain_circuit(&zkvm_guest);
}
Minimal entry point that initializes the zkVM guest environment and invokes the core circuit logic.

Core Circuit Logic

circuits-lib/src/header_chain/mod.rs The main verification function at mod.rs:69-95:
  1. Reads input from host (line 71)
  2. Verifies previous proof or initializes from genesis (lines 73-84)
  3. Applies block headers to chain state (line 87)
  4. Commits output (lines 90-94)
The apply_block_headers method at mod.rs:403-510 performs all validation logic for each header.

Build Configuration

risc0-circuits/header-chain/build.rs The build script:
  • Compiles header-chain-guest into RISC Zero ELF binary
  • Computes unique method ID for the compiled program
  • Handles BITCOIN_NETWORK environment variable
  • Optionally uses Docker for reproducible guest builds
  • Copies generated ELF to elfs folder

Network Constants

The circuit adapts verification rules based on the Bitcoin network:
pub const NETWORK_CONSTANTS: NetworkConstants = {
    match option_env!("BITCOIN_NETWORK") {
        Some(n) if matches!(n.as_bytes(), b"mainnet") => NetworkConstants {
            max_bits: 0x1D00FFFF,
            max_target: U256::from_be_hex(
                "00000000FFFF0000000000000000000000000000000000000000000000000000"
            ),
            // ...
        },
        // ... other networks
    }
};
Compile-time configuration ensures the circuit enforces correct consensus rules for each network.

Usage in Bridge Circuit

The Bridge Circuit consumes Header Chain Proofs to:
  1. Verify the Operator is following the canonical Bitcoin chain
  2. Extract the MMR for SPV proof verification
  3. Compare Operator’s total_work against Watchtower challenges
  4. Ensure payout blocks are part of the verified chain
See Bridge Circuit for integration details.

Next Steps

Work Only Circuit

Learn how proof-of-work is extracted for challenges

Bridge Circuit

See how header chain proofs enable secure peg-outs

Build docs developers (and LLMs) love