🅰️

ERC20トークンを秘匿化して送付できるEncrypted ERC

に公開

はじめに

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

https://amzn.asia/d/gxvJ0Pw

以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください!

https://twitter.com/cardene777

https://chaldene.net/

https://qiita.com/cardene

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://cardene.notion.site/EIP-2a03fa3ea33d43baa9ed82288f98d4a9

今回はAvalancheのEncrypted ERCについてまとめていきます。

以下の公式ドキュメントをもとにまとめていきます。

https://docs.avacloud.io/encrypted-erc

以下のZenn Booksで、より技術的に詳しく説明しているので、この記事を読んだ後にぜひ読んで理解を深めてください。

https://zenn.dev/books/844957de349939/edit

Encrypted ERCとは?

Encrypted ERCは、プライバシー重視で設計されたERC20トークンと互換性がある新しいトークンの仕組みです。
通常のERC20トークンでは、誰でも残高や取引履歴を見ることができますが、Encrypted ERCではゼロ知識証明などの暗号技術を使うことで、残高や取引金額を秘匿しつつ正しく処理されていることを証明できるようになっています。

ブロックチェーンの透明性を維持しながら、個人や企業の「見せたくない」を守る仕組みになっています。

また、プライバシー保護が強すぎると、マネーロンダリング(AML)やテロ資金供与(CFT)対策の観点で問題視される可能性があります。
eERCではこの点を考慮して、「特定の監査機関のみが復号できる権限付き鍵(Auditor Keys)」を導入しています。
これにより、通常はトランザクションの内容を隠しつつ、監査機関などは個別のトランザクションの内容を確認できるようになります。

Encrypted ERCの概要

Encrypted ERCは、スマートコントラクトとゼロ知識回路(ZKサーキット)を組み合わせて動いています。
アドレスの残高や送金額は暗号化された状態で扱われ誰でも検証できる仕組みになっています。
動作モードとして、以下の2つのバージョンがあります。

Standalone Mode

新しいプライベートトークンをゼロから発行したい場合に使用されます。
ERC20トークンの名前とシンボルを設定してプライベートなトークンを発行できます。

  • Private Minting
    発行量やトランザクションの詳細を非公開にした状態で、トークンを新たに発行する処理を実行します。
  • Private Burning
    発行量やトランザクションの詳細を非公開にした状態で、トークンをBurn(0アドレスに送付)する処理を実行します。

Converter Mode

すでに存在しているERC20トークンにプライバシー機能を適用したい場合に使用されます。
トークンそのものは変更せず、秘匿化されたトークンに切り替えられるようになります。

  • Deposit
    既存のERC20トークンを秘匿化トークンに変換して、残高を秘匿化する処理を実行します。
  • Withdraw
    プライベートトークンを元のERC20トークンに戻す処理を実行します。

内部構造

Encrypted ERCの内部では、以下のような複数のゼロ知識証明用の回路(circuits)を使用しています。

  • Registration Circuit
    アドレスの公開鍵が秘密鍵から正しく生成されたことを検証する回路。
  • Transfer Circuit
    残高を秘匿したまま、送金が正しく行われていることを検証する回路。送金の受信アドレスと監査者に向けた暗号化メッセージも生成されます。
  • Mint Circuit
    送付するトークン量を秘匿化したまま、トークンの新規発行(Mint)を実行する回路。
  • Burn Circuit
    トークン量を秘匿化したまま、トークンのBurnを実行する回路
  • Withdraw Circuit
    プライベートトークンを元のERC20トークンに戻す時、残高があることを証明する回路。

暗号技術とプライバシー保護

  • 楕円曲線暗号(BabyJubjub)
    ZK証明などで使用される、軽量かつ安全な楕円曲線です。
  • ElGamal暗号
    ユーザーの残高を暗号化したまま計算・送金できる非対称暗号方式
ElGamal暗号

ElGamal暗号

ElGamal(エルガマル)暗号は、公開鍵暗号方式の1つです。
ElGamalは、離散対数問題という数学の難問を基に安全性を確保しています。

離散対数問題

ある数 g を何回掛け合わせれば別の数 y になるか(𝑔^𝑥 = 𝑦)を見つける計算が非常に難しい問題です。
この「解きにくさ」が暗号の強度を支えています。

特徴

  • 暗号化と復号に「公開鍵」と「秘密鍵」を使う。
    送信者は「公開鍵」でメッセージ暗号化し、受信者の「秘密鍵」のみで復号できます。

  • 乱数を使うことで、同じメッセージでも毎回違う暗号になる
    例えば「100トークン」と書いても、暗号化すると毎回違うデータ表示になります。
    これによって、暗号文から中身を推測することが難しくなります。

  • 加法準同型性
    暗号化されたまま合計できる」という便利な性質です。
    例えば、2人の暗号化された残高を足し合わせたり、送金後の残高を計算したりできます。

    https://note.com/tesso57/n/n9070136d9426

構成要素

  • 大きな素数 p
    素数とは 1 と自分自身でしか割り切れない数。
  • 生成元 g
    この g を繰り返し掛け算(べき乗)することで、p 以下の「ほぼすべての整数」を生成できる特別な数。
  • 秘密鍵 x
    所有者だけが知る大きな数。
  • 公開鍵 y = g^x \bmod p
    秘密鍵から計算された値で、誰でも閲覧可能。
  • 乱数 k
    送信者が毎回ランダムに選ぶ値。メッセージごとに一度限り使われる秘密の乱数。暗号文に影響し、非決定性を担保する。
    毎回 k を変えるので、同じメッセージでも暗号文が一意になりません。

手順

鍵生成

  1. 送信者と受信者が同意した大きな素数 p と生成元 g を準備。
  2. 受信者(例:Alice)はランダムな秘密鍵 x を決め、公開鍵 y = g^x \bmod p を計算して公開。

暗号化

  1. 送信者(例:Bob)は、暗号化したいメッセージを数値 m に変換。

  2. Bobはランダムな一時鍵 k を選び、以下を計算し暗号文 (c_1, c_2) をAliceに送付。

    • c_1 = g^k \bmod p
    • c_2 = m \times y^k \bmod p

復号

  1. Aliceは自分の秘密鍵 x を使い、s = c_1^x \bmod p を計算し、元のメッセージ m = c_2 \times s^{-1} \bmod p を求める(s^{-1}s の逆元)。

ランダムな k を毎回使うため、同じメッセージでも暗号文は毎回変わる点がポイント。

逆元

ある数に掛けることで 1 になるような数のこと。
ある数 a に対して、以下の式を満たす数 b が「a の逆元」。

a \times b \equiv 1 \pmod{p}

  • a = 3
  • p = 7

上記の場合、53 の逆元になる。

この逆元を使うことで、暗号文から元のメッセージ m を正しく取り出すことができます。

ElGamal暗号の例

注意: 実際は桁数が非常に大きい数を使います。

  • p = 23, g = 5
  • アリスの秘密鍵 x = 6
  • 公開鍵 y = 5^6 \bmod 23 = 15625 \bmod 23 = 8

ボブがメッセージ m = 13 を送りたいとします。

  • ランダムに k = 10 を選択。

    • c_1 = 5^{10} \bmod 23 = 9
    • c_2 = 13 \times 8^{10} \bmod 23 = 13 \times 3 \bmod 23 = 17

暗号文 (9, 17) を送信。
アリスは以下のように計算することで元の 13 を取り出せます。

  • s = 9^6 \bmod 23 = 3
  • s^{-1} = 8(23を法とした逆数)
  • m = 17 \times 8 \bmod 23 = 136 \bmod 23 = 13
  • Poseidonハッシュ
    ゼロ知識証明に最適化された軽量ハッシュ関数で検証用ログの暗号化などに利用されます。
Poseidonハッシュ

Poseidonハッシュとは?

ゼロ知識証明(ZKP)に最適化されたハッシュ関数です。
通常のSHA256やKeccakよりもzkSNARKなどの暗号証明で圧倒的に軽く、高速に使えるよう設計された関数です。
従来のハッシュ(SHA256 など)関数では 0/1 のビット列を入力に使用しますが、Poseidonは有限体を用いて計算を行います。
この違いが、証明回路の軽量化につながります。

有限体

整数を「ある大きな素数 p で割った余り」だけを扱う集合のことです。
例)素数 p = 97 なら、096 だけが登場し、97 ≡ 098 ≡ 1 と見なします。

加算や乗算をしても必ず 0p-1 に戻るため、複雑な桁あふれ処理が要りません。
zk回路ではビットごとの演算より、有限体の加算・乗算の方が数十倍少ない制約で済みます。

アルゴリズム

Poseidonはスポンジ構造を採用したハッシュ関数です。
スポンジ構造では、「データの取り込み(吸収)」と「ハッシュ値の取り出し(絞り出し)」の間に複数回の置換処理を挟みます。

内部状態

  • t と呼ばれる要素数を持つ配列(例:t = 3 なら 3 要素)。
  • 各要素は有限体 \mathbb{F}_p 上の整数です。

ラウンドの処理手順

以下を複数ラウンド繰り返します。
手順は計算量を抑えつつ安全性を確保できるよう設計されています。

  1. AddRoundConstants
    各要素にラウンド固有の定数 C_i を加算し、入力パターンを崩します。

    \text{state}[i] \leftarrow \text{state}[i] + C_i
  2. S-box
    非線形変換として5乗(有限体乗算を4回)を適用し、逆算を困難にします。

    \text{state}[i] \leftarrow \bigl(\text{state}[i]\bigr)^5
  3. MDS 行列乗算
    あらかじめ選定された MDS(Maximum Distance Separable)行列を掛けて値を変換します。

    \text{state} \leftarrow M \times \text{state}

フルラウンドとパーシャルラウンド

  • フルラウンド
    • 全ての要素に手順2のS-boxを適用します。
  • パーシャルラウンド
    • 1要素だけにS-boxを適用し、残りの要素は手順1と3のみを行います。

これにより計算コスト(特に zk 回路の乗算回数)を削減できます。

パラメータの選定

設計者は以下を事前計算し公表しています。

  • ラウンド数(フル/パーシャルの組み合わせ)
  • ラウンド定数
  • MDS 行列

これらのパラメータは128 ビット相当の安全性を確保しつつ、ゼロ知識証明で求められる制約数を最小化するよう最適化されています。
実装時は公開パラメータをそのまま用いることで、安全かつ効率的なPoseidonハッシュを利用できます。

Poseidonの例

以下では幅 t = 3、素数 p = 17 の 非常に小さな有限体 を使い、1回のフルラウンドと続くパーシャルラウンドを数値で示します。
実際のPoseidonは 64 ビット~256 ビット規模の素数ともっと大きい定数を用いますが、流れは同じです。

前提

記号 説明
初期状態 [5, 8, 10] 取り込んだ入力データから生成
MDS 行列 \begin{bmatrix} 2 & 1 & 1 \\ 1 & 2 & 1 \\ 1 & 1 & 2 \end{bmatrix} 要素を混合する 3 × 3 行列
1ラウンド目 [3, 7, 2] AddRoundConstants で使用
2 ラウンド目 [5, 11, 13] AddRoundConstants で使用

1ラウンド目(フルラウンド)

手順 状態(各ステップ後)
初期状態 [5, 8, 10]
AddRoundConstants
\text{state}[i] += C_i \pmod{17}
[8, 15, 12]
S-box(全要素)
\text{state}[i] = \text{state}[i]^5 \pmod{17}
[9, 2, 3]
MDS 行列乗算
M \times \text{state} \pmod{17}
[6, 16, 0]

2 ラウンド目(パーシャルラウンド)

手順 状態
前ラウンド終了時 [6, 16, 0]
AddRoundConstants [11, 10, 13]
S-box(先頭要素のみ) [10, 10, 13]
MDS 行列乗算 [9, 9, 12]

ZKアプリでは「証明生成にかかるコスト・時間」がボトルネックになるので、Poseidonのようなzk回路で必要な制約(コスト)が非常に少なく設計されたハッシュ関数が必要になります。

これらの技術により、全ての操作がプライベートかつ安全に行えるようになっています。

アーキテクチャ

Balance & Amount

暗号化された残高と取引額

通常のERC20トークンでは、アドレスが保有している残高を誰でも見ることができました。
しかし、Encrypted ERCでは残高や取引金額が完全に暗号化された状態で保存されるため中身がわかりません。

残高管理

以下のような構造体で、アドレスごとに残高や取引履歴を管理しています。

struct EncryptedBalance {
    EGCT eGCT;  // 暗号化された残高
    mapping(uint256 index => BalanceHistory history) balanceList;  // 過去の残高履歴
    uint256 nonce;  // 残高のバージョン管理に使用
    uint256 transactionIndex;  // 各取引の番号
    uint256[7] balancePCT;  // 最新の残高をPoseidonで暗号化した値
    AmountPCT[] amountPCTs;  // 各取引の暗号化レシート
}

struct BalanceHistory {
    uint256 index;
    bool isValid;
}
  • eGCT
    • ElGamal Ciphertext。
    • ユーザーの現在の残高をElGamal暗号で暗号化した値。
    • 暗号化したまま演算を行うことができます。
  • balanceList
    • 取引ごとに、暗号化された残高(eGCT)と nonce をハッシュ化した値ごとに残高履歴を管理しています。
    • これにより、いつ・どのような残高が存在したかを検証できます。
  • nonce
    • 現在の残高のバージョンを示す管理番号。
    • トランザクション(入金、送金、出金など)ごとにインクリメントされ、同じ値が使用されることはありません。
    • balanceList のキーを生成するときの値の一部であるため、nonce が変更されると残高履歴が無効となります。
  • transactionIndex
    • ユーザーが実行したトランザクション(入金、送金、出金など)の番号。
    • 同じ値が使用されることはありません。
  • balancePCT
    • Poseidon Ciphertext Balance。
    • 現在の合計残高情報を暗号化した値。
    • 各情報を扱いやすくするために配列になっています。
    • amountPCTs の履歴と一致していることがゼロ知識証明可能です。
  • amountPCTs
    • Poseidon Ciphertext Amounts。
    • 各トランザクション(入金、送金、出金など)に対応する暗号化された取引の詳細データ。
    • 複数の取引があるため配列として格納され、後から履歴を検証するために使用されます。
  • BalanceHistory
    • index
      • transactionIndexと同じ値。
    • isValid
      • 残高スナップショットが有効かどうかを示す値。
      • 通常無効な値として記録されることはありません。

残高更新の流れ

ユーザーの残高が変更されるたびに以下の処理が行われます。

  1. ElGamal暗号で新しい残高(eGCT)を計算
  2. 新しい取引に対してAmountPCT(暗号化された取引の詳細データ)を生成・追加
  3. 暗号化された合計残高の情報を示す balancePCT を更新
  4. eGCTnonce を組み合わせてハッシュを生成
  5. 生成したハッシュ値をキーにして、balanceListtransactionIndex とそのトランザクションの状態を記録
  6. transactionIndex をインクリメント

鍵の登録

暗号化されたトークンの使用を行う前に、ユーザーの秘密鍵の情報を事前に登録する必要があります。
この時ユーザーが使用しているEOAの秘密鍵ではなく、別の秘密鍵と公開鍵が必要になります。
理由としては、Encrypted ERCで使用されているBabyJubJub曲線は、Ethereumのアドレスで使用されているsecp256k1曲線と異なるためです。

  • 鍵の正当性を事前に証明
    トランザクション実行時のユーザーの公開鍵が「正しく生成されたもの」であるかを担保します。
  • チェーンを跨いだなりすまし防止
    Chain ID・秘密鍵・アドレスを組み合わせたハッシュを事前登録し、別チェーンで証明された情報や他アドレスの証明の使用を防ぎます。
  • オンチェーンコストの最適化
    各トランザクションで登録した鍵の検証を繰り返す代わりに、一度の登録で済ませることでガスコストが削減できます。

登録するデータ

type RegistrationCircuit struct {
    Sender RegistrationSender
}

送信者の情報だけを受け取り、公開鍵と登録ハッシュを検証します。
公開鍵がBabyJubJub楕円曲線上に存在するかを検証し不正な値の場合は処理が失敗します。

楕円曲線上にあるとは?

暗号技術において「公開鍵」とは、楕円曲線上のある1点(x, y)として表現されます。
この点が曲線上に存在しているかを確認し、もし存在すれば有効とみなします。
存在しなければ不正な値として判断します。

chainId || privateKey || address をPoseidonハッシュでハッシュ化し、渡された値と一致するかを確認します。

登録フロー

  1. ユーザーが RegistrationCircuit 用のゼロ知識証明を生成
    • 公開鍵
    • 秘密鍵を元に生成されたゼロ知識証明
    • 登録ハッシュ
      • Poseidon(chainID, secretKey, userAddress) によって生成されたハッシュ値
  2. Registrarコントラクトにプルーフを送信
  3. Registrarコントラクトが RegistrationCircuit を使って以下を検証
    • 公開鍵がBabyJubJub曲線上に存在するか
    • 公開鍵が秘密鍵に対応しているか
    • 登録ハッシュが正しく計算されているか(チェーンID・秘密鍵・アドレス)
  4. すべての検証に成功した場合、Registrarコントラクトが以下の情報をオンチェーンに保存
    • ユーザーの公開鍵
    • 登録ハッシュ
  5. トランザクション(入金、送金、出金など)ごとに以下の処理を実行
    • トランザクション実行アドレスの登録情報をRegistrarコントラクトに問い合わせ
    • 登録済みであれば処理を許可

Registrar コントラクトの役割

  • 登録情報のリポジトリ
    公開鍵と登録ハッシュを保持し、「このアドレスは事前に検証済み」ということをオンチェーンで検証
  • 権限付与のハブ
    Encrypted ERCコントラクトは、トランザクション実行時にRegistrarコントラクトに登録済みのアドレスか問い合わせてから各処理(入金、送金、出金など)を実行します。

Deposit Operation

Encrypted ERCのConverterモードでは、ERC20トークンを暗号化したトークンに変換することができます。

Token Tracker

Converterモードでは、複数の異なるERC20トークンを同時に扱うために、Token Trackerという仕組みが導入されています。
発行されたトークンには 1 から連番されるユニークな tokenIdが割り当てられます。
ユーザーがまだ登録されていないトークンを初めて預け入れた場合、プロトコルはそのトークンを自動で登録して新しい tokenId を割り当てます。
これにより、さまざまなトークンを一元管理することが可能になります。

ゼロ知識証明が不要

Deposit操作は、Mint・Transfer・Withdraw・Registerと異なり、ゼロ知識証明を必要としません。
これは、Deposit操作が通常のERC20トークンをプライベートな暗号化したトークンに変換する一方向の処理なためです。
この設計により、以下のような利点があります。

  • 入金額はオンチェーンで確認できるため秘匿化する必要がない
  • 入金トランザクションはERC20transferFrom 関数で処理される
  • 秘匿化する部分がないため、ゼロ知識証明が不要

小数点の調整処理

ERC20トークンには、USDCのように「小数点以下6桁」のものもあれば、WETHのように「小数点以下18桁」のものもあります。
Encrypted ERC内部で扱う decimals とズレがあると正しく暗号化できません。
そのため、以下のように桁数の調整を行なっています。

if (tokenDecimals > decimals) {
    uint256 scalingFactor = 10 ** (tokenDecimals - decimals);
    value = _amount / scalingFactor;
    dust = _amount % scalingFactor;
} else if (tokenDecimals < decimals) {
    uint256 scalingFactor = 10 ** (decimals - tokenDecimals);
    value = _amount * scalingFactor;
    dust = 0;
}

この処理は、違う単位で扱われている通貨を共通の単位に合わせて調整するための変換処理です。
例えば、以下のように単位が異なる2つの通貨をそのまま交換することはできないため、共通の単位に変換する必要があります。

  • USDC:「1 USDC = 1,000,000」(小数点6桁)
  • WETH: 「1 WETH = 1,000,000,000,000,000,000」小数点18桁)

そこでEncrypted ERCでは、単位の違いを揃えるために金額を調整します。
小数点が6桁しかない通貨を18桁に揃えば時、元に戻す際に小数点7桁以下の値が扱えない端数(dust)となります。
Encrypted ERCではこの値も記録しており、小数点のズレによって処理しきれずに余ってしまうごく少量のトークンも同時に計算されるようになっています。

暗号化したトークンをERC20トークンに戻す出金時には、逆の処理が実行されて適切なトークン量がERC20トークンとして戻されます。

decimalsとdustの技術的詳細

前提

  • _amount
    • ユーザーが入金したいERC20のトークン数(uint256)
  • tokenDecimals
    • ERC20トークンの小数点の桁数(例:USDCなら6、WETHなら18)
  • decimals
    • Encrypted ERC内部で使用する共通の桁数(たとえば常に18)

処理フロー

  • ERC20トークンの decimals がEncrypted ERCより多い場合
    例: tokenDecimals = 20(小数点20桁)
    decimals = 18(Encrypted ERCでは18桁に統一)

    scalingFactor = 10 ^ (20 - 18) = 100;
    value = _amount / 100;
    dust  = _amount % 100;
    
    • value
      • Encrypted ERC用に調整された金額(整数)
    • dust
      • 割り切れずに余った分(≒ Encrypted ERCで扱えない端数)

    ERC20トークンの単位が細かすぎるため、Encrypted ERC側で扱える単位に切り下て」、余った部分は dust として記録します。

  • ERC20トークンの decimals がEncrypted ERCより少ない場合

    例: tokenDecimals = 6(USDC)
    decimals = 18

    scalingFactor = 10 ^ (18 - 6) = 1_000_000_000_000;
    value = _amount * scalingFactor;
    dust = 0;
    
    • value
      • Encrypted ERCの18桁精度に合わせて拡大した金額
    • dust
      • 乗算なので必ず割り切れる → 0

    ERC20トークンをEncrypted ERC内の細かい単位に変換するためにスケーリングします。

  • ERC20トークンの decimals とEncrypted ERCが一致

    何も処理が実行されない。

残高更新

ユーザーがERC20トークンの預け入れを行うと、預け入れられたトークン量(value)はElGamal暗号によってユーザーの公開鍵を使って暗号化されます。
その後以下のような残高処理が実行されます。

  • 初回の入金であれば eGCT (暗号化された残高)をそのままセット
  • 既存の残高があれば、ElGamalのホモモルフィック加算(暗号化されたまま値を加える演算)によって値を合算
  • そのトランザクションに紐づく amountPCT (暗号化されたトランザクションデータ)を生成し、履歴として記録

この amountPCT はゼロ知識証明の検証時に使用されます。

if (balance.eGCT.c1.X == 0 && balance.eGCT.c1.Y == 0) {
    balance.eGCT = _eGCT;
} else {
    balance.eGCT.c1 = BabyJubJub._add(balance.eGCT.c1, _eGCT.c1);
    balance.eGCT.c2 = BabyJubJub._add(balance.eGCT.c2, _eGCT.c2);
}

ユーザーの残高を暗号化した値(eGCT)に対して、新たな入金(_eGCT)をどのように加算するかを決めるロジックのコードです。
以下の流れで処理が実行されます。

  • ユーザーの現在の暗号化された残高が0(初期状態)かどうかを判定します。
  • 0なら、そのまま今回の入金額(_eGCT)を残高としてセットします。
  • すでに暗号化された残高がある場合は、**暗号化されたまま加算(ホモモルフィック加算)を行って
    新しい残高に更新します。
eGCT

eGCT はElGamal暗号で表現された構造体で、c1c2 という楕円曲線上の点(ECPoint)で構成されています。

c1.X == 0 && c1.Y == 0 は、暗号文の c1 が曲線上の無効(0)点であるかを調べています。
この判定が true の場合、まだ入金がない状態であるため、暗号化された残高に _eGCT がそのまま格納されます。

_add() はBabyJubJub楕円曲線上での加算処理を行います。
c1c2 の2つの値に更新をかけています。

既存の暗号残高に _eGCT を加えることで暗号化されたトークンの残高が更新されます。
ElGamal暗号は暗号化されたまま加算可能なので、暗号化された値を復号せずに演算を行うことができます。

Mint処理

Encrypted ERCのStandaloneモードでは、暗号化されたERC20トークンを発行(Mint)します。

Private Mint

Encrypted ERCのMintでは、ゼロ知識証明とElGamal暗号を使用して以下の処理が実行されます。

  • トークン発行量を公開せずにユーザーの残高に加算
  • 不正な二重発行を防止
  • 規制当局などに向けて必要な情報を保存

保存される情報

Mintでは、以下の4つの情報が回路(サーキット: ユーザーが正しい手順で操作したことを確認するためのルールセット)に入力されます。

type MintCircuit struct {
    Receiver      Receiver         // トークン受領者(公開鍵含む)
    Auditor       Auditor          // 規制監査用の鍵と暗号情報
    MintNullifier MintNullifier    // Mintの一意性を担保するハッシュ
    ValueToMint   frontend.Variable // 発行するトークン量(非公開)
}
  • Receiver
    • 暗号化されたトークンを受け取るユーザーの情報(ElGamal暗号で暗号化)
  • Auditor
    • 規制当局用の公開鍵で暗号化された情報を含む
  • MintNullifier
    • 同じMint証明を使い回さないようにするための一意なハッシュ値(chain IDのPoseidonハッシュ + Auditorの暗号値)
  • ValueToMint
    • 実際のトークン発行量。
    • この値はゼロ知識証明により非公開で処理

回路で行われる検証処理

MintCircuitでは以下の検証が実施されます。

  • MintNullifier(Mintの一意性の証明)が過去に使用されていないか
  • Receiver / Auditor の暗号文が正しく生成されているか
  • MintAmount が Receiver/Auditor` の暗号文に一貫して含まれているか

これにより、証明の使い回しによる二重Mintや、改ざんされた暗号文の使用を防ぎます。

スマートコントラクトでの処理

サーキットによる検証が完了した後、Encrypted ERCのスマートコントラクトでは以下の操作が行われます。

  1. Receiver の公開鍵を使用してElGamalで暗号化されたトークン(eGCT)を生成
  2. 現在の暗号化された残高(eGCT)と新たに発行した暗号化されたトークン、ホモモルフィック加算
    • 暗号状態のまま加算できるたね中身は一切公開されない。
  3. balancePCT (現在の暗号化された残高情報)を更新
    • Poseidon ハッシュにより生成され、後続のZKトランザクションの検証に利用

このプロセスによって、誰がどれだけトークンを受け取ったかは公開されないまま、整合性のあるトークン発行が実現できます。

Transfer Operation

Encrypted ERCでは、送金処理も完全に暗号化されており、送付トークン量や残高を一切公開せずに、送金の正当性をゼロ知識証明で担保して必要な監査情報だけを規制当局に開示可能な仕組みを提供しています。

Transferの構成要素

送金は TransferCircuit という暗号回路を通じて行われ、以下の情報を含みます。

type TransferCircuit struct {
	Sender          Sender
	Receiver        Receiver
	Auditor         Auditor
	ValueToTransfer frontend.Variable
}
  • Sender(送金者)
    • 送金元ユーザーの公開鍵や署名などの識別情報
  • Receiver(受取者)
    • 送金先ユーザーの公開鍵
  • Auditor(監査者)
    • 規制対応のための監査用公開鍵
  • ValueToTransfer
    • 送金する金額(暗号化対象)

送金処理の流れと暗号化の仕組み

送金が行われると、以下のステート変更が行われます。

  • 送金者の eGCT(暗号化残高)が送金額分マイナスされる
  • 送金者の BalancePCT が更新
  • 受取者の eGCT が送金額分プラス
  • 受取者の AmountPCT を新規作成・保存
  • 監査者向け暗号化情報を保存

この一連の処理は、全て実際の金額や残高を一切公開せずに完了します。

  1. ゼロ知識証明での検証

    送金前に、送金者が以下のことを証明します。

    • 送金者が送金する暗号化されたトークンの所有者である(秘密鍵に基づく署名)
    • 暗号化された残高が送金額以上ある(残高そのものは開示しない)

    この処理により、不正利用や残高不足による送金が防止されます。

  2. ElGamal暗号による暗号化送金

    送金額は以下のように暗号化されます。

    • 送金者
      • -X(マイナス送金額)を自分の公開鍵でElGamal暗号化
    • 受取者
      • +X(送金額)を相手の公開鍵でElGamal暗号化

    ElGamal暗号は ホモモルフィック加算(暗号化されたまま加減算が可能)を備えているため、実残高を知らなくても正しい計算が可能です。

    例:「3 → 1 = 残高2」を実際の数値を知らずに検証可能

  3. BalancePCTAmountPCT による状態更新

    • BalancePCT
      • 現在の暗号化された残高情報が更新されて、現在の状態を証明
    • AmountPCT
      • この送金に関連する詳細情報(送金額など)をPoseidon Hashにより暗号化して記録

    これにより、後の検証や監査で過去のすべての送金履歴を整合的に追えるようになります。

  4. Auditor向け暗号化情報の生成

    規制当局や監査機関向けに、以下の情報を含む追跡可能な送金記録が作成されます。

    • 送金額
    • 送信者と受信者の識別情報(それぞれの公開鍵や一意のユーザー識別子など)
    • 暗号化された署名・トランザクションメタデータ

    この情報は監査者の公開鍵で暗号化されているため、監査者だけが後で内容を復号して確認できます。

Withdrawal Operation

Encrypted ERCのConverterモードでは、ユーザーは暗号化されたトークンを、通常のERC20トークンに戻す(出金)ことができます。
ゼロ知識証明を用いて、ユーザーのプライバシーを守りながら整合性のある出金処理が行われます。

出金時に必要な情報と証明

type WithdrawCircuit struct {
	Sender      WithdrawSender
	Auditor     Auditor
	ValueToBurn frontend.Variable `gnark:",public"`
}

出金処理を行う WithdrawCircuit(出金用のZKサーキット)で、以下の情報を受け取ります。

  • Sender
    • 出金を行うユーザーの暗号化された残高情報と識別子
  • Auditor
    • 監査者の情報(出金追跡のため)
  • ValueToBurn
    • 出金するトークン量(公開される情報)

この回路では次のような検証を行います。

  • ユーザーが本当にその残高の持ち主であるか(鍵ペアによる確認)
  • 残高が出金額以上あるかどうか
  • 規制対応のため、監査者向けに送金記録を暗号化して生成

スマートコントラクト側での出金処理

サーキットの検証が成功すると、スマートコントラクトが以下の処理を実行します。

  1. 暗号化残高の更新

    • ユーザーの残高から出金額をElGamal暗号(ホモモルフィック加算)を使用して暗号化したまま減算
    • 減算後の残高は balancePCT(現在の暗号化残高状態)として更新
  2. ERC20トークンへの変換と送金

    暗号化された残高の更新後、通常のERC20トークンに変換してユーザーに送金されます。

    • トークンごとの decimal (小数点精度)を調整して正確なトークン数量を算出
    • ERC20transfer 関数を使ってユーザーに送金

規制準拠のためのAuditor向け記録

出金処理では、以下の情報を監査者の公開鍵で暗号化して記録します。

  • 出金額(公開されるが、誰が出金したかの詳細は暗号化)
  • ユーザーの識別子(公開鍵やハッシュなど)
  • トランザクションメタデータ(タイムスタンプ、nonceなど)

これにより、第三者には公開せずに監査者には確認可能な構造が実現されています。

最後に

今回はAvalancheのEncrypted ERCについてまとめました。

以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください!

https://twitter.com/cardene777

https://chaldene.net/

https://qiita.com/cardene

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://cardene.notion.site/EIP-2a03fa3ea33d43baa9ed82288f98d4a9

Discussion