事件

事件是区块链底层虚拟机日志基础设施提供的一个便利接口。当触发事件时,事件中的参数存储到交易收据的日志字段中,日志是一种特殊的数据结构,这些日志与合约地址相关联,并随交易收据记录到区块链中。每条交易收据中可以包含 0 条或多条日志记录。在分布式应用中,如果监听了某事件,则当该事件发生时,便会触发应用相应的回调。

创建事件

Liquid 中使用结构体(struct)语法定义事件。结构体中的每个成员都是事件的参数,为向 Liquid 告知该结构体用于定义事件,需要使用#[liquid(event)]属性标注该结构体,例如:

1
2
3
4
5
#[liquid(event)]
struct Foo {
    s: String,
    i: i32,
}

上述代码中,我们定义了一个名为Foo的事件,事件中包含两个参数,分别为Stringi32。更进一步,还可以使用#[liquid(indexed)]属性将事件参数标注为可被索引:

1
2
3
4
5
6
#[liquid(event)]
struct Foo {
    #[liquid(indexed)]
    s: String,
    i: i32,
}

被索引的参数本身不会被保存,但是分布式应用可以通过被索引参数的值来对事件进行检索。在 Liquid 中,一个事件最多有四个参数可被用于被索引,但是第一个索引恒定为事件签名(事件名及其参数类型)的哈希值,因此在事件定义中,最多可以使用#[liquid(indexed)]标注三个参数。

与状态变量定义类似,不能为定义事件的结构体添加可见性声明或模板参数。但和状态变量定义不同的是,其内部每个成员也不允许添加可见性声明。当前为与 Solidity 兼容,事件参数及索引参数的类型均需要在 Solidity 中存在相应的类型,具体的限制可参考类型一节,未来可能会放宽这一限制。

触发事件

在 Liquid 中,通过环境对象触发事件。环境对象由 Liquid 自动生成,可以在合约方法中通过调用self.env()来获取环境对象。获取环境对象后,可以通过调用环境对象的emit方法来触发我们之前定义的事件,例如:

1
2
3
4
self.env().emit(Foo {
    s: String::from("hello"),
    i: 42,
})

上述代码中,emit方法以事件对象为参数,事件对象可通过结构体初始化语法直接进行构造。提供给emit方法的参数类型一定需要是有效的事件类型(即被#[liquid(event)]属性标注的结构体类型),否则会报出类型不匹配的编译错误。事件被出发后,对应交易的回执中会多出一条日志记录,例如:

"logs": [
    {
        "address": "0x6119432a43a2a5da27f31fa4912f1c43400b1690",
        "data": "0x00000000000000000000000000000000000000000000000000000000000002a",
        "topics": [
            "0x1be2d150ed559c350b05f7dfa5a74669ec8d2ce63bb14c134730ffa02d2d111c",
            "0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8"
        ]
    }
]

日志记录,address字段是合约地址;data字段中保存了非索引参数的ABI 编码,此处因为我们只有一个非索引参数i,因此data字段中只保存了它的值 42;topics字段包含了两个可用于索引该事件的值,其中第一个是事件签名的哈希值,第二个则是事件中索引参数s的值的哈希值。对于String这类动态对象,Liquid 会将它们的哈希值作为事件索引,以提高检索效率并减少存储空间占用。因此若需要在应用中按照字符串检索事件,则需要在本地预先计算待检索字符串的哈希值。

注意

Liquid目前支持将合约编译为国密版本或非国密版本,两种版本的合约在计算哈希值时采用的哈希算法并不相同,分别为 sm3keccak256 。如果需要使用动态对象索引事件,则请务必确保所使用的哈希算法与产生日志的Liquid合约一致。