Opcodes进阶
Opcodes进阶
从现在开始就是一些和以太坊相关的操作码,原教程来自WTF Academy EVM101。
区块信息指令
在写智能合约时经常会用到区块链信息,比如blockhash
,block.number
,和block.timestamp
。EVM提供了一系列指令让智能合约访问当前或历史区块的信息,包括区块哈希、时间戳、coinbase等。
BLOCKHASH
查询特定区块(最近的256个区块,不包括当前区块)的hash,它的操作码为0x40
。它从堆栈中弹出一个值作为区块高度(block number),然后将该区块的hash压入堆栈,如果它不属于最近的256个区块,则返回0(你可以使用NUMBER
指令查询当前区块高度)。
COINBASE
将当前区块的coinbase**(矿工/受益人)地址**压入堆栈,它的操作码为0x41
。
TIMESTAMP
将当前区块的时间戳压入堆栈,它的操作码为0x42
。
NUMBER
将当前区块高度压入堆栈,它的操作码为0x43
。
PREVRANDAO
它返回 前一个区块的随机数(mixHash
)。这个值通常用于提供某种程度的 随机性,但需要注意,它是由区块生产者(矿工或验证者)控制的,不能被视为完全不可预测的随机数。操作码0x44
。
// solidity中利用prepvrandao抽奖的例子
uint256 randomIndex = uint256(block.prevrandao) % players.length;
GASLIMIT
将当前区块的gas限制压入堆栈,它的操作码为0x45
。
CHAINID
将当前的链ID压入堆栈,它的操作码为0x46
。
SELFBALANCE
将合约的当前余额压入堆栈,它的操作码为0x47
。
BASEFEE
将当前区块的基础费(base fee)压入堆栈,它的操作码0x48
。
这些一般都是solidity去调的,本地没法用,就先不举例了。
堆栈指令2
DUP
DUP
是一系列的指令,总共有16个,从DUP1
到DUP16
,操作码范围为0x80
到0x8F
,这些指令用于复制(Duplicate)堆栈上的指定元素(根据指令的序号)到堆栈顶部。例如,DUP1
复制栈顶元素,DUP2
复制距离栈顶的第二个元素,以此类推。
PUSH1 0x04
PUSH1 0x03
PUSH1 0x02
PUSH1 0x01
DUP3
// Stack(top -> bottom): [3, 1, 2, 3, 4]
SWAP
SWAP
指令用于交换堆栈顶部的两个元素。与DUP
类似,SWAP
也是一系列的指令,从SWAP1
到SWAP16
共16个,操作码范围为0x90
到0x9F
,SWAP1
交换堆栈的顶部和次顶部的元素,SWAP2
交换顶部和第三个元素,以此类推。
PUSH1 0x06
PUSH1 0x05
PUSH1 0x04
PUSH1 0x03
PUSH1 0x02
PUSH1 0x01
// Stack(top -> bottom): [1, 2, 3, 4, 5, 6]
SWAP5
// Stack(top -> bottom): [6, 2, 3, 4, 5, 1]
// 注意,SWAP5是换栈顶和第六个不是第五个,否则SWAP1没意义
SHA3指令
在EVM中,计算数据的哈希是一个常见的操作。以太坊使用Keccak算法(SHA-3)计算数据的哈希,并提供了一个专门的操作码SHA3
,Solidity中的keccak256()
函数就是建立在它之上的。
SHA3(offset, size)
指令从堆栈中取出两个参数,起始位置offset
和长度size
(以字节为单位),然后它从内存中读取起始位置offset
开始的size
长度的数据,计算这段数据的Keccak-256哈希,并将结果(一个32字节的值)压入堆栈。它的操作码为0x20
。
计算内存中前两个字节的哈希:
PUSH1 0x02
PUSH0
SHA3
// 54a8c0ab653c15bfb48b47fd011ba2b9617af01cb45cab344acd57c924d56798
账户指令
以太坊上的账户分两类:外部账户(Externally Owned Accounts,EOA)和合约账户。EOA是用户在以太坊网络上的代表,它们可以拥有ETH、发送交易并与合约互动;而合约账户是存储和执行智能合约代码的实体,它们也可以拥有和发送ETH,但不能主动发起交易。
账户地址是20字节(160位)的数据,可以用40位的16进制表示。每个地址都映射一个账户状态,包括:
- Balance:这是账户持有的ETH数量,用Wei表示(1 ETH = 10^18 Wei)。
- Nonce:对于外部账户,这是该账户发送的交易数。对于合约账户,它是该账户创建的合约数量。
- Storage:每个合约账户都有与之关联的存储空间,其中包含状态变量的值。
- Code:合约账户的字节码。
BALANCE
BALANCE
指令用于返回某个账户的余额。它从堆栈中弹出一个地址,然后查询该地址的余额并压入堆栈。它的操作码是0x31
。注意,账户要用PUSH20来存,否则要么缺位要么被补0:
PUSH20 9bbfed6889322e016e0a02ee459d306fc19545d8
BALANCE
EXTCODESIZE
用于返回某个账户的代码长度(以字节为单位)。它从堆栈中弹出一个地址,然后查询该地址的代码长度并压入堆栈。如果账户不存在或没有代码,返回0。他的操作码为0x3B
。
合约的代码长度就是它字节码的长度。
PUSH20 0x9bbfed6889322e016e0a02ee459d306fc19545d8
EXTCODESIZE
// 0x16
EXTCODECOPY
用于将某个账户的部分代码复制到EVM的内存中。它会从堆栈中弹出4个参数(addr, mem_offset, code_offset, length),分别对应要查询的地址,写到内存的偏移量,读取代码的偏移量和长度。它的操作码是0x3C
。
PUSH1 0x04
PUSH0
PUSH0
PUSH20 0x9bbfed6889322e016e0a02ee459d306fc19545d8
EXTCODECOPY
// 60045f5f00000000000000000000000000000000000000000000000000000000
交易指令
每一笔交易都包含以下属性:
nonce
:一个与发送者账户相关的数字,表示该账户已发送的交易数。
gasPrice
:交易发送者愿意支付的单位gas价格。
gasLimit
:交易发送者为这次交易分配的最大gas数量。
to
:交易的接收者地址。当交易为合约创建时,这一字段为空。
value
:以wei为单位的发送金额。
data
:附带的数据,通常为合约调用的输入数据(calldata)或新合约的初始化代码(initcode)。
v, r, s
:与交易签名相关的三个值。
ADDRESS
将当前执行合约的地址压入堆栈,操作码:0x30
。
ORIGIN
将交易的原始发送者(即签名者)地址压入堆栈,操作码:0x32
。
CALLER
将直接调用当前合约的地址压入堆栈,操作码:0x33
。
CALLVALUE
将发送给合约的ether的数量(以wei为单位)压入堆栈,操作码:0x34
。
CALLDATALOAD
从交易或合约调用的data
字段加载数据。它从堆栈中弹出calldata的偏移量(offset
),然后从calldata的offset
位置读取32字节的数据并压入堆栈。如果calldata剩余不足32字节,则补0,操作码:0x35
,这个data可以用Ethers.js里讲过的Interface
类来解读。
CALLDATASIZE
获取交易或合约调用的data
字段的字节长度,并压入堆栈,操作码:0x36
。
CALLDATACOPY
将data
中的数据复制到内存中。它会从堆栈中弹出3个参数(mem_offset, calldata_offset, length),分别对应写到内存的偏移量,读取calldata的偏移量和长度,操作码:0x37
。
CODESIZE
获取当前合约代码的字节长度,然后压入堆栈,操作码:0x38
。
CODECOPY
复制合约的代码到EVM的内存中。它从堆栈中弹出三个参数:目标内存的开始偏移量(mem_offset
)、代码的开始偏移量(code_offset
)、以及要复制的长度(length
),操作码:0x39
。
GASPRICE
获取交易的gas价格,并压入堆栈,操作码:0x3A
。
Log指令
EVM中的LOG
指令用于创建日志。指令LOG0
到LOG4
的区别在于它们包含的主题数量(topic部分)。
Log
指令从堆栈中弹出2 + n的元素。其中前两个参数是内存开始位置mem_offset
和数据长度length
,n是主题的数量(取决于具体的LOG
指令)。
所以对于LOG1
,我们会从堆栈中弹出3个元素:内存开始位置,数据长度,和一个主题。需要mem_offset
的原因是日志的数据(data
)部分存储在内存中,gas消耗低,而主题(topic
)部分直接存储在堆栈上。
PUSH1 0xaa
PUSH1 0x00
MSTORE
// 此时0xaa被存在了内存的0号位
PUSH1 0x11
PUSH1 0x01
PUSH1 0x1f
LOG1
// 从0x1f开始的1字节东西输出到日志的data;0x11作为一个主题输出到topic