以太坊-入门基础(以太坊黄皮书学习)
文章目录
- 一、什么是以太坊黄皮书
- 二、以太坊黄皮书
- 引言
- 区块链范式
- 约定
- 区块、状态和交易
- 世界状态
- 账户状态
- 交易
- 区块
- 总结
- Gas 及其支付
- gasPrice 和 gasLimit
- 矿工
- 总结
- 交易执行
- 交易必须符合合规的 RLP 编码
- 交易必须具备合法签名
- 交易 nonce 和账户 nonce 必须匹配
- 交易的固有成本必须小于该交易设置的 gas 上限
- 交易发送方的账户余额必须大于等于交易所需的预付款
- 交易的 Gas Limit 必须小于等于区块的 Gas 上限
- 执行交易
- 执行
- 合约创建
- 合约调用
- 虚拟机的执行模型
- 费用
- 执行过程
- 区块的最终确定
- Ommer的校验
- 校验交易
- 计算奖励
- 校验state和nonce
- 三、参考
一、什么是以太坊黄皮书
以太坊白皮书
2014 年初,由以太坊创始人 Vitalik Buterin ( V 神)发表,从技术方面来看,白皮书只是描述了一种新技术的理论。
以太坊黄皮书
2014 年 4 月,由 Gavin Wood 博士(以太坊联合创始人兼 CTO )发布,号称以太坊的技术圣经,将以太坊虚拟机(EVM)等重要技术规格化。
以太坊紫皮书
2016年,V 神发布了一份紫皮书,为解决区块链的效率和能耗问题,提供了一种将 POS 和基于分片证明进行合并的解决方案,包括提高可扩展性、确保经济终结性和提高计算机抗审查等。
比特币背后的核心技术就是区块链技术,在区块链里加入“智能合约”就是以太坊。以太坊是一个平台,可以在以太坊上创建任何高级的合约,货币及其它的去中心化的应用。简而言之,以太坊就是可编程的区块链,使用Solidity编程语言就可以编写能够在以太坊区块链上运行的程序,那么以太坊黄皮书是什么呢?
以太坊黄皮书是关于以太坊技术的实现规范,黄皮书中使用了大量的公式将以太坊的一些流程和状态都公式化了。区块链对于交易数据的加密已经通过一系列项目展示了它的实用性,特别是在比特币技术上,以太坊就是以更广义的方式实现了这种模式,以太坊提供了大量的资源,每一个资源都拥有独立的状态,而且可以通过消息传递的方式完成和其它资源交互的功能。以太坊黄皮书就是探讨研究了它的设计、实现难题以及以后可能会遇到的一些问题。
以太坊黄皮书大致分为简历、区块链、约定、块状态和交易、燃料和支付、交易执行、合约的创建、消息调用、执行模型、区块树到区块链以及执行合约等部分。
总而言之,以太坊黄皮书就是关于以太坊技术的实现规范,探讨研究以太坊中发现的问题以及以太坊的设计问题。
二、以太坊黄皮书
中文版:https://github.com/wanshan1024/ethereum\_yellowpaper/blob/master/ethereum\_yellow\_paper\_cn.pdf
解读以太坊黄皮书
参考URL: https://ethfans.org/ajian1984/articles/36719
以太坊黄皮书详解(一)
参考URL: https://www.jianshu.com/p/10a7b6a67232
1. 引言
在这一节中,作者介绍了以太坊项目的目标和驱动因素,并且引用了很多前人的大作。
2. 区块链范式
公式 1 是从状态转换顺序的角度对“以太坊计算机”下的数学定义 。我们来看一下:
σt+1≡Υ(σt,T) …… (1)
这个公式内包含以下几个参数:
- σt+1 代表下一个世界状态(后面会详细介绍世界状态)
- Υ 代表以太坊状态转换函数
- σt 代表当前世界状态
- T 代表一个交易
这个公式表明,交易(输入)会影响(处理)当前的世界状态(存储),最后得到一个新的世界状态(存储/输出)。
另一种思路就是将以太坊看作是状态转换机。在这个模型中,交易 T 就是当前状态 σ t 和 下 一 个 状 态 σ t + 1 σt和下一个状态σt+_1 σt和下一个状态σt+1 之间的弧线。
3. 约定
我使用了大量的排印约定来表示公式中的符号,其中一
些需要特别说明:
有两个高度结构化的顶层状态值,用粗体小写希腊字
母 σ 表示世界状态(world-state);用 µ 表示机器状态
(machine-state)。
作用在高度结构化数据上的函数,使用大写的希腊字母,
例如: Υ,是以太坊中的状态转换函数。
4. 区块、状态和交易
世界状态
以太坊中的世界状态指地址(Address)与账户状态(Account State)的集合。世界状态并不是存储在链上,而是通过Merkle Patricia tree来维护。
账户状态
以太坊中有两种账户类型:外部所有账户(Externally Owned Accounts 简称 EOA)以及合约账户。我们用来互相收发以太币、部署智能合约的账户就是 EOA 账户, 而部署智能合约时自动生成的账户则是合约账户。每一个智能合约都有其独一无二的以太坊账户。
账户状态反映了一个以太坊账户的各项信息。例如,它存储了当前账户以太币的余额信息、当前账户发送过的交易数量…每一个账户都有账户状态。
// 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
}
下面就来看看账户状态中都包括什么:
- nonce
从此地址发送出去的交易数量(如果当前为 EOA 账户)或者此账号产生的合约创建操作(现在先别管合约创建操作是什么)。 - balance
此账号所拥有的以太币数量(以 Wei 计量)。 - storageRoot
账户存储树的根节点哈希值 - codeHash
对于合约账户,就是此账户存储 EVM 代码的哈希值。对于 EOA 账户,此处留空。
账户状态中不容忽视的一个细节是,上述对象在内的所有对象都可变(除了 codeHash)。举例来说,当一个账户向其他账户发送以太币时,除了 nonce 会增加,账户的余额也会相应改变。
而 codeHash 的不可变性使得,如果部署了有漏洞的智能合约,也无法修复更新此合约。对应的,只能部署一个新合约(而有漏洞的版本会一直存在于区块链上)。这也是为什么使用 Truffle 进行智能合约的开发和部署十分必要,并且用 Solidity 编程时要遵循 最佳实践 的要求。
账户存储树是保存与账户相关联数据的结构。该项只有合约账户才有,而在 EOA 中, storageRoot 留空、 codeHash 则是一串空字符串的哈希值。**所有智能合约的数据都以 32 字节映射的形式保存在账户存储树中。**此处不再赘述账户状态树如何维持合约数据。
交易
交易推动当前状态到下一状态的转变。在以太坊中有三种交易:
- EOA 之间传输值的交易(例如,改变发送方和接收方余额大小)。
- 发送消息来调用合约的交易(例如,通过发送消息调用来触发 setter 方法,以设置合约中的值)。
- 用于部署合约的交易(由此创建了合约账户)。
(从技术角度来讲,前两种交易是一样的…它们都是通过消息调用来改变账户状态的交易,只不过一个是 EOA 账户,一个是合约账户。此处将交易分为三种是为了方便读者的理解。)
// 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:"-"`
}
交易由以下部分组成:
- nonce
此账户发出的交易序号数(校对注:可以粗略理解为“这是该账户的第几笔交易”)。与发送该交易的账户的nonce值一致。 - gasPrice
执行此交易、进行计算时为每单位 gas 所支付的费用(以 Wei 计量)。 - gasLimit
执行此交易时可以使用的最大 gas 数量。 - to
160位的接受者地址。
如果此交易用于传送以太币,此处为接收以太币的 EOA 地址。
如果此交易用于向合约发送消息(例如,调用智能合约中的方法),此处为合约的地址。
如果此交易用于创建合约,此处值为空。 - value
如果此交易用于收发以太币,此处为发往接收账户以 Wei 计量的代币数量。
如果此交易用于发送对合约的消息调用,此处为向接收此消息智能合约所给付的 Wei 数量。
如果此交易用于创建合约,此处为合约初始化时账户存放的以 Wei 计量的以太币数量 - v, r, s
在交易的密码学签名中用到的值,可以用于确定交易的发送方。 - data(只用于价值传输以及向智能合约发送消息调用)
发送消息调用时附带的输入数据(例如,假设你想要执行智能合约中的 setter 方法,数据区就应该包括 setter 方法的标识符,以及你想要设定的参数值)。 - init(只用于合约创建)
用于初始化合约的 EVM 代码。
区块中所有的交易也是存储在默克尔树中的。并且这棵树的根节点哈希值由区块头保存!
区块
区块分为两部分,即区块头和区块体。
区块头就是以太坊中的区块链部分。它保存了前一个区块(也可称为父区块)的哈希值,通过区块头的连接形成了一条由密码学背书的链。
区块体包含了此区块中记录的一系列交易,以及叔块(ommer)区块头列表。
// 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"`
}
下面就来介绍区块头包括哪些部分。
- parentHash
前一个区块的区块头哈希值。每个区块都包含前序区块的哈希值,一路可回溯至链上的创世块。这也就是维护数据不会被篡改的结构设计(任何对前序区块的篡改都会影响后续所有区块的哈希值)。 - ommersHash
叔块头以及部分区块体的哈希值。
- beneficiary
因为挖到此区块而获得收益的以太坊账户。
- stateRoot
世界状态树的根节点哈希值(在所有交易被执行后)。
- transactionsRoot
- 交易树根节点的哈希值。这棵树包含了区块体的所有交易。
receiptsRoot
每当交易执行时,以太坊都会生成对应结果的交易收据。此处就是这个交易收据树的根节点哈希。
- logsBloom
布隆过滤器,用于判断某区块的交易是否产生了某日志(如果对这方面感兴趣,可以查阅 Stack Overflow 的这个答案)。这避免了在区块中存储日志信息(节省了大量空间)。
- difficulty
此区块的难度值。这是当前区块挖矿难度的度量值(此处不对此概念的细节和计算作介绍)。
- number
前序区块的总数。这标示了区块链的高度(即区块链上有多少区块)。创世区块的 number 为 0 。
- gasLimit
每一个交易都需要消耗 gas 。gas limit 标示了该区块所记录的所有交易可以使用的 gas 总量。这是限制区块内交易数量的一种手段。
- gasUsed
区块中各条交易所实际消耗的 gas 总量。
- timestamp
区块创建时的 Unix 时间戳。谨记由于以太坊网络去中心化的特性,我们不能信任这个值,特别是撰写智能合约、涉及到时间相关的商业逻辑时不能依靠这个值。
- extraData
能输入任何东西的不定长字节数组。当矿工创建区块时,可以在这个区域添加任何东西。
- mixHash
用于验证一个区块是否被真正记录到链上的哈希值(如果想要真正理解这个概念,建议阅读这篇文章 Ethash proof-of-work function )。
- nonce
和 mixHash 一样,用于验证区块是否被真正记录到链上的值。
总结
总体而言,以太坊有四种前缀树:
- 世界状态树包括了从地址到账户状态之间的映射。 世界状态树的根节点哈希值由区块保存(在 stateRoot 字段),它标示了区块创建时的当前状态。整个网络中只有一个世界状态树。
- 账户存储树保存了与某一智能合约相关的数据信息。由账户状态保存账户存储树的根节点哈希值(在 storageRoot 字段)。每个账户都有一个账户存储树。
- 交易树包含了一个区块中的所有交易信息。由区块头(在 transactionsRoot 区域)保存交易树的根节点哈希值。每个区块都有一棵交易树。
- 交易收据树包含了一个区块中所有交易的收据信息。同样由区块头(在 receiptsRoot 区域)保存交易收据树的根节点哈希值;每个区块都有对应的交易收据树。
- 世界状态: 以太坊这台分布式计算机的硬盘。它是从地址到账户状态的映射。
- 账户状态: 保存着每个以太坊账户的状态信息。账户状态同样保存着账户状态树的 storageRoot,后者包含了该账户的存储数据。
- 交易: 标示了系统中的状态转移。它可以是资金的转移、消息调用或是合约的部署。
- 区块: 包括对前序区块(parentHash)的链接,并且保存了当执行时会在系统中产生新状态的交易。区块同时保存了 stateRoot 、transactionRoot 、 receiptsRoot 、 世界状态树的根节点哈希、交易树以及对应的交易收据树。
-区块、交易、账户状态对象以及以太坊的默克尔树-
5. Gas 及其支付
我们可以在以太坊黄皮书的附录 G,找到每一种 EVM 运算对应所需要消耗的 Gas 数量;这些数值看起来很随意,但其实背后是有道理的。一般来讲,这些数值反映了执行运算的成本(按时间维度度量),和占用的永久存储器资源(当写入数据的时候)。
从另一个角度来说,以太坊采取使用者付费的模式,能够避免资源的滥用。一旦你必须为每种运算支付费用,你就会尽可能的将代码写得简洁高效;Gas 的存在还能阻止攻击者通过无效运算,对以太坊网路进行泛洪(Flooding)攻击。(除非攻击者愿意支付一大笔钱来执行无效运算)
gasPrice 和 gasLimit
gasPrice 表示交易发送方对每单位 Gas 愿意支付的价格(以 Wei 计量),这意味着交易发送方可以自定义愿意支付的每单位 Gas 价格。假设一笔交易需要耗费 10 Gas,而我们愿意支付 3 Wei/Gas ,则发送这笔交易的成本总价就是 30 Wei(非实际数值,只是便于大家理解怎么计算的)。
gasLimit表示交易发送方最多能接受多少 Gas 被用于执行此交易。因为有时候,你无法确切知道执行一笔交易要耗费多少 Gas;又或是你的智能合约中,有永远跳不出的死循环 bug,假如没有 gasLimit,这会导致发送方的账户余额被误消耗殆尽。 gasLimit 就是一种安全机制,防止有人因为错误估算或 bug 而把账户中所有以太币消耗掉。
另一个有趣的点是,gasLimit 可以被视为预付的 Gas。当节点在验证交易时,先将 gasPrice 乘 gasLimit 算出交易的固定成本。如果交易发送方的账户余额小于交易固定成本,则该交易视为无效。交易执行完之后,剩余的 Gas 会退回至发送方账户;当然,如果交易执行中 Gas 耗尽,则不会退回任何东西。这也能解释为什么交易发送方总是将 gasLimit 设得高于预估的 Gas 量。
搞清楚这两个参数的意思之后,你可能会想问:“为什么是交易发送方自行决定每单位 Gas 的价格”。如果你跑去最近的加油站告诉收银员,“每升油我就愿意支付 5 分钱”;好一点的收银员可能就一笑而过,而理智的收银员可能会报警。所以想要了解设计机制,你需要知道矿工节点的工作以及手续费是什么。
矿工
区块是包含一组交易集合的数据结构,而以太坊中的矿工节点负责创建链上的区块。创建区块的时候,矿工会从交易缓存池(等待打包的交易堆)中选择交易并开始出块。
在以太坊中,每当矿工成功创建一个区块,** 就能获得定额的出块奖励及引用叔块的奖励(不在此展开),同时还能获得包含在这个区块中的所有交易的手续费;所以交易中的 gasPrice 设置得越高,矿工就能得到越多交易手续费。**
我们假设一个简单的场景。Bob 的账户里有 200 wei,John 的账户里有 100 wei,他俩都想要发送一笔需要耗用 90 Gas 的交易。
Bob 设置 gasLimit = 100,gasPrice = 2;John想将 gasLimit 设为 200,但不幸的是他只有 100 wei,这样设置会使得交易固定成本高于账户余额;所以John 最终设 gasLimit =100, gasPrice =1。
**当进入选择交易打包进块的环节时,矿工倾向选择手续费更高的交易。**在我们的例子中,Bob 的 gasPrice 比 John 的高两倍;因为两笔交易都需要 90 Gas,所以矿工选择 Bob 的交易能获得两倍的手续费奖励。-矿工会选择 gasPrice 最高的交易-
由交易发送方付费来奖励矿工的机制,在以太坊中形成一种能自我调节的经济体系。交易发送方千方百计想要降低交易成本,而矿工总是希望收益最大化,两者形成一种平衡。作为交易发送方,如果你把 gasPrice 设得越高,意味着矿工越有动力打包你的交易,则你的交易能越早被装进区块。
有的矿工甚至会设置自己的 gasPrice 下限,直接忽略那些 gasPrice 小于下限的交易。
当发送交易时,我们很难知道当前有效的最小 gasPrice 是多少。这些工具(https://ethgasstation.info/)能够扫描整个以太坊网络,算出当前其他交易的 gasPrice 均值,帮助发送方选择能被矿工接受的合理 gasPrice。
总结
从本文中,我们学到了就像汽车消耗燃油一样,执行以太坊交易需要消耗 Gas。
我们还探讨了 gasPrice 和 gasLimit 的重要性;如果智能合约出现 bug 或估算错误,gasPrice 能保护使用者避免平白损失以太币。
再者,我们还研究了交易手续费背后的经济机制,以及矿工如何选择交易以达到收益最大化。现在我们知道如何调整 gasPrice ,让自己发出的交易更吸引矿工,从而使得交易被更早打包。
6. 交易执行
交易执行是以太坊协议中最复杂的部分:它定义了状态转换函数 Υ。
交易验证
在执行交易之前,节点会先验证该交易是否满足一些基本(固有)规则。如果连这些基本规则都通过不了,节点就不会执行该交易。
这些交易的固有规则如下:
- 满足 RLP 编码格式
- 具备合法签名
- 具备合法 nonce (与交易发送方的当前 nonce 值相同)
- 执行交易的固有成本(intrinsic cost)小于该交易设置的 gas 上限交易
- 发送方的账户余额大于等于交易所需的预付款
还有一条规则,它不属于交易固有规则——如果一系列已准备好打包到区块中的交易,加上这条交易之后,会使得所有交易的总 Gas Limit 超过区块的 Gas 上限,那么该笔交易就不能和那些交易一起打包到一个区块中。
// 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
}
交易必须符合合规的 RLP 编码
这条规则可能最好直观理解。RLP (Recursive Length Prefix,又称为递归长度前缀编码)是一种用于序列化以太坊中的对象的编码方法;和其他方法相同,如果你不按照 RLP 对物件编码,则无法对该物件进行解码,你也就无法通过数据编码得到原始对象的信息。
该规则的目的是确保以太坊客户端收到交易后,能够成功解码并执行。
交易必须具备合法签名
以太坊采用非对称加密,确保只有实际控制者能够从账户发起交易。与此同时,这种密码学工具还能让其他人验证该交易的确是由账户的实际控制者发起。
当发送一笔交易时,私钥被用来签署交易(还记得 v 、r 、s 这几个包含在交易里的值吗?),接着所有节点就能确定这笔交易是不是真的由关联账户的私钥所有者签署的。
不具备合法签名的交易没有任何执行的意义,因此必须有合法签名就成了交易的固有规则之一。
交易 nonce 和账户 nonce 必须匹配
在以太坊中,**账户 nonce 值代表该账户发送的交易数量(如果是合约账户,则 nonce 值指的是账户所创建的合约数量)。如果没有 nonce ,同一笔交易可能被错误地执行多次(也就是所谓的 “重放攻击”)。**考虑到以太坊的分布式特性,不同的节点可能会试图把同一笔交易打包进不同的区块,将重复的交易上链。假设一笔你把钱转给某人的交易被误打包了两次,导致你重复转了两次钱,你心里一定很不是滋味。
每当用户创建一笔新的交易,他们必须设置能匹配当前账户 nonce 值的交易 nonce 值,当执行交易时,节点会检查交易 nonce 是否匹配账户 nonce 。
如果因为某些原因,导致同一笔交易被重复提交给节点,此时,因为账户 nonce 值已经增加,所以重复提交的交易会被视为不合法。
以太坊强制要求交易 nonce 值与账户 nonce 值匹配,这么做除了能避免重放攻击,还能确保一笔交易只会执行及改变状态一次。
交易的固有成本必须小于该交易设置的 gas 上限
每一笔交易都有与之关联的 gas ——发送一笔交易的成本包含两部分:固有成本和执行成本。
执行成本根据该交易需要使用多少以太坊虚拟机(EVM)的资源来运算而定,执行一笔交易所需的操作越多,则它的执行成本就越高。
固有成本由交易的负载( payload )决定,交易负载分为以下三种负载:
- 如果该交易是为了创建智能合约,则负载就是创建智能合约的 EVM 代码
- 如果该交易是为了调用智能合约的函数,则负载就是执行消息的输入数据
- 如果该交易只是单纯在两个账户间转账,则负载为空
假设 Nzeros 代表交易负载中,字节为 0 的字节总数;Nnonzeros 代表交易负载中,字节不为 0 的字节总数。可以通过下列公式计算出该交易的固有成本(黄皮书 6.2 章,方程式 54、55 和 56):
在黄皮书的附录 G 中,可以看到一份创建和执行交易的相关成本的费用表。与固有成本相关的内容如下:
Gtransaction = 21,000 Wei
Gtxcreate = 32,000 Wei
Gtxdatazero = 4 Wei
Gtxdatanonzero = 68 Wei (在伊斯坦布尔升级时会改为 16 wei)
当我们了解固有成本是什么,就能理解为什么一旦交易的固有成本高于 Gas 限制,则该交易就会被视为非法。Gas Limit 规定了一笔交易在执行时,能够消耗掉的 Gas 上限;如果还没开始执行该交易前,我们就知道它的固有成本高于 Gas 上限,那我们就没有理由执行这笔交易。
交易发送方的账户余额必须大于等于交易所需的预付款
交易预付款指的是在交易执行前,从交易发送方账户,预先扣除的 Gas 数量。
我们可以通过下面的公式算出交易预付款:
预付款 = gasLimit * gasPrice + value
一笔交易的 Gas Limit,指的是交易发送方愿意花在执行该交易上的 Gas 最大值;Gas Price 指的是每一单位 Gas 的单价;交易 Value 指的是发给消息接收者的 Wei 的数量(例如转账金额),或是投入要创建的合约中的准备金。如果要进一步了解什么是 Gas ,以及为什么执行交易要耗费 Gas,可以看我们前一篇博文。
因为交易预付款在交易执行前就会先扣除,所以一旦交易发送方的账户余额少于预扣额,这笔交易就没有执行的必要了。
交易的 Gas Limit 必须小于等于区块的 Gas 上限
这条规则不属于固有规则,不过这是节点在选择要打包的交易时,需要遵守的基本要求。区块 Gas 上限是能够 “装在” 该区块中的交易所用总 Gas 数的上限。
当节点在选择要打包的交易时,节点必须确保加入这笔交易后,区块里的交易所用总 Gas 数不会超过区块 Gas 上限。对于要被打包的交易来说,其 Gas Limit 加上其他交易的 Gas Limit 总和,必须小于等于区块 Gas 上限。当然,如果有一笔交易不能被打包进入当前区块,它还是有机会被后面的区块打包的。
7. 执行交易
以太坊黄皮书详解(二)
参考URL: https://www.jianshu.com/p/705836c75b87
交易执行是以太坊中最为重要的部分。
在执行交易之前首先需要对交易进行初步校验:
交易是RLP格式的,无多余字符
交易的签名是有效的
交易的nonce是有效的(与发送者账户的nonce值一致)
gasLimit的值不小于固有gas
账户余额至少够支付预付费用
当交易满足上述条件后,交易才会被执行。
// preCheck校验的后三条。
//交易校验的前两条是在其他地方执行的。对于矿工来说交易签名在加txpool的时候会检查,在commitTransactions的时候也会检查。
func (st *StateTransition) preCheck() error {
// Make sure this transaction's nonce is correct.
// 检查nonce值
if st.msg.CheckNonce() {
nonce := st.state.GetNonce(st.msg.From())
if nonce < st.msg.Nonce() {
return ErrNonceTooHigh
} else if nonce > st.msg.Nonce() {
return ErrNonceTooLow
}
}
return st.buyGas()
}
func (st *StateTransition) buyGas() error {
//gasLimit*gasPrice即为预付的费用v0
mgval := new(big.Int).Mul(new(big.Int).SetUint64(st.msg.Gas()), st.gasPrice)
if st.state.GetBalance(st.msg.From()).Cmp(mgval) < 0 {
return errInsufficientBalanceForGas
}
if err := st.gp.SubGas(st.msg.Gas()); err != nil {
return err
}
st.gas += st.msg.Gas()
//initialGas
st.initialGas = st.msg.Gas()
st.state.SubBalance(st.msg.From(), mgval)
return nil
}
执行
- 对交易进行初步检查,从发送者账户中扣除预付的交易费。预付交易费值如公式57所示,为gasLimitgasPrice + value。(代码中是gasLimitgasPrice,Value是在Call的过程中判断和扣除的)
- 计算固有gas消耗,如公式54-56所示。并消耗掉该花费。
- 如果是创建合约,则走合约创建流程。消耗相应花费。
- 如果是合约执行,则走合约执行流程。消耗相应花费。
- 计算退款余额,将余额退还到发送者账户。
- 将交易的交易费加到矿工账户。
返回当前状态,以及交易的花费。
/ The State Transitioning Model A state transition is a change made when a transaction is applied to the current world state The state transitioning model does all all the necessary work to work out a valid new state root. 1) Nonce handling 2) Pre pay gas 3) Create a new state object if the recipient is \032 4) Value transfer == If contract creation == 4a) Attempt to run transaction data 4b) If valid, use result as code for the new state object == end == 5) Run Script section 6) Derive new state root */
//gp 中一开始有gasLimit数量的gas
type StateTransition struct {gp *GasPool
msg Message
gas uint64
gasPrice *big.Int
initialGas uint64
value *big.Int
data []byte
state vm.StateDB
evm *vm.EVM
}
// TransitionDb will transition the state by applying the current message and
// returning the result including the the used gas. It returns an error if it
// failed. An error indicates a consensus issue.
func (st *StateTransition) TransitionDb() (ret []byte, usedGas uint64, failed bool, err error) {//交易检查,检查正确的话,st.gas为gasPrice*gasLimit,即预付的交易费。
if err = st.preCheck(); err != nil {
return
}
msg := st.msg
sender := vm.AccountRef(msg.From())
homestead := st.evm.ChainConfig().IsHomestead(st.evm.BlockNumber)
contractCreation := msg.To() == nil
// Pay intrinsic gas
// 固有gas,也就是g0
gas, err := IntrinsicGas(st.data, contractCreation, homestead)
if err != nil {
return nil, 0, false, err
}
if err = st.useGas(gas); err != nil {
return nil, 0, false, err
}
var (
evm = st.evm
// vm errors do not effect consensus and are therefor
// not assigned to err, except for insufficient balance
// error.
vmerr error
)
//创建合约
if contractCreation {
ret, _, st.gas, vmerr = evm.Create(sender, st.data, st.gas, st.value)
} else {
// Increment the nonce for the next transaction
//执行合约
st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1)
ret, st.gas, vmerr = evm.Call(sender, st.to(), st.data, st.gas, st.value)
}
if vmerr != nil {
log.Debug("VM returned with error", "err", vmerr)
// The only possible consensus-error would be if there wasn't
// sufficient balance to make the transfer happen. The first
// balance transfer may never fail.
if vmerr == vm.ErrInsufficientBalance {
return nil, 0, false, vmerr
}
}
//计算退款,并返回到发送者账户
st.refundGas()
//付交易费给矿工
st.state.AddBalance(st.evm.Coinbase, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.gasPrice))
return ret, st.gasUsed(), vmerr != nil, err
}
8. 合约创建
以太坊中有两类账户,一类为外部拥有账户,即通常意义上的用户账户。一类为合约账户。当一个交易是合约创建,是指该交易的目的是创建一个新的合约账户。合约账户创建的过程如下:
- 根据规则生成合约账户地址。公式77.
- 设置合约账户nonce值为1,其balance设为捐献值,storageRoot设为空,codeHash为空的Hash。公式78-79.
- 当前账户余额中减去捐献值。公式80-81.
- 运行合约,进行合约的初始化工作。如果运行过程中gas不足,则所有状态回滚,消耗掉所有gas。也就是被创建的合约账户也会被回滚掉,捐献值回滚到原账户。
- 如果合约初始化运行成功,则计算存储code的花费,若成功,则设置账户的code。
如果不足以支付存储费用,回滚状态。(这个地方根据配置不同,homestead 和 byzantium会有所不同)
// Create creates a new contract using code as deployment code.
func (evm EVM) Create(caller ContractRef, code []byte, gas uint64, value big.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) {// Depth check execution. Fail if we're trying to execute above the
// limit.
if evm.depth > int(params.CallCreateDepth) {
return nil, common.Address{ }, gas, ErrDepth
}
if !evm.CanTransfer(evm.StateDB, caller.Address(), value) {
return nil, common.Address{ }, gas, ErrInsufficientBalance
}
// Ensure there's no existing contract already at the designated address
nonce := evm.StateDB.GetNonce(caller.Address())
evm.StateDB.SetNonce(caller.Address(), nonce+1)
//生成合约账户地址
contractAddr = crypto.CreateAddress(caller.Address(), nonce)
//若之前该合约账户对应的地址不空,则返回地址冲突的错误
contractHash := evm.StateDB.GetCodeHash(contractAddr)
if evm.StateDB.GetNonce(contractAddr) != 0 || (contractHash != (common.Hash{ }) && contractHash != emptyCodeHash) {
return nil, common.Address{ }, 0, ErrContractAddressCollision
}
// Create a new account on the state
snapshot := evm.StateDB.Snapshot()
//创建合约账户
evm.StateDB.CreateAccount(contractAddr)
if evm.ChainConfig().IsEIP158(evm.BlockNumber) {
evm.StateDB.SetNonce(contractAddr, 1)
}
//将捐献值转移给合约账户
evm.Transfer(evm.StateDB, caller.Address(), contractAddr, value)
// initialise a new contract and set the code that is to be used by the
// EVM. The contract is a scoped environment for this execution context
// only.
contract := NewContract(caller, AccountRef(contractAddr), value, gas)
contract.SetCallCode(&contractAddr, crypto.Keccak256Hash(code), code)
if evm.vmConfig.NoRecursion && evm.depth > 0 {
return nil, contractAddr, gas, nil
}
if evm.vmConfig.Debug && evm.depth == 0 {
evm.vmConfig.Tracer.CaptureStart(caller.Address(), contractAddr, true, code, gas, value)
}
start := time.Now()
//运行合约,进行合约的初始化,错误交由最后处理
ret, err = run(evm, contract, nil)
// check whether the max code size has been exceeded
maxCodeSizeExceeded := evm.ChainConfig().IsEIP158(evm.BlockNumber) && len(ret) > params.MaxCodeSize
// if the contract creation ran successfully and no errors were returned
// calculate the gas required to store the code. If the code could not
// be stored due to not enough gas set an error and let it be handled
// by the error checking condition below.
//如果合约创建成功,无错误返回,则计算合约存储代码的花费。成功的话,设置合约账户的code。如果存储时gas不足,err交由下面处理。
if err == nil && !maxCodeSizeExceeded {
createDataGas := uint64(len(ret)) * params.CreateDataGas
if contract.UseGas(createDataGas) {
evm.StateDB.SetCode(contractAddr, ret)
} else {
err = ErrCodeStoreOutOfGas
}
}
// When an error was returned by the EVM or when setting the creation code
// above we revert to the snapshot and consume any gas remaining. Additionally
// when we're in homestead this also counts for code storage gas errors.
// 如果不是ErrCodeStoreOutOfGas的话,revert当前状态,消耗gas。说明ErrCodeStoreOutOfGas账户会创建成功。
if maxCodeSizeExceeded || (err != nil && (evm.ChainConfig().IsHomestead(evm.BlockNumber) || err != ErrCodeStoreOutOfGas)) {
evm.StateDB.RevertToSnapshot(snapshot)
if err != errExecutionReverted {
contract.UseGas(contract.Gas)
}
}
// Assign err if contract code size exceeds the max while the err is still empty.
if maxCodeSizeExceeded && err == nil {
err = errMaxCodeSizeExceeded
}
if evm.vmConfig.Debug && evm.depth == 0 {
evm.vmConfig.Tracer.CaptureEnd(ret, gas-contract.Gas, time.Since(start), err)
}
return ret, contractAddr, contract.Gas, err
}
9. 合约调用
[强烈推荐-将公式和代码联系起来了]以太坊黄皮书详解(二)
参考URL: https://www.jianshu.com/p/705836c75b87
合约调用的流程如下:
- 如果to账户地址不存在,则新建.
- 从sender中转账value值到to账户.
- 从合约账户中获取合约代码,进行设置,供虚拟机执行.
- 虚拟机执行合约代码。
如果合约执行出错,则回滚到合约执行之前的状态。
// Call executes the contract associated with the addr with the given input as
// parameters. It also handles any necessary value transfer required and takes
// the necessary steps to create accounts and reverses the state in case of an
// execution error or failed value transfer.
func (evm EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value big.Int) (ret []byte, leftOverGas uint64, err error) {if evm.vmConfig.NoRecursion && evm.depth > 0 {
return nil, gas, nil
}
// Fail if we're trying to execute above the call depth limit
if evm.depth > int(params.CallCreateDepth) {
return nil, gas, ErrDepth
}
// Fail if we're trying to transfer more than the available balance
if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) {
return nil, gas, ErrInsufficientBalance
}
var (
to = AccountRef(addr)
snapshot = evm.StateDB.Snapshot()
)
//to账户不存在,则新建
if !evm.StateDB.Exist(addr) {
precompiles := PrecompiledContractsHomestead
if evm.ChainConfig().IsByzantium(evm.BlockNumber) {
precompiles = PrecompiledContractsByzantium
}
if precompiles[addr] == nil && evm.ChainConfig().IsEIP158(evm.BlockNumber) && value.Sign() == 0 {
// Calling a non existing account, don't do antything, but ping the tracer
if evm.vmConfig.Debug && evm.depth == 0 {
evm.vmConfig.Tracer.CaptureStart(caller.Address(), addr, false, input, gas, value)
evm.vmConfig.Tracer.CaptureEnd(ret, 0, 0, nil)
}
return nil, gas, nil
}
evm.StateDB.CreateAccount(addr)
}
//转账
evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value)
// Initialise a new contract and set the code that is to be used by the EVM.
// The contract is a scoped environment for this execution context only.
//设置要执行的代码
contract := NewContract(caller, to, value, gas)
contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))
start := time.Now()
// Capture the tracer start/end events in debug mode
if evm.vmConfig.Debug && evm.depth == 0 {
evm.vmConfig.Tracer.CaptureStart(caller.Address(), addr, false, input, gas, value)
defer func() { // Lazy evaluation of the parameters
evm.vmConfig.Tracer.CaptureEnd(ret, gas-contract.Gas, time.Since(start), err)
}()
}
//执行代码
ret, err = run(evm, contract, input)
// When an error was returned by the EVM or when setting the creation code
// above we revert to the snapshot and consume any gas remaining. Additionally
// when we're in homestead this also counts for code storage gas errors.
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
if err != errExecutionReverted {
contract.UseGas(contract.Gas)
}
}
return ret, contract.Gas, err
}
10. 虚拟机的执行模型
以太坊虚拟机EVM是图灵完备虚拟机器。EVM存在而典型图灵完备机器不存在的唯一限制就是EVM本质上是被gas束缚。
因此,可以完成的计算总量本质上是被提供的gas总量限制的。
- EVM是基于栈(先进后出)的架构。EVM中每个堆栈项的大小为256位,堆栈有一个最大的大小,为1024位。
- EVM有内存,项目按照可寻址字节数组来存储。内存是易失性的,也就是数据是不持久的。
- EVM也有一个存储器。不像内存,存储器是非易失性的,并作为系统状态的一部分进行维护。EVM分开保存程序代码,在虚拟ROM 中只能通过特殊指令来访问。
EVM同样有属于它自己的语言:“EVM字节码”,在以太坊上运行的智能合约时,通常都是用高级语言例如Solidity来编写代码。然后将它编译成EVM可以理解的EVM字节码。
// EVM is the Ethereum Virtual Machine base object and provides
// the necessary tools to run a contract on the given state with
// the provided context. It should be noted that any error
// generated through any of the calls should be considered a
// revert-state-and-consume-all-gas operation, no checks on
// specific errors should ever be performed. The interpreter makes
// sure that any errors generated are to be considered faulty code.
//
// The EVM should never be reused and is not thread safe.
type EVM struct {// Context provides auxiliary blockchain related information
Context
// StateDB gives access to the underlying state
StateDB StateDB
// Depth is the current call stack
depth int
// chainConfig contains information about the current chain
chainConfig *params.ChainConfig
// chain rules contains the chain rules for the current epoch
chainRules params.Rules
// virtual machine configuration options used to initialise the
// evm.
vmConfig Config
// global (to this context) ethereum virtual machine
// used throughout the execution of the tx.
interpreter *Interpreter
// abort is used to abort the EVM calling operations
// NOTE: must be set atomically
abort int32
// callGasTemp holds the gas available for the current call. This is needed because the
// available gas is calculated in gasCall* according to the 63/64 rule and later
// applied in opCall*.
callGasTemp uint64
}
// Interpreter is used to run Ethereum based contracts and will utilise the
// passed environment to query external sources for state information.
// The Interpreter will run the byte code VM based on the passed
// configuration.
type Interpreter struct {evm *EVM
cfg Config
gasTable params.GasTable
intPool *intPool //栈
readOnly bool // Whether to throw on stateful modifications
returnData []byte // Last CALL's return data for subsequent reuse
}
费用
以太坊虚拟机执行过程中,主要有3类费用。
- 执行过程中的运算费用。
- 创建或者调用其他合约消耗的费用。
- 新增的存储的费用。
执行过程
- 执行刚开始时,内存和堆栈都是空的,程序计数器为0。
- 然后EVM开始递归的执行交易,为每个循环计算系统状态和机器状态。系统状态也就是以太坊的全局状态(global state)。机器状态包含:可获取的gas,程序计数器,内存的内容,内存中字的活跃数,堆栈的内容。
- 堆栈中的项从栈顶被删除(POP)或者添加(PUSH)。
- 每个循环,剩余的gas都会被减少相应的量,程序计数器也会增加。
在每个循环的结束,都有四种可能性:
- 机器到达异常状态(err != nil 的情况。例如 gas不足,无效指令,堆栈项不足,堆栈项会溢出1024,无效的JUMP/JUMPI目的地等等)因此停止,并丢弃任何的更改进入后续处理下一个循环。
- 机器到达了受控停止(到达执行过程的终点,halts或者revert),整个结束了,机器就会产生一个合成状态,执行之后的剩余gas、产生的子状态、以及组合输出。
假设执行没有遇到异常状态,继续循环执行下一步。
// Run loops and evaluates the contract’s code with the given input data and returns
// the return byte-slice and an error if one occurred.
//
// It’s important to note that any errors returned by the interpreter should be
// considered a revert-and-consume-all-gas operation except for
// errExecutionReverted which means revert-and-keep-gas-left.
func (in Interpreter) Run(contract Contract, input []byte) (ret []byte, err error) {// Increment the call depth which is restricted to 1024
in.evm.depth++
defer func() { in.evm.depth-- }()
// Reset the previous call's return data. It's unimportant to preserve the old buffer
// as every returning call will return new data anyway.
in.returnData = nil
// Don't bother with the execution if there's no code.
if len(contract.Code) == 0 {
return nil, nil
}
//机器状态包含:可获取的gas,程序计数器pc,内存的内容mem,内存中字的活跃数,堆栈的内容stack。
var (
op OpCode // current opcode
mem = NewMemory() // bound memory
stack = newstack() // local stack
// For optimisation reason we're using uint64 as the program counter.
// It's theoretically possible to go above 2^64. The YP defines the PC
// to be uint256. Practically much less so feasible.
pc = uint64(0) // program counter
cost uint64
// copies used by tracer
pcCopy uint64 // needed for the deferred Tracer
gasCopy uint64 // for Tracer to log gas remaining before execution
logged bool // deferred Tracer should ignore already logged steps
)
contract.Input = input
if in.cfg.Debug {
defer func() {
if err != nil {
if !logged {
in.cfg.Tracer.CaptureState(in.evm, pcCopy, op, gasCopy, cost, mem, stack, contract, in.evm.depth, err)
} else {
in.cfg.Tracer.CaptureFault(in.evm, pcCopy, op, gasCopy, cost, mem, stack, contract, in.evm.depth, err)
}
}
}()
}
// 循环直到三种结束状态中的一种。
// 1.异常情况,各种不合法的情况出现时,或者执行出错,会直接return
// 2.执行中返回REVERT错误。
// 3.执行中中断。
// The Interpreter main run loop (contextual). This loop runs until either an
// explicit STOP, RETURN or SELFDESTRUCT is executed, an error occurred during
// the execution of one of the operations or until the done flag is set by the
// parent context.
for atomic.LoadInt32(&in.evm.abort) == 0 {
if in.cfg.Debug {
// Capture pre-execution values for tracing.
logged, pcCopy, gasCopy = false, pc, contract.Gas
}
// Get the operation from the jump table and validate the stack to ensure there are
// enough stack items available to perform the operation.
// 获取当前要执行的操作
op = contract.GetOp(pc)
// 确认操作是否在操作集合表中
operation := in.cfg.JumpTable[op]
if !operation.valid {
return nil, fmt.Errorf("invalid opcode 0x%x", int(op))
}
// 确保目前栈满足操作要求,比方说有些操作是二元的,那么栈中至少要有两个数据。
if err := operation.validateStack(stack); err != nil {
return nil, err
}
// If the operation is valid, enforce and write restrictions
if err := in.enforceRestrictions(op, operation, stack); err != nil {
return nil, err
}
var memorySize uint64
// calculate the new memory size and expand the memory to fit
// the operation
// 计算新操作需要的内存空间
if operation.memorySize != nil {
memSize, overflow := bigUint64(operation.memorySize(stack))
if overflow {
return nil, errGasUintOverflow
}
// memory is expanded in words of 32 bytes. Gas
// is also calculated in words.
if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow {
return nil, errGasUintOverflow
}
}
// consume the gas and return an error if not enough gas is available.
// cost is explicitly set so that the capture state defer method can get the proper cost
// 消耗gas
cost, err = operation.gasCost(in.gasTable, in.evm, contract, stack, mem, memorySize)
if err != nil || !contract.UseGas(cost) {
return nil, ErrOutOfGas
}
if memorySize > 0 {
mem.Resize(memorySize)
}
if in.cfg.Debug {
in.cfg.Tracer.CaptureState(in.evm, pc, op, gasCopy, cost, mem, stack, contract, in.evm.depth, err)
logged = true
}
// execute the operation
// 执行具体的操作,比方说加法,就是从栈中取出两个值后相加
res, err := operation.execute(&pc, in.evm, contract, mem, stack)
// verifyPool is a build flag. Pool verification makes sure the integrity
// of the integer pool by comparing values to a default value.
if verifyPool {
verifyIntegerPool(in.intPool)
}
// if the operation clears the return data (e.g. it has returning data)
// set the last return to the result of the operation.
if operation.returns {
in.returnData = res
}
switch {
case err != nil:
return nil, err
case operation.reverts:
return res, errExecutionReverted
case operation.halts:
return res, nil
case !operation.jumps:
pc++ //计数器加一,接着执行
}
}
return nil, nil
}
11. 区块的最终确定
在区块的生成过程中,更多的时候是一棵树状的结构。为了在树形结构中确认一条链作为区块链,在以太坊中定义“最重”的链为主链。
这里的重指的是累计难度(td,total difficulty定义如公式153-154),也就是td最大的链为主链。
区块的最终确定涉及到四个阶段:
- 校验ommers。
- 校验交易。
- 计算和提交奖励。
- 验证世界状态和区块的nonce值。
Ommer的校验
Ommer或者说uncle是指父区块的兄弟区块,以太坊中block的uncles字段用以存储uncle区块,在区块的最终确定的时候需要对ommer进行校验。
// VerifyUncles verifies that the given block's uncles conform to the consensus
// rules of the stock Ethereum ethash engine.
func (ethash *Ethash) VerifyUncles(chain consensus.ChainReader, block *types.Block) error {
// If we're running a full engine faking, accept any input as valid
if ethash.config.PowMode == ModeFullFake {
return nil
}
// 每个区块最多有两个uncle区块
// Verify that there are at most 2 uncles included in this block
if len(block.Uncles()) > maxUncles {
return errTooManyUncles
}
// Gather the set of past uncles and ancestors
uncles, ancestors := set.New(), make(map[common.Hash]*types.Header)
// 统计七代以内的区块
number, parent := block.NumberU64()-1, block.ParentHash()
for i := 0; i < 7; i++ {
ancestor := chain.GetBlock(parent, number)
if ancestor == nil {
break
}
ancestors[ancestor.Hash()] = ancestor.Header()
for _, uncle := range ancestor.Uncles() {
uncles.Add(uncle.Hash())
}
parent, number = ancestor.ParentHash(), number-1
}
ancestors[block.Hash()] = block.Header()
uncles.Add(block.Hash())
// 确认该节点
// Verify each of the uncles that it's recent, but not an ancestor
for _, uncle := range block.Uncles() {
// Make sure every uncle is rewarded only once
hash := uncle.Hash()
if uncles.Has(hash) {
return errDuplicateUncle
}
uncles.Add(hash)
// Make sure the uncle has a valid ancestry
// 确认uncle区块是确实是uncle区块
if ancestors[hash] != nil {
return errUncleIsAncestor
}
if ancestors[uncle.ParentHash] == nil || uncle.ParentHash == block.ParentHash() {
return errDanglingUncle
}
// 验证uncle区块的header
if err := ethash.verifyHeader(chain, uncle, ancestors[uncle.ParentHash], true, true); err != nil {
return err
}
}
return nil
}
校验交易
- 校验block中的txHash是否为block.Transaction的hash。见本章开始部分代码中ValidateBody的最后部分。
- 虚拟机逐条执行交易,执行后校验所用的gas值是否和区块的已用gas值相同,见公式158.
- 校验虚拟机执行后的bloom以及receipt是否与区块头中的bloom以及receiptHash相同。
校验虚拟机执行之后的世界状态的树的根是否与区块头中的Root相同。
// ValidateState validates the various changes that happen after a state
// transition, such as amount of used gas, the receipt roots and the state root
// itself. ValidateState returns a database batch if the validation was a success
// otherwise nil and an error is returned.
func (v BlockValidator) ValidateState(block, parent types.Block, statedb *state.StateDB, receipts types.Receipts, usedGas uint64) error {header := block.Header()
// 校验交易的usedGas是否为区块的GasUsed
if block.GasUsed() != usedGas {
return fmt.Errorf("invalid gas used (remote: %d local: %d)", block.GasUsed(), usedGas)
}
// Validate the received block's bloom with the one derived from the generated receipts.
// For valid blocks this should always validate to true.
rbloom := types.CreateBloom(receipts)
if rbloom != header.Bloom {
return fmt.Errorf("invalid bloom (remote: %x local: %x)", header.Bloom, rbloom)
}
// 校验收款的hash值
// Tre receipt Trie's root (R = (Tr [[H1, R1], ... [Hn, R1]]))
receiptSha := types.DeriveSha(receipts)
if receiptSha != header.ReceiptHash {
return fmt.Errorf("invalid receipt root hash (remote: %x local: %x)", header.ReceiptHash, receiptSha)
}
// 校验state
// Validate the state root against the received state root and throw
// an error if they don't match.
if root := statedb.IntermediateRoot(v.config.IsEIP158(header.Number)); header.Root != root {
return fmt.Errorf("invalid merkle root (remote: %x local: %x)", header.Root, root)
}
return nil
}
计算奖励
- 矿工的奖励为固定奖励如公式164,如果区块uncles不为空,则每个uncle会带来额外32分之一的奖励。如公式160,矿工的余额为原余额加上奖励。
对于该区块中的uncles,每个uncle块,计算其矿工的奖励,如公式163,是根据uncle块与当前块的代差来计算的。例如差1代,uncle块为当前块父区块的兄弟,那么该uncle块的矿工可以拿到八分之七的奖励。差的代数越多,拿到的奖励越少。
// AccumulateRewards credits the coinbase of the given block with the mining
// reward. The total reward consists of the static block reward and rewards for
// included uncles. The coinbase of each uncle block is also rewarded.
func accumulateRewards(config params.ChainConfig, state state.StateDB, header types.Header, uncles []types.Header) {// Select the correct block reward based on chain progression
blockReward := FrontierBlockReward
if config.IsByzantium(header.Number) {
blockReward = ByzantiumBlockReward
}
// Accumulate the rewards for the miner and any included uncles
reward := new(big.Int).Set(blockReward)
// 计算uncle块中矿工的奖励
r := new(big.Int)
for _, uncle := range uncles {
r.Add(uncle.Number, big8)
r.Sub(r, header.Number)
r.Mul(r, blockReward)
r.Div(r, big8)
state.AddBalance(uncle.Coinbase, r)
r.Div(blockReward, big32)
reward.Add(reward, r)
}
// 当前矿工的奖励为固定奖励加上跟uncle区块数量相关的一部分奖励
state.AddBalance(header.Coinbase, reward)
}
校验state和nonce
- 区块执行前的世界状态定义为,如公式165,即执行Process之前的世界状态。
- 执行即对区块的每条交易,运行交易转换函数(虚拟机执行,ApplyTrasaction)如公式170,每条交易执行完后,走区块的转换函数(Finalize,增加矿工奖励等),如公式169和174所示。
- 执行交易过程中会产生交易的收据证明,相关内容如公式171-173所示。
执行后的区块与最终区块的主要区别为区块头中的nonce值,mixHash值,如公式166-168所示。
// Process processes the state changes according to the Ethereum rules by running
// the transaction messages using the statedb and applying any rewards to both
// the processor (coinbase) and any included uncles.
//
// Process returns the receipts and logs accumulated during the process and
// returns the amount of gas that was used in the process. If any of the
// transactions failed to execute due to insufficient gas it will return an error.
func (p StateProcessor) Process(block types.Block, statedb state.StateDB, cfg vm.Config) (types.Receipts, []types.Log, uint64, error) {var (
receipts types.Receipts
usedGas = new(uint64)
header = block.Header()
allLogs []*types.Log
gp = new(GasPool).AddGas(block.GasLimit())
)
// Mutate the the block and state according to any hard-fork specs
if p.config.DAOForkSupport && p.config.DAOForkBlock != nil && p.config.DAOForkBlock.Cmp(block.Number()) == 0 {
misc.ApplyDAOHardFork(statedb)
}
// Iterate over and process the individual transactions
for i, tx := range block.Transactions() {
// 虚拟机执行每条交易
statedb.Prepare(tx.Hash(), block.Hash(), i)
receipt, _, err := ApplyTransaction(p.config, p.bc, nil, gp, statedb, header, tx, usedGas, cfg)
if err != nil {
return nil, nil, 0, err
}
// 得到交易的收据与日志
receipts = append(receipts, receipt)
allLogs = append(allLogs, receipt.Logs...)
}
// Finalize the block, applying any consensus engine specific extras (e.g. block rewards)
p.engine.Finalize(p.bc, header, statedb, block.Transactions(), block.Uncles(), receipts)
return receipts, allLogs, *usedGas, nil
}
三、参考
「区块链人物志」工匠Gavin Wood
参考URL: https://baijiahao.baidu.com/s?id=1667898472709976585&wfr=spider&for=pc
[强烈推荐-将公式和代码联系起来了]以太坊黄皮书详解(二)
参考URL: https://www.jianshu.com/p/705836c75b87
还没有评论,来说两句吧...