xyyme.eth

發布於 2022-06-08到 Mirror 閱讀

Solidity Events 详解

Events 是 Solidity 中记录事件的工具,可以简单理解为日志。Events 的优点在于,一是能够利用较少的 Gas 就能将数据记录在区块链上,二是可以方便链下对链上数据进行监听。

代码示例

先来看一段简单的代码:

pragma solidity 0.8.10;

contract EventsDemo {
    // 定义 Events
    event Transfer(
        address indexed from, 
        address indexed to, 
        uint256 amount
    );

    function transfer(address to, uint256 amount) external {
        // 发送 Events
        emit Transfer(msg.sender, to, amount);
    }
}

在上述代码中,我们通过 event 关键字定义事件,通过 emit 关键字发送事件。这样,在调用 transfer 函数的时候,就会发送 Transfer 事件,将其数据记录在链上。

接下来我们实际部署一下,并调用 transfer 看看会发生什么。

调用 transfer 时的参数我们分别设为:

  • to → 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
  • amount → 123

调用成功后,查看 Etherscan 的 Logs 页面:

我们可以看到页面中有三部分,分别是:

  • Address
  • Topics
  • Data

其中 Address 是合约地址,这个很容易理解,那么剩下的两个字段分别代表什么呢?

基本数据结构

Events 打印出的日志中包含两种数据结构,分别是 topicdata。每个日志中最多可以包含 4 个 topicdata 则没有限制。topic 的第一个字段默认是事件签名,在上例中,Topics[0] 中的 0xddf25…b3ef 哈希值就是 Transfer 事件的签名:

keccak256(bytes("Transfer(address,address,uint256)"))

图示即为(图片来自于这里):

我们注意到,前面定义的 Transfer 事件中,fromto 字段都带有一个 indexed 关键字,它的作用就是将该字段列为 topic。由于最多限制 4 个 topic,且第一个 topic 默认是事件签名,因此在合约中定义事件的时候,最多只能有 3 个字段可以使用 indexed。那么 topic 到底有什么用呢?

将字段列为 topic 可以方便检索。例如,我们想要在链下监听 Transfer 事件,但是并不想监听每一笔事件,只想监听 from 是我的地址的事件,那么就必须将 from 设置为 topic,否则 data 类型的日志是没有办法做到的。

在上例中,我们将 fromto 设置为 indexed,因此我们可以在上图中看到,有三个 topic,分别是

  • 事件签名
  • from,这里是调用合约的地址
  • to

那么剩下的 data 部分就是 amount 字段的数据了。

data 的内容是将数据经过 abi-encoded 的结果。

我们在 Etherscan 上传代码,刷新页面:

这时 Etherscan 就可以识别出事件的 name,并且可以将 topicdata 数据解码。

Topic 的限制

前面我们说到,一个事件日志中,最多只能有 4 个 topic,也就是说最多有 3 个 indexed。同时,topic 本身也有长度限制,每个 topic 只能容纳 32 个字节的数据。那么像 string 或者 bytes 这种非定长的数据能否设置为 topic 呢?

答案是可以的,但是需要对其内容进行哈希。我们来看一段代码:

pragma solidity 0.8.10;

contract EventsDemo {
    event Message(
        address indexed from,
        address indexed to,
        string message
    );

    function sendMessage(address to, string memory message) public {
        emit Message(msg.sender, to, message);
    }
}

首先,我们先将 Message 事件中的 message 字段设置非 indexed,部署并调用,调用的参数分别是:

  • to → 0x5b38da6a701c568545dcfcb03fcb875f56beddc4
  • message → Hello World

结果为:

message 字段位于 data 中,可以被正常解码。

接下来,我们给 message 加上 indexed

event Message(
    address indexed from,
    address indexed to,
    string indexed message
);

部署并调用,使用同样的参数,结果为:

此时,data 中内容为空,而 topic[3] 的值就是 message 的哈希值:

keccak256(bytes("Hello World"))

Events 的 Gas 花费

根据以太坊黄皮书的内容,日志的基础费用是 375 Gas。另外每个 topic 同样需要花费 375 Gas,data 中每个字节需要花费 8 Gas。

因此,我们前面的 Transfer 事件花费的 Gas 为:

1756 = 375(基础费用)+ 375 * 3(3 个 topic)+ 32 * 8(data 中共 32 字节)

Low-level log

Solidity 老版本中还存在一种底层调用,例如:

pragma solidity >=0.4.10 <0.7.0;

contract C {
    function f() public payable {
        uint256 _id = 0x420042;
        log2(
            bytes32(msg.value),
            bytes32(uint256(msg.sender)),
            bytes32(_id)
        );
    }
}

包括 log0log1log2log3log4,不过新版文档中已经去除了这部分内容,因此我们也不再介绍,了解其存在即可,感兴趣的朋友可以查看这里

监听事件

链下监听事件有很多种方法,几乎所有主流语言都有对应的 web3 库可以实现。我前面写的一篇 ethers 使用教程的文章中包含了这部分内容,感兴趣的朋友可以前往查看,这里就不再赘述了。

总结

Solidity 中 Events 是一个很实用的日志工具,花费 Gas 少,利于链下来监听链上交易。

关于我

欢迎和我交流

参考

https://blog.chain.link/events-and-logging-in-solidity/

https://docs.soliditylang.org/en/latest/contracts.html#events

https://medium.com/mycrypto/understanding-event-logs-on-the-ethereum-blockchain-f4ae7ba50378