以太坊 黄皮书
写在篇头
本文是对以太坊的黄皮书的解析,并参照 go-ethereum 中的实现,将相应的代码也列了出来。黄皮书中使用了大量的公式将以太坊的一些流程和状态都公式化了。看不看得懂公式对理解的影响不大。本文中对公式进行了解析。嫌麻烦的可以跳过每部分公式解析的部分。
一、区块链范型
以太坊本质是一个基于交易的状态机(transaction-based state machine)。其以初始状态(genesis state) 为起点,通过执行交易来到达新的状态。
公式 1 表示 t + 1 时的状态,是由 t 时的状态经过交易 T 转变而来。转变函数为
如下图所示
公式 2 - 4 是从区块的角度来描述状态的转化过程。
公式 2: t+ 1 时的状态,是由 t 时的状态经过区块 B 转变而来。转变函数为
公式 3: 区块 B 是包含了一系列交易 T 的集合。
公式 4: 区块的状态转变函数
在以太坊中的实际情况就是区块验证和执行的过程。
- 逐一的执行交易(也就是使用交易转变函数操作
Υ 状态集)。实际就是交易比方是 A 向 B 转 10ether,则 A 账户值 -10,B 账户值 +10。(当然执行过程中还有 gas 消耗,这个后面详述) - 等整个 block 交易执行完毕后,需要对矿工进行奖励。也就是需要使用
Ω 进行一次状态转换。1.1 货币
以太坊中有以下四种单位的货币。以太坊中的各种计算都是以 Wei 为单位的。 (看有的地方好像有更多种单位,我这边是直接按照黄皮书走的)
Multiplier | Name |
---|---|
100 | Wei |
1012 | Szabo |
1015 | Finney |
1018 | Ether |
1.2 分叉
以太坊的正确运行建立在其链上只有一个链是有效的,所有人都必须要接受它。拥有多个状态(或多个链)会摧毁这个系统,因为它在哪个是正确状态的问题上不可能得到统一结果。如果链分叉了,你有可能在一条链上拥有 10 个币,一条链上拥有 20 个币,另一条链上拥有 40 个币。在这种场景下,是没有办法确定哪个链才是最”有效的“。不论什么时候只要多个路径产生了,一个”分叉“就会出现。 为了确定哪个路径才是最有效的以及防止多条链的产生,以太坊使用了一个叫做“GHOST 协议 (GHOST protocol.)”的数学机制。
简单来说,GHOST 协议就是让我们必须选择一个在其上完成计算最多的路径。一个方法确定路径就是使用最近一个区块(叶子区块)的区块号,区块号代表着当前路径上总的区块数(不包含创世纪区块)。区块号越大,路径就会越长,就说明越多的挖矿算力被消耗在此路径上以达到叶子区块。
二、 区块、状态与交易
2.1 世界状态
以太坊中的世界状态指地址 (Address) 与账户状态 (Account State) 的集合。世界状态并不是存储在链上,而是通过 Merkle Patricia tree 来维护。 账户状态(Account State)包含四个属性。
- nonce: 如果账户是一个外部拥有账户,nonce 代表从此账户地址发送的交易序号。如果账户是一个合约账户,nonce 代表此账户创建的合约序号。用
σ[a]n 来表示。 - balance:此地址拥有 Wei 的数量。1Ether=10^18Wei。用
σ[a]b 来表示。 - storageRoot: 理论上是指 Merkle Patricia 树的根节点 256 位的 Hash 值。用
σ[a]s 来表示。公式 6 中有介绍。 - codeHash:此账户 EVM 代码的 hash 值。对于外部拥有账户,codeHash 域是一个空字符串
的 Hash 值。对于合约账户,就是代码的 Hash 作为 codeHash 保存。用
σ[a]c 来表示。
// github.com/ethereum/go-ethereum/core/state/state_object.go
type Account struct {
Nonce uint64
Balance *big.Int
Root common.Hash // merkle root of the storage trie
CodeHash []byte
}
关于 storageRoot 的补充
公式 6, 由于有些时候我们不仅需要 state 的 hash 值的 trie,而是需要其对应的 kv 数据也包含其中。所以以太坊中的存储 State 的树,不仅包含 State 的 hash,同时也包含了存储这个账户的 address 的 hash 和它对应的 data 也就是其 Account 的值的数据对的集合。这里 storageRoot 实际上是这样的树的根节点 hash 值。
公式 7,指 state 对应的 kv 数据的 RLP 的形式化表示,是 k 的 hash 值作为 key,value 是 v 的 RLP 表示。也就是以太坊中实际存储的 state 是账户 address 的 hash
公式 8,指公式 7 中的 k 是 32 的字符数组。这个是由 KECCAK256 算法保证的。
注:原本公式 8 中要求的是 v 是个正整数,但是我看来下代码和下文公式 10,感觉这里的 v 都应该是 Account 的内容
一些符号化定义
以太坊中的账户有两类,一类是外部账户,一类是合约账户。其中外部账户被私钥控制且没有任何代码与之关联。合约账户,被它们的合约代码控制且有代码与之关联。以下几个公式定义了账户的各种状态。
公式 9,定义了函数
公式 10,定义
公式 11 与公式 12,对账户
公式 13,定义了空账户。若一个账户,其地址为空字符,并且该账户 nonce 值为 0,balance 值也为 0.
公式 14,定义了死账户,死账户要么为空
2.2 交易
- nonce: 与发送该交易的账户的 nonce 值一致。用
Tn 表示。 - gasPrice: 表示每 gas 的单价为多少 wei。用
Tp 表示。 - gasLimit: 执行该条交易最大被允许使用的 gas 数目。用
Tg 表示。 - to:160 位的接受者地址。当交易位创建合约时,该值位空。用
Tt 表示。 - value: 表示发送者发送的 wei 的数目。该值为向接受者转移的 wei 的数目,或者是创建合约时作为合约账户的初始 wei 数目。用
Tv 表示。 - v,r,s: 交易的签名信息,用以决定交易的发送者。分别用
Tw ,Tr ,Ts 表示。 - init: 如果是创建合约的交易, 则 init 表示一段不限长度的 EVM-Code 用以合约账户初始化的过程。用
Ti 表示。 - data: 调用合约的交易,会包含一段不限长度的输入信息,用
Td 表示。
// github.com/ethereum/go-ethereum/core/types/transaction.go
type Transaction struct {
data txdata
// caches
hash atomic.Value
size atomic.Value
from atomic.Value
}
type txdata struct {
AccountNonce uint64 `json:"nonce" gencodec:"required"`
Price *big.Int `json:"gasPrice" gencodec:"required"`
GasLimit uint64 `json:"gas" gencodec:"required"`
Recipient *common.Address `json:"to" rlp:"nil"` // nil means contract creation
Amount *big.Int `json:"value" gencodec:"required"`
Payload []byte `json:"input" gencodec:"required"`
// Signature values
V *big.Int `json:"v" gencodec:"required"`
R *big.Int `json:"r" gencodec:"required"`
S *big.Int `json:"s" gencodec:"required"`
// This is only used when marshaling to JSON.
Hash *common.Hash `json:"hash" rlp:"-"`
}
一些符号化定义
以太坊中根据交易中的 to 值是否为空,可以判断交易是创建合约还是执行合约。
公式 15 表示,如果 to 的值
公式 16,是对交易的各个字段限制的符号化定义。其意思是 nonce、value、gasPrice、gasLimit 以及特殊的用来验证签名的 r 和 s 都是小于 2 256的正整数,用来验证签名的 v (
公式 17,是对
公式 18,是对交易中的 to 字段的符号化定义,当其不为空的时候,是 20 位的字符,为空的时候是 0 位字符。
2.3 区块
以太坊中的一个区块由区块头 Header,以及交易列表
Header 包括以下字段。
- parentHash: 父节点的 hash 值。用
Hp 表示。 - ommersHash: uncle 节点的 hash 值,这块是跟 GHOST 相关的,用
Ho 表示。 - beneficiary: 矿工 address,用
Hc 表示。 - stateRoot: 当所有交易都执行完毕后的世界状态树的根节点,用
Hr 表示。 - transactionsRoot: 交易列表的根节点,用
Ht 表示。 - receiptsRoot: 收据的根节点,用
He 表示。 - logsBloom: 日志过滤器,用
Hb 表示。这个暂时没细看,不太确定。 - difficulty: 区块难度,根据上一个区块的难度以及时间戳算出来的值,用
Hd 表示。 - number: 区块号,用
Hi 表示。 - gasLimit: 区块的 gas 数量限制,即区块中交易使用掉的 gas 值不应该超过该值。用
Hl 表示。 - gasUsed: 区块使用掉的 gas 数量,用
Hg 表示。 - timestamp: 时间戳,用
Hs 表示。 - extraData: 额外的数据,合法的交易对长度有限制,用
Hx 表示。 - mixHash: 与 nonce 一起用作工作量证明,用
Hm 表示。 - nonce: 与 mixHash 一起用作工作量证明,用
Hn 表示。
// github.com/ethereum/go-ethereum/core/types/block.go
// "external" block encoding. used for eth protocol, etc.
type extblock struct {
Header *Header
Txs []*Transaction
Uncles []*Header
}
// Header represents a block header in the Ethereum blockchain.
type Header struct {
ParentHash common.Hash `json:"parentHash" gencodec:"required"`
UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"`
Coinbase common.Address `json:"miner" gencodec:"required"`
Root common.Hash `json:"stateRoot" gencodec:"required"`
TxHash common.Hash `json:"transactionsRoot" gencodec:"required"`
ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"`
Bloom Bloom `json:"logsBloom" gencodec:"required"`
Difficulty *big.Int `json:"difficulty" gencodec:"required"`
Number *big.Int `json:"number" gencodec:"required"`
GasLimit uint64 `json:"gasLimit" gencodec:"required"`
GasUsed uint64 `json:"gasUsed" gencodec:"required"`
Time *big.Int `json:"timestamp" gencodec:"required"`
Extra []byte `json:"extraData" gencodec:"required"`
MixDigest common.Hash `json:"mixHash" gencodec:"required"`
Nonce BlockNonce `json:"nonce" gencodec:"required"`
}
// Receipt represents the results of a transaction.
type Receipt struct {
// Consensus fields
PostState []byte `json:"root"`
Status uint `json:"status"`
CumulativeGasUsed uint64 `json:"cumulativeGasUsed" gencodec:"required"`
Bloom Bloom `json:"logsBloom" gencodec:"required"`
Logs []*Log `json:"logs" gencodec:"required"`
// Implementation fields (don't reorder!)
TxHash common.Hash `json:"transactionHash" gencodec:"required"`
ContractAddress common.Address `json:"contractAddress"`
GasUsed uint64 `json:"gasUsed" gencodec:"required"`
}
区块的符号化定义
2.3.1 交易收据
以太坊中为了将交易的信息进行编码,以方便索引以及查找或者零知识证明等相关的东西,为每条交易定义了一定收据。对于第 i 个交易,其收据用
每条收据都是一个四元组,包括区块当前累计使用的 gas 值
公式 20 对区块头中的收据作了定义,收据是个四元组,四元组定义如前文。
公式 21,收据的 RLP 形式化表示为
公式 22,表示
公式 23 对收据中当前累计使用的 gas 值
公式 24 对交易执行的日志
公式 25 对其限制进行了描述。
公式 26-30. 对日志过滤函数做了定义,这块涉及到东西是数据操作层面的,不影响对流程的理解。暂不作解释。
2.3.2 整体的合法性
上面介绍过了区块包含区块头,区块交易列表,以及区块的 ommer 区块的头三部分。以太坊中判断一个区块是否合法,首先需要对区块整体上做合法性判断。见公式 31
- 其区块头的状态树的根 stateRoot 也就是
Hr ,是否确实是状态树的根。 - 其区块头的 ommerHash 也就是
Ho , 是否与区块的 ommer 区块的头部分的 hash 值一致。 - 其区块头中的 transactionRoot 也就是
Ht,f 即区块中交易的树的根,是否和区块存储的交易列表中的交易一一对应。一一对应关系见公式 32,是将交易所在列表中的索引的 RLP 作为键,交易内容 v 的 RLP 作为值的键值对。 - 其区块头中的 recieptRoot 也就是
He ,即区块中收据的树的根,是否和区块存储的交易列表一一对应,是不是每条交易都有一条相应的收据。一一对应关系见公式 32,是将收据所在列表中的索引的 RLP 作为键,收据内容 v 的 RLP 作为值的键值对。 - 区块头中 logsBloom 也就是
Hb ,是否包含了区块的交易的所有日志。 - 执行该区块之前的状态树的根节点,是否与其父区块的中的根节点一致。见公式 33.
2.3.3 序列化
对区块,以及区块头的序列化表示如下。
公式 34 是区块头的序列化表示。
公式 35 是区块的序列化表示,即分别将区块头序列化,区块的交易列表,ommer 区块头序列化。其中交易序列化函数
公式 36 表示列表序列化和当个序列化的关系,列表的序列化,就是把列表中的元素分别序列化,然后将结果组成列表。
公式 37 是对区块头中属性的限制规定:
- 各种 hash 值都是 32 位字符。包括 parentHash
Hp ,ommerHashHo , stateRootHr , transactionRootHt , RecieptRootHe , mixHashHm 。 - 受益人也就是挖矿的人的地址 beneficiary
Hc , 是 20 位字符。 - 日志过滤器 logBoom
Hb , 是 256 位字符。 - 难度,区块号,gas 限制,用掉的 gas 等都是正整数。difficulty
Hd , NumberHi ,gasLimitHl gasUsedHg - 时间戳 timestamp
Hs 是个大的正整数,其值小于 2 256。 - 额外的数据 extraData
Hx 是未知长度字符。 - nonce
Hn 是 8 位的字符。2.3.4 区块头的合法性
确定区块是否合法除了整体性的合法校验,还需要对区块头进行更进一步的校验。主要校验规则有以下几点:
- parentHash 正确。即 parentHash 与其父区块的头的 hash 一致。
- number 为父区块 number 值加一。
- difficuilty 难度正确。区块合理的难度跟父区块难度,以及当前区块时间戳和父区块时间戳间隔以及区块编号有关。难度可以起到一定的调节出块时间的作用。,可以看出当出块变快(也就是出块间隔变小之后)难度会增加,相反难度会减小。
- gasLimit 和上一个区块的差值在规定范围内。
- gasUsed 小于等于 gasLimit
- timestamp 时间戳必须大于上一区块的时间戳。
- mixHash 和 nonce 必须满足 PoW。
- extraData 最多为 32 个字节。
// verifyHeader checks whether a header conforms to the consensus rules of the
// stock Ethereum ethash engine.
// See YP section 4.3.4. "Block Header Validity"
func (ethash *Ethash) verifyHeader(chain consensus.ChainReader, header, parent *types.Header, uncle bool, seal bool) error {
// Ensure that the header's extra-data section is of a reasonable size
// 验证extraData的长度
if uint64(len(header.Extra)) > params.MaximumExtraDataSize {
return fmt.Errorf("extra-data too long: %d > %d", len(header.Extra), params.MaximumExtraDataSize)
}
// Verify the header's timestamp
// 验证时间戳是否超过大小限制,是否过大,是否大于上一区块的时间戳等
if uncle {
if header.Time.Cmp(math.MaxBig256) > 0 {
return errLargeBlockTime
}
} else {
if header.Time.Cmp(big.NewInt(time.Now().Add(allowedFutureBlockTime).Unix())) > 0 {
return consensus.ErrFutureBlock
}
}
if header.Time.Cmp(parent.Time) <= 0 {
return errZeroBlockTime
}
// 验证难度是否正确
// Verify the block's difficulty based in it's timestamp and parent's difficulty
expected := ethash.CalcDifficulty(chain, header.Time.Uint64(), parent)
if expected.Cmp(header.Difficulty) != 0 {
return fmt.Errorf("invalid difficulty: have %v, want %v", header.Difficulty, expected)
}
// Verify that the gas limit is <= 2^63-1
cap := uint64(0x7fffffffffffffff)
//验证gasLimit是否超了上限
if header.GasLimit > cap {
return fmt.Errorf("invalid gasLimit: have %v, max %v", header.GasLimit, cap)
}
//验证已用的gas值是否小于等于gasLimit
// Verify that the gasUsed is <= gasLimit
if header.GasUsed > header.GasLimit {
return fmt.Errorf("invalid gasUsed: have %d, gasLimit %d", header.GasUsed, header.GasLimit)
}
// Verify that the gas limit remains within allowed bounds
//判断gasLimit与父区块的gasLimit差值是否在规定范围内
diff := int64(parent.GasLimit) - int64(header.GasLimit)
if diff < 0 {
diff *= -1
}
limit := parent.GasLimit / params.GasLimitBoundDivisor
if uint64(diff) >= limit || header.GasLimit < params.MinGasLimit {
return fmt.Errorf("invalid gas limit: have %d, want %d += %d", header.GasLimit, parent.GasLimit, limit)
}
// Verify that the block number is parent's +1
//验证区块号,是否是父区块号+1
if diff := new(big.Int).Sub(header.Number, parent.Number); diff.Cmp(big.NewInt(1)) != 0 {
return consensus.ErrInvalidNumber
}
// Verify the engine specific seal securing the block
//验证PoW
if seal {
if err := ethash.VerifySeal(chain, header); err != nil {
return err
}
}
// If all checks passed, validate any special fields for hard forks
if err := misc.VerifyDAOHeaderExtraData(chain.Config(), header); err != nil {
return err
}
if err := misc.VerifyForkHashes(chain.Config(), header, uncle); err != nil {
return err
}
return nil
}
校验区块号和区块 hash 的符号化表示
公式 39 表示,parentHash 值应该为父节点 Header 的 hash 值。
公式 40 表示,number 为父节点 number + 1.
难度计算的符号化表示
公式 41-46 为 difficulty 的计算方法。
公式 41,42 表示,当区块编号为 0 的时候其难度值是固定好的,在这里用
公式 43,调节系数(the adjustment factor )
公式 44,难度系数(diculty parameter)
公式 45,46,“difficulty bomb”, or “ice age”
// CalcDifficulty is the difficulty adjustment algorithm. It returns
// the difficulty that a new block should have when created at time
// given the parent block's time and difficulty.
func (ethash *Ethash) CalcDifficulty(chain consensus.ChainReader, time uint64, parent *types.Header) *big.Int {
return CalcDifficulty(chain.Config(), time, parent)
}
// CalcDifficulty is the difficulty adjustment algorithm. It returns
// the difficulty that a new block should have when created at time
// given the parent block's time and difficulty.
func CalcDifficulty(config *params.ChainConfig, time uint64, parent *types.Header) *big.Int {
next := new(big.Int).Add(parent.Number, big1)
switch {
case config.IsByzantium(next):
return calcDifficultyByzantium(time, parent)
case config.IsHomestead(next):
return calcDifficultyHomestead(time, parent)
default:
return calcDifficultyFrontier(time, parent)
}
}
// 黄皮书中的介绍的是Byzantium难度协议,所以这里只给出相应的代码。其他几种难度调节协议只是参数值上有区别。
// calcDifficultyByzantium is the difficulty adjustment algorithm. It returns
// the difficulty that a new block should have when created at time given the
// parent block's time and difficulty. The calculation uses the Byzantium rules.
func calcDifficultyByzantium(time uint64, parent *types.Header) *big.Int {
// https://github.com/ethereum/EIPs/issues/100.
// algorithm:
// diff = (parent_diff +
// (parent_diff / 2048 * max((2 if len(parent.uncles) else 1) - ((timestamp - parent.timestamp) // 9), -99))
// ) + 2^(periodCount - 2)
bigTime := new(big.Int).SetUint64(time)
bigParentTime := new(big.Int).Set(parent.Time)
// holds intermediate values to make the algo easier to read & audit
x := new(big.Int)
y := new(big.Int)
// (2 if len(parent_uncles) else 1) - (block_timestamp - parent_timestamp) // 9
x.Sub(bigTime, bigParentTime)
x.Div(x, big9)
if parent.UncleHash == types.EmptyUncleHash {
x.Sub(big1, x)
} else {
x.Sub(big2, x)
}
// max((2 if len(parent_uncles) else 1) - (block_timestamp - parent_timestamp) // 9, -99)
if x.Cmp(bigMinus99) < 0 {
x.Set(bigMinus99)
}
// parent_diff + (parent_diff / 2048 * max((2 if len(parent.uncles) else 1) - ((timestamp - parent.timestamp) // 9), -99))
y.Div(parent.Difficulty, params.DifficultyBoundDivisor)
x.Mul(y, x)
x.Add(parent.Difficulty, x)
// minimum difficulty can ever be (before exponential factor)
if x.Cmp(params.MinimumDifficulty) < 0 {
x.Set(params.MinimumDifficulty)
}
// calculate a fake block number for the ice-age delay:
// https://github.com/ethereum/EIPs/pull/669
// fake_block_number = min(0, block.number - 3_000_000
fakeBlockNumber := new(big.Int)
if parent.Number.Cmp(big2999999) >= 0 {
fakeBlockNumber = fakeBlockNumber.Sub(parent.Number, big2999999) // Note, parent is 1 less than the actual block number
}
// for the exponential factor
periodCount := fakeBlockNumber
periodCount.Div(periodCount, expDiffPeriod)
// the exponential factor, commonly referred to as "the bomb"
// diff = diff + 2^(periodCount - 2)
if periodCount.Cmp(big1) > 0 {
y.Sub(periodCount, big2)
y.Exp(big2, y, nil)
x.Add(x, y)
}
return x
}
gasLimit 限制的符号化表示
公式 47 表示区块的 gasLimit 必须大于等于 5000,且其和上一个区块的 gasLimit 差值不超过
时间戳的符号化表示
公式 48 表示当前区块的时间戳必须大于父区块的时间戳。(代码中要求区块的时间戳不能比当前时间大 15 秒以上)
mixHash 和 nonce 相关符号化表示
参考资料: