Q1ngying

今朝梦醒与君别,遥盼春风寄相思

0%

EVM_Opcodes(2)

EVM Opcodes(2)

原文章:https://www.wtf.academy/evm-opcodes-101

控制流指令

EVM 中的控制流

EVM 的控制流是由跳转指令JUMPJUMPIJUMPDEST)控制 PC (程序计数器)指向新的指令位置而实现的,这允许合约进行条件执行和循环执行。

STOP(停止)

  • 操作码:0x00
  • gas 消耗:0

STOP是 EVM 的停止指令,它的作用是停止当前上下文的执行。并成功退出

STOP操作码设置为0x00有一个好处:当一个调用被执行到一个没有代码的地址(EOA),并且 EVM 尝试读取代码数据时,系统会返回一个默认值 0,这个默认值对应的指令就是STOP指令,程序就会停止执行

JUMPDEST(跳转目标)

  • 操作码:0x5b
  • gas 消耗:1

JUMPDEST指令标记一个有效的跳转目标位置,不然无法使用JUMPJUMPI进行跳转。

但是0x5b有时候会作为PUSH的参数(详情可看黄皮书中的9.4.3. Jump Destination Validity),所以需要在运行代码前,筛选字节码中有效的JUMPDEST指令,使用ValidJumpDest来存储有效的JUMPDEST指令所在位置。

JUMP(跳转)

  • 操作码:0x56
  • gas 消耗:8

JUMP指令用于无条件跳转到一个新的程序计数器位置。他从堆栈中弹出一个元素,将这个元素设定为新的程序计数器(pc)的值。

举例:

0x600456005B(PUSH1 4 JUMP STOP JUMPDEST)。这段字节码将4推入堆栈,然后进行JUMP,跳转到pc = 4的位置,该位置正好是JUMPDEST指令,跳转成功,程序没有被STOP指令中断。

JUMPI(条件跳转)

  • 操作码:0x57
  • gas 消耗:10

JUMPI指令用于条件跳转,他从堆栈中弹出两个元素,如果第二个元素(条件,condition)不为 0,那么将第一个元素(目标,destination)设定位新的pc

举例:

0x6001600657005B(PUSH1 1 PUSH1 6 JUMPI STOP JUMPDEST)。这个字节码将16推入堆栈,然后进行JUMPI,由于条件不为0。执行跳转到pc = 6的位置,该位置正好是JUMPDEST指令,跳转成功,程序没有被STOP指令中断。

PC(程序计数器)

  • 操作码:0x58
  • gas 消耗:2

PC指令将当前的程序计数器(PC)的值压入到堆栈。

区块信息指令

区块信息

在编写智能合约时,经常会用到区块链信息,比如生成伪随机数时常用的blockhashblock.numberblock.timestamp

EVM 提供了一系列指令让智能合约访问当前或历史区块信息,包括区块哈希、时间戳、coinbase等。

区块信息指令

  1. BLOCKHASH

    • 操作码:0x40
    • gas 消耗:20

    查询特定区块(最近的 256 个区块,不包括当前区块)的 hash。从堆栈中弹出一个值作为区块高度(block number),然后将该区块的 hash 压入堆栈。(如果不属于最近的 256 个区块,则返回 0。)(可用使用NUMBER指令来查询当前区块高度)

  2. COINBASE

    • 操作码:0x41
    • gas 消耗:2

    将当前区块的coinbase(矿工/受益人)地址压入堆栈。

  3. TIMESTAMP

    • 操作码:0x42
    • gas 消耗:2

    将当前区块的时间戳压入堆栈

  4. NUMBER

    • 操作码:0x43
    • gas 消耗:2

    将当前区块高度压入堆栈

  5. PREVRANDAO

    • 操作码:0x44
    • gas 消耗:2

    替代了原先的DIFFICULTY(0x44)操作码,其返回值为 beacon 链随机性信标的输出。此变更允许智能合约在以太坊转向权益证明(PoS)后继续从原本的DIFFICULTY操作码处获得随机性。

  6. GASLIMIT

    • 操作码:0x45
    • gas 消耗:2

    将当前区块的 gas 限制压入堆栈。

  7. CHAINID

    • 操作码:0x46
    • gas 消耗:2

    将当前的链ID压入堆栈

  8. SELFBALANCE

    • 操作码:0x47
    • gas 消耗:2

    将合约的当前余额压入堆栈

  9. BASEFEE

    • 操作码:0x48
    • gas 消耗:2

    将当前区块的基础费(base fee)压入堆栈。

SHA3 指令

  • 操作码:0x20
  • gas 消耗:30 + 6 * 数据的字节长度 + 拓展内存成本

SHA3(offset, size)指令从堆栈中取出两个参数,起始位置offset和长度size(以字节为单位),然后它从内存中读取起始位置offset开始的size长度的数据,计算这段数据的Keccak-256哈希,并将结果(一个32字节的值)压入堆栈

示例:

0x5F5F20 (PUSH0 PUSH0 SHA3)。这个字节码将两个0推入堆栈,然后用SHA3指令计算0的哈希。

image-20231125164625289

账户指令

以太坊账户结构

以太坊上的账户分为两类:

  • 外部账户(Externally Owned Accounts,EOA)
    • 以太坊网络上的代表,他们可以拥有ETH,发送交易并与合约互动
  • 合约账户(Contract Account)
    • 存储和执行智能合约代码的实体,它们也可以拥有和发送 ETH,但不能主动发起交易

img

以太坊上的账户结构非常简单,可以理解为地址到账户状态的映射。 账户地址是 20 字节(160 位)的数据,可以用 40 位的 16 进制表示,比如0x9bbfed6889322e016e0a02ee459d306fc19545d8。而账户的状态(State)具有 4 中属性:

  • Balance:这是账户持有的原生代币(Native Token)数量,用 Wei 表示(1 ETH = 10 ^ 18 Wei)
  • Nonce:对于 EOA 账户,这是该账户发送的交易数。对于合约账户,它是该账户创建的合约数量。
  • Storage:每个合约账户都有与之关联的存储空间,其中包含状态变量的值
  • Code:合约账户的字节码

也就是说,只有合约账户拥有StorageCode,EOA 没有。

BALANCE

  • 操作码:0x31
  • gas 消耗:2600(cold address)或 100(warm address)

BALANCE指令用于返回某个账户的余额。从堆栈中弹出一个地址,然后查询该地址的余额并压入堆栈。

示例:

739bbfed6889322e016e0a02ee459d306fc19545d831(PUSH20 9bbfed6889322e016e0a02ee459d306fc19545d8 BALANCE)。这个字节码使用PUSH20将一个地址推入堆栈,然后使用BALANCE指令查询该地址的余额。

image-20231125170424202

EXTCODESIZE

  • 操作码:0x3B
  • gas 消耗:2600(cold address) 或 100(warm address)

EXTCODESIZE指令用于返回某个账户的代码长度(以字节为单位)。他从堆栈弹出一个地址,然后查询该地址的代码长度并压入堆栈。如果该账户不存在或者没有代码,则返回 0 。

示例:

739bbfed6889322e016e0a02ee459d306fc19545d83B(PUSH20 9bbfed6889322e016e0a02ee459d306fc19545d8 EXTCODESIZE)。这个字节码使用PUSH20将一个地址压入堆栈,然后使用EXTCODESIZE指令查询该地址的代码长度。

image-20231125170821422

EXTCODECOPY

  • 操作码:0x3C
  • gas 消耗:由读取代码长度、内存拓展成本、地址是否为 cold 三部分决定

EXTCODECOPY指令用于将某个账户的代码部分复制到 EVM 内存中。从堆栈中弹出 4 个参数(addr, mem_offset, code_offset, length),分别对应要查询的地址,写到内存的偏移量,读取代码的偏移量和长度。

示例:

60045F5F739bbfed6889322e016e0a02ee459d306fc19545d83C(PUSH1 4 PUSH0 PUSH0 PUSH20 9bbfed6889322e016e0a02ee459d306fc19545d8 EXTCODECOPY)。这个字节码将 4 (length),0(code_offset),0(mem_offset),地址(addr)分别推入堆栈,然后使用EXTCODECOPY指令将代码复制到内存中。

image-20231125172210366

EXTCODEHASH

  • 操作码:0x3F
  • gas 消耗:2600(cold address) or 100 (warm address)

EXTCODEHASH指令返回某个账户的代码的 Keccak256 哈希值。他从堆栈中弹出一个地址,然后查询该地址代码的哈希并压入堆栈。

示例:

739bbfed6889322e016e0a02ee459d306fc19545d83F(PUSH20 9bbfed6889322e016e0a02ee459d306fc19545d8 EXTCODEHASH)。这个字节码使用PUSH20将一个地址推入堆栈,然后使用EXTCODEHASH指令查询该地址的代码哈希。

image-20231125172615447

交易指令

交易的基本结构

img

再深入学习这些指令之前,我们先了解以太坊交易的基本结构。每一笔以太坊交易都包含以下属性:

  • nonce:一个与发送者账户相关的数字,表示该账户已发送的交易数
  • gasPrice:交易发送者愿意支付的单位 gas 价格。
  • gaslimit:交易发送者为这次交易分配的最大 gas 数量
  • to:交易的接收者地址。当交易为合约创建时,这一字段为空
  • value:以 wei 为单位的发送金额
  • data:附带的数据,通常为合约调用的输入数据(calldata)和新合约的初始代码(initcode)。

img

  • v, r, s:与交易签名相关的三个值

交易指令

ADDRESS

  • 操作码:0x30
  • gas 消耗:2
  • 功能:将当前执行合约的地址压入堆栈。
  • 使用场景:当合约需要知道自己的地址时使用

ORIGIN

  • 操作码:0x32
  • gas 消耗:2
  • 功能:将交易的原始发送者(即签名者)地址压入堆栈。
  • 使用场景:区分合约调用者与交易发起者

CALLER

  • 操作码:0x33
  • gas 消耗:2
  • 功能:将直接调用当前合约的地址压入堆栈
  • 使用场景:当合约需要知道是谁调用了它的时候

CALLVALUE

  • 操作码:0x34
  • gas 消耗:2
  • 功能:将发送给合约的 ether 的数量(以 wei 为单位)压入堆栈
  • 使用场景:当合约需要知道有多少以太币被发送时使用

CALLDATALOAD

  • 操作码:0x35
  • gas 消耗:3
  • 功能:从交易或合约调用的data字段加载数据。他从堆栈中弹出 calldata 的偏移量(offset),然后从 calldata 的offset 位置读取 32 字节的数据并压入堆栈。如果 calldata 剩余不足 32 字节,则补 0。
  • 使用场景:读取传入的数据。

CALLDATASIZE

  • 操作码:0x36
  • gas 消耗:2
  • 功能:获取交易的合约调用的data字段的字节长度,并压入堆栈。
  • 使用场景:在读取数据之前检查大小

CALLDATACOPY

  • 操作码:0x37
  • gas 消耗:3 + 3 * 数据长度 + 内存拓展成本
  • 功能:将data中的数据复制到内存中。他会从堆栈中弹出三个参数(mem_offset, calldata_offset, length),分别对应写到内存的偏移量,读取 calldata 的偏移量和长度。
  • 使用场景:将输入数据复制到内存

CODESIZE

  • 操作码:0x38
  • gas 消耗:2
  • 功能:获取当前合约代码的字节长度,然后压入堆栈
  • 使用场景:当合约需要访问自己的字节码时使用

CODECOPY

  • 操作码:0x39
  • gas 消耗:3 + 3 * 数据长度 + 内存拓展成本
  • 功能:复制合约的代码到 EVM 的内存中。他从堆栈中弹出三个参数:目标内存的开始偏移量(mem_offset)、代码的开始偏移量(code_offset)以及要复制的长度(length
  • 使用场景:当合约需要读取自己的部分字节码时使用

GASPRICE

  • 操作码:0x4A
  • gas 消耗:2
  • 功能:获取交易的 gas 价格,并压入堆栈。
  • 使用场景:当合约需要知道当前的 gas 价格时使用

Log 指令

EVM 中的日志和事件

在 Solidity 中,我们常常使用event来定义和触发事件。当这些事件被触发时,他们会生成日志,将数据永远存储在区块链上。日志分为主题(topic)和数据(data。第一个主题通常是事件签名的哈希值,后面的主题是由indexed修饰的事件参数。

EVM 中的LOG指令用于创建这些日志。指令从LOG0LOG4的区别在于它们包含的主题数量。例如,LOG0没有主题,而LOG4有四个主题。

LOG 指令

  • 操作码:A0A4
  • gas 消耗:gas = 375 + 375 * topic 数量 + 内存拓展成本

EVM 中有五个 LOG 指令:LOG0LOG1LOG2LOG3LOG4。他们的主要区别在于携带的主题数(topics):LOG0没有主题,LOG4有四个主题。

LOG指令从堆栈中弹出 2 + n 个元素。其中前两个参数是内存开始位置(mem_offset)和数据长度length,n 是主题的数量(取决于具体的LOG指令)。所有对于LOG1,我们会从堆栈中弹出 3 个元素:内存开始位置,数据长度,和一个主题。需要mem_offset的原因是日志的数据(data)数据存储在内存中,gas 消耗低,而主题(topic)部分直接存储在堆栈上。

示例:

  • **测试LOG0**:运行一个包含LOG0的字节码:60aa6000526001601fa0(PUSH1 aa PUSH1 0 MSTORE PUSH1 1 PUSH1 1f LOG0)。这个字节码将aa存在内存中,然后使用LOG0指令将aa输出到日志的数据部分。

    image-20231127142711708

  • 测试LOG1运行一个包含LOG1指令的字节码:60aa60005260116001601fa1(PUSH1 aa PUSH1 0 MSTORE PUSH 11 PUSH1 1 PUSH1 1f LOG1)。这个字节码将aa存在内存,然后将11压入堆栈,最后使用LOG1指令将aa输出到日志的数据部分,将11输出到日志的主题部分。

    image-20231127143239025