State Management is the process by which the blockchain tracks the current status of all participants and data, ensuring that every transaction results in a deterministic and cryptographically verifiable update to the system’s “global ledger.”
1. What is an Account?
In the storage system, an account is represented internally by a stateObject. Each account is identified by a unique Ethereum address (common.Address). An account consists of four primary components:
- Nonce: A counter indicating the number of transactions sent by the account (for EOAs) or the number of contracts created (for contract accounts).
- Balance: The amount of Ether (denominated in Wei) held by the account, managed using high-precision
uint256.Intvalues. - Storage Root: The root hash of a dedicated Merkle Patricia trie that contains all the account’s internal contract data.
- Code Hash: The cryptographic hash of the account’s EVM bytecode; for simple user accounts (EOAs), this is the hash of an empty string.
Accounts are stored as RLP-encoded blobs within the global state trie. If an account has no balance, a nonce of zero, and no contract code, it is considered “empty”.
2. How Balances Work
Balances are managed through the StateDB interface, which provides methods for modification and retrieval.
- Modification: The system uses
AddBalance,SubBalance, andSetBalanceto update an account’s funds. - Revertibility and Journaling: Every balance change is recorded in a
journal. This ensures that if a transaction fails or is reverted during execution, the system can callrevertToSnapshotto restore the balance to its original value. - Self-Destruct Handling: If an account is self-destructed, any Ether remaining in it is transferred to a beneficiary. If Ether is sent to an account after it has been self-destructed within the same block, that Ether is “burnt”.
3. State Roots and the Trie Hierarchy
The State Root is the “fingerprint” of the entire blockchain state at a specific block height. The architecture uses a nested hierarchy:
- Global State Root: Found in the block header, this is the root of the “Main Trie” containing all account addresses and their associated metadata.
- Account Storage Root: Each individual contract account has its own internal trie for its data slots. The hash of this internal trie’s root is stored within the account’s entry in the global trie.
- Cryptographic Integrity: Whenever data changes, the system calls
updateRootto recalculate the storage trie root, and then thecommitfunction to aggregate these changes into a new global state root.
4. The Mechanics of State Management
State management coordinates between several layers to balance speed and security:
- In-Memory Caches: The
stateObjectmaintains several “write caches” during a block’s execution:dirtyStorage: Tracks changes made within the current transaction.pendingStorage: Tracks changes made within the current block.uncommittedStorage: Marks entries that need to be pushed to the cryptographic trie at the end of the block.
- Snapshots: To avoid the high performance cost of navigating complex tries for every read, the
Snapshot Treemaintains a “flat” version of the state. It usesdiffLayers(in-memory journals of recent changes) and adiskLayer(the persistent base state). - The Final Write: When a block is finalized, the
CachingDBreplaces “dirty” in-memory nodes with their hashes, writes the new trie nodes to the disk-backed key-value store, and flattens the snapshot layers to ensure the system remains performant for the next block.
A stateObject represents an individual Ethereum account while it is being modified during transaction execution. It acts as a sophisticated in-memory cache for an account’s metadata (nonce, balance) and storage slots, ensuring that changes are tracked locally before being cryptographically committed to the global state.
The Anatomy of a stateObject
Internally, the stateObject carries the account’s current data and several specialized maps to handle different stages of the writing lifecycle.
type stateObject struct {
db *StateDB
address common.Address
addrHash common.Hash
origin *types.StateAccount
data types.StateAccount
// Write Caches
trie Trie
code []byte
originStorage Storage
dirtyStorage Storage
pendingStorage Storage
uncommittedStorage Storage
// Status Flags
dirtyCode bool
selfDestructed bool
newContract bool
}
Interaction Lifecycle Example
A. Modification: AddBalance, SubBalance, and SetBalance
These methods update the account’s funds while ensuring the change is recorded for potential rollbacks.
Lifecycle: AddBalance
- Initial State:
Account 0xAA...has a balance of5000. - Input:
AddBalance(addr, 1000). - Lifecycle:
- Journal Entry: The system calls
s.db.journal.balanceChange(s.address, 5000)to record the original balance. - Calculation: The new balance is calculated:
5000 + 1000 = 6000. - Update: The
stateObject.data.Balanceis updated to6000.
- Journal Entry: The system calls
- Output: The method returns the previous balance (
5000).
Lifecycle: SetBalance
The SetBalance method is the fundamental operation used to overwrite an account’s balance while ensuring the change is recorded for potential rollbacks.
- Initial State:
Account 0xCC...has a balance of5000. - Input:
SetBalance(addr, 1234). - Lifecycle:
- Capture Previous Value: The system retrieves the current balance (
5000) and stashes it as theprevvalue. - Journal Entry: The system calls
s.db.journal.balanceChange(s.address, s.data.Balance)to record the state before modification. - Update: The internal
setBalance(1234)is called, which directly assigns the newuint256.Intvalue tostateObject.data.Balance. - Reversion Logic (If Required): If the transaction fails, the journal entry’s
revert()method is called, which executess.getStateObject(ch.account).setBalance(ch.prev), effectively restoring the balance to5000.
- Capture Previous Value: The system retrieves the current balance (
- Output: The method returns the previous balance (
5000).
Lifecycle: SubBalance
The SubBalance method (often invoked through the StateDB interface) is used to deduct funds from an account, typically for gas costs or value transfers.
- Initial State:
Account 0xDD...has a balance of10000. - Input:
SubBalance(addr, 2000, reason)(e.g.,reason: BalanceDecreaseTransfer). - Lifecycle:
- Hook Interaction: If using a
hookedStateDB, the system first identifies the operation and prepares to emit a tracing event. - Calculation: The system calculates the new balance:
10000 - 2000 = 8000. - Underlying SetBalance: The operation triggers the underlying
SetBalance(8000)logic. - Journal Entry: Just as with
SetBalance, the journal records abalanceChangeentry forAccount 0xDD...with the previous value of10000. - Update: The
stateObject.data.Balanceis updated to the calculated value of8000. - Tracing: If an
OnBalanceChangehook exists, it is called with the address, the old balance (10000), the new balance (8000), and the specific reason for the change.
- Hook Interaction: If using a
- Output: The method returns the previous balance (
10000).
Key Internal Component Summary
| Method | Role in State Management | Journaling Interaction |
|---|---|---|
SetBalance | Directly updates the account metadata in memory. | Creates a balanceChange entry using the current balance as the “previous” state. |
SubBalance | Deducts a specific amount; relies on the same atomicity as SetBalance. | Records the original balance in the journal before the subtraction occurs. |
setBalance | The low-level internal assignment that updates the data.Balance field. | None; this is the final step of the higher-level methods. |
B. Revertibility and Journaling
The journal is an ordered list of every modification made during a transaction. This allows the system to precisely “undo” changes if an execution fails.
Example: Reverting a Balance Change
- Snapshot: Before starting an operation, the EVM calls
statedb.Snapshot(), which returns a revision ID (e.g.,Revision: 10). - Modification: An
AddBalanceoperation occurs as described above. The journal now contains abalanceChangeentry. - Failure: The transaction hits an
ErrOutOfGasorREVERT. - Reversion: The system calls
revertToSnapshot(10).- Logic: The system iterates backward through the journal from the current index down to the index associated with Revision 10.
- Action: For the
balanceChangeentry, it callsrevert(), which performss.getStateObject(0xAA...).setBalance(5000).
- Final State: The account balance is restored to
5000, and the journal entry is discarded.
C. Self-Destruct Handling and the “Burn” Mechanism
When an account is destroyed via SELFDESTRUCT, its funds are sent to a beneficiary, and the account is marked for deletion.
Example: Destructing an account with 100 Ether
- Input:
SelfDestruct(0xAA...)with beneficiary0xBB.... - Step 1: Marking: The
stateObjectfor0xAA...setsselfDestructed = true. - Step 2: Transfer: The 100 Ether balance is added to
0xBB...viaAddBalance. - Step 3: Post-Destruction Interaction: If a later transaction in the same block sends 10 Ether to
0xAA..., the balance of0xAA...increases again even though it is marked for destruction. - Step 4: The Final Burn: During
Finalise(true)(at the end of the block), the system checks all “dirty” accounts in the journal.- Logic: If an account has
selfDestructed == truebut still has a non-zero balance (the 10 Ether from Step 3), that balance is burnt. - Output: A tracing event is emitted with the reason
BalanceDecreaseSelfdestructBurn, and the Ether is permanently removed from the total supply.
- Logic: If an account has