Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3
1057 words
5 minutes
Geth(4) QA

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:

MapLifetimePurpose
originStorageBlock”Clean” baseline values read from disk
dirtyStorageTransactionValues modified in the current transaction; cleared after Finalise()
pendingStorageBlockAccumulated modifications across all transactions; written to trie
uncommittedStorageSince last commitAll changes since last trie commit

The layers exist because rollback granularity differs:

  • REVERT within a transaction → only dirtyStorage needs to be undone; pendingStorage is unaffected
  • Between transactions → Finalise() merges dirtyStorage into pendingStorage, then clears dirtyStorage for 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'd

The 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 balance
stateDB.AddBalance(addr, amount) // add balance
stateDB.GetState(addr, slot) // read storage
stateDB.SetState(addr, key, val) // write storage
stateDB.Snapshot() // take snapshot
stateDB.RevertToSnapshot(id) // rollback

Position 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 storage

Key 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 during Commit().
  • trie — account trie, opened lazily on first write, not at StateDB creation.

Lifecycle#

One StateDB per block, disposable:

1. New(parentRoot, db) → bind to parent block's state root
2. Execute tx 1, 2, 3... → GetBalance/SetState/AddBalance...
3. Finalise() → per transaction: dirty → pending, clean up empty accounts
4. IntermediateRoot() → flush to trie, compute new state root (for block header)
5. Commit() → write trie nodes + contract code + snapshot to database
6. Discard → GC reclaims; next block creates a fresh StateDB

Relationship with stateObject#

StateDBstateObject
GranularityEntire world stateSingle account
Count1 per block1 per accessed address
RoleRoute requests, manage trie, coordinate commitCache 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 deposit

This is necessary for two reasons:

  1. 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.
  2. 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() → discarded
Block #101: new StateDB created (from block #100's root) → all caches start empty
Geth(4) QA
https://kehaozheng.vercel.app/posts/chainethgeth/04_qa/
Author
Kehao Zheng
Published at
2026-04-13
License
CC BY-NC-SA 4.0

Some information may be outdated