以太坊 事件和日志

admin
admin 2019年08月29日
  • 在其它设备中阅读本文章

一、事件

事件 是以太坊提供的一种链内链外沟通的一种机制。通过触发事件,智能合约可以通知链外组件某个交易完成了什么事儿。下面是一个 ERC20 合约里常见的 Transfer 事件定义,通过 event 关键字表明这是一个事件定义声明。

event Transfer(address indexed from, address indexed to, uint256 value);

在 transfer 方法的实现中,我们会像下面代码里展示的这样通过 emit 关键字触发事件的发生。

function transfer(address _to, uint256 _value) public returns (bool) {
  ...
  emit Transfer(msg.sender, _to, _value);
  return true;
}

二、日志

在以太坊的语境里,日志 代表对事件的存储。在下面的交易回执里,我们可以看到 logs 数据项,这个就是我们这里所说的日志,合约执行时每触发一次事件,在交易收据里的 logs 数据项数组里就会多一个日志条目出来。

>eth.getTransactionReceipt("0xe03fac05ff4dde83fc9267184fd8c08bd78599f950e817dbf7fa4a4d4d319ce2");
{
  blockHash: "0x7eaf6abe64592d10828e136635aa6be6f4d09da3bb5b9fddf87773ee152d657c",
  blockNumber: 4654718,
  contractAddress: null,
  cumulativeGasUsed: 52464,
  from: "0x076979a0b3c87334e5d72e3afcafaa80f7888cac",
  gasUsed: 52464,
  logs: [{
      address: "0x73c2a5b1a32fa8e33101a6ab119203f4417feae4",
      blockHash: "0x7eaf6abe64592d10828e136635aa6be6f4d09da3bb5b9fddf87773ee152d657c",
      blockNumber: 4654718,
      data: "0x0000000000000000000000000000000000000000000000056bc75e2d63100000",
      logIndex: 0,
      removed: false,
      topics: ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000076979a0b3c87334e5d72e3afcafaa80f7888cac", "0x000000000000000000000000cd9f286ba6a3d2df7885f4a2be267fc524d32bd3"],
      transactionHash: "0xe03fac05ff4dde83fc9267184fd8c08bd78599f950e817dbf7fa4a4d4d319ce2",
      transactionIndex: 0
  }],
  logsBloom: "0x20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000008000000000400000000000000000000000000000000000000040000000000000000100000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000200000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000400",
  status: "0x1",
  to: "0x73c2a5b1a32fa8e33101a6ab119203f4417feae4",
  transactionHash: "0xe03fac05ff4dde83fc9267184fd8c08bd78599f950e817dbf7fa4a4d4d319ce2",
  transactionIndex: 0
}

在这个日志里,我们可以看到很多和事件触发上下文相关的信息,比如 合约地址 (address)、所在 区块哈希 (blockHash)、所在 区块号 (blockNumber)、所属 交易哈希 (transactionHash)等等。这里面最核心的就两个数据:topicsdata。我们这里看到 topics 是个数组,这个数组的第一个元素就代表所触发的事件,是个 16 进制表示 256 位的数字。只是看这么个字符串,我们并不能确定这是哪个事件,这时候就需要借助于合约的 ABI 文件。在 ABI 文件中找出 type 为 event 的那些元素,如下面所示:

{
      "anonymous": false,
      "inputs": [
        {
          "indexed": true,
          "name": "owner",
          "type": "address"
        },
        {
          "indexed": true,
          "name": "spender",
          "type": "address"
        },
        {
          "indexed": false,
          "name": "value",
          "type": "uint256"
        }
      ],
      "name": "Approval",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": true,
          "name": "from",
          "type": "address"
        },
        {
          "indexed": true,
          "name": "to",
          "type": "address"
        },
        {
          "indexed": false,
          "name": "value",
          "type": "uint256"
        }
      ],
      "name": "Transfer",
      "type": "event"
    }

为了找出 topic 所对应的事件,我们需要计算每个事件的签名并找到匹配的签名(事件名和输入参数类型的 sha3 散列)。对于事件 Transfer(address indexed from, address indexed to, uint256 value),签名就是 sha3('Transfer(address,address,uint256)'),这些都是可以从 ABI 中获得。

>web3.sha3("Transfer(address,address,uint256)")
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
>web3.sha3("Approval(address,address,uint256)")
"0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925"

很明显可以看出,"Transfer(address,address,uint256)" 这个事件的签名和上面提到过的 topics 的第一个数据元素是一致的,说明这个日志就是对应的 Transfer 事件。

["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x000000000000000000000000076979a0b3c87334e5d72e3afcafaa80f7888cac",
"0x000000000000000000000000cd9f286ba6a3d2df7885f4a2be267fc524d32bd3"],

现在我们知道了,日志里面 topics 数组里的第一个数据元素就是事件的签名。那么 topics 里其它的元素是什么呢?会看上面截取的 ABI,我们可以看到在事件元素的 inputs 数据项中,有的 indexed 的值为 true,有的为 false,我们在声明一个事件时,也可以指定事件的参数是否 indexed,比如下面这个 Transfer 事件的声明,from 就是表明为 indexed,表明是被索引收录的。

event Transfer(address indexed from, address indexed to, uint256 value);

topics 里的其它数据元素就是被索引收录的事件参数值,所有在 topics 里的内容,都是被索引收录,可以通过 bloom filter 进行过滤的。
日志里还有一个关键内容就是 data,这个比较容易理解,就是触发事件的时候传给事件的实际参数值,值得注意的是这里面只包含未索引的数据项。
日志数据项总结:topics 值为事件 sha3 签名和 indexed 的参数值,data 值为没有 indexed 的参数值

三、作用

  1. 帮助用户客户端(web3js)读取智能合约的返回值:在真实的环境中我们需要发送交易(Transaction)来调用某个智能合约。这时我们将无法获得智能合约的返回值。因为该交易当前只是被发送、打包、执行还有一段时间(需要矿工的工作量证明来实现),此时调用的返回值只是该交易哈希。
  2. 智能合约异步通知用户客户端(web3js): 在交易打包到区块当中后,智能合约调用的事件记录到收据 Log 当中,如果一个 web3js 客户端正在监听将会收到这个事件。
  3. 用于智能合约的存储(比 Storage 便宜得多):相比智能合约账户的 Storage,用日志的方式存储一些信息会便宜很多。Storage 中大致的价格是每 32 字节(256 位)存储需要消耗 20,000 气(Gas),而日志大致是每字节 8 气(Gas)。但是日志存储的信息智能合约没法读取。

四、web3js 监听事件

以太坊提供了基于 http 和 WebSocket 的 JSON-RPC 事件订阅监听,使用 web3js 客户端可以订阅指定的合约事件。

调用:

myContract.events.MyEvent([options][, callback])

参数:

  • options - Object: 可选,用于部署的选项,包含以下字段:

    • filter - Object : 可选,按索引参数过滤事件。
    • fromBlock - Number: 可选,仅监听该选项指定编号的块中发生的事件
    • topics - Array : 可选,用来手动为事件过滤器设定主题。
  • callback - Function: 可选,该回调函数触发时,其第二给参数为事件对象,第一个参数为错误对象

返回值:
EventEmitter: 事件发生器,声明有以下事件:

  • "data" 返回 Object: 接收到新的事件时触发,参数为事件对象
  • "changed" 返回 Object: 当事件从区块链上移除时触发,该事件对象将被添加额外的属性 "removed: true"
  • "error" 返回 Object: 当发生错误时触发

返回的事件对象结构如下:

  • event - String: 事件名称
  • signature - String|Null: 事件签名,如果是匿名事件,则为 null
  • address - String: 事件源地址
  • returnValues - Object: 事件返回值,例如 {myVar: 1, myVar2: '0x234...'}.
  • logIndex - Number: 事件在块中的索引位置
  • transactionIndex - Number: 事件在交易中的索引位置
  • transactionHash 32 Bytes - String: 事件所在交易的哈希值
  • blockHash 32 Bytes - String: 事件所在块的哈希值,pending 的块该值为 null
  • blockNumber - Number: 事件所在块的编号,pending 的块该值为 null
  • raw.data - String: 该字段包含未索引的日志参数
  • raw.topics - Array: 最多可保存 4 个 32 字节长的主题字符串数组。主题 1 -3 包含事件的索引参数

示例代码:

myContract.events.MyEvent({
    filter: {myIndexedParam: [20,23], myOtherIndexedParam: '0x123456789...'}, // Using an array means OR: e.g. 20 or 23
    fromBlock: 0
}, function(error, event){ console.log(event); })
.on('data', function(event){
    console.log(event); // same results as the optional callback above
})
.on('changed', function(event){
    // remove event from local database
})
.on('error', console.error);

// event output example
> {
    returnValues: {
        myIndexedParam: 20,
        myOtherIndexedParam: '0x123456789...',
        myNonIndexParam: 'My String'
    },
    raw: {
        data: '0x7f9fade1c0d57a7af66ab4ead79fade1c0d57a7af66ab4ead7c2c2eb7b11a91385',
        topics: ['0xfd43ade1c09fade1c0d57a7af66ab4ead7c2c2eb7b11a91ffdd57a7af66ab4ead7', '0x7f9fade1c0d57a7af66ab4ead79fade1c0d57a7af66ab4ead7c2c2eb7b11a91385']
    },
    event: 'MyEvent',
    signature: '0xfd43ade1c09fade1c0d57a7af66ab4ead7c2c2eb7b11a91ffdd57a7af66ab4ead7',
    logIndex: 0,
    transactionIndex: 0,
    transactionHash: '0x7f9fade1c0d57a7af66ab4ead79fade1c0d57a7af66ab4ead7c2c2eb7b11a91385',
    blockHash: '0xfd43ade1c09fade1c0d57a7af66ab4ead7c2c2eb7b11a91ffdd57a7af66ab4ead7',
    blockNumber: 1234,
    address: '0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe'
}