前言

正如以太坊官方黄皮书所描述的一样: 以太坊是一个巨大的状态机,每次交易的执行、区块的产生都是状态的迁移变化。

本文就将围绕以太坊的state模块进行讨论以太坊状态的变化。

关于以太坊中的“状态”

状态的定义

一个账户对应一个状态,而以太坊是所有状态的集合。
比如,最开始的状态是:{A有10元,B有0元},后来A发起了交易,给B2元,状态变成{A有8元,B有2元},这中间的过程就是状态转移。
在以太坊中,从创世状态开始,每次执行block的交易后,状态被修改成最新的的最终状态,在任何时刻这个最终状态都代表着以太坊当前的最新状态。

状态转换示意如下图:

以太坊状态转换

状态的表示

以太坊中用root来表示某一时刻的状态。
以太坊使用Trie组织状态,Trie可以理解为是字典树和默克尔树的结合,它有一个树根root,有这个root,你就可以访问所有的状态数据,即每个账户的信息,所以用root来表示一个状态。

状态存在哪

状态不存在区块中。区块头中存放了root,这只是一个地址,从区块中并不能找到状态的数据。

每一个新的区块都关联以前的区块信息。
状态只是临时的数据,可以再生成。创世块是最初的状态,把第一个区块中的交易都执行一遍,就得到了一个新的状态,把这个状态的root存到第一个区块头的Root中。如果有所有的区块,就可以把所有的交易都执行,然后生成最新区块中的状态。

状态存放在外部数据库。以太坊底层的数据库是LevelDB(k-v),区块存放在里面,状态也存放在里面。但状态是一个Trie,不能直接存在LevelDB里面。

状态的转变

从不同的角度看,以太坊的状态分为交易级别的状态转换和区块级别的状态转换。

图片来源:以太坊状态转换流程分析

以太坊交易级的状态转换.
以太坊区块级的状态转换.

关于以太坊存储的设计

以太坊存储模块的架构图:

以太坊储存层次逻辑

state所在的目录是:core/state,它的文件和每个文件的主要功能如下:

1
2
3
4
5
6
7
8
9
core/state
├── database.go,底层的存储设计,`Trie`和`Database`定义在此文件。
├── dump.go,用来dumpstateDB数据。
├── iterator.go,用来遍历`Trie`。
├── journal.go,用来记录状态的每一步改变。
├── managed_state.go,给txpool使用,具体功能未研究。
├── state_object.go,每一个账户的状态。
├── statedb.go,以太坊整个的状态。
├── sync.go,用来和downloader结合起来同步state。

我们先介绍它的设计思路,然后再介绍一些它的骨干实现。

stateObject.go

stateObject代表最小粒度的状态,它是一个账户的状态信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 帐户的结构
type Account struct {
Nonce uint64 // 帐户发起交易的次数
Balance *big.Int // 帐户的余额
Root common.Hash // 存储空间的Merkle根节点hash
CodeHash []byte // 合约代码的hash值(合约帐户特有)
}

// stateObject是对Account的抽象
type stateObject struct {
address common.Address // 帐户地址
addrHash common.Hash // hash of ethereum address of the account 帐户地址hash
data Account // 帐户信息,struct Account
db *StateDB // 所属的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.
// EVM不处理db层的错误,先记录下来,最后返回,只能保存1个错误,保存存的第一个错误
dbErr error

// Write caches.
// 使用trie组织stateObj的数据
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
// 需要写到磁盘的缓存
dirtyStorage Storage // Storage entries that need to be flushed to disk

// Cache flags.
// When an object is marked suicided it will be delete from the trie
// during the "update" phase of the state transition.
// 标记stateObject.code被修改了
dirtyCode bool // true if the code was updated
// 标记suicided,代表这个对象要从trie删除,在update阶段
suicided bool // 标记上层调用了自杀命令
deleted bool // 标记账户已经从数据库中删除
}

stateObject保存了2个重要信息:

  • 账户的信息:Account、Address、Code。创建账户之后,这些数据就不变了。
  • 账户的数据:trie。对于合约账户,trie用来存储数据,因此trie是经常变化的。比如,有新的转账交易,就有新的数据(余额)产生和改变,trie也就发生改变。

database.go

statedb.go

  • 存储所有的账户信息(stateObject)。
  • 提供增删、修改账户的状态数据(stateObject)的接口。
  • Finalise和提交修改的账户信息(stateObject)。
  • 对每个状态数据改变记录日志,创建快照,实现回滚。