虚拟机的 Event 机制

Event 基础

Log 是虚拟机的重要功能之一,用于在虚拟机运行合约过程中,输出特定二进制数据并记录在交易回执(transaction-info / transaction-receipt)中。基于 Log 功能实现了 Event (事件)机制,可帮助开发人员确认,检查和快速检索智能合约的特定状态。本文介绍 Event 机制基础并介绍如何解码 Event Log 。

Solidity Event

Solidity 中的 Event 通过 event 关键字定义。触发并记录 Event 通过 emit 关键字实现。 Event 不止是特定事件名称,还可以包含若干参数。

contract EventExampleContract {
    event Transfer(address indexed toAddress, uint256 amount);
    constructor() payable public{}
    function contractTransfer(address toAddress, uint256 amount){
        toAddress.transfer(amount);
        emit Transfer(toAddress, amount);
    }
}

如上代码:

  • event Transfer 定义了一个 Transfer 事件,包含两个参数,第一个是 toAddress 表示转账目标地址,第二个是 amount 代表转账的数额
  • emit Transfer(toAddress, amount) 在合约完成转账功能后,触发对应 Event

📘

提示

代码规范中一般要求 Event 名字首字母大写,区别于对应函数。例如事件 Transfertransfer 函数。

Event 结构

Solidity 使用 LOG 指令在交易回执(transaction-info / transaction-receipt)中记录事件信息。该结构位于 TransactionInfo 的 log 字段,相关 protobuf 定义如下:

message Log {
    bytes address = 1;
    repeated bytes topics = 2;
    bytes data = 3;
}

message TransactionInfo {
  // ...
  // A list of LOG represent list of events in a transaction
  repeated Log log = 8;
  // ...
}

其中 topics 字段表示事件的主题,例如 Transfer(...) 这一主题。同时,所有标记为 indexed 的参数也依次在 topics 字段列出。
data 字段表示事件的其他非 indexed 参数,例如 amount

📘

提示

使用 topics 用来保存 indexed 参数的原因是因为,区块链储存一般会选择使用 LevelDB 或 RockDB 一类的 key-value 储存引擎。这些引擎一般都支持 prefix-scan 一类的操作,相当于既可以快速检索 Transfer 事件,又可以快速检索某一特定 toAddressTransfer 事件。

Event 解码示例

上述 Transfer 事件的 ABI 定义为:

{
  "anonymous": false,
  "inputs": [
    {
      "indexed": true,
      "name": "toAddress",
      "type": "address"
    },
    {
      "indexed": false,
      "name": "amount",
      "type": "uint256"
    }
  ],
  "name": "Transfer",
  "type": "event"
}

使用 gettransactioninfobyid API 获取到如下结果:

log:
  address:
    289C4D540B32C7BC56953E55631F8B141EB86434 # contract address in 20-byte format
  data:
    0000000000000000000000000000000000000000000000000000000000000001
  topics:
    69ca02dd4edd7bf0a4abb9ed3b7af3f14778db5d61921c7dc7cd545266326de2  # topics[0]
    000000000000000000000000E552F6487585C2B58BC2C9BB4492BC1F17132CD0  # topics[1]

与 ABI 一起对照可以得到:

  • topics[0] 69ca02dd4edd7bf0a4abb9ed3b7af3f14778db5d61921c7dc7cd545266326de2 即 keccak256("Transfer(address,uint256)"), 表示事件本身
  • topics[1] 000000000000000000000000E552F6487585C2B58BC2C9BB4492BC1F17132CD0 是第一个 indexed 参数 toAddress。需要注意的是,为了于 EVM 兼容,这里使用了兼容地址格式,即去掉地址的前缀 0x41,成为 20 字节地址。
  • data 字段是 0000000000000000000000000000000000000000000000000000000000000001 。代表 amount=1 (uint256 类型)。当存在多个非 indexed 参数时,按照 ABI 编码规则依次列出。

Event 的订阅

在实际应用中,可以通过订阅事件的方式辅助完成开发需求。可参考如下文档: