以太坊源码分析 state

1. 源码说明

  • database.go:底层的存储设计 dump.go  用来 dump stateDB 数据
  • iterator.go:用来遍历 Trie
  • journal.go:用来记录状态的改变
  • state_object.go:通过 state object 操作账户值,并将修改后的 storage trie 写入数据库
  • statedb.go:以太坊整个的状态
  • sync.go:用来和 downloader 结合起来同步 state

2. 状态机

以太坊的本质就是一个 基于交易的状态机(transaction-based state machine)。在计算机科学中,一个 状态机 是指可以读取一系列的输入,然后根据这些输入,会转换成一个新的状态出来的东西。

我们从创世纪状态(genesis state) 开始,在网络中还没有任何交易的时候产生状态。当第一个区块执行第一个交易时候开始产生状态,直到执行完 N 个交易,第一个区块的最终状态产生,第二个区块的第一笔交易执行后将会改变第一个区块链的最终状态,以此类推,从而产生最终的区块状态。

3. 以太坊状态数据库

区块的状态数据并非保存在链上,而是将这些状态维护在默克尔压缩前缀树中,在区块链上仅记录对应的 Trie Root 值。使用 LevelDB 维护树的持久化内容,而这个用来维护映射的数据库叫做 StateDB。

首先我们用一张图来大致了解一下 StateDB:

19a31132e22ad9d0ec5ad9b9389fc463.png

可以看到图中一共有两种状态,一个是世界状态 Trie,一个是 storage Trie,两者都是 MPT 树。

世界状态包含了一个个的账户状态,账户状态通过以账户地址为键,维护在表示世界状态的树中,而每个账户状态中存储这账户存储树的 Root。

账户状态存储信息:

  • nonce: 表示此账户发出的交易数量。
  • balance: 账户余额。
  • storageRoot:  账户存储树的 Root 根,用来存储合约信息。
  • codeHash: 账户的 EVM 代码哈希值,当这个地址接收到一个消息调用时,这些代码会被执行; 它和其它字段不同,创建后不可更改。如果 codeHash 为空,则说明该账户是一个简单的外部账户,只存在 nonce 和 balance。

4. 数据结构

4.1 Account

Account 存储的是账户状态信息。

type Account struct {
 Nonce    uint64      // 账户发出的交易数量
 Balance  *big.Int    // 账户的余额
 Root     common.Hash // 账户存储树的 Root 根,用来存储合约信息
 CodeHash []byte      // 账户的 EVM 代码哈希值
}

4.2 StateObject

StateObject 表示一个状态对象,可以从中获取到账户状态信息。

type stateObject struct {
 address  common.Address
 addrHash common.Hash // 账户地址哈希
 data     Account
 db       *StateDB // 所属的 StateDB
 dbErr error // VM 不处理 db 层的错误,先记录下来,最后返回,只能保存 1 个错误,保存的第一个错误
 
 // Write caches.
 trie Trie // storage trie, 使用 trie 组织 stateObj 的数据
 code Code // 合约字节码,在加载代码时设置

 // 将原始条目的存储高速缓存存储到 dedup 重写中,为每个事务重置
 originStorage Storage 

 // 在整个块的末尾需要刷新到磁盘的存储条目
 pendingStorage Storage 

 // 在当前事务执行中已修改的存储条目
 dirtyStorage Storage 
}

4.3 StateDB

StateDB 用来存储 stateObject 状态对象。

type StateDB struct {
 db Database
 trie Trie // 当前所有账户组成的 MPT 树

 // 相关账户状态修改
 stateObjects        map[common.Address] *stateObject // 存储缓存的账户状态信息
 stateObjectsPending map[common.Address] struct{} // 状态对象已经完成,但是还没有写入到 Trie 中
 stateObjectsDirty   map[common.Address] struct{} // 在当前执行中修改的状态对象,用于后续 commit 
}

4.4 StateDB、StateObject、Account 的关系

StateDB->Trie->Account->stateObject,从 StateDB 中取出 Trie 根,根据地址从 Trie 树中获取账户的 rlp 编码数据,再进行解码成 Account,然后根据 Account 生成 stateObject。

5. StateDB 存储状态

StateDB 读写状态主要关心以下几个文件:

  • database.go
  • state_object.go
  • statedb.go

接下来分别介绍这么几个文件,相当关键。

5.1 database.go

根据世界状态 root 打开世界状态树

从 StateDB 中打开一个 Trie 大致经历以下过程:

OpenTrie(root common.Hash)->NewSecure->New

根据账户地址和 stoage root 打开状态存储树

创建一个账户的存储 Trie 过程如下:

OpenStorageTrie(addrHash, root common.Hash)->NewSecure->New

Account 和 StateObject

以太坊的账户分为普通账户和合约账户,以 Account 表示,Account 是账户的数据,不包含账户地址,账户需要使用地址来表示,地址在 stateObject 中。

type Account struct {
 Nonce    uint64
 Balance  *big.Int
Root     common.Hash // 存储树的 merkle 树根
CodeHash []byte // 合约账户专属,合约代码编译后的 Hash 值
}
type stateObject struct {
  address  common.Address // 账户地址
 addrHash common.Hash // 账户地址哈希
 data     Account
db       *StateDB // 所属的 StateDB
 dbErr error // VM 不处理 db 层的错误,先记录下来,最后返回,只能保存1个错误,保存存的第一个错误
trie Trie // storage trie, 使用 trie 组织 stateObj 的数据
 code Code // 合约字节码,在加载代码时设置
originStorage Storage // 将原始条目的存储高速缓存存储到 dedup 重写中,为每个事务重置
pendingStorage Storage // 在整个块的末尾需要刷新到磁盘的存储条目
dirtyStorage Storage // 在当前事务执行中已修改的存储条目
}

创建 StateObject

创建状态对象会在两个地方进行调用:

  • 检索或者创建状态对象
  • 创建账户

最终都会去调用createObject创建一个新的状态对象。如果有一个现有的帐户给定的地址,老的将被覆盖并作为第二个返回值返回

func (s *StateDB) createObject(addr common.Address) (newobj, prev *stateObject) {
prev = s.getDeletedStateObject(addr) // 如果存在老的,获取用来以后删除掉

 newobj = newObject(s, addr, Account{})
 newobj.setNonce(0) 
 if prev == nil {
  s.journal.append(createObjectChange{account: &addr})
 } else {
  s.journal.append(resetObjectChange{prev: prev})
 }
 s.setStateObject(newobj)
 return newobj, prev
}

5.2 state_object.go

state_object.go是很重要的文件,我们直接通过比较重要的函数来了解它。

增加账户余额

AddBalance->SetBalance

将对象的存储树保存到db

主要就做了两件事:

  • updateTrie将缓存的存储修改写入对象的存储Trie。
  • 将所有节点写入到trie的内存数据库中
func (s *stateObject) CommitTrie(db Database) error {
 s.updateTrie(db)
 ...
 root, err := s.trie.Commit(nil)
 ...
}

第一件事会在下面继续讲,第二件事可以参照我之前关于 死磕以太坊源码分析之MPT树-下的讲解。

①:将缓存的存储修改写入对象的存储Trie

主要流程: 最终还是调用了trie.go的insert方法

updateTrie->TryUpdate->insert

  • s.finalise() 将dirtyStorage中的所有数据移动到pendingStorage中
  • 根据账户哈希和账户root打开账户存储树
  • 将key与trie中的value关联,更新数据
func (s *stateObject) updateTrie(db Database) Trie {
 s.finalise() ①
...
 
 tr := s.getTrie(db) ②
 for key, value := range s.pendingStorage {
  ...
  if (value == common.Hash{}) {
   s.setError(tr.TryDelete(key[:]))
   continue
  }
 ...
  s.setError(tr.TryUpdate(key[:], v)) ③
 }
...
}

整个核心也就是updateTrie,调用了trie的insert方法进行处理。

②:将所有节点写入到trie的内存数据库,其key以sha3哈希形式存储

流程:

trie.Commit->t.trie.Commit->t.hashRoot

func (t *SecureTrie) Commit(onleaf LeafCallback) (root common.Hash, err error) {
 if len(t.getSecKeyCache()) > 0 {
  t.trie.db.lock.Lock()
  for hk, key := range t.secKeyCache {
   t.trie.db.insertPreimage(common.BytesToHash([]byte(hk)), key)
  }
  t.trie.db.lock.Unlock()

  t.secKeyCache = make(map[string][]byte)
 }
 return t.trie.Commit(onleaf)
}

如果KeyCache中已经有了,直接插入到磁盘数据库,否则的话插入到Trie的内存数据库。

将trie根设置为的当前根哈希

func (s *stateObject) updateRoot(db Database) {
 s.updateTrie(db)
 if metrics.EnabledExpensive {
  defer func(start time.Time) { s.db.StorageHashes += time.Since(start) }(time.Now())
 }
 s.data.Root = s.trie.Hash()
}

方法也比较简单,底层调用UpdateTrie然后再更新root.

State_object.go的核心方法也就这么些内容。

5.3 statedb.go

创建账户

创建账户的核心就是创建状态对象,然后再初始化值。

func (s *StateDB) CreateAccount(addr common.Address) {
 newObj, prev := s.createObject(addr)
 if prev != nil {
  newObj.setBalance(prev.data.Balance)
 }
}
func (s *StateDB) createObject(addr common.Address) (newobj, prev *stateObject) {
 prev = s.getDeletedStateObject(addr) 

 newobj = newObject(s, addr, Account{})
 newobj.setNonce(0) 
 if prev == nil {
  s.journal.append(createObjectChange{account: &addr})
 } else {
  s.journal.append(resetObjectChange{prev: prev})
 }
 s.setStateObject(newobj)
 return newobj, prev
}

删除、更新、获取状态对象

func (s *StateDB) deleteStateObject(obj *stateObject) 
func (s *StateDB) updateStateObject(obj *stateObject) 
func (s *StateDB) getStateObject(obj *stateObject) {

这三个方法底层分别都是调用Trie.TryDelete、Trie.TryUpdate、Trie.TryGet方法来分别获取。

这里大致的讲一下getStateObject,代码如下:

func (s *StateDB) getDeletedStateObject(addr common.Address) *stateObject {
 // Prefer live objects if any is available
 if obj := s.stateObjects[addr]; obj != nil {
  return obj
 }
 // Track the amount of time wasted on loading the object from the database
 if metrics.EnabledExpensive {
  defer func(start time.Time) { s.AccountReads += time.Since(start) }(time.Now())
 }
 // Load the object from the database
 enc, err := s.trie.TryGet(addr[:])
 if len(enc) == 0 {
  s.setError(err)
  return nil
 }
 var data Account
 if err := rlp.DecodeBytes(enc, &data); err != nil {
  log.Error("Failed to decode state object", "addr", addr, "err", err)
  return nil
 }
 // Insert into the live set
 obj := newObject(s, addr, data)
 s.setStateObject(obj)
 return obj
}

大致就做了以下几件事:

  • 先从StateDB中获取stateObjects,有的话就返回。
  • 如果没有的话就从stateDB的trie中获取账户状态数据,获取到rlp编码的数据之后,将其解码。
  • 根据状态数据Account 构造stateObject

余额操作

余额的操作大致有添加、减少、和设定。我们就拿添加来分析:

根据地址获取stateObject,然后addBalance.

func (s *StateDB) AddBalance(addr common.Address, amount *big.Int) {
 stateObject := s.GetOrNewStateObject(addr)
 if stateObject != nil {
  stateObject.AddBalance(amount)
 }
}

储存快照和回退快照

func (s *StateDB) Snapshot() int 
func (s *StateDB) RevertToSnapshot(revid int)

储存快照和回退快照,我们可以在提交交易的流程中找到:

func (w *worker) commitTransaction(tx *types.Transaction, coinbase common.Address) ([]*types.Log, error) {
 snap := w.current.state.Snapshot()

 receipt, err := core.ApplyTransaction(w.chainConfig, w.chain, &coinbase, w.current.gasPool, w.current.state, w.current.header, tx, &w.current.header.GasUsed, *w.chain.GetVMConfig())
 if err != nil {
  w.current.state.RevertToSnapshot(snap)
  return nil, err
 }
 w.current.txs = append(w.current.txs, tx)
 w.current.receipts = append(w.current.receipts, receipt)

 return receipt.Logs, nil
}

首先我们会对当前状态进行快照,然后执行ApplyTransaction,如果在预执行交易的阶段出错了,那么会回退到备份的快照位置。之前的修改全部会回退。

计算状态Trie的当前根哈希

计算状态Trie的当前根哈希是由IntermediateRoot来完成的。

①:确定所有的脏存储状态(简单理解就是当前执行修改的所有对象)

func (s *StateDB) Finalise(deleteEmptyObjects bool) {
 for addr := range s.journal.dirties {
  obj, exist := s.stateObjects[addr]
  if !exist {
   continue
  }
  if obj.suicided || (deleteEmptyObjects && obj.empty()) {
   obj.deleted = true
  } else {
   obj.finalise()
  }
  s.stateObjectsPending[addr] = struct{}{}
  s.stateObjectsDirty[addr] = struct{}{}
 }
 s.clearJournalAndRefund()
}

其实这个跟state_object的finalise方法是一个方式,底层就是调用了obj.finalise将dirty状态的所有数据全部推入到pending中去,等待处理。

②:处理stateObjectsPending中的数据

先更新账户的Root根,然后再将将给定的对象写入trie。

for addr := range s.stateObjectsPending {
  obj := s.stateObjects[addr]
  if obj.deleted {
   s.deleteStateObject(obj)
  } else {
   obj.updateRoot(s.db)
   s.updateStateObject(obj)
  }
 }

将状态写入底层内存Trie数据库

这部分功能由commit方法完成。

  • 计算状态Trie的当前根哈希
  • 将状态对象中的所有更改写入到存储树

第一步在上面已经讲过了,第二步的内容如下:

for addr := range s.stateObjectsDirty {
  if obj := s.stateObjects[addr]; !obj.deleted {
   ....
   if err := obj.CommitTrie(s.db); err != nil {
    return common.Hash{}, err
   }
  }
 }

核心就是objectCommitTrie,这也是上面state_object的内容。

总结流程如下:

  • 1.IntermediateRoot
  • 2.CommitTrie->updateTrie->trie.Commit->trie.db.insertPreimage(已经有了直接持久化到硬盘数据库)->t.trie.Commit(没有就提交到存储树中)

最后看一下以太坊数据库的读写过程:

1cce3269b37e0edbfd6e390c6aaff0c2.png

本文讲述 Ubuntu 和  Windows 下搭建以太坊私有链。1. 安装 Ubuntu Geth 客户端以太坊官方对 Ubuntu 支持很好,在各个 linux 系统中安装最简单。Geth官方安装指南:ht ...