Bitcoin does not track balances like a bank or an account‑based chain. It tracks **unspent transaction outputs (UTXOs) **: discrete chunks of value locked by scripts. The global UTXO set is the ledger state.[1]

This article is a mental model for backend and infra engineers. I’ll treat Bitcoin as a state machine over a UTXO set, compare it to account models, walk through how nodes actually maintain this state, and end with a minimal Kotlin UTXO tracker. The goal is that you can reason about UTXO semantics the same way you reason about tables and indexes in a database, and implement a basic tracker without guessing.


1. Core Mental Model: Transactions as State Transitions

Think of Bitcoin as a sequence of state transitions over a set of UTXOs.

Each transaction:

  • Consumes some existing UTXOs as inputs.
  • Produces new outputs which may become UTXOs.

Only outputs that are not yet referenced by any valid transaction input are in the UTXO set.

[ Coinbase TX ]
       |
       v
  +--------------+
  |  TX #1       |
  |  out0: 1 BTC |----.
  |  out1: 2 BTC |--. |
  +--------------+  | |
                    | |
                    v v
              +-------------+
              |  TX #2      |
              |  in0: #1:0  |
              |  in1: #1:1  |
              |  out0: 2.5  |
              |  out1: 0.4  |
              +-------------+

Once an output is used as an input in a valid transaction, it is removed from the UTXO set permanently.[1] There is no “balance update”; there is only consume some outputs, create new ones.

When your wallet says “3.1 BTC”, that is the sum of UTXOs whose locking scripts you can satisfy. Nothing in the protocol stores “3.1” anywhere as a single field.


2. What Exactly Is a UTXO?

A UTXO is an unspent output identified by (txid, index) plus its value and locking script.[1]

(txid, output_index, value, locking_script)
  • txid: hash of the transaction that created the output.
  • output_index: 0‑based index in the outputs array.
  • value: amount in satoshis.
  • locking_script (scriptPubKey): the conditions to spend the output.

The global UTXO set is just a big key‑value map:

+-----------------------------+
|        UTXO Set             |
+-----------------------------+
| (txA, 0) -> {0.8 BTC, ...}  |
| (txB, 1) -> {0.3 BTC, ...}  |
| (txC, 2) -> {1.2 BTC, ...}  |
| ...                         |
+-----------------------------+

Every block mutates this utxo set: remove spent entries, insert new ones.[2] That’s the whole state.


3. Spending, Change, and Indivisibility

From the protocol’s point of view, UTXOs are indivisible. You can’t partially spend a UTXO. You consume it fully and return “change” as a new output.[3]

Example: you hold a single 1 BTC UTXO and pay 0.3 BTC.

Before:
  UTXO Set:
    (txA,0) -> 1.0 BTC (yours)

TX:
  inputs:
    (txA,0) [1.0 BTC]

  outputs:
    (txX,0) -> 0.3 BTC (recipient)
    (txX,1) -> 0.7 BTC (your change)

After:
  UTXO Set:
    (txX,0) -> 0.3 BTC (recipient)
    (txX,1) -> 0.7 BTC (yours)

The original (txA,0) no longer exists in the live state. Two new UTXOs take its place.

This has practical consequences:

  • Your UTXO patterns are visible to any indexer; they are part of your on‑chain footprint.
  • Coin selection (which UTXOs you pick) affects fee efficiency, fragmentation, and privacy.[4]
  • Wallets and services can periodically consolidate many small UTXOs into fewer larger ones to keep state manageable.

4. The UTXO Set as the Ledger State

A Bitcoin full node maintains a UTXO set in a key–value store (Bitcoin Core uses LevelDB / RocksDB variants) plus in‑memory caches.[1][2]

Model it like this:

Key                         Value
-------------------------------------------------------------
(txid:abc..., index:0)  ->  { value:  50_000_000, script: ... }
(txid:def..., index:1)  ->  { value: 200_000_000, script: ... }
...

When a new transaction arrives (via mempool or block), the node:

  1. For each input:
    • Looks up (txid, index) in the UTXO set.
    • Fails if missing → double spend or invalid reference.
  2. Retrieves the locking_script for that UTXO.
  3. Executes script validation with the unlocking data (scriptSig / witness).[1]
  4. Checks economic rules: sum(inputs) ≥ sum(outputs), fee sane, etc.
  5. If everything passes:
    • Removes all referenced UTXOs.
    • Inserts all new outputs as fresh UTXOs.

Nodes apply this in blocks, not one transaction at a time in isolation, so they can roll back whole blocks during chain reorganisations.[2]

Double‑spend detection

Double spends are just conflicting claims on the same key:

  • If two transactions reference the same (txid, index), only the one that ends up in the best chain “wins”.[3][6]
  • After that block is final enough for your risk profile, the losing transaction is irrevocably invalid; its inputs no longer exist in the UTXO set.

Conceptually: an input is valid iff its referenced UTXO is present in the UTXO set you trust.


5. UTXO vs Account Model

Many chains (Ethereum, most Cosmos chains) use an account model instead.[5][7][8] Comparing them clarifies what UTXOs buy you.

+----------------------+---------------------------+------------------------+
| Aspect               | UTXO Model                | Account Model          |
+----------------------+---------------------------+------------------------+
| State representation | Set of unspent outputs    | Map: account -> state  |
| Balance view         | Sum of owned UTXOs        | Balance field per acct |
| Updates              | Consume old, create new   | In-place balance       |
|                      | outputs                   | increments/decrements  |
| Concurrency          | Per-UTXO independence;    | Shared global state;   |
|                      | easy parallelism          | hotspots on big accts  |
| Privacy              | Many short-lived outputs; | Long-lived addresses;  |
|                      | graph analysis required   | easier to track        |
+----------------------+---------------------------+------------------------+

In practice:

  • Validation can be parallelised by UTXO; conflicts are obvious.
  • You pay with more complex wallet logic and more involved indexing.
  • UTXOs give you explicit, localised state transitions and straightforward double‑spend prevention.

6. How Nodes Maintain the UTXO Set

A high‑level node pipeline looks like this:

[ P2P Network ]
       |
       v
[ Block + TX Download ]
       |
       v
[ Validation ]
  - syntax / size
  - scripts / signatures
  - fees, consensus rules
       |
       v
[ UTXO Set Update ] ---> [ KV Store + Caches ]

For each block:

for tx in block.transactions:
    # inputs must exist in UTXO set
    # scripts must validate
    # sum(inputs) >= sum(outputs)
    # (fees = sum(inputs) - sum(outputs))

    # state transition
    remove all spent UTXOs
    insert all new outputs as UTXOs

All of this is normally done in a single atomic batch per block. If a reorg happens, the node discards the old blocks, rolls back the UTXO modifications, and applies the new chain.[2]

Production note: The heavy part in practice is not script evaluation, it’s storage: reading / writing a large key‑value map at block cadence and keeping hot parts cached. UTXO semantics are simple; the performance story is mostly about I/O patterns and cache design.

7. Minimal UTXO Tracker: State and Flow (No Code)

You can think of a minimal UTXO tracker as three things:

  1. A UTXO table keyed by (txid, index).
  2. A stream of blocks containing transactions.
  3. A state transition that consumes inputs and produces new outputs.

1. Data model (conceptual)

Block
-----
- hash:         block hash
- height:       block height
- transactions: ordered list of Transaction

Transaction
-----------
- txid:         unique transaction id
- inputs:       list of TxIn
- outputs:      list of TxOut

Transaction Input (TxIn)
------------------------
- txid:         transaction id of the output being spent
- index:        index of that output in the previous transaction

Transaction Output (TxOut)
--------------------------
- value:        value (satoshis)
- script:       bytes (scriptPubKey)

The UTXO set is simply:

UTXO Set
========
Key              -> Value
-----------------------------------------------
(txid_A, 0)      -> { value: 5000, script: ... }
(txid_B, 1)      -> { value: 1200, script: ... }
(txid_C, 2)      -> { value: 3000, script: ... }
...

2. Block processing pipeline

At a high level, the tracker sits behind a full node and sees an ordered stream of validated blocks:

[ Full Node ]  --validated blocks-->  [ UTXO Tracker ]
                                             |
                                             v
                                     [ UTXO Set State ]

Zooming into the tracker:

        Incoming Block
              |
              v
    +-------------------+
    | For each tx in    |
    | block.transactions|
    +---------+---------+
              |
              v
  +------------------------+
  | Apply Transaction      |
  | 1. Spend inputs        |
  | 2. Create outputs      |
  +------------------------+
              |
              v
     [ Updated UTXO Set ]

3. Transaction application (state transition)

For each transaction in a block, the tracker performs two phases against the UTXO set:

Phase 1: Spend inputs
---------------------

For each input (TxIn):
  - Derive UTXO key = (txid, index)
  - Look up key in UTXO Set
  - If found:
      -> remove that entry from the UTXO Set
  - If not found:
      -> error: "unknown or already-spent input"

Phase 2: Create new outputs
---------------------------

For each output position i in tx.outputs:
  - Derive UTXO key = (txid, i)
  - Ensure this key is not already present
      -> if present: error "duplicate UTXO key"
  - Insert into UTXO Set:
      key   = (txid, i)
      value = { output.value, output.locking_script }

Visually:

         +-----------------------+
         |     UTXO Set (S)      |
         +-----------------------+
                     ^
                     |
             before block N
                     |
                     v
  +---------------------------------------+
  |   Apply Block N                       |
  |   - for each tx:                      |
  |       1. remove spent UTXOs from S    |
  |       2. add new outputs as UTXOs     |
  +---------------------------------------+
                     |
                     v
         +-----------------------+
         |   UTXO Set (S')       |
         +-----------------------+
                     ^
                     |
              after block N

Assumptions:

  • The full node has already:
    • verified transaction syntax,
    • checked scripts and signatures,
    • enforced consensus and fee rules.
  • The tracker only maintains derived state:
    • it trusts the node’s block order,
    • it does not re‑do consensus.

In other words, the UTXO tracker is “just” a deterministic map update:

New_State = Apply(Block, Old_State)

where Apply means “delete all referenced inputs, then insert all new outputs”.

8. Testing and Validation Strategies

Tests should exercise the state transition model, not just individual methods.

Good patterns:

  1. Toy graphs Construct small synthetic transaction graphs where you can compute the expected UTXO set by hand, then assert your tracker matches that after each block.

  2. Change and consolidation scenarios Include:

    • “Pay + change” patterns.
    • Transactions with multiple inputs and multiple outputs.
    • Consolidation transactions that merge many small UTXOs into a single larger one.
  3. Negative cases

    • Try to spend a non‑existent (txid, index); must fail.
    • Try to spend the same UTXO twice; the second attempt must fail.
    • Try to create conflicting outputs (duplicate (txid, index) pairs); must fail.
  4. Replay against a real node Take a small chain segment from a reference node, feed its blocks into your tracker, and compare your final UTXO set against the node’s reported UTXOs.[2][3]

You can describe most scenarios as tables:

Initial UTXOs  ->  Transactions Applied  ->  Expected UTXOs

and generate tests from that specification. The language is incidental; the invariants are not.


9. Production Notes and Failure Modes

Once you leave the lab, you run into operational concerns more than protocol puzzles.[2][4]

Some non‑academic pain points:

  • I/O and indexing dominate cost. Reading blocks, updating state, and serving queries stress your storage engine. Batch updates per block and careful index design matter more than micro‑optimizing the map.

  • Reorgs are rare but decisive. You need a rollback mechanism: either store per‑block diffs or maintain snapshots with a bounded rollback window. Design this from day zero; patching it in later is painful.

  • Mempool vs. chain views. Many applications need both “confirmed” and “pending” UTXO views. Keep the semantics clear: don’t silently mix them unless your domain explicitly wants that.

  • Privacy vs analytics. A rich indexer makes analytics easy, but also lowers the cost of de-anonymisation. Be honest about this in a multi‑tenant or regulatory context.[3][4]

From the trenches: Most “UTXO bugs” I’ve seen in production were not protocol misunderstandings. They were reorg edge cases, mismatches between node height and indexer height, or silent divergence between mempool and confirmed views. Other problems I observed are related to inconsistent views of the chain or miss-interpretation of concept such as confirmation or finality especially in combination of concept such as total or reserved amount which might be based on different confirmation level such as 1 block total vs 100 blocks reserved causing temporary inconsistent state.


10. Conclusion and Suggested Next Steps

The UTXO model is simple to state and powerful in practice:

  • The live state is a set of unspent outputs.
  • Transactions are pure state transitions: remove some entries, add new ones.
  • Double‑spend prevention and concurrency fall out of this explicit structure.[1][5][7]

With that model in mind, the Kotlin tracker above is almost trivial. A production‑grade indexer is “just” this logic plus persistence, indexing, and a robust rollback story.

Natural follow‑ups from here:

  • Protocol depth: transaction serialization, script types, SegWit / Taproot, and policy rules that affect what nodes accept into the mempool.[1][3][6]
  • Indexer architecture: how to build a horizontally scalable, multi‑chain indexer that consumes data from full nodes and exposes APIs tuned for your product.

Once you understand the UTXO set as a state machine, these are incremental layers, not new worlds.


References and Further Reading