UTXO and account models · Lesson 4 of 6
Reconstructing balances and histories
8 min read
"How much did this address hold on that date, and how did it get there?" is one of the most common questions an analyst is asked, whether for an exchange's risk review, a court-appointed expert's report or a journalist's story. There are two ways to answer it, deriving the balance from history or reading it from chain state, and a careful analyst does both and explains any difference. This lesson shows how for UTXO coins, native account balances and tokens.
Two routes to a balance
| Route | How | Strengths | Weaknesses |
|---|---|---|---|
| Derive from history | Add up every credit and debit up to a block | Explains each movement; works in a warehouse | Easy to miss a category of movement |
| Read from state | Ask a node for the balance at a block | Authoritative for that chain and block | No explanation; old blocks need archive state |
The derived figure is what goes into a report, because it tells the story. The state figure is the check. When they agree, the history is complete; when they differ, something is missing, and the size of the gap usually says what.
UTXO balances
On a UTXO chain the balance of a set of scripts at height H is the sum of outputs to those scripts created at or before H and not spent at or before H (see the lesson The UTXO model for analysts). A worked example for one address:
| Outpoint | Created at | Amount (BTC) | Spent at |
|---|---|---|---|
4f3a…c21e:1 | 880,100 | 0.4000 | 880,900 |
a91c…3b07:0 | 880,450 | 1.2500 | not spent |
77d2…e4a0:3 | 881,020 | 0.0800 | 885,300 |
c0b5…91f2:0 | 884,700 | 0.3000 | not spent |
At height 882,000 the unspent outputs are a91c…3b07:0 and 77d2…e4a0:3: 1.33 BTC. At height 886,000 they are a91c…3b07:0 and c0b5…91f2:0: 1.55 BTC. Total received is 2.03 BTC and total sent 0.48 BTC, and received minus sent at 886,000 gives the same 1.55 BTC: a useful internal check.
Points to watch:
- A wallet is not an address. A person's holdings span many addresses and often several script types. If a wallet's output descriptors or extended public key are known (for example, supplied by the owner in a dispute), derive all its addresses, including change addresses, before summing.
- Unconfirmed and reorganized data. Mempool transactions are not balances. Snapshot at a height deep enough to be stable (see the lesson Blocks and headers as data).
- Coinbase outputs less than 100 blocks old exist but cannot yet be spent; say so if a miner's balance is in question.
- State checks are limited: Bitcoin Core's
scantxoutsetsees only the current UTXO set, so historical checks need an indexer or a second, independently built dataset.
Native balances on account chains
For ETH and other native currencies, a balance at block N decomposes into categories that live in different datasets:
balance(N) = value received in successful top-level transactions
+ value received in successful internal calls (traces)
+ consensus-layer withdrawals and fee-recipient rewards
- value sent in successful top-level transactions and internal calls
- gas fees paid on every transaction sent, successful or failed
(Genesis allocations and pre-Merge mining rewards add terms for older addresses.) A worked example for 0xA1b2…9F at block N:
| Item | Source | Amount (ETH) |
|---|---|---|
| Deposit from an exchange | Transaction | +5.000000 |
| Payout from a contract | Trace (internal call) | +1.000000 |
| Validator withdrawals | Block withdrawals list | +0.032000 |
| Payment sent | Transaction | −2.500000 |
| Gas on that payment: 21,000 × 20 gwei | Receipt | −0.000420 |
| Failed transaction: value not sent | Receipt status = 0 | 0 |
| Gas on the failed transaction: 70,000 × 30 gwei | Receipt | −0.002100 |
| Derived balance | 3.529480 |
If eth_getBalance(0xA1b2…9F, N) returns 3.529480 ETH, the history is complete. The typical failures leave fingerprints in the gap. Ignoring traces under-states by exactly the internal credit (1.0). Counting the failed transaction's value under-states by its value; forgetting its gas over-states by 0.0021. Missing withdrawals produces a gap made of small, regular amounts. Gas is gas used multiplied by the effective gas price from the receipt, not the gas limit, plus blob fees for type 3 transactions.
Token balances: events versus balanceOf
Token balances live in the token contract's storage. They can be derived by summing Transfer events, or read with balanceOf(holder) through eth_call at block N. The derived version in SQL, using a decoded transfers table:
-- Token balances at block 21000000, derived from Transfer events
WITH moves AS (
SELECT to_address AS holder, amount_raw AS delta
FROM token_transfers
WHERE token_address = '0x5d3a…7c10' AND block_number <= 21000000
UNION ALL
SELECT from_address AS holder, -amount_raw AS delta
FROM token_transfers
WHERE token_address = '0x5d3a…7c10' AND block_number <= 21000000
)
SELECT holder, SUM(delta) / POWER(10, 6) AS balance -- token has 6 decimals
FROM moves
GROUP BY holder
HAVING SUM(delta) <> 0;
Each transfer becomes two rows, a credit to the receiver and a debit to the sender, and the sum per holder is the balance. Mints appear as transfers from the zero address, so the zero address ends with a negative balance equal to everything ever minted minus everything burned to it: a built-in supply check. Keep raw integer amounts until the final division; converting each row to decimals first accumulates rounding error. Use a wide decimal type, because raw amounts can exceed 64-bit integers.
Events and balanceOf disagree for known reasons:
- Rebasing tokens change every holder's balance through a global multiplier, with no per-holder
Transferevent. Event sums go stale immediately. - Interest-bearing tokens compute
balanceOffrom a growing index; events capture principal movements, not accrued interest. - Fee-on-transfer tokens credit the receiver less than the sender sends; most emit events for the net amounts, but some emit the gross amount.
- Non-standard contracts, mostly older ones, mint without emitting
Transfer, or issuer freeze-and-burn functions emit a custom event instead.
For these tokens, state is the truth and events are only the story of explicit transfers; the lesson Reading token transfers covers them in detail.
Snapshots at a block height
A balance "on March 31" is ambiguous until it is tied to a block. Convert the time to the last block whose timestamp is at or before the cut-off, in UTC, and record both the height and the block hash. On Ethereum, state methods such as eth_getBalance and eth_call accept either a block number or, under EIP-1898, a block hash, which guarantees the answer comes from that exact block even if a reorganization later replaces the block at that height. Querying old blocks needs a node that keeps historical state (an archive node or provider).
For multi-chain snapshots, each chain has its own block for the same instant: Bitcoin's roughly 10-minute blocks and Ethereum's 12-second slots will give different granularity, and a rollup's L2 block may reflect deposits that its L1 has only just included. State the block chosen on each chain in the report.
Reconciliation checks
A reconstruction is not finished until it has been checked. Useful tests:
- Derived versus state: compare derived balances with node state for every address in scope, or a random sample at several blocks for large sets.
- Conservation: the sum of all derived token balances, excluding the zero address, should equal
totalSupply()at the same block; on a UTXO chain, the total of the UTXO set should not exceed total issuance. - No negative balances: a negative running balance means a missing credit, most often an internal call, a mint without an event or a withdrawal.
- Boundaries: the balance before an address's first activity is zero, and the balance after a known sweep matches what the sweep left.
- Residuals explained: set a tolerance in advance (for example, zero for native balances, a stated amount for rebasing tokens) and document the cause of every difference.
| Symptom | Likely cause |
|---|---|
| Derived balance too low by amounts matching contract payouts | Missing internal calls from traces |
| Derived balance too high by small amounts | Gas fees on failed transactions not deducted |
| Small regular unexplained credits | Consensus-layer withdrawals or fee-recipient rewards |
Token holders drift from balanceOf daily | Rebasing or interest-bearing token |
Sum of balances differs from totalSupply() | Mints or burns without Transfer events |
Exam focus: the difference between deriving a balance from history and reading it from state, and why reports need both; computing a UTXO balance at a height and the received-minus-sent check; the components of a native account balance, especially internal calls, withdrawals and gas on failed transactions; computing gas as gas used × effective gas price; deriving token balances from Transfer events and the zero-address supply check; why rebasing, interest-bearing and fee-on-transfer tokens break event sums; pinning snapshots to a block height and hash (EIP-1898); standard reconciliation checks and what each kind of gap suggests.