以太坊源码解读(23)stateDB
前面介绍了以太坊MPT树的结构和方法,而stateDB对象就是对以太坊状态MPT进行管理的对象。其管理功能包括: 1、初始化:New 2、增加:StateDB.createObject 3、删除:StateDB.Suicide 4、修改:StateDB.AddBalance 5、查询:StateDB.GetBalance 6、拍摄快照:StateDB.Snopshot 7、恢复快照:StateDB.RevertToSnopshot 8、将状态写入状态树:StateDB.Finalise 9、获得树根:StateDB.IntermediateRoot 10、将状态写入数据库:StateDB.Commit
我们需要理解的是,以太坊的最上层是应用层,最底层是数据库,StateDB是一级缓存,状态树是二级缓存。也就是说我们的应用要从数据库中增删查找数据,首先是把数据从数据库拿到状态树里,再从状态树中取出放入StateDB中,最后从StateDB中提取数据。反过来也一样,新增一个账户我们就先再StateDB中createObject()一个对象,然后Finalise到状态树,最后从状态树Commit到数据库。状态树是stateDB和数据库的桥梁。
type StateDB struct { db Database trie Trie // 这个map相当于是stateDB的缓存,存放着活动的账户。如果有账户在这里找不到,则通过trie从数据库中找到目标账户,然后存到这个map里 stateObjects map[common.Address]*stateObject stateObjectsDirty map[common.Address]struct{} dbErr error // The refund counter, also used by state transitioning. refund uint64 thash, bhash common.Hash txIndex int logs map[common.Hash][]*types.Log logSize uint preimages map[common.Hash][]byte // Journal of state modifications. This is the backbone of // Snapshot and RevertToSnapshot. journal *journal validRevisions []revision nextRevisionId int }
我们看到StateDB首先是连接了底层的数据库,然后持有了一个Trie接口。另外要注意的是,stateObjects这个map实际上就是所有的账户,以太坊中的账户的全部信息就是一个stateObject结构,我们叶子节点中储存的valueNode也就是stateObject的RLP编码。这个map相当于是stateDB的缓存,存放着活动的账户。如果有账户在这里找不到,则通过trie从数据库中找到目标账户,然后存到这个map里。
stateObjectsDirty表示“脏账户”。什么叫“脏账户”呢?就是在StateDB中,账户已经被修改了,但是状态树中的值还没有修改,这些账户就被存到stateObjectsDirty中。
type stateObject struct { address common.Address // 账户 地址 addrHash common.Hash // 账户地址的hash data Account // 账户数据 db *StateDB dbErr error // Write caches. trie Trie // storage trie, which becomes non-nil on first access code Code // 合约代码 originStorage Storage // Storage cache of original entries to dedup rewrites dirtyStorage Storage // Storage entries that need to be flushed to disk dirtyCode bool // 标记stateObject.code被修改了 suicided bool // 标记上层调用了自杀命令 deleted bool // 标记账户已经从数据库中删除 } type Account struct { Nonce uint64 // 账户的nonce值,每发起一笔交易确认后nonce值加一 Balance *big.Int // 账户余额 Root common.Hash // 账户的storage树根 CodeHash []byte // 账户的code hash值 }
stateDB中还值得注意的一个是journal,即日志,它存的是账户变动的反向操作。这对以太坊实现快照功能以及回滚世界状态非常有用。validRevisions是一个revision的切片,后者存的是日志(journal)的索引:
revision { id int //id == nextRevisionId journalIndex // journalIndex == len(jouranl) } type journal struct { entries []journalEntry // Current changes tracked by the journal dirties map[common.Address]int // Dirty accounts and the number of changes }
通过revision的journalIndex可以索引到日志切片的位置,回滚到某个revision就只要去查找当前journals切片中大于revision.journalIndex的那些日志,并执行,即可回滚到当前revision指定的世界状态。
stateDB的生命周期是怎样的?
stateDB是用来管理世界状态的,准确的说是用来改变世界状态的。那么世界状态什么时候会改变呢?
1、打包交易进行挖矿的时候; 2、收到区块广播执行同步的时候;
也就是说,stateDB是从挖矿时从交易池取出交易并执行,然后打包等待挖矿,最后当区块挖矿成功后,将stateDB中的账户改变刷入数据库后,stateDB的使命就结束了,就可以从内存中删除了。
一、New函数
主要功能:使用给定的树根创建一个世界状态
task1:使用给定的树根构建状态树; task2:实例化stateDB类;
func New(root common.Hash, db Database) (*StateDB, error) { tr, err := db.OpenTrie(root) if err != nil { return nil, err } return &StateDB{ db: db, trie: tr, stateObjects: make(map[common.Address]*stateObject), stateObjectsDirty: make(map[common.Address]struct{}), logs: make(map[common.Hash][]*types.Log), preimages: make(map[common.Hash][]byte), journal: newJournal(), }, nil }
调用时机: 1、BlockChain插入区块链时进行状态验证:BlockChain.inertChain —> state.New 2、BlockChain初始化时验证状态是否可读:BlockChain.loadLastState —> state.New
二、StateDB.createObject函数
主要功能:创建一个新账户
task1:实例化一个stateObject对象; task2:在日志中添加创建新账户事件; task3:将新账户添加到缓存中;
func (self *StateDB) createObject(addr common.Address) (newobj, prev *stateObject) { // 先看下这个地址之前的账户状态,如果有就不用新建账户 prev = self.getStateObject(addr) newobj = newObject(self, addr, Account{}) // 设置nonce的初始值 newobj.setNonce(0) if prev == nil { self.journal.append(createObjectChange{account: &addr}) } else { self.journal.append(resetObjectChange{prev: prev}) } // 将新账户添加到缓存 self.setStateObject(newobj) return newobj, prev } func (self *StateDB) setStateObject(object *stateObject) { self.stateObjects[object.Address()] = object }
我们来看下什么时日志?什么是反向操作?
当prev不存在时,我们向journal中添加了createObjectChange{account: &addr},这个对象实现的方法有:
func (ch createObjectChange) revert(s *StateDB) { delete(s.stateObjects, *ch.account) delete(s.stateObjectsDirty, *ch.account) }
revert()方法传入StateDB,然后从stateObjects和stateObjectsDirty中把刚才createObject生成的stateObject给删除掉,这就是一个反向操作。
我们再看resetObjectChange{prev:prev},这是当我们创建指定地址的账户发现该账户已经存在时在日志中添加的反向操作,目的就是将StateObjcet设置成之前的对象。因为我们上面是将一个已经存在的stateObject替换成了新的stateObject,尽管地址一样,但其他的属性可能都变了。所以反向操作就是直接替换回来。
func (ch resetObjectChange) revert(s *StateDB) { s.setStateObject(ch.prev) }
三、StateDB.Suicide函数
主要功能:销毁账户
task1:获取账户的stateObject对象 task2:添加删除日志 task3:标记账户自杀,余额清零
func (self *StateDB) Suicide(addr common.Address) bool { // 获取账户的stateObject对象 stateObject := self.getStateObject(addr) if stateObject == nil { return false } // 添加日志 self.journal.append(suicideChange{ account: &addr, prev: stateObject.suicided, prevbalance: new(big.Int).Set(stateObject.Balance()), }) // 标记自杀,余额清零 stateObject.markSuicided() stateObject.data.Balance = new(big.Int) return true } func (ch suicideChange) revert(s *StateDB) { obj := s.getStateObject(*ch.account) if obj != nil { obj.suicided = ch.prev obj.setBalance(ch.prevbalance) } }
它的反向操作就是获取该stateObject,恢复自杀标记,然后余额返回账户。
四、StateDB.GetBalance函数
很简单,先获取stateObject,然后返回余额。这个没有日志,因为没有更改账户。
func (self *StateDB) GetBalance(addr common.Address) *big.Int { stateObject := self.getStateObject(addr) if stateObject != nil { return stateObject.Balance() } return common.Big0 }
其他的GetNonce、GetCode、GetCodeSize,GerCodeHash都是同样的道理。
五、StateDB.Snapshot函数
主要功能:生成快照;
调用时机: 1、EVM调用Call、CallCode、DelegateCall、StaticCall、Create的过程中:EVM.Create —> StateDB.Snapshot
2、应用交易的过程:Work.commitTransaction —> StateDB.Snapshot
这些函数都是在执行交易的时候进行快照的,因为如果交易执行失败,需要按照快照进行回滚。每次账户的变动都会在dirties中记录,所以回滚后要重新恢复原来的dirties标记。
type journal struct { entries []journalEntry // Current changes tracked by the journal dirties map[common.Address]int // 记录了变动的账户及账户变动的次数 } func (self *StateDB) Snapshot() int { // 首先获取快照id,从0开始计数 id := self.nextRevisionId self.nextRevisionId++ // 然后将快照保存,即reversion{id, journal.length} self.validRevisions = append(self.validRevisions, revision{id, self.journal.length()}) return id } // 恢复快照 // 1、检查快照编号是否有效 // 2、通过快照编号获取日志长度 // 3、调用日志中的revert函数进行恢复 // 4、移除恢复点后面的快照 func (self *StateDB) RevertToSnapshot(revid int) { // 找出validReversion[0,n]中最小的下标偏移i,能够满足第二个函数f(i) == true idx := sort.Search(len(self.validRevisions), func(i int) bool { return self.validRevisions[i].id >= revid }) if idx == len(self.validRevisions) || self.validRevisions[idx].id != revid { panic(fmt.Errorf("revision id %v cannot be reverted", revid)) } snapshot := self.validRevisions[idx].journalIndex // Replay the journal to undo changes and remove invalidated snapshots self.journal.revert(self, snapshot) self.validRevisions = self.validRevisions[:idx] } func (j *journal) revert(statedb *StateDB, snapshot int) { for i := len(j.entries) - 1; i >= snapshot; i-- { // 从后往前逐个执行revert函数 j.entries[i].revert(statedb) // 将所有账户变动的标记删掉 if addr := j.entries[i].dirtied(); addr != nil { if j.dirties[*addr]--; j.dirties[*addr] == 0 { delete(j.dirties, *addr) } } } j.entries = j.entries[:snapshot] }
六、StateDB.IntermediateRoot
主要功能:更新状态树,同时计算状态树根
调用时机: 1、BlockChain验证一个区块的状态树根是否正确:BlockChain.insertChain —>BlockValidator.ValidateState —>stateDB.intermediateRoot。也就是区块插入规范链时,执行完交易后要验证此时本地的状态树树根与发来的区块头中的是否一致
2、Worker递交工作的过程中执行全部交易后,需要得到状态树根来填充区块头的root:worker.CommitNewWork —> Ethash.Finalize —> stateDB.IntermeidateRoot。因为挖矿之前要先执行交易,还要结算挖矿奖励,然后生成最新的状态。这时候就需要获得状态树树根,放在区块头中,一起打包用于挖矿。
task1:遍历更新的账户,将被更新的账户写入状态树,清除变更日志、快照、返利 task2:计算状态树根
我们上面分析的所有日志及回滚都是在StateObjects这个map缓存中进行的,一旦这些状态被写进状态树,日志就没用了,不能再回滚了,所以将日志、快照、返利都清除。
func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { s.Finalise(deleteEmptyObjects) // Trie的hash折叠函数 return s.trie.Hash() } // 遍历更新的账户,将被更新的账户写入状态树,清除变更日志、快照、返利 func (s *StateDB) Finalise(deleteEmptyObjects bool) { // dirties中记录的是变更过的账户 for addr := range s.journal.dirties { // 验证这个账户再stateObjects列表中也存在,如果不存在就跳过这个账户 stateObject, exist := s.stateObjects[addr] if !exist { continue } // 如果账户已经销毁,从状态树中删除账户;如果账户为空,且deleteEmptyObjects标志为true,则删除商户 if stateObject.suicided || (deleteEmptyObjects && stateObject.empty()) { s.deleteStateObject(stateObject) } else { // 否则将账户的storage变更写入storage树,更新storage树根 stateObject.updateRoot(s.db) // 将当前账户写入状态树 s.updateStateObject(stateObject) } // 当账户被更新到状态树后,将改动的账户标记为脏账户 s.stateObjectsDirty[addr] = struct{}{} } // Invalidate journal because reverting across transactions is not allowed. s.clearJournalAndRefund() }
七、StateDB.Commit()函数
主要功能:将状态树写入数据库
调用时机:BlockChain调用WriteBlockWithState写区块链的过程中:BlockChain.WriteBlockWithState —>StateDB.Commit
task1:遍历日志列表,更新脏账户(因为我们只需要重写那些改动了的账户,没有改动的账户不需要处理) task2:遍历被更新的账户,将被更新的账户写入状态树 task3:将状态树写入数据库
func (s *StateDB) Commit(deleteEmptyObjects bool) (root common.Hash, err error) { defer s.clearJournalAndRefund() // 遍历脏账户,将被更新的账户写入状态树 for addr := range s.journal.dirties { s.stateObjectsDirty[addr] = struct{}{} } // 遍历被更新账户,将其写入状态树 for addr, stateObject := range s.stateObjects { _, isDirty := s.stateObjectsDirty[addr] switch { case stateObject.suicided || (isDirty && deleteEmptyObjects && stateObject.empty()): // 如果账户标记为销毁,则从状态树中删除之 // 如果账户为空,且deleteEmptyObjects标志为true,从状态树中删除账户 s.deleteStateObject(stateObject) case isDirty: // 如果有代码更新,则将code以codeHash为key,以code为value存入db数据库 if stateObject.code != nil && stateObject.dirtyCode { s.db.TrieDB().InsertBlob(common.BytesToHash(stateObject.CodeHash()), stateObject.code) stateObject.dirtyCode = false } // 更新storage树 if err := stateObject.CommitTrie(s.db); err != nil { return common.Hash{}, err } // 更新状态树 s.updateStateObject(stateObject) } delete(s.stateObjectsDirty, addr) } // task3:将状态树写入数据库 root, err = s.trie.Commit(func(leaf []byte, parent common.Hash) error { var account Account if err := rlp.DecodeBytes(leaf, &account); err != nil { return nil } if account.Root != emptyState { s.db.TrieDB().Reference(account.Root, parent) } code := common.BytesToHash(account.CodeHash) if code != emptyCode { s.db.TrieDB().Reference(code, parent) } return nil }) log.Debug("Trie cache stats after commit", "misses", trie.CacheMisses(), "unloads", trie.CacheUnloads()) return root, err }
下一章:以太坊开发基础(1)以太坊开发环境的搭建
一、以太坊开发环境 首先,以太坊上开发区块链应用需要哪些工具?我们首先要在系统中搭建怎样的开发环境? 以太坊相较于比特币,有着更加完整的生态系统: geth:最常用的以太坊客户端,用go语言编写,是 ...