自分を攻略していく記録

自分がやりたいことを達成するには何をすればいいのか、その攻略していく過程をつらつらと

Truffleを使ってEthereumのスマートコントラクトを実装してみる

Ethereumとは

f:id:ngo275:20171118183351p:plain

Ethereum とは Ethereum Virtual Machine (EVM) 上でスマートコントラクトを実行するためのプラットフォームである。ビットコインスクリプトよりも表現力が高く、チューリング完全であると言われている。また、あらゆるスマートコントラクトを実行することができる World Computer である、とも言われている。EVM上で実行されるすべての処理はEthereumのブロックチェーンに参加している全ノードで同時に実行されるが、その実行のコストとして gas が消費される。このgasこそがコインチェックやbitFlyerで扱われている ETH である。

EVM上で実行されるのは PUSH1 0 CALLDATALOAD SLOAD NOT PUSH1 9 JUMPI STOP JUMPDEST PUSH1 32 CALLDATALOAD PUSH1 0 CALLDATALOAD SSTORE のようなopcodeである。さすがにこれを人が実装するのは大変なので、高級言語として Solidity などの言語が開発されている。ここでは一番人気のあるSolidityを利用する。

Ethereumには本番のブロックチェーンネットワーク mainnetropstenkovanrinkeby といった開発用のtestnetが存在する。そして、開発や利用するにはそのネットワークに繋がるためのクライアントが必要になる。golangで実装されている geth や先日バグで問題が発覚して話題になった parity などがある。個人的にはparityはわかりやすくて好き。

Truffleを使って開発してみる

TruffleConsenSys が開発しているSolidityのフレームワークである。これを利用すると簡単にオリジナルのトークンも作ることができる。2017年11月に発表されたTruffleV4を使うとtestrpc(ローカルの開発用のネットワークのイメージ)も組み込まれているのでテストも簡単に実行できる。ここではテストは割愛。

github.com

セットアップする

$ npm install -g truffle

$ mkdir solidity-sample
$ cd solidity-sample
$ truffle init

これでプロジェクトの雛形が作成される。これをもとにスマートコントラクトを実装していく。

スマートコントラクトを実装する

$ truffle create contract ProofOfExistence

とするとファイルが contracts 以下に自動生成される。これを以下のように実装してみる。

pragma solidity ^0.4.17;

contract ProofOfExistence {
  mapping (bytes32 => bool) private proofs;

  function storeProof(bytes32 proof) {
    proofs[proof] = true;
  }

  function notarize(string document) {
    var proof = proofFor(document);
    storeProof(proof);
  }

  function proofFor(string document) constant returns (bytes32) {
    return sha256(document);
  }

  function checkDocument(string document) constant returns (bool) {
    var proof = proofFor(document);
    return hasProof(proof);
  }

  function hasProof(bytes32 proof) constant returns(bool) {
    return proofs[proof];
  }
}

testrpcで確認してみる。

次に、このスマートコントラクトを確認するために migrations/2_deploy_contracts.js を作成する。

var ProofOfExistence = artifacts.require("./ProofOfExistence.sol");

module.exports = function(deployer, network, accounts) {
  deployer.deploy(ProofOfExistence);
};

そして、ターミナルを開いてTruffleのtestrpcを開く。

$ truffle develop
...

truffle(default)> migrate --reset
...
truffle(default)> var p = ProofOfExistence.at(ProofOfExistence.address)
truffle(default)> p.checkDocument('Hello world');
false
truffle(default)> p.notarize('Hello world');
truffle(default)> p.checkDocument('Hello world');
true

このようにスマートコントラクトの処理が確認できる。

testnet kovanにデプロイする

parityでkovanを利用してみる。parityがインストールされてなければインストールする。この作業はせずにいきなり web3 で動作確認しても問題ない。

truffle.js で利用するネットワークを書いておく。

module.exports = {
  networks: {
    development: {
      host: 'localhost',
      port: 8545,
      network_id: '42'
    }
  }
};

parity --chain kovan --rpccorsdomain "*" を呼んで動かしている状態で、parityを開く。必要であればアカウントの登録を済ます。kovan上でのETHがなければfaucetでアドレスをポストすればすぐもらえる。syncに結構時間がかかるので注意。

gitter.im

次に、 truffle migrate を実行してkovanにデプロイする。

parityで動作確認する

画像のようにSETTINGSからCONTRACTSタブを追加しておく。

f:id:ngo275:20171118175509p:plain

CONTRACTSタブを選択し、 + WATCH からさきほどデプロイしたスマートコントラクトを登録する。デプロイした時の、デプロイ先のアドレスを貼り付ける。また、migrateコマンドを実行した時に生成された build/contracts/ProofOfExistence.json の中の abi の部分(配列ごと)もコピーして貼り付ける。

f:id:ngo275:20171118174454p:plain

f:id:ngo275:20171118174653p:plain

これで登録完了なので、実際にfunctionが機能しているか確認してみる。

f:id:ngo275:20171118175726p:plain

hello とだけ入力して checkDocument をクエリしても何も起こらない。

次にCONTARACTSタブのすぐ下にある EXECUTE を押して、 notarize を実行して hello を記録する。

f:id:ngo275:20171118175851p:plain

もう一度さきほどと同様に checkDocumenthello を渡して呼んでみる。すると、 true が返ってくる。すごく微妙だが動作は確認できた。

f:id:ngo275:20171118180125p:plain

mainnetはまたsyncに時間がかかりそうなので一旦後回し...。

ついでにweb3でたたいてみる

parityやgethはそれぞれのノードがブロックチェーンを保持して常に同期されている。さすがにモバイルアプリや普通のブラウザでそのままEthereumを利用するにはリソースを食い過ぎてしまう。そこで web3 というEthereumとのインターフェースになるライブラリを使って、さきほど作成したスマートコントラクトを普通のブラウザから利用してみる。

app/index.html を新規作成して以下のように編集してみる。abiとコントラクトアドレスはさきほど利用したものと同じものである。 notarize を実行する時に認証があるが、その認証を毎回聞かれないように、ここでは ps.txt というファイルを作成して、そこにパスワードを書いて保存し、 parity --chain kovan --rpccorsdomain "*" --unlock 0x005041C1a70B270DB90adaEbb109f4C9501d2C6B --password pw.txt でparityを起動する。pw.txtはもれないように、 .gitignore に書いておくなりしておく。 --unlock の後には自分のkovanでのアドレスを入れておく。 parity --chain kovan --rpccorsdomain "*" でparityを起動すると、 http://localhost:8545 でweb3がEthereum(kovan)にアクセスできる。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <title>Ethereum web3 Sample</title>
    <script src="https://cdn.rawgit.com/ethereum/web3.js/0.19.0/dist/web3.min.js"></script>
    <script>
      web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
      abi = [     {       "constant": true,       "inputs": [         {           "name": "document",           "type": "string"         }       ],       "name": "checkDocument",       "outputs": [         {           "name": "",           "type": "bool"         }       ],       "payable": false,       "stateMutability": "view",       "type": "function"     },     {       "constant": false,       "inputs": [         {           "name": "document",           "type": "string"         }       ],       "name": "notarize",       "outputs": [],       "payable": false,       "stateMutability": "nonpayable",       "type": "function"     },     {       "constant": false,       "inputs": [         {           "name": "proof",           "type": "bytes32"         }       ],       "name": "storeProof",       "outputs": [],       "payable": false,       "stateMutability": "nonpayable",       "type": "function"     },     {       "constant": true,       "inputs": [         {           "name": "proof",           "type": "bytes32"         }       ],       "name": "hasProof",       "outputs": [         {           "name": "",           "type": "bool"         }       ],       "payable": false,       "stateMutability": "view",       "type": "function"     },     {       "constant": true,       "inputs": [         {           "name": "document",           "type": "string"         }       ],       "name": "proofFor",       "outputs": [         {           "name": "",           "type": "bytes32"         }       ],       "payable": false,       "stateMutability": "view",       "type": "function"     }   ];
      address = '0x6B3Ac171153526C4B6d61500c21335B893E22548';
      contract = web3.eth.contract(abi).at(address);

      function getInput() {
        let get = {};
        let query = window.location.search.substring(1).split("&");
        for (let i in query) {
          if (query === "") continue;
          const param = query[i].split("=");
          get[decodeURIComponent(param[0])] = decodeURIComponent(param[1] || "");
        }
        return get.word;
      }
      function checkDocument() {
        const word = getInput();
        const res = contract.checkDocument(word);
        document.write(`The result of ProofOfExistence.checkDocument( ${word} ) is ${res}`);
      }
      function notarize(word) {
        console.log(word);
        const res = contract.notarize(word).call()
      }
    </script>
  </head>
  <body>
    <h1>Ethereum Smart Contract Sample</h1>
    <p>Latest block: <script>document.write(web3.eth.blockNumber + "<br><br>")</script></p>
    <form onSubmit="return notarize()">
      Notarize a word
      <input type=text size=50 name=word><input type="submit">
    </form>
    <form>
      CheckDocument a word
      <input type=text size=50 name=word><input type="submit">
    </form>
    <br>
    <p><script>checkDocument()</script></p>
  </body>
</html>

app/index.html を保存したら、 python -m SimpleHTTPServer などのコマンドでサーバーを起動して http://localhost:8000 でアクセスしてみる。一度notarizeで登録した単語をcheckDocumentで確認するとtrueになるはず。

f:id:ngo275:20171119144338p:plain

GitHubにあげています。

github.com

参考

truffleframework.com

github.com

blog.zeppelin.solutions

blog.infura.io