アルゴリズムとかオーダーとか

仕事で勉強したことなどをまとめてます

ContractのEventの仕組み

Ethereum Advent Calendar 2017 の 7 日目の記事です。


ContractにEventを定義すると、Contractの状態が変更された時などに必要な人が通知を受け取れるようになります。
しかし、getter系のfunctionにはEventが設定できなかったりします。
今回はこのEventの挙動はなぜなのか?Eventはどのように動いているのか?について調べたことをまとめます。

Eventの実態について

Eventの実態はTransactionReceiptに記述されるLogデータです。

  event Deposit(address _from, bytes32 _id, uint _value);

このようなEventを発行した時にはTransactionReceiptのlogsのパートにこのEventの情報が記録されます。

{
  "tx": "0xfc263e9ff336325d5593e919634f64ad931eecae9020f08857b39668bacfe9a8",
  "receipt": {
    "transactionHash": "0xfc263e9ff336325d5593e919634f64ad931eecae9020f08857b39668bacfe9a8",
    "transactionIndex": 0,
    "blockHash": "0xa7d16eb8c8ad659e65ce428ccf0aede4746652f475267a5babc17a275722eaa9",
    "blockNumber": 6,
    "gasUsed": 23268,
    "cumulativeGasUsed": 23268,
    "contractAddress": null,
     "logs": [
      {
        "logIndex": 0,
        "transactionIndex": 0,
        "transactionHash": "0xfc263e9ff336325d5593e919634f64ad931eecae9020f08857b39668bacfe9a8",
        "blockHash": "0xa7d16eb8c8ad659e65ce428ccf0aede4746652f475267a5babc17a275722eaa9",
        "blockNumber": 6,
        "address": "0x345ca3e014aaf5dca488057592ee47305d9b3e10",
        "data": "0x000000000000000000000000627306090abab3a6e1400e9345bc60c78a8bef5730000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
        "topics": [
          "0x19dacbf83c5de6658e14cbf7bcae5c15eca2eedecf1c66fbca928e4d351bea0f"
        ],
        "type": "mined"
      }
    ]
  }
}

そのため、getterなどのTransactionを必要としない(Contractの状態を変更しない)constantなfunctionではTransactionReceiptが発行されないため、Eventの発行ができません。
constantを外して無理やりEventを発行できるようにはかけますが、その場合はsendTransactionが必要となりreturn valueが受け取れなくなります。

topicsのデータはEvent定義をkeccak256で求めたハッシュ値(いわゆるsignature)になります。これはEventのfilteringのために使われます。

まとめると

  • Eventの実態はTransactionReceiptのlogs
  • sendTransactionを必要としないconstantなfunctionでは発行できない。

ContractがどのようにしてEventを発行するのかついてはSolidityの公式ドキュメントに詳しく書いてあります。
Solidity low-level-interface-to-logs


Event監視のためのweb3.eth.filterについて

たとえば、TruffleでEventを監視したい時は次のように記述します。

const instance = await ClientReceipt.new()
const event = instance.Deposit()

await event.watch((err, log) => {
  if (!err) console.log("event=", log)
  else console.log("event error =", err)
})

let receiptTx = await instance.deposit(1)
receiptTx = await instance.deposit(2)
receiptTx = await instance.deposit(3)

event.stopWatching()

このeventとして取得したinstance.Deposit()の中身は

Filter {
  requestManager: 
   RequestManager {
     provider: Provider { provider: [Object] },
     polls: {},
     timeout: null },
  options: 
   { topics: 
      [ '0x19dacbf83c5de6658e14cbf7bcae5c15eca2eedecf1c66fbca928e4d351bea0f' ],
     from: undefined,
     to: undefined,
     address: '0x345ca3e014aaf5dca488057592ee47305d9b3e10',
     fromBlock: undefined,
     toBlock: undefined },
  implementation: 
   { newFilter: { [Function: send] request: [Function: bound ], call: [Function: newFilterCall] },
     uninstallFilter: { [Function: send] request: [Function: bound ], call: 'eth_uninstallFilter' },
     getLogs: { [Function: send] request: [Function: bound ], call: 'eth_getFilterLogs' },
     poll: { [Function: send] request: [Function: bound ], call: 'eth_getFilterChanges' } },
  filterId: null,
  callbacks: [],
  getLogsCallbacks: [],
  pollFilters: [],
  formatter: [Function: bound ] }

です。

つまり、web3.eth.filterであり、次のような記述でも同様の結果となります。

const instance = await ClientReceipt.new()
const event = web3.eth.filter({address: instance.address, topics: [web3.sha3("Deposit(address,bytes32,uint256)")]})

await event.watch((err, log) => {
  if (!err) console.log("event=", log)
  else console.log("event error =", err)
})

let receiptTx = await instance.deposit(1)
receiptTx = await instance.deposit(2)
receiptTx = await instance.deposit(3)

event.stopWatching()



web3.eth.filterのwatchとgetの使い分け

filter#watch(callback): callbackをfilterに登録してtoBlockまで、もしくはfilter#stopWatchig()を呼ぶまでnodeを監視し続けます。
filter#get: callback functionを引数に渡した場合と渡さなかった場合で挙動が違います。

  • filter#get(callback): fromBlockからtoBlockまでの間に発生したEventをcallbackに渡します。多分大量にログがある場合は何度かに分けて渡されてくるっぽい?filter#stopWatching()が呼ばれるまでfilterに登録されたままですが、watchとは違いはその後に発生したEventは拾いません。
  • filter#get(): fromBlockからtoBlockまでの間に発生したEventをすべて返します。toBlockが'latest'やundefinedの時は、getをcallした時点までのEventを返します。

サンプルコード

const ClientReceipt = artifacts.require('ClientReceipt.sol')
const delay = time => new Promise(res => setTimeout(() => res(), time))
contract('ClientReceiptTest', accounts => {
  it.only("check event.", async () => {
    const instance = await ClientReceipt.new()
    const event = instance.Deposit({}, {fromBlock: 0, toBlock: 'latest'})
    let receiptTx = await instance.deposit(1)
    receiptTx = await instance.deposit(2)
    event.watch((err, log) => {
      if (!err) console.log("event=", log)
      else console.log("event error =", err)
    })

    await delay(2000)
    receiptTx = await instance.deposit(3)

    await delay(2000)
    event.stopWatching()
    console.log(event.get.toString())
  })
})

このTruffle Testを実行すると最後のawait instance.deposit(3)のEventまでconsoleにログ出力されます。
しかし、9行目のevent.watch((err, log) => { を event.get((err, log) => {に書き換えると、呼び出した前に実行された2つのEventしかconsoleに出力されません。

まとめると

  • ずっと監視したい時はfilter#watch
  • 過去に発生した少量のEventを取得したい時はfilter#get()
  • 過去に発生した大量のEventを取得したい時はfilter#get(callback)

と使い分けるといいのかもしれません。


# filter#get(callback)はtoBlockにlatestを指定していても、呼び出した時点までのEventしか返してくれないし、filter#stopWatchingを呼ぶまではfilter#implementation.getLogsからの応答を待機したままになって、filter#stopWatching呼び出すまで終了してくれないのがちょっと不満


(2017/12/08追記) indexedについての記事も書きました。
Eventにつけるindexedの役割 - アルゴリズムとかオーダーとか