跳到主要内容

WTF Opcodes极简入门: 15. Log指令

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

推特:@0xAA_Science

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

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


这一讲,我们将介绍EVM中与日志(Log)相关的5个指令::从LOG0LOG4。日志是EVM中一个重要的概念,用于记录与合约交互的重要信息,是智能合约中事件(Event)的基础。这些记录永久保存在在区块链上,方便检索,但不会影响区块链的状态,是DApps和智能合约开发者的一个强大工具。

EVM中的日志和事件

在Solidity中,我们常常使用event来定义和触发事件。当这些事件被触发时,它们会生成日志,将数据永久存储在区块链上。日志分为主题(topic)和数据(data)。第一个主题通常是事件签名的哈希值,后面的主题是由indexed修饰的事件参数。如果你对event不了解,推荐阅读WTF Solidity的相应章节

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

为了在我们的极简EVM中支持日志功能,我们首先需要定义一个Log类来表示一个日志条目,他会记录发出日志的合约地址address,数据部分data,和主体部分topics

class Log:
def __init__(self, address, data, topics=[]):
self.address = address
self.data = data
self.topics = topics

def __str__(self):
return f'Log(address={self.address}, data={self.data}, topics={self.topics})'

然后,我们需要在EVM的初始化函数中增加一个logs列表,记录这些日志:

class EVM:
def __init__(self, code, txn = None):

# ... 初始化其他变量 ...

self.logs = []

LOG指令

EVM中有五个Log指令:LOG0LOG1LOG2LOG3LOG4。它们的主要区别在于携带的主题数(topics):LOG0没有主题,而LOG4有四个。操作码从A0A4,gas消耗由以下公式计算:

gas = 375 + 375 * topic数量 + 内存扩展成本

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

接下来,我们实现LOG指令:

def log(self, num_topics):
if len(self.stack) < 2 + num_topics:
raise Exception('Stack underflow')

mem_offset = self.stack.pop()
length = self.stack.pop()
topics = [self.stack.pop() for _ in range(num_topics)]

data = self.memory[mem_offset:mem_offset + length]
log_entry = {
"address": self.txn.thisAddr,
"data": data.hex(),
"topics": [f"0x{topic:064x}" for topic in topics]
}
self.logs.append(log_entry)

最后,我们需要在run方法中为不同的LOG指令添加支持:

def run(self):
while self.pc < len(self.code):
op = self.next_instruction()

# ... 其他指令的处理 ...

elif op == LOG0:
self.log(0)
elif op == LOG1:
self.log(1)
elif op == LOG2:
self.log(2)
elif op == LOG3:
self.log(3)
elif op == LOG4:
self.log(4)

测试

测试LOG0

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

# LOG0
code = b"\x60\xaa\x60\x00\x52\x60\x01\x60\x1f\xa0"
evm = EVM(code, txn)
evm.run()
print(evm.logs)
# output: [{'address': '0x9bbfed6889322e016e0a02ee459d306fc19545d8', 'data': 'aa', 'topics': []}]

测试LOG1

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

# LOG1
code = b"\x60\xaa\x60\x00\x52\x60\x11\x60\x01\x60\x1f\xa1"
evm = EVM(code, txn)
evm.run()
print(evm.logs)
# output: [{'address': '0x9bbfed6889322e016e0a02ee459d306fc19545d8', 'data': 'aa', 'topics': ['0x0000000000000000000000000000000000000000000000000000000000000011']}]

总结

这一讲,我们学习了EVM中与日志和事件相关的5个指令。这些指令在智能合约开发中扮演着关键角色,允许开发者在区块链上永久记录重要信息,同时不会影响区块链的状态。目前,我们已经学习了144个操作码中的131个!