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.Int values.
  • 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, and SetBalance to 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 call revertToSnapshot to 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 updateRoot to recalculate the storage trie root, and then the commit function 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 stateObject maintains 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 Tree maintains a “flat” version of the state. It uses diffLayers (in-memory journals of recent changes) and a diskLayer (the persistent base state).
  • The Final Write: When a block is finalized, the CachingDB replaces “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 of 5000.
  • Input: AddBalance(addr, 1000).
  • Lifecycle:
    1. Journal Entry: The system calls s.db.journal.balanceChange(s.address, 5000) to record the original balance.
    2. Calculation: The new balance is calculated: 5000 + 1000 = 6000.
    3. Update: The stateObject.data.Balance is updated to 6000.
  • 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 of 5000.
  • Input: SetBalance(addr, 1234).
  • Lifecycle:
    1. Capture Previous Value: The system retrieves the current balance (5000) and stashes it as the prev value.
    2. Journal Entry: The system calls s.db.journal.balanceChange(s.address, s.data.Balance) to record the state before modification.
    3. Update: The internal setBalance(1234) is called, which directly assigns the new uint256.Int value to stateObject.data.Balance.
    4. Reversion Logic (If Required): If the transaction fails, the journal entry’s revert() method is called, which executes s.getStateObject(ch.account).setBalance(ch.prev), effectively restoring the balance to 5000.
  • 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 of 10000.
  • Input: SubBalance(addr, 2000, reason) (e.g., reason: BalanceDecreaseTransfer).
  • Lifecycle:
    1. Hook Interaction: If using a hookedStateDB, the system first identifies the operation and prepares to emit a tracing event.
    2. Calculation: The system calculates the new balance: 10000 - 2000 = 8000.
    3. Underlying SetBalance: The operation triggers the underlying SetBalance(8000) logic.
    4. Journal Entry: Just as with SetBalance, the journal records a balanceChange entry for Account 0xDD... with the previous value of 10000.
    5. Update: The stateObject.data.Balance is updated to the calculated value of 8000.
    6. Tracing: If an OnBalanceChange hook exists, it is called with the address, the old balance (10000), the new balance (8000), and the specific reason for the change.
  • Output: The method returns the previous balance (10000).

Key Internal Component Summary

MethodRole in State ManagementJournaling Interaction
SetBalanceDirectly updates the account metadata in memory.Creates a balanceChange entry using the current balance as the “previous” state.
SubBalanceDeducts a specific amount; relies on the same atomicity as SetBalance.Records the original balance in the journal before the subtraction occurs.
setBalanceThe 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

  1. Snapshot: Before starting an operation, the EVM calls statedb.Snapshot(), which returns a revision ID (e.g., Revision: 10).
  2. Modification: An AddBalance operation occurs as described above. The journal now contains a balanceChange entry.
  3. Failure: The transaction hits an ErrOutOfGas or REVERT.
  4. 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 balanceChange entry, it calls revert(), which performs s.getStateObject(0xAA...).setBalance(5000).
  5. 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 beneficiary 0xBB....
  • Step 1: Marking: The stateObject for 0xAA... sets selfDestructed = true.
  • Step 2: Transfer: The 100 Ether balance is added to 0xBB... via AddBalance.
  • Step 3: Post-Destruction Interaction: If a later transaction in the same block sends 10 Ether to 0xAA..., the balance of 0xAA... 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 == true but 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.