以太坊作为全球领先的智能合约平台,其核心在于维护一个全球共享的、不断变化的状态数据库,这个状态数据库记录了所有账户(外部账户和合约账户)的余额、nonce、代码以及存储数据等关键信息,理解以太坊状态存储的源码,对于深入把握以太坊的运作机制、进行底层开发或优化至关重要,本文将尝试从源码层面,剖析以太坊状态存储的核心数据结构与关键机制。
以太坊的状态可以被看作是一个巨大的、持久化的键值(Key-Value)存储,每个账户都有一个唯一的地址(Address)作为键,对于账户本身,其基本属性(如余额、nonce、根哈希、代码)直接存储在状态中,而对于合约账户,其更复杂的存储数据(即合约变量)则以另一种键值结构存储,通常被称为“存储”(Storage)。
状态存储的实现需要满足以下关键需求:

在以太坊的Go语言实现(go-ethereum)中,状态存储的核心逻辑主要集中在 core/state 包和 trie 包中。
StateDB - 状态数据库的入口StateDB 是 core/state 包中的核心结构,它代表了以太坊的状态数据库实例,它维护了以下关键信息:
type StateDB struct {
db Database // 底层数据库接口,如LevelDB
// ... 其他字段如缓存、临时状态等
// 以下是与状态直接相关的字段
StateCache StateCache // 状态缓存,用于加速访问
// ...
preimages map[common.Hash][]byte // 预映像,用于某些哈希计算
}
Database:这是一个底层数据库的抽象接口,实际可能是LevelDB、BadgerDB等。StateDB 通过这个接口与磁盘上的持久化数据交互。StateCache:为了提高性能,StateDB 广泛使用缓存来存储最近访问或修改的账户和存储数据。Account - 账户结构账户信息本身也有一个结构:
type Account struct {
Root common.Hash // 合约账户的存储根哈希(指向MPT)
Balance *big.Int // 余额
Nonce uint64 // 交易计数
CodeHash common.Hash // 代码哈希
// ... 可能还有其他字段,如Dirty标记等
}
Root:对于合约账户,这个字段指向一个MPT的根哈希,这个MPT存储了该合约的所有存储变量(键值对)。CodeHash:账户代码的哈希值,外部账户的代码哈希为空。trie.Trie - Merkle Patricia Trietrie 包实现了MPT数据结构。StateDB 内部会持有一个顶层的MPT根节点,这个根节点的哈希就是当前状态根(State Root)。

// trie.Trie 接口(简化)
type Trie interface {
Get(key []byte) ([]byte, error)
Update(key, value []byte) error
Delete(key []byte) error
Hash() common.Hash
Root() []byte
// ...
}
Get(key):根据键获取值。Update(key, value):插入或更新键值对。Delete(key):删除键值对。Hash():计算并返回当前Trie的根哈希。在以太坊中:
StateRoot。Root字段指向的Trie就是该合约的存储Trie,这个存储Trie以一个256位的键(通常由变量slot索引派生)为键,以变量的RLP编码为值。NewStateDB)func NewStateDB(db Database, config *StateDBConfig) *StateDB {
// ...
s := &StateDB{
db: db,
originalRoot: common.Hash{},
stateCache: newStateCache(config.CacheSize),
// ...
}
// ...
return s
}
NewStateDB 函数根据传入的底层数据库接口 Database 创建一个新的 StateDB 实例,它初始化了缓存等结构。
GetAccount, CreateAccount, UpdateAccount)获取账户 (GetAccount):
func (s *StateDB) GetAccount(addr common.Address) *Account {
// 1. 先从缓存查找
if acc := s.stateCache.getAccount(addr); acc != nil {
return acc
}
// 2. 缓存未命中,从底层Trie加载
// ...
} 首先从内存缓存中查找,若未找到,则从底层的MPT中读取,读取时,会以账户地址为键,从状态Trie中获取账户的RLP编码数据,然后解码为Account结构。

创建/更新账户 (UpdateAccount):
func (s *StateDB) UpdateAccount(addr common.Address, account *Account) {
// ...
// 将账户标记为Dirty(已修改),并更新缓存
s.stateCache.addAccount(addr, account, true)
// ...
} 当账户信息(如余额、nonce)发生变化时,会调用此方法,账户会被标记为“Dirty”,并更新到缓存中,这些修改不会立即写入底层Trie,而是在后续的“提交”(Commit)过程中批量处理。
GetState, SetState)合约账户的存储操作是状态管理中更为复杂的一部分。
获取存储值 (GetState):
func (s *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash {
// ...
acc := s.GetAccount(addr)
if acc == nil {
return common.Hash{}
}
// 获取或创建该合约的存储Trie
storageRoot := acc.Root
var storage *trie.Trie
// 如果存储Trie尚未加载,则从数据库中加载
// ...
// 从存储Trie中根据hash(key)获取值
value, err := storage.Get(hash.Bytes())
// ...
}
storageRoot。storageRoot 为根,从数据库中实例化一个存储Trie。hash(通常是由变量slot索引计算而来)为键,查找对应的值。设置存储值 (SetState):
func (s *StateDB) SetState(addr common.Address, key, value common.Hash) {
// ...
acc := s.GetAccount(addr)
// ...
// 获取或创建该合约的存储Trie
// ...
// 如果value为零,则删除,否则更新
if value == (common.Hash{}) {
storage.Delete(key.Bytes())
} else {
storage.Update(key.Bytes(), value.Bytes())
}
// 标记存储Trie为Dirty
// ...
}
key 和 value 更新存储Trie。value 为零,则删除该键值对。Root 字段,使得账户本身也变为“Dirty”。Commit)当区块中的所有交易执行完毕后,需要将 StateDB 中所有“Dirty”的状态变更持久化到底层数据库中,这个过程就是 Commit。
func (s *StateDB) Commit(deleteEmptyObjects bool) (common.Hash, error