EVM从0-1
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/
Opcodes简介
假如有一个合约
1 | // SPDX-License-Identifier: MIT |
合约编译后的bytecode(字节码)为:
1 | 60806040523480156100... |
通过bytecode,我们可以得到合约对应的opcodes(操作码)为:
1 | PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ... |
平时很多合约是不公开合约代码的,需要我们进行手搓bytecode….
EVM 基础
官方介绍:https://www.evm.codes/about
EVM的基本架构主要包括堆栈,内存,存储,EVM字节码,和燃料费
1. 堆栈 Stack
EVM是基于堆栈的,这意味着它处理数据的方式是使用堆栈数据结构进行大多数计算。
怎么说,就是要叠盘子:
当需要加一个盘子时,就只能放在最上面(也就是堆栈的最上面)我们把这个动作叫压入PUSH
当需要取一个盘子时,只能取最上面那个,叫弹出POP
。
许多Opcodes涉及将数据压入堆栈或从堆栈弹出数据。
在堆栈中,每个元素长度为256位(32字节),最大深度为1024元素,但是每个操作只能操作堆栈顶的16个元素。这也是为什么有时Solidity会报Stack too deep
错误。
2. 内存 Memory
堆栈虽然计算高效,但是存储能力有限,
因此EVM使用内存来支持交易执行期间的数据存储和读取。
EVM的内存是一个线性寻址存储器,可以把它理解为一个动态字节数组,可以根据需要动态扩展。
它支持以8或256 bit写入(MSTORE8
/MSTORE
),但只支持以256 bit读取(MLOAD
)。
EVM的内存是“易失性”的:交易开始时,所有内存位置的值均为0;
易执行期间,值被更新;
交易结束时,内存中的所有数据都会被清除,不会被持久化。
3. 存储 Storage
EVM的账户存储(Account Storage)是一种映射(mapping,键值对存储),每个键和值都是256 bit的数据,它支持256 bit的读和写。这种存储在每个合约账户上都存在,并且是持久的,它的数据会保持在区块链上,直到被明确地修改。
对存储的读取(SLOAD
)和写入(SSTORE
)都需要gas,并且比内存操作更昂贵。这样设计可以防止滥用存储资源,因为所有的存储数据都需要在每个以太坊节点上保存。
4. EVM 字节码
Solidity智能合约会被编译为EVM字节码,然后才能在EVM上运行。
这个字节码是由一系列的Opcodes组成的,通常表现为一串十六进制的数字。
EVM字节码在执行的时候,会按照顺序一个一个地读取并执行每个Opcode。
例如,字节码6001600101
可以被解码为:
1 | PUSH1 0x01 |
这段Opcodes的含义是将两个1相加,得到结果2。
5. Gas
EVM的每笔交易计算都是通过opcodes计算的,以太坊规定了每个opcode的Gas消耗,复杂度越高的opcodes消耗越多gas
比如:
1 | ADD操作消耗3 gas |
一笔交易的gas消耗等于其中所有opcodes的gas成本总和。
当你调用一个合约函数时,你需要预估这个函数执行所需要的Gas,并在交易中提供足够的Gas。如果提供的Gas不够,那么函数执行会在中途停止,已经消耗的Gas不会退回。
6. 执行模型
所有以上的串联起来,就是EVM的执行模型。
- 当一个交易被接收并准备执行时,以太坊会初始化一个新的执行环境并加载合约的字节码。
- 字节码被翻译成Opcode,被逐一执行。每个Opcodes代表一种操作,比如算术运算、逻辑运算、存储操作或者跳转到其他操作码。
- 每执行一个Opcodes,都要消耗一定数量的Gas。如果Gas耗尽或者执行出错,执行就会立即停止,所有的状态改变(除了已经消耗的Gas)都会被回滚。
- 执行完成后,交易的结果会被记录在区块链上,包括Gas的消耗、交易日志等信息。
Opcodes
evm.codes
EVM Codes - An Ethereum Virtual Machine Opcodes Interactive Reference
在这里找到完整的Opcodes列表。
还提供了在线Opcodes https://www.evm.codes/playground
编写示例:
用Opcodes编写一个简单的程序,这个程序将在堆栈中计算1+1,并将结果保存到内存中。代码如下:
1 | PUSH1 0x01 |
第1-2行:
PUSH1
指令将一个长度为1字节的数据压入堆栈顶部。1
2
3
4PUSH1 0x01
// stack: [1]
PUSH1 0x01
// stack: [1, 1]第3行:
ADD
指令会弹出堆栈顶部的两个元素,计算它们的和,然后将结果压入堆栈。1
2ADD
// stack: [2]第4行:
PUSH0
指令将0压入堆栈。1
2PUSH0
// stack: [0, 2]第5行:
MSTORE
属于内存指令,它会弹出堆栈顶的两个数据[offset, value]
(偏移量和值),然后将value
(长度为32字节)保存到内存索引(偏移量)为offset
的位置。1
2
3MSTORE
// stack: []
// memory: [0: 2]
在evm.codes中验证执行过程和结果。(可以点右上的箭头进行debug)
分类简介
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,可以执行PUSH
和POP
指令。
程序计数器
在EVM中,程序计数器(通常缩写为 PC)是一个用于跟踪当前执行指令位置的寄存器。每执行一条指令(opcode),程序计数器的值会自动增加,以指向下一个待执行的指令。但是,这个过程并不总是线性的,在执行跳转指令(JUMP
和JUMPI
)时,程序计数器会被设置为新的值。
使用Python创建一个简单的EVM程序计数器:
1 | class EVM: |
它的功能就是利用程序计数器遍历字节码中的opcode
堆栈指令
EVM是基于堆栈的,堆栈遵循 LIFO(后入先出)原则,最后一个被放入堆栈的元素将是第一个被取出的元素。PUSH和POP指令就是用来操作堆栈的。
PUSH(压入)
在EVM中,PUSH是一系列操作符,共有32个(在以太坊上海升级前),从PUSH1
,PUSH2
,一直到PUSH32
,操作码范围为0x60
到0x7F
。它们将一个字节大小为1到32字节的值从字节码压入堆栈(堆栈中每个元素的长度为32字节),每种指令的gas消耗都是3。
以PUSH1
为例,它的操作码为0x60
,它会将字节码中的下一个字节压入堆栈。例如,字节码0x6001
就表示把0x01
压入堆栈。PUSH2
就是将字节码中的下两个字节压入堆栈,例如,0x610101
就是把0x0101
压入堆栈。其他的PUSH指令类似。
python实现PUSH0
到PUSH32
1 | PUSH0 = 0x5F |
字节码0x60016001 在evm.codes上验证时候(注意要把字节码开头的0x去掉)
POP(移除)
POP
指令(操作码0x50
,gas消耗2
)用于移除栈顶元素;如果当前堆栈为空,就抛出一个异常。
示例:
1 | PUSH0 = 0x5F |
算数指令
ADD (加法)
ADD
指令从堆栈中弹出两个元素,将它们相加,然后将结果推入堆栈。如果堆栈元素不足两个,那么会抛出异常。这个指令的操作码是0x01
,gas消耗为3
。
可以将ADD
指令的实现添加到我们的EVM模拟器中:
1 | def add(self): |
在run()
函数中添加对ADD
指令的处理:
1 | def run(self): |
完整为:
1 | PUSH0 = 0x5F |
MUL (乘法)
MUL
指令将堆栈的顶部两个元素相乘。操作码是0x02
,gas消耗为5
。
实现到EVM模拟器中:
1 | def mul(self): |
完整代码如下:
1 | PUSH0 = 0x5F |
SUB (减法)
SUB
指令从堆栈顶部弹出两个元素,然后计算第二个元素减去第一个元素,最后将结果推入堆栈。这个指令的操作码是0x03
,gas消耗为3
。
1 | def sub(self): |
在run中添加
1 | def run(self): |
同时记得定义:
1 | MUL = 0x02 |
后文中方法所代常量将不再说明
1 | code = b"\x60\x03\x60\x02\x03" #包含SUB指令的字节码:0x6002600303(PUSH1 2 PUSH1 3 SUB)这个字节码将2和3推入堆栈,然后将它们相减 |
DIV (除法)
DIV
指令从堆栈顶部弹出两个元素,然后将第二个元素除以第一个元素,最后将结果推入堆栈。如果第一个元素(除数)为0,则将0推入堆栈。这个指令的操作码是0x04
,gas消耗为5
。
1 | def div(self): |
在run中定义
1 | def run(self): |
运行一个包含DIV
指令的字节码:0x6002600304
(PUSH1 2 PUSH1 3 DIV)。这个字节码将2
和3
推入堆栈,然后将它们相除。
1 | code = b"\x60\x06\x60\x03\x04" |
其他算数指令(SDIV,MOD,SMOD,ADDMOD,MULMOD,EXP,SIGNEXTEND)
SDIV: 带符号整数的除法指令。与DIV
类似,这个指令会从堆栈中弹出两个元素,然后将第二个元素除以第一个元素,结果带有符号。如果第一个元素(除数)为0,结果为0。它的操作码是0x05
,gas消耗为5。要注意,EVM字节码中的负数是用二进制补码(two’s complement)形式,比如-1
表示为0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
,它加一等于0。
1 | def sdiv(self): |
MOD: 取模指令。这个指令会从堆栈中弹出两个元素,然后将第二个元素除以第一个元素的余数推入堆栈。如果第一个元素(除数)为0,结果为0。它的操作码是0x06
,gas消耗为5。
1 | def mod(self): |
SMOD: 带符号的取模指令。这个指令会从堆栈中弹出两个元素,然后将第二个元素除以第一个元素的余数推入堆栈,结果带有第二个元素的符号。如果第一个元素(除数)为0,结果为0。它的操作码是0x07
,gas消耗为5。
1 | def smod(self): |
ADDMOD: 模加法指令。这个指令会从堆栈中弹出三个元素,将前两个元素相加,然后对第三个元素取模,将结果推入堆栈。如果第三个元素(模数)为0,结果为0。它的操作码是0x08
,gas消耗为8。
1 | def addmod(self): |
MULMOD: 模乘法指令。这个指令会从堆栈中弹出三个元素,将前两个元素相乘,然后对第三个元素取模,将结果推入堆栈。如果第三个元素(模数)为0,结果为0。它的操作码是0x09
,gas消耗为5。
1 | def mulmod(self): |
EXP: 指数运算指令。这个指令会从堆栈中弹出两个元素,将第二个元素作为底数,第一个元素作为指数,进行指数运算,然后将结果推入堆栈。它的操作码是0x0A
,gas消耗为10。
1 | def exp(self): |
SIGNEXTEND: 符号位扩展指令,即在保留数字的符号(正负性)及数值的情况下,增加二进制数字位数的操作。举个例子,若计算机使用8位二进制数表示数字“0000 1010”,且此数字需要将字长符号扩充至16位,则扩充后的值为“0000 0000 0000 1010”。此时,数值与符号均保留了下来。SIGNEXTEND
指令会从堆栈中弹出两个元素,对第二个元素进行符号扩展,扩展的位数由第一个元素决定,然后将结果推入堆栈。它的操作码是0x0B
,gas消耗为5。
1 | def signextend(self): |
比较指令
EVM中用于比较运算的6个指令,包括LT
(小于),GT
(大于),和EQ
(相等)
LT (小于)
LT
指令从堆栈中弹出两个元素,比较第二个元素是否小于第一个元素。如果是,那么将1
推入堆栈,否则将0
推入堆栈。如果堆栈元素不足两个,那么会抛出异常。这个指令的操作码是0x10
,gas消耗为3
。
1 | def lt(self): |
run()函数中
1 | def run(self): |
包含LT
指令的字节码:0x6002600310
(PUSH1 2 PUSH1 3 LT)。这个字节码将2
和3
推入堆栈,然后比较2
是否小于3
。
1 | code = b"\x60\x02\x60\x03\x10" |
GT (大于)
GT
指令和LT
指令非常类似,不过它比较的是第二个元素是否大于第一个元素。操作码是0x11
,gas消耗为3
。
1 | def gt(self): |
1 | def run(self): |
包含GT
指令的字节码:0x6002600311
(PUSH1 2 PUSH1 3 GT)。这个字节码将2
和3
推入堆栈,然后比较2
是否大于3
。
1 | code = b"\x60\x02\x60\x03\x11" |
EQ (等于)
EQ
指令从堆栈中弹出两个元素,如果两个元素相等,那么将1
推入堆栈,否则将0
推入堆栈。该指令的操作码是0x14
,gas消耗为3
。
1 | def eq(self): |
run()
1 | elif op == EQ: |
运行一个包含EQ
指令的字节码:0x6002600314
(PUSH1 2 PUSH1 3 EQ)。这个字节码将2
和3
推入堆栈,然后比较两者是否相等。
1 | code = b"\x60\x02\x60\x03\x14" |
ISZERO (是否为零)
ISZERO
指令从堆栈中弹出一个元素,如果元素为0,那么将1
推入堆栈,否则将0
推入堆栈。该指令的操作码是0x15
,gas消耗为3
。
1 | def iszero(self): |
run()
函数
1 | elif op == ISZERO: |
运行一个包含ISZERO
指令的字节码:0x600015
(PUSH1 0 ISZERO)。这个字节码将0
推入堆栈,然后检查其是否为0。
1 | code = b"\x60\x00\x15" |
其他比较指令
SLT (有符号小于): 这个指令会从堆栈中弹出两个元素,然后比较第二个元素是否小于第一个元素,结果以有符号整数形式返回。如果第二个元素小于第一个元素,将1
推入堆栈,否则将0
推入堆栈。它的操作码是0x12
,gas消耗为3
。
1 | def slt(self): |
SGT (有符号大于): 这个指令会从堆栈中弹出两个元素,然后比较第二个元素是否大于第一个元素,结果以有符号整数形式返回。如果第二个元素大于第一个元素,将1
推入堆栈,否则将0
推入堆栈。它的操作码是0x13
,gas消耗为3
。
1 | def sgt(self): |
位级指令
AND (与)
AND
指令从堆栈中弹出两个元素,对它们进行位与运算,并将结果推入堆栈。操作码是0x16
,gas消耗为3
。
1 | def and_op(self): |
run()
函数
1 | def run(self): |
尝试运行一个包含AND
指令的字节码:0x6002600316
(PUSH1 2 PUSH1 3 AND)。这个字节码将2
(0000 0010)和3
(0000 0011)推入堆栈,然后进行位级与运算,结果应该为2
(0000 0010)
1 | code = b"\x60\x02\x60\x03\x16" |
OR (或)
OR
指令与AND
指令类似,但执行的是位或运算。操作码是0x17
,gas消耗为3
。
1 | def or_op(self): |
我们在run()
函数中添加对OR
指令的处理:
1 | def run(self): |
尝试运行一个包含OR
指令的字节码:0x6002600317
(PUSH1 2 PUSH1 3 OR)。这个字节码将2
(0000 0010)和3
(0000 0011)推入堆栈,然后进行位级与运算,结果应该为3
(0000 0011)。
1 | code = b"\x60\x02\x60\x03\x17" |
XOR (异或)
XOR
指令与AND
和OR
指令类似,但执行的是异或运算。操作码是0x18
,gas消耗为3
。
1 | def xor_op(self): |
1 | def run(self): |
尝试运行一个包含XOR
指令的字节码:0x6002600318
(PUSH1 2 PUSH1 3 XOR)。这个字节码将2
(0000 0010)和3
(0000 0011)推入堆栈,然后进行位级与运算,结果应该为1
(0000 0001)。
1 | code = b"\x60\x02\x60\x03\x18" |
NOT
NOT
指令执行按位非操作,取栈顶元素的补码,然后将结果推回栈顶。它的操作码是0x19
,gas消耗为3
。
1 | def not_op(self): |
在run()
函数中添加对NOT
指令的处理:
1 | elif op == NOT: # 处理NOT指令 |
尝试运行一个包含NOT
指令的字节码:0x600219
(PUSH1 2 NOT)。这个字节码将2
(0000 0010)推入堆栈,然后进行位级非运算,结果应该为很大的数
(0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd)。
1 | # NOT |
SHL
SHL
指令执行左移位操作,从堆栈中弹出两个元素,将第二个元素左移第一个元素位数,然后将结果推回栈顶。它的操作码是0x1B
,gas消耗为3
。
1 | def shl(self): |
run()
1 | elif op == SHL: # 处理SHL指令 |
运行一个包含XOR
指令的字节码:0x600260031B
(PUSH1 2 PUSH1 3 SHL)。这个字节码将2
(0000 0010)和3
(0000 0011)推入堆栈,然后将2
左移3
位,结果应该为16
(0001 0000)。
1 | code = b"\x60\x02\x60\x03\x1B" |
SHR
SHR
指令执行右移位操作,从堆栈中弹出两个元素,将第二个元素右移第一个元素位数,然后将结果推回栈顶。它的操作码是0x1C
,gas消耗为3
。
1 | def shr(self): |
run()
1 | elif op == SHR: # 处理SHR指令 |
尝试运行一个包含XOR
指令的字节码:0x601060031C
(PUSH1 16 PUSH1 3 SHL)。这个字节码将16
(0001 0000)和3
(0000 0011)推入堆栈,然后将16
右移3
位,结果应该为2
(0000 0010)。
1 | code = b"\x60\x10\x60\x03\x1C" |
其他位级指令(BYTE,SAR)
BYTE: BYTE
指令从堆栈中弹出两个元素(a
和b
),将第二个元素(b
)看作一个字节数组,并返回该字节数组中第一个元素指定索引的字节(b[a]
),并压入堆栈。如果索引大于或等于字节数组的长度,则返回0
。操作码是0x1a
,gas消耗为3
。
1 | def byte_op(self): |
SAR: SAR
指令执行算术右移位操作,与SHR
类似,但考虑符号位:如果我们对一个负数进行算术右移,那么在右移的过程中,最左侧(符号位)会被填充F
以保持数字的负值。它从堆栈中弹出两个元素,将第二个元素以符号位填充的方式右移第一个元素位数,然后将结果推回栈顶。它的操作码是0x1D
。由于Python的>>
操作符已经是算术右移,我们可以直接复用shr
函数的代码。
1 | def sar(self): |
内存指令
内存的读写比存储(Storage)的读写要便宜的多,每次读写有固定费用3 gas,另外如果首次访问了新的内存位置(内存拓展),则需要付额外的费用(由当前偏移量和历史最大偏移量决定),计算方法见链接。
EVM中的内存
EVM的内存,它是一个线性寻址存储器,类似一个动态的字节数组,可以根据需求动态扩展。它的另一个特点就是易失性,交易结束时所有数据都会被清零。它支持以8或256 bit写入(MSTORE8
/MSTORE
),但只支持以256 bit取(MLOAD
)。
我们可以用Python内置的bytearray
来代表内存:
1 | def __init__(self, code): |
MSTORE (内存写)
MSTORE
指令用于将一个256位(32字节)的值存储到内存中。它从堆栈中弹出两个元素,第一个元素为内存的地址(偏移量 offset),第二个元素为存储的值(value)。操作码是0x52
,gas消耗根据实际内存使用情况计算(3+X)。
1 | def mstore(self): |
在run()
函数中
1 | def run(self): |
尝试运行一个包含MSTORE
指令的字节码:0x6002602052
(PUSH1 2 PUSH1 0x20 MSTORE)。这个字节码将2
和0x20
(32)推入堆栈,然后进行MSTORE
,将2
存到偏移量为0x20
的地方。
1 | # MSTORE |
MSTORE8 (内存8位写)
MSTORE8
指令用于将一个8位(1字节)的值存储到内存中。与MSTORE
类似,但只使用最低8位。操作码是0x53
,gas消耗根据实际内存使用情况计算(3+X)。
1 | def mstore8(self): |
run()
函数中
1 | elif op == MSTORE8: # 处理MSTORE8指令 |
尝试运行一个包含MSTORE8
指令的字节码:0x6002602053
(PUSH1 2 PUSH1 0x20 MSTORE8)。这个字节码将2
和0x20
(32)推入堆栈,然后进行MSTORE8
,将2
存到偏移量为0x20
的地方。
1 | # MSTORE8 |
为什么是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 | def mload(self): |
尝试运行一个包含MLOAD
指令的字节码:0x6002602052602051
(PUSH1 2 PUSH1 0x20 MSTORE PUSH1 0x20 MLOAD)。这个字节码将2
和0x20
(32)推入堆栈,然后进行MSTORE
,将2
存到偏移量为0x20
的地方;然后将0x20
推入堆栈,然后进行MLOAD
,将刚才存储在内存的值读取出来。
1 | # MSTORE |
MSIZE (内存大小)
MSIZE
指令将当前的内存大小(以字节为单位)压入堆栈。操作码是0x59
,gas消耗为2。
1 | def msize(self): |
存储指令
EVM的存储,和内存不同,它是一种持久化存储空间,存在存储中的数据在交易之间可以保持。它是EVM的状态存储的一部分,支持以256 bit为单位的读写。
由于存储使用键值对存储数据,每个键和值都是256 bit,因此我们可以用Python内置的dict
(字典)来代表存储:
1 | def __init__(self, code): |
SSTORE (存储写)
SSTORE
指令用于将一个256位(32字节)的值写入到存储。
它从堆栈中弹出两个元素,第一个元素为存储的地址(key),第二个元素为存储的值(value)。操作码是0x55
,gas消耗根据实际改变的数据计算(下面给出)。
1 | def sstore(self): |
run中
1 | def run(self): |
运行一个包含SSTORE
指令的字节码:0x6002600055
(PUSH1 2 PUSH1 0 SSTORE)。这个字节码将2
和0
推入堆栈,然后进行SSTORE
,将2
存到键为0x0
的存储槽。
1 | # SSTORE |
为什么输出的是{0: 2}而不是{2: 0} 因为栈是只能栈顶,也就是所谓的“上层”,最后被压入的是栈顶 所以0x00为栈顶
SLOAD (存储读)
SLOAD
指令从存储中读取一个256位(32字节)的值并推入堆栈。
从堆栈中弹出一个元素,从该元素表示的存储槽中加载值,并将其推入堆栈。操作码是0x54
。
1 | def sload(self): |
run()
函数
1 | elif op == SLOAD: |
尝试运行一个包含SLOAD
指令的字节码:0x6002600055600054
(PUSH1 2 PUSH1 0 SSTORE PUSH1 0 SLOAD)。这个字节码将2
和0
推入堆栈,然后进行SSTORE
,将2
存到键为0
的地方;然后将0
推入堆栈,然后进行SLOAD
,将刚才写入0x0
存储槽的值读取出来。
1 | # SLOAD |
访问集 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