Skip to main content

简介


Opcodes(操作码)是以太坊智能合约的基本单元。

一般来说智能合约会先被编译成字节码,然后才能在EVM上运行。而字节码就是由一系列Opcodes组成。当用户在EVM中调用这个智能合约的函数时,EVM就会解析并且执行这些Opcodes,以实现合约逻辑。

例如:

  • PUSH1:将一个字节的数据压入堆栈。例如:PUSH1 0x60 就是将0x60压入堆栈。
  • DUP1:复制堆栈顶部的一个元素
  • SWAP1: 交换堆栈顶部的前两个元素。

EVM基础

由于Opcodes直接操作EVM的资源,比如堆栈、内存、存储,因此了解EVM基础很重要。

堆栈 Stack

EVM基于堆栈,所以EVM处理数据的方式是使用对战数据结构进行大多数的计算。堆栈是一种后进先出的数据结构。

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

Memory

堆栈虽然高效,但是存储能力有限,为此EVM使用内存来支持交易执行期间的数据存取和读取。EVM的内存是一个线性寻址存储器,末种意义上是一个动态字节数组,可以根据需要动态拓展。支持以8或者256 bit写入(MSTORE8/MSTORE),but only support load by 256bit (MLOAD).

Remember , The memory in EVM is “易失性”的. 当交易开始的时候,所有内存位置的值都是0,交易执行期间,值被更新,交易结束时候,内存中所有数据都会被清除,不会被持久化报错,如果需要永久的保存数据,就需要使用EVM的存储。

Storage

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

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

EVM字节码

Solidity智能合约会被编译为EVM字节码,然后才能在EVM上运行。这个字节码是由一系列的Opcodes组成的,通常表现为一串十六进制的数字。EVM字节码在执行的时候,会按照顺序一个一个地读取并执行每个Opcode。 eg: 字节码6001600101

PUSH1 0x01
PUSH1 0x01
ADD

Gas

Gas是以太坊中执行交易和运行合约的"燃料"。每个交易或合约调用都需要消耗一定数量的Gas,这个数量取决于它们进行的计算的复杂性和数据存储的大小。

EVM上每笔交易的gas是如何计算的呢?其实是通过opcodes。以太坊规定了每个opcode的gas消耗,复杂度越高的opcodes消耗越多的gas,比如:

  • ADD操作消耗3 gas
  • SSTORE操作消耗20000 gas
  • SLOAD操作消耗200 Gas

一笔交易的gas消耗等于其中所有opcodes的gas成本总和。当你调用一个合约函数时,你需要预估这个函数执行所需要的Gas,并在交易中提供足够的Gas。如果提供的Gas不够,那么函数执行会在中途停止,已经消耗的Gas不会退回。 ![[Pasted image 20240105145627.png]]

Excute

  1. 当一个交易被接收并准备执行时,以太坊会初始化一个新的执行环境并加载合约的字节码。

  2. 字节码被翻译成Opcode,被逐一执行。每个Opcodes代表一种操作,比如算术运算、逻辑运算、存储操作或者跳转到其他操作码。

  3. 每执行一个Opcodes,都要消耗一定数量的Gas。如果Gas耗尽或者执行出错,执行就会立即停止,所有的状态改变(除了已经消耗的Gas)都会被回滚。

  4. 执行完成后,交易的结果会被记录在区块链上,包括Gas的消耗、交易日志等信息。

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)。

官方文档:https://www.evm.codes/?fork=shanghai

Example

PUSH1 0x02
PUSH1 0x03
ADD
PUSH0
MSTORE
  1. 首先PUSH1,指令将一个长度为1字节的数据压入了堆栈顶部
PUSH1 0x01
// stack: [1]
PUSH1 0x01
// stack: [1, 1]
  1. ADD指令会弹出堆栈顶部的两个元素,计算他们的和,然后再将结果压入堆栈
ADD
// stack: [2]
  1. PUSH0将0压入堆栈
PUSH0
// stack: [0, 2]
  1. MSTORE属于内存指令,MSTORE 指令需要两个参数:一个是要存储的值,另一个是存储的内存地址。也就是它会弹出堆栈顶的两个数据 [offset, value](偏移量和值),然后将value(长度为32字节)保存到内存索引(偏移量)为offset的位置。
MSTORE
// stack: []
// memory: [0: 2]

堆栈指令

程序计数器

在EVM中,程序技术器,是一个用于跟踪当前执行指令位置的寄存器。每执行一条指令,程序计数器的值就会自动增加,以指向下一条待执行的指令。但这个过程不总是线性的,在执行跳转指令的时候,程序计数器会被设置为新的值

堆栈指令

EVM是基于堆栈的,堆栈遵循LIFO的原则。

PUSH

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

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

以太坊上海升级新加入了PUSH0,操作码为0x5F(即0x60的前一位),用于将0压入堆栈,gas消耗为2,比其他的PUSH指令更省gas。s ![[Pasted image 20240105154933.png]]

POP

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

算数指令


ADD

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

MUL

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

SUB

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

DIV

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

SDIV

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

MOD

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

SMOD

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

ADDMOD

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

MULMOD

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

EXP

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

SIGNEXTEND

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

比较指令

LT

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

比较的是压入顺序

GT

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

EQ

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

ISZERO

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

SLT

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

SGT

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

位级指令

包括AND(与),OR(或),和XOR(异或)

XOR

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

内存指令

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

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

MSTORE

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

MSTORE8

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

MLOAD

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

MSIZE

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

存储指令

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

SSTORE

SSTORE指令用于将一个256位(32字节)的值写入到存储。它从堆栈中弹出两个元素,第一个元素为存储的地址(key),第二个元素为存储的值(value)。操作码是0x55,gas消耗根据实际改变的数据计算(下面给出)。

SLOAD

SLOAD指令从存储中读取一个256位(32字节)的值并推入堆栈。它从堆栈中弹出一个元素,从该元素表示的存储槽中加载值,并将其推入堆栈。操作码是0x54,gas消耗后面给出。

控制流指令

STOP

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

STOP是EVM的停止指令,它的作用是停止当前上下文的执行,并成功退出。它的操作码是0x00,gas消耗为0。

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

JUMPDEST

JUMPDEST指令标记一个有效的跳转目标位置,不然无法使用JUMPJUMPI进行跳转。它的操作码是0x5b,gas消耗为1。

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

JUMP

JUMP指令用于无条件跳转到一个新的程序计数器位置。它从堆栈中弹出一个元素,将这个元素设定为新的程序计数器(pc)的值。操作码是0x56,gas消耗为8。

JUMPI

JUMPI指令用于条件跳转,它从堆栈中弹出两个元素,如果第二个元素(条件,condition)不为0,那么将第一个元素(目标,destination)设定为新的pc的值。操作码是0x57,gas消耗为10。

PC

PC指令将当前的程序计数器(pc)的值压入堆栈。操作码为0x58,gas消耗为2。

区块信息指令

堆栈指令2

DUP1

在EVM中,DUP是一系列的指令,总共有16个,从DUP1DUP16,操作码范围为0x800x8F,gas消耗均为3。这些指令用于复制(Duplicate)堆栈上的指定元素(根据指令的序号)到堆栈顶部。例如,DUP1复制栈顶元素,DUP2复制距离栈顶的第二个元素,以此类推。

SWAP

SWAP指令用于交换堆栈顶部的两个元素。与DUP类似,SWAP也是一系列的指令,从SWAP1SWAP16共16个,操作码范围为0x900x9F,gas消耗均为3。SWAP1交换堆栈的顶部和次顶部的元素,SWAP2交换顶部和第三个元素,以此类推。

SHA3指令

账户指令

以太坊上的账户分两类:外部账户(Externally Owned Accounts,EOA)和合约账户。EOA是用户在以太坊网络上的代表,它们可以拥有ETH、发送交易并与合约互动;而合约账户是存储和执行智能合约代码的实体,它们也可以拥有和发送ETH,但不能主动发起交易。 ![[Pasted image 20240108152351.png]] 以太坊上的账户结构非常简单,你可以它理解为地址到账户状态的映射。账户地址是20字节(160位)的数据,可以用40位的16进制表示,比如0x9bbfed6889322e016e0a02ee459d306fc19545d8。而账户的状态具有4种属性:

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

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

BALANCE

BALANCE 指令用于返回某个账户的余额。它从堆栈中弹出一个地址,然后查询该地址的余额并压入堆栈。它的操作码是0x31,gas为2600(cold address)或100(warm address)。

EXTCODESIZE

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

EXTCODECOPY

EXTCODECOPY 指令用于将某个账户的部分代码复制到EVM的内存中。它会从堆栈中弹出4个参数(addr, mem_offset, code_offset, length),分别对应要查询的地址,写到内存的偏移量,读取代码的偏移量和长度。它的操作码是0x3C,gas由读取代码长度、内存扩展成本和地址是否为cold这三部分决定。

EXTCODEHASH

EXTCODEHASH 指令返回某个账户的代码的Keccak256哈希值。它从堆栈中弹出一个地址,然后查询该地址代码的哈希并压入堆栈。它的操作码是0x3F,gas为2600(cold address)或100(warm address)。

交易指令

![[Pasted image 20240108140043.png]]

每一笔以太坊交易都包含以下属性:

  • nonce:一个与发送者账户相关的数字,表示该账户已发送的交易数。
  • gasPrice:交易发送者愿意支付的单位gas价格。
  • gasLimit:交易发送者为这次交易分配的最大gas数量。
  • to:交易的接收者地址。当交易为合约创建时,这一字段为空。
  • value:以wei为单位的发送金额。
  • data:附带的数据,通常为合约调用的输入数据(calldata)或新合约的初始化代码(initcode)。
  • 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中的数据复制到内存中。它会从堆栈中弹出3个参数(mem_offset, calldata_offset, length),分别对应写到内存的偏移量,读取calldata的偏移量和长度。
  • 使用场景:将输入数据复制到内存。

CODESIZE

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

CODECOPY

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

GASPRICE

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

RETURN指令

EVM的返回数据,通常称为returnData,本质上是一个字节数组。它不遵循固定的数据结构,而是简单地表示为连续的字节。当合约函数需要返回复杂数据类型(如结构体或数组)时,这些数据将按照ABI规范被编码为字节,并存储在returnData中,供其他函数或合约访问。

RETURN

  • 操作码0xF3
  • gas消耗:内存扩展成本。
  • 功能:从指定的内存位置提取数据,存储到returnData中,并终止当前的操作。此指令需要从堆栈中取出两个参数:内存的起始位置mem_offset和数据的长度length
  • 使用场景:当需要将数据返回给外部函数或交易时。

RETURNDATASIZE

  • 操作码0x3D
  • gas消耗:2
  • 功能:将returnData的大小推入堆栈。
  • 使用场景:使用上一个调用返回的数据。

RETURNDATACOPY

  • 操作码:0x3E
  • gas消耗: 3 + 3 * 数据长度 + 内存扩展成本
  • 功能:将returnData中的某段数据复制到内存中。此指令需要从堆栈中取出三个参数:内存的起始位置mem_offset,返回数据的起始位置return_offset,和数据的长度length
  • 使用场景:使用上一个调用返回的部分数据。

CREATE指令

可以让合约创建新的合约

init初始代码

以太坊有两种交易,一种是合约调用,而另一种是合约创建。在合约创建的交易中,to字段设为空,而data字段应填写为合约的初始代码(initcode)。initcode也是字节码,但它只在合约创建时执行一次,目的是为新合约设置必要的状态和返回最终的合约字节码(contract code)。

CREATE

在EVM中,当一个合约想要创建一个新的合约时,会使用CREATE指令。它的简化流程:

  1. 从堆栈中弹出value(向新合约发送的ETH)、mem_offsetlength(新合约的initcode在内存中的初始位置和长度)。
  2. 计算新合约的地址,计算方法为:
    address = keccak256(rlp([sender_address,sender_nonce]))[12:]
  3. 更新ETH余额。
  4. 初始化新的EVM上下文evm_create,用于执行initcode
  5. evm_create中执行initcode
  6. 如果执行成功,则更新创建的账户状态:更新balance,将nonce初始化为0,将code字段设为evm_create的返回数据,将storage字段设置为evm_createstorage
  7. 如果成功,则将新合约地址推入堆栈;若失败,将0推入堆栈。
### CREATE操作码的工作原理

1. **内存中的代码**

- `CREATE` 操作码会读取内存中的一段特定代码。这段代码通常是智能合约的初始化代码,也称为构造函数代码。
2. **如何指定代码段**

- `CREATE` 需要两个参数:内存中代码的起始位置和代码的长度。这些参数通常在执行 `CREATE` 之前被推送到栈上。
3. **合约的创建**

-`CREATE` 被执行时,EVM 会从指定的内存位置开始读取指定长度的代码,并使用这段代码来创建新合约。
- 这段代码通常会包含一些初始化操作,比如设置合约状态变量的初始值。
4. **运行时代码**

- 初始化代码执行完毕后,它可以通过 `RETURN` 操作码返回一段数据。这段数据被视为新合约的运行时代码,即合约的主体代码。
- 这个运行时代码被存储在区块链上,并在合约被调用时执行。
5. **合约地址的生成**

- 新创建的合约会被分配一个独一无二的地址。这个地址是通过一定的算法计算出来的,通常涉及到创建合约的交易发起者的地址和其发送的交易数量(nonce)。
6. **返回值**

- `CREATE` 操作码执行完毕后,会将新合约的地址推送到栈上作为返回值。

GAS指令

在EVM中,交易和执行智能合约需要消耗计算资源。为了防止用户恶意的滥用网络资源和补偿验证者所消耗的计算能源,以太坊引入了一种称为Gas的计费机制,使每一笔交易都有一个关联的成本。

在发起交易时,用户设定一个最大Gas数量(gasLimit)和每单位Gas的价格(gasPrice)。如果交易执行超出了gasLimit,交易会回滚,但已消耗的Gas不会退还。

以太坊上的Gas用gwei衡量,它是ETH的子单位,1 ETH = 10^9 gwei。一笔交易的Gas成本等于每单位gas价格乘以交易的gas消耗,即gasPrice * gasUsed。gas价格会随着时间的推移而变化,具体取决于当前对区块空间的需求。gas消耗由很多因素决定,并且每个以太坊版本都会有所改动,下面总结下:

  1. calldata大小:calldata中的每个字节都需要花费gas,交易数据的大小越大,gas消耗就越高。calldata每个零字节花费4 Gas,每个非零字节花费16 Gas(伊斯坦布尔硬分叉之前为 64 个)。

  2. 内在gas:每笔交易的内在成本为21000 Gas。除了交易成本之外,创建合约还需要 32000 Gas。该成本是在任何操作码执行之前从交易中支付的。

  3. opcode固定成本:每个操作码在执行时都有固定的成本,以Gas为单位。对于所有执行,该成本都是相同的。比如每个ADD指令消耗3 Gas。

  4. opcode动态成本:一些指令消耗更多的计算资源取决于其参数。因此,除了固定成本之外,这些指令还具有动态成本。比如SHA3指令消耗的Gas随参数长度增长。

  5. 内存拓展成本:在EVM中,合约可以使用操作码访问内存。当首次访问特定偏移量的内存(读取或写入)时,内存可能会触发扩展,产生gas消耗。比如MLOADRETURN

  6. 访问集成本:对于每个外部交易,EVM会定义一个访问集,记录交易过程中访问过的合约地址和存储槽(slot)。访问成本根据数据是否已经被访问过(热)或是首次被访问(冷)而有所不同。

  7. Gas退款:SSTORE的一些操作(比如清除存储)可以触发Gas退款。退款会在交易结束时执行,上限为总Gas消耗的20%(从伦敦硬分叉开始)。

GAS

EVM中的GAS指令会将当前交易的剩余Gas压入堆栈。它的操作码为0x5A,gas消耗为2

CALL指令

CALL

CALL指令会创建一个子环境来执行其他合约的部分代码,发送ETH,并返回数据。返回数据可以使用RETURNDATASIZERETURNDATACOPY获取。若执行成功,会将1压入堆栈;否则,则压入0。如果目标合约没有代码,仍将1压入堆栈(视为成功)。如果账户ETH余额小于要发送的ETH数量,调用失败,但当前交易不会回滚。

它从堆栈中弹出7个参数,依次为:

  • gas:为这次调用分配的gas量。
  • to:被调用合约的地址。
  • value:要发送的以太币数量,单位为wei
  • mem_in_start:输入数据(calldata)在内存的起始位置。
  • mem_in_size:输入数据的长度。
  • mem_out_start:返回数据(returnData)在内存的起始位置。
  • mem_out_size:返回数据的长度。

它的操作码为0xF1,gas消耗比较复杂,包含内存扩展和代码执行等成本。