Skip to content
On this page

L4-2: Privacy Mixer(プライバシーミキサー)

1. 問題

イーサリアムのトランザクションはデフォルトで完全に透明です。すべての送金の送信者、受信者、金額が公開台帳に永久的に記録されます。この透明性は監査とセキュリティに有益ですが、ユーザーの財務プライバシーを深刻に侵害します。誰でもあなたのウォレット残高、取引履歴、および対話した DeFi プロトコルを見ることができます。これはプライバシーの問題であるだけでなく、セキュリティの問題でもあります。大量の暗号資産を保有する個人は攻撃の標的になる可能性があります。

プライバシーミキサーの目標は、ブロックチェーンの分散性と検証可能性を損なうことなく、入金と出金の間のオンチェーン関連付けを断ち切ることです。Tornado Cash は最も有名な実装です。ユーザーは固定額面の ETH(例:0.1 ETH)を入金し、秘密の証憑を受け取ります。後に誰でも(リレイヤーを通じてを含む)この証憑を使用して新しいアドレスに 0.1 ETH を出金でき、オンチェーンの観察者は入金トランザクションと出金トランザクションを関連付けることができません。

これは極めて複雑な暗号学的課題をもたらします。どの入金か明かさずに、特定の入金の証憑を知っていることをどのように証明するか?答えはゼロ知識証明(ZKP)です。具体的には、すべての入金をマークルツリーでコミットし、ZK 回路を使用して葉の位置を明かさずに、ある葉ノードのプリイメージ(nullifier + secret)を知っていることを証明します。

2. 理由

プライバシー保護は、イーサリアムエコシステムにおいて最も重要でありながら未解決の問題の一つです。Tornado Cash は技術的実現可能性を証明しました(制裁前に70億ドル以上の取引高を処理)が、プライバシーツールの合法性に関する激しい議論も引き起こしました。技術的な観点から見ると、プライバシーミキサーはゼロ知識証明を理論から実践に持ち込む完璧なケーススタディです。ZKP がスマートコントラクト内で妥当なガスコスト(約500K gas)で複雑な計算を検証できることを実証しています。

プライバシーミキサーを理解することは開発者にとって多面的な価値があります。

  • 暗号学的実践:マークルツリー + コミットメントスキーム + ゼロ知識証明のエンドツーエンド実装
  • ZKP エンジニアリング:Circom 回路の記述、Groth16 証明の生成、オンチェーン検証の完全なワークフローを理解する
  • プライバシー設計パターン:固定額面、匿名セット、nullifier による二重使用防止。これらのパターンはすべてのオンチェーンプライバシーシステムに適用可能
  • 規制と哲学:プライバシーツールの技術的必要性、合法的なユースケース、および規制上の課題を理解する

Tornado Cash は法的課題に直面していますが、その基盤技術(ZK 証明 + マークルコミットメント)は合法的なプライバシープロジェクトで広く採用されています。zkSync のプライバシー取引、Railgun(コンプライアンスベースのプライバシープロトコル)、そして新興の Privacy Pools(ZK 証明を使用して不正な資金源からの資金を除外するミキサー)などです。

3. 解決策

PrivacyMixer.sol は固定額面(0.1 ETH)のプライバシーミキサーを実装しています。教育的な簡略化として完全な ZK 証明ではなく直接的な鍵開示を使用していますが、そのアーキテクチャは Tornado Cash のコアデザインを完全に保持しています。

3.1 コア暗号プリミティブ

コミットメントスキーム(Commitment Scheme)

  • ユーザーは2つのランダムな32バイト値を生成:secretnullifier
  • コミットメントを計算:commitment = keccak256(abi.encodePacked(secret, nullifier))
  • 入金時にコミットメントをオンチェーンに提出し、マークルツリーに挿入
  • 秘密証憑(secretnullifier)はオフチェーンに保存され、ユーザーが保管する

マークルツリー

  • 高さ20の二分木で、最大2^20 ≈ 1,048,576 件の入金をサポート
  • 増分挿入アルゴリズム(Incremental Merkle Tree)を使用:
    • 各レベル i は「埋め込み中」のサブツリー filledSubtrees[i] を維持
    • 偶数インデックス:現在の葉はそのレベルのサブツリーの左半分。右半分は事前計算された zeros[i] で埋められる
    • 奇数インデックス:現在の葉はそのレベルのサブツリーの右半分。以前に保存された filledSubtrees[i] とペアハッシュされる
    • 各挿入でルートまでのパス全体のハッシュを更新
  • ゼロ値の事前計算:zeros[0] = keccak256(bytes32(0))zeros[i] = keccak256(zeros[i-1], zeros[i-1])

Nullifier による二重使用防止

  • nullifierHash = keccak256(abi.encodePacked(nullifier))
  • 出金のたびに nullifierHash を公開し、使用済みとしてマーク
  • 同じ nullifier を2回の出金に使用できない。同じ入金が複数回引き出されるのを防止する

3.2 入金フロー

  1. ユーザーがオフチェーンで secretnullifier を生成(各32ランダムバイト)
  2. commitment = keccak256(secret, nullifier) を計算
  3. deposit(commitment) を呼び出し、正確に 0.1 ETH を送信
  4. コントラクトがコミットメントをマークルツリーに挿入
  5. commitments[commitment] = leafIndex + 1 を保存(1-indexed、0は未使用を意味する)
  6. 新しいマークルルートを記録:roots[depositCount] = currentRoot
  7. ユーザーが出金証憑として (secret, nullifier, leafIndex) を保存

3.3 出金フロー

  1. ユーザーが (secret, nullifier, recipient, relayer, fee) を提供
  2. コントラクトが commitment = keccak256(secret, nullifier) を再構築し、入金済みであることを検証
  3. nullifierHash = keccak256(nullifier) を計算し、未使用であることを検証
  4. fee < DENOMINATION(リレイヤー手数料が額面を超えてはならない)を検証
  5. nullifierSpent[nullifierHash] = true をマーク(CEI パターンに従い、送金前にマーク)
  6. 送金:recipientDENOMINATION - fee を受け取り、relayerfee を受け取る
  7. リレイヤーメカニズムにより、出金者(recipient)がトランザクションの開始者と異なることが可能になり、プライバシーがさらに強化される

3.4 ZK 検証器インターフェース(IVerifier)

コントラクトには IVerifier インターフェースが予約されており、verifyProof() メソッドが定義されています。完全な Tornado Cash の実装では:

  • 出金操作は secretnullifier を直接提出しない
  • 代わりに「あるコミットメントが MiMC(nullifier, secret) に等しく、かつマークルツリー内にあり、nullifier ハッシュが公開値であることを知っている」ことを証明する Groth16 ZK 証明を提出する
  • Verifier コントラクトが証明、マークルルート、nullifierHash を検証する

3.5 増分マークルツリーアルゴリズムの詳細

_insertLeaf() 関数は効率的な増分挿入を実装します。

function _insertLeaf(leaf):
    index = depositCount
    currentHash = leaf
    
    for i in 0..MAX_DEPTH-1:
        if index % 2 == 0:  // 偶数インデックス — 現在の葉は左側
            filledSubtrees[i] = currentHash
            currentHash = hash(currentHash, zeros[i])
        else:                 // 奇数インデックス — 現在の葉は右側
            currentHash = hash(filledSubtrees[i], currentHash)
        index = index / 2
    
    currentRoot = currentHash

このアルゴリズムの重要な洞察は、ツリー全体のすべてのノードではなく、各レベルに1つの「埋め込み中」のサブツリー値のみを保存すればよいことです。高さ20のツリーでは、ストレージの複雑さが O(2^h) から O(h) に削減されます。これにより、コントラクトはオンチェーンで数百万規模のマークルツリーを効率的に維持できます。

参考ソースファイル:src/level4/PrivacyMixer.sol

3.6 完全な Tornado Cash ZK 回路(概念レベル)

実際の Tornado Cash では、出金は secretnullifier を直接公開せず、Circom ZK 回路を通じて証明を生成します。

text
template Mixer(levels) {
    // プライベート入力(オンチェーンに公開されない)
    signal input nullifier;
    signal input secret;
    signal input pathElements[levels];   // マークル証明
    signal input pathIndices[levels];    // 0=左, 1=右

    // パブリック入力(オンチェーンに公開され、コントラクトが検証)
    signal input root;          // マークルルート
    signal input nullifierHash; // 二重使用防止
    signal input recipient;     // 受取アドレス

    // Commitment = MiMC(nullifier, secret)
    component hasher = MiMC7(2, 91);
    hasher.in[0] <== nullifier;
    hasher.in[1] <== secret;

    // コミットメントがマークルツリー内にあることを検証
    signal leaf = hasher.out;
    for (var i = 0; i < levels; i++) {
        // ルートまで層ごとにハッシュ
        hasher[i] = MiMC7(2, 91);
        // pathIndices に基づいて左右を決定
        hasher[i].in[0] <== pathIndices[i] == 0 ? leaf : pathElements[i];
        hasher[i].in[1] <== pathIndices[i] == 1 ? leaf : pathElements[i];
        leaf <== hasher[i].out;
    }
    root === leaf;  // 最終ハッシュは公開入力の root と一致しなければならない

    // nullifierHash = MiMC(nullifier)
    component nullifierHasher = MiMC7(1, 91);
    nullifierHasher.in[0] <== nullifier;
    nullifierHash === nullifierHasher.out;
}

なぜ keccak256 ではなく MiMC を使用するのか:keccak256 は ZK 回路内での実行コストが極めて高くなります(多数の制約/ゲートが必要)。MiMC(Minimal Multiplicative Complexity)は ZK フレンドリーに特化して設計されたハッシュ関数で、有限体での実装に必要な乗算操作が非常に少なくなります(乗算は ZK 回路で最も高価な制約です)。Tornado Cash 回路は MiMC7(7ラウンド)を使用し、BN254 曲線上で1ラウンドあたり約250の制約しか必要としませんが、keccak256 は数十万の制約が必要です。

4. 遭遇した落とし穴

  • ゼロ知識証明生成の計算コスト:ブラウザで Groth16 証明を生成するには10-30秒かかり、少額の出金にはユーザー体験が悪い
  • 匿名セットが小さすぎる:少数の人だけがミキサーを使用している場合(例:10件の入金のみ)、入金と出金の関連付けは統計分析で簡単に推論できる
  • 固定額面 vs. 柔軟な額面:固定額面(すべての入金が同額)は匿名性を強化するが、ユーザー体験が悪い(正確な金額が必要)。柔軟な額面は便利だが匿名セットを破壊する(異なる金額が関連付けに使用される可能性)
  • nullifier 紛失のリスク:ユーザーが保存した (secret, nullifier) が失われた場合、入金はコントラクトに永久にロックされる。回復メカニズムはない
  • 増分マークルツリーのストレージ管理roots マッピングは各挿入後のルートを保存する。出金にはルートが既知かどうかのチェックが必要で、すべての履歴ルートを反復するガスコストは入金数に比例して線形増加する
  • MiMC と keccak256 の ZK 差異PrivacyMixer.sol はハッシュ関数として keccak256 を使用しているが(教育的簡略化)、実際の ZK 回路は MiMC を使用する。コントラクトと回路が異なるハッシュ関数を使用した場合、検証は失敗する
  • フロントエンドのプライバシー漏洩:ユーザーがミキサーを使用しても、ブラウザが IP アドレスを露出したり、同じ RPC ノードを使用したり、ソーシャルメディアで取引情報を漏らしたりした場合、プライバシーは依然として破壊される
  • Groth16 の信頼できるセットアップ:Tornado Cash の ZK 回路は Powers of Tau 儀式で生成された CRS(共通参照文字列)が必要。セットアッププロセス中に悪意のある参加者が存在し、有害廃棄物が適切に破棄されなかった場合、証明を偽造できる

5. 落とし穴の原因

匿名セットが小さすぎることは、ミキサーの最も根本的な課題であり、「鶏が先か卵が先か」の問題です。少数の入金しかない場合、誰でもすべての可能な入金-出金の対応関係を列挙できます。例えば、3件の入金と3件の出金がある場合、理論的には 3! = 6 通りの対応関係しかなく、プライバシーは深刻に損なわれます。入金数が数千から数百万に増加するにつれて、可能な対応関係の数は指数関数的に増加し、匿名性は大幅に強化されます。

Tornado Cash は固定額面(0.1 ETH、1 ETH、10 ETH、100 ETH)を通じて異なる規模の独立した匿名セットを作成します。同じ額面プール内のすべての入金は区別できません。しかし、これは異なる金額に異なるプールを使用することを意味し、各金額の匿名セットの規模を縮小します。Privacy Pools(Ameen Soleimani が提案した Tornado Cash の後継)は「関連セット」の概念を導入し、ユーザーが自分の入金が特定のフラグ付きアドレスセットに含まれていないことを証明できるようにすることで、法的コンプライアンス要件を満たしつつプライバシーを維持します。

ZK 証明生成の計算コストは Groth16 証明システムの特性に由来します。証明生成は3つのステップで構成されます。(1) ウィットネス計算(JavaScript/WASM で回路を実行)、(2) 証明生成(マルチスカラー乗算とペアリング計算)、(3) Solidity calldata 形式への証明のシリアライズ。ステップ(2)が最も時間がかかります。PLONK、Halo2、Nova などの新しい証明システムは、信頼できるセットアップを排除したり再帰証明をサポートしたりすることで改善を図っていますが、まだオンチェーン検証における Groth16 の支配的地位を完全に置き換えるには至っていません。

増分マークルツリーのルートチェック問題:出金者がマークルルートを提出する際、コントラクトはこのルートが履歴ルートの中に存在するかどうかを検証する必要があります。_isKnownRoot()roots[1..depositCount] を反復し、このループのガスコストは入金数に比例して線形増加します。数百万の入金を持つプールでは、これによりガスコストが法外になります。解決策は root -> bool のマッピングを維持し、クエリを O(n) から O(1) に最適化することです。

6. 落とし穴の解決方法

匿名セットの拡大:額面を標準化し、クロスプールインタラクションを可能にすることでプールサイズを最大化する。複数のアプリケーションが同じミキサーコントラクトを共有できるようにする(Tornado Cash Nova の ETH + ERC-20 マルチアセットプールなど)。

証明生成の最適化

  • Web Worker を使用してバックグラウンドスレッドで証明を生成し、UI をブロックしない
  • ウィットネスを事前計算する(ユーザーインタラクション前に計算を開始)
  • Leo のオンライン証明ジェネレーターや RISC Zero の ZK VM など、新しい証明システムを探索する
  • L2 上にミキサーをデプロイし、より速いブロック確認時間とより低いガス代を活用して全体のレイテンシを削減する

ルートクエリ最適化

solidity
mapping(bytes32 => bool) public isKnownRoot;

function _insertLeaf(bytes32 leaf) internal {
    // ... ツリー更新 ...
    isKnownRoot[currentRoot] = true; // O(1) マーキング
}

function _isKnownRoot(bytes32 root) internal view returns (bool) {
    return isKnownRoot[root];
}

注意:このマッピングは入金数に比例して無限に増加します。2000万件の入金 × 32バイト = 約640MBのストレージで、イーサリアムの実現可能範囲を超えます。超大規模の場合は、ロールアップソリューションを使用できます。ルートを L2 に保存し、L1 では ZK 証明の検証のみを行います。

フロントエンドのプライバシー保護

  • Tor または VPN を使用して RPC ノードに接続する
  • リレイヤー(relayer)パターンを使用する。トランザクションは第三者によって提出され、ユーザーの IP に関連付けられない
  • 時間差出金。入金後、ランダムな時間(数日から数週間)待ってから出金する
  • 出金を入金アドレスとインタラクション履歴のあるアドレスに直接送金しない
  • ブラウザのプライバシーモードを使用し、Cookie をクリアし、トラッキングを無効にする

信頼できるセットアップのリスク緩和

  • Powers of Tau 儀式のセキュリティは、少なくとも1人の参加者が正直にランダムデータを破棄したことに依存する
  • コミュニティで広く検証されたセットアップを使用する(Ethereum Foundation の Perpetual Powers of Tau など)
  • STARK のような透明なセットアップまたはセットアップ不要の証明システムの採用を検討する

7. 技術ポイント

技術ポイント説明
コミットメントスキームcommitment = hash(secret, nullifier)。入金時に鍵を公開しない
マークルツリー高さ20、100万件以上の入金をサポート、増分挿入アルゴリズム
Nullifier 二重使用防止nullifierHash を使用済みとしてマーク。同じ鍵で1回のみ出金可能
固定額面すべての入金が0.1 ETH。匿名セットの均一性を強化
リレイヤーパターン出金開始者 != 受取者。トランザクショングラフの関連付けを断ち切る
CEI パターン送金前に nullifier をマークし、再入を防止
IVerifier インターフェースZK 証明検証の抽象化レイヤー。異なる証明システムに対応可能
Groth16Tornado Cash が使用する証明システム。証明が小さく(~128バイト)、検証が高速
MiMC ハッシュZK フレンドリーなハッシュ関数。低乗法複雑度
CircomZK 回路記述 DSL。R1CS にコンパイル後、証明を生成
増分マークルツリーO(h) ストレージ、O(h) 挿入。ツリー全体を保存する必要なし
ゼロ値事前計算zeros[i] = hash(zeros[i-1], zeros[i-1])。空のサブツリーを置き換え
匿名セットプール内の入金が多いほど、プライバシーが強い
信頼できるセットアップGroth16 は Powers of Tau 儀式の CRS が必要

Built with AiAda