Ethereumスマートコントラクトでルーレットゲームの整合性を確認する





今日、暗号通貨、特にビットコインについて聞いたことのある人はほとんどいません。 2014年、ビットコインへの関心をきっかけに、新しい暗号通貨-Ethereumが登場しました。 今日、2017年には、ビットコインに続いて大文字で2番目になります。 ビットコインとの最も重要な違いの1つは、チューリング完全な仮想マシンであるEVMの使用です。 放送に関する詳細情報は、彼のイエローペーパーに記載されています。



イーサリアムスマート契約は通常、 Solidityで記述されています。 Habréには、スマートコントラクトの作成とテストに関する記事が既にありました(例1、2、3) 。 また、スマートコントラクトとサイトの接続については、たとえば、 スマートコントラクトの簡単な投票の作成に関する記事を読むことができます。 この記事ではMistウォレットに組み込まれたブラウザーを使用しますが、 MetaMaskなどのChromeプラグインを使用しても同じことができます。 これが、MetaMaskを通じてゲームがどのように動作するかを示しています。



ゲームはヨーロピアンルーレットの実装です。フィールドには0から36までの番号が付けられた37個のセルがあります。特定の数字または数字のセット(偶数/奇数、赤/黒、1-12、1-18など)に賭けることができます。 各ラウンドでは、ゲームテーブルの対応するフィールドにトークン(0.01 ETH≈$ 0.5に相当)を追加することにより、複数のベットを行うことができます。 各フィールドはペイアウト率に対応しています。 たとえば、赤の賭けは係数2に対応します。つまり、0.01 ETHを支払うと、勝つと0.02 ETHを受け取ります。 そして、ゼロに賭けた場合、係数は36になります。同じ0.01 ETHを賭けに支払うことで、勝った場合0.36を受け取ります。



ただし、開発者は別の名称を使用します:35:1
契約コードでは、このベットの係数も35として示され、支払い前にベット額が勝ち額に加算されます。 おそらくこれはゲームの世界で採用されている指定ですが、36をすぐに使用する方が理にかなっています。



すべての賭けが行われたら、プレーヤーは「再生」ボタンをクリックし、MetaMaskを介して、ゲームのスマートコントラクトのアドレスへの賭け*をイーサリアムブロックチェーンに送信します。 契約は引き数を決定し、ベットの結果を計算し、必要に応じてプレーヤーに賞金を送ります。

*- ベットという用語は、プレーヤーが1ラウンド以内に行う一連のベット(つまり、ベットタイプのペア-ベットごとのトークンの数)を指すために使用します。 より正確な用語を知っている場合は、私に書いてください、私はそれを修正します。



ゲームが正直に機能するかどうかを理解するために(つまり、カジノはドロップされた数の決定を有利に操作しないのか)、スマートコントラクトの作業を分析します。



彼の住所はゲームのウェブサイトにリストされています。 さらに、支払いを確認する前に、ベットの送付先住所を確認できます。 0xDfC328c19C8De45ac0117f836646378378c10e0CdA3で契約を分析します 。 Etherscanはコードを表示します。簡単に表示するには、 Solidity Browserを使用できます。



契約は、 placeBet()関数の呼び出しで始まります。



コードシート
function placeBet(uint256 bets, bytes32 values1,bytes32 values2) public payable { if (ContractState == false) { ErrorLog(msg.sender, "ContractDisabled"); if (msg.sender.send(msg.value) == false) throw; return; } var gamblesLength = gambles.length; if (gamblesLength > 0) { uint8 gamblesCountInCurrentBlock = 0; for(var i = gamblesLength - 1;i > 0; i--) { if (gambles[i].blockNumber == block.number) { if (gambles[i].player == msg.sender) { ErrorLog(msg.sender, "Play twice the same block"); if (msg.sender.send(msg.value) == false) throw; return; } gamblesCountInCurrentBlock++; if (gamblesCountInCurrentBlock >= maxGamblesPerBlock) { ErrorLog(msg.sender, "maxGamblesPerBlock"); if (msg.sender.send(msg.value) == false) throw; return; } } else { break; } } } var _currentMaxBet = currentMaxBet; if (msg.value < _currentMaxBet/256 || bets == 0) { ErrorLog(msg.sender, "Wrong bet value"); if (msg.sender.send(msg.value) == false) throw; return; } if (msg.value > _currentMaxBet) { ErrorLog(msg.sender, "Limit for table"); if (msg.sender.send(msg.value) == false) throw; return; } GameInfo memory g = GameInfo(msg.sender, block.number, 37, bets, values1,values2); if (totalBetValue(g) != msg.value) { ErrorLog(msg.sender, "Wrong bet value"); if (msg.sender.send(msg.value) == false) throw; return; } address affiliate = 0; uint16 coef_affiliate = 0; uint16 coef_player; if (address(smartAffiliateContract) > 0) { (affiliate, coef_affiliate, coef_player) = smartAffiliateContract.getAffiliateInfo(msg.sender); } else { coef_player = CoefPlayerEmission; } uint256 playerTokens; uint8 errorCodeEmission; (playerTokens, errorCodeEmission) = smartToken.emission(msg.sender, affiliate, msg.value, coef_player, coef_affiliate); if (errorCodeEmission != 0) { if (errorCodeEmission == 1) ErrorLog(msg.sender, "token operations stopped"); else if (errorCodeEmission == 2) ErrorLog(msg.sender, "contract is not in a games list"); else if (errorCodeEmission == 3) ErrorLog(msg.sender, "incorect player address"); else if (errorCodeEmission == 4) ErrorLog(msg.sender, "incorect value bet"); else if (errorCodeEmission == 5) ErrorLog(msg.sender, "incorect Coefficient emissions"); if (msg.sender.send(msg.value) == false) throw; return; } gambles.push(g); PlayerBet(gamblesLength, playerTokens); }
      
      





Solidityを初めて使用する場合は、パブリックおよび支払い可能な修飾子は、関数がコントラクトAPIの一部であり、呼び出されたときにエーテルを送信できることを意味することを説明します。 この場合、送信者に関する情報とブロードキャストエアの量は、msg変数を介して利用できます。



コールパラメータは、ベットタイプのビットマスクと、各タイプのトークン数を含む2つの32バイト配列です。 GameInfoタイプの定義と関数getBetValueByGamble()getBetValue()を見れば、これを推測できます。



別の小さなコードシート
 struct GameInfo { address player; uint256 blockNumber; uint8 wheelResult; uint256 bets; bytes32 values; bytes32 values2; }
      
      





 // n - number player bet // nBit - betIndex function getBetValueByGamble(GameInfo memory gamble, uint8 n, uint8 nBit) private constant returns (uint256) { if (n <= 32) return getBetValue(gamble.values , n, nBit); if (n <= 64) return getBetValue(gamble.values2, n - 32, nBit); // there are 64 maximum unique bets (positions) in one game throw; }
      
      



 // n form 1 <= to <= 32 function getBetValue(bytes32 values, uint8 n, uint8 nBit) private constant returns (uint256) { // bet in credits (1..256) uint256 bet = uint256(values[32 - n]) + 1; if (bet < uint256(minCreditsOnBet[nBit]+1)) throw; //default: bet < 0+1 if (bet > uint256(256-maxCreditsOnBet[nBit])) throw; //default: bet > 256-0 return currentMaxBet * bet / 256; }
      
      





getBetValue()はトークンではなくweiでベット額を返すことに注意してください。



最初に、 placeBet()は契約がオフになっていないことを確認してから、ベットチェックを開始します。

ギャンブル配列は、この契約で行われたすべての賭けのリポジトリです。 placeBet()は、ブロック内のすべての賭けを見つけ、指定されたプレーヤーがこのブロックで別の賭けを送信したかどうか、およびブロックで許可された賭けの数を超えたかどうかを確認します。 次に、最小および最大ベット額の制限がチェックされます。



エラーが発生した場合、throwコマンドによってコントラクトの実行が中断され、トランザクションがロールバックされ、プレイヤーにエーテルが返されます。



さらに、関数に渡されるパラメーターはGameInfo構造体に保存されますwheelResultフィールド数値37に初期化されることが重要です。



賭け金が送信されたエーテルの量と一致することを再度確認した後、RLTトークンが配布され、紹介プログラムが処理され、賭け情報がギャンブルに保存され、その番号と賭け金でPlayerBetイベントが生成され、ゲームのWebパーツに表示されます。



トークンについて
各ベットで、プレイヤーには一定量のRLT、イーサリアムトークンが与えられます。これは、ゲームの作者が受け取った利益から配当を受け取るトークンホルダーの権利を決定します。 詳細については、 ホワイトペーパーをご覧ください



ベットのさらなるライフはProcessGames()関数の呼び出しから始まります。この関数は、新しいベットが出現した後、現在アドレス0xa92d36dc1ca4f505f1886503a0626c4aa8106497から実行されています。 このような呼び出しは、ゲームコントラクトのトランザクションリストを表示するときに明確に表示さます。Value= 0です。

ProcessGamesコード
 function ProcessGames(uint256[] gameIndexes, bool simulate) { if (!simulate) { if (lastBlockGamesProcessed == block.number) return; lastBlockGamesProcessed = block.number; } uint8 delay = BlockDelay; uint256 length = gameIndexes.length; bool success = false; for(uint256 i = 0;i < length;i++) { if (ProcessGame(gameIndexes[i], delay) == GameStatus.Success) success = true; } if (simulate && !success) throw; }
      
      



呼び出しパラメーターでは、計算が必要なベット番号の配列が転送され、各ProcessGameに対して呼び出されます。



 function ProcessGame(uint256 index, uint256 delay) private returns (GameStatus) { GameInfo memory g = gambles[index]; if (block.number - g.blockNumber >= 256) return GameStatus.Stop; if (g.wheelResult == 37 && block.number > g.blockNumber + delay) { gambles[index].wheelResult = getRandomNumber(g.player, g.blockNumber); uint256 playerWinnings = getGameResult(gambles[index]); if (playerWinnings > 0) { if (g.player.send(playerWinnings) == false) throw; } EndGame(g.player, gambles[index].wheelResult, index); return GameStatus.Success; } return GameStatus.Skipped; }
      
      





コールパラメータは、ベット番号と、ベットとその処理の間で受け渡しが必要なブロック数です。 ProcessGames()またはProcessGameExt()から呼び出された場合このパラメーターは現在1に等しく、この値はgetSettings()の呼び出しの結果から見つけることができます。



処理が行われるブロック番号がベットブロックから255ブロック以上離れている場合、処理することはできません。 ブロックハッシュは最後の256ブロックのみ利用可能であり 、ドロップアウト数を決定する必要があります。



次に、ゲームの結果がすでに計算されているかどうかを確認します(wheelResultは37で初期化されていますが、これは抜けられませんか?)そして、必要なブロック数が既に過ぎているかどうかを確認します。



条件が満たされた場合、 getRandomNumber()の呼び出しが行われ、抜けた数が決定され、getGameResult()の呼び出しがゲインを計算します。 nullでない場合、ブロードキャストはプレーヤーに送信されます: g.player.send(playerWinnings) 。 次に、EndGameイベントが生成され、ゲームWebパーツから読み取ることができます。



最も興味深い、ドロップされた数値がどのように決定されるかを見てみましょう:getRandomNumber()関数。



 function getRandomNumber(address player, uint256 playerblock) private returns(uint8 wheelResult) { // block.blockhash - hash of the given block - only works for 256 most recent blocks excluding current bytes32 blockHash = block.blockhash(playerblock+BlockDelay); if (blockHash==0) { ErrorLog(msg.sender, "Cannot generate random number"); wheelResult = 200; } else { bytes32 shaPlayer = sha3(player, blockHash); wheelResult = uint8(uint256(shaPlayer)%37); } }
      
      





その引数は、プレーヤーの住所と、ベットが行われたブロック番号です。 まず、この関数は、将来BlockDelayブロックに賭けるブロックから遠く離れたブロックのハッシュを受け取ります。



これは重要なポイントです。プレーヤーがこのブロックのハッシュを前もって何らかの方法で見つけることができれば、勝つことが保証された賭けを形成できるからです。 叔父のブロックがイーサリアムに存在することを思い出すと、問題がある可能性があり、さらなる分析が必要です。



次に、プレイヤーのアドレスと受信したブロックハッシュを接着してSHA-3を計算します。 結果の数は、SHA-3の結果を37で割った余りを計算して計算されます。



私の観点からは、アルゴリズムは非常に正直であり、カジノはプレーヤーよりも有利ではありません。

なぜカジノは所有者に利益をもたらすのですか?
37セルのフィールド。 1つの特定のフィールドで1つのトークンに100,000ベットしたいとします。

おそらく、私は約2703回勝ち、それ以外の時間はすべて失います。 この場合、カジノからの賞金については2703 * 36 = 97 308トークンを受け取ります。 そして、賭けに使った2692トークンはカジノに残ります。

他のすべてのタイプのベットについても、同様の計算を行うことができます。





ペイオフがどのように計算されるかを見るのは興味深いです。 見てきたように、 getGameResult()関数がこれを行います。



 function getGameResult(GameInfo memory game) private constant returns (uint256 totalWin) { totalWin = 0; uint8 nPlayerBetNo = 0; // we sent count bets at last byte uint8 betsCount = uint8(bytes32(game.bets)[0]); for(uint8 i=0; i<maxTypeBets; i++) { if (isBitSet(game.bets, i)) { var winMul = winMatrix.getCoeff(getIndex(i, game.wheelResult)); // get win coef if (winMul > 0) winMul++; // + return player bet totalWin += winMul * getBetValueByGamble(game, nPlayerBetNo+1,i); nPlayerBetNo++; if (betsCount == 1) break; betsCount--; } } }
      
      





ここでのパラメーターは、計算されたベットのデータとともにGameInfo構造を渡します。 また、そのwheelResultフィールドには既にドロップアウトされた数値が入力されています。



game.betsビットマスクがチェックされ、チェックされているタイプのビットが設定されている場合、 winMatrix.getCoeff()が要求される、すべてのタイプのベットのサイクルが表示されます。 winMatrixは、 SmartRoulettee ()コンストラクターにロードされる0x073D6621E9150bFf9d1D450caAd3c790b6F071F2のコントラクトです。



この関数のパラメーターとして、ベットタイプと描かれた数字の組み合わせが渡されます:



 // unique combination of bet and wheelResult, used for access to WinMatrix function getIndex(uint16 bet, uint16 wheelResult) private constant returns (uint16) { return (bet+1)*256 + (wheelResult+1); }
      
      





WinMatrixの契約コードの分析は宿題として残しますが、そこには予期しないことは何もありません。係数のマトリックスが生成され、 getCoeff()を呼び出すと、必要なものが返されます。 必要に応じて、契約ページでこの関数を手動で呼び出すことで簡単に確認できます



All Articles