死磕以太坊源碼分析之state
- 2021 年 1 月 13 日
- 筆記
- 區塊鏈, 以太坊, 以太坊源碼分析, writeBlockWithState, storage Trie, add balance, stateObject
死磕以太坊源碼分析之state
配合以下程式碼進行閱讀://github.com/blockchainGuide/
希望讀者在閱讀過程中發現問題可以及時評論哦,大家一起進步。
源碼目錄
|-database.go 底層的存儲設計
|-dump.go 用來dumpstateDB數據
|-iterator.go,用來遍歷Trie
|-journal.go,用來記錄狀態的改變
|-state_object.go 通過state object操作賬戶值,並將修改後的storage trie寫入資料庫
|-statedb.go,以太坊整個的狀態
|-sync.go,用來和downloader結合起來同步state
基礎概念
狀態機
以太坊的本質就是一個基於交易的狀態機(transaction-based state machine)。在電腦科學中,一個 狀態機 是指可以讀取一系列的輸入,然後根據這些輸入,會轉換成一個新的狀態出來的東西。
我們從創世紀狀態(genesis state)開始,在網路中還沒有任何交易的時候產生狀態。當第一個區塊執行第一個交易時候開始產生狀態,直到執行完N個交易,第一個區塊的最終狀態產生,第二個區塊的第一筆交易執行後將會改變第一個區塊鏈的最終狀態,以此類推,從而產生最終的區塊狀態。

以太坊狀態資料庫
區塊的狀態數據並非保存在鏈上,而是將這些狀態維護在默克爾壓縮前綴樹中,在區塊鏈上僅記錄對應的Trie Root 值。使用LevelDB維護樹的持久化內容,而這個用來維護映射的資料庫叫做 StateDB。
首先我們用一張圖來大致了解一下StateDB:

可以看到圖中一共有兩種狀態,一個是世界狀態Trie,一個是storage Trie,兩者都是MPT樹,世界狀態包含了一個個的賬戶狀態,賬戶狀態通過以賬戶地址為鍵,維護在表示世界狀態的樹中,而每個賬戶狀態中存儲這賬戶存儲樹的Root。賬戶狀態存儲一下資訊:
- nonce: 表示此賬戶發出的交易數量
- balance: 賬戶餘額
- storageRoot: 賬戶存儲樹的Root根,用來存儲合約資訊
- codeHash: 賬戶的 EVM 程式碼哈希值,當這個地址接收到一個消息調用時,這些程式碼會被執行; 它和其它欄位不同,創建後不可更改。如果 codeHash 為空,則說明該賬戶是一個簡單的外部賬戶,只存在
nonce和balance。
接下來將會分析State相關的一些類,著重關注statedb.go、state_object.go、database.go,其中涉及的Trie相關的程式碼可以參照:死磕以太坊源碼分析之MPT樹-下
關鍵的數據結構
Account
Account存儲的是賬戶狀態資訊。
type Account struct {
Nonce uint64 //賬戶發出的交易數量
Balance *big.Int // 賬戶的餘額
Root common.Hash //賬戶存儲樹的Root根,用來存儲合約資訊
CodeHash []byte // 賬戶的 EVM 程式碼哈希值
}
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
StateDB
用來存儲狀態對象。
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
}
三者之間的關係:
StateDB->Trie->Account->stateObject
從StateDB中取出Trie根,根據地址從Trie樹中獲取賬戶的rlp編碼數據,再進行解碼成Account,然後根據Account生成stateObject
StateDB存儲狀態
StateDB讀寫狀態主要關心以下幾個文件:
- database.go
- state_object.go
- statedb.go
接下來分別介紹這麼幾個文件,相當關鍵。
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
}
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的核心方法也就這麼些內容。
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(沒有就提交到存儲樹中)
最後看一下以太坊資料庫的讀寫過程:

如果覺得文章還可以,關注下//github.com/blockchainGuide此項目哦,歡迎有想法的人一起維護。
參考
//www.jianshu.com/p/20d7f7c37b03
//hackernoon.com/getting-deep-into-ethereum-how-data-is-stored-in-ethereum-e3f669d96033
//web.xidian.edu.cn/qqpei/files/Blockchain/4_Data.pdf
