以太坊EVM源码注释之State
Ethereum State
EVM在给定的状态下使用提供的上下文(Context)运行合约,计算有效的状态转换(智能合约代码执行的结果)来更新以太坊状态(Ethereum state)。因此可以认为以太坊是基于交易的状态机,外部因素(账户持有者或者矿工)可以通过创建、接受、排序交易来启动状态转换(state transition)。

从状态的角度来看,可以将以太坊看作是一条状态链;
从实现来看,可以将以太坊看作是一条区块组成的链,即"区块链(Blockchain)"。
在顶层,有以太坊世界状态(world state),是以太坊地址(20字节,160bit)到账户(account)的映射。
在更低的层面,每个以太坊地址表示一个包含余额、nonce、storage、code的帐户。以太坊账户分为两种类型:
- EOA(Externally owned account), 由一个私钥控制,不能包含EVM代码,不能使用storage;
- Contract account,由EVM代码控制。
可以认为是在以太坊世界状态的沙箱副本上运行EVM,如果由于任何原因无法完成执行,则将完全丢弃这个沙箱版本。如果成功执行,那么现实世界的状态就会更新到与沙箱版本一致,包括对被调用合约的存储数据的更改、创建的新合约以及引起的账户余额变化等。[3]
State模块主要源代码目录如下:
~/go-ethereum-master/core/state# tree
.
├── database.go // 提供了trie树的抽象,提供了一个数据库的抽象。实现了CacheDB结构
├── dump.go // dump
├── iterator.go // 迭代trie,后序遍历整个状态树
├── iterator_test.go
├── journal.go // 操作日志,针对各种操作的日志提供了对应的回滚功能
├── statedb.go // stateDB结构定义及操作方法
├── statedb_test.go
├── state_object.go // stateObject结构定义及操作方法
├── state_object_test.go
├── state_test.go
├── sync.go // 用于状态同步功能
└── sync_test.go
StateDB
以太坊state模块实现了账户余额模型,它记录了每个账户的状态信息,每当有交易发生,就更改相应账户的状态。state 模块中的主要对象是
StateDB
,它通过大量的
stateObject
对象集合管理所有账户信息,提供了各种管理账户信息的方法。
StateDB
的
db
字段类型是
Database
接口,
Database
封装了对树(trie)和合约代码的访问方法,在实际的调用代码中,它只有一个实例
cachingDB
。
cachingDB
封装的
trie
的访问方法操作的都是
SecureTrie
对象,
SecureTrie
实现了
state.Trie
接口。
StateDB
有一个
state.Trie
类型成员
trie
,它又被称为
storage trie
,这个MPT结构中存储的都是
stateObject
对象,每个
stateObject
对象以其地址作为插入节点的Key;每次在一个区块的交易开始执行前,trie由一个哈希值(hashNode)恢复(resolve)出来。另外还有一个map结构
stateObjects
,存放
stateObject
,地址作为map的key,用来缓存所有从数据库中读取出来的账户信息,无论这些信息是否被修改过都会缓存在这里。
stateObjectsPending
用来记录已经完成修改但尚未写入
trie
的账户,
stateObjectsDirty
用来记录哪些账户信息被修改过了。需要注意的是,这两个字段并不时刻与
stateObjects
对应,并且也不会在账户信息被修改时立即修改这两个字段。在进行
StateDB.Finalise
等操作时才会将
journal
字段中记录的被修改的账户整理到
stateObjectsPending
和
stateObjectsDirty
中。在代码实现中,这两个字段用法并无太大区别,一般会成对出现,只有在
createObjectChange
的
revert
方法中单独出现了
stateObjectsDirty
。因此
stateObjectsPending
和
stateObjectsDirty
的区别可能在于:
stateObjectsPending
存的账户已经完成更改,状态已经确定下来,只是还没有写入底层数据库,应该不会再进行回滚;
stateObjectsDirty
的账户修改还没最终确定,可能继续修改,也有可能回滚。但是state模块的代码并没有体现出来这种区别,不清楚别的模块代码有没有相关内容。
journal
字段记录了
StateDB
进行的所有操作,以便将来进行回滚。在调用
StateDB.Finalise
方法将
juournal
记录的账户更改"最终确定(finalise)"到
stateObjects
以后,
journal
字段会被清空,无法再进行回滚,因为不允许跨事务回滚(一般会在事务结束时才会调用
stateObject
的
finalise
方法)。
如上图所示,每当一个
stateObject
有改动,亦即账户状态有变动时,这个
stateObject
会标为dirty,然后这个
stateObject
对象会更新,此时所有的数据改动还仅仅存储在
stateObjects
里。当调用
IntermediateRoot()
时,所有标为dirty的stateObject才会被一起写入
trie
。而整个
trie
中的内容只有在调用
Commit()
时被一起提交到底层数据库。可见,
stateObjects
被用作本地的一级缓存,
trie
是二级缓存,底层数据库是第三级,这样逐级缓存数据,每一级数据向上一级提交的时机也根据业务需求做了合理的选择。[7]
StateDB
结构源码如下:
core/state/statedb.go
// StateDBs within the ethereum protocol are used to store anything
// within the merkle trie. StateDBs take care of caching and storing
// nested states. It's the general query interface to retrieve:
// * Contracts
// * Accounts
// stateDB用来存储以太坊中关于merkle trie的所有内容。 StateDB负责缓存和存储嵌套状态。
// 这是检索合约和账户的一般查询界面:
type StateDB struct {
db Database // 后端的数据库
trie Trie // 树 main account trie
// This map holds 'live' objects, which will get modified while processing a state transition.
// 下面的Map用来存储当前活动的对象,这些对象在状态转换的时候会被修改。
stateObjects map[common.Address]*stateObject
// State objects finalized but not yet written to the trie 已完成修改的状态对象(state object),但尚未写入trie
// 只记录地址,并不记录实际内容,也就是说只要Map里有键就行,不记录值,对应的值从stateObjects找,然后进行相关操作。
stateObjectsPending map[common.Address]struct{}
// State objects modified in the current execution 在当前执行过程中修改的状态对象(state object)
stateObjectsDirty map[common.Address]struct{}
// DB error. 数据库错误
// State objects are used by the consensus core and VM which are
// unable to deal with database-level errors. Any error that occurs
// during a database read is memoized here and will eventually be returned
// by StateDB.Commit.
// stateObject会被共识算法的核心和VM使用,在这些代码内部无法处理数据库级别的错误。
// 在数据库读取期间发生的任何错误都会记录在这里,最终由StateDB.Commit返回。
dbErr error
// The refund counter, also used by state transitioning.
// 退款计数器,用于状态转换
refund uint64
thash, bhash common.Hash // 当前的transaction hash 和block hash
txIndex int // 当前的交易的index
logs map[common.Hash][]*types.Log // 日志 key是交易的hash值
logSize uint // 日志大小
preimages map[common.Hash][]byte // SHA3的原始byte[], EVM计算的 SHA3->byte[]的映射关系
// Journal of state modifications. This is the backbone of
// Snapshot and RevertToSnapshot.
// 状态修改日志。这是快照和回滚到快照的支柱。
journal *journal
validRevisions []revision
nextRevisionId int
// Measurements gathered during execution for debugging purposes
// 为调试目的而在执行期间收集的度量
AccountReads time.Duration
AccountHashes time.Duration
AccountUpdates time.Duration
AccountCommits time.Duration
StorageReads time.Duration
StorageHashes time.Duration
StorageUpdates time.Duration
StorageCommits time.Duration
}
除了各种管理账户信息的方法和
stateObject
对象的增删改查方法之外,
StateDB
还有几个重要的方法需要解释:
Copy
core/state/statedb.go
// Copy creates a deep, independent copy of the state.
// Snapshots of the copied state cannot be applied to the copy.
// Copy创建状态的一个独立的深拷贝。
func (s *StateDB) Copy() *StateDB {
// Copy all the basic fields, initialize the memory ones
// 复制所有的基础字段,初始化内存字段
state := &StateDB{
db: s.db,
trie: s.db.CopyTrie(s.trie),
stateObjects: make(map[common.Address]*stateObject, len(s.journal.dirties)),
stateObjectsPending: make(map[common.Address]struct{}, len(s.stateObjectsPending)),
stateObjectsDirty: make(map[common.Address]struct{}, len(s.journal.dirties)),
refund: s.refund,
logs: make(map[common.Hash][]*types.Log, len(s.logs)),
logSize: s.logSize,
preimages: make(map[common.Hash][]byte, len(s.preimages)),
journal: newJournal(),
}
// Copy the dirty states, logs, and preimages
// 复制脏状态,日志,和原象(preimages)。hash = SHA3(byte[]),这里的原始byte[]即为preimage。
for addr := range s.journal.dirties {
// As documented [here](https://github.com/ethereum/go-ethereum/pull/16485#issuecomment-380438527),
// and in the Finalise-method, there is a case where an object is in the journal but not
// in the stateObjects: OOG after touch on ripeMD prior to Byzantium. Thus, we need to check for
// nil
// 正如文档和Finalise方法中所述,有这样一种情况:对象在日志中但不在stateObjects中:
// 在拜占庭版本之前,在ripeMD上创建但出现了OOG(out of Gas)错误。因此,我们需要检查nil
if object, exist := s.stateObjects[addr]; exist {
// Even though the original object is dirty, we are not copying the journal,
// so we need to make sure that anyside effect the journal would have caused
// during a commit (or similar op) is already applied to the copy.
// 即使原始对象是脏的,我们也不会复制日志 为什么不复制journal??
// 因此我们需要确保在提交(或类似的操作)期间日志可能造成的任何副作用已经应用到副本上。
state.stateObjects[addr] = object.deepCopy(state)
// Mark the copy dirty to force internal (code/state) commits 将副本标记为dirty以强制执行内部(代码/状态)提交
state.stateObjectsDirty[addr] = struct{}{}
// Mark the copy pending to force external (account) commits 将副本标记为pending以强制外部(帐户)提交
state.stateObjectsPending[addr] = struct{}{}
}
}
// Above, we don't copy the actual journal. This means that if the copy is copied, the
// loop above will be a no-op, since the copy's journal is empty.
// Thus, here we iterate over stateObjects, to enable copies of copies
// 以上,我们不复制实际的日志。这意味着如果复制的对象是副本,上面的循环将是no-op,因为副本的日志是空的。
// 因此,这里我们遍历stateObjects,以便可以复制副本的副本。
for addr := range s.stateObjectsPending {
if _, exist := state.stateObjects[addr]; !exist { // 如果stateObjects里还不存在
state.stateObjects[addr] = s.stateObjects[addr].deepCopy(state) // 深拷贝
}
state.stateObjectsPending[addr] = struct{}{}
}
for addr := range s.stateObjectsDirty {
if _, exist := state.stateObjects[addr]; !exist {
state.stateObjects[addr] = s.stateObjects[addr].deepCopy(state)
}
state.stateObjectsDirty[addr] = struct{}{}
}
// 日志
for hash, logs := range s.logs {
cpy := make([]*types.Log, len(logs))
for i, l := range logs {
cpy[i] = new(types.Log)
*cpy[i] = *l
}
state.logs[hash] = cpy
}
// preimages
for hash, preimage := range s.preimages {
state.preimages[hash] = preimage
}
return state
}
Finalise
StateDB.Finalise
操作将
journal
字段中记录的被修改的账户整理到
stateObjectsPending
和
stateObjectsDirty
中。
core/state/statedb.go
// Finalise finalises the state by removing the self destructed objects and clears
// the journal as well as the refunds. Finalise, however, will not push any updates
// into the tries just yet. Only IntermediateRoot or Commit will do that.
// Finalise通过移除自毁对象和清除日志以及退款来完成状态的最后确定。
// 然而,Finalise不会将更新写入trie。只有IntermediateRoot 和 Commit做那个工作。
// 也就是说这个时候更改还在stateObjects里,没有写入trie,更没有写入底层的数据库落地成文件。
func (s *StateDB) Finalise(deleteEmptyObjects bool) {
for addr := range s.journal.dirties {
obj, exist := s.stateObjects[addr]
if !exist {
// ripeMD is 'touched' at block 1714175, in tx 0x1237f737031e40bcde4a8b7e717b2d15e3ecadfe49bb1bbc71ee9deb09c6fcf2
// That tx goes out of gas, and although the notion of 'touched' does not exist there, the
// touch-event will still be recorded in the journal. Since ripeMD is a special snowflake,
// it will persist in the journal even though the journal is reverted. In this special circumstance,
// it may exist in `s.journal.dirties` but not in `s.stateObjects`.
// Thus, we can safely ignore it here
// ripeMD在第1714175个块创建,在交易号为0x1237f737031e40bcde4a8b7e717b2d15e3ecadfe49bb1bbc71ee9deb09c6fcf2的交易中
// 那个tx的gas不足,虽然“touched”的概念并不存在,但touch-event仍然会被记录在日志中。因为ripeMD是一个特殊的个例,
// 即使日志被回滚,它也将在日志中保持。在这种特殊情况下,它可能存在于`s.journal.dirties`中,但不在`s.stateObjects`中。
// 因此,我们可以放心地忽略它
continue
}
if obj.suicided || (deleteEmptyObjects && obj.empty()) {
obj.deleted = true
} else {
obj.finalise()
}
s.stateObjectsPending[addr] = struct{}{}
s.stateObjectsDirty[addr] = struct{}{}
}
// Invalidate journal because reverting across transactions is not allowed.
// 使日志失效,因为不允许跨事务恢复。
s.clearJournalAndRefund()
}
IntermediateRoot
此方法将
StateDB.stateObjectsPending
字段中的账户的更改写入
trie
中,然后将该字段清空。
core/state/statedb.go
// IntermediateRoot computes the current root hash of the state trie.
// It is called in between transactions to get the root hash that
// goes into transaction receipts.
// IntermediateRoot计算状态树的当前根hash。这个方法会在交易执行的过程中被调用。会被存入交易收据中
func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
// Finalise all the dirty storage states and write them into the tries
// Finalise所有的脏存储状态并将它们写入树中。
s.Finalise(deleteEmptyObjects)
// 将更改写入树中
for addr := range s.stateObjectsPending {
obj := s.stateObjects[addr]
if obj.deleted {
s.deleteStateObject(obj)
} else {
obj.updateRoot(s.db)
s.updateStateObject(obj)
}
}
// 清空
if len(s.stateObjectsPending) > 0 {
s.stateObjectsPending = make(map[common.Address]struct{})
}
// Track the amount of time wasted on hashing the account trie
// 跟踪在得到帐户树hash花费的时间
if metrics.EnabledExpensive {
defer func(start time.Time) { s.AccountHashes += time.Since(start) }(time.Now())
}
return s.trie.Hash()
}
Commit
此方法先调用
IntermediateRoot
方法,然后将
StateDB.stateObjectsDirty
字段中的账户的更改写入
trie
中,然后将该字段清空。最后将更改都写入数据库。
core/state/statedb.go
// Commit writes the state to the underlying in-memory trie database.
// Commit将状态写入底层内存trie数据库。
func (s *StateDB) Commit(deleteEmptyObjects bool) (common.Hash, error) {
// Finalize any pending changes and merge everything into the tries
// Finalize挂起的更改,并将所有内容合并到树中
s.IntermediateRoot(deleteEmptyObjects)
// Commit objects to the trie, measuring the elapsed time
// 将对象提交到trie,统计运行时间
for addr := range s.stateObjectsDirty {
if obj := s.stateObjects[addr]; !obj.deleted {
// Write any contract code associated with the state object
// 写入与状态对象相关联的任何合约代码,写入代码更改
if obj.code != nil && obj.dirtyCode {
s.db.TrieDB().InsertBlob(common.BytesToHash(obj.CodeHash()), obj.code)
obj.dirtyCode = false
}
// Write any storage changes in the state object to its storage trie
// 将状态对象中的任何存储更改写入其存储trie
if err := obj.CommitTrie(s.db); err != nil {
return common.Hash{}, err
}
}
}
if len(s.stateObjectsDirty) > 0 { // 清空
s.stateObjectsDirty = make(map[common.Address]struct{})
}
// Write the account trie changes, measuing the amount of wasted time
// 记录账户的变化,统计花费的时间
if metrics.EnabledExpensive {
defer func(start time.Time) { s.AccountCommits += time.Since(start) }(time.Now())
}
// The onleaf func is called _serially_, so we can reuse the same account
// for unmarshalling every time.
// onleaf func被称为_serially_,因此我们可以在每次解码时重用相同的帐户。
var account Account
// 调用trie的Commit方法。
return s.trie.Commit(func(leaf []byte, parent common.Hash) error {
if err := rlp.DecodeBytes(leaf, &account); err != nil {
return nil
}
if account.Root != emptyRoot {
// Reference添加父节点到子节点的新引用。
s.db.TrieDB().Reference(account.Root, parent)
}
code := common.BytesToHash(account.CodeHash)
if code != emptyCode {
s.db.TrieDB().Reference(code, parent)
}
return nil
})
}
快照和回滚
StateDB
的状态版本管理功能依赖于两个数据结构:
revision
和
journalEntry
。
core/state/statedb.go
// 用于回滚的版本 快照版本
type revision struct {
id int
journalIndex int
}
core/state/journal.go
// journalEntry is a modification entry in the state change journal that can be
// reverted on demand.
// journalEntry是状态更改日志中的一个修改条目,可以根据需要恢复。
type journalEntry interface {
// revert undoes the changes introduced by this journal entry.
// revert还原由本日志条目引入的更改。undo操作
revert(*StateDB)
// dirtied returns the Ethereum address modified by this journal entry.
// dirtied返回被此日志条目修改的以太坊地址。
dirtied() *common.Address
}
// journal contains the list of state modifications applied since the last state
// commit. These are tracked to be able to be reverted in case of an execution
// exception or revertal request.
// journal包含自上次状态提交以来应用的状态修改列表。对它们进行跟踪是为了可以在执行异常或恢复请求的情况下进行恢复。
// journal 就是journalEntry的列表。
type journal struct {
entries []journalEntry // Current changes tracked by the journal. journal追踪了当前的变化
dirties map[common.Address]int // Dirty accounts and the number of changes 脏帐户和更改的数量
}
其中
journal
对象是
journalEntry
的列表,长度不固定,可任意添加元素。接口
journalEntry
存在若干种实现体,描述了从单个账户操作到
account trie
变化的各种最小单元事件。
revision
用来描述一个版本,它的两个整型成员
id
和
journalIndex
,都是基于
journal
列表进行操作的。
上图简述了
StateDB
中账户状态的版本是如何管理的。首先
journal
列表会记录所有发生过的操作事件;当某个时刻需要产生一个账户状态版本时,调用
Snapshopt()
方法,会产生一个新
revision
对象,记录下当前
journal
散列的长度,和一个自增1的版本号。
core/state/statedb.go
// Snapshot可以创建一个快照, 然后通过 RevertToSnapshot可以回滚到哪个状态,这个功能是通过journal来做到的。
// 每一步的修改都会往journal里面添加一个undo日志。 如果需要回滚只需要执行undo日志就行了
// Snapshot returns an identifier for the current revision of the state.
// 快照返回状态的当前revision的标识符。
func (s *StateDB) Snapshot() int {
id := s.nextRevisionId
s.nextRevisionId++
s.validRevisions = append(s.validRevisions, revision{id, s.journal.length()})
return id
}
基于以上的设计,当发生回退要求时,只要根据相应的
revision
中的
journalIndex
,在
journal
列表上,根据所记录的所有
journalEntry
,即可使所有账户回退到那个状态。
StateDB.Snapshot
方法创建一个快照,返回一个
int
值作为快照的
ID
。
StateDB.RevertToSnapshot
用这个
ID
将
StateDB
的状态恢复到某一个快照状态。
StateDB.nextRevisionId
字段用来生成快照的有效
ID
,而
StateDB.validRevisions
记录所有有效快照的信息。
core/state/statedb.go
// RevertToSnapshot reverts all state changes made since the given revision.
// RevertToSnapshot将恢复自给定revision以来所做的所有状态更改。
func (s *StateDB) RevertToSnapshot(revid int) {
// Find the snapshot in the stack of valid snapshots.
// 在有效快照的堆栈中找到快照。
idx := sort.Search(len(s.validRevisions), func(i int) bool {
return s.validRevisions[i].id >= revid
})
// 没找到指定revid的revision
if idx == len(s.validRevisions) || s.validRevisions[idx].id != revid {
panic(fmt.Errorf("revision id %v cannot be reverted", revid))
}
snapshot := s.validRevisions[idx].journalIndex
// Replay the journal to undo changes and remove invalidated snapshots
// 重新执行日志以撤消更改并删除无效的快照
s.journal.revert(s, snapshot)
s.validRevisions = s.validRevisions[:idx]
}
journal.entries
中积累了所有操作的回滚操作。当调用
StateDB.RevertToSnapshot
进行回滚操作时,就会调用
journal.revert
方法:
core/state/journal.go
// revert undoes a batch of journalled modifications along with any reverted
// dirty handling too.
// revert撤消一批日志修改以及还原脏处理。
func (j *journal) revert(statedb *StateDB, snapshot int) { //根据snapshot编号回滚。
for i := len(j.entries) - 1; i >= snapshot; i-- {
// Undo the changes made by the operation
// 撤消操作所做的更改
j.entries[i].revert(statedb)
// Drop any dirty tracking induced by the change
// 删除由更改引起的脏跟踪
if addr := j.entries[i].dirtied(); addr != nil {
if j.dirties[*addr]--; j.dirties[*addr] == 0 { // 修改的数量减至0则删除。
delete(j.dirties, *addr)
}
}
}
j.entries = j.entries[:snapshot]
}
综上,state 模块实现快照和回滚功能的过程如下[6]:
- 将所有可能的修改作一个统计。
- 实现所有可能单元操作对应的回滚操作对象。
- 在每次进行操作前,将对应的回滚对象加入到回滚操作的数组中,例如
。journal.entries
- 要在当前状态下创建一个快照,就记录下当前
的长度。journal.entries
- 要恢复某个快照(即实现回滚),就从
中最后一项开始,向前至指定的快照索引,逐一调用这些对象的journal.entries
方法。revert
stateObject
每个
stateObject
对应着一个正在修改的以太坊账户。
stateObject
的缓存结构与
StateDB
类似,结构如下:
core/state/state_object.go
// stateObject represents an Ethereum account which is being modified.
// stateObject表示一个正在修改的Ethereum帐户。
//
// The usage pattern is as follows:
// First you need to obtain a state object.
// Account values can be accessed and modified through the object.
// Finally, call CommitTrie to write the modified storage trie into a database.
// 使用模式如下:
// 首先,获得一个状态对象(stateObject)。
// 帐户值可以通过这个对象访问和修改。
// 最后,调用CommitTrie将修改后的存储树写入数据库。
type stateObject struct {
address common.Address // 以太坊账户地址
addrHash common.Hash // hash of ethereum address of the account 以太坊账户地址的hash
data Account // 以太坊账户的信息
db *StateDB // 状态数据库
// DB error.
// State objects are used by the consensus core and VM which are
// unable to deal with database-level errors. Any error that occurs
// during a database read is memoized here and will eventually be returned
// by StateDB.Commit.
// 数据库错误。
// stateObject会被共识算法的核心和VM使用,在这些代码内部无法处理数据库级别的错误。
// 在数据库读取期间发生的任何错误都会记录在这里,最终由StateDB.Commit返回。
dbErr error
// Write caches. 写缓存
trie Trie // storage trie, which becomes non-nil on first access 用户的存储树 ,在第一次访问的时候变得非空
code Code // contract bytecode, which gets set when code is loaded 合约字节码,加载代码时被设置
// 原始对象的存储缓存,用来避免对每个交易的重复重写、重置。
originStorage Storage // Storage cache of original entries to dedup rewrites, reset for every transaction
// 需要在整个块结束时刷入磁盘的用户存储对象。相当于写缓存
pendingStorage Storage // Storage entries that need to be flushed to disk, at the end of an entire block
// 在当前交易执行中被修改的存储项
dirtyStorage Storage // Storage entries that have been modified in the current transaction execution
fakeStorage Storage // Fake storage which constructed by caller for debugging purpose.伪存储,由调用者构造,用于调试目的。
// Cache flags.
// When an object is marked suicided it will be delete from the trie
// during the "update" phase of the state transition.
// Cache 标志
// 当一个对象被标记为自杀时,它将在状态转换的“更新”阶段从树中删除。
dirtyCode bool // true if the code was updated 如果代码被更新,设置为true
suicided bool // 自杀标志
deleted bool // 删除标志
}
// Account is the Ethereum consensus representation of accounts.
// These objects are stored in the main account trie.
// Account是账户的以太坊一致性表示。这些对象存储在主帐户树(main account trie)中。
type Account struct {
Nonce uint64
Balance *big.Int
Root common.Hash // merkle root of the storage trie
CodeHash []byte
}
stateObject
内部也有一个
state.Trie
类型的成员
trie
,被称为
storage trie
,它里面存放的是
state
数据,
state
跟每个账户相关,是
[Hash, Hash]
键值对。
stateObject
内部也有类似
StateDB
一样的二级数据缓存机制,用来缓存和更新这些
state
。
stateObject
的
originStorage
字段用来缓存原始对象,内容与数据库中一致;
pendingStorage
字段相当于写缓存,暂时将完成修改的
state
在内存中保存,稍后将更改写入
trie
;
dirtyStorage
字段在当前交易执行中被修改的
state项
,当交易结束时,
finalise
方法将此字段的内容转储到
pendingStorage
中,稍后进行hash或者提交。
stateObject
定义了
storage
,结构为
type Storage map[common.Hash]common.Hash
,用来存放
state
数据。当调用
SetState()
时,
storage
内部相应的
state
被更新,同时标记为"dirty"。之后,待有需要时(比如调用
updateTrie()
方法),那些标为"dirty"的
state
被一起写入
storage trie
,而
storage trie
中的所有内容在调用
CommitTrie()
时再一起提交到底层数据库。[7]
deepCopy
core/state/state_object.go
// deepCopy提供了stateObject的深拷贝。
func (s *stateObject) deepCopy(db *StateDB) *stateObject {
stateObject := newObject(db, s.address, s.data) // 分配一个新的stateObject
if s.trie != nil { // 复制存储树
stateObject.trie = db.db.CopyTrie(s.trie)
}
// 各种参数的复制
stateObject.code = s.code
stateObject.dirtyStorage = s.dirtyStorage.Copy()
stateObject.originStorage = s.originStorage.Copy()
stateObject.pendingStorage = s.pendingStorage.Copy()
stateObject.suicided = s.suicided
stateObject.dirtyCode = s.dirtyCode
stateObject.deleted = s.deleted
return stateObject
}
SetState
core/state/state_object.go
// SetState updates a value in account storage.
// SetState在账户存储里更新一个值。
func (s *stateObject) SetState(db Database, key, value common.Hash) {
// If the fake storage is set, put the temporary state update here.
// 如果设置了fake storage,将临时状态更新放在这里。
if s.fakeStorage != nil {
s.fakeStorage[key] = value
return
}
// If the new value is the same as old, don't set
// 如果新值和原来的一样,不做操作。
prev := s.GetState(db, key)
if prev == value {
return
}
// New value is different, update and journal the change
// 新值和原来的不同,更新它然后日志记录此更改。
s.db.journal.append(storageChange{
account: &s.address,
key: key,
prevalue: prev,
})
s.setState(key, value)
}
// 更新状态,直接将更改放入dirtyStorage。
func (s *stateObject) setState(key, value common.Hash) {
s.dirtyStorage[key] = value
}
finalise
core/state/state_object.go
// finalise moves all dirty storage slots into the pending area to be hashed or
// committed later. It is invoked at the end of every transaction.
// finalise将所有脏的存储槽移动到挂起的写缓存区域,稍后进行hash或者提交。它在每个事务结束时调用。
func (s *stateObject) finalise() {
// 转储
for key, value := range s.dirtyStorage {
s.pendingStorage[key] = value
}
if len(s.dirtyStorage) > 0 { // 清空
s.dirtyStorage = make(Storage)
}
}
updateTrie
core/state/state_object.go
// updateTrie writes cached storage modifications into the object's storage trie.
// It will return nil if the trie has not been loaded and no changes have been made
// updateTrie将缓存的存储修改写入对象的存储树。如果没有加载树并且没有进行任何更改,那么它将返回nil
func (s *stateObject) updateTrie(db Database) Trie {
// Make sure all dirty slots are finalized into the pending storage area
// 确保所有的脏存储槽都被转储到pending storage区域。
s.finalise()
// 没有任何更改,直接返回
if len(s.pendingStorage) == 0 {
return s.trie
}
// Track the amount of time wasted on updating the storge trie
// 跟踪更新storge trie所耗费的时间
if metrics.EnabledExpensive {
defer func(start time.Time) { s.db.StorageUpdates += time.Since(start) }(time.Now())
}
// Insert all the pending updates into the trie
// 将所有挂起的更新插入存储树中。
tr := s.getTrie(db)
for key, value := range s.pendingStorage {
// Skip noop changes, persist actual changes
// 跳过空操作,持久化实际更改。
if value == s.originStorage[key] {
continue
}
// 将新值加入原始对象的缓存
s.originStorage[key] = value
// 删除操作
if (value == common.Hash{}) {
s.setError(tr.TryDelete(key[:]))
continue
}
// Encoding []byte cannot fail, ok to ignore the error.
// 编码操作不会出现错误,允许忽略返回的错误
v, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(value[:]))
// 更新操作
s.setError(tr.TryUpdate(key[:], v))
}
// 清空缓存
if len(s.pendingStorage) > 0 {
s.pendingStorage = make(Storage)
}
return tr
}
// UpdateRoot sets the trie root to the current root hash of
// UpdateRoot将树根设置为当前根的hash
func (s *stateObject) updateRoot(db Database) {
// If nothing changed, don't bother with hashing anything
// 如果没有任何更改,那就不用去对任何东西进行散列。
if s.updateTrie(db) == nil {
return
}
// Track the amount of time wasted on hashing the storge trie
if metrics.EnabledExpensive {
defer func(start time.Time) { s.db.StorageHashes += time.Since(start) }(time.Now())
}
s.data.Root = s.trie.Hash() // 换成当前根
}
CommitTrie
core/state/state_object.go
// CommitTrie the storage trie of the object to db.
// This updates the trie root.
// CommitTrie将对象的存储树提交到数据库。
// 这将更新树的根。
func (s *stateObject) CommitTrie(db Database) error {
// If nothing changed, don't bother with hashing anything
if s.updateTrie(db) == nil {
return nil
}
// 若有错误,返回错误
if s.dbErr != nil {
return s.dbErr
}
// Track the amount of time wasted on committing the storge trie
// 跟踪提交storge trie所耗费的时间
if metrics.EnabledExpensive {
defer func(start time.Time) { s.db.StorageCommits += time.Since(start) }(time.Now())
}
root, err := s.trie.Commit(nil) // 提交
if err == nil {
s.data.Root = root
}
return err
}
参考文献
-
Ethereum Yellow Paper
ETHEREUM: A SECURE DECENTRALISED GENERALISED TRANSACTION LEDGER
https://ethereum.github.io/yellowpaper/paper.pdf
-
Ethereum White Paper
A Next-Generation Smart Contract and Decentralized Application Platform
https://github.com/ethereum/wiki/wiki/White-Paper
-
ethereumbook
The Ethereum Virtual Machine
https://github.com/ethereumbook/ethereumbook/blob/develop/13evm.asciidoc
-
Ethereum EVM Illustrated
https://github.com/takenobu-hs/ethereum-evm-illustrated
-
Go Ethereum Code Analysis
https://github.com/ZtesoftCS/go-ethereum-code-analysis
-
以太坊源码解析:state
https://yangzhe.me/2019/06/19/ethereum-state/
-
以太坊数据结构与存储
https://blog.csdn.net/weixin_41545330/article/details/79394153