Blockchain Consensus Mechanisms

How distributed networks — with no central authority and potentially many malicious participants — agree on a single version of truth. From the Byzantine Generals Problem to Proof of Work, Proof of Stake, and modern BFT protocols.

Prereq: basic cryptography, distributed systems concepts Time to read: ~35 min Interactive figures: 1

1. The Byzantine Generals Problem

Before blockchains existed, computer scientists had already identified the hardest version of the distributed consensus problem. In 1982, Leslie Lamport, Robert Shostak, and Marshall Pease published a paper that would become one of the most-cited in distributed systems: The Byzantine Generals Problem.

The scenario: several Byzantine army generals have surrounded an enemy city. They must agree on a coordinated attack or retreat — acting independently guarantees defeat. They communicate only by messenger. Some generals are traitors who will send conflicting or false messages to sabotage coordination. The question: can the loyal generals always reach agreement?

FORMAL RESULT

A distributed system can reach Byzantine fault-tolerant consensus if and only if the number of total nodes $n$ satisfies $n > 3f$, where $f$ is the maximum number of Byzantine (arbitrarily malicious) nodes. Equivalently, you need more than two-thirds of participants to be honest.

Why $3f + 1$? With only $3f$ nodes and $f$ traitors, the $f$ loyal nodes in one partition cannot distinguish the $f$ silent/lying nodes from the $f$ legitimate nodes in another partition — there aren't enough honest nodes to outvote the traitors in every scenario. The proof involves constructing scenarios where $n \le 3f$ leads to irreconcilable views.

Oral vs. Signed Messages

Lamport et al. distinguish two variants. With oral messages (unauthenticated): $n > 3f$ required. With signed messages (digital signatures): $n > f + 2$ suffices — cryptographic signatures let honest nodes prove what they received, making it much harder for traitors to lie undetected. All modern blockchain protocols use the signed-message model.

The FLP Impossibility Theorem

In 1985, Fischer, Lynch, and Paterson proved a devastating result: in a fully asynchronous network (where messages can be delayed arbitrarily), no deterministic protocol can guarantee both safety (all nodes agree on the same value) and liveness (the protocol eventually terminates) in the presence of even a single crash failure.

This is not a problem to be engineered around — it is a mathematical impossibility. Every consensus protocol must therefore make a choice:

Satoshi's insight in the Bitcoin whitepaper was elegant: sidestep identity-based voting entirely. In an open internet, identities are free — an attacker can create a billion fake accounts (Sybil attack). Replace identity with scarce physical resource: compute power. One CPU cycle = one vote. You can't fake electricity.

2. Nakamoto Consensus (Proof of Work)

The Bitcoin whitepaper (2008) describes a system where nodes compete to extend a shared ledger by solving a computational puzzle. The puzzle is deliberately hard to solve but trivial to verify — an asymmetry that sits at the heart of the entire design.

The Hash Puzzle

Each block contains a nonce $N$. Miners must find an $N$ such that the double-SHA-256 hash of the block header falls below a target $T$:

$$\text{SHA256}\!\left(\text{SHA256}(\text{blockheader} \| N)\right) \;<\; T$$

SHA-256 produces a 256-bit output, effectively uniformly distributed. The probability that any single hash attempt satisfies the condition is:

$$p = \frac{T}{2^{256}}$$

At Bitcoin's 2024 difficulty (~50 trillion hashes per block), $p \approx 1.4 \times 10^{-23}$, requiring on average $7 \times 10^{22}$ hashes to find one valid block. The entire network produces roughly $6 \times 10^{20}$ hashes per second (600 EH/s), so blocks arrive every ~10 minutes.

Key Properties of PoW

Progress-free
Each hash attempt is independent. The number of additional attempts needed is always geometrically distributed regardless of how many you've already tried. No miner builds an "advantage" by mining longer — it's a pure lottery each attempt.
Unpredictable
SHA-256 is a one-way function — there is no shortcut to find a preimage. Miners must genuinely expend computation; they cannot predict or pre-compute valid nonces.
Asymmetric
Finding a valid nonce requires $O(1/p)$ work. Verifying a claimed nonce requires exactly two SHA-256 calls — $O(1)$. This asymmetry is what makes trustless verification cheap.
Sybil-resistant
Creating additional identities (nodes) does not grant additional voting power. Only real compute does. A miner with 10 machines has 10× the vote — they cannot amplify influence through fake accounts.

Probabilistic Finality

Unlike BFT protocols, Bitcoin does not produce deterministic finality. After a transaction is included in a block, it can still be reversed if an attacker mines a longer competing chain. The probability of a reversal decreases exponentially with the number of confirmations $k$ built on top. For an attacker with fraction $q$ of hash rate, the probability of catching up from $k$ blocks behind is:

$$P(\text{catch up}) \approx \left(\frac{q}{1-q}\right)^k \quad \text{for } q < 0.5$$

For $q = 0.1$ and $k = 6$: $P \approx 0.001\%$. Six confirmations is roughly 1 hour and is considered "settled" for most purposes.

3. Mining Mechanics and Difficulty Adjustment

Block Header Structure

A Bitcoin block header is exactly 80 bytes. The miner iterates over the nonce field trying to find a valid hash:

version 4 B prev_hash 32 B merkle_root 32 B time 4 B bits 4 B nonce 4 B ← iterate (4B = 4 billion tries)
Bitcoin block header: 80 bytes total. The nonce is the only field miners iterate over. Once all $2^{32}$ nonces are exhausted, miners change the extraNonce in the coinbase transaction (which changes the Merkle root), effectively resetting the nonce space.

The Nonce Exhaustion Problem

The nonce is only 32 bits — $2^{32} \approx 4$ billion values. At modern ASIC speeds (~100 TH/s per chip), a single miner exhausts all nonces in ~0.04 milliseconds. This is far too fast. Miners therefore also modify the extraNonce — a field in the coinbase (first) transaction of each block. Changing the coinbase changes the Merkle root, which changes the effective search space. The real search is over (nonce, extraNonce) pairs — a $2^{64}$ space more than sufficient.

Difficulty Adjustment

Bitcoin's target block time is 10 minutes. As mining hardware improves and more miners join, the network hash rate increases — blocks would arrive faster without adjustment. Every 2016 blocks (~2 weeks), the protocol recalculates the target:

$$T_{\text{new}} = T_{\text{old}} \times \frac{\text{actual time for last 2016 blocks}}{2016 \times 10\ \text{min}}$$

The adjustment is clamped to prevent runaway changes: it cannot increase or decrease by more than a factor of 4 in a single period:

$$T_{\text{new}} = \text{clamp}\!\left(T_{\text{old}} \times \frac{t_{\text{actual}}}{t_{\text{target}}},\ \frac{T_{\text{old}}}{4},\ 4 \cdot T_{\text{old}}\right)$$
Worked example

Suppose the last 2016 blocks took 12 days instead of the target 14 days. The network was mining 14/12 ≈ 1.167× too fast.

New difficulty = old difficulty × (14/12) ≈ old × 1.167

Equivalently, the target decreases by that factor (harder puzzle). If old difficulty was 50T, new difficulty = 58.3T.

The clamping ensures that a sudden 99% drop in hash rate (e.g., China mining ban) never causes more than a 4× difficulty decrease per period.

This elegant feedback loop is one of Bitcoin's most underappreciated features. It ensures block time converges to 10 minutes regardless of how much hardware joins or leaves the network — the protocol is self-regulating.

4. The Longest-Chain Rule and 51% Attacks

Fork Resolution

Two miners occasionally find valid blocks at nearly the same time, creating a fork: two valid chains of equal length. Nodes temporarily hold both chains. The fork resolves when one chain receives a new block — it is now longer (has more cumulative proof-of-work), so all honest nodes switch to it. The other chain's blocks become orphans and their transactions return to the mempool.

More precisely, Bitcoin uses total accumulated work (not just chain length) to select the canonical chain. This matters if different blocks have different difficulties. The canonical chain is the one with the highest cumulative $\sum 1/T_i$ across all its blocks.

#100 #101 #102A #103A #104A #102B #103B orphaned #105 #106 longer chain wins Chain A is longer (4 blocks) — all honest nodes switch to it. Block 102B/103B are orphaned; miners lose their reward.

Selfish Mining

In 2013, Eyal and Sirer showed that rational miners need not always publish blocks immediately. Selfish mining: an attacker with $\alpha > 1/3$ hash rate can earn disproportionate rewards by:

  1. Finding a block, but not publishing it — building a private chain
  2. If the honest network finds a block, immediately publish the private block to create a tie, then publish a second private block to win
  3. Orphan honest miners' blocks, wasting their work while the attacker's blocks get into the chain

Selfish mining doesn't require 51% — the threshold is approximately 33% of hash rate. It's an attack on miner revenue fairness, not on transaction integrity. The fix requires honest nodes to propagate blocks faster (Bitcoin Core's fast-relay protocol helps).

51% Attack Mechanics

An attacker controlling a majority of the network's hash rate can:

Double-spend
Send coins to a merchant on the public chain. Mine a secret chain where those coins were sent to yourself instead. After receiving goods, release the secret chain (which has more work) — honest nodes adopt it, the merchant's payment disappears.
Transaction censorship
Refuse to include certain addresses' transactions in any block the attacker mines, effectively freezing those funds.
NOT: coin creation
The attacker cannot create coins out of thin air. Block rewards are consensus rules; nodes reject blocks with invalid coinbase amounts. The attacker cannot steal funds protected by a private key — signatures are cryptographically secure.
ECONOMIC DETERRENCE

Executing a 51% attack on Bitcoin in 2024 requires renting or purchasing sufficient ASIC hardware. Estimates suggest the marginal cost exceeds $5 billion per hour of sustained attack. Even if successful, the attack would likely crash Bitcoin's price — making the attacker's own coins (and any stolen funds) worth far less. The attack is economically irrational for large, established PoW chains. It remains a real threat for smaller networks (e.g., Bitcoin Gold and Ethereum Classic have both suffered 51% attacks).

5. Proof of Stake

Proof of Stake replaces physical resource expenditure (electricity) with financial stake (locked collateral). Instead of competing on compute, validators are chosen to propose and attest to blocks in proportion to their staked tokens. Misbehavior is punished by slashing — destroying a portion of the validator's stake.

Core Intuition

In PoW, an attacker must acquire 51% of hash power — an external, physical resource. In PoS, an attacker must acquire 51% of staked tokens. The key insight: if you own 51% of the staked ETH, you have 51% of the chain's economic value at risk. Attacking the chain would destroy that value. Honest participation earns staking rewards; attacks cost you your stake. The system is cryptoeconomically secure.

Ethereum's Casper FFG + LMD-GHOST

Ethereum's consensus after "The Merge" (2022) combines two mechanisms:

Slots & Epochs
Time is divided into 12-second slots. Every 32 slots (6.4 minutes) is an epoch. Each slot has a designated block proposer; each epoch reshuffles validators into committees.
LMD-GHOST
Latest Message Driven Greediest Heaviest Observed SubTree. The fork-choice rule: at each branching point, choose the subtree with the most accumulated validator weight (from their latest attesting messages). This is PoS's analog of longest-chain.
Casper FFG
Friendly Finality Gadget. Overlaid on top of LMD-GHOST, Casper provides finality. A checkpoint (the first block of each epoch) is finalized when two-thirds of total staked ETH has voted for it in two consecutive rounds. Once finalized, it cannot be reverted without slashing at least $\frac{1}{3}$ of all staked ETH.

Slashing Conditions

Validators can be slashed for two categories of misbehavior:

The slashing penalty is initially 1/32 of stake, but scales with the fraction of validators slashed simultaneously — if many validators are slashed at once (suggesting a coordinated attack), the penalty can reach 100% of stake. This correlation penalty specifically punishes organized attacks while being lenient on honest mistakes.

Validator Economics

As of 2024, ~$35B of ETH is staked. To disrupt finality requires controlling 33% of that; to rewrite history requires 51%. The cost of attacking is thus measured in tens of billions of dollars — comparable to the energy cost of attacking Bitcoin, but with the additional property that the attack is detectable and the attacker's stake can be burned.

$$\text{Annual staking reward} \approx \frac{C}{\sqrt{N_{\text{validators}}}}$$

where $C$ is a constant and $N_{\text{validators}}$ is the number of active validators. As more ETH is staked, per-validator rewards decrease — a natural equilibrium mechanism.

Deposited 32 ETH staked Pending in activation queue Active attesting & proposing Exiting voluntary exit Slashed double vote / surround Withdrawn wait activate voluntary equivocation Ethereum PoS Validator Lifecycle

6. BFT-Based Consensus

Byzantine Fault Tolerant protocols trade decentralization for performance. They use explicit voting rounds with a known, bounded validator set, achieving deterministic finality — once a block is committed, it is final, no probabilistic waiting required. They are the consensus engine of choice for permissioned blockchains and modern high-performance L1s.

PBFT (Practical Byzantine Fault Tolerance)

Castro and Liskov's 1999 paper showed that BFT consensus is practical for small validator sets. PBFT operates in three phases per consensus round:

Primary Replica 1 Replica 2 Replica 3 PRE-PREPARE (O(n)) PREPARE (O(n²)) COMMIT (O(n²)) COMMITTED
PBFT three-phase protocol. The primary broadcasts a PRE-PREPARE. All replicas broadcast PREPARE messages to each other. Once a node collects 2f+1 PREPARE messages, it enters COMMIT phase. After 2f+1 COMMIT messages, the block is finalized.

The message complexity is $O(n^2)$ — each of the $n$ nodes sends messages to all other $n$ nodes in the PREPARE and COMMIT phases. This is fine for $n \le 100$ but becomes untenable at larger scales (~10,000 validators). PBFT also requires a known, static validator set, making it unsuitable for permissionless blockchains.

Primary Replica 1 Replica 2 Replica 3 ①PRE-PREPARE ②PREPARE ③COMMIT ✓ commit ✓ commit ✓ commit ✓ commit O(n²) messages per round — practical for n ≤ ~100 validators

Tendermint

Tendermint (2014, Jae Kwon) adapts PBFT for blockchain contexts. It is used by Cosmos, Binance Chain, and dozens of application-specific chains. Key differences from PBFT:

Tendermint provides instant deterministic finality but sacrifices liveness under network partitions (it halts rather than fork). Communication is still $O(n^2)$, limiting practical validator sets to ~150 validators for reasonable latency.

HotStuff

HotStuff (2019, Yin et al., used in Facebook's Diem/Libra project and now Aptos/Sui) achieves a major breakthrough: linear communication complexity $O(n)$ per consensus round, using threshold signatures.

Instead of each validator broadcasting to all others, all validators send their votes to the current leader. The leader aggregates votes into a single Quorum Certificate (QC) using threshold BLS signatures — a compact proof that $\ge \frac{2}{3}$ of validators approved a block.

THE 3-CHAIN COMMITMENT RULE

HotStuff commits a block $b$ only when it is followed by a chain of 3 consecutive blocks, each carrying a QC for the previous one: $b \leftarrow b' \leftarrow b''$. Once block $b''$ with QC for $b'$ with QC for $b$ exists, block $b$ is final. This 3-round pipeline enables responsive throughput: the leader can propose a new block every round without waiting for explicit commit acknowledgment.

The pipeline means HotStuff is always working on three blocks simultaneously — propose, vote, decide — giving it throughput comparable to a non-BFT system. HotStuff-2 (2023) further reduces the number of phases, and variants like DiemBFT, Jolteon, and Bullshark extend the design.

7. Comparative Analysis

THE BLOCKCHAIN TRILEMMA

No consensus system perfectly achieves all three of decentralization (permissionless, many validators), security (resistant to attack), and scalability (high throughput, low latency). Every design is a tradeoff. PoW maximizes decentralization and security at the cost of scalability and energy. BFT maximizes throughput and finality at the cost of permissionlessness. PoS attempts a middle ground.

Property Proof of Work Proof of Stake BFT (Tendermint / HotStuff)
Energy consumption Enormous (~150 TWh/yr for Bitcoin) ~99.95% less than PoW Minimal (just CPU/network)
Finality Probabilistic (grows with confirmations) Probabilistic until checkpoint; then deterministic (~12 min) Deterministic (single round)
Sybil resistance Hash power (physical hardware) Staked capital (economic) Identity + stake (permissioned or delegated)
Validator set Fully open (anyone with hardware) Open (anyone with 32 ETH for Ethereum) Bounded set (~150 for Tendermint, ~100 for HotStuff)
Message complexity $O(1)$ per block (broadcast) $O(n)$ attestations per slot $O(n^2)$ PBFT/Tendermint; $O(n)$ HotStuff
Throughput ~7 TPS (Bitcoin) ~15 TPS L1 (Ethereum); 1000s with rollups 1,000–100,000 TPS depending on implementation
Key attack vector 51% hash power, selfish mining 33%/51% stake, long-range attack 33% Byzantine validators, liveness failure under partition
Liveness under partition Yes (always produces blocks) Yes (LMD-GHOST; delayed finality) No (halts rather than forks — CP not AP)
Examples Bitcoin, Litecoin, Monero Ethereum 2.0, Solana (PoS+PoH), Cardano Cosmos (Tendermint), Aptos/Sui (HotStuff variants)
Finality: Probabilistic (PoW) vs Deterministic (BFT/PoS) Proof of Work — Probabilistic P(reversal) 0 1 2 3 6 confirmations 6 confs ≈ 60 min ≈ safe for large txs BFT / PoS — Deterministic propose prevote precommit FINAL ✓ ≥⅔ precommit → irreversible in 1 block Tendermint: ~1-6 sec. Casper: 12.8 min epoch

Long-Range Attacks on PoS

One vulnerability unique to PoS: the long-range attack. An attacker who once held stake (even years ago) could use those old private keys to re-mine history from that point — since there's no computational cost to creating PoS blocks. Defenses include weak subjectivity checkpoints (new nodes must obtain a recent trusted checkpoint from a social source, not just the longest chain) and key evolving schemes (validators regularly rotate keys, invalidating old ones). Ethereum requires nodes syncing from scratch to use a checkpoint no older than the weak subjectivity period (~2 weeks).

8. Interactive: Block Mining Simulator

This simulator demonstrates the core PoW puzzle using a simplified hash function (djb2 applied iteratively — not SHA-256, but captures the same statistical behavior). Set the difficulty (number of leading zero hex digits required), then click Mine to watch the nonce search in real time.

BLOCK HEADER
prev_hash:
0x00000abc...def1
merkle_root:
0xdeadbeef...cafe
nonce:
0
2
NONCE TRIED HASH OUTPUT (simplified djb2) PASS? Ready. Set difficulty and click Mine!
ATTEMPTS
0
TIME (ms)
RESULT

Note: This uses djb2 hashing (not SHA-256) for browser speed. The statistical properties (random-looking output, geometric distribution of success) are identical to real PoW. At difficulty 4 (four leading zeros), expect ~65,536 average attempts.

9. Source Code

Source — Proof of Work, Difficulty Adjustment, Longest Chain, BFT Round, Merkle Validation
// ── 1. PoW Mining Loop ─────────────────────────────────────────────────
function mine(block_header, target):
    nonce = 0
    loop:
        hash = SHA256(SHA256(block_header || nonce))
        if hash < target:
            return nonce, hash
        nonce += 1
        if nonce > MAX_NONCE:
            increment extraNonce in coinbase tx   // changes merkle_root
            nonce = 0

// ── 2. Difficulty Adjustment ───────────────────────────────────────────
function adjust_difficulty(old_target, actual_time_seconds):
    TARGET_TIME = 2016 * 600   // 2016 blocks × 10 min each = 1,209,600 s
    ratio = actual_time_seconds / TARGET_TIME
    ratio = clamp(ratio, 0.25, 4.0)   // max 4x change per period
    return old_target * ratio

// ── 3. Longest-Chain Selection ─────────────────────────────────────────
function best_chain(chains[]):
    // chains is a list of chain tip blocks
    best = None
    best_work = 0
    for tip in chains:
        total_work = sum(1 / block.target for block in chain(tip))
        if total_work > best_work:
            best = tip
            best_work = total_work
    return best

// ── 4. Simple BFT Round (Tendermint-style) ─────────────────────────────
function bft_round(validators, proposed_block):
    // Phase 1: Prevote
    prevotes = collect_votes(validators, proposed_block, "PREVOTE")
    if count_weighted(prevotes) < 2/3 * total_stake:
        return nil   // no block this round, increment round number

    // Phase 2: Precommit
    precommits = collect_votes(validators, proposed_block, "PRECOMMIT")
    if count_weighted(precommits) >= 2/3 * total_stake:
        return finalize(proposed_block)
    return nil

// ── 5. Merkle Block Validation ─────────────────────────────────────────
function verify_merkle_proof(tx_hash, proof_path, merkle_root):
    current = tx_hash
    for (sibling, is_right) in proof_path:
        if is_right:
            current = SHA256(SHA256(current || sibling))
        else:
            current = SHA256(SHA256(sibling || current))
    return current == merkle_root
import hashlib
import time
from typing import List, Tuple, Optional


# ── 1. PoW Mining Loop ─────────────────────────────────────────────────

def double_sha256(data: bytes) -> bytes:
    return hashlib.sha256(hashlib.sha256(data).digest()).digest()

def mine_block(header_bytes: bytes, target_bits: int) -> Tuple[int, bytes]:
    """
    Find nonce such that double_sha256(header || nonce) has target_bits leading zero bits.
    Returns (nonce, hash_bytes).
    """
    target = (1 << (256 - target_bits)) - 1
    nonce = 0
    while True:
        candidate = header_bytes + nonce.to_bytes(4, 'little')
        h = double_sha256(candidate)
        h_int = int.from_bytes(h, 'big')
        if h_int <= target:
            return nonce, h
        nonce = (nonce + 1) & 0xFFFFFFFF
        if nonce == 0:
            raise OverflowError("Exhausted 32-bit nonce space — change extraNonce")


# ── 2. Difficulty Adjustment ───────────────────────────────────────────

TARGET_TIMESPAN = 2016 * 600  # seconds (2 weeks)

def adjust_target(old_target: int, actual_timespan: int) -> int:
    """
    Recalculate PoW target after 2016 blocks.
    old_target: current target as 256-bit integer
    actual_timespan: seconds taken for last 2016 blocks
    Returns new target as 256-bit integer.
    """
    # Clamp timespan to [TARGET/4, TARGET*4]
    actual_timespan = max(TARGET_TIMESPAN // 4, min(actual_timespan, TARGET_TIMESPAN * 4))
    new_target = (old_target * actual_timespan) // TARGET_TIMESPAN
    # Enforce max target (genesis difficulty)
    MAX_TARGET = 0x00000000FFFF0000000000000000000000000000000000000000000000000000
    return min(new_target, MAX_TARGET)


# ── 3. Longest-Chain Selection ─────────────────────────────────────────

class Block:
    def __init__(self, height: int, prev_hash: str, work: int):
        self.height = height
        self.prev_hash = prev_hash
        self.work = work  # 1 / target for this block (as integer approximation)
        self.total_work = 0  # set when building chain

def compute_chain_work(tip: Block, blocks_by_hash: dict) -> int:
    """Walk backwards from tip summing work."""
    total = 0
    current = tip
    while current is not None:
        total += current.work
        current = blocks_by_hash.get(current.prev_hash)
    return total

def select_best_chain(tips: List[Block], blocks_by_hash: dict) -> Block:
    """Nakamoto consensus: pick chain with highest total work."""
    best_tip = None
    best_work = -1
    for tip in tips:
        work = compute_chain_work(tip, blocks_by_hash)
        if work > best_work:
            best_work = work
            best_tip = tip
    return best_tip


# ── 4. Simple BFT Round (Tendermint-style) ─────────────────────────────

def tendermint_round(
    validators: List[dict],
    proposed_block_hash: str,
    round_number: int
) -> Optional[str]:
    """
    Simplified single-round Tendermint simulation.
    validators: list of {'id': str, 'stake': int, 'honest': bool}
    Returns committed block hash or None if round fails.
    """
    total_stake = sum(v['stake'] for v in validators)
    quorum = (2 * total_stake) // 3 + 1

    # Prevote phase
    prevote_weight = sum(
        v['stake'] for v in validators
        if v['honest']  # honest validators prevote for valid proposal
    )
    if prevote_weight < quorum:
        print(f"Round {round_number}: Prevote failed — insufficient honest stake")
        return None

    # Precommit phase
    precommit_weight = prevote_weight  # simplification: same honest validators precommit
    if precommit_weight >= quorum:
        print(f"Round {round_number}: Block {proposed_block_hash[:8]}... committed!")
        return proposed_block_hash

    return None


# ── 5. Merkle Proof Validation ─────────────────────────────────────────

def merkle_hash(a: bytes, b: bytes) -> bytes:
    return double_sha256(a + b)

def verify_merkle_proof(
    tx_hash: bytes,
    proof: List[Tuple[bytes, bool]],  # (sibling_hash, sibling_is_on_right)
    merkle_root: bytes
) -> bool:
    """
    Verify that tx_hash is included in the Merkle tree with given root.
    proof: list of (sibling, is_right) pairs from leaf to root.
    """
    current = tx_hash
    for sibling, sibling_is_right in proof:
        if sibling_is_right:
            current = merkle_hash(current, sibling)
        else:
            current = merkle_hash(sibling, current)
    return current == merkle_root


# ── Example usage ──────────────────────────────────────────────────────
if __name__ == "__main__":
    # Mine a toy block (difficulty 18 bits → ~262k expected hashes)
    header = b"version=1|prev=abc|merkle=def|time=1700000000|bits=18"
    start = time.time()
    nonce, found_hash = mine_block(header, target_bits=18)
    elapsed = time.time() - start
    print(f"Found nonce={nonce}, hash={found_hash.hex()[:16]}..., time={elapsed:.3f}s")

    # Difficulty adjustment example
    old_target = 0x00000000FFFF << (256 - 32)  # toy target
    new_target = adjust_target(old_target, actual_timespan=12 * 24 * 3600)  # 12 days
    print(f"Target changed by factor {new_target / old_target:.4f} (expected ~1.167)")
// ── 1. PoW Mining (using Web Crypto SHA-256) ───────────────────────────

async function doubleSHA256(data) {
  const first  = await crypto.subtle.digest('SHA-256', data);
  const second = await crypto.subtle.digest('SHA-256', first);
  return new Uint8Array(second);
}

async function mineBlock(headerBytes, leadingZeroBits) {
  const nonce = new Uint8Array(4);
  const view  = new DataView(nonce.buffer);
  let attempts = 0;

  while (true) {
    const candidate = new Uint8Array(headerBytes.length + 4);
    candidate.set(headerBytes);
    candidate.set(nonce, headerBytes.length);

    const hash = await doubleSHA256(candidate);
    if (hasLeadingZeroBits(hash, leadingZeroBits)) {
      return { nonce: view.getUint32(0, true), hash, attempts };
    }
    const n = view.getUint32(0, true);
    view.setUint32(0, (n + 1) >>> 0, true);
    attempts++;
  }
}

function hasLeadingZeroBits(hashBytes, bits) {
  let remaining = bits;
  for (const byte of hashBytes) {
    if (remaining <= 0) break;
    if (remaining >= 8) {
      if (byte !== 0) return false;
      remaining -= 8;
    } else {
      const mask = 0xFF << (8 - remaining);
      if ((byte & mask) !== 0) return false;
      remaining = 0;
    }
  }
  return true;
}


// ── 2. Difficulty Adjustment ───────────────────────────────────────────

const TARGET_TIMESPAN = 2016 * 600; // 2 weeks in seconds

function adjustTarget(oldTargetHex, actualTimespanSec) {
  const oldTarget = BigInt('0x' + oldTargetHex);
  let timespan = BigInt(Math.max(
    TARGET_TIMESPAN / 4,
    Math.min(actualTimespanSec, TARGET_TIMESPAN * 4)
  ));
  const newTarget = (oldTarget * timespan) / BigInt(TARGET_TIMESPAN);
  return newTarget.toString(16).padStart(64, '0');
}


// ── 3. Longest-Chain Selection ─────────────────────────────────────────

function selectBestChain(tips, blockMap) {
  // tips: array of block hashes; blockMap: hash -> {work, prevHash}
  let bestTip = null, bestWork = 0n;

  for (const tip of tips) {
    let totalWork = 0n;
    let curr = tip;
    while (curr && blockMap[curr]) {
      totalWork += BigInt(blockMap[curr].work);
      curr = blockMap[curr].prevHash;
    }
    if (totalWork > bestWork) {
      bestWork = totalWork;
      bestTip = tip;
    }
  }
  return bestTip;
}


// ── 4. BFT Round Simulation ────────────────────────────────────────────

function tendermintRound(validators, proposedBlockHash) {
  const totalStake = validators.reduce((s, v) => s + v.stake, 0);
  const quorum = Math.floor(totalStake * 2 / 3) + 1;

  // Prevote: honest validators vote for valid block
  const prevoteWeight = validators
    .filter(v => v.honest)
    .reduce((s, v) => s + v.stake, 0);

  if (prevoteWeight < quorum) return null;

  // Precommit
  const precommitWeight = prevoteWeight; // same set in this simulation
  if (precommitWeight >= quorum) return proposedBlockHash;

  return null;
}


// ── 5. Merkle Proof Verification ───────────────────────────────────────

async function sha256(data) {
  const buf = await crypto.subtle.digest('SHA-256', data);
  return new Uint8Array(buf);
}

async function merkleHash(a, b) {
  const combined = new Uint8Array(a.length + b.length);
  combined.set(a); combined.set(b, a.length);
  const first = await sha256(combined);
  return sha256(first);
}

async function verifyMerkleProof(txHash, proof, merkleRoot) {
  // proof: [{sibling: Uint8Array, siblingIsRight: bool}]
  let current = txHash;
  for (const { sibling, siblingIsRight } of proof) {
    current = siblingIsRight
      ? await merkleHash(current, sibling)
      : await merkleHash(sibling, current);
  }
  return current.every((b, i) => b === merkleRoot[i]);
}
#include <stdint.h>
#include <string.h>
#include <stdio.h>
// Assumes sha256() writes 32 bytes to output buffer.
// (Use OpenSSL or a standalone SHA-256 implementation)
extern void sha256(const uint8_t *data, size_t len, uint8_t *out);

void double_sha256(const uint8_t *data, size_t len, uint8_t *out) {
    uint8_t tmp[32];
    sha256(data, len, tmp);
    sha256(tmp, 32, out);
}

/* ── PoW mining loop ─────────────────────────────────────────── */
int mine_block(const uint8_t *header, size_t header_len,
               int leading_zero_bytes, uint32_t *found_nonce)
{
    uint8_t buf[256], hash[32];
    memcpy(buf, header, header_len);

    for (uint32_t nonce = 0; ; nonce++) {
        uint32_t le = __builtin_bswap32(nonce);  // little-endian nonce
        memcpy(buf + header_len, &le, 4);
        double_sha256(buf, header_len + 4, hash);

        int ok = 1;
        for (int i = 0; i < leading_zero_bytes; i++) {
            if (hash[i] != 0) { ok = 0; break; }
        }
        if (ok) { *found_nonce = nonce; return 1; }
        if (nonce == 0xFFFFFFFF) return 0;  // exhausted
    }
}

/* ── Difficulty adjustment ───────────────────────────────────── */
#define TARGET_TIMESPAN (2016 * 600)  /* seconds */

void adjust_target(uint8_t target[32], int32_t actual_seconds) {
    /* clamp */
    if (actual_seconds < TARGET_TIMESPAN / 4) actual_seconds = TARGET_TIMESPAN / 4;
    if (actual_seconds > TARGET_TIMESPAN * 4) actual_seconds = TARGET_TIMESPAN * 4;

    /* multiply target by ratio using 256-bit big-endian arithmetic */
    /* (production code uses multi-precision integer library) */
    uint64_t ratio_num = (uint64_t)actual_seconds;
    uint64_t ratio_den = (uint64_t)TARGET_TIMESPAN;

    /* Simple 64-bit approximation using most-significant 8 bytes */
    uint64_t hi;
    memcpy(&hi, target, 8);
    hi = __builtin_bswap64(hi);
    hi = hi * ratio_num / ratio_den;
    hi = __builtin_bswap64(hi);
    memcpy(target, &hi, 8);
}

/* ── Merkle proof verification ───────────────────────────────── */
typedef struct { uint8_t hash[32]; int sibling_is_right; } ProofStep;

int verify_merkle_proof(const uint8_t *tx_hash, const ProofStep *proof,
                        int proof_len, const uint8_t *root)
{
    uint8_t current[32], combined[64], next[32];
    memcpy(current, tx_hash, 32);

    for (int i = 0; i < proof_len; i++) {
        if (proof[i].sibling_is_right) {
            memcpy(combined,      current,         32);
            memcpy(combined + 32, proof[i].hash,   32);
        } else {
            memcpy(combined,      proof[i].hash,   32);
            memcpy(combined + 32, current,         32);
        }
        double_sha256(combined, 64, next);
        memcpy(current, next, 32);
    }
    return memcmp(current, root, 32) == 0;
}
#include <array>
#include <cstdint>
#include <cstring>
#include <optional>
#include <vector>
#include <iostream>
// Requires a SHA-256 implementation (e.g., OpenSSL's EVP or standalone)
using Hash256 = std::array<uint8_t, 32>;
extern Hash256 sha256(const uint8_t* data, size_t len);

Hash256 double_sha256(const uint8_t* data, size_t len) {
    auto first = sha256(data, len);
    return sha256(first.data(), 32);
}

// ── PoW Mining ─────────────────────────────────────────────────────────
struct MineResult { uint32_t nonce; Hash256 hash; uint64_t attempts; };

std::optional<MineResult> mine(
    const std::vector<uint8_t>& header,
    int leading_zero_bytes)
{
    std::vector<uint8_t> buf(header.size() + 4);
    std::copy(header.begin(), header.end(), buf.begin());

    for (uint32_t nonce = 0; ; nonce++) {
        buf[header.size()    ] = nonce & 0xFF;
        buf[header.size() + 1] = (nonce >> 8)  & 0xFF;
        buf[header.size() + 2] = (nonce >> 16) & 0xFF;
        buf[header.size() + 3] = (nonce >> 24) & 0xFF;

        Hash256 h = double_sha256(buf.data(), buf.size());

        bool ok = true;
        for (int i = 0; i < leading_zero_bytes && ok; i++)
            ok = (h[i] == 0);

        if (ok) return MineResult{nonce, h, (uint64_t)nonce + 1};
        if (nonce == 0xFFFFFFFF) return std::nullopt;
    }
}

// ── Difficulty Adjustment ──────────────────────────────────────────────
constexpr int64_t TARGET_TIMESPAN = 2016LL * 600;

Hash256 adjust_target(Hash256 old_target, int64_t actual_seconds) {
    actual_seconds = std::max(TARGET_TIMESPAN / 4,
                     std::min(actual_seconds, TARGET_TIMESPAN * 4));
    // Big-integer multiply (simplified to leading 8 bytes here)
    Hash256 result = old_target;
    uint64_t hi = 0;
    for (int i = 0; i < 8; i++) hi = (hi << 8) | old_target[i];
    hi = static_cast<uint64_t>((__uint128_t)hi * actual_seconds / TARGET_TIMESPAN);
    for (int i = 7; i >= 0; i--) { result[i] = hi & 0xFF; hi >>= 8; }
    return result;
}

// ── Merkle Proof Verification ──────────────────────────────────────────
struct ProofStep { Hash256 sibling; bool sibling_is_right; };

bool verify_merkle_proof(
    const Hash256& tx_hash,
    const std::vector<ProofStep>& proof,
    const Hash256& root)
{
    Hash256 current = tx_hash;
    std::array<uint8_t, 64> combined;
    for (const auto& step : proof) {
        if (step.sibling_is_right) {
            std::copy(current.begin(), current.end(), combined.begin());
            std::copy(step.sibling.begin(), step.sibling.end(), combined.begin() + 32);
        } else {
            std::copy(step.sibling.begin(), step.sibling.end(), combined.begin());
            std::copy(current.begin(), current.end(), combined.begin() + 32);
        }
        current = double_sha256(combined.data(), 64);
    }
    return current == root;
}
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.List;

public class NakamotoConsensus {

    // ── SHA-256 helpers ────────────────────────────────────────────────
    static byte[] sha256(byte[] data) throws Exception {
        return MessageDigest.getInstance("SHA-256").digest(data);
    }

    static byte[] doubleSha256(byte[] data) throws Exception {
        return sha256(sha256(data));
    }

    // ── PoW Mining Loop ────────────────────────────────────────────────
    record MineResult(int nonce, byte[] hash, long attempts) {}

    static MineResult mine(byte[] header, int leadingZeroBytes) throws Exception {
        byte[] buf = Arrays.copyOf(header, header.length + 4);

        for (int nonce = 0; nonce != -1; nonce++) {
            buf[header.length    ] = (byte)  (nonce        & 0xFF);
            buf[header.length + 1] = (byte) ((nonce >>  8) & 0xFF);
            buf[header.length + 2] = (byte) ((nonce >> 16) & 0xFF);
            buf[header.length + 3] = (byte) ((nonce >> 24) & 0xFF);

            byte[] hash = doubleSha256(buf);
            boolean ok = true;
            for (int i = 0; i < leadingZeroBytes && ok; i++)
                ok = (hash[i] == 0);

            if (ok) return new MineResult(nonce, hash, (long)nonce + 1);
        }
        throw new RuntimeException("Nonce space exhausted — change extraNonce");
    }

    // ── Difficulty Adjustment ──────────────────────────────────────────
    static final long TARGET_TIMESPAN = 2016L * 600;

    static double adjustDifficulty(double oldDifficulty, long actualSeconds) {
        actualSeconds = Math.max(TARGET_TIMESPAN / 4, Math.min(actualSeconds, TARGET_TIMESPAN * 4));
        return oldDifficulty * TARGET_TIMESPAN / actualSeconds;
    }

    // ── Longest-Chain Selection ────────────────────────────────────────
    record BlockInfo(String hash, String prevHash, long work) {}

    static String selectBestChain(List<String> tips, java.util.Map<String, BlockInfo> blockMap) {
        String bestTip = null;
        long bestWork = 0;
        for (String tip : tips) {
            long totalWork = 0;
            String curr = tip;
            while (curr != null && blockMap.containsKey(curr)) {
                BlockInfo b = blockMap.get(curr);
                totalWork += b.work();
                curr = b.prevHash();
            }
            if (totalWork > bestWork) { bestWork = totalWork; bestTip = tip; }
        }
        return bestTip;
    }

    // ── Merkle Proof Verification ──────────────────────────────────────
    record ProofStep(byte[] sibling, boolean siblingIsRight) {}

    static boolean verifyMerkleProof(byte[] txHash, List<ProofStep> proof,
                                     byte[] root) throws Exception {
        byte[] current = txHash;
        for (ProofStep step : proof) {
            byte[] combined = new byte[64];
            if (step.siblingIsRight()) {
                System.arraycopy(current,       0, combined,  0, 32);
                System.arraycopy(step.sibling(), 0, combined, 32, 32);
            } else {
                System.arraycopy(step.sibling(), 0, combined,  0, 32);
                System.arraycopy(current,        0, combined, 32, 32);
            }
            current = doubleSha256(combined);
        }
        return Arrays.equals(current, root);
    }

    public static void main(String[] args) throws Exception {
        // Toy example: mine with 2 leading zero bytes (~65k expected attempts)
        byte[] header = "v1|prev=abc|merkle=def|time=1700000000".getBytes();
        var result = mine(header, 2);
        System.out.printf("Found nonce=%d after %d attempts, hash=%s%n",
            result.nonce(), result.attempts(),
            bytesToHex(result.hash()).substring(0, 16) + "...");

        double newDiff = adjustDifficulty(50e12, 12 * 24 * 3600L);
        System.out.printf("New difficulty after 12-day period: %.2fT%n", newDiff / 1e12);
    }

    static String bytesToHex(byte[] b) {
        StringBuilder sb = new StringBuilder();
        for (byte x : b) sb.append(String.format("%02x", x & 0xFF));
        return sb.toString();
    }
}
package consensus

import (
	"crypto/sha256"
	"encoding/binary"
	"encoding/hex"
	"errors"
	"math/big"
)

// ── SHA-256 helpers ────────────────────────────────────────────────────

func doubleSHA256(data []byte) [32]byte {
	first  := sha256.Sum256(data)
	return sha256.Sum256(first[:])
}

// ── PoW Mining ─────────────────────────────────────────────────────────

type MineResult struct {
	Nonce    uint32
	Hash     [32]byte
	Attempts uint64
}

func MineBlock(header []byte, leadingZeroBytes int) (MineResult, error) {
	buf := make([]byte, len(header)+4)
	copy(buf, header)

	for nonce := uint32(0); ; nonce++ {
		binary.LittleEndian.PutUint32(buf[len(header):], nonce)
		h := doubleSHA256(buf)

		ok := true
		for i := range leadingZeroBytes {
			if h[i] != 0 {
				ok = false
				break
			}
		}
		if ok {
			return MineResult{Nonce: nonce, Hash: h, Attempts: uint64(nonce) + 1}, nil
		}
		if nonce == 0xFFFFFFFF {
			return MineResult{}, errors.New("nonce space exhausted")
		}
	}
}

// ── Difficulty Adjustment ──────────────────────────────────────────────

const targetTimespan = int64(2016 * 600) // 2 weeks in seconds

func AdjustTarget(oldTarget *big.Int, actualSeconds int64) *big.Int {
	// Clamp
	if actualSeconds < targetTimespan/4 {
		actualSeconds = targetTimespan / 4
	} else if actualSeconds > targetTimespan*4 {
		actualSeconds = targetTimespan * 4
	}

	newTarget := new(big.Int).Mul(oldTarget, big.NewInt(actualSeconds))
	newTarget.Div(newTarget, big.NewInt(targetTimespan))
	return newTarget
}

// ── Longest-Chain Selection ────────────────────────────────────────────

type BlockRecord struct {
	PrevHash string
	Work     *big.Int // proportional to 1/target
}

func SelectBestChain(tips []string, blockMap map[string]BlockRecord) string {
	bestTip := ""
	bestWork := new(big.Int)

	for _, tip := range tips {
		totalWork := new(big.Int)
		curr := tip
		for {
			b, ok := blockMap[curr]
			if !ok {
				break
			}
			totalWork.Add(totalWork, b.Work)
			curr = b.PrevHash
		}
		if totalWork.Cmp(bestWork) > 0 {
			bestWork = totalWork
			bestTip = tip
		}
	}
	return bestTip
}

// ── Merkle Proof Verification ──────────────────────────────────────────

type ProofStep struct {
	Sibling        [32]byte
	SiblingIsRight bool
}

func VerifyMerkleProof(txHash [32]byte, proof []ProofStep, root [32]byte) bool {
	current := txHash
	for _, step := range proof {
		var combined [64]byte
		if step.SiblingIsRight {
			copy(combined[:32], current[:])
			copy(combined[32:], step.Sibling[:])
		} else {
			copy(combined[:32], step.Sibling[:])
			copy(combined[32:], current[:])
		}
		current = doubleSHA256(combined[:])
	}
	return current == root
}

// ── Example main (in cmd/main.go) ─────────────────────────────────────
/*
func main() {
	header := []byte("v1|prev=abc|merkle=def|time=1700000000")
	result, err := consensus.MineBlock(header, 2)
	if err != nil { log.Fatal(err) }
	fmt.Printf("nonce=%d hash=%s... attempts=%d\n",
		result.Nonce, hex.EncodeToString(result.Hash[:8]), result.Attempts)
}
*/
_ = hex.EncodeToString // suppress unused import warning in snippet

10. Summary

Blockchain consensus is fundamentally the problem of agreeing on a shared history in the presence of Byzantine adversaries — participants who may actively lie, collude, or attempt to rewrite the past. The theoretical limits were established decades before Bitcoin: FLP impossibility, the 3f+1 bound, and the CAP theorem all constrain what is achievable. Every consensus protocol navigates these constraints differently.

Nakamoto PoW
Sidesteps identity with proof-of-physical-work. Open, decentralized, energy-intensive. Probabilistic finality. Vulnerable to 51% attack and selfish mining above 33%. The longest-chain rule converges on truth over time but never achieves irreversibility in finite time.
Proof of Stake
Replaces energy with economic stake. Comparable attack cost to PoW but attacks are detectable and punishable by slashing. Deterministic finality achievable (~12 min on Ethereum) via Casper FFG. Requires defense against long-range attacks via weak subjectivity checkpoints.
BFT (PBFT / Tendermint / HotStuff)
Deterministic finality in a single round. Requires known, bounded validator set (limits decentralization). FLP-constrained: halts under partition rather than forking. HotStuff achieves O(n) communication via threshold signatures. The workhorse of high-performance L1 chains and enterprise blockchains.

The design space is not static. Current research explores:

The Byzantine Generals Problem turns 43 years old in 2025. Satoshi solved the open-internet version not by clever protocol design alone, but by introducing a new resource — provably spent electricity — as a vote. That reframing made trustless global consensus practical. The decade since has been an exploration of the enormous design space that insight opened up.

KEY TAKEAWAYS
  • Honest consensus requires >⅔ of nodes (by weight) to be honest — this is a mathematical lower bound, not a design choice.
  • No deterministic protocol can guarantee both safety and liveness in an asynchronous network (FLP). Every real system makes a trade-off.
  • PoW's genius: replace unverifiable identity with verifiable physical work. Cost to Sybil = cost to compute.
  • PoS's economics: attack cost is proportional to your own stake — rational attackers don't attack.
  • BFT protocols give you deterministic finality and high throughput, at the cost of permissionless participation and CAP-theorem liveness.
  • The blockchain trilemma (decentralization, security, scalability) is not a law but a statement about current engineering trade-offs. Layer-2 solutions, DAGs, and threshold cryptography are actively pushing the frontier.