Crypto OS
Technical Crypto OSStage 1 · Blockchain Developer Mental Model

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.

  1. Transaction: what I want to do
  2. State: what the world became
  3. Event: what I chose to tell you I did
These are three different kinds of data. An Event is a voluntary statement by a contract, not a complete record of state changes.

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 readWhat it really isWhat it misses
Account balanceA slice of current stateIntermediate steps and multiple changes within a block
Transaction listExternally initiated actionsCalls and transfers inside contracts
Event logsStatements emitted voluntarily by contractsContracts that emit no Event or emit one nonstandardly
Execution traceThe full call treeVery 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 slots

Four points will save you many detours:

  1. A Receipt exists only after execution. Inclusion in a block does not mean success; check status separately. Showing "success" when a transaction was merely broadcast is a common frontend bug, covered further in T14.
  2. 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.
  3. 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.
  4. 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

BlockBlock

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.

TransactionTransaction

A request initiated by an external account to change state. It records "what to do," not "what the outcome was."

StateState

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.

ReceiptReceipt

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.

Event / LogEvent / Log

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.

Internal TransactionInternal Transaction

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

Hands onRead ten blocks directly with JSON-RPCAny RPC endpoint + curl or NodeFree with a public endpoint

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

AI LabAsk AI to explain the difference between State and Event and give an example of a failureLevel 2 · AI Copilot

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

Native-coin transfers emit no Transfer event

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.

Fee-on-transfer tokens

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.

Internal transfers through a contract

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.

Chain reorganization

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

What ifThe contract author forgot to emit an Event

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.

What ifYou need the balance of an address from one year ago

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.

What ifSwitch to Solana

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

1
What problem does it solve?

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.

2
Why do we need blockchains?

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.

9
Who bears the risk?

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

Key idea

A chain stores state and the transactions that change it; events are merely a projection for the outside world.

Finished this chapter’s lab? Mark it completeView all progress

On this page