はじめに

これは ドリコム Advent Calendar 2018 の1日目です。

自己紹介

どうも、DRIP エンジニアの小川です。
DRIP は Drecom Invention Project の略称で、ドリコムが発明を産み続けるためのプロジェクトです。
今年は AR とブロックチェーンをメイン領域として活動しています。

DRIP - Drecom Invention Project

今回も DApps に関するお話です。
過去記事はこちら。

Ethereum と連携する DApps を作る場合、スマートコントラクトの開発は避けて通れません。
スマートコントラクトに定義したメソッドを呼び出す場合、その内部ではメソッドの Method ID を指定する必要があるのですが、今回はその Method ID について気になる事があったので、調べた事をまとめておきます。
そもそも「Method ID て何?」という方が大半かと思いますので、まずは DApps 内で行われるスマートコントラクトへのアクセスがどんなものなのかというところから始めさせていただきます。

  • Ethereum(イーサリアム) とは「分散アプリケーションのためのプラットフォーム」であり、ブロックチェーンをベースに構築されています。
  • DApps とは「ブロックチェーンを利用した非中央集権の分散型アプリケーション」の事で、本稿では「Ethereum のブロックチェーンを利用したアプリケーション」を指します。参考情報はこちら。
  • スマートコントラクトとは「契約のスムーズな検証、執行、実行、交渉を意図したコンピュータプロトコル」の事で、本稿では Ethereum 上にデプロイ可能かつ動作するコードそのものを指します。

スマートコントラクトへのアクセス

DApps の中でも Web ブラウザをインターフェイスとして動作しているものは、ブラウザからスマートコントラクトへアクセスする際に web3 を用いているケースが大半です。
web3 に ABI と スマートコントラクトデプロイ先のアドレスを渡して Contract オブジェクトを生成し、そのオブジェクトからスマートコントラクトのメソッドを叩きます。
ABI は Application Binary Interface の略で、web3 がスマートコントラクトとやり取りする時に必要な情報です。
Solidity のビルドで出力される JSON ファイルの中に記述されています。

// 例:という発行したトークン総数を取得できる totalSupply() が定義されたコントラクト
contract = new web3.eth.Contract(contractAbi, contractAddress)
contract.methods.totalSupply().call({from: currentWalletAddress})
  .then(result => {
    console.log(result) // 10000000
  })
  • web3 とは主に Web フロントエンドで使用する Ethereum JavaScript API である web3.js の略称です。
  • Solidity とは Ethereum Smart Contract を開発可能な言語の1つで、自由度が高く情報も豊富なため多くの DApps 開発者に利用されています。

Ethereum クライアント上で動作するスマートコントラクトにアクセスするには JSON-RPC プロトコルに沿ったリクエストを投げる必要があり、web3 は内部でそのリクエストを生成し通信しています。
リクエストの中に「data」という項目があり、そこには「どのメソッドにアクセスするか」と「引数それぞれの値」が示された 16進数 バイトコードを記述します。
そして「data」の先頭4バイトによって、スマートコントラクトにあるどのメソッドにアクセスするかを指定します。
Ethereum において、メソッドを表す先頭4バイトは「Method ID」と言われ、メソッド名と引数のハッシュによって生成されます。

function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256) {
  uint256 tokenIndex = tokenOwnerMap[_owner][_index]
  return tokens[tokenIndex].id
}

-----
methodName = 'tokenOfOwnerByIndex(address,uint256)'
methodHash = web3.sha3(methodName) // '0x2f745c59a57ba1667616e5a9707eeaa36ec97c283ee24190b75d9c8d14bcb215'
methodId = methodHash.substr(2,8) // '2f745c59'

上記のように、メソッド名と引数の型をスペース無しでまとめた文字列を keccak256(sha3) ハッシュ化し、0x を除いた先頭4バイトつまり8文字が Method ID となります。

Method ID の衝突

この仕様を知った時、思いました。
「ハッシュを利用しているということは、奇跡的に衝突する可能性もあるのか…」
衝突とは、異なる2つのインプットから全く同じハッシュが生成される現象を言います。
今回のケースで言うと、もし全く同じ Method ID になるメソッドを同じスマートコントラクト内で定義した場合、片方のメソッドは呼び出し不可になってしまう可能性があります。
(data の持つ引数の種類や個数によって判別してくれる可能性はありますが、未検証です)
生成したハッシュのうち4バイトしか使ってないから衝突しやすいのか?と思いきや確率はおよそ 0.00000002% なので、遭遇する事はほぼ無いと言って良さそうです。

が!

可能性がゼロではない以上、一応気を払っておくに越した事はありません。

意図的に衝突させてみた

同名メソッド

同名かつ同引数なメソッドからなら確実に一致する Method ID が作られます。

pragma solidity ^0.4.22;

contract ColTest {
  function withdraw(uint256 _index) pure external returns (uint256) {
    return _index * 2;
  }

  function withdraw(uint256 _index) pure external returns (uint256) {
    return _index * 4;
  }
}
Compiling ./contracts/ColTest.sol...
Compiling ./contracts/Migrations.sol...

/Users/ogawa_mitsunori/Projects/test/collision-test/contracts/ColTest.sol:4:3: DeclarationError: Function with same name and arguments defined twice.
  function withdraw(uint256 _index) pure external returns (uint256) {
  ^ (Relevant source part starts here and spans across multiple lines).
/Users/ogawa_mitsunori/Projects/test/collision-test/contracts/ColTest.sol:8:3: Other declaration is here:
  function withdraw(uint256 _index) pure external returns (uint256) {
  ^ (Relevant source part starts here and spans across multiple lines).
Compilation failed. See above.

Function with same name and arguments defined twice.
同じ名前で同じ引数のメソッドが2回定義されてる、というエラーになります。
そもそも Method ID 関係ない。

異名メソッド

ちゃんと衝突するような別名のメソッドを見つけられるのか?と思っていたが意図的に衝突するメソッド定義を見つけた人の投稿を見つけたので、それを使います。

pragma solidity ^0.4.22;

contract ColTest {
  function withdraw(uint256 _index) pure external returns (uint256) {
    return _index * 2;
  }

  function OwnerTransferV7b711143(uint256 _index) pure external returns (uint256) {
    return _index * 3;
  }
}
Compiling ./contracts/ColTest.sol...
Compiling ./contracts/Migrations.sol...

/Users/ogawa_mitsunori/Projects/test/collision-test/contracts/ColTest.sol:3:1: TypeError: Function signature hash collision for OwnerTransferV7b711143(uint256)
contract ColTest {
^ (Relevant source part starts here and spans across multiple lines).
Compilation failed. See above.
ogawa_mitsunori@YG48-35273 ~/Projects/test/collision-test $

なんと、同じ Method ID になる異名メソッドをコンパイルすると Function signature hash collision for xxxx というエラーが返ってきます!
コンパイラが面倒見てくれるとわかったので、Method ID が衝突したままデプロイしちゃう、みたいなことは起きなさそうです。
よかったよかった。

余談:テストを書いた

コンパイラが面倒見てくれると知らなかった世界線の自分が「これテストで検知できるようにしておけばいいんじゃないか?」と思い立ち作り出したテストコードを書き残しておきます。

const MyContract = artifacts.require("MyContract")

contract('Method ID', (accounts) => {
  let methods = Array()

  before(async function () {
    MyContract._json.abi.filter(method => method.type && method.type === 'function')
      .forEach(method => {
        const argments = method.inputs.map(input => input.type)
        methods.push(`${method.name}(${argments.join(',')})`)
      })
  })

  describe('Not duplicated', () => {
    it('method names', () => {
      const ids = methods.map(name => { return web3.sha3(name).substr(2,8) })
      const multipleIds = ids.filter((x, i, self) => { return self.indexOf(x) !== self.lastIndexOf(x) })
      assert.isEmpty(multipleIds)
    })
  })
})

スマートコントラクトの ABI からメソッドだけを抽出し、名前と引数から Method ID を作成して重複がないかどうかをチェックするテストコードです。
コンパイラが検知してくれる事がわかった今、このテストコードを使う事は無いだろう。
ありがとう、さようなら。

まとめ

というわけで、今回は Method ID 重複時の挙動をまとめました。
ただし上記に書いた通り衝突する確率は 0.00000002% しかなく、ジャンボ宝くじで 1等 を当てる確率 0.00001% より更に小さいので、普通に開発していれば Function signature hash collision for xxxx に遭遇する事はほぼ無いんじゃないかなと思います。
もし自然なメソッド定義で遭遇した方いらっしゃったら是非教えてください。その運にあやからせていただきます。

さいごに

明日は sonoda さんです。