The Block & the Header
In the last chapter you built a Hash type and a sha256d function. On their own those are just
fingerprints of arbitrary bytes. Now we’ll give them something meaningful to fingerprint: a block.
A block is the fundamental unit a blockchain agrees on. Everything else — Proof of Work, the chain links, the ledger of who owns what — hangs off the block. So before we can link blocks together (next chapter) or mine them (the chapter after), we need to define what a block is in Rust and learn how to turn one into a hash.
This chapter is where you’ll really get comfortable with Rust structs, the #[derive(...)]
attribute, and the idea of canonical byte encoding — turning a structured value into one exact,
reproducible sequence of bytes. That last idea is the quiet hero of the whole system: if two computers
encode the same block to different bytes, they’d compute different hashes and never agree on
anything.
Two structs: a header and a body
Section titled “Two structs: a header and a body”Open src/block.rs. The file starts with its imports:
//! Blocks: an 80-byte-style header plus the list of transactions it commits to.
use serde::{Deserialize, Serialize};
use crate::hashing::{sha256d, Hash};use crate::merkle::merkle_root;use crate::pow::meets_target;use crate::transaction::Transaction;A few things to notice. use serde::{Deserialize, Serialize} pulls in two traits from the serde
crate — we’ll see in a moment what deriving them buys us. The use crate::... lines pull in code from
other modules in our own crate: the Hash type and sha256d you already wrote, plus merkle_root,
meets_target, and Transaction, which belong to later chapters. Rust requires you to name everything
you use up front, so this import block doubles as a table of contents for the file.
A real Bitcoin block is split into two parts, and our design mirrors that exactly:
- a small, fixed-size header that miners hash over and over, and
- the body, the variable-length list of transactions.
Splitting them this way matters: the header is tiny (about 80 bytes), so hashing it billions of times during mining is cheap, yet it still commits to every transaction through the Merkle root. Let’s build the header first.
The block header
Section titled “The block header”/// The block header — the small, fixed-size part that miners hash.#[derive(Clone, Debug, Serialize, Deserialize)]pub struct BlockHeader { pub version: u32, pub prev_block_hash: Hash, pub merkle_root: Hash, pub timestamp: u64, /// Difficulty: required number of leading zero bits (see `pow`). pub bits: u32, pub nonce: u64,}A struct groups related values under one name. BlockHeader has six fields, and each is pub
(public) so other modules can read and set them. Here’s what each one is for:
version: u32— a format version number, so the rules can evolve over time. Au32is an unsigned 32-bit integer (0 to about 4.3 billion).prev_block_hash: Hash— the hash of the previous block’s header. This is the link that turns a pile of blocks into a chain; we’ll lean on it heavily next chapter.merkle_root: Hash— a single hash that fingerprints all the transactions in the block. Change any transaction and this value changes. (We’ll build the Merkle tree later.)timestamp: u64— when the block was made, as seconds since 1970 (au64, an unsigned 64-bit integer — plenty of room).bits: u32— the difficulty: how many leading zero bits the block hash must have. More on this in the Proof of Work chapter.nonce: u64— the “number used once” that miners spin to search for a valid hash.
That #[derive(Clone, Debug, Serialize, Deserialize)] line on top is an attribute, and derive
asks the compiler to write some trait implementations for us automatically:
Clonelets us make a deep copy with.clone().Debuglets us print the struct for inspection with{:?}inprintln!.Serialize/Deserializecome fromserde. They let us convert the struct to and from formats like JSON — which is how we’ll save the chain to disk in the persistence chapter. We get all of this for free, just by listing the trait names.
Encoding the header to canonical bytes
Section titled “Encoding the header to canonical bytes”A hash function eats bytes, but a BlockHeader is a structured value. We need a single, agreed-upon way
to flatten it into bytes — the same way, every time, on every machine. That’s encode:
impl BlockHeader { /// Canonical byte encoding of the header (this is what we hash). pub fn encode(&self) -> Vec<u8> { let mut buf = Vec::with_capacity(4 + 32 + 32 + 8 + 4 + 8); buf.extend_from_slice(&self.version.to_le_bytes()); buf.extend_from_slice(&self.prev_block_hash); buf.extend_from_slice(&self.merkle_root); buf.extend_from_slice(&self.timestamp.to_le_bytes()); buf.extend_from_slice(&self.bits.to_le_bytes()); buf.extend_from_slice(&self.nonce.to_le_bytes()); buf }impl BlockHeader { ... } is an implementation block — it’s where methods for BlockHeader live.
The &self parameter means this method borrows the header (reads it without taking ownership), so the
caller still owns their header afterward. The return type Vec<u8> is a growable vector of bytes — Vec
is Rust’s standard dynamic array, and u8 is a single byte.
Walk through the body:
Vec::with_capacity(4 + 32 + 32 + 8 + 4 + 8)makes an empty vector but pre-reserves room for exactly 88 bytes — the sum of the field sizes. This is just an optimization so the vector never has to reallocate while we fill it; those numbers are the byte sizes of each field in order.buf.extend_from_slice(&...)appends a slice of bytes onto the end ofbuf. We call it once per field, in a fixed order.- For the integer fields we call
.to_le_bytes(). Au32likeversionis four bytes in memory, but there are two conventions for which byte comes first.to_le_bytesgives little-endian order: least-significant byte first. The twoHashfields (prev_block_hash,merkle_root) are already[u8; 32]byte arrays, so we append them directly.
Finally, the bare buf on the last line is the return value. Rust returns the value of the last
expression in a block when there’s no semicolon — no return keyword needed.
Hashing the header
Section titled “Hashing the header”With a canonical byte string in hand, the block hash is almost anticlimactic:
/// The block hash: double-SHA-256 of the header. pub fn hash(&self) -> Hash { sha256d(&self.encode()) }}We encode the header to bytes, then run it through sha256d — the double-SHA-256 (SHA-256 applied
twice) you built last chapter. The result is the block hash: the block’s unique fingerprint and the
value the next block will store in its prev_block_hash.
Notice that hash only depends on the header, never the transaction list directly. That’s safe because
the header’s merkle_root already commits to every transaction. Tamper with one coin in one transaction
and the Merkle root would have to change, which changes the header, which changes the hash. Small input,
total coverage.
The full block
Section titled “The full block”Now the body. A Block is just a header plus its transactions:
/// A full block.#[derive(Clone, Debug, Serialize, Deserialize)]pub struct Block { pub header: BlockHeader, pub transactions: Vec<Transaction>,}
impl Block { pub fn hash(&self) -> Hash { self.header.hash() }Same derive set as before, for the same reasons. The transactions field is a Vec<Transaction> — a
growable list of Transaction values (a type we’ll define in a later chapter). The block’s hash method
simply delegates to the header: self.header.hash(). There’s no separate “block hash” — the header hash
is the block hash, which is exactly why the header must commit to everything.
The rest of impl Block contains two methods we’ll meet later:
/// Compute the Merkle root of this block's transactions. pub fn compute_merkle_root(&self) -> Hash { let txids: Vec<Hash> = self.transactions.iter().map(|tx| tx.txid()).collect(); merkle_root(&txids) }
/// Brute-force the nonce until the header hash meets the difficulty target. /// (Real miners also roll the timestamp and a coinbase extra-nonce; one /// counter is enough to demonstrate the idea.) pub fn mine(&mut self) { self.header.nonce = 0; loop { if meets_target(&self.header.hash(), self.header.bits) { break; } self.header.nonce = self.header.nonce.wrapping_add(1); } }}Don’t worry about these yet — we’ll cover Merkle roots and mining soon. For now, just notice the shape:
compute_merkle_rootfolds every transaction’s id into a single hash. We’ll build the tree in Merkle Trees & Proofs.minetakes&mut self(a mutable borrow, because it changes thenonce) and loops, bumping the nonce until the hash is small enough. That’s the heart of Proof of Work & Mining.
You now have everything you need to create a block and fingerprint it — which is exactly what the next chapter builds on to link blocks into a chain.
Try it
Section titled “Try it”Add a quick experiment to main.rs (or a #[test]) that builds a header by hand and prints its hash.
Because every field is fixed, the same header always produces the same hash:
use blockchain_from_scratch::block::BlockHeader;use blockchain_from_scratch::hashing::ZERO_HASH;
fn main() { let header = BlockHeader { version: 1, prev_block_hash: ZERO_HASH, merkle_root: ZERO_HASH, timestamp: 0, bits: 16, nonce: 0, }; println!("encoded {} bytes", header.encode().len()); println!("hash: {}", hex::encode(header.hash()));}Then run it:
cargo runYou should see encoded 88 bytes and a 64-character hex hash. Change the nonce to 1 and run again —
the hash changes completely. That avalanche effect is what makes mining a search problem.
Check your understanding
Section titled “Check your understanding”- The
BlockHeaderhas six fields. Which one links a block to the one before it, and which one commits to all the transactions? - Why does
hash()only need the header, even though a block also contains a list of transactions? - What does
#[derive(Serialize, Deserialize)]give us, and which later feature relies on it? - What would happen if one node encoded
timestampas big-endian while another used little-endian? - Why does
encodeuseVec::with_capacity(...)instead of a plainVec::new()? Is the program incorrect without it?
Show answers
prev_block_hashlinks a block to the one before it (it stores the previous block’s hash);merkle_rootcommits to all the transactions (it’s a single hash fingerprinting every transaction in the block).- Because the header’s
merkle_rootalready commits to every transaction — tampering with any coin changes a txid, which changes the Merkle root, which changes the header and thus the hash. So hashing just the small, fixed-size header gives total coverage of the block’s contents cheaply (which is what makes mining fast). - It gives us
Serialize/Deserialize— the ability to convert the struct to and from JSON — for free. The persistence feature (saving the chain to disk) relies on it. - They’d flatten identical headers into different byte sequences, producing different hashes, so the two nodes would reject each other’s blocks. The encoding is part of the consensus rules — everyone must agree on the convention; little-endian isn’t “more correct,” just agreed-upon.
Vec::with_capacity(...)pre-reserves room (88 bytes) so the vector never has to reallocate while being filled — a pure optimization. The program is not incorrect without it; a plainVec::new()would grow as needed and produce the exact same bytes, just with a few possible reallocations.