Chain Selection & Reorgs
Work through examples, try the reasoning yourself, then put it into practice.
View SlidesBefore you start
- Trace UTXO spends and block parent links.
What you will practice
- Find a fork and reverse state in the correct order.
- Validate a candidate on its parent branch without publishing partial changes.
In this lesson
Lesson
5 min readChain selection and reversible state
A block tree contains several histories; the UTXO map represents exactly one of
them. Keep those two ideas separate. Blocks, Height, and TotalWork describe
accepted branches. Tip identifies the active branch, and UTXO must equal the
unspent outputs obtained by executing that branch from genesis.
Work selects a history
For a child b, compute Height[b] = Height[parent] + 1 and
TotalWork[b] = TotalWork[parent] + b.Work. The exercise supplies work directly;
it does not derive work from a proof-of-work target. Height alone cannot select
the winner when per-block work differs. A candidate with equal total work is
stored but leaves the current tip unchanged. Here the return value reorg=true
means any active-tip change, including an ordinary extension.
Consider G(work=1) → A(work=2) → C(work=2), whose total is 5. The competing
branch is G → B(work=3) → D(work=2), whose total is 6. B alone has total 4 and
loses; D makes its branch win. Both tips have height 2, so the fork search walks
C and D back to A and B, then to G. For unequal heights, first move only the
deeper pointer upward until the heights match, then move both together. The
first identical block is the common ancestor; it is never disconnected.
Work through the UTXO transition
Let genesis create g:50. A spends g and creates a:40; C spends a and creates
c:35. B instead spends g and creates b:45; D spends b and creates d:42.
The suffix after each colon is an output value, not an identifier.
| Step | Operation | UTXO afterward |
|---|---|---|
| Active C | Starting state | {c:35} |
| Disconnect C | Remove c, restore a | {a:40} |
| Disconnect A | Remove a, restore g | {g:50} |
| Connect B | Consume g, create b | {b:45} |
| Connect D | Consume b, create d | {d:42} |
Old blocks are disconnected from tip toward fork; new blocks are connected from fork toward tip. Applying D before B fails because b does not yet exist. Undo records store the previous output, including its value, because the spending transaction cannot reconstruct it.
The subtle case: a spend inside one block
Suppose one block contains g → x followed by x → y. Its final state is {y}.
After disconnecting that block the state must be {g}, never {g,x}. An undo
list that indiscriminately records every spent input restores x even though x
was created after the block began.
The supplied interface uses a flat undo list. Therefore record only consumed outputs that existed before this block. Track the outpoints created while applying the block and omit them from the undo list. Disconnect by removing all outputs created by the block, then restoring the saved pre-block outputs. An alternative design could store per-transaction undo and reverse transactions, but that is a different representation.
A rejected candidate must leave no trace
Now let D contain two transactions: the first validly spends b, while the second
spends an unknown outpoint. If the implementation has already disconnected A and
C or deleted b from the live map, simply returning false leaves Tip=C paired
with the wrong UTXO state. Restoring the tip variable alone does not fix this.
Build a private UTXO view of the candidate's parent: copy the active map,
disconnect back to the common ancestor, then connect the parent's branch in
forward order. Validate the entire candidate in that view. Only after success
publish its metadata and undo record; publish the private UTXO view and new tip
only if total work is strictly greater. Invalid candidates, including losing
side branches, leave all state maps unchanged and return (false,false).
This also explains why a valid sibling may spend g even when g is absent from the active tip's UTXO. Validation uses the candidate's parent history, not whatever branch happens to be active. Known accepted IDs are idempotent no-ops; a rejected ID must not accidentally become a known accepted block.
Scope and reconstruction checks
The constructor assumes a valid genesis and callers treat stored blocks as immutable. The exercise checks missing/repeated spends, malformed coinbase inputs, repeated transaction IDs within a block, live output overwrites, and work overflow. It omits signatures, value conservation, coinbase subsidy rules, networking, persistence and crash recovery. Copying a view makes a function call atomic in this in-memory model; it is not a durable database transaction.
Predict these outcomes before checking the answers:
- A shorter branch has total work 12; the active longer branch has total 11. Answer: activate the shorter valid branch; height only aids ancestry.
- A losing side branch spends an output created only on the active sibling. Answer: reject it without mutation; that output is absent at its parent.
- A block creates x, spends x, then is disconnected. Answer: x remains absent; only pre-block consumed outputs are restored.
- Two siblings both have total work 8. Answer: retain the active sibling and store the other valid candidate.
For the production mechanisms behind these ideas, see Bitcoin Core's chain activation and disconnect/connect implementation. Its cache layers, validation rules and disk recovery are substantially broader than this exercise's model.
Practice sequence
Use the order below for a guided path, or choose an item to revisit. A pass records a successful attempt; try a fresh example later to check what you can reconstruct.