死磕以太坊源碼分析之state

死磕以太坊源碼分析之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個交易,第一個區塊的最終狀態產生,第二個區塊的第一筆交易執行後將會改變第一個區塊鏈的最終狀態,以此類推,從而產生最終的區塊狀態。

image-20210112090020770

以太坊狀態資料庫

區塊的狀態數據並非保存在鏈上,而是將這些狀態維護在默克爾壓縮前綴樹中,在區塊鏈上僅記錄對應的Trie Root 值。使用LevelDB維護樹的持久化內容,而這個用來維護映射的資料庫叫做 StateDB。

首先我們用一張圖來大致了解一下StateDB:

image-20210112165612767

可以看到圖中一共有兩種狀態,一個是世界狀態Trie,一個是storage Trie,兩者都是MPT樹,世界狀態包含了一個個的賬戶狀態,賬戶狀態通過以賬戶地址為鍵,維護在表示世界狀態的樹中,而每個賬戶狀態中存儲這賬戶存儲樹的Root。賬戶狀態存儲一下資訊:

  1. nonce: 表示此賬戶發出的交易數量
  2. balance: 賬戶餘額
  3. storageRoot: 賬戶存儲樹的Root根,用來存儲合約資訊
  4. 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

創建狀態對象會在兩個地方進行調用:

  1. 檢索或者創建狀態對象
  2. 創建賬戶

最終都會去調用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

主要就做了兩件事:

  1. updateTrie將快取的存儲修改寫入對象的存儲Trie。
  2. 將所有節點寫入到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

  1. s.finalise() 將dirtyStorage中的所有數據移動到pendingStorage中
  2. 根據賬戶哈希和賬戶root打開賬戶存儲樹
  3. 將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
}

大致就做了以下幾件事:

  1. 先從StateDB中獲取stateObjects,有的話就返回。
  2. 如果沒有的話就從stateDB的trie中獲取賬戶狀態數據,獲取到rlp編碼的數據之後,將其解碼。
  3. 根據狀態數據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方法完成。

  1. 計算狀態Trie的當前根哈希
  2. 將狀態對象中的所有更改寫入到存儲樹

第一步在上面已經講過了,第二步的內容如下:

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(沒有就提交到存儲樹中)

最後看一下以太坊資料庫的讀寫過程:

image-20210113111013494

如果覺得文章還可以,關注下//github.com/blockchainGuide此項目哦,歡迎有想法的人一起維護。

參考

//mindcarver.cn

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

//www.ltk100.com/article-112-1.html

//learnblockchain.cn/books/geth/part3/statedb.html