Ethereumは、ブロックチェーン上のコントラクトを誰もが参照できる点に魅力がある。
ここではvDiceのサービスを例にブロックチェーン上にデプロイされたコントラクトメソッドを呼び出してみる。
このサービス自体はギャンブルなので、利用自体を推奨するわけではないので注意してほしい。
vDice:
https://www.vdice.io/
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のコントラクトを見てみよう。
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
出力されるのは、上から次のとおり
・プレイヤーアドレス
・ベット額(ETHの単位にするには10^18で割る)
・結果
Step3.環境の準備
Ethereumのライブネットと同期した自ノードの環境を準備する。
http://qiita.com/umidachi/items/44a50ce86e94ff0df8f2
Step4.コントラクトメソッドをABI形式にする。
次のコントラクトメソッドについて、ABI形式のファンクションパラメータを作る
- numBets関数
> web3.sha3("numBets()").substring(0,10)
結果:
"0xdf06f906"
numBets関数は引数がない。そのためファンクションパラメータは16進4バイトデータだけですむ。
- getBets関数
> 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の記載は次のようになる。
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関数の生のレスポンスは例として次のようになる
{'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に出力するコードに書き換え、下図のとおり散布図にしてみた。
横軸がベット回数。右に行くほど、直近の結果になる。
縦軸はベット結果(0~9999)を示す。
図を見る限り満遍なくプロットされているように見える。
結果として
- 総ベット数:7642回
- 出力結果が9001以上になった回数(つまり予想が外れた回数):762回
つまり
9000以下が出力される確率: 1 - ( 762/7642 ) =0.9003 ≒ 90 %
上記結果から、次のvDiceのコントラクトは統計的に90%相当の確率で的中することがわかる。
・0x7DA90089A73edD14c75B0C827cb54f4248D47eCc (≦9000)
ブロックチェーンは同期さえしていれば、不特定多数の人間がコントラクトの結果を見ることができる、興味深い仕組みだ。