以太坊虚拟机(EVM)
以太坊虚拟机(Ethereum Virtual Machine,简称EVM)是一个基于栈的虚拟机,基于特定的环境数据,执行一系列的字节代码形式的指令,以修改系统状态。EVM目前提供了11类,140个指令。
EVM是一个准图灵机,这个“准”的限定来源于其中的运算是通过参数gas来限制的,也就是限定了可以执行的运算总量。EVM的具体逻辑定义为代码执行函数(Ξ):
(
σ
′
,
g
′
,
A
,
o
)
≡
Ξ
(
σ
,
g
,
I
)
(\boldsymbol{\sigma}', g', A, \mathbf{o}) \equiv \Xi(\boldsymbol{\sigma}, g, I)
(σ′,g′,A,o)≡Ξ(σ,g,I)
其中,函数的参数部分,σ表示状态,g表示可用gas,I表示执行环境数据;函数的返回值部分,σ’表示计算后的状态,g’表示剩余gas,A表示累积子状态,o表示结果输出。
EVM的具体实现逻辑包含:
- 基本定义: 对环境、指令集、EVM状态的定义
- 合约代码执行: 将合约字节代码解析为一系列指令及其参数,并循环执行。
- 指令执行: 根据执行环境和EVM状态,对单条指令进行解析、计费、执行
- 指令解析: 获取将字节码解析为指令及参数
- GAS计费: 计算指令执行所要消耗的gas数量,并校验gas是否充足
- 栈操作: 出入栈操作,及其校验
- 内存操作: 读写内存操作,内存分配等
- 指令运算: 执行指令的具体逻辑
- 异常停止: 因Gas不足、栈溢出等问题导致的程序执行终止。
- 正常停止: 按照代码逻辑完成指令的执行,并正常退出和输出结果。
- 状态修改: 基于执行前的状态数据,完成gas支付、空账户清除(及其补贴用于抵扣部分gas)等状态修改操作。
基本定义
指令集
目前,EVM提供了140个指令。其中,以太坊黄皮书中定义了135个,包含特定的无效指令INVALID ;以太坊改进提案(Ethereum Improvement Proposals,下文中简称EIPs)中定义了5个。
指令定义
每个指令的定义信息包括:
- 字节码 :或称指令编号,数值范围0x00 - 0xff。
- 助记符 :或称指令名称
- 出栈数量
- 入栈数量
- 指令含义 :指令的基本描述、Gas费用、运算逻辑,以及机器状态(栈、内存)的操作
private final byte opcode;
private final int require;
private final Tier tier;
private final int ret;
private final EnumSet<CallFlags> callFlags;
private OpCode(int op, int require, int ret, Tier tier, CallFlags ... callFlags) ...
指定定义示例
下面以加法算术指令ADD 指令为例,其指令定义为:
- 字节码:
0x01 - 出栈数量:
2 ,即ADD 指令参数,被加数与加数 - 入栈数量:
1 ,即ADD 指令运算结果,被加数与加数之和 - Gas费用级别:
VeryLowTier ,对应gas数量为3
ADD(0x01, 2, 1, VeryLowTier),
指令类别
以太坊黄皮书中将指令划分为11个类别,且每类指令的编号的数值范围不同。比如,停止和算术运算类指令编号的数组区间为0x00 - 0x0f(表示为0s)。
指令类别及其部分指令:
- 0s:停止和算术运算
- 停止指令:
STOP - 算术类(部分):
ADD (加)、SUB (减)、MUL (乘)、DIV (除)、MOD (取模)、EXP (指数) - 10s:比较和按位逻辑运算
- 比较逻辑运算(部分):
LT (小于)、GT (大于)、EQ (等于)、ISZERO (否,结合EQ指令实现不等于) - 按位逻辑运算:
AND (与)、OR (或)、NOT (非)、XOR (异或) - 20s:SHA3运算,
SHA3 (Keccak-256哈希) - 30s:环境信息(部分),
ADDRESS (获取Ia)、GASPRICE (获取Ip)、BALANCE (获取给定账户余额) - 40s:区块信息
BLOCKHASH :获取指定高度的祖先区块的头部哈希COINBASE :获取该区块的矿工账户地址TIMESTAMP :获取该区块的时间戳NUMBER :获取区块的号码DIFFICULTY :获得区块的难度GASLIMIT :获得区块的gas上限 - 50s:栈,内存,存储和流程操作
- 栈:
POP (出栈) - 内存:
MLOAD (从内存中加载字)、MSTORE (将字保存到内存)、MSTORE8 (将字节保存到内存)、MSIZE (获取活动内存的大小) - 存储:
SLOAD (从存储加载字)、SSTORE (将字保存到存储) - 流程控制:
JUMP (跳转)、JUMPI (有条件的跳转)、JUMPDEST (μpc加1)、PC (获取μpc)、GAS (获取μg) - 60s & 70s: 入栈(Push)操作,
PUSH1 - PUSH32 - 80s: 复制(Duplicate)操作,
DUP1 - DUP16 - 90s: 替换(Exchange)操作,
SWAP1 - SWAP16 - a0s: 日志操作,
LOG0 - LOG4 - f0s: 系统操作(部分)
CREATE :创建合约CALL :消息调用合约CALLCODE :使用指定帐户的代码对当前帐户进行消息调用RETURN :停止执行,返回输出数据DELEGATECALL :使用指定帐户的代码对当前帐户进行消息调用,但保留sender和value属性现有的值。INVALID :保留的无效指令SELFDESTRUCT :销毁合约
此外,还在EIPs中定义了如下指令:
- EIP-145中定义了3个按位逻辑运算(10s)
SHL :0x1b,左移 (shift left)SHR :0x1c,逻辑右移 (logical shift right)SAR :0x1d,算术右移 (arithmetic shift right) - EIP-1014中定义了1个系统操作指令(f0s)
- EIP-1052中定义了1个环境信息获取指令(30s)
EXTCODEHASH :0x3f,获取合同代码的keccak256哈希
执行环境
执行环境数据,表示为I ,包含属性:
Ia ,拥有正在执行的代码的账户地址,一般是指交易的发送方账户S(T) ,在合约调用合约的情况下则不同。Io ,触发这次执行的初始交易的发送者地址,初始交易是指由外部账户发起的交易S(T) 。Ip ,触发这次执行的初始交易的 gas 价格。Id ,这次执行的输入数据字节数组;如果执行代理是一个交易,这就是交易数据。Is ,触发这次执行的账户地址;如果执行代理是一个交易,则为交易发送者地址。Iv ,作为这次执行的一部分传到当前账户的转账金额,以 Wei为单位;如果执行代理是一个交易, 这就是交易的转账金额。Ib ,所要执行的EVM字节码数组。IH ,当前区块的区块头。Ie ,当前消息调用或合约创建的深度(也就是当前已经被执行的 CALL 或 CREATE 的数量)。Iw ,修改状态的许可。
这里我们来回顾一下交易数据与待执行代码的对照关系:
- 消息调用交易
Ib :即交易的被调用合约Tt ,存储在世界状态树中的合约代码σ[Tt]c Id :交易的消息调用输入数据Td ,存储在交易附言(Transaction.data)属性。 - 合约创建交易
Ib :即交易的合约初始化代码Ti ,也存储在交易附言(Transaction.data)属性。Id :此类交易无输入数据
EVM状态
EVM状态,也称机器状态(Machine State),表示为μ 。
可用gas,表示为μg ,根据交易gas上限和已用gas数量计算:gasLimit - gasUsed。
private long gasUsed;
public long getGasLong() {
return invoke.getGasLong() - getResult().getGasUsed();
}
程序计数器,表示为μpc ,初始值为0。
private int pc;
内存,表示为μm ,内存模型是基于字寻址(word-addressed)的字节数组,所有内存中的数据都会初始化为0。
private Memory memory;
public class Memory ... {
private static final int CHUNK_SIZE = 1024;
private static final int WORD_SIZE = 32;
private List<byte[]> chunks = new LinkedList<>();
...
}
内存已激活字数,表示为μi,初始值为0。
private int softSize;
栈,表示为μs ,其中栈中数据项的大小(即字长)是256位,最大深度为1024,初始值为空序列。
private Stack stack;
public class Stack extends java.util.Stack<DataWord> ...
主要是指消息调用指令的结果输出数据,表示为μo ,初始值为空序列。
private byte[] returnDataBuffer;
交易子状态
交易执行过程中会产生一些特定的信息,记录交易相关的账户、日志等内容,也称为交易子状态或累积子状态。
合约代码执行过程中,通过SELFDESTRUCT 指令销毁账户的地址的集合,表示为As ,初始值为空集合。
private Set<DataWord> deleteAccounts;
交易执行过程中所接触账户的地址的集合,表示为At ,初始值为空集合。此集合的特征是交易执行过程中账户状态发生过变化,包含新创建的合约账户、消息调用接收方账户、销毁账户余额的继承账户等,交易执行完成时将删除其中的空账户。
private ByteArraySet touchedAccounts = new ByteArraySet();
合约代码执行中,输出的一系列带有主题和数据内容的日志,并且记录了输出日志的账户地址,表示为Al ,初始值为空。其中,日志的所属账户地址和主题可以作为检索条件,通过区块头部和交易收据中的Bloom过滤器,实现区块、交易二级索引的信息查询。 合约开发者通过日志,可以获取合约执行的详细状态,方便合约的上层应用实现一些类似于异步通知或回调功能,为应用最终用户提供更加友好的体验。
private List<LogInfo> logInfoList;
以太坊为了鼓励合约开发者尽可能少的占用计算资源,对于释放已占用资源的操作给予一定的gas补贴以抵扣部分交易费用,如通过SSTORE 指令释放合约存储资源,通过SELFDESTRUCT 指令清空指定账户的状态数据。合约代码执行中,将根据约定计算返还gas数量,表示为Ar ,初始值为0。
private long futureRefund = 0;
|