Ethereum
solidity
geth
Contract
コントラクト

PHPを使ってEthereumのスマートコントラクトを操作してみた

Ethereumでコントラクトを作成したり実行するためにはライブラリweb3.jsを使い、JavaScriptやnode.jsで実行します。

しかし、node.jsのPromiseなどをきちんと理解し、きれいにコーディングできるエンジニアの確保はなかなか難しいです。フレームワークもLaravelを使っているとnode.jsのAPIレイヤーを用意するのもなかなかスタートアップにはきついですw

そこで、gethが Json RPCに対応しているので、これを使いPHPから操作するサンプルを作成したので共有します。解説はあとで追記の予定ですので今はコメントを参考にしてください。

主な処理手順は下記です。

  • coinbaseやgasLimitの取得
  • コントラクトの作成
  • マイニングの完了待ち
  • コントラクトのset関数の実行
  • マイニングの完了待ち
  • コントラクトのget関数の実行

JSON RPCの仕様は下記にあります。
https://github.com/ethereum/wiki/wiki/JSON-RPC

下記のようにcoinbaseをunlockした状態でgethを起動しておけば動きます。

geth --datadir ~/gethdata --networkid 772  --nodiscover --rpc --rpcapi "db,eth,net,web3,personal,admin,miner" --rpccorsdomain="*" --rpcaddr "0.0.0.0" --mine --unlock 161dc166362a4805d6f405ad0db5363bdb3ef661 --password ~/coinbasepass.txt console

Ethereumでのコントラクトの基礎知識は私のスライドでハンズオン形式で解説していますので、初めての方はここを実践してから実行すると理解ははやいと思います。
- https://speakerdeck.com/mogiken/ethereumwosawatuteshi-gan-suruburotukutienhanzuon
- https://www.slideshare.net/mogiken1/ethereum-81133776

<?php

//JsonRPCをgethにPOST.毎回接続するのは非効率
function postGeth($data){
  $curl = curl_init();
  curl_setopt($curl, CURLOPT_URL, 'http://localhost:8545'); // エンドポイント
  curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST');
  curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
  curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
  $response = curl_exec($curl);
  curl_close($curl);
  $result = json_decode($response);
  return($result);
}

//参考 https://ethereum.gitbooks.io/frontier-guide/content/rpc.html

//Coinbaseを得る
$data = [
   "jsonrpc"=> "2.0",
   "method"=> "eth_coinbase",
   "params"=> [],
   "id" => 64
];
$coinbase = postGeth($data)->result;
print("\ncoinbase\n");
var_export($coinbase);

//gasLimitを得る.0ブロックの値を使用
$data = [
   "jsonrpc"=> "2.0",
   "method"=> "eth_getBlockByNumber",
   "params"=> ["0x0",true],
   "id" => 1
];
$gasLimit = postGeth($data)->result->gasLimit;
print("\ngasLimit\n");
var_export($gasLimit);

//下記Contract作成
/*
pragma solidity ^0.4.6;
contract SingleNumRegister {
        uint storedData;
        function set(uint x) {
                storedData = x;
        }
        function get() constant returns (uint retVal) {
            return storedData;
        }
}
*/
//http://ethereum.github.io/browser-solidity/で作成した上記コードのコンパイル結果を設定.
//CompileメニューのDetailボタンのポップアップ画面から[BYTECODE]=codeと[INTERFACE ABI]=ABIの2つをコピーする。
$code = "0x" . "6060604052341561000f57600080fd5b60d38061001d6000396000f3006060604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c14606e575b600080fd5b3415605857600080fd5b606c60048080359060200190919050506094565b005b3415607857600080fd5b607e609e565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a72305820de960eb93f505b302ca868e4e1c12f301aeaacf03c9e0bb6b4dc5352dde12e3b0029";
//$abi = '[{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"retVal","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]';
$data = [
   "jsonrpc"=> "2.0",
   "method"=> "eth_sendTransaction",
   "params"=> [
      [
        "from" => $coinbase,
        "gas" => $gasLimit,
        "data" => $code
      ]
    ],
   "id" => 1
];
$txHash = postGeth($data)->result;//トランザクションのハッシュ
print("\ntxHash\n");
var_export($txHash);

//ちゃんとトランザクションが作成されたのか確認
//値がない時はnonce errorとしてgethで発生している。
$data = [
   "jsonrpc"=> "2.0",
   "method"=> "eth_getTransactionByHash",
   "params"=> [$txHash],
   "id" => 1
];
$txHashDetail = postGeth($data)->result;//データがない時はnonce errorにする必要がある。ToDo
print("\ntxHashDetail\n");
var_export($txHashDetail);

//マイニングを待つ
//本当は、別プロセスで監視プログラム的に実行すべき。マイニングされない場合もあるのでタイムアウトも必要。ToDo
while(true){
  $data = [
     "jsonrpc"=> "2.0",
     "method"=> "eth_getTransactionReceipt",
     "params"=> [$txHash],
     "id" => 1
  ];
  $txReceipt = postGeth($data)->result;//マイニングされたらデータがある。
  //マイニング完了か?
  if($txReceipt){
    print("\ntxReceipt\n");
    var_export($txReceipt);
    //マイニング完了ならコントラクトのアドレスが存在する。ここにコントラクトが保存されている
    $contractAddress = $txReceipt->contractAddress;
    print("\ncontractAddress\n");
    var_export($contractAddress);

    //コントラクトでthrowされてないか確認。マニングされてもthrowされる可能性がある。
    $data = [
       "jsonrpc"=> "2.0",
       "method"=> "eth_getCode",
       "params"=> [$contractAddress,"latest"],
       "id" => 1
    ];
    $getCode = postGeth($data)->result;
    //codeが3文字よりあるなら実行されてる。それ以外はエラー処理。ToDo
    //1.6.x以降のgethではgenesis.json でhomesteadBlockの設定がないと常に0x0がかえるみたい。
    //参考:https://qiita.com/zaburo/items/9c8fab5da3b7d782ee89
    if(strlen($getCode) > 3){
      print("\ngetCode\n");
      var_export($getCode);
    }
    break;
  }
}

//コントラクトのsetメソッドを実行
//contractの値が変化するのでsendTransactionを使う。toにcontractのアドレスを指定する。
//関数名や引数はここを参考に変換する。かなり面倒。https://goo.gl/sYoS3v
//引数の変換はこのライブラリー使えるかも???https://goo.gl/qsEnX4

//http://ethereum.github.io/browser-solidity/でset(2)を実行した結果[input]の値を張り付けてます。
$codeFunc = "0x"."60fe47b10000000000000000000000000000000000000000000000000000000000000002";
$data = [
   "jsonrpc"=> "2.0",
   "method"=> "eth_sendTransaction",
   "params"=> [
      [
        "from" => $coinbase,
        "to" => $contractAddress,
        "gas" => $gasLimit,
        "data" => $codeFunc
      ]
    ],
   "id" => 1
];
$txHashSet = postGeth($data)->result;//トランザクションのハッシュ
print("\ntxHashSet\n");
var_export($txHashSet);

//あとはcontractを作成した時と同じ処理で
//ちゃんとトランザクションが作成されたのか確認
//値がない時はnonce errorとしてgethで発生している。
$data = [
   "jsonrpc"=> "2.0",
   "method"=> "eth_getTransactionByHash",
   "params"=> [$txHashSet],
   "id" => 1
];
$txHashDetail = postGeth($data)->result;//データがない時はnonce errorにする必要がある。ToDo
print("\ntxHashDetail\n");
var_export($txHashDetail);

//マイニングを待つ
//本当は、別プロセスで監視プログラム的に実行すべき。マイニングされない場合もあるのでタイムアウトも必要。ToDo
while(true){
  $data = [
     "jsonrpc"=> "2.0",
     "method"=> "eth_getTransactionReceipt",
     "params"=> [$txHashSet],
     "id" => 1
  ];
  $txReceipt = postGeth($data)->result;//マイニングされたらデータがある。
  //マイニング完了か?contractのアドレスは変化しない
  if($txReceipt){
    print("\ntxReceipt\n");
    var_export($txReceipt);

    //コントラクトでthrowされてないか確認。マニングされてもthrowされる可能性がある。
    $data = [
       "jsonrpc"=> "2.0",
       "method"=> "eth_getCode",
       "params"=> [$contractAddress,"latest"],
       "id" => 1
    ];
    $getCode = postGeth($data)->result;
    //codeが3文字よりあるなら実行されてる。それ以外はエラー処理。ToDo
    //1.6.x以降のgethではgenesis.json でhomesteadBlockの設定がないと常に0x0がかえるみたい。
    //参考:https://qiita.com/zaburo/items/9c8fab5da3b7d782ee89
    if(strlen($getCode) > 3){
      print("\ngetCode\n");
      var_export($getCode);
    }
    break;
  }
}

//getを実行

//http://ethereum.github.io/browser-solidity/でget()を実行した結果[input]の値を張り付けてます。
$codeFunc = "0x"."6d4ce63c";
$data = [
   "jsonrpc"=> "2.0",
   "method"=> "eth_call",
   "params"=> [
      [
        "from" => $coinbase,
        "to" => $contractAddress,
        "data" => $codeFunc
      ],
      "latest"
    ],
   "id" => 1
];
$getValue = postGeth($data)->result;//getの値
print("\ngetValue\n");
var_export($getValue);

※補足
genesis.jsonで下記のようにByzantiumBlock:0を設定しないと、getTransactionReceiptのstatus(0x0=エラー、0x1=正常)が取得できない。
getCodeを実行後、statusのチェックで実行時のエラーを確認する必要がある。

  "config": {
          "homesteadBlock": 0,
          "ByzantiumBlock":0
          }