Blockchain
Ethereum
solidity
SmartContract

ブロックチェーンとスマートコントラクトで、抽選の公正性を証明する

mixiグループ Advent Calendar 2017 19日目の記事です

2017年は仮想通貨・暗号通貨が大いに盛り上がりましたが、基盤技術であるブロックチェーンは今後どのように展開してゆくのでしょうか.非金融分野に応用する動きもあるようですが、実際どのようなサービスができそうなんでしょう.

本稿では応用例をひとつ、Ethereum をつかって考察したいと思います.自身も初級者なので多分に初級向けの内容ですが、十分わかってる方もそうでない方も、今年末らしい話題として楽しんでいただければ幸いです.

はじめに

「ブロックチェーン」とは

いまさらですが定義を見ておきましょう 1 2

ブロックチェーン(英語: Blockchain)とは、分散型台帳技術、または、分散型ネットワークである。ビットコインの中核技術(Satoshi Nakamotoが開発)を原型とするデータベースである。ブロックと呼ばれる順序付けられたレコードの連続的に増加するリストを持つ。各ブロックには、タイムスタンプと前のブロックへのリンクが含まれている。理論上、一度記録すると、ブロック内のデータを遡及的に変更することはできない。ブロックチェーンデータベースは、Peer to Peerネットワークと分散型タイムスタンプサーバーの使用により、自律的に管理される

暗号通貨としての用法にとどまらずより広い分散型台帳技術ととらえ、分散ネットワークとチェーン状のデータ構造により耐障害性や耐改ざん性を高めた仕組み全般を、一般的にブロックチェーンとみなすようです. 3 4

管理者有無によるブロックチェーンの分類

ブロックチェーンは中央管理組織の数によって3つに大別されます 5

  • パブリックチェーン:中央管理なし
  • コンソーシアムチェーン:複数組織で管理
  • プライベートチェーン:単一組織で管理

ビットコインなどの仮想通貨に代表されるパブリックチェーンは、中央機関をおかず不特定多数のノードで経済的なインセンティブによって運営される形態で、一般にイメージされるブロックチェーンだと思います.一方コンソーシアムチェーンとプライベートチェーンはデータ構造にハッシュとチェーン構造をもたせるものの、既存の分散システムと同様、中央機関のあるチェーンとなっています.

個人的に、パブリックでないチェーンは少数でデータ管理すれば改ざん容易になり、ブロックチェーンのもつ耐改ざん性を損ねると思うのですが、秘匿したい情報を扱うケースではパブリックにできないですし、それなりに耐改ざん性が得られることと分散によって耐障害性を得るだけでも利点があるとみられているようです 6

本稿ではこの後パブリックでないチェーンについては割愛し、パブリックチェーンに着目して書きます.

分散アプリケーション基盤としてのブロックチェーン

ブロックチェーンで得られる耐障害性、耐改ざん性を分散アプリケーション基盤として利用するというアイデアから「スマートコントラクト」と呼ばれる機能を実装したチェーンがあり、従来の中央集権型アプリケーションを置き換えてゆくのではないかと期待を集めています 7 8(スマートコントラクト・分散アプリケーション基盤としての色合いの強いプロジェクトとして Ethereum が知られています 9 10

スマートコントラクトは「『決められたある条件を満たすと自動的に契約(コントラクト)を実行する』ということが記述されたコード」を指し、有名なたとえとして自動販売機の挙動などが挙げられます.

このスマートコントラクトはいわゆる普通のプログラム言語でコードを書き、やりたい動きを自由に作ることができるものです.自由に書いたコードをブロックチェーンというインフラにデプロイすると、あとは自律的に「働きかけに応じて決められた動きを実行する、ネットワーク上にノードが存続し続ける限り半永久的に生き続ける」というおもしろい存在になります.

サービス提供者として「単一障害点をもたず、インフラコスト不要なアプリケーション基盤」というのは魅力的な選択肢ではないでしょうか.

スマートコントラクトの特性

非常に有用性を感じる存在ですが、ブロックチェーンの特性上避けられない課題を背負っています.
それぞれ改善しようとする動きがあり次第に解消されるかもしれませんが、現時点では主に以下が意識されているようです.

実行速度

台帳更新を伴う処理はマイニング処理を待たなければいけないため、処理が完了するまで数十秒〜数分の待ち時間が発生します.中央集権型システムであれば瞬時に大量に更新処理を捌くこともできましたが、そうした挙動とは全く異なります.高速化のための施策がいくつかあるようですが、基本的には、大量のリアルタイム更新が必要になる挙動は苦手といえるでしょう. 11
またパブリックチェーンのネットワークでは、処理速度は分散化されたそれぞれのP2Pノード性能に依存します.中央集権型であればサービス提供側でインフラ性能をコントロールできますが、パブリックチェーンではコントロールできません.環境を問わない軽量頑健なコードが求められるかもしれません.

耐改ざん性

改ざんの難しさはデータの信頼性にとって良いことですが、プログラムコードにとっては怖い性質です.ブロックチェーン上のブロックにプログラムコードが載るため、一度ブロックに載ったプログラムコードはあとから改変できません.
バグや仕様変更をプログラムコードからなくすことは不可能なので、ブロックチェーンの内外で何層か用意して、プログラムコードの載った参照先ブロックを切り替えられるような(DNSのような)工夫が必要になるでしょう12

手数料

台帳を更新する手数料は、リクエストを出したエンドユーザが負担します(台帳更新に必要な計算リソースを提供するユーザ(一般にマイナーと呼ぶ)に支払います).
中央集権型システムで考えれば、システム利用のたび手数料を取られる体験はそう多くなく、エンドユーザに相応の価値を提供するものでなければ一般的に容認されません(銀行のATM手数料を考えれば忌避具合が分かるでしょう).なにを台帳に載せるかはよく考える必要があります.しかも台帳更新のコストはデータサイズに依存するので、ハッシュ値や数値以上のデータを載せると割に合わないコストになりかねません.台帳に記録する価値のあるデータに厳選する必要があるでしょう.

高い透明性

パブリックチェーンに登録したデータは基本的には全公開のため、他者に公開したくないデータ(位置情報などプライベートな情報)を扱うには難しさがあります.zk-SNARK などパブリックチェーンで秘匿情報を扱うための技術が導入されてきていますが、手数料が多く掛かるなど問題が残っておりまだ大きい制約といえそうです.

応用例

上記の特性・制約をふまえ、現状でのパブリックチェーンのユースケースは非常に限られたものになりそうです.暗号通貨はこれら特性に非常にフィットし、制約があっても十分に価値を発揮できる用途といえますが、逆にそれ以外の領域ではコストに見合うユースケースを見出すのが難しそうです.天から閃きが降るのを待つばかりですね.

難しい謎解きのようですが、あえて(本題の)応用例をひとつみてみましょう.

抽選は公正に行われたか?

オンラインカジノや懸賞、モバイルゲームのレアアイテムなど、当落は公平公正に決められているでしょうか?運営側のイカサマや確率操作は無いのでしょうか?

現実社会では、抽選を実施する団体を信頼して結果を妥当とみなすしかありません.失うものがお金ならまだいいですが、進学や徴兵その他人生に影響する重要な抽選なら、その公平性は明確にしたくなるでしょう.

ブロックチェーン技術を使うと、このような疑念を払って、公平公正な抽選を実現できるかもしれません13 14

課題設定

応募者に抽選券を払い出して、抽選期日に発生させた乱数で当落を決定する(年末ジャンボ宝くじのような)ケース.この場合に、当落の公平性(乱数発生の妥当性)を証明する

実装例

話を分かりやすくするために、1ユニット抽選券1000枚発行として、1枚 3 Ether で販売、下表の通り賞金がでることにします.

等級 本数 獲得比率 賞金総額 賞金額
1等 1本 35% 1,050 Ether 1,050 Ether
2等 5本 10% 300 Ether 60 Ether
3等 10本 5% 150 Ether 15 Ether
合計 16本 50% 1500 Ether --

※抽選券払い出しは所定の枚数(1000枚)で一巡し、別ユニットで再度1番目から発行されるようにする
※1ユニット1000枚払い出されていない場合は、販売分の賞金を獲得比率で掛けた分の賞金を得る

検証環境

  • Ethereum
  • Geth 1.5.5
  • Solidity 0.4.19
  • Truffle 4.0.3

実行フロー

  1. 抽選ロジックをブロックチェーンに載せ公開する
    以下の機能を備える
    • 抽選券を払い出す機能
    • 保有する抽選券番号を確認する機能
    • 当選券を決定する機能(乱数生成)
    • 当選券を保有するアカウントに賞金を送付する機能
    • 当選券を決定するロジックを検証する機能
  2. 抽選券の募集
    • 販売価格 1 枚 3 Ether
    • ユニットごと
      • 発行枚数 1,000 枚
      • 売上総額 3,000 Ether
      • 賞金 1等 1,050 Ether、2等3等・・以下略 総額 1,500 Ether
  3. 応募者からの申し込み(購入)
    • トランザクション手数料は応募者が支払う
    • 応募者にはリクエストの到着順に抽選券番号を割り当てる
    • ブロックチェーン上の台帳に抽選券番号とアカウントアドレスを記録する
    • 抽選券の対価は賞金額として蓄積される
  4. 応募締切
    • 最終ブロック日時などで判定
    • 払い出された抽選券総数が確定する
  5. 抽選期日に抽選を実施
    • 以下のような、応募時からは予測の難しい値を取得して記録して公開する
      • 期日到来時点の最終ブロック番号
      • など
    • 上記をseedとして、当選券決定器(乱数生成器)によって順次当選番号を払い出す
    • 当選券を決定させ、当選券を所有するアカウントのアドレスへ送金を行う
  6. 抽選終了後、当選券の決定器を使って同様の結果が得られることを検証できる
    • 当初から登録されているコントラクトに改ざんが無いことを証明できる
    • 決定に際して利用した情報をすべて開示し、不正のないことを証明できる

コントラクト例

(とりあえず書きましたが検証したりきれいに直すまでできませんでした><;ふわっと雰囲気を見ていただければと思います.仕様をみてこうするといいよ!という編集リクエストいただけたらうれしいです.)

pragma solidity ^0.4.19;

contract YearEndLottery {

    /** variables **/

    /* 抽選応募期日 2017-12-31 00:00:00 JST */
    uint constant endtime = 1514646000;

    address admin;
    uint ticketPrice = 3 ether;
    uint numberMaxPerUnit;
    uint lastUnitNumber;

    /* 賞金額 */
    struct potByUnit {
        uint unitNumber;
        uint pot;
    }
    mapping (uint => potByUnit) potList;

    /* 抽選券 */
    struct ticket {
        uint ticketIndex;
        uint unitNumber; // uint( ticketIndex / numberMaxPerUnit )
        uint ticketNumber; // ticketIndex % numberMaxPerUnit
        address participant;
    }
    mapping (uint => ticket) ticketList;
    uint totalTickets;

    /* 当選表 */
    /* 当選等級と獲得賞金比率 */
    struct winningGrade {
        uint grade;
        uint winningRate; /* 千分率 */
        uint winnersCount;
        mapping (uint => address) winnerOnUnit;
        mapping (uint => uint) winningAmountOnUnit;
        mapping (uint => bool) isTookPot;
    }
    mapping (uint => winningGrade) winningGradeList;
    uint winningGradeListLength;

    /* 当選確定の際に利用する情報 */
    uint blockNumberOnLotteryEnd;

    /* 抽選状態 */
    bool betClosed = false;
    bool gameClosed = false;
    bool potTransferred = false;
    bool winnersDetermined = false;


    /** constructor **/

    /* (ブロックチェーンにデプロイする際に一度だけ呼ばれる) */
    function YearEndLottery() public {
        admin = msg.sender;
        numberMaxPerUnit = 1000;
        betClosed = false;
        gameClosed = false;
        potTransferred = false;
        winnersDetermined = false;
        lastUnitNumber = 0;
        totalTickets = 0;
        potList[0] = potByUnit(0,0);

        /* 当選表の初期化 */
        winningGradeListLength = 0;
        while ( winningGradeListLength < 16 ) {
            if ( winningGradeListLength == 0 ) {
                winningGradeList[winningGradeListLength] = winningGrade(1,350,0);
            } 
            if ( winningGradeListLength >= 1 && winningGradeListLength <= 5 ) {
                winningGradeList[winningGradeListLength] = winningGrade(2,20,0);
            }
            if ( winningGradeListLength >= 6 ) {
                winningGradeList[winningGradeListLength] = winningGrade(3,5,0);
            }
            winningGradeListLength++;
        }
    }


    /** modifiers **/

    modifier adminOnly() {
        require(msg.sender == admin);
        _;
    }
    modifier correctPayment() {
        require(msg.value == ticketPrice);
        _;
    }
    modifier betOngoing() {
        require(!betClosed);
        _;
    }
    modifier gameOngoing() {
        require(!gameClosed);
        _;
    }
    modifier afterBetClosed() {
        require(betClosed);
        _;
    }
    modifier afterGameClosed() {
        require(gameClosed);
        _;
    }
    modifier afterWinnersDetermined() {
        require(winnersDetermined);
        _;
    }
    modifier afterPotTransferred() {
        require(potTransferred);
        _;
    }


    /** events **/

    event UserPutBets(address user, uint unitNumber, uint ticketNumber, uint ticketIndex, uint price);
    event HoldingTicket(address user, uint unitNumber, uint ticketNumber, uint ticketIndex);
    event WinnerFixed(address winner, uint unitNum, uint grade, uint ticketNum, uint amount);
    event WinnerTookIt(address winner, uint unitNum, uint grade, uint amount);
    event WinnerFailedToTakeWin(address winner, uint unitNum, uint grade, uint amount);
    event WinnerTookItAlready(address winner, uint unitNum, uint grade, uint amount);
    event OutputWinningNumber(uint grade, uint ticketNumber);
    event StatusUpdated(string message);
    event CollectedAmount(string message, address sendto, uint unitNum, uint amount);


    /** functions **/

    /* 抽選に応募する */
    function takeTicket() public payable correctPayment betOngoing gameOngoing {
        uint ticketIndex = totalTickets;
        uint unitNumber = uint( totalTickets / numberMaxPerUnit );
        uint ticketNumber = uint( totalTickets % numberMaxPerUnit );
        ticketList[totalTickets] = ticket(ticketIndex, unitNumber, ticketNumber, msg.sender);
        potList[unitNumber].pot += msg.value;
        lastUnitNumber = unitNumber;
        UserPutBets(ticketList[totalTickets].participant, ticketList[totalTickets].unitNumber, ticketList[totalTickets].ticketNumber, ticketList[totalTickets].ticketIndex, potList[unitNumber].pot);
        totalTickets += 1;
    }

    /* 抽選を締め切る */
    function endBet() public adminOnly betOngoing gameOngoing {
        if ( block.timestamp >= endtime ) {
            betClosed = true;
            StatusUpdated("bet closed");
        }
    }

    /* 抽選を終了する */
    function endLottery() public adminOnly afterBetClosed gameOngoing {
        if (totalTickets == 0) {
            gameClosed = true;
            StatusUpdated("game closed with no tickets.");
            return;
        }
        /* 当選番号決定に使う乱数の seed を確定する */
        blockNumberOnLotteryEnd = block.number;
        gameClosed = true;
        StatusUpdated("game closed");
    }

    /* 当選者を確定させる */
    function determineWinners() public adminOnly afterGameClosed {
        uint[16] memory winningNumbers = pickupWinnersByRandSeed(blockNumberOnLotteryEnd);

        /* 当選番号を保有するアドレスを取得して当選表に登録 */
        for (uint winIdx=0; winIdx < winningGradeListLength; winIdx++) {
            uint grade = winningGradeList[winIdx].grade;
            for ( uint unitNum=0; unitNum <= lastUnitNumber; unitNum++) {
                uint ticketIndex = uint(( unitNum * numberMaxPerUnit ) + winningNumbers[winIdx] );
                if (ticketList[ticketIndex].participant != address(0)) {
                    address winner = ticketList[ticketIndex].participant;
                    uint ticketNum = ticketList[ticketIndex].ticketNumber;
                    uint winningAmount = uint( potList[unitNum].pot * winningGradeList[winIdx].winningRate / 1000 );
                    winningGradeList[winIdx].winnersCount += 1;
                    winningGradeList[winIdx].winnerOnUnit[unitNum] = winner;
                    winningGradeList[winIdx].winningAmountOnUnit[unitNum] = winningAmount;
                    WinnerFixed(winner, unitNum, grade, ticketNum, winningAmount);
                }
            }
        }
        winnersDetermined = true;
        StatusUpdated("winner determined");
    }

    /* 賞金を当選者に送金する */
    function transferPotToWinner() public adminOnly afterWinnersDetermined {
        for ( uint winIdx=0; winIdx <= winningGradeListLength; winIdx++) {
            uint grade = winningGradeList[winIdx].grade;
            uint winnersCount = winningGradeList[winIdx].winnersCount;
            for ( uint unitNum=0; unitNum < winnersCount; unitNum++ ) {
                address winner = winningGradeList[winIdx].winnerOnUnit[unitNum];
                uint winningAmount = winningGradeList[winIdx].winningAmountOnUnit[unitNum];
                bool isTookPot = winningGradeList[winIdx].isTookPot[unitNum];
                if (!isTookPot) {
                    if (winner.send(winningAmount)) {
                        potList[unitNum].pot = potList[unitNum].pot - winningAmount;
                        winningGradeList[winIdx].isTookPot[unitNum] = true;
                        WinnerTookIt(winner, unitNum, grade, winningAmount);
                    } else {
                        WinnerFailedToTakeWin(winner, unitNum, grade, winningAmount);
                    }
                } else {
                    WinnerTookItAlready(winner, unitNum, grade, winningAmount);
                }
            }
        }
        potTransferred = true;
        StatusUpdated("pot transferred");
    }

    /* 保有する抽選券数と抽選券番号を返す */
    function getTicketListByAddress() public returns (uint) {
        uint ticketCount = 0;
        for ( uint i=0; i < totalTickets; i++) {
            if ( ticketList[i].participant == msg.sender ) {
                uint ticketIndex = ticketList[i].ticketIndex;
                uint ticketNum = ticketList[i].ticketNumber;
                uint unitNum = ticketList[i].unitNumber;
                HoldingTicket(msg.sender, unitNum, ticketNum, ticketIndex);
                ticketCount += 1;
            }
        }
        return ticketCount;
    }

    /* 抽選終了後 pot 内の残金を引き揚げる */
    function collectRemainingAmount() public adminOnly /* afterPotTransferred */ {
        for ( uint unitNum=0; unitNum <= lastUnitNumber; unitNum++) {
            if ( potList[unitNum].pot > 0 ) {
                if ( admin.send(potList[unitNum].pot) ) {
                    potList[unitNum].pot = 0;
                    CollectedAmount("collected pot.", admin, unitNum, potList[unitNum].pot);
                }
            }
        }
    }

    /* block number を受け取って当選番号リストを返す */
    function pickupWinnersByRandSeed(uint blockNumber) internal view returns (uint[16]) {
        uint[16] memory winningNumbers;
        uint idx = 0;
        uint blockIdx = blockNumber;
        while ( idx < winningGradeListLength ) {
            uint randNumber = ( uint(block.blockhash(blockIdx--)) % numberMaxPerUnit ) + 1;
            if ( !isExistKey(winningNumbers,randNumber) ) {
                winningNumbers[idx] = randNumber;
                idx++;
            }
        }
        return (winningNumbers);
    }

    /* リストに値が存在するかどうかを返す */
    function isExistKey(uint[16] dataList, uint val) internal pure returns (bool) {
        for ( uint i=0; i<dataList.length; i++) {
            if ( dataList[i] == val ) {
                return true;
            }
        }
        return false;
    }

    /* 変換 */
    function uintToString(uint v) internal pure returns (string str) {
        uint maxlength = 100;
        bytes memory reversed = new bytes(maxlength);
        uint i = 0;
        while (v != 0) {
            uint remainder = v % 10;
            v = v / 10;
            reversed[i++] = byte(48 + remainder);
        }
        bytes memory s = new bytes(i + 1);
        for (uint j = 0; j <= i; j++) {
            s[j] = reversed[i - j];
        }
        str = string(s);
    }


    /** getters **/

    function getLotteryAssets() public adminOnly constant returns (
        uint totaltickets,
        uint lastUnitNum,
        uint thisBalance ) {
        return (totalTickets, lastUnitNumber, this.balance);
    }

    function getLotteryConditions() public constant returns (
        address constAdminAddress,
        uint constTicketPrice,
        uint constNumberMaxPerUnit ) {
        return (admin, ticketPrice, numberMaxPerUnit);
    }

    function getLotteryStatus() public constant returns (
        bool isBetClosed,
        bool isGameClosed,
        bool isWinnersDetermined,
        bool isPotTransferred ) {
        return ( betClosed, gameClosed, winnersDetermined, potTransferred);
    }

    /* 抽選終了後、当選決定時のseedを取得できる */
    function getBlockNumberOnLotteryEnd() public afterGameClosed constant returns(uint)  {
        return blockNumberOnLotteryEnd;
    }

    /* 抽選終了後、当選決定時のseedを使って結果を検証できる */
    function checkWinnersBySpecifiedRandomSeed(uint blockNum) public afterGameClosed  {
        uint[16] memory winningNumbers = pickupWinnersByRandSeed(blockNum);
        for (uint winIdx=0; winIdx < winningGradeListLength; winIdx++) {
            uint grade = winningGradeList[winIdx].grade;
            OutputWinningNumber(grade, winningNumbers[winIdx]);
        }
    }
}

コードに関する補足

  • スマートコントラクトでは一般的な乱数生成器は用意されていないため(おそらく分散ネットワーク上で不確実要素を排除するコンセプトのため)、最終ブロック番号などを辿って擬似的に乱数生成するなど工夫が求められます.
  • 時刻を参照したり、時間によって挙動を変化させることも、ノード提供者によって悪用される可能性があり考慮する必要があります.

既存手法に比べた優位性

名高い 年末ジャンボ宝くじ では、実施団体と抽選方法(的当て)・公平性も明確で問題視されることはありませんが、オンラインカジノや無名組織による懸賞やくじになると疑念は避けられず、場合によっては中傷など受けるリスクもあるでしょう.抽選の実施主体としても、公平性を証明する手法があれば積極的に公開して無用なトラブルを避けられるようになります.

また政府や教育機関など信頼の高い主体であっても、結果が重大なケースではその公平性を検証する方法が求められます.これまで十分説明できなかった公平性を明らかにできる点で、ブロックチェーンを活用する利点は大きいのではないでしょうか.

おわりに

プライベートチェーンなどで既存システムを置き換える方向ではブロックチェーンの優位性・有用性を疑問視する意見は多く 15 個人的にも同感で、真価はパブリックチェーンにあると感じます.宿命的に信頼を得ることが難しく、かつ公平公正であることがビジネス上重要な領域(カジノあるいはカジノ業者のような)では、ブロックチェーンを利用するメリットが大きいかもしれません.

処理速度や改ざん困難性、エンドユーザにかかるインフラコスト、手数料にみあう価値、透明すぎる公開性など、一見するとパブリックチェーンがもつ特性にフィットするユースケースはごく僅かのように感じますが、制約が解消される動きが(急速に)進んでいるので、この進化には注意を払う必要があります.

予想を超える進化と閃きによって、2018年にはキラーコンテンツが生み出されているかもしれません.期待を胸に、来年も楽しんでいきましょう!

参考資料