はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

今回は、ゼロ知識証明を使用してETHを誰もアクセスできないアドレスに送信し、後から再ミントできるようにすることで、Ethereumにネイティブなプライバシーソリューションを提供するEIP7503についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

他にも様々なEIP・BIP・SLIP・CAIP・ENSIP・RFC・ACPについてまとめています。

概要

EIP7503とは、「Zero-Knowledge Wormholes」(ゼロ知識ワームホール)と呼ばれる技術を使用して、ETHを誰もアクセスできないアドレスに送信してBurnし、後からそのBurnしたETHをゼロ知識証明で再ミントできるようにする提案のことです。 これにより、送信者のプライバシーを強力に保護しながら、資金を新しいアドレスに移動できます。

「誰もアクセスできないアドレスにETHを送ったら、そのETHは永久に失われるのでは?」と思うかもしれません。 通常はその通りですが、この提案ではプロトコルレベルで新しいルールを導入します。 誰もアクセスできないアドレスにETHが送られたことをゼロ知識証明で証明すると、プロトコルが同額のETHを別のアドレスに新たにミント(発行)します。 元のETHは確かに取り出せませんが、同額の新しいETHが発行されるため、ユーザーの資産は失われません。 送金元と送金先の関連性が断たれることで、プライバシーが保護される仕組みです。

主要な特徴

特徴 説明
秘密のBurn 誰もアクセスできないアドレスにETHを送信することでBurn
ゼロ知識ミント BurnしたETHを証明なしに追跡できない形で再ミント
Plausible Deniability 送信者がプライバシープロトコルに参加したことを証明不可能
ネイティブ匿名性プール 一度も送信したことがないすべてのアカウントがデフォルトで匿名性プールに含まれる
プライバシープール統合 責任あるプライバシー使用のための検証機構を内蔵
最大入金額 32 ETHまで

Plausible Deniability とは、ある行動を行ったことを合理的に否定できる状態のことです。 この提案では、送信者がプライバシープロトコルに参加したことを第三者が証明できないため、強力なPlausible Deniabilityが実現されます。

動機

TornadoCashのような暗号通貨ミキサーはEthereumのトランザクションを効果的に難読化できますが、政府による使用禁止が容易です。 ミキサーコントラクトとやり取りする人は、送信者であれ受信者であれ、マークされる可能性があります。

EIP7503は、資金を送信するためにスマートコントラクトとのやり取りを必要とせず、プレーンなEOA間の転送のみを使用することで、送信者のプライバシー漏洩を最小化しようとしています。

以下の図は、TornadoCashのようなミキサーとEIP7503のアプローチの違いを示しています。

sequenceDiagram
    participant U as ユーザー
    participant M as ミキサーコントラクト
    participant C as ブロックチェーン

    Note over U,C: TornadoCash(既存ミキサー)
    U->>M: ETHを入金(コントラクト呼び出し)
    Note right of M: ミキサーとのやり取りが<br/>オンチェーンで記録される
    M->>U: ETHを出金(コントラクト呼び出し)
    Note right of U: 送受信者がマーク可能

    Note over U,C: EIP7503(この提案)
    U->>C: ETHを通常送金(EOA→EOA)
    Note right of C: 友人への送金と区別不可能
    U->>C: Wormholeトランザクションで再ミント
    Note right of U: プライバシープロトコルへの<br/>参加を証明不可能

ミキサーではコントラクトとのやり取りが記録されるため追跡可能ですが、EIP7503では通常のEOA間送金のみを使用するため、プライバシープロトコルへの参加を第三者が証明できません。

既存ミキサーの問題点

問題点 説明
コントラクトとの明示的なやり取り ミキサー使用が追跡可能
規制リスク 政府による禁止や制裁が容易
送受信者の識別 ミキサーとの関連付けでマーク可能

EIP7503のアプローチ

この提案では、ETHを一旦Burnして別のアドレスで再ミントする仕組みを実現するために、すべてのEthereumアドレスを2つのグループに分割します。

アドレス空間 説明
使用可能なアドレス空間 秘密鍵 ss が存在し keccak(s×G)=pkeccak(s \times G)=p となる、またはスマートコントラクト cc が存在し keccak(cparams)=pkeccak(c_{params})=p となるアドレス
誰もアクセスできないアドレス空間 上記のいずれも存在しないアドレス

s×Gs \times G は、秘密鍵 ss と楕円曲線の基準点 GG を掛け合わせて公開鍵を生成する計算です。 keccak(s×G)=pkeccak(s \times G)=p は、その公開鍵のkeccakハッシュがアドレス pp と一致することを意味します。 つまり「使用可能なアドレス」とは、対応する秘密鍵が存在するEOA、またはデプロイ条件が一致するコントラクトアドレスのことです。

通常のアドレスと誰もアクセスできないアドレスは外見上区別できないため、この事実を利用して、誰もアクセスできないアドレスに送られた資金に対する新たなルールを定義できます。

仕様

パラメータ

定数 値 説明
MAGIC_ADDRESS 0xfe(1バイト) secretと組み合わせて送金先アドレスを生成するためのプレフィックス
MAGIC_NULLIFIER 0x01(1バイト) secretからナリファイア(二重使用防止用の識別子)を生成するためのプレフィックス
MAGIC_POW 0x02(1バイト) secretに対するProof of Work検証用のプレフィックス
MAGIC_CHANGE 0x0404...04(32バイト) お釣り入金のログイベントを識別するためのトピック値
POW_LOG_DIFFICULTY 24 Proof of Workの難易度(24ビット)
MAX_DEPOSIT 32 * 10**18 wei 1回の入金で受け付ける最大額(32 ETH)
WormholeTxType TBD この提案で導入する新しいトランザクションタイプの識別子
WORMHOLE_NULLIFIER_ADDRESS TBD ナリファイアの記録とETHミントに使用するコントラクトアドレス
RECEIPT_PREFIX TBD(データ型 List[bool]) マークルツリー内のレシート位置を示すプレフィックス

以下の図は、secretから各パラメータがどのように導出されるかを示しています。

flowchart LR
    S[secret] --> A["sha256(MAGIC_ADDRESS + secret)[12:]<br/>→ 送金先アドレス"]
    S --> B["sha256(MAGIC_NULLIFIER + secret)<br/>→ ナリファイア(二重ミント防止)"]
    S --> C["sha256(MAGIC_POW + secret)<br/>→ PoW検証(衝突攻撃防止)"]

1つのsecretから、送金先アドレス、ナリファイア、PoW検証値の3つが導出されます。 送金先アドレスはETHの送信先として使用され、ナリファイアは同じ入金からの二重ミントを防ぎ、PoW検証はハッシュ衝突攻撃の難易度を引き上げます。

トランザクションタイプ

新しいEIP2718トランザクションタイプを定義します。 TransactionTypeはWormholeTxTypeで、TransactionPayloadの形式は以下の通りです。

javascript
[chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to, data, access_list, root_beacon_block_number, proof]

このトランザクションペイロードには、標準的なトランザクションフィールドに加えて、root_beacon_block_numberとproofが含まれます。 root_beacon_block_numberは証明で使用するビーコンブロックの番号を指定し、proofはゼロ知識証明を含みます。

ゼロ知識証明の検証

このタイプのトランザクションを検証するには、以下を確認する必要があります。

ゼロ知識証明では、private入力は証明者だけが知っている秘密の情報で、public入力は誰でも確認できる公開情報です。 証明者は「private入力を知っている」ことを、private入力自体を明かさずに証明できます。

証明の入力
パラメータ 入力タイプ 説明
secret private 入金時に生成した秘密の値
main_index private ビーコンチェーンのマークルツリー内でのレシート位置
main_branch private ビーコンチェーンのマークル分岐(存在証明用)
privacy_pool_index private プライバシープールのマークルツリー内での位置
privacy_pool_branch private プライバシープールのマークル分岐(存在証明用)
deposit_value private 入金額
deposit_sender private 入金の送信者アドレス(お釣り入金の場合は0)
change_value_salt private お釣り額のハッシュ化に使用するソルト値
change_value private お釣り額
beacon_block_root public 参照するビーコンブロックのルートハッシュ
nullifier public 二重ミント防止用の識別子
privacy_pool_root public プライバシープールのマークルルート
withdraw_value public 出金額
change_value_hash public お釣り額のハッシュ値

以下の図は、private入力とpublic入力の関係を示しています。

flowchart TD
    subgraph private["private入力(秘密)"]
        S[secret]
        MI[main_index / main_branch]
        PI[privacy_pool_index / privacy_pool_branch]
        DV[deposit_value / deposit_sender]
        CV[change_value / change_value_salt]
    end

    subgraph public["public入力(公開)"]
        BR[beacon_block_root]
        NF[nullifier]
        PR[privacy_pool_root]
        WV[withdraw_value]
        CH[change_value_hash]
    end

    S -->|"sha256(MAGIC_NULLIFIER + secret)"| NF
    CV -->|"sha256(salt, value)"| CH
    DV -->|"deposit - change"| WV
    MI -->|マークル検証| BR
    PI -->|マークル検証| PR

private入力からpublic入力が正しく導出されていることをゼロ知識証明で検証します。 検証者はpublic入力のみを確認し、private入力の中身は知ることができません。

検証条件

証明は以下のすべての条件を検証する必要があります。

条件 検証内容 説明
メインマークル分岐 verify_merkle_branch(root=beacon_block_root, index=RECEIPT_PREFIX + main_index, leaf=make_receipt_hash(...), branch=main_branch) 入金がビーコンチェーンの状態に実際に存在することを確認
プライバシープールマークル分岐 verify_merkle_branch(root=privacy_pool_root, index=privacy_pool_index, leaf=make_receipt_hash(...), branch=privacy_pool_branch) 入金がプライバシープール内の許可リストに含まれることを確認
ナリファイア nullifier == sha256(MAGIC_NULLIFIER + secret) 同じ入金から二重にミントされることを防止するための識別子を検証
Proof of Work sha256(MAGIC_POW + secret) % 2**POW_LOG_DIFFICULTY == 0 ハッシュ衝突攻撃の難易度を引き上げるための計算量制約
お釣り値ハッシュ change_value_hash == sha256(change_value_salt, change_value) お釣り入金の値が正しくハッシュ化されていることを確認
値の整合性 change_value + withdraw_value == deposit_value(符号なし整数、オーバーフローなし) お釣り額と出金額の合計が元の入金額と一致することを確認
最大入金額 deposit_value <= MAX_DEPOSIT 1回の入金が32 ETH以下であることを確認

以下の図は、検証条件の全体像を示しています。

flowchart TD
    A[Wormholeトランザクション受信] --> B{入金がビーコンチェーンに<br/>存在するか?}
    B -->|No| X[拒否]
    B -->|Yes| C{入金がプライバシープールに<br/>含まれるか?}
    C -->|No| X
    C -->|Yes| D{ナリファイアが<br/>正しいか?}
    D -->|No| X
    D -->|Yes| E{PoW条件を<br/>満たすか?}
    E -->|No| X
    E -->|Yes| F{お釣り額のハッシュが<br/>正しいか?}
    F -->|No| X
    F -->|Yes| G{お釣り額 + 出金額<br/>= 入金額か?}
    G -->|No| X
    G -->|Yes| H{入金額が<br/>32 ETH以下か?}
    H -->|No| X
    H -->|Yes| I[ZK証明の検証通過]
    I --> J{追加の検証}

ZK証明の検証を通過した後、さらにオンチェーンで以下の追加検証が行われます。

追加の検証
検証項目 説明
ナリファイア未使用の確認 SLOAD(WORMHOLE_NULLIFIER_ADDRESS, proof.nullifier) == 0。同じナリファイアが過去に使用されていないことを確認し、二重ミントを防止
ビーコンブロックルートの確認 get_beacon_block_root(root_beacon_block_number) == proof.beacon_block_root。証明が参照しているビーコンブロックが実際のチェーン上に存在することを確認

make_receipt_hash関数

python
def make_receipt_hash(deposit_sender: Address, value: uint256, change_value_salt: bytes32, secret: bytes32):
    if deposit_sender != 0:
        to_address = sha256(MAGIC_ADDRESS + secret)[12:]
        topics = [MAGIC_EIP_7708, deposit_sender, to_address]
        logdata = int_to_bytes32(value)
    else:
        topics = [MAGIC_CHANGE, hash(change_value_salt, value)]
        logdata = bytes([])
    return get_ssz_root_hash(Log(topics=topics, data=logdata))

この関数は、入金のレシートハッシュを生成します。 以下の図は、deposit_senderの値に応じた処理の分岐を示しています。

flowchart TD
    A[make_receipt_hash呼び出し] --> B{deposit_sender != 0 ?}
    B -->|Yes: 通常入金| C[secretから送金先アドレスを導出]
    C --> D[EIP7708のETH転送ログ形式で<br/>topics と logdata を設定]
    B -->|No: お釣り入金| E[お釣り額のハッシュを計算]
    E --> F[MAGIC_CHANGE をtopicに設定<br/>logdata は空]
    D --> G[SSZルートハッシュを返す]
    F --> G

通常の入金では、secretから誰もアクセスできないアドレスを導出し、EIP7708のETH転送ログ形式でハッシュを生成します。 お釣り入金では、MAGIC_CHANGEをトピックに設定し、お釣り額のハッシュ値のみを記録します。

トランザクション実行

トランザクションが確認された場合、以下の処理が実行されます。

sequenceDiagram
    participant TX as Wormholeトランザクション
    participant WN as WORMHOLE_NULLIFIER_ADDRESS
    participant TO as 送金先アドレス

    TX->>WN: 1. withdraw_value分のETHを生成
    TX->>WN: 2. ナリファイアを記録<br/>SSTORE(nullifier, 1)
    WN->>TO: 3. 生成したETHで通常のコールを実行
    TX->>TX: 4. お釣りのログイベントを発行<br/>topics=[MAGIC_CHANGE, change_value_hash]
  1. WORMHOLE_NULLIFIER_ADDRESSにproof.withdraw_value分のETHを生成
  2. コールの前にSSTORE(WORMHOLE_NULLIFIER_ADDRESS, proof.nullifier, 1)を設定し、同じナリファイアの再使用を防止
  3. その値を使用してこのアドレスから送金先への通常のコールを実行
  4. topics = [MAGIC_CHANGE, proof.change_value_hash]、logdata = bytes([])のログイベントを発行

トランザクションのハッシュ(証明を除く)がゼロ知識証明を生成するためのランダム性に含まれる必要があります。 これにより、証明が署名として機能します。

補足

アドレスの使用可能性

楕円曲線ベースのデジタル署名では、通常、秘密の値 ss(秘密鍵)が存在し、そこから公開鍵が計算されます(基準点 GG との乗算: s×Gs \times G)。 EthereumのEOAアドレスは公開鍵のkeccakハッシュです。

ss は秘密鍵(ランダムな大きな数値)、GG は楕円曲線上の基準点です。 s×Gs \times G の結果が公開鍵となり、そのkeccakハッシュの下位20バイトがEthereumアドレスになります。 秘密鍵 → 公開鍵 → アドレスの順に一方向で導出され、アドレスから秘密鍵を逆算することはできません。

ハッシュ関数の原像耐性(preimage resistance)により、rr がランダムな値の場合、keccak(x)=rkeccak(x)=r となる xx を見つけることはできません。

keccak(x)=rkeccak(x)=r は「何らかの入力 xx をkeccakハッシュにかけた結果がアドレス rr と一致する」という意味です。 原像耐性とは、ハッシュ値 rr から元の入力 xx を逆算できない性質のことです。 つまり、ランダムなアドレス rr に対応する秘密鍵は誰にもわからないため、そこに送られたETHは誰も引き出せません。

したがって、ランダムなEthereumアドレス rr に送られた資金は誰も引き出せませんが、他の人々は rr が本当にランダムで s×Gs \times G の計算結果ではないことをどうやって確認できるでしょうか?

ランダムな値を生成する手段としてハッシュ関数があります。 アドレスが秘密の値 ss のハッシュと等しい場合、keccak(x)=h(s)keccak(x)=h(s) となる xx を見つけることは計算上不可能なため、そのアドレスからは誰も資金を引き出せないと結論付けられます。

h(s)h(s) は秘密の値 ss を別のハッシュ関数(sha256)に通した結果です。 keccak(x)=h(s)keccak(x)=h(s) は「keccakハッシュとsha256ハッシュという2つの異なるハッシュ関数の出力が一致する入力を見つけられるか」という問題です。 異なるハッシュ関数間でこのような一致を見つけることは計算上不可能とされており、これがアドレスの安全性を保証しています。

入金と出金の仕組み

以下のフロー図は、入金から出金までのプロセスを示しています。

flowchart TD
    A[ユーザー] --> B[秘密を生成]
    B --> C[誰もアクセスできないアドレスを計算<br/>sha256 MAGIC_ADDRESS + secret の下位20バイト]
    C --> D[ETHを誰もアクセスできないアドレスに送信]
    D --> E[ゼロ知識証明を生成]
    E --> F[Wormholeトランザクションを送信]
    F --> G[証明の検証]
    G --> H[新しいアドレスにETHをミント]

入金は2つの場所から行われます。

入金タイプ 説明
標準入金 ETHをsha256(MAGIC_ADDRESS + secret)[12:]のアドレスに送信
お釣り入金 他の入金からの出金時に自動生成

入金全額を出金する必要はありません。 以下の図は、お釣り入金が発生する具体的な例を示しています。

flowchart LR
    A[元の入金<br/>20 ETH] --> B[Wormhole<br/>トランザクション]
    B --> C[出金<br/>12 ETH]
    B --> D[お釣り入金<br/>8 ETH<br/>値はハッシュ化]
    D --> E[次回の出金に<br/>使用可能]

20 ETHを入金して12 ETHのみを出金する場合、システムは8 ETHの新しいお釣り入金を作成します。 このお釣り入金の値はハッシュ化されるため、チェーンを見ている人は「お釣り入金値 + 出金値 = 元の入金値」を計算して元の入金を特定することができません。

プライバシープール機構

この提案にはプライバシープール機構が含まれており、入金のリストに対応するプライバシープールルートを提供する必要があります。 以下の図は、プライバシープールの検証の仕組みを示しています。

flowchart TD
    A[プライバシープール<br/>許可された入金のリスト] --> B[マークルツリーを構築]
    B --> C[プライバシープールルート]
    D[自分の入金] --> E{入金がプール内に<br/>存在するか?}
    C --> E
    E -->|Yes| F[ZK証明でプール内の<br/>入金であることを証明<br/>どの入金かは明かさない]
    E -->|No| G[出金不可]

自分の入金がそのリスト内の入金の1つであることを、どの入金かを明かさずに証明します。

利点 説明
ゼロコスト この第2のマークル分岐機構が証明に統合されているため、ユーザーの追加コストはゼロ
最大採用 コストがないため最大限の採用を確保
責任あるプライバシー 合法的なユーザーのほぼ全員が使用することでエコシステムに利益

プライバシープールの論文では、このメカニズムの価値と、ほぼすべての合法的なユーザーがそれを使用するエコシステムの利点について詳しく説明されています。

PoWと32 ETH制限

組み込みのProof of Workと32 ETH制限は、ハッシュ衝突攻撃を防ぎます。

保護機構 説明
衝突攻撃 create2_address(..., x1) == sha256(MAGIC_ADDRESS + x2)となる衝突(x1, x2)を見つける攻撃
基本難易度 約2802^{80}ハッシュが必要
PoWによる強化 24ビットのPoWで難易度を2922^{92}に増加
攻撃者へのインセンティブ Bitcoinのブロックマイニング(約2792^{79}ハッシュ)の方がより収益性が高い

2802^{80} は約1,000,000,000,000,000,000,000,000(10の24乗)回のハッシュ計算を意味します。 PoWにより24ビット分の追加計算が必要になるため、280×212=2922^{80} \times 2^{12} = 2^{92} に難易度が増加します。 参考として、現在のBitcoinマイニングでは1ブロックあたり約2792^{79}回のハッシュ計算が行われており、この攻撃はそれよりも約8,000倍困難です。

攻撃者がこのような衝突を見つけた場合、ETHを2回引き出すことができます(ただし2回のみ)。 しかし、攻撃に必要な計算能力を持つ者にとっては、Bitcoinマイニングの方がはるかに収益性の高い機会です。

スケーラビリティへの影響

回路が単一の証明で複数のBurnの合計を同時に再ミントできる場合、店舗や中央集権型取引所(CEX)は誰もアクセスできないアドレスで支払いを受け取り、単一の証明(と複数のナリファイア)をブロックチェーンに保存することで、資金を単一のアドレスに集約できます。 これにより、ブロックチェーン上のトランザクション数が大幅に削減されます。

この提案をスケーラビリティソリューションとして使用する人々は、プロトコルのプライバシー保証も向上させます。

後方互換性

ミント機能を使用して生成されたETHは、元のETHと何ら変わりがないはずです。 ミントされたETHはガス代の支払いにも使用できる必要があります。

この提案はEIP7708と、レシートをSSZ形式に変更するEIPが必要です。 これにより、証明はログを検証する通常のSHA256マークル分岐のみを検証すればよく、VerkleやRLPなどの複雑なロジックを証明内で行う必要がなくなります。

セキュリティ

この提案の実装に欠陥がある場合、人々は無限の量のETHをミントする可能性があり、Ethereumの価格が崩壊します。

リスク 説明
無限ミント 実装の欠陥により無制限にETHをミント可能になる可能性
価格崩壊 無限ミントによりETH価格が崩壊する可能性

この提案は非常に強力な機能を提供しますが、実装には細心の注意が必要です。 ゼロ知識証明システムの正確性と、ナリファイアの適切な管理が不可欠です。

参考実装

ZK-SNARK実装要件

実装は以下の条件を満たす必要があります。

要件 説明
透明なセットアップ EIP4844のセットアップを再利用することが理想的。Groth16やその他のアプリケーション固有のセットアップは使用しない
直接検証可能 Circomのようなコンパイラを盲目的に信頼せずに回路を直接検証可能であるべき
SHA256の実装 8ビットルックアップテーブルを使用したPLOOKUPで実装可能
楕円曲線の使用 Starkは大きすぎるため、楕円曲線を使用する必要がある

引用

最後に

今回は「EIP7503によるゼロ知識ワームホールを使用したプライバシーソリューション」についてまとめてきました! いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

他の媒体でも情報発信しているのでぜひ他も見ていってください!