系统学习EVM的设计原理。
在阅读这篇文章之前,请您先阅读初步理解以太坊虚拟机和以太坊的数据组织,它将会介绍 EVM 的基本知识,帮助您形成基本的认识。在开始之前,假设您已经掌握了上文中的基础,我们根据黄皮书进一步地补充理论基础。由于原始的黄皮书公式过多,不易阅读,可以参考按照论文重写后的版本。其次,本文使用的图片来自其他资料,会在参考资料部分注明。
非常推荐读者观看这个视频:EVM: From Solidity to byte code, memory and storage ,这是配套的 PDF。
它梳理源码到字节码的流程,演示操作码的变化,非常棒。如果读者通过前面提到的文章以及理解 EVM 的存储空间布局的话,这个视频可以为您提供字节码编写合约的基础:smirk_cat:.请善用 Remix IDE 的单步调试功能,可以通过实操,大大加深理解。
以太坊可以抽象的分成两部分,一部分是状态,另外一部分是用于改变状态的 EVM。因此,以太坊在整体上可以看作一个基于交易的状态机:起始于一个创世区块(Genesis)状态,然后随着交易的执行,状态逐步改变。
关于不可篡改性,黄皮书的重写版的表述不错:
Ethereum programs can be trusted to execute without any interference from external non-network forces.
Rather than storing program code in generally-accessible memory or storage, it is stored separately in a virtual ROM interactable only through specialized instructions
不允许外部的任何干扰,特殊的读取程序的方式。
以太坊发行自身的货币,用于衡量计算消耗,它不仅是作为金融工具,更是要作为世界计算机,为所有的应用服务。所有的交易在机器层面都是以 wei 作为单位。
世界状态可以被视作以太坊地址到账户状态的映射。存储时,地址和值经过 RLP 编码,以键值对的形式,通过 MPT 的组织方式存储在数据库中。这个数据库被称作状态数据库。
前辈们的分析非常精湛,建议仔细阅读我们整合、修正过的 MPT 树.
我们在以太坊的数据组织中介绍了 RLP 的编码规则,适合对数据组织的方式形成基本的认识。
前面提到了状态是以键值对的方式存储,账户状态也是以键值对存储,主要包括:nonce、balance、storage root(256 位的的账户数的 MPT 树根)、code hash(字节码的哈希,当账户接收到消息后改变)
我们尝试解读过,但是由于缺乏工程经验,对于并发和调度不熟悉,因此只是半成品,但是也有一定的参考意义。可见这篇文章。
请阅读这篇笔记。
收据可以用于索引、零知识证明等方面,它是交易执行中某些信息的编码。详细内容请阅读博客——理解收据。
交易是以太坊账户之间通信的最基本的方式,可以视作是签名后发送给 EVM 的执行指令。每一笔交易都会造成以太坊状态的改变以及产生临时存储的状态。
交易的组成如下
请注意,这是最开始的设计思路,后面经过诸多的 EIP 后,有些改变,更详细的内容可见 理解交易。
交易费的收取可以分成三部分,第一部分是最普遍的基础费用和指令消耗。第二部分是用于交易中的子消息调用或者创建合约。第三类是内存拓展的开销。当交易执行时,需要多少内存并不是预定的,而是根据操作的需要,拓展 32 字节一组的 slot。存储是抱着能省就省的目的,因此清除存储中的某一项内容,不但不消耗交易费,反而会退回一部分手续费。
交易的执行是以太坊协议中最复杂的部分。首先任意交易在执行之前必须通过初始的有效性测试。包括:
交易是 RLP 格式数据,没有多余的后缀字节;
交易的签名是有效的;
交易的 nonce 是有效的(等于发送者账户当前的 nonce);
gas 上限不小于交易所要使用的 gas;
发送者账户的 balance 应该不少于实际费用,且需要提前支付。
交易的执行过程中会累积产生一些特定的信 息,我们称为交易子状态,它包括四部分:
在交易执行后,确定最终的状态前,会处于一个临时状态,在这个状态中,操作码会逐步执行。这个状态包括:
如果交易失败,这些状态将会重置为空,因此达到了回滚的目的,执行失败的交易不影响世界状态。因此,为了改变世界状态,交易要么完整的执行完毕,要么毫无作用。
前面提到的子状态中包括日志,日志中有比较特殊的一项,叫做交易的收据,用于记录交易的执行结果。日志的集合与包含事件的布隆过滤器,都存储在收据中。交易执行后的状态码和使用的 gas 也在收据中。
为了方便读者理解,下面是 geth 中的定义:
type Receipt struct {
// Consensus fields: These fields are defined by the Yellow Paper
Type uint8 `json:"type,omitempty"` //交易类型
PostState []byte `json:"root"` //交易成功/失败时的 RLP 编码
Status uint64 `json:"status"` //交易成功/失败的状态码
//区块中直到这一笔交易累积使用的 gas
CumulativeGasUsed uint64 `json:"cumulativeGasUsed" gencodec:"required"`
//布隆过滤器
Bloom Bloom `json:"logsBloom" gencodec:"required"`
//合约的日志列表
Logs []*Log `json:"logs" gencodec:"required"`
//处理交易时的字段
// Implementation fields: These fields are added by geth when processing a transaction.
// They are stored in the chain database.
TxHash common.Hash `json:"transactionHash" gencodec:"required"`
ContractAddress common.Address `json:"contractAddress"`
GasUsed uint64 `json:"gasUsed" gencodec:"required"`
//记录区块信息和交易索引,用于叫检查交易与对应收据的兼容性
// Inclusion information: These fields provide information about the inclusion of the
// transaction corresponding to this receipt.
BlockHash common.Hash `json:"blockHash,omitempty"`
BlockNumber *big.Int `json:"blockNumber,omitempty"`
TransactionIndex uint `json:"transactionIndex"`
}
消息调用很类似于交易的执行,黄皮书上定义合约账户接到指令后调用其他对象的行为是消息调用。执行时会创建运行时的子对象,每个子对象对应子状态,必须等所有的子对象都完成计算后,才能确定最终的状态。
消息调用的环境参数包括:
account_address
所有。注意创建合约的交易和调用合约的交易,在处理上是非常不一样的。创建合约的交易的 to
字段为空。
总而言之,交易的执行可以抽象成运行的执行状态和系统状态逐步改变的过程。运行时的执行状态叫做 machine_state
,它包括:
具体的执行过程,请阅读 智能合约审计的深入字节码分析部分。
在消息调用时有几个预编译的合约,作为链的基础架构的一部分。
详细可见 https://www.evm.codes/precompiled
交易具有原子性,要么完全的执行成功,如果执行过程某部分执行异常,会立即注销状态。异常终止并不是通过操作码实现,而是通过一系列的检查完成的,有以下情况:
具体的说,
machine_status
中获取当前 gas。正常终止可以分成两种情况:
以太坊的区块链可以视作是一棵从树根到叶子的区块树,分叉是树的分叉,主网选择区块树的工作量最大的路径以保持共识。一般而言,这条路径上的叶子是最多的,每个叶子对应一个成功验证的有效区块。路径越长,挖矿所需要的努力就越多。
形成区块的流程如下:
github: geth-analyze 个人博客:https://www.blog-blockchain.xyz/
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!