python3
Blockchain
Ethereum
SmartContract

vDiceでみるブロックチェーン上のスマートコントラクトへのアクセスについて

Ethereumは、ブロックチェーン上のコントラクトを誰もが参照できる点に魅力がある。

ここではvDiceのサービスを例にブロックチェーン上にデプロイされたコントラクトメソッドを呼び出してみる。

このサービス自体はギャンブルなので、利用自体を推奨するわけではないので注意してほしい。

vDice:
https://www.vdice.io/
0.png

vDiceの特徴

-vDiceのコントラクトは0~9999までのランダムな数字を出力する。
-プレイヤーは出力される0~9999までの値で、コントラクトが出力する値を予測する。
-たとえば、出力される値が500以下と予測するのであればコントラクトAに、9000以下と予測するのであればコントラクトBのアドレスにETHを送付する。
-予想が的中すればオッズ分のETHが、自身のウォレットに上乗せされて返金される。また、予測される範囲の値は狭ければ狭いほど、的中したときにリターンとして得られる倍率は高くなる。(≦500のとき19.6倍)

やったこと

  • これまでvDiceのサービスとして実行された結果を確認する。
  • 過去vDiceのコントラクトが出力した値がまんべんなく0~9999の間に納まっているか確認する。 (今回9000以下と予測するコントラクトアドレスを対象に、これまでの勝敗の結果を出力してみる。 0~9999の間で9000以下と予測するため、結果自体も90%以内に収まっているか確認をする。)

 確認対象のコントラクトアドレス
 ・0x7DA90089A73edD14c75B0C827cb54f4248D47eCc (≦9000)

本家Webページを見ると、直近10件の取引履歴しか表示されていないが
ブロックチェーンの特性として、実行されたコントラクト結果は確認できるはず。

Step1.コントラクトコードを読む

まず、ブロックチェーンに組み込まれているvDiceのコントラクトを見てみよう。

https://etherscan.io/address/0x7DA90089A73edD14c75B0C827cb54f4248D47eCc#code

   function bet()
        payable
        onlyIfNotStopped {

        uint oraclizeFee = OraclizeI(OAR.getAddress()).getPrice("URL", ORACLIZE_GAS_LIMIT + safeGas);
        if (oraclizeFee >= msg.value) throw;
        uint betValue = msg.value - oraclizeFee;
        if ((((betValue * ((10000 - edge) - pwin)) / pwin ) <= (maxWin * getBankroll()) / 10000) && (betValue >= minBet)) {
            LOG_NewBet(msg.sender, betValue);
            bytes32 myid =
                oraclize_query(
                    "nested",
                    "[URL] ['json(https://api.random.org/json-rpc/1/invoke).result.random.data.0', '\\n{\"jsonrpc\":\"2.0\",\"method\":\"generateSignedIntegers\",\"params\":{\"apiKey\":${[decrypt] BIYkzb1GQzRZFNsTzF7fh+n8VmT8GEyW3mHYlrU8It5O6/bam6/LVVxqkury8YZDJPjm0mWQeqQGebGAVSFrFw16/VHJ65QMFBfIHN2frhav/d10ARqECjoOvse5v4/DIT3LQUHPEx0Z/5UdtqYTQydW/pbC5BM=},\"n\":1,\"min\":1,\"max\":10000${[identity] \"}\"},\"id\":1${[identity] \"}\"}']",
                    ORACLIZE_GAS_LIMIT + safeGas
                );
            bets[myid] = Bet(msg.sender, betValue, 0);
            betsKeys.push(myid);
        }
        else {
            throw;
        }
    }

vDiceのコントラクト自体、下記のurlからAPIにより乱数を取得していることがわかる。
https://api.random.org

次にgetBet関数、numBets関数を見てみる。

    function getBet(uint id)
        constant
        returns(address, uint, uint) {

        if (id < betsKeys.length) {
            bytes32 betKey = betsKeys[id];
            return (bets[betKey].playerAddress, bets[betKey].amountBet, bets[betKey].numberRolled);
        }
    }

    function numBets()
        constant
        returns(uint) {

        return betsKeys.length;
    }

getBet関数は引数にbetsKeys.length > idとなる値を入力することでプレイヤーのアドレス、Bet額、結果(numRoll)を返す。

下段にあるnumBets関数はbetsKeys.lengthを返すことがわかる

つまり、過去の結果をすべて出力するためには次のことさえできれば良い。

1.numBets関数でbetskeys配列の要素数を取得する。
2.getBet関数をbetskeys配列の要素数分実行する。

Step2.コントラクト結果を参照してみる

ためしにEtherscan上から、結果を確認する。

getBet関数にid=7600を入力し、Queryボタンを押す。

https://etherscan.io/address/0x7DA90089A73edD14c75B0C827cb54f4248D47eCc#readContract
9.png

出力されるのは、上から次のとおり
・プレイヤーアドレス
・ベット額(ETHの単位にするには10^18で割る)
・結果

Step3.環境の準備

 Ethereumのライブネットと同期した自ノードの環境を準備する。
 http://qiita.com/umidachi/items/44a50ce86e94ff0df8f2

Step4.コントラクトメソッドをABI形式にする。

次のコントラクトメソッドについて、ABI形式のファンクションパラメータを作る

  • numBets関数
gethコンソール
> web3.sha3("numBets()").substring(0,10)

結果:
"0xdf06f906"

numBets関数は引数がない。そのためファンクションパラメータは16進4バイトデータだけですむ。

  • getBets関数
gethコンソール
> web3.sha3("getBet(uint256)").substring(0,10)

結果:
"0x061e494f"

一方、getBet関数の引数はuint256。
そのため、abiのメソッドパラメータは16進4バイトデータ+
32バイトデータの構成になる。

pythonでabiパラメータを作るとしたら、次の記述だと楽に作れる。

b="0x061e494f" #getBet(uint256)
d="{0:064x}".format((bet_id))
getBet_abi=b+d

Step5.pythonで実装する。

サービス当初のベット結果から直近の結果を得るまで、pythonの記載は次のようになる。

vDice.py
import requests
import json

#Json-rpcでeth_callを呼び出す
def eth_call(url,to,data):
    headers = {'content-type': 'application/json'}
    payload = {
        "jsonrpc": "2.0",
        "method": "eth_call",
        "params":[{"to":to,
                   "data":data},
                   "latest"],
        "id": 0
    }

    response = requests.post(url, data=json.dumps(payload), headers=headers).json()
    #print(response)
    return response['result']

def numBet():
    contract_address={"vdice":"0x7DA90089A73edD14c75B0C827cb54f4248D47eCc"}
    numBets_abi="0xdf06f906"
    result=eth_call(url,contract_address['vdice'],numBets_abi)
    i=int(result,16)
    return i

def getBet(bet_id):
    contract_address={"vdice":"0x7DA90089A73edD14c75B0C827cb54f4248D47eCc"}
    b="0x061e494f" #getBet(uint256)
    d="{0:064x}".format((bet_id))
    getBet_abi=b+d

    result=eth_call(url,contract_address['vdice'],getBet_abi)

    d=result[2:194]    #先頭0xの表記を省略
    #print(d)
    print("PlayerAddress:",d[24:64])
    print("AmountBet:",int(d[64:128],16)/pow(10,18))
    print("numRoll:",int(d[129:193],16))


if __name__ == "__main__":
    url = "http://localhost:8545"
    betnum=numBet()

    print("---betnum:",betnum)
    for i in range(1,betnum):
        print("---num:",i)
        getBet(i-1)

今回、betNumは7642が最大であるため、0~7642回分のgetBet関数の結果を出力する。

出力結果

---betnum: 7642
---num: 1
PlayerAddress: d19e13d40188a0123da1561c8f063ce5cadecd56
AmountBet: 0.22
numRoll: 7821
---num: 2
PlayerAddress: d19e13d40188a0123da1561c8f063ce5cadecd56
AmountBet: 0.2104874128878282
numRoll: 3903
---num: 3
PlayerAddress: 3f69019bc9d6e62432900caa46021e16c1f376a6
AmountBet: 0.2104874128878282
numRoll: 7746
---num: 4
PlayerAddress: 3f69019bc9d6e62432900caa46021e16c1f376a6
AmountBet: 0.2104874128878282
numRoll: 353
---num: 5
PlayerAddress: d19e13d40188a0123da1561c8f063ce5cadecd56
AmountBet: 0.2104874128878282

........
........

補足:
getBet関数の生のレスポンスは例として次のようになる

getBetのレスポンス
{'jsonrpc': '2.0', 'id': 0, 'result': '0x0000000000000000000000006bd933140ff6b40
630ef12928979eb22d17ddc7100000000000000000000000000000000000000000000000004174e8
b1d32337e0000000000000000000000000000000000000000000000000000000000000056'}

'result'に結果が格納されるが内容は次のとおりとなっている。

・プレイヤーアドレス [32byte]
・ベット額(ETHの単位にするには10^18を割る)[32byte]
・結果(0~9999)[32byte]

先頭0xを含めると194文字が返却されるため
受け入れ結果をわかりやすくするため次の記載をしている。

参考
    d=result[2:194]    #先頭0xの表記を省略
    print("PlayerAddress:",d[24:64])
    print("AmountBet:",int(d[64:128],16)/pow(10,18))
    print("numRoll:",int(d[129:193],16))

確認結果

取得結果をCSVに出力するコードに書き換え、下図のとおり散布図にしてみた。

chart.png

横軸がベット回数。右に行くほど、直近の結果になる。
縦軸はベット結果(0~9999)を示す。

図を見る限り満遍なくプロットされているように見える。

結果として
- 総ベット数:7642回
- 出力結果が9001以上になった回数(つまり予想が外れた回数):762回

つまり
9000以下が出力される確率: 1 - ( 762/7642 ) =0.9003 ≒ 90 %
 
上記結果から、次のvDiceのコントラクトは統計的に90%相当の確率で的中することがわかる。
 ・0x7DA90089A73edD14c75B0C827cb54f4248D47eCc (≦9000)

ブロックチェーンは同期さえしていれば、不特定多数の人間がコントラクトの結果を見ることができる、興味深い仕組みだ。