深入解析以太坊状态存储源码,数据结构与核心机制

以太坊作为全球领先的智能合约平台,其核心在于维护一个全球共享的、不断变化的状态数据库,这个状态数据库记录了所有账户(外部账户和合约账户)的余额、nonce、代码以及存储数据等关键信息,理解以太坊状态存储的源码,对于深入把握以太坊的运作机制、进行底层开发或优化至关重要,本文将尝试从源码层面,剖析以太坊状态存储的核心数据结构与关键机制。

以太坊状态存储概述

以太坊的状态可以被看作是一个巨大的、持久化的键值(Key-Value)存储,每个账户都有一个唯一的地址(Address)作为键,对于账户本身,其基本属性(如余额、nonce、根哈希、代码)直接存储在状态中,而对于合约账户,其更复杂的存储数据(即合约变量)则以另一种键值结构存储,通常被称为“存储”(Storage)。

状态存储的实现需要满足以下关键需求:

  1. 持久化:状态数据需要长期保存,不能因节点重启而丢失。
  2. 一致性:在区块执行过程中,状态变更需要原子性和一致性。
  3. 高效查询与更新:频繁的读取和写入操作需要高效的性能。
  4. Merkle Patricia Trie(MPT)支持:状态数据需要组织成MPT结构,以便生成状态根哈希,保障数据完整性并支持轻客户端验证。

在以太坊的Go语言实现(go-ethereum)中,状态存储的核心逻辑主要集中在 core/state 包和 trie 包中。

核心数据结构

StateDB - 状态数据库的入口

StateDBcore/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 Trie

trie 包实现了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的根哈希。

在以太坊中:

  • 状态Trie:以账户地址为键,以账户的RLP编码为值,这个Trie的根哈希就是区块头中的StateRoot
  • 存储Trie:对于每个合约账户,其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())
        // ...
    }
    1. 首先获取目标合约账户。
    2. 获取该账户的存储根哈希 storageRoot
    3. storageRoot 为根,从数据库中实例化一个存储Trie。
    4. 在存储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
        // ...
    }
    1. 获取合约账户。
    2. 获取或创建其存储Trie。
    3. 根据传入的 keyvalue 更新存储Trie。value 为零,则删除该键值对。
    4. 存储Trie被标记为“Dirty”,其根哈希也会随之改变,进而影响账户的 Root 字段,使得账户本身也变为“Dirty”。

状态提交 (Commit)

当区块中的所有交易执行完毕后,需要将 StateDB 中所有“Dirty”的状态变更持久化到底层数据库中,这个过程就是 Commit

func (s *StateDB) Commit(deleteEmptyObjects bool) (common.Hash, error

相关文章