T1 · What Is Blockchain Data, Really?
What is actually stored on a blockchain?
- Capabilities practiced
- BuilderOnchain Literacy
- Hands on
- Write a script that fetches the latest ten blocks and prints each block's transaction count, gas used, and the recipient of its first transaction.
- AI Lab
- Have AI explain the difference between State and Event, then give an example where looking only at Events leads to a wrong conclusion.
A Real-World Problem
A product team gives you a request: notify a user whenever a particular address receives USDC.
It looks like a five-minute job. You open the documentation and immediately face the first question: where should you read this "receipt"?
- Check the address balance every few seconds and notify when it increases?
- Listen for Transfer events from the USDC contract?
- Fetch every block and parse its transactions one by one?
- Take the easy route and use a third-party API?
All four approaches can produce a demo, but they behave very differently in production. Some miss transfers. Some send duplicate notifications. Some notify a transfer that disappears after a chain reorganization.
Choosing badly costs you correctness, not just performance. To choose well, you first need to understand what a chain actually stores.
Thought Experiment
Think of a chain as two things, not one.
First: an append-only transaction journal.
At intervals, a new block collects transactions in order. Once written, an entry cannot be edited or inserted earlier in the queue. This journal records actions only: who called what, with which parameters, and how much fee they paid.
Second: a table of current state derived from that journal.
It shows each account's current balance and the current value of each contract storage slot. This table is not itself "written" into the chain. It is the result of executing every transaction in order, starting from the genesis block.
Now for the key question: when a transaction executes, which parts of state change?
The journal does not say so directly. It records only something like "call contract A's transfer function with address B and amount 100." To learn how many storage slots changed, or whether other contracts were called along the way, you must actually execute it.
That is why there is a third thing: logs. During execution, a contract can announce, "I just transferred 100 from B to C." That announcement is recorded in the transaction receipt for the outside world to hear.
- Transaction: what I want to do
- State: what the world became
- Event: what I chose to tell you I did
Your Decision
Return to the notification request. Which approach do you choose?
What Happens
All four choices reveal the same thing: what you call "on-chain data" is actually one projection of it.
| What you read | What it really is | What it misses |
|---|---|---|
| Account balance | A slice of current state | Intermediate steps and multiple changes within a block |
| Transaction list | Externally initiated actions | Calls and transfers inside contracts |
| Event logs | Statements emitted voluntarily by contracts | Contracts that emit no Event or emit one nonstandardly |
| Execution trace | The full call tree | Very little, but it is slow and expensive |
Which layer to read depends on what your business can tolerate. State is enough to display a balance. Reconciliation of funds requires traces. For notifications and indexing, Events offer the best cost-to-benefit ratio.
There is no single "most accurate layer," only the layer that matches your need.
Build a Model
Lay out a chain's data in layers:
Block block: container for a batch of transactions
├─ header parent hash, height, timestamp, state root
└─ transactions[] transactions ordered in the block
└─ Transaction one transaction: from, to, value, data, nonce, gas
└─ Receipt produced only after execution
├─ status success or failure
├─ gasUsed gas actually consumed
└─ logs[] events emitted by contracts
└─ Log topics (indexed for filtering) + data (content)
State derived by executing all transactions in order
├─ account balance account balance
├─ contract code contract code
└─ contract storage contract storage slotsFour points will save you many detours:
- A Receipt exists only after execution. Inclusion in a block does not mean success; check
statusseparately. Showing "success" when a transaction was merely broadcast is a common frontend bug, covered further in T14. - Log topics are indexed; data is not. This determines which filters are efficient: contract address and topics work, but ordinary fields inside event data do not.
- State tells you only "now." Reading state at an old block height requires an archive node, covered in T4. This is a common cost trap.
- An Event is not state. A contract can emit an event that contradicts the actual state change, and attacks have exploited this. To confirm state, read state.
What It Is Called
A container for a batch of transactions. The parentHash in its header links blocks into a chain; changing a historical block invalidates everything after it.
A request initiated by an external account to change state. It records "what to do," not "what the outcome was."
The combined current balances, contract code, and storage of all accounts. It is the result of executing every historical transaction, not a separate copy of data written into the chain.
The result recorded after a transaction executes: whether it succeeded, how much gas it used, and which logs it produced.
To learn whether a transaction succeeded, check the Receipt's status, not merely whether the transaction is in a block.
A structured record that a contract deliberately writes to a receipt for off-chain systems to read. The contract itself cannot read it.
topics are indexed fields, with the first usually being a hash of the event signature; data holds the remaining content.
A call or transfer made by a contract during execution.
It is not an independent transaction, so it does not appear in the transaction list. Only an execution trace can reconstruct it. This explains many cases of "the block explorer shows the money, but my program did not find it."
Hands-on
Call JSON-RPC directly, without an SDK. The goal is to see the raw shape of the data.
Get the latest block height.
curl -s -X POST <your-RPC-URL> \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}'The response is hexadecimal. This will trip you up repeatedly: almost all numeric values returned by a chain are hexadecimal strings.
Fetch the complete contents of a block.
curl -s -X POST <your-RPC-URL> \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getBlockByNumber","params":["latest",true]}'When the second parameter is true, you receive full transactions; when it is false, you receive only transaction hashes. Compare response sizes to see why indexers must control this parameter.
Fetch a transaction receipt and find status and logs.
curl -s -X POST <your-RPC-URL> \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getTransactionReceipt","params":["<transaction-hash>"]}'Compare that transaction with its block explorer page and confirm that the fields match.
Filter events over a block range.
curl -s -X POST <your-RPC-URL> \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getLogs","params":[{
"fromBlock":"0x...","toBlock":"0x...",
"address":"<token-contract-address>",
"topics":["<Transfer-event-signature-hash>"]}]}'This is the core of every indexer. Increase the block range until the RPC endpoint rejects it. That limit directly affects how your indexer partitions its requests.
Turn it into a script that prints a table.
Fetch the ten most recent blocks. Print each height, timestamp, transaction count, total gas used, and first transaction recipient.
This is the first version of your stage project.
Use a testnet or public read-only endpoint throughout. No private key is needed. This chapter sends no transactions.
AI Lab
Ask in two steps. The second matters much more than the first:
Step one:
Explain the difference between State and Event on a blockchain, and why they may disagree.
Step two:
Give me three concrete situations in which an indexer listening only for Transfer events would reach the wrong conclusion.
For each, explain what it misses, what happens as a result, and the correct approach.The second step is the point of this Lab. Asking a model to find counterexamples to its own solution is more useful than asking it for the solution alone.
After you get three scenarios, find a real on-chain example to verify one. If you find it, the pitfall is real. If you cannot, ask for "a real contract address or transaction hash"—and you may discover that the hash it gives does not exist.
That is an experience worth having firsthand.
Verify these points yourself after using AI
- Its JSON-RPC method names and parameter order match the official specification
- Its indexed fields include only topics; it does not claim that fields inside data can be filtered
- You can find a real on-chain case matching its example of something missed by listening only for Events
- Its sample code converts between hexadecimal and decimal correctly
- It does not mistake inclusion in a block for transaction success
Real-World Case
Transfers of native coins such as ETH and SOL do not pass through a contract, so they do not emit a Transfer event.
An indexer listening only for Transfer events will miss them entirely. This is a classic beginner trap: token transfers produce notifications, but native-coin transfers do not.
Correct approach: use the transaction's value field for native coins, or use execution traces to cover internal transfers.
Some tokens automatically deduct a portion of every transfer as a tax or burn.
Their Transfer event may report "sent 100" while the recipient actually receives only 97. A system that accounts from events alone will calculate a balance that never reconciles.
Correct approach: when reconciling funds, treat state as authoritative and Events only as indexing clues.
A user transfers through an aggregator contract. The outer transaction's to points to the aggregator, not the final recipient.
A system reading only the transaction list attributes the money to the wrong party. Block explorers show "internal transactions" because they run full execution traces.
Correct approach: use traces for complete money flows and Events for notifications alone.
The chain tip can reorganize: a block you have already processed is rolled back, and its transactions may enter a different block or never reappear.
If you send a notification as soon as one block confirms, a reorganization can leave you with a "notification for a transfer that never existed."
Correct approach: choose a confirmation count that matches the business risk, and make your indexer capable of rollback. T17 covers this in full.
Change One Variable
On-chain state changes correctly, but off-chain systems cannot see it.
This shows something important: observability is part of contract design, not a built-in feature of the chain. In T7, deciding when to emit an event and which fields it should contain is a deliberate design task.
A normal node retains only a recent portion of historical state and will return an error for an older query. You need an archive node, whose storage cost is an order of magnitude higher.
That choice determines the cost structure of many data products. T4 covers the trade-off between cost and capability.
The model has a different shape: no global state tree, with data spread across individual accounts. Transactions must declare in advance which accounts they will read and write, allowing parallel execution.
Logs are handled differently too. T6 compares the models in full—comparison is the fastest way to understand either one.
Takeaway Questions
This chapter answered "how can we correctly read what happened on-chain?" It sounds basic, but everything that follows depends on it. If you read the data incorrectly, everything built on top is wrong.
Why do we need a blockchain? Its distinctive answer here is that anyone can independently verify the same history. By running your own node, you can reach the same result as everyone else without trusting an API provider.
If your indexer misses a transfer, who bears the loss? Usually the user, while you might never learn about the miss. That is why indexing correctness depends on replayability, not on "it looks fine while running."
Chapter Check
No. Failed transactions are included too, and they still pay a fee.
Check the receipt's status for success. Showing "successful" for a transaction merely because it is on-chain is one of the most common frontend bugs in this field.
Yes. An Event is a voluntary statement from a contract, which can emit an event that does not match the state change.
Therefore, Events are useful for indexing and notifications, but financial reconciliation must rely on State.
Only topics are indexed; data is not. Contract authors decide which fields become topics when they define an event, usually no more than three.
This constraint affects your index design: to filter by a field, it must be declared as indexed when the event is defined.
A reference solution:
- Use
eth_getLogswith the contract address and Transfer topic, fetching block ranges in batches instead of polling balances. - Wait for several confirmations before notifying; choose the number based on the risk the business can bear.
- Record the last processed block height as a checkpoint and support replay from any height.
- Handle reorganizations: when parent hashes no longer connect, roll back affected blocks and process them again.
- Reconcile periodically against state queries and alert on differences.
If you included points 3 and 4, you are already ahead of most people who have only built demos.
Key idea
A chain stores state and the transactions that change it; events are merely a projection for the outside world.