主页 > imtoken钱包网址 > 以太坊源码分析之详细状态分析
以太坊源码分析之详细状态分析
以太坊源码分析之详细状态分析
使用以下代码阅读它:
希望读者在阅读过程中发现问题及时提出意见,共同进步。
源目录
|-database.go 底层的存储设计
|-dump.go 用来dumpstateDB数据
|-iterator.go,用来遍历Trie
|-journal.go,用来记录状态的改变
|-state_object.go 通过state object操作账户值,并将修改后的storage trie写入数据库
|-statedb.go,以太坊整个的状态
|-sync.go,用来和downloader结合起来同步state
基本概念状态机
以太坊的本质是一个基于交易的状态机。 在计算机科学中,状态机是一种可以读取一系列输入并根据这些输入转换到新状态的东西。
我们从创世状态开始,这是一种在网络中还没有交易时生成的状态。 当第一个区块执行完第一笔交易后,状态开始产生,直到执行完N笔交易,才会产生第一个区块的最终状态,第一个交易执行完后,第二个区块的第一个交易会发生变化. 每个区块链的最终状态,依此类推,产生最终的区块状态。
图片-20210112090020770
以太坊状态数据库
区块的状态数据并不保存在链上,而是将这些状态维护在 Merkle 压缩前缀树中,区块链上只记录对应的 Trie Root 值。 使用LevelDB来维护树的持久化内容,用于维护映射的数据库称为StateDB。
首先我们用一张图来大致了解一下StateDB:
图片-20210112165612767
可以看出图中有两种状态,一种是世界状态Trie,一种是存储Trie以太坊外部账户和合约账户,都是MPT树。 世界状态包含各个账户状态,以账户地址为键维护账户状态。 在代表世界状态的树中,每个账户状态存储账户存储树的根。 帐户状态存储以下信息:
nonce:表示本账户发送的交易数 balance:账户余额 storageRoot:账户存储树的根,用于存储合约信息是强制执行的; 与其他字段不同,它在创建后无法更改。 如果 codeHash 为空,则表示该账户是一个简单的外部账户,只有 nonce 和 balance。
接下来我们将分析State相关的一些类,重点分析statedb.go、state_object.go、database.go。 其中涉及到的Trie相关代码可以参考:MPT tree-down
关键数据结构
帐户
帐户存储帐户状态信息。
type Account struct {
Nonce uint64 //账户发出的交易数量
Balance *big.Int // 账户的余额
Root common.Hash //账户存储树的Root根,用来存储合约信息
CodeHash []byte // 账户的 EVM 代码哈希值
}
状态对象
表示可从中获取帐户状态信息的状态对象。
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
状态数据库
用于存储状态对象。
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根据世界状态根打开世界状态树
从StateDB中打开一个Trie一般会经过以下过程:
OpenTrie(root common.Hash)->NewSecure->New
根据账户地址和存储根打开状态存储树
创建账户的存储Trie过程如下:
OpenStorageTrie(addrHash, root common.Hash)->NewSecure-New
帐户和状态对象
以太坊账户分为普通账户和合约账户,用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 //在当前事务执行中已修改的存储条目
}
创建状态对象
创建一个状态对象在两个地方被调用:
检索或创建一个状态对象 创建一个帐户
最终会调用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
}
状态对象.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->插入
s.finalise() 将dirtyStorage中的所有数据移动到pendingStorage中,根据账户hash和账户根打开账户存储树,将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 hash的形式存储
过程:
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的finalize方法是一样的。 底层是调用obj.finalise将脏状态的数据全部推入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.中间根
2.CommitTrie->updateTrie->trie.Commit->trie.db.insertPreimage(已经有直接持久化到硬盘数据库)
->t.trie.Commit(提交到没有它的存储树)
最后看一下以太坊数据库的读写过程:
图片-20210113111013494
参考