Q1: What is stateObject and how does it relate to StateAccount?
StateAccount is the disk format — a minimal 4-field struct representing an account on disk:
type StateAccount struct { Nonce uint64 Balance *uint256.Int Root common.Hash // storage trie root hash CodeHash []byte}This is just a “snapshot on paper” — enough for storage, but not enough for the EVM to work with during execution.
stateObject (core/state/state_object.go) is the in-memory working copy that wraps StateAccount with everything needed at runtime:
type stateObject struct { db *StateDB // back-pointer to parent container address common.Address // account address addrHash common.Hash // keccak256(address), the trie key
origin *types.StateAccount // original data from disk (before any changes) data types.StateAccount // current data (with all modifications applied)
trie Trie // this account's storage trie, lazily opened code []byte // contract bytecode, loaded on demand
originStorage Storage // values read from disk dirtyStorage Storage // values modified in the current transaction pendingStorage Storage // modifications accumulated across transactions uncommittedStorage Storage // changes since last commit
dirtyCode bool selfDestructed bool newContract bool}Analogy: StateAccount is your bank passbook (just balance and serial number). stateObject is the teller’s working screen — it has the passbook data plus modification history, rollback capability, and caches.
Four-layer storage cache
Storage is just map[common.Hash]common.Hash. The four maps form a layered cache:
| Map | Lifetime | Purpose |
|---|---|---|
originStorage | Block | ”Clean” baseline values read from disk |
dirtyStorage | Transaction | Values modified in the current transaction; cleared after Finalise() |
pendingStorage | Block | Accumulated modifications across all transactions; written to trie |
uncommittedStorage | Since last commit | All changes since last trie commit |
The layers exist because rollback granularity differs:
REVERTwithin a transaction → onlydirtyStorageneeds to be undone;pendingStorageis unaffected- Between transactions →
Finalise()mergesdirtyStorageintopendingStorage, then clearsdirtyStoragefor a clean workspace
With a single map, a transaction rollback couldn’t distinguish “this transaction’s changes” from “previous transactions’ changes.”
origin vs data
origin— the account data as loaded from disk. Never modified. Serves as the rollback reference.data— the live, evolving state. Updated on every balance change, nonce increment, etc.
Q2: When is a stateObject created? Is it only created during transaction writes?
No. A stateObject is created on any access to an account — reads included, not just writes.
The key function is getStateObject:
func (s *StateDB) getStateObject(addr common.Address) *stateObject { // 1. Already in memory cache? Return it. if obj := s.stateObjects[addr]; obj != nil { return obj } // 2. Destroyed in this block? Return nil. if _, ok := s.stateObjectsDestruct[addr]; ok { return nil } // 3. Load from disk, wrap as stateObject, cache it. acct, err := s.reader.Account(addr) // ... obj := newObject(s, addr, acct) s.setStateObject(obj) return obj}Both GetBalance() (read) and AddBalance() (write) call this function. So any query to an account triggers stateObject creation and caching.
Lifecycle
Once created, a stateObject is reused for the entire block, not just one transaction:
Block #100 starts → StateDB created
Tx 1: GetBalance(alice) → load from disk, create stateObject, cache Tx 1: AddBalance(alice) → cache hit, reuse same stateObject Tx 2: GetBalance(alice) → cache hit, still the same stateObject Tx 2: GetBalance(bob) → load from disk, create new stateObject
Commit()
Block #100 ends → StateDB discarded, all stateObjects GC'dThe stateObjects map (map[Address]*stateObject) ensures each address is loaded from disk at most once per block.
Q3: What is StateDB and what does it do?
StateDB (core/state/statedb.go) is the central hub for all state operations within a block. The EVM never touches tries or databases directly — it only talks to StateDB:
stateDB.GetBalance(addr) // read balancestateDB.AddBalance(addr, amount) // add balancestateDB.GetState(addr, slot) // read storagestateDB.SetState(addr, key, val) // write storagestateDB.Snapshot() // take snapshotstateDB.RevertToSnapshot(id) // rollbackPosition in the architecture
EVM executes transactions │ ▼ StateDB ← all state reads/writes go through here │ ├─ stateObject (alice) ├─ stateObject (bob) ├─ stateObject (contract) │ ▼ Trie + Reader ← MPT from Chapter 03 │ ▼ triedb → ethdb ← disk storageKey fields
type StateDB struct { db Database // bridge to trie database and snapshot layer reader Reader // loads accounts/storage from disk trie Trie // account trie (lazily loaded)
originalRoot common.Hash // state root at block start
stateObjects map[common.Address]*stateObject // cache of all accessed accounts stateObjectsDestruct map[common.Address]*stateObject // accounts destroyed in this block mutations map[common.Address]*mutation // which accounts were modified
journal *journal // undo log for rollback support}stateObjects— core cache. Every account accessed during this block lives here.journal— undo log. Records old values before each write;RevertToSnapshot()replays entries in reverse.mutations— tracks which accounts are modified/deleted; only these are processed duringCommit().trie— account trie, opened lazily on first write, not atStateDBcreation.
Lifecycle
One StateDB per block, disposable:
1. New(parentRoot, db) → bind to parent block's state root2. Execute tx 1, 2, 3... → GetBalance/SetState/AddBalance...3. Finalise() → per transaction: dirty → pending, clean up empty accounts4. IntermediateRoot() → flush to trie, compute new state root (for block header)5. Commit() → write trie nodes + contract code + snapshot to database6. Discard → GC reclaims; next block creates a fresh StateDBRelationship with stateObject
| StateDB | stateObject | |
|---|---|---|
| Granularity | Entire world state | Single account |
| Count | 1 per block | 1 per accessed address |
| Role | Route requests, manage trie, coordinate commit | Cache account data, track dirty storage |
StateDB is the manager; stateObject is the managed individual.
Q4: Is StateDB preserved across transactions?
Yes across transactions, no across blocks.
Across transactions — preserved
Block #100 → StateDB created (bound to block #99's state root)
Tx 1: transfer(alice → bob, 1 ETH) GetBalance(alice) → load from disk, create stateObject, cache AddBalance(bob) → load from disk, create stateObject, cache Finalise() → dirty → pending
Tx 2: transfer(alice → charlie, 2 ETH) GetBalance(alice) → cache hit, same stateObject from Tx 1 balance reflects Tx 1's deduction, not the stale disk value Finalise() → dirty → pending
Tx 3: call(bob.someMethod()) GetBalance(bob) → cache hit, balance reflects Tx 1's depositThis is necessary for two reasons:
- Correctness — transactions execute sequentially. Tx 2 must see Tx 1’s results. If each transaction reloaded from disk, Tx 1’s balance change would be lost.
- Performance — a block may have hundreds of transactions touching the same hot contract (e.g., Uniswap). Cache once, reuse hundreds of times via in-memory map lookup instead of repeated disk reads.
Across blocks — discarded
After Commit(), the entire StateDB is thrown away. Same disposable design as the Trie struct from Chapter 03 — once committed, internal state is no longer complete. A fresh StateDB must be created from the new root for the next block.
Block #100: StateDB created → execute txs → Commit() → discardedBlock #101: new StateDB created (from block #100's root) → all caches start emptySome information may be outdated






