以太坊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??
// 因此我们需要确保在提交(或类似的操作)期间日志可能