日志和过滤器(Logs and Filtering)
在区块链的应用程序中经常用到日志和过滤器,因为它可以对索引数据进行高效查询,并且针对不在链上的数据可以提供低成本的数据存储。
这些可以结合提供者事件API(Provider Event API) 以及合约事件API(Contract Event API) 一起使用。
合约事件API也提供了高级方法去计算和查询数据,这种方式要优先于低级的过滤器。
当合约创建一条日志时,它可以包含了4条可索引的数据。这些索引数据经过哈希加密,且包含在一个Bloom Filter中, Bloom Filter是一种允许高效过滤的数据结构。
因此,过滤器最多可对应4个主题集(topic-set),其中每个主题集指向一个条件,该条件必须匹配该位置的索引日志主题(即每个条件都是AND
语句连接在一起的)。
如果一个主题集为 null
, 则该位置的日志主题 不过滤 且与 任何值 都匹配。
如果一个主题集是单个主题,则该位置的日志主题 必须 匹配 这个主题。
如果主题集是一个数组,则该位置的日志主题必须匹配其中 任何一个 主题(即每个条件都是OR
语句连接在一起的)。
虽然一开始听起来有点复杂绕口,但通过以下例子可以很好地帮助你理解。
主题集 | 匹配日志 | |
[ A ] | topic[0] = A | |
[ A, null ] | |
[ null, B ] | topic[1] = B | |
[ null, [ B ] ] | |
[ null, [ B ], null ] | |
[ A, B ] | (topic[0] = A) AND (topic[1] = B) | |
[ A, [ B ] ] | |
[ A, [ B ], null ] | |
[ [ A, B ] ] | (topic[0] = A) OR (topic[0] = B) | |
[ [ A, B ], null ] | |
[ [ A, B ], [ C, D ] ] | [ (topic[0] = A) OR (topic[0] = B) ] AND [ (topic[1] = C) OR (topic[1] = D) ] | |
匹配日志的例子 | |
filter = {
address: tokenAddress,
topics: [
id("Transfer(address,address,uint256)"),
hexZeroPad(myAddress, 32)
]
};
filter = {
address: tokenAddress,
topics: [
id("Transfer(address,address,uint256)"),
null,
hexZeroPad(myAddress, 32)
]
};
filter = {
address: tokenAddress,
topics: [
id("Transfer(address,address,uint256)"),
null,
[
hexZeroPad(myAddress, 32),
hexZeroPad(myOtherAddress, 32),
]
]
};
在这里解释一下,为了简化, 合约的API如下:
abi = [
"event Transfer(address indexed src, address indexed dst, uint val)"
];
contract = new Contract(tokenAddress, abi, provider);
contract.filters.Transfer(myAddress)
// {
// address: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
// topics: [
// '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
// '0x0000000000000000000000008ba1f109551bd432803012645ac136ddd64dba72'
// ]
// }
contract.filters.Transfer(null, myAddress)
// {
// address: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
// topics: [
// '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
// null,
// '0x0000000000000000000000008ba1f109551bd432803012645ac136ddd64dba72'
// ]
// }
contract.filters.Transfer(myAddress, otherAddress)
// {
// address: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
// topics: [
// '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
// '0x0000000000000000000000008ba1f109551bd432803012645ac136ddd64dba72',
// '0x000000000000000000000000ea517d5a070e6705cc5467858681ed953d285eb9'
// ]
// }
contract.filters.Transfer(null, [ myAddress, otherAddress ])
// {
// address: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
// topics: [
// '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
// null,
// [
// '0x0000000000000000000000008ba1f109551bd432803012645ac136ddd64dba72',
// '0x000000000000000000000000ea517d5a070e6705cc5467858681ed953d285eb9'
// ]
// ]
// }
这是一个在Solidity中关于“事件是如何计算”的简要不全面的概述。
这可能超出了大多数开发人员的了解范围,但对于那些希望了解更多底层技术的人来说,可能会很感兴趣。
Solidity 提供了两种类型的事件,匿名和非匿名。默认值是非匿名的,大多数开发人员不需要关注匿名事件。
对于非匿名事件,最多可以索引3个主题(而不是4个),因为保留第一个主题是事件本身的签名。这允许非匿名事件总是通过其事件签名进行过滤。
这个主题哈希值总是在索引数据的第一个位置中,通过规范化事件签名并取其keccak256哈希来计算。
对于匿名事件,最多可以索引4个主题,并且没有签名的主题哈希,因此此事件不能通过事件签名进行过滤。
每个额外索引属性(主题)的处理取决于其长度是固定的还是动态的。
对于固定长度的类型 (例如uint
, bytes5
),这些全部都恰好是32字节,较短的类型用零填充,数字值类型(numeric values)填充在左边,数据值类型(data values)填充在右边, 这些直接包含在32个字节的数据里面。
对于动态类型 (例如string
, uint256[]
) , 这些值通过 keccak256 哈希(hashed),并且使用这个哈希值。
因为动态类型是被哈希加密的,所以在解析事件时应该记录一些重要的结果。主要是其事件丢失了初始值,因此,一个主题(topics)匹配给定的字符串(string)是可能的,但如果两者不匹配,就没有办法确定最初的值是什么。
如果开发人员需要一个字符串值既能被过滤也能被读取,那么这个值必须在签名中被包含两次,一次是索引的,一次是非索引的(例如 someEvent(string indexed searchBy, string clearText)
)。
有关更详细的描述,请参阅 Solidity事件文档。
将会详细阐述字符串和事件的细节,如何过滤和保留值。