コミュニティ

Ethereumでスマートコントラクトのログを確認したい

はじめに

Ethereumで開発するコントラクトのログについて

  • 出力 → メソッド処理内でイベントを発火させる
  • 確認 → getTransaction等のAPIを呼んで、ログらしき項目を確認する

という漠然とした理解だったので、改めて勉強しました。
特に、ログの確認方法は試行錯誤し、現時点で最適と思った方法を記すことにしました。

結論

トランザクション実行結果からコントラクトのログを確認する流れ
※コントラクトのABIは知っていることが前提

  1. getTransactionReceipt APIのレスポンスを取得する。
    • 項目logsに、ログデータ(16進数のバイト文字列)が含まれる。
    • 項目topicsに、ログ出力のために発火したイベントの情報(イベント名と引数から導出されたハッシュ値)が含まれる。
  2. web3jsのdecodeLogを使用してデコードする。
$ var txReceipt = await web3.eth.getTransactionReceipt("0x...")
$ var eventAbi = MyContract.abi.filter(element => element.signature == txReceipt.logs[0].topics);
$ web3.eth.abi.decodeLog(eventAbi[0].inputs, txReceipt.logs[0].data, txReceipt.logs[0].topics)

以降は、確認した手順を備忘録として残しています。

前提

環境

  • コントラクト開発環境
    • Truffle v5.1.2
    • Solidity v0.5.12
    • Node v8.11.4
    • Web3.js v1.2.2
  • ブロックチェーン
    • Quorum v2.4.0

コントラクト

簡単な自動販売機コントラクトを用意しました。

VendingContract.sol
pragma solidity ^0.5.0;
pragma experimental ABIEncoderV2;

contract VendingContract {

  struct Drink {
    string name;
    uint32 price;
    uint32 quantity;
  }

  Drink[] public products;

  // 在庫イベント
  event Stock(string name, uint32 quantity);

  constructor() public {
    Drink memory water = Drink("water", 100, 5);
    Drink memory cola = Drink("cola", 150, 5);

    products.push(water);
    products.push(cola);
  }

  // 商品一覧取得メソッド
  function getProducts() public view returns (Drink[] memory) {
    return products;
  }

  // 購入メソッド
  function purchase(uint32 _id) public {
    products[_id].quantity -= 1;
    // 在庫イベント発火
    emit Stock(products[_id].name, products[_id].quantity);
  }

  // 補充メソッド
  function replenish(uint32 _id, uint32 _quantity) public {
    products[_id].quantity += _quantity;
    // 在庫イベント発火
    emit Stock(products[_id].name, products[_id].quantity);
  }
}

ログ出力

コントラクトでログを出力するための実装方法です。
まず、イベントを宣言します。引数にはログで出力したい項目を定義します。

...
  // 在庫イベント(商品名と在庫数)
  event Stock(string name, uint32 quantity);
...

あとは、メソッド内でemitを使ってイベントを発火させるだけです。

...
  // 購入
  function purchase(uint32 _id) public {
    products[_id].quantity -= 1;
    // 在庫イベント発火
    emit Stock(products[_id].name, products[_id].quantity);
  }
...

これでログを出力するための準備ができました。

コントラクトデプロイ~トランザクション実行

実際にトランザクションを送信してみます。
主題ではないのでさらっと流します。
truffle-config.jsにはあらかじめ、Quorumで構築したブロックチェーンへの接続情報(quorum)を定義しています。

$ truffle console --network quorum
// コントラクトデプロイ
$ truffle(quorum)> var instance = await VendingContract.new()
// トランザクション実行(id=0の商品の購入)
$ truffle(quorum)> var tx = await instance.purchase(0)
// 実行結果の確認
$ truffle(quorum)> tx
{ tx: '0x02091d6f2ccdf0c1c608b0dcfa1174addef7235fb40f83b2d02a5aa257b90eb9',
  receipt:
   { blockHash: '0x1612f423b3cf376a4a65205a76b961958d9beae2dc176438ea1bd503ca2dcaf2',
     blockNumber: 567,
     contractAddress: null,
     cumulativeGasUsed: 31092,
     from: '0xed9d02e382b34818e88b88a309c7fe71e65f419d',
     gasUsed: 31092,
     logs: [ [Object] ],
     logsBloom: '0x00000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000001000000000000000000000000000000000000000000001000000000000000000200000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
     status: true,
     to: '0x586e8164bc8863013fe8f1b82092b028a5f8afad',
     transactionHash: '0x02091d6f2ccdf0c1c608b0dcfa1174addef7235fb40f83b2d02a5aa257b90eb9',
     transactionIndex: 0,
     rawLogs: [ [Object] ] },
  logs:
   [ { address: '0x586E8164bC8863013fe8F1b82092B028A5F8aFAd',
       blockNumber: 567,
       transactionHash: '0x02091d6f2ccdf0c1c608b0dcfa1174addef7235fb40f83b2d02a5aa257b90eb9',
       transactionIndex: 0,
       blockHash: '0x1612f423b3cf376a4a65205a76b961958d9beae2dc176438ea1bd503ca2dcaf2',
       logIndex: 0,
       removed: false,
       id: 'log_c6299d36',
       event: 'Stock',
       args: [Object] } ] }

戻り値にはログ(tx.logs)が含まれています。

// ログ確認
$ truffle(quorum)> tx.logs[0]
{ address: '0x586E8164bC8863013fe8F1b82092B028A5F8aFAd',
  blockNumber: 567,
  transactionHash: '0x02091d6f2ccdf0c1c608b0dcfa1174addef7235fb40f83b2d02a5aa257b90eb9',
  transactionIndex: 0,
  blockHash: '0x1612f423b3cf376a4a65205a76b961958d9beae2dc176438ea1bd503ca2dcaf2',
  logIndex: 0,
  removed: false,
  id: 'log_c6299d36',
  event: 'Stock',
  args:
   Result {
     '0': 'water',
     '1': <BN: 4>,
     __length__: 2,
     name: 'water',
     quantity: <BN: 4> } }

ログ確認

ここからが本題です。
上述のように、web3jsを使用してトランザクションを呼び出すと、戻り値にはログが含まれていました。
ただ実際には、後からトランザクションを照会して、実行結果からログを確認したいケースがありますので、その場合の手順を確認します。

まずは、getTransactionReceipt APIを呼んで、トランザクション実行結果を取得します。トランザクションハッシュ値は分かっていることが前提です。

$ truffle(quorum)> var txReceipt = await web3.eth.getTransactionReceipt("0x02091d6f2ccdf0c1c608b0dcfa1174addef7235fb40f83b2d02a5aa257b90eb9")
$ truffle(quorum)> txReceipt
{ blockHash: '0x1612f423b3cf376a4a65205a76b961958d9beae2dc176438ea1bd503ca2dcaf2',
  blockNumber: 567,
  contractAddress: null,
  cumulativeGasUsed: 31092,
  from: '0xed9d02e382b34818e88b88a309c7fe71e65f419d',
  gasUsed: '0x7974',
  logs:
   [ { address: '0x586E8164bC8863013fe8F1b82092B028A5F8aFAd',
       topics: [Array],
       data: '0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000057761746572000000000000000000000000000000000000000000000000000000',
       blockNumber: 567,
       transactionHash: '0x02091d6f2ccdf0c1c608b0dcfa1174addef7235fb40f83b2d02a5aa257b90eb9',
       transactionIndex: 0,
       blockHash: '0x1612f423b3cf376a4a65205a76b961958d9beae2dc176438ea1bd503ca2dcaf2',
       logIndex: 0,
       removed: false,
       id: 'log_c6299d36' } ],
  logsBloom: '0x00000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000001000000000000000000000000000000000000000000001000000000000000000200000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
  status: true,
  to: '0x586e8164bc8863013fe8f1b82092b028a5f8afad',
  transactionHash: '0x02091d6f2ccdf0c1c608b0dcfa1174addef7235fb40f83b2d02a5aa257b90eb9',
  transactionIndex: 0 }

戻り値のlogs[0].dataがログっぽいです。
ただ、16進数のバイト文字列で表現されていて、人の目で見てもよく分かりません。

そこで、デコードします。
発火したイベントが何かを知っている(この例では、発火されたイベントはStockであり、第一引数はstring型・第二引数はuint256型であると分かっている)ことを前提とする場合、web3jsのAPI decodeParameters を使って次のように確認できます。

$ truffle(quorum)> web3.eth.abi.decodeParameters(['string', 'uint256'], txReceipt.logs[0].data)
Result { '0': 'water', '1': '4', __length__: 2 }

とりあえず、それっぽいログを確認できました。
発火したイベントが何かを知っていれば、これで確認できます。

しかし、イベントのインターフェースを知っていることが前提とするのは、あまり現実的ではありません。

そこで、getTransactionReceiptのレスポンスに含まれるtopicsを確認します。

$ truffle(quorum)> txReceipt.logs[0].topics
[ '0xdea46a461417e7109bb02a22b41124d9ccea21aa4a17f24add83f98f4bd32f52' ]

この値は、イベントのインターフェース(イベント名と引数の型)によって導出されるハッシュ値です。
今回の例であるStockイベントのハッシュ値を計算すると、topicsに含まれる値と一致することが分かります。

$ truffle(quorum)> web3.utils.keccak256('Stock(string,uint32)')
'0xdea46a461417e7109bb02a22b41124d9ccea21aa4a17f24add83f98f4bd32f52'

ハッシュ値の計算方法の詳細はこちらに説明があります。

topics[0]: keccak(EVENT_NAME+"("+EVENT_ARGS.map(canonical_type_of).join(",")+")") (canonical_type_of is a function that simply returns the canonical type of a given argument, e.g. for uint indexed foo, it would return uint256). If the event is declared as anonymous the topics[0] is not generated;
引用:https://solidity.readthedocs.io/en/develop/abi-spec.html#events

このハッシュ値は、デプロイしたコントラクトのABIにもsignatureとして含まれています。ABIは、コントラクトが実装するコンストラクタ・メソッド・イベントのインターフェースの一覧です。ABIからsignatureの値をキーにして検索すれば、目的とするイベントのインターフェース情報を取得できそうです。

truffle(quorum)> VendingContract.abi
[ { inputs: [],
    payable: false,
    stateMutability: 'nonpayable',
    type: 'constructor',
    constant: undefined,
    signature: 'constructor' },
  { anonymous: false,
    inputs: [ [Object], [Object] ],
    name: 'Stock',
    type: 'event',
    constant: undefined,
    payable: undefined,
    signature: '0xdea46a461417e7109bb02a22b41124d9ccea21aa4a17f24add83f98f4bd32f52' },
  { constant: true,
    inputs: [ [Object] ],
    name: 'products',
    outputs: [ [Object], [Object], [Object] ],
    payable: false,
    stateMutability: 'view',
    type: 'function',
    signature: '0x7acc0b20' },
  { constant: true,
    inputs: [],
    name: 'getProducts',
    outputs: [ [Object] ],
    payable: false,
    stateMutability: 'view',
    type: 'function',
    signature: '0xc29b2f20' },
  { constant: false,
    inputs: [ [Object] ],
    name: 'purchase',
    outputs: [],
    payable: false,
    stateMutability: 'nonpayable',
    type: 'function',
    signature: '0xc7f04e65' },
  { constant: false,
    inputs: [ [Object], [Object] ],
    name: 'replenish',
    outputs: [],
    payable: false,
    stateMutability: 'nonpayable',
    type: 'function',
    signature: '0xbfa1efe7' } ]

最終的に、web3jsのdecodeLogを使うと、コントラクトのABIを元にログを確認できました。項目名と紐づけでログが出力されて、より分かりやすくなりました。

// eventのインターフェースを取得
$ truffle(quorum)> $ eventabi = VendingContract.abi.filter(element => element.signature == txReceipt.logs[0].topics);
// decodeLog(第一引数に)
$ truffle(quorum)> $ web3.eth.abi.decodeLog(eventabi[0].inputs, txReceipt.logs[0].data, txReceipt.logs[0].topics)
Result {
  '0': 'water',
  '1': '4',
  __length__: 2,
  name: 'water',
  quantity: '4' }

おわりに

今回は簡易なコントラクトでの確認でしたが、イベントの引数が配列であるケースや、複数のeventを発火させるケースなども、機会があれば調べたいと思います。

参考

ログのデコードに使用したweb3js API
https://web3js.readthedocs.io/en/v1.2.6/web3-eth-abi.html#web3-eth-abi

nthm
ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
ユーザーは見つかりませんでした