Skip to content

The Mempool & Mining Real Transactions

So far we can describe coins (the UTXO model), hash them (txids and Merkle roots), and sign them (secp256k1). But nothing has actually assembled a block out of real payments yet. That’s the job of this chapter. We’ll build the three functions that turn a pile of candidate transactions into a mined block: the coinbase that mints the reward, mine_block that picks which transactions go in and collects their fees, and build_transaction that constructs a signed payment in the first place.

This is the building half of the chain. (Next chapter is the checking half — the rules that reject cheating.) Along the way you’ll meet a deeply Rust-y idea: cloning a working view of state, mutating the copy as you go, and only committing once you’re sure. You’ll also see iterators, Vec, plain accumulation in a loop, and the full signing flow end to end.

Every block contains one special transaction that has no real inputs — it conjures the block reward out of nothing. It’s called the coinbase, and it’s how new coins enter circulation. From src/blockchain.rs:

/// Build the coinbase transaction that pays the miner `subsidy(height) + fees`.
pub fn create_coinbase(recipient: Hash, height: u64, fees: u64, extra_nonce: u64) -> Transaction {
// Coinbase input carries arbitrary data so two coinbases with the same
// outputs still get distinct txids.
let mut data = Vec::new();
data.extend_from_slice(&height.to_le_bytes());
data.extend_from_slice(&extra_nonce.to_le_bytes());
Transaction {
inputs: vec![TxInput {
previous_output: OutPoint::null(),
signature: data,
pubkey: Vec::new(),
}],
outputs: vec![TxOutput {
value: subsidy(height) + fees,
recipient,
}],
}
}

There’s a subtle problem this code solves. Two blocks mined at the same height that pay the same miner the same amount would produce identical coinbase transactions — and therefore identical txids. That would be a disaster: txids are supposed to be unique identities. So the coinbase input’s signature field (which a coinbase doesn’t need for its real purpose, since it spends nothing) is reused to carry arbitrary data: the block height plus an extra_nonce. We build that data buffer with Vec::new() and extend_from_slice, packing each number into little-endian bytes with to_le_bytes. Because that data flows into the txid, every coinbase gets a distinct id.

The input itself points at OutPoint::null() — the all-zeros, u32::MAX sentinel from the transactions chapter that means “this spends no real output.” And the single output pays subsidy(height) + fees: the freshly-minted block reward plus every fee collected from the transactions in the block. That + fees is the whole reason miners bother including your transaction — more on that in Fees, Change & Conservation.

Now the centerpiece. Given a Vec of candidate transactions and the miner’s address, mine_block decides which candidates are valid, collects their fees, prepends the coinbase, computes the Merkle root, and mines:

/// Assemble and mine the next block from a set of candidate transactions,
/// paying the subsidy + collected fees to `miner_address`.
pub fn mine_block(&self, candidates: Vec<Transaction>, miner_address: Hash) -> Block {
let height = self.blocks.len() as u64;
let mut view = self.utxos.clone();
let mut fees = 0u64;
let mut included = Vec::new();
for tx in candidates {
if let Ok(fee) = validate_tx(&tx, &view) {
fees += fee;
view.add_transaction(tx.txid(), &tx);
included.push(tx);
}
}

The key line is let mut view = self.utxos.clone();. We make a working copy of the live UTXO set and validate against that, never touching the real one. Why clone? Because we’re about to test transactions speculatively — some may turn out invalid, some may depend on each other — and we don’t want to corrupt the chain’s true state while we experiment. The clone is a sandbox.

Then we loop over the candidates (note: for tx in candidates consumes the Vec by value, since we own it and want to move each transaction into the block). For each one we call validate_tx, which returns Result<u64, String> — the fee on success, or an error string. The if let Ok(fee) = ... pattern says “if validation succeeded, bind the fee and run this block; otherwise just skip it.” A bad transaction is silently dropped, never mined.

When a transaction is valid we do three things: add its fee to our running fees total (plain accumulation — fees += fee, no fancy machinery needed), apply it to the working view with view.add_transaction(...), and push it onto included. Applying it to the view matters: it means a later candidate can spend an output that an earlier candidate in the same block created. The view evolves as we go.

let coinbase = create_coinbase(miner_address, height, fees, 0);
let mut transactions = vec![coinbase];
transactions.extend(included);
let root = merkle_root(&transactions.iter().map(|t| t.txid()).collect::<Vec<_>>());
let mut block = Block {
header: BlockHeader {
version: 1,
prev_block_hash: self.tip_hash(),
merkle_root: root,
timestamp: now(),
bits: DIFFICULTY_BITS,
nonce: 0,
},
transactions,
};
block.mine();
block
}

Now that we know the total fees, we can build the coinbase that pays the miner subsidy + fees. The coinbase must come first in the block (a rule we’ll enforce next chapter), so we start a Vec with just the coinbase — vec![coinbase] — and then transactions.extend(included) appends all the real transactions after it.

The Merkle root is computed from the txids of every transaction in order: transactions.iter().map(|t| t.txid()).collect::<Vec<_>>() is a classic iterator pipeline — iterate the transactions, map each to its txid(), and collect the results into a Vec. (The ::<Vec<_>> is a turbofish telling collect what kind of collection to build; the _ lets the compiler infer the element type.) That Vec of txids goes into merkle_root, producing the single fingerprint that commits to the block’s entire contents — see Merkle Trees & Proofs.

Finally we wrap everything in a Block, fill the header (linking prev_block_hash to the current tip), and call block.mine() — the Proof of Work loop that grinds the nonce until the hash meets the target. That’s the expensive part, the actual “mining,” covered in The Mining Process.

Where do those candidate transactions come from? A wallet builds them. build_transaction takes a sender, the UTXO set, a recipient, an amount, and a fee, and returns a fully signed Transaction:

/// Construct a signed transaction sending `amount` (+`fee`) from `sender`.
pub fn build_transaction(
sender: &Wallet,
utxos: &UtxoSet,
recipient: Hash,
amount: u64,
fee: u64,
) -> Result<Transaction, String> {
let sender_addr = sender.address();
let need = amount + fee;
// Coin selection: grab the sender's UTXOs until we cover the amount.
let mut inputs = Vec::new();
let mut total = 0u64;
for ((txid, vout), output) in utxos.utxos.iter() {
if output.recipient == sender_addr {
inputs.push(TxInput {
previous_output: OutPoint { txid: *txid, vout: *vout },
signature: Vec::new(),
pubkey: Vec::new(),
});
total += output.value;
if total >= need {
break;
}
}
}
if total < need {
return Err(format!("insufficient funds: have {total}, need {need}"));
}

This is coin selection. Remember from the UTXO chapter that you have no single balance — you have a scattering of discrete outputs. To send amount + fee, you must gather enough of your outputs to cover it. We iterate utxos.utxos.iter(), which yields ((txid, vout), output) pairs (destructured right in the for pattern). For each output locked to sender_addr, we turn it into a TxInput — empty signature and pubkey for now, we’ll fill those in below — push it, and add its value to total. The moment total >= need, we break: we’ve gathered enough, no point grabbing more. If we run out of UTXOs before covering need, we bail with an Err and a helpful message.

let mut outputs = vec![TxOutput { value: amount, recipient }];
let change = total - need;
if change > 0 {
outputs.push(TxOutput { value: change, recipient: sender_addr });
}
let mut tx = Transaction { inputs, outputs };
// Every input is owned by the sender, so they all sign the same sighash.
let sighash = tx.sighash();
let signature = sender.sign(&sighash);
let pubkey = sender.pubkey_bytes();
for input in &mut tx.inputs {
input.signature = signature.clone();
input.pubkey = pubkey.clone();
}
Ok(tx)
}

UTXOs are spent whole, so we almost always gather more than we need. The first output pays the recipient the amount. The leftover — change = total - need — is sent back to ourselves as a second output, but only if it’s nonzero. (The fee is never an output; it’s simply the gap between inputs and outputs that the miner scoops up via the coinbase. This is the conservation rule from Fees, Change & Conservation.)

Then comes signing. Because every input we selected belongs to the sender, they can all be authorized by one key against one message. We compute the sighash once (the blanked encoding from the transactions chapter), produce a single signature and pubkey, and loop for input in &mut tx.inputs to stamp each input with copies. We need .clone() because each input owns its own bytes — we can’t hand the same Vec to several inputs. The result is a transaction whose every input proves the sender owns it.

Run the demo and watch a fee flow from a payment into the miner’s coinbase:

Terminal window
cd rust/blockchain-from-scratch
cargo run -- demo

When Alice pays Bob, she includes a small fee. That fee isn’t burned — it’s collected in mine_block, folded into fees, and paid to whoever mines the block via subsidy + fees in the coinbase. Watch the miner’s balance: it’s the block reward plus the fees from every transaction it chose to include.

  1. Two blocks mined at the same height paying the same miner the same amount would normally produce identical coinbase txids. What does create_coinbase put in the input’s signature field to prevent that, and why is reusing that field safe for a coinbase?
  2. Why does mine_block clone self.utxos into a view instead of validating against the live UTXO set directly? Name two behaviors the evolving working copy enables.
  3. In build_transaction, what is change, why might it be zero, and where does the fee end up if it’s never an output?
  4. The coinbase is built after the loop over candidates, not before. Why must the fee total be known first?
  5. mine_block uses if let Ok(fee) = validate_tx(...) and silently skips failures. What kind of candidate would get dropped here rather than crashing the miner?
Show answers
  1. It packs the block height plus an extra_nonce (as little-endian bytes) into the coinbase input’s signature field, so that data flows into the txid and every coinbase gets a distinct id. Reusing that field is safe because a coinbase spends nothing real, so its input needs no actual signature — the field is otherwise unused.
  2. So it can validate transactions speculatively against a sandbox without corrupting the chain’s true UTXO state — some candidates may turn out invalid. The evolving working copy enables two behaviors: a later candidate can spend an output an earlier candidate in the same block created, and a candidate trying to double-spend an output an earlier one already consumed is rejected (it’s no longer in the view).
  3. change is the leftover, total - need — because UTXOs are spent whole, you almost always gather more than you need, so the surplus is sent back to yourself as a second output. It’s zero when the selected inputs exactly cover amount + fee (no second output is added). The fee is never an output; it’s the gap between inputs and outputs that the miner scoops up via the coinbase (subsidy + fees).
  4. Because the coinbase pays the miner subsidy(height) + fees, and fees isn’t known until every candidate has been validated and its fee accumulated. You must finish the loop to total the fees before you can build a coinbase that claims exactly the allowed amount.
  5. An invalid transaction — one that validate_tx rejects, e.g. spending a non-existent or already-spent output, a forged/mismatched signature, or outputs exceeding inputs. It’s silently dropped rather than crashing the miner, so one bad candidate never stops the block from being mined.