.
账户指令

我最近在重新学以太坊opcodes,也写一个“WTF EVM Opcodes极简入门”,供小白们使用。

推特:@0xAA_Science

社区:Discord|微信群|官网 wtf.academy

所有代码和教程开源在github: github.com/WTFAcademy/WTF-Opcodes


在这一讲,我们将探索EVM中与账户(Account)相关的4个指令,包括BALANCE, EXTCODESIZE, EXTCODECOPY, 以及 EXTCODEHASH。我们能利用这些指令获取以太坊账户的信息。

以太坊账户结构

以太坊上的账户分两类:外部账户(Externally Owned Accounts,EOA)和合约账户。EOA是用户在以太坊网络上的代表,它们可以拥有ETH、发送交易并与合约互动;而合约账户是存储和执行智能合约代码的实体,它们也可以拥有和发送ETH,但不能主动发起交易。

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

  • Balance:这是账户持有的ETH数量,用Wei表示(1 ETH = 10^18 Wei)。

  • Nonce:对于外部账户(EOA),这是该账户发送的交易数。对于合约账户,它是该账户创建的合约数量。

  • Storage:每个合约账户都有与之关联的存储空间,其中包含状态变量的值。

  • Code:合约账户的字节码。

也就是说,只有合约账户拥有Storage和Code,EOA没有。

为了让极简EVM支持账户相关的指令,我们利用dict做一个简单账户数据库:

account_db = {
    '0x9bbfed6889322e016e0a02ee459d306fc19545d8': {
        'balance': 100, # wei
        'nonce': 1, 
        'storage': {},
        'code': b'\x60\x00\x60\x00'  # Sample bytecode (PUSH1 0x00 PUSH1 0x00)
    },
    # ... 其他账户数据 ...
}

下面,我们将介绍账户相关指令。

BALANCE

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

def balance(self):
    if len(self.stack) < 1:
        raise Exception('Stack underflow')
    addr_int = self.stack.pop()
    # 将stack中的int转换为bytes,然后再转换为十六进制字符串,用于在账户数据库中查询
    addr_str = '0x' + addr_int.to_bytes(20, byteorder='big').hex()
    self.stack.append(account_db.get(addr_str, {}).get('balance', 0))

我们可以尝试运行一个包含BALANCE指令的字节码:739bbfed6889322e016e0a02ee459d306fc19545d831(PUSH20 9bbfed6889322e016e0a02ee459d306fc19545d8 BALANCE)。这个字节码使用PUSH20将一个地址推入堆栈,然后使用BALANCE指令查询该地址的余额。

# BALANCE
code = b"\x73\x9b\xbf\xed\x68\x89\x32\x2e\x01\x6e\x0a\x02\xee\x45\x9d\x30\x6f\xc1\x95\x45\xd8\x31"
evm = EVM(code)
evm.run()
print(evm.stack)
# output: 100

EXTCODESIZE

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

def extcodesize(self):
    if len(self.stack) < 1:
        raise Exception('Stack underflow')
    addr_int = self.stack.pop()
    # 将stack中的int转换为bytes,然后再转换为十六进制字符串,用于在账户数据库中查询
    addr_str = '0x' + addr_int.to_bytes(20, byteorder='big').hex()
    self.stack.append(len(account_db.get(addr_str, {}).get('code', b'')))

我们可以尝试运行一个包含EXTCODESIZE指令的字节码:739bbfed6889322e016e0a02ee459d306fc19545d83B(PUSH20 9bbfed6889322e016e0a02ee459d306fc19545d8 EXTCODESIZE)。这个字节码使用PUSH20将一个地址推入堆栈,然后使用EXTCODESIZE指令查询该地址的代码长度。

# EXTCODESIZE
code = b"\x73\x9b\xbf\xed\x68\x89\x32\x2e\x01\x6e\x0a\x02\xee\x45\x9d\x30\x6f\xc1\x95\x45\xd8\x3B"
evm = EVM(code)
evm.run()
print(evm.stack)
# output: 4

EXTCODECOPY

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

def extcodecopy(self):
    # 确保堆栈中有足够的数据
    if len(self.stack) < 4:
        raise Exception('Stack underflow')
    addr = self.stack.pop()
    mem_offset = self.stack.pop()
    code_offset = self.stack.pop()
    length = self.stack.pop()
    
    code = account_db.get(addr, {}).get('code', b'')[code_offset:code_offset+length]
    
    while len(self.memory) < mem_offset + length:
        self.memory.append(0)
        
    self.memory[mem_offset:mem_offset+length] = code

我们可以尝试运行一个包含EXTCODECOPY指令的字节码:60045F5F739bbfed6889322e016e0a02ee459d306fc19545d83C(PUSH1 4 PUSH0 PUSH0 PUSH20 9bbfed6889322e016e0a02ee459d306fc19545d8 EXTCODECOPY)。这个字节码将4(length), 0(code_offset), 0(mem_offset), 地址(addr)分别推入堆栈,然后,然后使用EXTCODECOPY指令将代码复制到内存中。

# EXTCODECOPY
code = b"\x60\x04\x5F\x5F\x73\x9b\xbf\xed\x68\x89\x32\x2e\x01\x6e\x0a\x02\xee\x45\x9d\x30\x6f\xc1\x95\x45\xd8\x3C"
evm = EVM(code)
evm.run()
print(evm.memory.hex())
# output: 60006000

EXTCODEHASH

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

import sha3

def extcodehash(self):
    if len(self.stack) < 1:
        raise Exception('Stack underflow')
    addr_int = self.stack.pop()
    # 将stack中的int转换为bytes,然后再转换为十六进制字符串,用于在账户数据库中查询
    addr_str = '0x' + addr_int.to_bytes(20, byteorder='big').hex()
    code = account_db.get(addr_str, {}).get('code', b'')        
    code_hash = int.from_bytes(sha3.keccak_256(code).digest(), 'big')  # 计算哈希值
    self.stack.append(code_hash)

我们可以尝试运行一个包含EXTCODEHASH指令的字节码:739bbfed6889322e016e0a02ee459d306fc19545d83F(PUSH20 9bbfed6889322e016e0a02ee459d306fc19545d8 EXTCODEHASH)。这个字节码使用PUSH20将一个地址推入堆栈,然后使用EXTCODEHASH指令查询该地址的代码哈希。

# EXTCODEHASH
code = b"\x73\x9b\xbf\xed\x68\x89\x32\x2e\x01\x6e\x0a\x02\xee\x45\x9d\x30\x6f\xc1\x95\x45\xd8\x3F"
evm = EVM(code)
evm.run()
print(hex(evm.stack[-1]))
# output: 0x5e3ce470a8506d55e59815db7232a08774174ae0c7fdb2fbc81a49e4e242b0d6

总结

这一讲,我们简单介绍了以太坊账户结构,并学习了与账户相关的一系列指令。这些指令使得合约可以与以太坊上的其他账户交互并获取相关信息,为合约间的交互提供了基础。目前,我们已经学习了144个操作码中的116个!

PreviousNext

EVM Opcodes 101

1. Hello Opcodes
2. Opcodes分类
3. 堆栈指令
4. 算数指令
5. 比较指令
6. 位级指令
7. 内存指令
8. 存储指令
9. 控制流指令
10. 区块信息指令
11. 堆栈指令2
12. SHA3指令
13. 账户指令
14. 交易指令
15. Log指令