死磕以太坊源码分析之state
死磕以太坊源码分析之state
配合以下代码进行阅读:https://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个交易,第一个区块的最终状态产生,第二个区块的第一笔交易执行后将会改变第一个区块链的最终状态,以此类推,从而产生最终的区块状态。

以太坊状态数据库
区块的状态数据并非保存在链上,而是将这些状态维护在默克尔压缩前缀树中,在区块链上仅记录对应的Trie Root 值。使用LevelDB维护树的持久化内容,而这个用来维护映射的数据库叫做 StateDB。
首先我们用一张图来大致了解一下StateDB:

可以看到图中一共有两种状态,一个是世界状态Trie,一个是storage Trie,两者都是MPT树,世界状态包含了一个个的账户状态,账户状态通过以账户地址为键,维护在表示世界状态的树中,而每个账户状态中存储这账户存储树的Root。账户状态存储一下信息:
- nonce: 表示此账户发出的交易数量
- balance: 账户余额
- storageRoot: 账户存储树的Root根,用来存储合约信息
- 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
创建状态对象会在两个地方进行调用:
- 检索或者创建状态对象
- 创建账户
最终都会去调用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
主要就做了两件事:
- 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->insert
s.finalise()将dirtyStorage中的所有数据移动到pendingStorage中- 根据账户哈希和账户
root打开账户存储树 - 将
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
}
大致就做了以下几件事:
- 先从
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的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方法完成。
- 计算状态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.IntermediateRoot
2.CommitTrie->updateTrie->trie.Commit->trie.db.insertPreimage(已经有了直接持久化到硬盘数据库)
->t.trie.Commit(没有就提交到存储树中)
最后看一下以太坊数据库的读写过程:

如果觉得文章还可以,关注下https://github.com/blockchainGuide此项目哦,欢迎有想法的人一起维护。
参考
https://github.com/blockchainGuide
https://www.jianshu.com/p/20d7f7c37b03
https://hackernoon.com/getting-deep-into-ethereum-how-data-is-stored-in-ethereum-e3f669d96033
https://web.xidian.edu.cn/qqpei/files/Blockchain/4_Data.pdf
死磕以太坊源码分析之state的更多相关文章
- 死磕以太坊源码分析之Kademlia算法
死磕以太坊源码分析之Kademlia算法 KAD 算法概述 Kademlia是一种点对点分布式哈希表(DHT),它在容易出错的环境中也具有可证明的一致性和性能.使用一种基于异或指标的拓扑结构来路由查询 ...
- 死磕以太坊源码分析之p2p节点发现
死磕以太坊源码分析之p2p节点发现 在阅读节点发现源码之前必须要理解kadmilia算法,可以参考:KAD算法详解. 节点发现概述 节点发现,使本地节点得知其他节点的信息,进而加入到p2p网络中. 以 ...
- 死磕以太坊源码分析之rlpx协议
死磕以太坊源码分析之rlpx协议 本文主要参考自eth官方文档:rlpx协议 符号 X || Y:表示X和Y的串联 X ^ Y: X和Y按位异或 X[:N]:X的前N个字节 [X, Y, Z, ... ...
- 死磕以太坊源码分析之Fetcher同步
死磕以太坊源码分析之Fetcher同步 Fetcher 功能概述 区块数据同步分为被动同步和主动同步: 被动同步是指本地节点收到其他节点的一些广播的消息,然后请求区块信息. 主动同步是指节点主动向其他 ...
- 死磕以太坊源码分析之Ethash共识算法
死磕以太坊源码分析之Ethash共识算法 代码分支:https://github.com/ethereum/go-ethereum/tree/v1.9.9 引言 目前以太坊中有两个共识算法的实现:cl ...
- 死磕以太坊源码分析之downloader同步
死磕以太坊源码分析之downloader同步 需要配合注释代码看:https://github.com/blockchainGuide/ 这篇文章篇幅较长,能看下去的是条汉子,建议收藏 希望读者在阅读 ...
- 死磕以太坊源码分析之txpool
死磕以太坊源码分析之txpool 请结合以下代码阅读:https://github.com/blockchainGuide/ 写文章不易,也希望大家多多指出问题,交个朋友,混个圈子哦 交易池概念原理 ...
- 死磕以太坊源码分析之MPT树-上
死磕以太坊源码分析之MPT树-上 前缀树Trie 前缀树(又称字典树),通常来说,一个前缀树是用来存储字符串的.前缀树的每一个节点代表一个字符串(前缀).每一个节点会有多个子节点,通往不同子节点的路径 ...
- 死磕以太坊源码分析之MPT树-下
死磕以太坊源码分析之MPT树-下 文章以及资料请查看:https://github.com/blockchainGuide/ 上篇主要介绍了以太坊中的MPT树的原理,这篇主要会对MPT树涉及的源码进行 ...
随机推荐
- jupyterlab 增加新内核的方法ipykernel
参考: https://blog.csdn.net/C_chuxin/article/details/82690830
- 原创:DynamicDataDisplay波形显示自定义格式
原创:DynamicDataDisplay 原版本在日期显示的格式上与我们的习惯不一样,特做如下修改: 自定义日期格式修改: //MainWindow.cs中 var ds = new Enumera ...
- 一文打尽PWM协议、PPM协议、PCM协议、SBUS协议、XBUS协议、DSM协议 | STM32的通用定时器TIM3实现PPM信号输出
PWM.PPM.PCM.SBUS.XBUS.DSM都是接收机与其他设备通信的协议. 请注意这里不要将遥控器和接收机之间的协议混淆.遥控器和接收机之间会采用某种协议来互相沟通,这些协议往往各个厂牌各自有 ...
- NetCDF格式.nc
netcdf sfc_pres_temp { dimensions: latitude = 6 ; //纬度轴 longitude = 12 ; //经度轴 variables: float lati ...
- C++ 虚函数表与多态 —— 关键字 final 的用法
final 字面上最终.最后.不可改变的意思,final 这个关键字在 Jave PHP C++中都有用到,其作用也基本一致. C++中的 final 是C++11新增,他可以用来修饰类,让类无法被继 ...
- Object not found! The requested URL was not found on this server.... 报错解决方案
服务器(centos6.5) lnmp 报错如下 Object not found! The requested URL was not found on this server. The link ...
- UML—20—003
博客班级 https://edu.cnblogs.com/campus/fzzcxy/2018SE1 作业要求 https://edu.cnblogs.com/campus/fzzcxy/2018SE ...
- matplotlib的学习16-animation动画
from matplotlib import pyplot as plt from matplotlib import animation import numpy as np fig, ax = p ...
- NGINX镜像的制作
NGINX镜像的制作 # mkdir -pv /opt/nginx # cd /opt/nginx/ # cat index.html www.dexter.com 编写Dockerfile # ...
- 【入门必看】不理解「对象」?很可能有致命bug:简单的Python例子告诉你
简介:越来越多的人要在学习工作中用到『编程』这个工具了,其中很大一部分人用的是Python.大部分人只是做做简单的科研计算.绘图.办公自动化或者爬虫,但-- 这就不需要理解「指针与面向对象」了吗? 在 ...