【入門編】 フロントランニング対策としてのコミット・リビール(Commit-Reveal)スキーム – IoT・OT(制御システム) & ブロックチェーンセキュリティ防御ガイド

ブロックチェーンの世界へようこそ!スマートコントラクトの開発に足を踏み入れたばかりだと、なんだか専門用語が多くて圧倒されてしまいますよね。でも、一歩ずつ実例を交えて紐解いていけば大丈夫です。

今回は、ブロックチェーンならではの厄介な罠である「フロントランニング(先回り取引)」と、それに対抗するための「コミット・リビール(Commit-Reveal)スキーム」について、身近な防犯の例えを使いながら優しく解説していきます。一緒にしっかり学んでいきましょう!

—

街中のオープンカフェで「秘密の合言葉」を叫ぶ危険性

まずは、ブロックチェーンの仕組みから少しイメージしてみましょう。
イーサリアムなどのブロックチェーンに送られたトランザクション(取引のデータ)は、承認されるまでの間、インターネット上の誰もが見られる「メモリプール(Mempool)」という待合室のような場所に一時的に置かれます。

これは例えるなら、「ガラス張りのオープンカフェで、次の宝くじの当たり番号を大声で叫んでいる状態」です。

通りすがりの悪い人(攻撃者のボット)が、その「当たり番号」を盗み聞きして、あなたよりも高い手数料(ガス代)を払って先に宝くじ売り場に走ってしまったらどうなるでしょうか? あなたが買おうとしたお宝は、先に横取りされてしまいますよね。これが、Web3の世界で「フロントランニング」と呼ばれる攻撃のメカニズムです。

玄関の鍵をかける前の「二段階の防犯」

この泥棒を防ぐために考案されたのが、今回学ぶ「コミット・リビール(Commit-Reveal)スキーム」です。

私たちの生活に置き換えてみましょう。例えば、オークションで「絶対に秘密にしておきたい入札価格」があるとします。
そのまま紙に書いて提出すると、受付の人が中身を盗み見して不正をするかもしれません。そこで、次のような防犯ルールを作ります。

1. コミット(Commit)フェーズ:
金額をそのまま書くのではなく、パスワードと一緒に金庫に入れ、ガチガチに鍵をかけてしまいます。外からは「何か重いものが入っているな」とはわかりますが、中の金額は絶対にわかりません。この「鍵をかけた箱」をまず提出します。
2. リビール(Reveal)フェーズ:
みんなが箱を出し終わったら、後から「実はこのパスワードで開きます」と明かし、みんなの前で箱を開けて中身を証明します。

これなら、途中で泥棒に中身を盗み見られる心配もありませんよね。スマートコントラクトの世界でも、この「先に秘密のハッシュ(暗号の塊)だけを登録し、後から本当のデータをバラして証明する」という手法を使います。

—

実装コードで見てみよう:SolidityでのCommit-Reveal

それでは、実際にSolidity(スマートコントラクトの開発言語)でどのように実装するのか、分かりやすいコメント付きのコードを見ていきましょう。

今回は、「秘密の数字を当てっこするミニゲーム」を例にします。プレイヤーは自分の答えを隠したまま登録し、後から答えを暴いて正解を証明します。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/**
 * @title コミット・リビールスキームのサンプルコントラクト
 * @notice メモリプールでの盗み見を防ぎながらデータを安全に登録する仕組みです
 */
contract CommitRevealGame {

    // プレイヤーごとのコミット(ハッシュ値)を保存するマッピング
    mapping(address => bytes32) public commits;
    
    // すでに答えを明かした(リビールした)かどうかを記録するフラグ
    mapping(address => bool) public revealed;

    /**
     * @notice ステップ1:コミット(秘密の隠し場所を作る)
     * @param _commitment 秘密の数字とソルト(パスワード)を混ぜてハッシュ化した値
     */
    function commit(bytes32 _commitment) external {
        // まだコミットしていない場合のみ受け付ける
        require(commits[msg.sender] == bytes32(0), "すでにコミット済みです!");
        
        // 届いたハッシュ値をそのまま保存
        commits[msg.sender] = _commitment;
    }

    /**
     * @notice ステップ2:リビール(秘密のベールを脱ぐ)
     * @param _secretNumber プレイヤーが隠していた本当の数字
     * @param _salt ハッシュ化の際に混ぜたパスワード的な文字列
     */
    function reveal(uint256 _secretNumber, string memory _salt) external {
        // まだリビールしておらず、かつ過去にコミットしていること
        require(commits[msg.sender] != bytes32(0), "先にコミットしてください!");
        require(!revealed[msg.sender], "すでにリビール済みです!");

        // 送られてきたデータが、最初に送ったハッシュと本当に一致するか再計算して検証
        bytes32 calculatedCommitment = keccak256(abi.encodePacked(_secretNumber, _salt));
        require(calculatedCommitment == commits[msg.sender], "嘘のデータです!ハッシュが一致しません。");

        // 検証成功!フラグを立てて、安全に次の処理(ゲームの判定など)へ進む
        revealed[msg.sender] = true;

        // ここに本来のビジネスロジック(報酬の付与など)を記述します
    }
}

コードのポイント解説

  • keccak256(abi.encodePacked(_secretNumber, _salt)) という部分で、秘密の数字と合言葉を混ぜ合わせて、外から推測できない暗号の塊(ハッシュ値)を作っています。これが「鍵をかけた箱」の正体です。
  • 攻撃者はメモリプール上で commit() の引数を見ても、ごちゃ混ぜになったハッシュ値しか見えないため、元の数字を特定することができません。

—

フロントエンド(JavaScript / ethers.js)側の実装例

コントラクトの仕組みが分かったところで、今度はユーザーが操作するアプリ側(フロントエンド)のJavaScriptコードを見てみましょう。ブラウザからどのようにこの仕組みを呼び出すのか、一歩ずつ確認していきます。

// 必要なライブラリ(ethers.jsなど)が読み込まれている前提のコードです

async function participateInGame(contract, secretNumber, secretSalt) {
    try {
        console.log("ステップ1: 秘密のデータをハッシュ化しています...");

        // Solidityの keccak256(abi.encodePacked(...)) と同じ形式でハッシュを生成
        // 注意: 文字列や数値を安全に結合してハッシュを作ります
        const commitmentHash = ethers.solidityPackedKeccak256(
            ["uint256", "string"], 
            [secretNumber, secretSalt]
        );

        console.log("生成されたコミットハッシュ:", commitmentHash);

        // まずはハッシュ値だけをブロックチェーンに送信(コミット)
        const txCommit = await contract.commit(commitmentHash);
        console.log("コミットトランザクション送信中... 承認を待っています。");
        await txCommit.wait();
        console.log("コミット成功!誰も中身は分かりません。");

        // ―― ここでしばらく時間(他のプレイヤーのコミット期間など)を置きます ――

        console.log("ステップ2: 自分の正体を明かします(リビール)...");

        // 本当の数字とソルト(パスワード)をそのまま送信して証明する
        const txReveal = await contract.reveal(secretNumber, secretSalt);
        console.log("リビールトランザクション送信中...");
        await txReveal.wait();
        
        console.log("おめでとうございます!安全に処理が完了しました。");

    } catch (error)  {
        console.error("エラーが発生しました:", error);
    }
}

実装時の注意点(初心者がハマりやすい罠)

  • ソルト(secretSalt)の管理が命!:もしユーザーがブラウザのローカルストレージなどにパスワード(ソルト)を保存し忘れたり、ブラウザを閉じたりしてしまうと、後から reveal() ができなくなって自分の資金がコントラクト内にロックされてしまいます。アプリを作る際は、ソルトの管理画面やバックアップに十分気を配りましょう。

—

まとめ:安全なWeb3開発への第一歩

今回は、フロントランニングを防ぐための「コミット・リビールスキーム」について解説しました。

  • メモリプールはガラス張りのカフェ:トランザクションの内容は承認される前に誰かに盗み見られるリスクがある。
  • コミットで隠し、リビールで証明する:先にハッシュ値だけを登録し、後から本当のデータを明かすことで、先回り(フロントランニング)を完全に封じ込めることができる。

最初は少し難しく感じるかもしれませんが、実務の現場でも頻繁に使われる非常に重要でエレガントなセキュリティパターンです。ぜひご自身のプロジェクトやテスト環境でも試してみてくださいね。一歩ずつ、安全なスマートコントラクトの作り手を一緒に目指していきましょう!

コメント

タイトルとURLをコピーしました