Skip to content

The Blockchain: Linking Blocks

You can now build a single Block and compute its hash. But one block is not a chain. In this chapter we build the Blockchain type itself: the ordered list of blocks, the first block (the genesis block), and — the whole point — the links that chain each block to the one before it.

That link is one field doing enormous work. Because every block’s header stores the hash of the previous block, the blocks form a one-way chain where changing any old block would break every link after it. This is what makes a blockchain tamper-evident, and it’s the answer to the book’s recurring question: how do untrusting strangers agree on one history? They agree because the history is welded together by hashes.

Open src/blockchain.rs.

Before the chain itself, the file defines a few constants that set the economics of our coin:

/// The smallest unit; 1 coin = 100,000,000 units (like satoshis).
pub const COIN: u64 = 100_000_000;
/// Block reward before the first halving.
pub const INITIAL_SUBSIDY: u64 = 50 * COIN;
/// How many blocks between halvings.
pub const HALVING_INTERVAL: u64 = 210_000;
/// Required leading zero bits for a valid block hash (low, for fast demos).
pub const DIFFICULTY_BITS: u32 = 16;

A const is a compile-time constant — a name for a fixed value. We work in the smallest unit, like Bitcoin’s satoshis: one COIN is 100 million units, so we never need fractions. INITIAL_SUBSIDY is the reward a miner gets for a fresh block — 50 coins, just like early Bitcoin. HALVING_INTERVAL is how often that reward is cut in half. DIFFICULTY_BITS we’ll use for mining; it’s set low here so demos finish fast.

Now the subsidy function — how many new coins a block at a given height creates:

/// The block subsidy at a given height (halves every `HALVING_INTERVAL`).
pub fn subsidy(height: u64) -> u64 {
let halvings = height / HALVING_INTERVAL;
if halvings >= 64 {
0
} else {
INITIAL_SUBSIDY >> halvings
}
}

Read it top to bottom. We figure out how many halvings have happened by integer-dividing the block height by the interval. Then the clever part: INITIAL_SUBSIDY >> halvings. The >> operator is a bit shift right, and shifting an integer right by one bit is the same as dividing by two. So shifting by halvings divides the reward by two, that many times — exactly what “halving” means. The guard if halvings >= 64 returns 0 first, because shifting a 64-bit number by 64 or more is undefined; after enough halvings the reward is effectively zero anyway, so the coin supply caps out.

Here’s the type at the center of everything:

pub struct Blockchain {
pub blocks: Vec<Block>,
pub utxos: UtxoSet,
}

Just two fields. blocks is a Vec<Block> — a growable, ordered list of blocks. Order is history: blocks[0] is the genesis block, blocks[1] extends it, and so on. The utxos field holds the UTXO set: the live ledger of every coin that currently exists and who can spend it. (We build the UTXO model in its own chapter — for now, think of it as “the current balances.”) Every method we write either reads from these two fields or carefully updates them.

Every chain needs a first block. It can’t point at a previous block (there isn’t one), so it’s special and we build it by hand in new:

impl Blockchain {
/// Create a new chain with a mined genesis block (coinbase to the
/// unspendable zero address, like Bitcoin's genesis).
pub fn new() -> Self {
let mut chain = Blockchain {
blocks: Vec::new(),
utxos: UtxoSet::new(),
};
let coinbase = create_coinbase(ZERO_HASH, 0, 0, 0);
let txs = vec![coinbase];
let root = merkle_root(&txs.iter().map(|t| t.txid()).collect::<Vec<_>>());
let mut genesis = Block {
header: BlockHeader {
version: 1,
prev_block_hash: ZERO_HASH,
merkle_root: root,
timestamp: 0,
bits: DIFFICULTY_BITS,
nonce: 0,
},
transactions: txs,
};
genesis.mine();
for tx in &genesis.transactions {
chain.utxos.add_transaction(tx.txid(), tx);
}
chain.blocks.push(genesis);
chain
}

new returns Self — shorthand for “the type this impl block is for,” i.e. Blockchain. Step by step:

  • We start with an empty chain: Vec::new() is an empty vector, UtxoSet::new() an empty ledger. It’s let mut chain because we’re about to modify it.
  • create_coinbase(ZERO_HASH, 0, 0, 0) builds the coinbase transaction — the special one that mints new coins. The genesis coinbase pays the ZERO_HASH address, which nobody holds the key to, so (like Bitcoin’s genesis) those first coins are unspendable. We wrap it in a one-element vector with vec![coinbase].
  • merkle_root(...) folds the transaction ids into the header’s merkle_root. We’ll explain that pipeline in the Merkle chapter; for now note the genesis still commits to its transactions.
  • We construct the genesis block. The crucial field is prev_block_hash: ZERO_HASH — the genesis has no parent, so it points at all-zeros. Every later block will point at a real hash.
  • genesis.mine() runs the Proof of Work loop to find a valid nonce (covered in the mining chapter).
  • The for tx in &genesis.transactions loop records the new coins in the UTXO set, then chain.blocks.push(genesis) appends the block. The bare chain on the last line returns the finished chain.

When we later save the chain to disk and reload it, we get back a Vec<Block> but no UTXO set. The UTXO set is derived state, so we replay the blocks to rebuild it:

/// Rebuild a chain from stored blocks, replaying them to derive the UTXO set.
pub fn from_blocks(blocks: Vec<Block>) -> Self {
let mut utxos = UtxoSet::new();
for block in &blocks {
for tx in &block.transactions {
utxos.add_transaction(tx.txid(), tx);
}
}
Blockchain { blocks, utxos }
}

Two nested for loops walk every transaction in every block and feed it to the UTXO set. By the end, utxos reflects exactly the balances those blocks imply. This is a recurring blockchain idea: the chain of blocks is the source of truth, and everything else can be recomputed from it.

Asking the chain about itself: tip and height

Section titled “Asking the chain about itself: tip and height”

Two small read-only helpers tell us where the chain currently ends:

pub fn tip_hash(&self) -> Hash {
self.blocks.last().map(|b| b.hash()).unwrap_or(ZERO_HASH)
}
/// Height of the tip (genesis is height 0).
pub fn height(&self) -> u64 {
(self.blocks.len() as u64).saturating_sub(1)
}

tip_hash gives the hash of the most recent block — the tip — which is exactly what the next block must store in its prev_block_hash. The implementation is a nice tour of Option, Rust’s way of expressing “maybe there’s a value here.” self.blocks.last() returns an Option<&Block>: Some(block) if the chain is non-empty, or None if it’s empty. We .map(|b| b.hash()) to turn a present block into its hash, then .unwrap_or(ZERO_HASH) to supply a fallback when there’s nothing there. So an empty chain reports ZERO_HASH — meaning a brand-new block would correctly “extend” the void, the same sentinel the genesis used.

height returns the position of the tip, counting genesis as 0. We take the number of blocks, cast it to u64 with as, and subtract one — but with saturating_sub(1), which clamps at 0 instead of underflowing if the chain were somehow empty. (Subtracting 1 from an unsigned 0 would otherwise be a bug.)

Finally, add_block is how the chain grows:

/// Validate then append a block, updating the UTXO set.
pub fn add_block(&mut self, block: Block) -> Result<(), String> {
self.validate_block(&block)?;
for tx in &block.transactions {
self.utxos.add_transaction(tx.txid(), tx);
}
self.blocks.push(block);
Ok(())
}
}

At a high level, add_block does three things, in order: it validates the block, updates the UTXO set with the block’s transactions, and appends the block to the list. It takes &mut self because it changes the chain, and returns Result<(), String> — either Ok(()) for success, or Err with a message if the block is rejected. The ? after validate_block is shorthand for “if this returned an error, stop and return that error now”; we’ll lean on ? a lot once we reach error handling.

The first and most important check inside validate_block is the link itself: a new block is only accepted if its prev_block_hash equals the current tip_hash(). That single comparison is what keeps the chain a single, ordered history rather than a loose bag of blocks.

Here’s the payoff. Each block’s hash is computed from its header, and the header includes prev_block_hash. So block 5’s hash depends on block 4’s hash, which depends on block 3’s, all the way back to genesis. If an attacker quietly edits a transaction in block 2, block 2’s Merkle root changes, so block 2’s hash changes — but block 3 still stores the old block 2 hash in its prev_block_hash. The link no longer matches. To hide the tampering they’d have to re-mine block 2 and block 3 and every block after it, faster than the honest network extends the chain. That’s the wall Proof of Work builds, and the chain link is the mortar holding it together.

The file ends with a small convenience:

impl Default for Blockchain {
fn default() -> Self {
Self::new()
}
}

Default is a standard trait for “the obvious starting value” of a type. By implementing it, anyone can write Blockchain::default() (or use it in contexts that call default() automatically) and get a fresh chain with its genesis block. Here it just delegates to new(), so the two are interchangeable.

The companion crate’s demo builds a chain and adds blocks. Run it and watch the links:

Terminal window
cargo run -- demo

Look closely at the printed block hashes. The genesis hash becomes block 1’s prev_block_hash; block 1’s hash becomes block 2’s prev_block_hash. Each block reaches back and grips the one before it — that chain of hashes is the blockchain.

  1. What two fields does the Blockchain struct hold, and why can one of them always be recomputed from the other?
  2. In the genesis block, why is prev_block_hash set to ZERO_HASH?
  3. How does subsidy() use the >> operator to implement halvings, and why is a bit shift used instead of floating-point division?
  4. tip_hash() returns ZERO_HASH when the chain is empty. Trace how Option, .map, and .unwrap_or produce that result.
  5. If an attacker edits a transaction in an old block, why does every block after it stop validating?
Show answers
  1. blocks (the ordered Vec<Block>, the canonical history) and utxos (the UTXO set, the live ledger). The UTXO set can always be recomputed from the blocks by replaying every transaction from genesis — it’s derived state, a cache over the source of truth.
  2. Because the genesis block has no parent — there’s no previous block to point at — so its prev_block_hash uses the all-zeros ZERO_HASH sentinel meaning “nothing before this.” Every later block points at a real previous hash.
  3. subsidy() computes halvings = height / HALVING_INTERVAL, then returns INITIAL_SUBSIDY >> halvings — and shifting right by one bit divides by two, so shifting by halvings halves the reward that many times. A bit shift is used instead of floating-point division because it’s a single, exact, integer-only operation with no rounding surprises, which matters in code that decides who owns money.
  4. self.blocks.last() returns Option<&Block>Some(block) if non-empty, else None. .map(|b| b.hash()) turns a present block into its hash (and leaves None as None); .unwrap_or(ZERO_HASH) then supplies ZERO_HASH as the fallback when there’s nothing there — so an empty chain reports ZERO_HASH, the same sentinel a brand-new genesis “extends.”
  5. Editing a transaction changes that block’s Merkle root, hence its header hash — but the next block still stores the old hash in its prev_block_hash, so the link no longer matches and it fails validation. To hide it the attacker would have to re-mine that block and every block after it, faster than the honest network extends the chain — which is the wall Proof of Work builds.