AvalancheのEncrypted ERC
はじめに
初めまして。
『DApps開発入門』という本や色々記事を書いているかるでねです。
以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください!
今回はAvalancheのEncrypted ERCについてまとめていきます。
以下の公式ドキュメントをもとにまとめていきます。
Encrypted ERCとは?
Encrypted ERCは、プライバシー重視で設計されたERC20トークンと互換性がある新しい仕組みです。
通常のERC20トークンでは、誰でも残高や取引履歴を見ることができますが、Encrypted ERCではゼロ知識証明などの暗号技術を使うことで、残高や取引金額を秘匿しつつ正しく処理されていることを証明できるようになっています。
ブロックチェーンの透明性を維持しながら、個人や企業の「見せたくない取引」を守る仕組みになっています。
Encrypted ERCの概要
プロトコルの構成
Encrypted ERCは、スマートコントラクトとゼロ知識回路(ZKサーキット)を組み合わせて動いています。
アドレスの残高や送金額は公開されずプライベートな状態でやりとりされますが、それが不正でないことは誰でも検証できる仕組みになっています。
以下の2つのバージョンがあり、それぞれ目的が異なります。
Standaloneバージョン
このバージョン「新しくプライバシー付きのトークンを作りたい人向け」のバージョンです。
ERC20のように名前やシンボルを設定して、完全にプライベートなトークンを発行できます。
- Private Minting
トークンを新しく発行できますが、その量や誰が発行したかなどの情報は非公開になります。
- Private Burning
トークンをBurnする処理も秘匿化され、誰がいくらBurnしたか公開されません。
Converterバージョン
こちらは、すでに存在しているERC20トークンにプライバシー機能を追加したい場合に使います。
トークンの元の仕組みを変えることなく、「公開されたトークン」と「秘匿化されたトークン」を切り替えられるようになります。
- Deposit(預け入れ)
既存のERC20トークンをプライベートバージョンに変換して、残高を秘匿化できます。
- Withdraw(引き出し)
プライベートバージョンのトークンを元のERC20トークンに戻せます。
システムの内部構造
Encrypted ERCの内部では、用途ごとに異なるZKサーキットが使われています。
- Registration Circuit
アドレスの公開鍵が秘密鍵から正しく導出されたことを証明します。
- Transfer Circuit
送金者が十分な残高を持っていること、送金額が正しく暗号化されていることなどを確認します。
送金は、受取人と監査者にだけ読めるように暗号化されます。
- Mint Circuit
Mintされるトークンの量が正しく暗号化されていて、第三者によって検証できることを保証します。
- Withdraw Circuit
プライベートトークンを元のERC20トークンに戻す時、残高があるかどうかや正しく処理されているかを検証します。
Encrypted ERCでは以下のような技術が使われています。
- 楕円曲線暗号(BabyJubjub)
軽量でZK向けに設計された曲線で、署名や鍵管理に使われます。
- ElGamal暗号
残高を暗号化するのに使われます。
誰にも中身がわからないけど、計算上は正しいとわかるような処理ができます。
- Poseidonハッシュ
ゼロ知識証明に適したハッシュ関数で検証用ログの暗号化などに利用されます。
これらの技術により、全ての操作がプライベートかつ安全に行えるようになっています。
アーキテクチャ
Balance & Amount
暗号化された残高と取引額
通常のERC20トークンでは、残高は単なる数値で管理されており、誰でもその数値を見ることができます。
しかし、Encrypted ERCでは残高や取引金額が完全に暗号化された状態で保存されるため中身がわかりません。
使用されている2種類の暗号化
Encrypted ERCでは、以下の2つの暗号方式が使われています。
- ElGamal暗号(EGCT)
残高そのものの暗号化に使われ、暗号化されたままでも足し算や引き算ができるのが特徴です。
スマートコントラクト上の balances
マッピングで、各アドレスとトークンIDごとに管理されます。
- Poseidon暗号化(PCT)
取引履歴や残高の正しさを検証するために使われる暗号です。
ゼロ知識証明に適した軽量な設計になっています。
EncryptedBalance構造体の中身
以下のような形で、アドレスごとに残高や取引履歴を管理しています。
struct EncryptedBalance {
EGCT eGCT; // 暗号化された残高(ElGamal)
mapping(uint256 index => BalanceHistory history) balanceList; // 過去の残高の履歴
uint256 nonce; // 残高のバージョン管理に使う
uint256 transactionIndex; // 各取引の番号
uint256[7] balancePCT; // 最新の残高をPoseidonで暗号化したもの
AmountPCT[] amountPCTs; // 各取引の暗号化レシート
}
Amount PCT(取引ごとの暗号化レシート)
Amount PCTは、アドレスが行ったそれぞれの取引(入金、送金、Burnなど)を記録した暗号化されたレコードです。
アドレスごとに複数のトランザクションが紐づくため配列で管理されています。
Balance PCT(暗号化された現在の合計残高)
Balance PCTは、複数のAmount PCTの結果としての合計残高を暗号化した1つの値です。
つまり、「このアドレスの現在の残高はこれです」と言える値をPoseidonで暗号化しています。
balanceListとnonceの役割
balanceList
は、過去に有効だった残高のスナップショットを保存しておくためのマッピング配列です。
各残高の状態を一意に特定するために、以下のような処理を行います。
- 暗号化された残高(EGCT)と、現在の
nonce
を使ってハッシュを作成。 - それをキーとして
balanceList
に保存。 - そのときの取引番号(
transactionIndex
)と一緒にBalanceHistory
として記録
nonceとは?
nonce
は、バージョン番号のようなものです。
新しい取引があるたびにインクリメント(+1)されます。
これによって、古い残高の状態を自動的に「無効」と判定できるようになります。
残高更新の流れ
アドレスの残高が変わるときは、以下のステップで処理されます。
- 新しいEGCT(暗号化残高)に更新する。
- その取引に対応するAmount PCTを保存する。
- 必要に応じてBalance PCT(合計残高)を更新する。
- EGCTとnonceをハッシュ化して、新しい状態のキーを作る。
- その状態を
balanceList
に記録し、現在のtransactionIndex
を保存する。 -
transactionIndex
をインクリメント(次の取引の準備)。
Deposit Operation
Converterバージョンとトークントラッカー
Encrypted ERCのConverterバージョンでは、様々なERC20トークンを「プライベートなトークン」に変換することができます。
そのために「Token Tracker(トークントラッカー)」という仕組みを使って、どのERC20トークンが登録されているかを管理しています。
- 最初にデポジットされるとき、トークンが未登録なら、自動で登録されて一意のIDが付けられます(IDは
1
から始まり、0
はStandaloneバージョン専用)。 - この仕組みにより、複数の異なるERC20トークンを一つのプライベート空間で管理できるようになっています。
Deposit処理の特徴
送金やMint、引き出し、登録などの操作はゼロ知識証明が必要ですが、Depositだけは以下の理由から証明なしで実行できます。
- 元の残高や取引はpublicなためブロックチェーン上で確認できる。
-
ERC20の
transferFrom
やbalanceOf
で正しくトークンを預けたか検証できる。 - 暗号化された状態に変えるだけで、何らかの新しい情報を秘匿化しようとしているわけではない。
デシマル(小数点)の調整処理
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;
}
この処理で、小数点のズレによって処理しきれずに余ってしまうごく少量のトークンも同時に計算されます。
ElGamal暗号による暗号化と加算
デポジットされた金額(value)は、ElGamal暗号を使ってアドレスの公開鍵で暗号化されます。
この時点で、その暗号化トークンは「そのアドレスからしか読めない秘匿化されたトークン」になります。
初回デポジットの場合は、そのまま暗号化された残高(eGCT)をセットします。
2回目以降のデポジットでは、ホモモルフィック加算(暗号化されたまま加算)を行います。
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);
}
このようにすることで、残高を暗号化したまま安全に加算できます。
amountPCTの記録
デポジットごとに、Amount PCTという「取引ごとの暗号化レシート」が保存されます。
これは将来、送金や引き出しをするときに「過去の取引は正しかった」と証明するために使われます。
Dust(端数)の扱い
小数点調整の時に出てくる「dust(端数)」は、変換処理の都合で暗号化できないごく少量のトークンです。
アドレスごとにdust
は記録されており、自動的に返却されるように設計されています。
Transfer Operation
送金には、以下の3つが関わります。
- 送金者(Sender)
- 受取人(Receiver)
- 監査者(Auditor)
この3つを活用して、プライバシーを保ちつつセキュリティや規制への対応もできる送金が実現されます。
Transferの全体的な流れ
- 送金者がゼロ知識証明を使って送金を開始
- 自分に十分な残高があることを証明(中身は公開しない)
- 正当な所有者(秘密鍵を持っている)であることを証明
- 送金額を暗号化し、送金処理を実行
TransferCircuitの構成
送金用の回路(TransferCircuit)は以下のような構成です。
type TransferCircuit struct {
Sender Sender
Receiver Receiver
Auditor Auditor
ValueToTransfer frontend.Variable
}
この構造の中で、それぞれの当事者と、送金金額(ValueToTransfer
)が含まれています。
実際の送金処理
送金額(value
)はElGamal暗号を使って処理され、以下のように記録されます。
- 送金アドレスから送金額を引く)
- 受取アドレスに送金額を加える)
この時、ElGamal暗号には「ホモモルフィック加算」(暗号化されたまま加算できる)があるので、暗号化したまま残高を更新することができます。
暗号処理には BabyJubJub という楕円曲線が使われています。
AmountPCTとBalancePCTの更新
送金時には以下の処理が実行されます。
- 送金アドレスの BalancePCT(暗号化された合計残高)を更新
- 受取アドレスの AmountPCT(取引ごとの暗号化レシート)を新たに保存
- 受取アドレスの BalancePCT も更新
監査者(Auditor)向けの暗号化サマリー
規制対応や透明性確保のために、送金内容の要約を暗号化した「監査者専用のデータ」も作成されます。
このデータは監査者だけが復号可能であり、誰が誰にいくら送ったかの確認が可能(第三者には非公開)です。
データの保存とトラッキング
- 送信アドレスの
encrypted balance
は減少される。 -
BalancePCT
(合計残高)も更新されて履歴が保存される。 -
nonce
がインクリメントされて、古い状態は自動的に無効になる。 - 受取アドレス側も同様に
AmountPCT
を追加してBalancePCT
を更新。
公開される情報・されない情報
誰から誰へ「送金があった」という事実は公開され、実際の金額・残高・詳細な取引情報は暗号化されます。
これにより、プライバシーを守りながらも取引が行われていることを保証しています。
Withdrawal Operation
Withdrawalは、暗号化された残高を通常のERC20トークンとして引き出す処理です。
つまり、秘匿化されていたトークンを再びpublicに戻す操作になります。
この処理では、残高があることを証明しつつ規制にも対応できるようにすることがポイントです。
Withdrawal処理の流れ
- ユーザーがWithdrawalをリクエスト
- Withdrawal用の回路(WithdrawCircuit) が起動
- 本人確認と残高確認(ゼロ知識証明で行う)
- 送金内容を監査者向けに暗号化
- 暗号化残高から送金分を減らす
- publicのERC20トークンに変換してユーザーに送る
WithdrawCircuitの構成
Withdrawalに使われるサーキット(回路)は以下のようになっています。
type WithdrawCircuit struct {
Sender WithdrawSender
Auditor Auditor
ValueToBurn frontend.Variable `gnark:",public"`
}
ここで重要なのは、ValueToBurn が "public"
として宣言されている点です。
この金額は、最終的にERC20トークンとして誰でも確認できる形でブロックチェーン上に記録されるため、最初から公開していても問題ないということです。
証明処理の内容
Withdrawal回路では以下のような確認をします。
- 本当に本人か?
公開鍵と秘密鍵を組み合わせたゼロ知識証明で確認。
- 引き出すだけの残高があるか?
残高は暗号化されているけど、ゼロ知識証明で「十分ある」と証明。
- 監査者向けのログも作成されているか?
送金内容の要約を暗号化して、指定された監査者だけが読めるようにする。
スマートコントラクトでの処理
ゼロ知識証明が終わったら、スマートコントラクトが実際の引き出し処理を行います。
- アドレスの暗号化残高(EGCT)から、指定された金額を引く
- BalancePCT(合計残高の証拠)も更新
decimals
の調整をしてERC20トークンに変換- ERC20トークンをユーザーのウォレットに送る
例えば、decimals
18桁精度で、USDCのように6桁精度のトークンを取り出す場合は逆方向のスケーリング処理が行われます。
プライバシーは守られる?
Withdrawalでは暗号化された残高の詳細や個別の取引履歴は公開されません。
公開されるのは、「誰が、どのトークンを、いくら引き出したか」の金額だけです。
そして、監査者向けに渡される暗号化データを除き、他の人が中身を見ることはできません。
使い方
SDKの概要
eERC SDK
は、L1上にデプロイされた EncryptedERC プロトコルと簡単に連携できるようにする開発キットです。
このSDKを使用すると、暗号化トークンに関する以下のような処理をスムーズに行えるようになります。
- 暗号化・復号処理(ElGamalベース)
- ゼロ知識証明の生成
- ユーザーの登録
- 暗号トークンのMint、Burn、Transferなど
複雑な暗号処理やプロトコルとのやり取りを抽象化してくれるので開発者は簡単にアプリに統合できます。
インストール方法
以下のコマンドのいずれかを実行することでインストールできます。
npm install @avalabs/ac-eerc-sdk
# または
pnpm install @avalabs/ac-eerc-sdk
# または
yarn add @avalabs/ac-eerc-sdk
※このSDKを使うには、事前にwagmi
(ウォレット接続ライブラリ)がアプリに組み込まれている必要があります。
2つの主要なReact Hook
このSDKでは、主に以下の2つのカスタムフックが使いやすく提供されています。
useEERC
eERCプロトコル全体との接続を管理するフックです。
このフックを使うことで、以下のような操作が簡単に行えます。
- プロトコルの初期化
- ユーザーの登録
- Mint(新規発行)
- Burn
- Transfer
useEncryptedBalance
アドレスごとの暗号化残高の管理を担うフックです。
- 暗号化された残高の取得
- 暗号化された残高の復号
- ゼロ知識証明を使った残高検証 など
このフックを通じて、暗号化残高をアプリ上で安全かつスムーズに扱うことができます。
ドキュメントとリソース
- [GitHub](準備中)
- npmパッケージ
useEERC
useEERC
は、Encrypted ERC(eERC)プロトコルとやり取りするためのエントリーポイントとなるReact Hookです。
このフックを使うことで、以下のようなeERCの主要機能を簡単に扱うことができます。
- SDKの初期化
- アドレス登録
- 暗号化された残高の取得・復号
- トークンのMint、Burn、Transfer
- 監査者向けの暗号化データの復号
使用方法
まずはpublicClient
とwalletClient
を用意し、必要な引数と一緒にuseEERC
を呼び出します。
const { publicClient } = usePublicClient();
const { walletClient } = useWalletClient();
const {
isInitialized,
isRegistered,
register,
generateDecryptionKey,
publicKey,
name,
symbol,
// 他にも多数の返却値や関数あり
} = useEERC(
publicClient,
walletClient,
contractAddress,
{ transferURL: '', multiWasmURL: '' },
decryptionKey
);
引数の説明
パラメータ名 | 説明 |
---|---|
publicClient |
読み取り用のクライアント(ネットワーク情報取得など) |
walletClient |
送金などのトランザクション送信用クライアント |
contractAddress |
デプロイ済みのeERCコントラクトアドレス |
urls |
ゼロ知識証明用のWASMファイルのURLセット(後述) |
decryptionKey (任意) |
ユーザーに紐づく復号鍵。登録時や後から生成も可能 |
urls
に渡す値の例。
{
transferURL: '/wasm/transfer.wasm',
multiWasmURL: '/wasm/multi.wasm'
}
- ローカルのWASMファイルは
/
から始める必要があります。 - URLが正しくないと証明が生成されないので注意。
また、ウォレットはMPC(マルチパーティ計算)などではなく、seed
から作られた一貫性のあるウォレットを使う必要があります。
復号鍵は署名から導出するため、seed
がないと毎回違う鍵になってしまいます。
戻り値(state)
このフックは、プロトコルの状態やユーザーの登録状況などを含むオブジェクトを返します。
値名 | 説明 |
---|---|
isInitialized |
SDKが正しく初期化されたかどうか |
isAllDataFetched |
必要なデータがすべて取得済みか |
auditorAddress |
プロトコルの監査者のアドレス |
owner |
プロトコルの所有者(デプロイ者)アドレス |
isRegistered |
ユーザーがeERCに登録済みかどうか |
publicKey |
ユーザーの公開鍵(暗号化に使用) |
auditorPublicKey |
監査者の公開鍵(監査用復号に使用) |
shouldGenerateDecryptionKey |
復号鍵の生成が必要かどうか |
areYouAuditor |
現在のユーザーが監査者かどうか |
hasBeenAuditor |
過去に監査者だったかのフラグと確認状態 |
戻り値(関数)
このフックは、いくつかの便利な関数も返してくれます。
register()
ユーザーをeERCプロトコルに登録する関数。
1回登録すればOKです。
register(): Promise<{ key: string; transactionHash: string }>
-
key
- 復号鍵(あとで保存して再利用する)。
-
transactionHash
- 登録時のトランザクションハッシュ。
generateDecryptionKey()
ユーザー用の復号鍵を生成する関数。
register()
時に生成されることもあります。
generateDecryptionKey(): Promise<string>
auditorDecrypt()
暗号化された取引情報を復号する関数(監査者でないと処理が失敗します)。
auditorDecrypt(): Promise<DecryptedTransaction[]>
type DecryptedTransaction = {
type: string;
amount: string;
sender: `0x${string}`;
receiver: `0x${string}` | null;
transactionHash: `0x${string}`;
};
isAddressRegistered(address: string)
任意のアドレスがプロトコルに登録済みかを確認できる関数。
isAddressRegistered('0x...') // => { isRegistered: true, error: '' }
setContractAuditorPublicKey(address: string)
プロトコルの監査者を設定する関数。
オーナーのみ実行可能。
setContractAuditorPublicKey('0x...') // => { transactionHash: '0x...' }
useEncryptedBalance
useEncryptedBalance
は、EncryptedERC(eERC)プロトコル内の暗号化されたトークン残高を管理するためのReactフックです。
このフックを使うことで、暗号化された残高の取得、復号、Mint・Burn・Transferといった操作を簡単かつ安全に行うことができます。
使い方
まず、useEERC
から useEncryptedBalance
を取り出して使用します。
const { useEncryptedBalance } = useEERC(...);
const {
decryptedBalance,
encryptedBalance,
decimals,
// 操作
privateMint,
privateBurn,
privateTransfer,
refetchBalance
} = useEncryptedBalance(tokenAddress);
パラメータ
tokenAddress
省略できますが、Converterモード(既存のERC20トークンを暗号化するモード)ではこのパラメータが必要になります。
対象のトークンアドレスを指定してください(例:0xabc...
)。
戻り値(状態)
変数名 | 説明 |
---|---|
decryptedBalance |
復号されたユーザーの残高(bigint) |
encryptedBalance |
暗号化された残高(bigint[]の配列) |
decimals |
トークンの小数点桁数(EncryptedERC内部での精度) |
戻り値(関数)
privateMint(recipient: string, amount: bigint)
指定したアドレスに対して暗号化トークンをMint(新規発行)する関数。
この処理はコントラクトのオーナーのみ実行可能です。
await privateMint('0xRecipientAddress', 1000000000000000000n);
- 戻り値
{ transactionHash: '0x...' }
privateBurn(amount: bigint)
自分の暗号化残高から指定した金額をBurnする関数。
トークンを完全に無効化させる処理を実行します。
await privateBurn(500000000000000000n);
- 戻り値
{ transactionHash: '0x...' }
privateTransfer(to: string, amount: bigint)
他のユーザーに暗号化トークンを秘匿化した状態で送金する関数。
中身はElGamal暗号化されているので送信額は他人には見えません。
await privateTransfer('0xRecipient', 250000000000000000n);
- 戻り値
{ transactionHash: '0x...' }
refetchBalance()
現在の残高(暗号化・復号済み両方)を取得する関数。
MintやBurnのあとに呼び出すことで最新のデータを取得できます。
refetchBalance();
最後に
今回はAvalancheのEncrypted ERCについてまとめました。
以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください!
Discussion