LoginSignup
2

More than 3 years have passed since last update.

posted at

[ethereum]solidityのデザインパターンの学習(Commit and Reveal Pattern)

記事の内容

スマートコントラクトのデザインパターンを学習しています。
Commit and Reveal Patternについて学習したことをメモします。

参考

以下の記事を参考にさせていただきました。
参考1
LayerX社 ブログ

参考2
Learning Solidity Part 2: Commit-Reveal Voting

Commit Reveal Pattern

Commit Reveal PatternではCommitとRevealの二つの役割を持ちます。

Commit:データを秘匿性を持たせて保持(hash化して保持)
Reveal:誰がコミットしたものなのかを誰もが検証可能にする

課題・解決策

この仕組みは以下の課題に対する対策として使えます。(参考1 LayerXさんのブログ)

課題:コントラクトに記録するデータを機密的に扱いたい。

解決策
コミットする時点ではコミット主だけがデータ内容を知っており、コミットデータを公開する時点で、コミットデータと公開データで整合性が取れていることを明らかにすることでコミットデータが不正なく公開されたことを証明する。

想定するユースケース

選挙・投票

「コミットする時点でコミット主がデータ内容を知っている」
「コミットデータと公開データで整合性が取れていることを明らかにすることで、コミットデータが不正なく公開されたことを証明する」

この2点を考えると選挙・投票に使えそうです。

参考2の記事には「誰がコミットしたものなのか誰もが証明可能」というような内容が記載されています。
選挙・投票において、「誰がコミットしたものなのか?」を分かる仕組みが無記名投票に反してしまいます。
分散型の社会ではこういうものなのかな?と一旦置いておきます。

Commit and Reveal サンプルコード

サンプル1

このコードは参考1の記事に載っていたものをSolidity v0.5.16用に修正したものです。

CommitRevealPattern.sol
pragma solidity ^0.5.16;

contract CommitReveal {

    struct Commit {
        string choice;
        string secret;
        string status;
    }

    mapping(address => mapping(bytes32 => Commit)) public userCommits;

    event LogCommit(bytes32, address);
    event LogReveal(bytes32, address, string, string);

    function commit(bytes32 _commit) public returns (bool success) {

        Commit memory userCommit = userCommits[msg.sender][_commit];

        if(bytes(userCommit.status).length != 0) {
            return false;
        }

        userCommit.status = "c";

        emit LogCommit(_commit, msg.sender);

        return true;
    }

    function reveal(string memory _choice, string memory _secret, bytes32 _commit) public returns (bool success) {
        Commit storage userCommit = userCommits[msg.sender][_commit];

        bytes memory bytesStatus = bytes(userCommit.status);

        if(bytesStatus.length == 0) {
            return false;
        } else if(bytesStatus[0] == "r") {
            return false;
        }

        if(_commit != keccak256(abi.encodePacked(_choice, _secret))) {
            return false;
        }

        userCommit.choice = _choice;
        userCommit.secret = _secret;
        userCommit.status = "r";

        emit LogReveal(_commit, msg.sender, _choice, _secret);

        return true;
    }

    function traceCommit(address _address, bytes32 _commit) public view
        returns (string memory choice, string memory secret, string memory status) {

        Commit memory  userCommit = userCommits[_address][_commit];

        require(bytes(userCommit.status)[0] == "r", "Status is Read.");
        return (userCommit.choice, userCommit.secret, userCommit.status);
    }
}

ざっくり解説
このコードは投票をイメージするとイメージが湧きやすいかなと思います。

    struct Commit {
        string choice;
        string secret;
        string status;
    }

構造体の定義
選択情報/秘密情報/コミットステータスを持つ

データの関係性としてchoiceとsecretを使ってコミット情報となるバイト情報を作成します。

    mapping(address => mapping(bytes32 => Commit)) public userCommits;

    event LogCommit(bytes32, address);
    event LogReveal(bytes32, address, string, string);

コミット情報を保持するマッピングの生成
イベント定義

    function commit(bytes32 _commit) public returns (bool success) {

        Commit storage userCommit = userCommits[msg.sender][_commit];

        if(bytes(userCommit.status).length != 0) {
            return false;
        }

        userCommit.status = "c";

        emit LogCommit(_commit, msg.sender);

        return true;
    }

引数として受け取ったバイト情報でコミットします。

まずは、マッピングからCommit情報を取得し、ステータスの確認。
一度でもコミット済みのバイト情報が渡されるとstatusには「c」が設定されているのでfalseが返ります。
コミットされていないデータだとstatusに「c]を設定し、「true」を返します。

ここで私がふと疑問に思ったこと。
引数で受け取った「_commit」を設定しているところが無い!
if文のuserCommit.statusでヌルポ出るんじゃね?(完全なJava脳)

調べてみたところ、Solidityのmappingは想定される全てのキーで初期化されているとのこと。
その為、最初のuserCommitは初期化された状態のCommitが取得出来る様です。便利・・。

    function reveal(string memory _choice, string memory _secret, bytes32 _commit) public returns (bool success) {

        Commit storage userCommit = userCommits[msg.sender][_commit];

        bytes memory bytesStatus = bytes(userCommit.status);

        if(bytesStatus.length == 0) {
            return false;
        } else if(bytesStatus[0] == "r") {
            return false;
        }

        if(_commit != keccak256(abi.encodePacked(_choice, _secret))) {
            return false;
        }

        userCommit.choice = _choice;
        userCommit.secret = _secret;
        userCommit.status = "r";

        emit LogReveal(_commit, msg.sender, _choice, _secret);

        return true;
    }

コミットしたはずの情報とその元データとなるchoiceとsecretを渡して、正しいデータでコミットされていることを確認します。

まずはmappingからコミット情報の取得
次にstatusの判定をしています。コミットがまだの状態ならlengthが0、revealを一度でも実行している状態ならbyteStatus[0]が"r"になります。

次の条件式では引数として送られてきた情報が正しいかを確認
問題がなければ、引数のデータをuserCommitに設定してTrueを返しています。

    function traceCommit(address _address, bytes32 _commit) public view
        returns (string memory choice, string memory secret, string memory status) {

        Commit memory  userCommit = userCommits[_address][_commit];

        require(bytes(userCommit.status)[0] == "r", "Status is Read.");
        return (userCommit.choice, userCommit.secret, userCommit.status);
    }

コミット情報を確認するための処理です。
この人(address)がこの情報をコミットしたのか?を確認できます。

サンプル2

このコードは参考2の記事を書かれたコードをSolidity v0.5.16用に修正したものです。
元ファイルは以下になります。
github

CommitRevealElection.sol
pragma solidity ^0.5.16;

contract CommitRevealElection {
    // The two choices for your vote
    string public choice1;
    string public choice2;

    // Information about the current status of the vote
    uint public votesForChoice1;
    uint public votesForChoice2;
    uint public commitPhaseEndTime;
    uint public numberOfVotesCast = 0;

    // The actual votes and vote commits
    bytes32[] public voteCommits;
    mapping(bytes32 => string) voteStatuses; // Either `Committed` or `Revealed`

    // Events used to log what's going on in the contract
    event logString(string);
    event newVoteCommit(string, bytes32);
    event voteWinner(string, string);

    // Constructor used to set parameters for the this specific vote
    constructor (uint _commitPhaseLengthInSeconds,
                                  string memory _choice1,
                                  string memory _choice2) public {
        if (_commitPhaseLengthInSeconds < 20) {
            revert("Commit time out..");
        }
        commitPhaseEndTime = now + _commitPhaseLengthInSeconds * 1 seconds;
        choice1 = _choice1;
        choice2 = _choice2;
    }

    function commitVote(bytes32 _voteCommit) public {
        if (now > commitPhaseEndTime)
            revert("Only allow commits during committing period");

        // Check if this commit has been used before
        bytes memory bytesVoteCommit = bytes(voteStatuses[_voteCommit]);
        if (bytesVoteCommit.length != 0)
            revert("already commited."); //Bytes vote commit length wrong.

        // We are still in the committing period & the commit is new so add it
        voteCommits.push(_voteCommit);
        voteStatuses[_voteCommit] = "Committed";
        numberOfVotesCast ++;
        emit newVoteCommit("Vote committed with the following hash:", _voteCommit);
    }

    function revealVote(string memory _vote, bytes32 _voteCommit) public{
        if (now < commitPhaseEndTime)
            revert("Only reveal votes after committing period is over");

        // FIRST: Verify the vote & commit is valid
        bytes memory bytesVoteStatus = bytes(voteStatuses[_voteCommit]);
        if (bytesVoteStatus.length == 0) {
            emit logString('A vote with this voteCommit was not cast');
        } else if (bytesVoteStatus[0] != 'C') {
            emit logString('This vote was already cast');
            return;
        }

        if (_voteCommit != keccak256(abi.encodePacked(_vote))) {
            emit logString('Vote hash does not match vote commit');
            return;
        }

        // NEXT: Count the vote!
        bytes memory bytesVote = bytes(_vote);
        if (bytesVote[0] == '1') {
            votesForChoice1 = votesForChoice1 + 1;
            emit logString('Vote for choice 1 counted.');
        } else if (bytesVote[0] == '2') {
            votesForChoice2 = votesForChoice2 + 1;
            emit logString('Vote for choice 2 counted.');
        } else {
            emit logString('Vote could not be read! Votes must start with the ASCII character `1` or `2`');
        }
        voteStatuses[_voteCommit] = "Revealed";
    }

    function getWinner () public returns(string memory _choice) {
        if (now < commitPhaseEndTime)
            revert("Not counting time");
        // Make sure all the votes have been counted
        if (votesForChoice1 + votesForChoice2 != voteCommits.length)
            revert("There was an invalid votes.");

        if (votesForChoice1 > votesForChoice2) {
            emit voteWinner("And the winner of the vote is:", choice1);
            return choice1;
        } else if (votesForChoice2 > votesForChoice1) {
            emit voteWinner("And the winner of the vote is:", choice2);
            return choice2;
        } else if (votesForChoice1 == votesForChoice2) {
            emit voteWinner("The vote ended in a tie!", "");
            return "It was a tie!";
        }
    }
}

ざっくり解説
このコードは投票をよりイメージし易い内容になっています。

    // The two choices for your vote
    string public choice1;                   // 選択肢1
    string public choice2;                   // 選択肢2

    // Information about the current status of the vote
    uint public votesForChoice1;             // 選択肢1の投票数
    uint public votesForChoice2;             // 選択肢2の投票数
    uint public commitPhaseEndTime;          // 投票期間
    uint public numberOfVotesCast = 0;       // 総投票数

    // The actual votes and vote commits
    bytes32[] public voteCommits;            // 投票情報
    mapping(bytes32 => string) voteStatuses; // 投票のステータス

まずは変数の定義。内容はコメントを追記

    // Events used to log what's going on in the contract
    event logString(string);
    event newVoteCommit(string, bytes32);
    event voteWinner(string, string);

イベントの定義

    // Constructor used to set parameters for the this specific vote
    constructor (uint _commitPhaseLengthInSeconds,
                                  string memory _choice1,
                                  string memory _choice2) public {
        if (_commitPhaseLengthInSeconds < 20) {
            revert("Commit time out..");
        }
        commitPhaseEndTime = now + _commitPhaseLengthInSeconds * 1 seconds;
        choice1 = _choice1;
        choice2 = _choice2;
    }

コンストラクタの定義
設定している内容は以下
第1引数:投票期間(コントラクタのデプロイ日時+指定した秒数が投票期間になる)
第2引数:選択肢1の名前
第3引数:選択肢2の名前

内容はコミット時間が短すぎるとエラーとし、それ以外は引数で受け取った値を設定する。

    function commitVote(bytes32 _voteCommit) public {
        if (now > commitPhaseEndTime)
            revert("Only allow commits during committing period");

        // Check if this commit has been used before
        bytes memory bytesVoteCommit = bytes(voteStatuses[_voteCommit]);
        if (bytesVoteCommit.length != 0)
            revert("already commited."); //Bytes vote commit length wrong.

        // We are still in the committing period & the commit is new so add it
        voteCommits.push(_voteCommit);
        voteStatuses[_voteCommit] = "Committed";
        numberOfVotesCast ++;
        emit newVoteCommit("Vote committed with the following hash:", _voteCommit);
    }

投票処理

一つ目の条件式で投票期間を過ぎてたらエラーとしている。
二つめの条件式で既に投票済みであればエラーとしている。

問題なければ、投票情報への追加、ステータスの更新、投票総数のカウントアップを実行

    function revealVote(string memory _vote, bytes32 _voteCommit) public{
        if (now < commitPhaseEndTime)
            revert("Only reveal votes after committing period is over");

        // FIRST: Verify the vote & commit is valid
        bytes memory bytesVoteStatus = bytes(voteStatuses[_voteCommit]);
        if (bytesVoteStatus.length == 0) {
            emit logString('A vote with this voteCommit was not cast');
        } else if (bytesVoteStatus[0] != 'C') {
            emit logString('This vote was already cast');
            return;
        }

        if (_voteCommit != keccak256(abi.encodePacked(_vote))) {
            emit logString('Vote hash does not match vote commit');
            return;
        }

        // NEXT: Count the vote!
        bytes memory bytesVote = bytes(_vote);
        if (bytesVote[0] == '1') {
            votesForChoice1 = votesForChoice1 + 1;
            emit logString('Vote for choice 1 counted.');
        } else if (bytesVote[0] == '2') {
            votesForChoice2 = votesForChoice2 + 1;
            emit logString('Vote for choice 2 counted.');
        } else {
            emit logString('Vote could not be read! Votes must start with the ASCII character `1` or `2`');
        }
        voteStatuses[_voteCommit] = "Revealed";
    }

投票結果の確認処理。投票箱に入った投票用紙を1枚取り出して確認するイメージが近いかもしれません。

第二引数が投票情報のハッシュ値、第一引数が元データ

一つ目の条件式で投票期間が過ぎているかを確認している。
二つ目の条件式で投票済みの情報かを確認している。
三つ目の条件式で引数で受け取ったデータの整合性確認

最後に投票数のカウントアップ

この仕組みだと投票した人数分この処理を呼び出さないといけないっぽいです。

    function getWinner () public returns(string memory _choice) {
        if (now < commitPhaseEndTime)
            revert("Not counting time");
        // Make sure all the votes have been counted
        if (votesForChoice1 + votesForChoice2 != voteCommits.length)
            revert("There was an invalid votes.");

        if (votesForChoice1 > votesForChoice2) {
            emit voteWinner("And the winner of the vote is:", choice1);
            return choice1;
        } else if (votesForChoice2 > votesForChoice1) {
            emit voteWinner("And the winner of the vote is:", choice2);
            return choice2;
        } else if (votesForChoice1 == votesForChoice2) {
            emit voteWinner("The vote ended in a tie!", "");
            return "It was a tie!";
        }
    }

どちらが勝ったのかを確認する

一つ目の条件式は投票期間を過ぎているか確認している。
二つ目の条件式は無効票があるかを確認している。

最後の条件式で勝者の確認

感想

実際に「投票」というユースケースで使われるようなコードを見ながらデザインパターンを学習するとイメージが湧きます。
JavaのSingletonみたいにコレと決まった実装方法があるわけではなく、こういう風に実装しましょうというルールみたいな感じですね。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
2