EVM0-1

从WTF开始学习EVM

参考资料:https://www.ethervm.io/ Ethereum Virtual Machine Opcodes

https://www.evm.codes/ An Ethereum Virtual Machine Opcodes Interactive Reference

https://www.wtf.academy/docs/evm-opcodes-101/

https://www.wtf.academy/docs/evm-opcodes-102/

https://chatgpt.com/

Opcodes简介

假如有一个合约

1
2
3
4
5
6
7
8
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Add {
function add() public pure returns (uint256 result) {
result = 1+1;
}
}

合约编译后的bytecode(字节码)为:

1
60806040523480156100...

通过bytecode,我们可以得到合约对应的opcodes(操作码)为:

1
PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ...

平时很多合约是不公开合约代码的,需要我们进行手搓bytecode….

EVM 基础

官方介绍:https://www.evm.codes/about

EVM的基本架构主要包括堆栈,内存,存储,EVM字节码,和燃料费

image-20240602160025842

1. 堆栈 Stack

EVM是基于堆栈的,这意味着它处理数据的方式是使用堆栈数据结构进行大多数计算。

怎么说,就是要叠盘子:

当需要加一个盘子时,就只能放在最上面(也就是堆栈的最上面)我们把这个动作叫压入PUSH 当需要取一个盘子时,只能取最上面那个,叫弹出POP

许多Opcodes涉及将数据压入堆栈或从堆栈弹出数据。

在堆栈中,每个元素长度为256位(32字节),最大深度为1024元素,但是每个操作只能操作堆栈顶的16个元素。这也是为什么有时Solidity会报Stack too deep错误。

image-20240529180007089

2. 内存 Memory

堆栈虽然计算高效,但是存储能力有限,

因此EVM使用内存来支持交易执行期间的数据存储和读取。

EVM的内存是一个线性寻址存储器,可以把它理解为一个动态字节数组,可以根据需要动态扩展。

它支持以8或256 bit写入(MSTORE8/MSTORE),但只支持以256 bit读取(MLOAD)。

EVM的内存是“易失性”的:交易开始时,所有内存位置的值均为0;

易执行期间,值被更新;

交易结束时,内存中的所有数据都会被清除,不会被持久化。

image-20240529180349684

3. 存储 Storage

EVM的账户存储(Account Storage)是一种映射(mapping,键值对存储),每个键和值都是256 bit的数据,它支持256 bit的读和写。这种存储在每个合约账户上都存在,并且是持久的,它的数据会保持在区块链上,直到被明确地修改。

对存储的读取(SLOAD)和写入(SSTORE)都需要gas,并且比内存操作更昂贵。这样设计可以防止滥用存储资源,因为所有的存储数据都需要在每个以太坊节点上保存。

image-20240529182621461

4. EVM 字节码

Solidity智能合约会被编译为EVM字节码,然后才能在EVM上运行。

这个字节码是由一系列的Opcodes组成的,通常表现为一串十六进制的数字。

EVM字节码在执行的时候,会按照顺序一个一个地读取并执行每个Opcode。

例如,字节码6001600101可以被解码为:

1
2
3
PUSH1 0x01
PUSH1 0x01
ADD

这段Opcodes的含义是将两个1相加,得到结果2。

5. Gas

EVM的每笔交易计算都是通过opcodes计算的,以太坊规定了每个opcode的Gas消耗,复杂度越高的opcodes消耗越多gas

比如:

1
2
3
ADD操作消耗3 gas
SSTORE操作消耗20000 gas //store
SLOAD操作消耗200 Gas //sload

一笔交易的gas消耗等于其中所有opcodes的gas成本总和。

当你调用一个合约函数时,你需要预估这个函数执行所需要的Gas,并在交易中提供足够的Gas。如果提供的Gas不够,那么函数执行会在中途停止,已经消耗的Gas不会退回。

image-20240529232335249

6. 执行模型

所有以上的串联起来,就是EVM的执行模型。

  1. 当一个交易被接收并准备执行时,以太坊会初始化一个新的执行环境并加载合约的字节码。
  2. 字节码被翻译成Opcode,被逐一执行。每个Opcodes代表一种操作,比如算术运算、逻辑运算、存储操作或者跳转到其他操作码。
  3. 每执行一个Opcodes,都要消耗一定数量的Gas。如果Gas耗尽或者执行出错,执行就会立即停止,所有的状态改变(除了已经消耗的Gas)都会被回滚。
  4. 执行完成后,交易的结果会被记录在区块链上,包括Gas的消耗、交易日志等信息。
image-20240602155957590

Opcodes

evm.codes

EVM Codes - An Ethereum Virtual Machine Opcodes Interactive Reference

在这里找到完整的Opcodes列表。

还提供了在线Opcodes https://www.evm.codes/playground

编写示例:

用Opcodes编写一个简单的程序,这个程序将在堆栈中计算1+1,并将结果保存到内存中。代码如下:

1
2
3
4
5
PUSH1 0x01
PUSH1 0x01
ADD
PUSH0
MSTORE
  1. 第1-2行:PUSH1指令将一个长度为1字节的数据压入堆栈顶部。

    1
    2
    3
    4
    PUSH1 0x01
    // stack: [1]
    PUSH1 0x01
    // stack: [1, 1]
  2. 第3行:ADD指令会弹出堆栈顶部的两个元素,计算它们的和,然后将结果压入堆栈。

    1
    2
    ADD
    // stack: [2]
  3. 第4行: PUSH0指令将0压入堆栈。

    1
    2
    PUSH0
    // stack: [0, 2]
  4. 第5行: MSTORE 属于内存指令,它会弹出堆栈顶的两个数据 [offset, value](偏移量和值),然后将value(长度为32字节)保存到内存索引(偏移量)为offset的位置。

    1
    2
    3
    MSTORE
    // stack: []
    // memory: [0: 2]

在evm.codes中验证执行过程和结果。(可以点右上的箭头进行debug)

image-20240602160008973

分类简介

Opcodes可以根据功能分为以下几类:

  • 堆栈(Stack)指令: 这些指令直接操作EVM堆栈。这包括将元素压入堆栈(如PUSH1)和从堆栈中弹出元素(如POP)。
  • 算术(Arithmetic)指令: 这些指令用于在EVM中执行基本的数学运算,如加法(ADD)、减法(SUB)、乘法(MUL)和除法(DIV)。
  • 比较(Comparison)指令: 这些指令用于比较堆栈顶部的两个元素。例如,大于(GT)和小于(LT)。
  • 位运算(Bitwise)指令: 这些指令用于在位级别上操作数据。例如,按位与(AND)和按位或(OR)。
  • 内存(Memory)指令: 这些指令用于操作EVM的内存。例如,将内存中的数据读取到堆栈(MLOAD)和将堆栈中的数据存储到内存(MSTORE)。
  • 存储(Storage)指令: 这些指令用于操作EVM的账户存储。例如,将存储中的数据读取到堆栈(SLOAD)和将堆栈中的数据保存到存储(SSTORE)。这类指令的gas消耗比内存指令要大。
  • 控制流(Control Flow)指令: 这些指令用于EVM的控制流操作,比如跳转JUMP和跳转目标JUMPDEST
  • 上下文(Context)指令: 这些指令用于获取交易和区块的上下文信息。例如,获取msg.sender(CALLER)和当前可用的gas(GAS)。

堆栈指令

EVM中的程序计数器(Program Counter)和堆栈指令,同时用Python实现一个简化版的EVM,可以执行PUSHPOP指令。

程序计数器

在EVM中,程序计数器(通常缩写为 PC)是一个用于跟踪当前执行指令位置的寄存器。每执行一条指令(opcode),程序计数器的值会自动增加,以指向下一个待执行的指令。但是,这个过程并不总是线性的,在执行跳转指令(JUMPJUMPI)时,程序计数器会被设置为新的值。

使用Python创建一个简单的EVM程序计数器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class EVM:
# 初始化
def __init__(self, code):
self.code = code # 初始化字节码,bytes对象
self.pc = 0 # 初始化程序计数器为0
self.stack = [] # 堆栈初始为空

# 获取当前指令
def next_instruction(self):
op = self.code[self.pc] # 获取当前指令
self.pc += 1 # 递增
return op

def run(self):
while self.pc < len(self.code):
op = self.next_instruction() # 获取当前指令
#-----------------------
code = b"\x01\x02\x03"
evm = EVM(code)
evm.run()

它的功能就是利用程序计数器遍历字节码中的opcode

堆栈指令

EVM是基于堆栈的,堆栈遵循 LIFO(后入先出)原则,最后一个被放入堆栈的元素将是第一个被取出的元素。PUSH和POP指令就是用来操作堆栈的。

PUSH(压入)

在EVM中,PUSH是一系列操作符,共有32个(在以太坊上海升级前),从PUSH1PUSH2,一直到PUSH32,操作码范围为0x600x7F。它们将一个字节大小为1到32字节的值从字节码压入堆栈(堆栈中每个元素的长度为32字节),每种指令的gas消耗都是3。

PUSH1为例,它的操作码为0x60,它会将字节码中的下一个字节压入堆栈。例如,字节码0x6001就表示把0x01压入堆栈。PUSH2就是将字节码中的下两个字节压入堆栈,例如,0x610101就是把0x0101压入堆栈。其他的PUSH指令类似。

python实现PUSH0PUSH32

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
PUSH0 = 0x5F
PUSH1 = 0x60
PUSH32 = 0x7F

class EVM:
def __init__(self, code):
self.code = code # 初始化字节码,bytes对象
self.pc = 0 # 初始化程序计数器为0
self.stack = [] # 堆栈初始为空

def next_instruction(self):
op = self.code[self.pc] # 获取当前指令
self.pc += 1 # 递增
return op

def push(self, size):
data = self.code[self.pc:self.pc + size] # 按照size从code中获取数据
value = int.from_bytes(data, 'big') # 将bytes转换为int
self.stack.append(value) # 压入堆栈
self.pc += size # pc增加size单位

def run(self):
while self.pc < len(self.code):
op = self.next_instruction()

if PUSH1 <= op <= PUSH32:
size = op - PUSH1 + 1
self.push(size)
elif op == PUSH0:
self.stack.append(0)
code = b"\x60\x01\x60\x01" #字节码0x60016001
evm = EVM(code)
evm.run()
print(evm.stack)
# output: [1, 1]

字节码0x60016001 在evm.codes上验证时候(注意要把字节码开头的0x去掉)

POP(移除)

POP指令(操作码0x50,gas消耗2)用于移除栈顶元素;如果当前堆栈为空,就抛出一个异常。

示例:

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
44
45
46
PUSH0 = 0x5F
PUSH1 = 0x60
PUSH32 = 0x7F
POP = 0x50

class EVM:
def __init__(self, code):
self.code = code # 初始化字节码,bytes对象
self.pc = 0 # 初始化程序计数器为0
self.stack = [] # 堆栈初始为空

def next_instruction(self):
op = self.code[self.pc] # 获取当前指令
self.pc += 1 # 递增
return op

def push(self, size):
data = self.code[self.pc:self.pc + size] # 按照size从code中获取数据
value = int.from_bytes(data, 'big') # 将bytes转换为int
self.stack.append(value) # 压入堆栈
self.pc += size # pc增加size单位

def pop(self):
if len(self.stack) == 0:
raise Exception('Stack underflow')
return self.stack.pop() # 弹出堆栈

def run(self):
while self.pc < len(self.code):
op = self.next_instruction()

if PUSH1 <= op <= PUSH32: # 如果为PUSH1-PUSH32
size = op - PUSH1 + 1
self.push(size)
elif op == PUSH0: # 如果为PUSH0
self.stack.append(0)
self.pc += size
elif op == POP: # 如果为POP
self.pop()

code = b"\x60\x01\x60\x01\x50" #字节码0x6001600150(PUSH1 1 PUSH1 1 POP)会将两个1压入堆栈,然后再弹出一个1
evm = EVM(code)
evm.run()
evm.stack
print(evm.stack)
# output: [1]

算数指令

ADD (加法)

ADD指令从堆栈中弹出两个元素,将它们相加,然后将结果推入堆栈。如果堆栈元素不足两个,那么会抛出异常。这个指令的操作码是0x01,gas消耗为3

可以将ADD指令的实现添加到我们的EVM模拟器中:

1
2
3
4
5
6
7
def add(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
a = self.stack.pop()
b = self.stack.pop()
res = (a + b) % (2**256) # 加法结果需要模2^256,防止溢出
self.stack.append(res)

run()函数中添加对ADD指令的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def run(self):
while self.pc < len(self.code):
op = self.next_instruction()

if PUSH1 <= op <= PUSH32:
size = op - PUSH1 + 1
self.push(size)
elif op == PUSH0:
self.stack.append(0)
self.pc += size
elif op == POP:
self.pop()
elif op == ADD: # 处理ADD指令
self.add()

完整为:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
PUSH0 = 0x5F
PUSH1 = 0x60
PUSH32 = 0x7F
POP = 0x50
ADD = 0x01

class EVM:
def __init__(self, code):
self.code = code # 初始化字节码,bytes对象
self.pc = 0 # 初始化程序计数器为0
self.stack = [] # 堆栈初始为空

def next_instruction(self):
op = self.code[self.pc] # 获取当前指令
self.pc += 1 # 递增
return op

def push(self, size):
data = self.code[self.pc:self.pc + size] # 按照size从code中获取数据
value = int.from_bytes(data, 'big') # 将bytes转换为int
self.stack.append(value) # 压入堆栈
self.pc += size # pc增加size单位

def pop(self):
if len(self.stack) == 0:
raise Exception('Stack underflow')
return self.stack.pop() # 弹出堆栈

def add(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
a = self.stack.pop()
b = self.stack.pop()
res = (a + b) % (2**256) # 加法结果需要模2^256,防止溢出
self.stack.append(res)


def run(self):
while self.pc < len(self.code):
op = self.next_instruction()

if PUSH1 <= op <= PUSH32:
size = op - PUSH1 + 1
self.push(size)
elif op == PUSH0:
self.stack.append(0)
self.pc += size
elif op == POP:
self.pop()
elif op == ADD: # 处理ADD指令
self.add()

code = b"\x60\x02\x60\x03\x01" #字节码:0x6002600301(PUSH1 2 PUSH1 3 ADD)。这个字节码将2和3推入堆栈,然后将它们相加
evm = EVM(code)
evm.run()
evm.stack
print(evm.stack)
# output: [5]
MUL (乘法)

MUL指令将堆栈的顶部两个元素相乘。操作码是0x02,gas消耗为5

实现到EVM模拟器中:

1
2
3
4
5
6
7
def mul(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
a = self.stack.pop()
b = self.stack.pop()
res = (a * b) % (2**256) # 乘法结果需要模2^256,防止溢出
self.stack.append(res)

完整代码如下:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
PUSH0 = 0x5F
PUSH1 = 0x60
PUSH32 = 0x7F
POP = 0x50
ADD = 0x01
MUL = 0x02

class EVM:
def __init__(self, code):
self.code = code # 初始化字节码,bytes对象
self.pc = 0 # 初始化程序计数器为0
self.stack = [] # 堆栈初始为空

def next_instruction(self):
op = self.code[self.pc] # 获取当前指令
self.pc += 1 # 递增
return op

def push(self, size):
data = self.code[self.pc:self.pc + size] # 按照size从code中获取数据
value = int.from_bytes(data, 'big') # 将bytes转换为int
self.stack.append(value) # 压入堆栈
self.pc += size # pc增加size单位

def pop(self):
if len(self.stack) == 0:
raise Exception('Stack underflow')
return self.stack.pop() # 弹出堆栈

def add(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
a = self.stack.pop()
b = self.stack.pop()
res = (a + b) % (2**256) # 加法结果需要模2^256,防止溢出
self.stack.append(res)

def mul(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
a = self.stack.pop()
b = self.stack.pop()
res = (a * b) % (2**256) # 乘法结果需要模2^256,防止溢出
self.stack.append(res)


def run(self):
while self.pc < len(self.code):
op = self.next_instruction()

if PUSH1 <= op <= PUSH32:
size = op - PUSH1 + 1
self.push(size)
elif op == PUSH0:
self.stack.append(0)
self.pc += size
elif op == POP:
self.pop()
elif op == ADD: # 处理ADD指令
self.add()
elif op == MUL: # 处理MUL指令
self.mul()

code = b"\x60\x02\x60\x03\x02"
evm = EVM(code)
evm.run()
evm.stack
print(evm.stack)
#[6]
SUB (减法)

SUB指令从堆栈顶部弹出两个元素,然后计算第二个元素减去第一个元素,最后将结果推入堆栈。这个指令的操作码是0x03,gas消耗为3

1
2
3
4
5
6
7
def sub(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
a = self.stack.pop()
b = self.stack.pop()
res = (b - a) % (2**256) *# 结果需要模2^256,防止溢出*
self.stack.append(res)

在run中添加

1
2
3
4
5
6
def run(self):
#.....
elif op == SUB: # 处理SUB指令
self.sub()


同时记得定义:

1
MUL = 0x02   

后文中方法所代常量将不再说明

1
2
3
4
5
code = b"\x60\x03\x60\x02\x03" #包含SUB指令的字节码:0x6002600303(PUSH1 2 PUSH1 3 SUB)这个字节码将2和3推入堆栈,然后将它们相减
evm = EVM(code)
evm.run()
print(evm.stack)
# output: [1]
DIV (除法)

DIV指令从堆栈顶部弹出两个元素,然后将第二个元素除以第一个元素,最后将结果推入堆栈。如果第一个元素(除数)为0,则将0推入堆栈。这个指令的操作码是0x04,gas消耗为5

1
2
3
4
5
6
7
8
9
10
def div(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
a = self.stack.pop()
b = self.stack.pop()
if a == 0:
res = 0
else:
res = (b // a) % (2**256)
self.stack.append(res)

在run中定义

1
2
3
4
def run(self):
#.....
elif op == DIV: # 处理DIV指令
self.div()

运行一个包含DIV指令的字节码:0x6002600304(PUSH1 2 PUSH1 3 DIV)。这个字节码将23推入堆栈,然后将它们相除。

1
2
3
4
5
code = b"\x60\x06\x60\x03\x04"
evm = EVM(code)
evm.run()
print(evm.stack)
# output: [2]
其他算数指令(SDIV,MOD,SMOD,ADDMOD,MULMOD,EXP,SIGNEXTEND)

SDIV: 带符号整数的除法指令。与DIV类似,这个指令会从堆栈中弹出两个元素,然后将第二个元素除以第一个元素,结果带有符号。如果第一个元素(除数)为0,结果为0。它的操作码是0x05,gas消耗为5。要注意,EVM字节码中的负数是用二进制补码(two’s complement)形式,比如-1表示为0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff,它加一等于0。

1
2
3
4
5
6
7
def sdiv(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
a = self.stack.pop()
b = self.stack.pop()
res = b//a % (2**256) if a!=0 else 0
self.stack.append(res)

MOD: 取模指令。这个指令会从堆栈中弹出两个元素,然后将第二个元素除以第一个元素的余数推入堆栈。如果第一个元素(除数)为0,结果为0。它的操作码是0x06,gas消耗为5。

1
2
3
4
5
6
7
def mod(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
a = self.stack.pop()
b = self.stack.pop()
res = b % a if a != 0 else 0
self.stack.append(res)

SMOD: 带符号的取模指令。这个指令会从堆栈中弹出两个元素,然后将第二个元素除以第一个元素的余数推入堆栈,结果带有第二个元素的符号。如果第一个元素(除数)为0,结果为0。它的操作码是0x07,gas消耗为5。

1
2
3
4
5
6
7
def smod(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
a = self.stack.pop()
b = self.stack.pop()
res = b % a if a != 0 else 0
self.stack.append(res)

ADDMOD: 模加法指令。这个指令会从堆栈中弹出三个元素,将前两个元素相加,然后对第三个元素取模,将结果推入堆栈。如果第三个元素(模数)为0,结果为0。它的操作码是0x08,gas消耗为8。

1
2
3
4
5
6
7
8
def addmod(self):
if len(self.stack) < 3:
raise Exception('Stack underflow')
a = self.stack.pop()
b = self.stack.pop()
n = self.stack.pop()
res = (a + b) % n if n != 0 else 0
self.stack.append(res)

MULMOD: 模乘法指令。这个指令会从堆栈中弹出三个元素,将前两个元素相乘,然后对第三个元素取模,将结果推入堆栈。如果第三个元素(模数)为0,结果为0。它的操作码是0x09,gas消耗为5。

1
2
3
4
5
6
7
8
def mulmod(self):
if len(self.stack) < 3:
raise Exception('Stack underflow')
a = self.stack.pop()
b = self.stack.pop()
n = self.stack.pop()
res = (a * b) % n if n != 0 else 0
self.stack.append(res)

EXP: 指数运算指令。这个指令会从堆栈中弹出两个元素,将第二个元素作为底数,第一个元素作为指数,进行指数运算,然后将结果推入堆栈。它的操作码是0x0A,gas消耗为10。

1
2
3
4
5
6
7
def exp(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
a = self.stack.pop()
b = self.stack.pop()
res = pow(b, a) % (2**256)
self.stack.append(res)

SIGNEXTEND: 符号位扩展指令,即在保留数字的符号(正负性)及数值的情况下,增加二进制数字位数的操作。举个例子,若计算机使用8位二进制数表示数字“0000 1010”,且此数字需要将字长符号扩充至16位,则扩充后的值为“0000 0000 0000 1010”。此时,数值与符号均保留了下来。SIGNEXTEND指令会从堆栈中弹出两个元素,对第二个元素进行符号扩展,扩展的位数由第一个元素决定,然后将结果推入堆栈。它的操作码是0x0B,gas消耗为5。

1
2
3
4
5
6
7
8
9
10
11
def signextend(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
b = self.stack.pop()
x = self.stack.pop()
if b < 32: # 如果b>=32,则不需要扩展
sign_bit = 1 << (8 * b - 1) # b 字节的最高位(符号位)对应的掩码值,将用来检测 x 的符号位是否为1
x = x & ((1 << (8 * b)) - 1) # 对 x 进行掩码操作,保留 x 的前 b+1 字节的值,其余字节全部置0
if x & sign_bit: # 检查 x 的符号位是否为1
x = x | ~((1 << (8 * b)) - 1) # 将 x 的剩余部分全部置1
self.stack.append(x)

比较指令

EVM中用于比较运算的6个指令,包括LT(小于),GT(大于),和EQ(相等)

LT (小于)

LT指令从堆栈中弹出两个元素,比较第二个元素是否小于第一个元素。如果是,那么将1推入堆栈,否则将0推入堆栈。如果堆栈元素不足两个,那么会抛出异常。这个指令的操作码是0x10,gas消耗为3

1
2
3
4
5
6
def lt(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
a = self.stack.pop()
b = self.stack.pop()
self.stack.append(int(b < a)) # 注意这里的比较顺序

run()函数中

1
2
3
4
5
6
7
8
def run(self):
while self.pc < len(self.code):
op = self.next_instruction()

# ... 其他指令的处理 ...

elif op == LT: # 处理LT指令
self.lt()

包含LT指令的字节码:0x6002600310(PUSH1 2 PUSH1 3 LT)。这个字节码将23推入堆栈,然后比较2是否小于3

1
2
3
4
5
code = b"\x60\x02\x60\x03\x10"  
evm = EVM(code)
evm.run()
print(evm.stack)
# output: [1]
GT (大于)

GT指令和LT指令非常类似,不过它比较的是第二个元素是否大于第一个元素。操作码是0x11,gas消耗为3

1
2
3
4
5
6
def gt(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
a = self.stack.pop()
b = self.stack.pop()
self.stack.append(int(b > a)) # 注意这里的比较顺序
1
2
3
4
5
6
7
8
def run(self):
while self.pc < len(self.code):
op = self.next_instruction()

# ... 其他指令的处理 ...

elif op == GT: # 处理GT指令
self.gt()

包含GT指令的字节码:0x6002600311(PUSH1 2 PUSH1 3 GT)。这个字节码将23推入堆栈,然后比较2是否大于3

1
2
3
4
5
code = b"\x60\x02\x60\x03\x11"
evm = EVM(code)
evm.run()
print(evm.stack)
# output: [0]
EQ (等于)

EQ指令从堆栈中弹出两个元素,如果两个元素相等,那么将1推入堆栈,否则将0推入堆栈。该指令的操作码是0x14,gas消耗为3

1
2
3
4
5
6
def eq(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
a = self.stack.pop()
b = self.stack.pop()
self.stack.append(int(a == b))

run()

1
2
elif op == EQ: 
self.eq()

运行一个包含EQ指令的字节码:0x6002600314(PUSH1 2 PUSH1 3 EQ)。这个字节码将23推入堆栈,然后比较两者是否相等。

1
2
3
4
5
code = b"\x60\x02\x60\x03\x14"
evm = EVM(code)
evm.run()
print(evm.stack)
# output: [0]
ISZERO (是否为零)

ISZERO指令从堆栈中弹出一个元素,如果元素为0,那么将1推入堆栈,否则将0推入堆栈。该指令的操作码是0x15,gas消耗为3

1
2
3
4
5
def iszero(self):
if len(self.stack) < 1:
raise Exception('Stack underflow')
a = self.stack.pop()
self.stack.append(int(a == 0))

run()函数

1
2
elif op == ISZERO: 
self.iszero()

运行一个包含ISZERO指令的字节码:0x600015(PUSH1 0 ISZERO)。这个字节码将0推入堆栈,然后检查其是否为0。

1
2
3
4
5
code = b"\x60\x00\x15"
evm = EVM(code)
evm.run()
print(evm.stack)
# output: [1]
其他比较指令

SLT (有符号小于): 这个指令会从堆栈中弹出两个元素,然后比较第二个元素是否小于第一个元素,结果以有符号整数形式返回。如果第二个元素小于第一个元素,将1推入堆栈,否则将0推入堆栈。它的操作码是0x12,gas消耗为3

1
2
3
4
5
6
def slt(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
a = self.stack.pop()
b = self.stack.pop()
self.stack.append(int(b < a)) # 极简evm stack中的值已经是以有符号整数存储了,所以和lt一样实现

SGT (有符号大于): 这个指令会从堆栈中弹出两个元素,然后比较第二个元素是否大于第一个元素,结果以有符号整数形式返回。如果第二个元素大于第一个元素,将1推入堆栈,否则将0推入堆栈。它的操作码是0x13,gas消耗为3

1
2
3
4
5
6
def sgt(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
a = self.stack.pop()
b = self.stack.pop()
self.stack.append(int(b > a)) # 极简evm stack中的值已经是以有符号整数存储了,所以和gt一样实现

位级指令

AND (与)

AND指令从堆栈中弹出两个元素,对它们进行位与运算,并将结果推入堆栈。操作码是0x16,gas消耗为3

1
2
3
4
5
6
def and_op(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
a = self.stack.pop()
b = self.stack.pop()
self.stack.append(a & b)

run()函数

1
2
3
4
5
6
7
8
def run(self):
while self.pc < len(self.code):
op = self.next_instruction()

# ... 其他指令的处理 ...

elif op == AND: # 处理AND指令
self.and_op()

尝试运行一个包含AND指令的字节码:0x6002600316(PUSH1 2 PUSH1 3 AND)。这个字节码将2(0000 0010)和3(0000 0011)推入堆栈,然后进行位级与运算,结果应该为2(0000 0010)

1
2
3
4
5
code = b"\x60\x02\x60\x03\x16"
evm = EVM(code)
evm.run()
print(evm.stack)
# output: [2]
OR (或)

OR指令与AND指令类似,但执行的是位或运算。操作码是0x17,gas消耗为3

1
2
3
4
5
6
def or_op(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
a = self.stack.pop()
b = self.stack.pop()
self.stack.append(a | b)

我们在run()函数中添加对OR指令的处理:

1
2
3
4
5
6
7
8
def run(self):
while self.pc < len(self.code):
op = self.next_instruction()

# ... 其他指令的处理 ...

elif op == OR: # 处理OR指令
self.or_op()

尝试运行一个包含OR指令的字节码:0x6002600317(PUSH1 2 PUSH1 3 OR)。这个字节码将2(0000 0010)和3(0000 0011)推入堆栈,然后进行位级与运算,结果应该为3(0000 0011)。

1
2
3
4
5
code = b"\x60\x02\x60\x03\x17"
evm = EVM(code)
evm.run()
print(evm.stack)
# output: [3]
XOR (异或)

XOR指令与ANDOR指令类似,但执行的是异或运算。操作码是0x18,gas消耗为3

1
2
3
4
5
6
def xor_op(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
a = self.stack.pop()
b = self.stack.pop()
self.stack.append(a ^ b)
1
2
3
4
5
6
7
8
def run(self):
while self.pc < len(self.code):
op = self.next_instruction()

# ... 其他指令的处理 ...

elif op == XOR: # 处理XOR指令
self.xor_op()

尝试运行一个包含XOR指令的字节码:0x6002600318(PUSH1 2 PUSH1 3 XOR)。这个字节码将2(0000 0010)和3(0000 0011)推入堆栈,然后进行位级与运算,结果应该为1(0000 0001)。

1
2
3
4
5
code = b"\x60\x02\x60\x03\x18"
evm = EVM(code)
evm.run()
print(evm.stack)
# output: [1]
NOT

NOT 指令执行按位非操作,取栈顶元素的补码,然后将结果推回栈顶。它的操作码是0x19,gas消耗为3

1
2
3
4
5
def not_op(self):
if len(self.stack) < 1:
raise Exception('Stack underflow')
a = self.stack.pop()
self.stack.append(~a % (2**256)) # 按位非操作的结果需要模2^256,防止溢出

run()函数中添加对NOT指令的处理:

1
2
elif op == NOT: # 处理NOT指令
self.not_op()

尝试运行一个包含NOT指令的字节码:0x600219(PUSH1 2 NOT)。这个字节码将2(0000 0010)推入堆栈,然后进行位级非运算,结果应该为很大的数(0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd)。

1
2
3
4
5
6
# NOT
code = b"\x60\x02\x19"
evm = EVM(code)
evm.run()
print(evm.stack)
# output: [很大的数] (fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd)
SHL

SHL指令执行左移位操作,从堆栈中弹出两个元素,将第二个元素左移第一个元素位数,然后将结果推回栈顶。它的操作码是0x1B,gas消耗为3

1
2
3
4
5
6
def shl(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
a = self.stack.pop()
b = self.stack.pop()
self.stack.append((b << a) % (2**256)) # 左移位操作的结果需要模2^256

run()

1
2
elif op == SHL: # 处理SHL指令
self.shl()

运行一个包含XOR指令的字节码:0x600260031B(PUSH1 2 PUSH1 3 SHL)。这个字节码将2(0000 0010)和3(0000 0011)推入堆栈,然后将2左移3位,结果应该为16(0001 0000)。

1
2
3
4
5
code = b"\x60\x02\x60\x03\x1B"
evm = EVM(code)
evm.run()
print(evm.stack)
# output: [16] (0x000000010 << 3 => 0x00010000)
SHR

SHR指令执行右移位操作,从堆栈中弹出两个元素,将第二个元素右移第一个元素位数,然后将结果推回栈顶。它的操作码是0x1C,gas消耗为3

1
2
3
4
5
6
def shr(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
a = self.stack.pop()
b = self.stack.pop()
self.stack.append(b >> a) # 右移位操作

run()

1
2
elif op == SHR: # 处理SHR指令
self.shr()

尝试运行一个包含XOR指令的字节码:0x601060031C(PUSH1 16 PUSH1 3 SHL)。这个字节码将16(0001 0000)和3(0000 0011)推入堆栈,然后将16右移3位,结果应该为2(0000 0010)。

1
2
3
4
5
code = b"\x60\x10\x60\x03\x1C"
evm = EVM(code)
evm.run()
print(evm.stack)
# output: [2] (0x00010000 >> 3 => 0x000000010)
其他位级指令(BYTE,SAR)

BYTE: BYTE指令从堆栈中弹出两个元素(ab),将第二个元素(b)看作一个字节数组,并返回该字节数组中第一个元素指定索引的字节(b[a]),并压入堆栈。如果索引大于或等于字节数组的长度,则返回0。操作码是0x1a,gas消耗为3

1
2
3
4
5
6
7
8
9
10
def byte_op(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
position = self.stack.pop()
value = self.stack.pop()
if position >= 32:
res = 0
else:
res = (value // pow(256, 31 - position)) & 0xFF
self.stack.append(res)

SAR: SAR指令执行算术右移位操作,与SHR类似,但考虑符号位:如果我们对一个负数进行算术右移,那么在右移的过程中,最左侧(符号位)会被填充F以保持数字的负值。它从堆栈中弹出两个元素,将第二个元素以符号位填充的方式右移第一个元素位数,然后将结果推回栈顶。它的操作码是0x1D。由于Python的>>操作符已经是算术右移,我们可以直接复用shr函数的代码。

1
2
3
4
5
6
def sar(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
a = self.stack.pop()
b = self.stack.pop()
self.stack.append(b >> a) # 右移位操作

内存指令

内存的读写比存储(Storage)的读写要便宜的多,每次读写有固定费用3 gas,另外如果首次访问了新的内存位置(内存拓展),则需要付额外的费用(由当前偏移量和历史最大偏移量决定),计算方法见链接

EVM中的内存

EVM的内存,它是一个线性寻址存储器,类似一个动态的字节数组,可以根据需求动态扩展。它的另一个特点就是易失性,交易结束时所有数据都会被清零。它支持以8或256 bit写入(MSTORE8/MSTORE),但只支持以256 bit取(MLOAD)。

image-20240602192010425

我们可以用Python内置的bytearray来代表内存:

1
2
3
4
5
def __init__(self, code):
self.code = code
self.pc = 0
self.stack = []
self.memory = bytearray() # 内存初始化为空
MSTORE (内存写)

MSTORE指令用于将一个256位(32字节)的值存储到内存中。它从堆栈中弹出两个元素,第一个元素为内存的地址(偏移量 offset),第二个元素为存储的值(value)。操作码是0x52,gas消耗根据实际内存使用情况计算(3+X)。

1
2
3
4
5
6
7
8
def mstore(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
offset = self.stack.pop()
value = self.stack.pop()
while len(self.memory) < offset + 32:
self.memory.append(0) # 内存扩展
self.memory[offset:offset+32] = value.to_bytes(32, 'big')

run()函数中

1
2
3
4
5
6
7
8
def run(self):
while self.pc < len(self.code):
op = self.next_instruction()

# ... 其他指令的处理 ...

elif op == MSTORE: # 处理MSTORE指令
self.mstore()

尝试运行一个包含MSTORE指令的字节码:0x6002602052(PUSH1 2 PUSH1 0x20 MSTORE)。这个字节码将20x20(32)推入堆栈,然后进行MSTORE,将2存到偏移量为0x20的地方。

1
2
3
4
5
6
# MSTORE
code = b"\x60\x02\x60\x20\x52"
evm = EVM(code)
evm.run()
print(evm.memory[0x20:0x40])
# 输出: [0, 0, 0, ..., 0, 2]
MSTORE8 (内存8位写)

MSTORE8指令用于将一个8位(1字节)的值存储到内存中。与MSTORE类似,但只使用最低8位。操作码是0x53,gas消耗根据实际内存使用情况计算(3+X)。

1
2
3
4
5
6
7
8
def mstore8(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
offset = self.stack.pop()
value = self.stack.pop()
while len(self.memory) < offset + 32:
self.memory.append(0) # 内存扩展
self.memory[offset] = value & 0xFF # 取最低有效字节

run()函数中

1
2
elif op == MSTORE8: # 处理MSTORE8指令
self.mstore8()

尝试运行一个包含MSTORE8指令的字节码:0x6002602053(PUSH1 2 PUSH1 0x20 MSTORE8)。这个字节码将20x20(32)推入堆栈,然后进行MSTORE8,将2存到偏移量为0x20的地方。

1
2
3
4
5
6
# MSTORE8
code = b"\x60\x02\x60\x20\x53"
evm = EVM(code)
evm.run()
print(evm.memory[0x20:0x40])
# 输出: [2, 0, 0, ..., 0, 0]

为什么是MSTORE是[0, 0, 0, …, 0, 2]而MSTORE8是[2, 0, 0, …, 0, 0]

因为MSTORE是32字节 前面30个字节为0,最后两个字节为2

MSTORE8只会在内存中存储一个字节的值,即2

MLOAD (内存读)

MLOAD指令从内存中加载一个256位的值并推入堆栈。它从堆栈中弹出一个元素,从该元素表示的内存地址中加载32字节,并将其推入堆栈。操作码是0x51,gas消耗根据实际内存使用情况计算(3+X)。

1
2
3
4
5
6
7
8
def mload(self):
if len(self.stack) < 1:
raise Exception('Stack underflow')
offset = self.stack.pop()
while len(self.memory) < offset + 32:
self.memory.append(0) # 内存扩展
value = int.from_bytes(self.memory[offset:offset+32], 'big')
self.stack.append(value)

尝试运行一个包含MLOAD指令的字节码:0x6002602052602051(PUSH1 2 PUSH1 0x20 MSTORE PUSH1 0x20 MLOAD)。这个字节码将20x20(32)推入堆栈,然后进行MSTORE,将2存到偏移量为0x20的地方;然后将0x20推入堆栈,然后进行MLOAD,将刚才存储在内存的值读取出来。

1
2
3
4
5
6
7
8
# MSTORE
code = b"\x60\x02\x60\x20\x52\x60\x20\x51"
evm = EVM(code)
evm.run()
print(evm.memory[0x20:0x40])
# 输出: [0, 0, 0, ..., 0, 2]
print(evm.stack)
# output: [2]
MSIZE (内存大小)

MSIZE指令将当前的内存大小(以字节为单位)压入堆栈。操作码是0x59,gas消耗为2。

1
2
def msize(self):
self.stack.append(len(self.memory))

存储指令

EVM的存储,和内存不同,它是一种持久化存储空间,存在存储中的数据在交易之间可以保持。它是EVM的状态存储的一部分,支持以256 bit为单位的读写。

image-20240605153418696

由于存储使用键值对存储数据,每个键和值都是256 bit,因此我们可以用Python内置的dict(字典)来代表存储:

1
2
3
4
5
6
def __init__(self, code):
self.code = code
self.pc = 0
self.stack = []
self.memory = bytearray() # 内存初始化为空
self.storage = {} # 存储初始化为空字典
SSTORE (存储写)

SSTORE指令用于将一个256位(32字节)的值写入到存储。

它从堆栈中弹出两个元素,第一个元素为存储的地址(key),第二个元素为存储的值(value)。操作码是0x55,gas消耗根据实际改变的数据计算(下面给出)。

1
2
3
4
5
6
def sstore(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
key = self.stack.pop()
value = self.stack.pop()
self.storage[key] = value

run中

1
2
3
4
5
6
7
8
def run(self):
while self.pc < len(self.code):
op = self.next_instruction()

# ... 其他指令的处理 ...

elif op == SSTORE: # 处理SSTORE指令
self.sstore()

运行一个包含SSTORE指令的字节码:0x6002600055(PUSH1 2 PUSH1 0 SSTORE)。这个字节码将20推入堆栈,然后进行SSTORE,将2存到键为0x0的存储槽。

1
2
3
4
5
6
# SSTORE
code = b"\x60\x02\x60\x00\x55"
evm = EVM(code)
evm.run()
print(evm.storage)
# Output: {0: 2}

为什么输出的是{0: 2}而不是{2: 0} 因为栈是只能栈顶,也就是所谓的“上层”,最后被压入的是栈顶 所以0x00为栈顶

SLOAD (存储读)

SLOAD指令从存储中读取一个256位(32字节)的值并推入堆栈。

从堆栈中弹出一个元素,从该元素表示的存储槽中加载值,并将其推入堆栈。操作码是0x54

1
2
3
4
5
6
def sload(self):
if len(self.stack) < 1:
raise Exception('Stack underflow')
key = self.stack.pop()
value = self.storage.get(key, 0) # 如果键不存在,返回0
self.stack.append(value)

run()函数

1
2
elif op == SLOAD: 
self.sload()

尝试运行一个包含SLOAD指令的字节码:0x6002600055600054(PUSH1 2 PUSH1 0 SSTORE PUSH1 0 SLOAD)。这个字节码将20推入堆栈,然后进行SSTORE,将2存到键为0的地方;然后将0推入堆栈,然后进行SLOAD,将刚才写入0x0存储槽的值读取出来。

1
2
3
4
5
6
7
8
# SLOAD
code = b"\x60\x02\x60\x00\x55\x60\x00\x54"
evm = EVM(code)
evm.run()
print(evm.storage)
# 输出: {0: 2}
print(evm.stack)
# 输出: [2]
访问集 EIP-2929

访问集(Access Sets)是EIP-2929提出的一种新概念,它的引入有助于优化Gas计费和以太坊的网络性能。访问集是在每个外部交易中定义的,并且在交易过程中会跟踪和记录每个交易访问过的合约地址和存储槽(slot)。

  • 合约地址:在执行交易过程中,任何被访问到的地址都会被添加到访问集中。
  • 存储槽:这个列表包含了一个交易在执行过程中访问过的所有存储槽。

如果一个地址或存储槽在访问集中,我们称它为”warm”,否则称之为”cold”。一个地址或存储槽在一次交易中首次被访问时,它会从”cold”变为”warm”。在交易执行期间,如果一个指令需要访问一个”cold”的地址或存储槽,那么这个指令的Gas消耗会更高。而对 “warm” 的地址或存储槽的访问,则会有较低的 Gas 消耗,因为相关数据已经被缓存了。

!Gas Cost:SLOAD (存储读),SSTORE (存储写)

https://www.wtf.academy/docs/evm-opcodes-101/StorageOp/#gas-cost