Transactions & UTXO
Work through examples, try the reasoning yourself, then put it into practice.
View SlidesBefore you start
- Encode and decode canonical CompactSize.
- Identify a transaction output by transaction ID and index.
What you will practice
- Trace byte offsets through a variable-length transaction and compute its raw ID.
- Apply a UTXO transition atomically with duplicate-input and overflow checks.
In this lesson
Lesson
5 min readTransactions: Bytes and State Transitions
You will build two different boundaries: a parser that recognizes transaction bytes, and a validator that decides whether a transaction may change a UTXO set. A successful parse says nothing about whether funds exist. A balanced transaction says nothing about whether a signature authorizes it. Keep those responsibilities separate so a failure points to one rule.
Readiness and exercise interfaces
First encode 253 with CompactSize and explain why an outpoint needs both a
transaction ID and an output index. A transaction may create several outputs;
(T,0) and (T,1) are different spendable entries.
The serialization exercise uses scripts, sequences, and a computed ID. The UTXO
exercise intentionally uses a smaller type with a supplied ID and Coinbase
flag. It does not parse bytes or check signatures. These are separate packages,
not types you can paste together unchanged. The capstone later combines the
responsibilities under one explicit interface.
Trace a complete 63-byte transaction
Use Version=2, one input referring to an all-zero ID at index 3, input script
aa bb, Sequence=ffffffff, one output of value 1000 with script 51, and
LockTime=0. Scripts are opaque here; recognizing an opcode is not necessary.
Offsets below are zero-based and ranges include both ends.
| Offsets | Field | Bytes |
|---|---|---|
| 0–3 | Version | 02 00 00 00 |
| 4 | Input count | 01 |
| 5–36 | Previous transaction ID | 32 zero bytes |
| 37–40 | Previous output index | 03 00 00 00 |
| 41 | Input script length | 02 |
| 42–43 | Input script | aa bb |
| 44–47 | Sequence | ff ff ff ff |
| 48 | Output count | 01 |
| 49–56 | Value 1000 | e8 03 00 00 00 00 00 00 |
| 57 | Output script length | 01 |
| 58 | Output script | 51 |
| 59–62 | LockTime | four zero bytes |
Double-SHA256 these 63 raw bytes to get the raw digest, displayed here as hex:
52f480cc97cd7eba3825a9f2874370dbb69eb11a8b743aab807235ecea89a497.
The exercise returns raw digest bytes; do not reverse them to match a wallet's
presentation convention. The field arrangement follows the simplified legacy
format in the Bitcoin transaction reference;
our unsigned values, opaque scripts, and exact parser contract remain authoritative.
Complete the parser decisions
After reading the input script length at offset 41, a parser knows it needs two script bytes and a four-byte sequence. It must check availability before slicing. At each variable-length field, compare its length with the remaining bytes before converting or allocating. An attacker-controlled count is not a safe allocation hint.
Change the example to an input script of length 253, leaving every other field unchanged. Predict the total length and the new output-count offset.
Check the shifted boundaries
The script gains 251 bytes, and its length prefix grows from one to three bytes.
The total becomes 63 + 251 + 2 = 316; the output count moves from 48 to 301.
Changing only the script bytes and forgetting the prefix growth shifts every
later field incorrectly.
A CompactSize primitive may leave trailing bytes for its caller. In contrast,
DeserializeTx must consume the entire transaction buffer. Extra bytes after
LockTime, truncated fields, and noncanonical counts all fail. Implement
Transaction Serialization and test these
failure classes separately from the known digest above.
Derive value conservation on a real history
Start with UTXOs A:0=7, A:1=5, and B:0=9. Transaction T spends both A
outputs and creates T:0=8, T:1=3. Distinct inputs total 12, outputs total 11,
so its fee is 1. On success, the new map is {B:0=9, T:0=8, T:1=3}.
The total live value changes from 21 to 20; the missing unit is the fee.
A validator that sees [A:0,A:0] must reject the repeated outpoint. Counting
7 twice would appear to fund an output of 13, but there is only one output to
consume. A transaction spending [A:0,missing] also fails. If the implementation
deletes A:0 while checking the first input, rejection has already destroyed money.
Use two phases. First read all inputs, reject repeats or missing entries, and sum inputs and outputs with checked uint64 addition. Only after every condition passes should application delete inputs and insert outputs by their indices. The stated exercise assumes fresh transaction IDs; it does not implement the capstone's additional live-output collision check.
Finish the state trace
After T above, U spends T:0=8 and creates U:0=6; V then tries to spend
A:1=5. Determine U's fee, the live map after U, and whether V changes it.
Check the updates
U's fee is 2. The map becomes {B:0=9, T:1=3, U:0=6}. V fails because T already
consumed A:1. Rejection leaves the map unchanged. Replaying U again must also
fail because T:0 is now spent.
Coinbase is the explicit exception to ordinary input funding: this exercise recognizes zero inputs and at least one output, returning fee zero. It cannot know a block's subsidy or total fees, so it cannot enforce the reward limit. That check belongs in block validation. All value sums that are checked in the stated ordinary-transaction contract must reject overflow rather than wrap.
From tests to an explanation
Implement UTXO Validation. Keep a copy of the input map when testing rejection; verify equality afterward. Cover a repeated input, an already-spent input, insufficient value, overflow, and a successful multi-output transaction. Explain why validation itself must be read-only.
Then take the checkpoint. As a transfer challenge, describe why two transactions that each validate against the original map can conflict when applied in sequence. The block assembler must validate against evolving state, and the node must validate against the candidate branch's parent state rather than whichever branch is currently active.
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.
- Step 1
Transaction Serialization
- Step 2
UTXO Validation
- Step 3
Transactions & UTXO Checkpoint
Trace exact representations and state, diagnose a failure, and return to its specific explanation.