概要
このドキュメントでは、次の特性を持つモジュール式のシールド プールである Shielder について説明します。
- PSP22(ERC20相当)トークンを共通のシールドプールに預け入れ、引き出しすることをサポートします。
- シールダー内のユーザーには秘密の識別子ZK-IDがある
- オプションのレジストラをサポートします。これは、ZK-IDをホワイトリストに登録して、シールダーに新しいユーザーを登録する責任を負う当事者または当事者のグループです。
- オプションの匿名性取り消し機能(特別な状況下で特定のユーザーの匿名性を解除する権限を持つ当事者または当事者の集合)をサポートします。
- 「安全な方法」でシールドされた転送をサポートします。つまり、転送が完了したとみなされるには、受信者が転送を要求する必要があります。
- ZKプラグインをサポート: 保護されたアカウントに保存された秘密データに対して機能する補助的なプログラム可能なロジック。
以降のページでは、これらのコンポーネントについて段階的に詳しく説明します。
悪質な行為者に対する設計
プライバシーシステムにおける悪質な行為者
ブロックチェーンの許可不要の性質と、シールド プール (このドキュメントで説明されているものなど) によって実現される匿名性の組み合わせにより、悪意のある人物が追跡不可能な方法で資金を移動できるという問題が必然的に発生します。よく引用される例としては、ブラック ハット ハッカーがブリッジ ハッキングで盗んだ資金をシールド プールに預け、後で引き出すつもりで、最初の預け入れへのリンクを消去するというものがあります。これにより、資金が効果的に「ロンダリング」され、実際の出所が隠されます。
シールドプールを経由した資金は、資金が凍結されるリスクなしに、中央集権型取引所 (CEX) またはオフランプに安全に預けることができます。したがって、マネーロンダリング対策を実装していないシールドプールに基づくプライバシーシステム (このようなシステムのすべてではないにしても大多数を占める) は、ハッカーなどの悪意のある人物が盗んだ資金や違法な資金を持ち出すのに最適なツールです。
なぜ気にするのか
私たちは、UX の問題とともに、悪質な行為者の問題がプライバシー システムが広く採用される上での大きな障害であると考えています。実際、この問題を単純に無視できない理由はいくつかあります。
- 違法行為に使用されるため、プライバシー システムは多くのブロックチェーン ユーザーの間では怪しく望ましくないものと見なされています。
- シールドプールの正当なユーザーは、使用しているプロトコルが 1 つまたは複数の国で制裁を受ける可能性があることを常に心配しています (主な例は、OFAC によって制裁を受けた TornadoCash です)。そのような場合、ユーザーは深刻な影響を受ける可能性があります。特定のブラックリストに載ったり、資金を引き出すのに問題が生じたりします。
- プライバシー システムは、正当なユーザーのプライバシーを保護する一方で、悪意のある人物が暗号資産から法定通貨に移行することを容易にします。最終的にその総合的な効果が良いかどうかを評価するのは決して容易ではありません。
プライバシー システムのプロトコル設計を改善し、それを悪用する悪意のある人物に対する対策を講じなければ、このテクノロジ (暗号化層では非常に成熟しているにもかかわらず) が採用される可能性は低くなります。
アプローチの分類
プライバシー システムにおける悪質な行為者の問題の重要性は広く認識されており、その結果、この問題に対処するためのさまざまなアプローチが提案されてきました。以下では、シンプルでありながら包括的なソリューションの分類を提案します。
プライバシー システムの前提は、特定のブロックチェーン アクションを匿名にすることです。一方、悪質な行為者やマネー ロンダリングに対抗するため、プロトコルにはこれらの匿名アクションの作成者を明らかにするメカニズムがいくつか追加されています。分類は、誰が明らかにする必要があるかを決定できるかどうかに基づいています。
- 自発的な開示:これは、ユーザーが自分の匿名性を完全に管理し、ユーザーのみが特定の関係者に自分の行動を明らかにすることを決定できるソリューションのカテゴリです。
- 強制的な開示:このカテゴリでは、ユーザーが同意しない場合でも強制的に開示される可能性があります。
以下では、両方のカテゴリからのいくつかのアプローチについて説明します。
自発的な開示 — アプローチ
キーの表示
表示キーは、ユーザーが匿名のアクションの一部を当事者に公開する方法です。技術的には、ユーザーは特定の当事者に暗号の一部を送信し、これにより、その当事者はブロックチェーン トランザクションの一部をユーザーのものとして公開できます (およびその詳細)。表示キーの考え方は、特定のアクター (監査人、またはシールド プールを通過した資金が正当な出所を持つことを証明する CEX/オフランプ) からの要求に応じてキーを使用することです。この方法で、ユーザーは信頼する (または特定の規制により信頼せざるを得ない) アクターのグループにアクションの一部を公開できます。
悪質な行為者に対抗する効果。選択的開示というアイデアは強力で、ユーザーは自分のデータを管理し続けることができますが、シールド プールでの資産洗浄の問題を解決するものではないと考えています。これは、ユーザー トレースの開示は完全に任意であるため、悪質な行為者はそれを行わないからです。CEX、オフランプ、およびユーザーの預金を扱うその他の機関は、キーの表示による開示を要求し、資金の出所の正当性チェックを実施すべきであり、そのような場合、キーの表示は私たちの問題に対する満足のいく解決策になると主張することもできます。ただし、これは正当なポイントです。
- これらの機関は、情報開示を義務付けることは決してしないだろう。また、たとえ義務付けたとしても、これらの措置を強制的に実施させるプロセスには時間がかかりすぎるため、実行可能とはならないだろう。
- すべての機関がユーザーに資金源を尋ねると、公開されたデータが簡単に漏洩したり、利益のために売却されたりする可能性があるため、シールドプールの価値は急速に低下します。
結論。表示キーは、ユーザーが希望する場合に第三者による履歴監査を行えるため、便利です。技術的には複雑な追加ではなく、重要なユーザー向け機能を有効にするため、各シールド プールは何らかの形式の表示キーを実装する必要があると考えています (Shielder が実装しているように)。ただし、表示キーだけでは、不正な資金を混入する悪質な行為者の問題を解決するには不十分です。
無実の証明
「無実の証明」という概念は、この分野の複数の研究者によって独立して提案されており、 Buterin らによるプライバシー プールの設計の重要な部分となっています。主なアイデアは次のとおりです。
- すべての参加者は資産を預けることでプールに参加できます。
- プールを離れる際(または、後で引き出した資金を使用する際)、ユーザーは、プールへの対応する入金(ここでは、簡単にするために入金と引き出しが 1 対 1 であると仮定しています)が、a) 特定の「ホワイトリスト」入金セットの一部であるか、b) 特定の「ブラックリスト」入金セットの一部ではないかのいずれかであるという zk 証明を生成できます。
ホワイトリストとブラックリストの維持はプロトコルから独立しており、複数のリストが存在する場合もあり、ユーザーは必要に応じて複数のリストに対して正当性を証明することができます。このようなブラックリストは、「違法な」預金、つまり盗まれたか違法に取得されたことが証明された資産で構成されることが予想されます。もちろん、このようなリストに何を含めるかについては明確で客観的なルールは決してありませんが、これはこの記事の範囲外の問題です。
このアイデアは、ユーザーが具体的な預金を明らかにするのではなく、預金セットに含まれていることを証明するという点で、Viewing Keys の一般化と見ることができます。なぜこれを「自発的な開示」カテゴリに含めたのか疑問に思う人もいるかもしれません。これは、ユーザーの許可なしにユーザーの詳細が明らかにされることはないからです。プロトコルがブラックリストに対して無実を証明せずに引き出しを許可しない場合でも、ユーザーの匿名化を解除する必要はありません。実際、ユーザーはプールに無期限に留まることができます。これは問題のようには思えないかもしれませんが、以下の分析で説明するように、それでも大きな問題を引き起こします。
悪質な行為者に対抗する効果。無実の証明の概念は非常に一般的であり、プールへの入金時、プールからの引き出し時、またはプールから第三者への資金の入金時など、いくつかの異なるステップで適用できることは言及する価値があります。最後のオプション、つまり第三者機関が無実の証明を確認する場合、キーの表示と同じ問題に直面します。機関はそのような対策の実装に消極的で時間がかかり、そのような対策を実装していない主要なオフランプ プラットフォームが少なくとも 1 つある限り、ソリューションは全体として機能しません。プールへの入金または引き出し時に証明を生成することに関しては、このソリューションを無効にする固有の欠陥があります。ユーザーが生成することを余儀なくされる証明は、現在のブラックリストまたはホワイトリストに違反しています。特定の資金が違法であると見なされる (ブラックリストに載る) のは、長い時間が経過した後であることがよくあります。これは、悪質な行為者がプールに入金し、待機し、入金がブラックリストに載ることなく引き出すのに十分な場合があります。預金がブラックリストに載ると、彼らはとっくに姿を消しているかもしれないし、さらに悪いことに、彼らがプールに 2 度目の預金をしても、2 度目の預金を不正な最初の預金に結び付ける方法がないかもしれない。これは効率がゼロだと結論付けることはできない。なぜなら、悪質な行為者の仕事が難しくなるだけであり、彼らはブラックリストに載せるタイミングを気にする必要があるからだ。しかし、全体として、これは満足できるとは程遠い。悪質な行為者がシステムを悪用できるからだ。
結論。鍵の表示と同様に、無実の証明は、大規模な機関が悪意のある行為者に対する適切な対策を実施し始めると、効果を発揮する可能性がある優れたアイデアです。ただし、現段階では、それだけでは問題を解決するのに十分ではありません。バニラ バージョンでは、無実の証明は簡単に操作でき (早期の入金と引き出しによって)、その有効性は維持されているブラックリストの品質に大きく依存しますが、これは簡単な問題ではありません。
不本意な暴露 — アプローチ
このアプローチの具体的な例を列挙する代わりに、主なアイデアだけを説明します。具体的なバリエーションの 1 つは、Anonymity Revokersで説明されています。
非自発的開示の考え方は非常にシンプルです。悪質な人物が資金洗浄のためにシールド プールに参加するため、特定の預金からユーザーの痕跡を完全に明らかにするメカニズムが必要です。言い換えると、不正な預金が行われた場合、システムはこの預金を行ったユーザーのすべての行動を明らかにし、ユーザーがシールド プール内で匿名性を獲得するのを防ぎます。特に、引き出し取引は預金に完全にリンクできるため、トークンをシールドしてもそのようなユーザーに影響はありません。重要なのは、ユーザーが匿名化解除に同意する必要がないことです。これが「非自発的開示」と呼ばれる理由です。
技術的には、強制的な開示を実装するには、通常、トランザクションとともに何らかの暗号化が公開され、特定の状況下ではシステム内の特定のアクターによって復号化される可能性があります。また、システム内のどのユーザーを匿名化解除するかを決定できるプロセスと、そのような決定が下された場合にのみ匿名化解除が行われるようにするメカニズムも必要です。暗号学的には、このようなメカニズムを実装するためのアプローチがいくつかあります。以下に、最も風変わりで実現可能性の低いものから、よりシンプルで実用的なものまで、それらをリストします。
- 証人暗号化: 特定のイベントがオンチェーンで発生した場合にのみ復号化可能な暗号文を作成できます。たとえば、特定のユーザーの匿名性を解除するというガバナンス決定が下されます。ただし、これは実用的とは言えません。
- しきい値復号化: ノード委員会が共有キーを保持し、匿名化解除要求があった場合にのみそれを使用することが想定されます。
- TEE: 信頼できるハードウェアはキーを保持し、ユーザーの匿名性を解除する必要がある証明書を受け取った場合にのみそれに基づいて動作するようにプログラムされています。
- 信頼できる当事者: 信頼できる機関が復号化キーを保持します。
悪質な行為者への対抗効果。強制開示は資金洗浄に対抗するのに非常に効果的です。実際、非常に効果的であるため、悪質な行為者はそのような保護されたプールで資金洗浄を試みることすらありません。それは労力と時間の無駄になるからです。このアプローチは、悪質な行為者が短期間でプールに出入りするという無実証明のような問題に悩まされないことに注意してください。この場合、悪質な行為者は確かにそれを行うことができますが、その後、入金と出金の取引は遡及的にリンクされたままになるため、ブロックチェーン分析会社や法執行機関はそのようなユーザーを追跡することができます。
欠点。前述のように、強制的な開示は、シールド プールで資金を洗浄しようとする悪質な行為者に対抗するのに非常に効果的ですが、このアプローチに欠点がないわけではありません。ユーザーが基本的に失うのは、トランザクション トレースを誰に開示するかを決定する完全な権限です。匿名性の取り消しが具体的にどのように実装されるかによって、これは多かれ少なかれ心配になる可能性がありますが、どのように行われるかに関係なく、懸念されるのは次の点です。
- ユーザーが不当に匿名化解除される可能性がある。
- 匿名性取り消し者の漏洩した鍵により、ユーザーの匿名性が失われる可能性がある。
- 特定の実装では、一部の当事者が、このことが明らかにされることなくすべてのユーザーを追跡する可能性があります。
これらの懸念はすべて正当なものですが、匿名性の取り消しの暗号化基盤がより成熟するにつれて、これらの懸念の深刻さを軽減できると考えています。また、上で徹底的に議論したように、残りのアプローチはすべて十分ではなく、問題を実際に解決するものではないため、トレードオフを受け入れなければなりません。
その他のアプローチ
シールドプールにおける資産ロンダリングに対する可能な対抗策としてコミュニティで提案または言及されている他のいくつかのアイデアについて説明します。それぞれについて、その実現可能性を評価します。
ユーザー ゲーティング (KYC)
シールドプールに入る際に KYC を強制すれば、シールドプールでの資金洗浄の問題が魔法のように消えるという誤解があります。これは真実とは程遠く、実際には KYC はまったく役に立ちません。確かに、引き出しと預金がリンクできない場合 (シールドプールの主な前提)、特定の預金が違法行為による資金で構成されていたことを知った後、できることはあまりありません。実際、資金はリンクできないトランザクションを介して引き出されます。預金者の身元を知っていても、プールをミキシングに使用するのを防ぐのにはあまり役立ちません。
- ユーザーの身元を知っても、混在を防ぐことはできない。
- 悪意のある人物が偽造 KYC を入手するのは非常に簡単なので、入金に使用された身元を持つ個人を起訴することさえ不可能です。なぜなら、その人物は犯罪者ではない可能性が高いからです。
上記を踏まえると、この対策を回避するために必要なのは、KYC を購入することだけです。これは、悪意のある行為者が行うことが知られています。
レート制限
シールドプールで最も危険な行為は、常に多額の入金と多額の引き出しです。これは、悪意のある人物が資金を混ぜようとする典型的な方法です。したがって、ユーザーごとに適切な引き出しと入金の制限という形で対策を提案することができます。
この解決策にはいくつか問題があります。
- この対策を実装するには、まずシビル耐性を実現する方法が必要です。これはプライバシー システムでは可能ですが、難しい作業です。シビル耐性がなければ、ユーザーは ID を複数に分割して制限を回避します。
- シビル耐性が最も強力な形態である KYC があっても、悪意のある人物が複数の ID を購入し、資金を複数のアカウントに分割して洗浄するのを防ぐことはできません。新しい KYC を取得するコストは、彼らが混合しようとしている資産の価値と比較すると、おそらく無視できるほど小さいでしょう。
- シールド プールの正当な使用方法として、ユーザーが 1 日のうちに同じ資金を複数回引き出したり預け入れたりすることが挙げられます。たとえば、シールド プールから外部のパブリック コントラクトとのやり取りを実装する場合などです。このようなやり取りのすべてが制限にカウントされると、ユーザーはすぐに制限に達し、ユーザー エクスペリエンスが低下します。
- 制限を調整することは、ユーザーエクスペリエンスとマネーロンダリング対策のバランスを取るのに非常に難しいかもしれません。実際、制限が 1 日あたり 1,000 米ドルであっても、悪意のある人物は年間 365,000 米ドルをマネーロンダリングできます。これは大きな金額であり、悪意のある人物にとって特に問題にはなりません。
準備 – ZK 関係
このドキュメントでは、ZK-SNARK を使用して証明される特定の関係を頻繁に説明します。このセクションの目的は、そのような各記述がどのように構成されているか、および記述の各部分が何を意味するかを説明することです。
通常、このような低レベルの詳細には到達しませんが、関係のすべての入力はフィールド要素であることを覚えておくことが重要です (基礎となるペアリング システムのスカラー フィールドを考えてください。暗号化 を参照してください)。コードでは、スカラー フィールド要素の型を で表します。制約を設計する場合、これらの低レベルの詳細が重要になることがよくあります。制約がスカラー フィールドの小さな算術回路で表現可能であること (言い換えると、「SNARK 対応」であること) により、算術化時に複雑さが低くなることが望まれます。詳細な背景については、MOOC https://zk-learning.org/Scalar
など、オンラインで利用できる SNARK に関する優れたリソースを参照してください。
ZK関係
以下のような構造を持つボックスで関係を記述します。
relation R
inputs:
- x_1
- x_2
- ...
- x_n
witnesses:
- w_1
- w_2
- ...
- w_m
constraints:
1. C_1
2. C_2
3. ...
4. C_r
このボックスが何を意味するのかの正確な数学的説明に興味がある場合は、以下のR の数学的説明を参照してください。興味がない場合は、ボックスの 3 つのセクションに何がリストされるかについて、いくつかの直感を説明します。
入力
このセクションでは、関係に対する「公開入力」を指定します (公開入力のベクトルを と表しますx
)。つまり、ユーザーが秘密にする必要のない、チェーン上で公開される入力です。これが署名スキームである場合、このセクションで公開キーを公開する可能性があります。または、このセクションで暗号化されたメッセージを公開したり、プレーンテキストで入力する必要があるデータを公開したりすることもできます。多くの場合、いくつかの大きな構造のハッシュが入力として公開されますが、これには 2 つの目的があります。
- 圧縮: ハッシュは、基礎となる、おそらく大きな構造よりも小さくなります。
- プライバシー: 基礎となるデータは、非公開のコミットメントの役割を果たすために、ランダムなソルトを使用してハッシュされることがよくあります。
証人
このセクションには、与えられた入力に対して関係が成り立つw
ことを証明者が証明するために必要なデータが含まれています。実際、証明関数の典型的な実装は次のようになります。x
R(x)
generate_proof(R, x, w)
したがって、関係の説明R
(おそらく算術回路として)、入力ベクトル、および証明者が証明を生成するために必要な回路上のすべての必要な値を生成できるようにするx
「証拠」を入力として受け取ります。w
w
読者は、 が関係への入力であると考えたくなるかもしれません。この直感は間違ってはいませんが、時々混乱を招き、表記法や実装において誤解を招く可能性があります。心に留めておくべきより良い直感は、 「R の数学的説明」で説明されていることです。大まかに言えば、 への入力は のみR
ですx
が、残りの入力 (制約に現れるもの) はy
すべて、証拠 が与えられれば効率的に計算できます。w
制約
制約は、パブリック入力のバイナリ述語を定義しますR(x)
(技術的な詳細については以下を参照)。制約は通常、高水準言語の擬似コードとして記述されますが、最終的には算術回路 (または同様の算術化を使用) として記述されることを念頭に置くことが重要です。これらの制約には、入力や証人として言及されていない他の変数が明示的または暗黙的に含まれることがよくあります。これらは、存在量指定子によってバインドされていると考える必要があります。
例
relation R
inputs:
- x_1
- x_2
- x_3
witnesses:
- w_1
constraints:
1. w_1 * w_1 = y_1
2. y_1 <= x_1
3. hash(x_2, y_1) = y_2
4. hash(y_2) = x_3
上記では以下の点に注意してください:
- および が与えられれば、すべての変数を
y_1, y_2
効率的に計算できます。しかし、 がなければ、それらを計算することはできません。x
w
w
- 制約は
y_1 <= x_1
高水準言語で記述されています。低水準でこの制約を算術回路として表現できるようにするには、y_1
この 1 つの高水準制約をエミュレートするためだけに、新しい暗黙の変数 ( のバイナリ分解) と多くの算術制約を導入する必要がある可能性があります。この場合、暗黙の変数は証人として表示されませんが、y_1
およびから計算可能でありx
、したがってw
およびからも計算可能ですx
。
Rの数学的記述
数式をそのまま使用して、以下の文章を日本語に翻訳しました:
上記のボックスは、関係 ( R(x) ) を記述しています。ここで、( x = (x_1, x_2, …, x_n) ) です。関係 ( R ) を定義するために、式 ( C_1, …, C_r ) に登場する変数のうち ( x ) 以外のすべての変数を含むベクトルを ( y = (y_1, …, y_n’) ) と表します。つまり、式は実際には以下のようになります:
[C_1(x, y), C_2(x, y), …, C_r(x, y)]
ここで ( R ) は次のように定義されます:
[R(x) = \exists y (C_1(x, y) \land C_2(x, y) \land … \land C_r(x, y))]
上記の記述では、証人 ( w ) は明示的には言及されていません。特に、必ずしも ( y = w ) である必要はありません(通常、この等式は成立しません)。重要なのは、( w ) が ( y ) の中の変数の部分集合であるということだけです。より一般的な直感として覚えておくべき点は、固定された ( x ) が与えられたとき、「適切な証人」 ( w ) を使って、以下を満たす ( y ) を効率的に生成できることです:
[C_1(x, y) \land … \land C_r(x, y)]
メモとアカウント
ここでは、基本的なシールダーの設計について説明します。次に、「ZK-ID とレジストラ」セクションでは、シビル耐性のために ZK-ID を強化し、「匿名性リボーカー」セクションでは、悪意のある行為者と戦うのに役立つ改善を提案します。
はshielder
、次の内容を保持するスマート コントラクトです。
notes
— 固定深度のバイナリ マークル ツリーH
— このツリーの各ノードはScalar
要素です。ツリーのリーフにはユーザーのハッシュが保持されますNotes
(下記参照)。nullifier_set
Scalar
–古いメモを無効にすることを目的としたタイプの要素のセットroots
— 技術的な理由から必要な、すべての歴史的なマークルのルートのリスト- 簡潔にするために省略した、関連性の低いその他のストレージ項目。
注記
マークルツリーの各葉はnotes
のハッシュですnote
。 はNote
データ構造です。
struct Note {
id: Scalar, // the ZK-ID of a user
trapdoor: Scalar, // a secret needed to prove ownership of the note
nullifier: Scalar, // a secret used to invalidate the note
account_hash: Scalar, // the hash of the user’s Account state
}
1) のハッシュを Merkle Tree に保存しNote
、2) はtrapdoor
永久に秘密のまま (ユーザーのみが知っている) であるため、が公開されても とid
は非公開のままであることに注意してください。account
nullifier
ZK-IDについては、ZK-IDとレジストラで説明されています。
アカウント
構造がどのようなものか具体的に記述する代わりにAccount
、アカウントが持つべき操作/プロパティを抽象的に定義します。Rust の用語を採用することで、Account
「特性」を定義します。つまり、アカウントで定義する必要があるすべてのメソッドを指定します。
fn new() -> Account
新しいアカウントを作成します。fn hash(acc: Account) -> Scalar
Scalar
(フィールド要素) へのハッシュfn update(acc: Account, op: Operation) -> Account
update
は、またはのOperation
ような状態が与えられた場合のアカウントの状態遷移関数です。add 2 ETH
subtract 5 AZERO
操作のセットは、具体的に何をサポートしたいかによって異なります。ただし、次のようなことを念頭に置く必要があります。
enum OperationSimple {
depositFT(Amount, TokenId, AccountId),
withdrawFT(Amount, TokenId, AccountId),
depositNFT(Id, AccountId),
withdrawNFT(Id, AccountId),
}
アカウントの表現方法とアクセス方法に関する詳細により、操作の説明に関して追加の技術的な詳細が生じますが、それらは高レベルの理解に必須ではありません。
代替可能なトークンの固定リストのみをサポートする最も単純なアカウント構造は次のようになります。
AccountSimple {
balance_AZERO: Scalar,
balance_USDT: Scalar,
balance_USDC: Scalar,
balance_wETH: Scalar,
}
depositFT
この構造は非常にシンプルで、2つの操作とがあると仮定して、必要なすべてのメソッドを実装できますwithdrawFT
。欠点は、より多くのトークンタイプやNFTに簡単に拡張できないことです。そのため、以下のように、より複雑な構造を使用すると有利になる場合があります。
AccountAdvanced {
balance_AZERO: Scalar,
other: Array<Scalar, 256>,
}
このother
フィールドはエントリの配列を意味し256
、各エントリは FT または NFT のいずれかの資産で、ハッシュとして表され、たとえばhash(ETH, 4)
4 ETH を表します。このアカウント構造は確かにより柔軟ですが、ハッシュ化して正しい更新を効率的に証明する際に問題が発生します。より具体的には、次の ZK 関係を効率的に証明することに関心があります。
relation R_update_account
inputs:
- op: Operation // operation to be performed
- h_acc_old: Scalar,
- h_acc_new: Scalar,
witnesses:
- acc_new: Account,
- acc_old: Account,
constraints:
1. acc_new = Account::update(acc_old, op)
2. h_acc_old = Account::hash(acc_old) // Account::hash is the hash method of the Account trait
3. h_acc_new = Account::hash(acc_new)
私たちにとって重要なのは、R_update_account
を小さな算術回路として記述して、 の SNARK をR_update_account
効率的に生成できる (証明器の効率) ようにすることです。 の公開入力がとR_update_account
のハッシュであり、値そのものではないことも偶然ではありません。 アカウントのサイズは ( のように) 重要である可能性があるため、回路に明示的に含めないようにします。 制約でとについて言及されているにもかかわらず、 が十分にスマートである限り (たとえば、でMerklize できる)、回路はアカウント全体をアンパックする必要はありません。 この方法では、 のサイズで回路のサイズを対数 (または一定)にできる可能性があります。acc_old
acc_new
AccountAdvanced
acc_old
acc_new
Account::hash
other
AccountAdvanced
R_update_account
Account
オペレーション
最大限の柔軟性と、それほど重要ではない特定の使用パターンを可能にするために、 に抽象化レイヤーを導入しますOperation
。つまり、各操作は次のようop: Operation
に分割できると想定します。
op_priv: OpPriv
– ユーザーが明かさない操作の「プライベート」な部分op_pub: OpPub
– トランザクション内で表示される操作の「公開」部分
さらに、関数が存在すると仮定する。
fn combine(op_priv: OpPriv, op_pub: OpPub) -> Option<Operation>
Operation
これにより、パブリックおよびプライベートの対応物が与えられた場合、上記のようなを抽出できます。 の出力combine
は でありOption<Operation>
、Operation
失敗する可能性があることを意味しているわけではないことに注意してください。後続の例から、なぜそうなるのかは明らかです。
覚えておくべき直感は、op_pub
プレーンテキストでは がユーザーが送信するトランザクション( の一部calldata
)に添付されるのに対し、op_priv
はユーザーがトランザクションを実行するときに証明する ZK 関係の証拠の一部にすぎないということです。通常、op_priv
は次のいずれかを保持するために使用されます。
- ユーザーが隠したいデータ。たとえば、別のユーザーに資金を送金する場合、
op_priv
受取人の「アドレス」と送金金額が含まれる場合があります。 - 操作の公開実行(トランザクションの説明を参照)には必要なく、アカウントの表現方法に関する技術的な詳細であるデータ。たとえば、
other
特定の資産に関するデータを保存するために配列のどのインデックスを使用するかについての詳細を含めたい場合があります。AccountAdvanced
op_priv = op
興味深いオプションの 1 つは、 と を設定することです。op_pub = hash(op)
これにより、サイズがop_pub
1になりScalar
、検証の複雑さに適しています。ソルトを使用してハッシュをランダム化することで、完全なプライバシーを実現することもできます。ただし、これは、契約が特定の金額のパブリック トークン転送を受け入れる必要がDeposit
ある操作には適していない可能性があり、 がプライベートなshielder
場合は実行できません。amount
例:
AccountSimple
を使用しAccount
、Operation
型が に似ている場合は、 and (unit type — “empty”)OperationSimple
を使用できます。OpPub = Operation
OpPriv = ()
AccountAdvanced
を使用する場合、Account
型Operation
には単なる詳細以上のものが含まれている必要がありますOperationSimple
。実際、ユーザーがdeposit
で操作を行う場合+10 ETH
、配列op
のどのセルをどのように変更する必要があるかという情報が含まれている必要があります。したがって、は の非決定論的な詳細を指定し、はの「人間が読める」表現にすぎないother
と考えることができます。op_priv
op
op_pub
op
ノートの更新
ブラックボックスとして使うことでR_update_account
、ノートを更新するために必要な関係を定式化できる。
relation R_update_note
inputs:
- op_pub: OpPub // the public part of the operation to be performed
- h_note_new: Scalar,
- merkle_root: Scalar,
- h_nullifier_old: Scalar,
witnesses:
- note_new, note_old: Note,
- trapdoor_new, trapdor_old: Scalar
- nullifier_new, nullifier_old: Scalar,
- proof: MerkleProof
- op_priv: OpPriv
- id: Scalar
constraints:
1. h_note_new = hash(note_new)
2. note_new = Note { id, trapdoor_new, nullifier_new, h_acc_new }
3. h_note_old = hash(note_old)
4. note_old = Note { id, trapdoor_old, nullifier_old, h_acc_old }
5. h_nullifier_old = hash(nullifier_old)
6. verify_merkle_proof(merkle_root, h_note_old, proof)
7. op = combine(op_pub, op_priv)
8. R_update_account(op, h_acc_old, h_acc_new)
ヌルファイアのハッシュは公開されており、コントラクトはそれを に追加してnullifier_set
、同じ紙幣を再度使用することを防ぎます。ヌルファイア自体を公開しない理由は、フロントランニング攻撃を防ぐためです。具体的には、悪意のある人物がユーザーのヌルファイアを傍受し、そのヌルファイアを使用して独自の紙幣を作成し、ユーザーが使用する前に使用して、ユーザーの紙幣を無効にする可能性があります。
注:実際には、効率性のために、を公開入力としてR_update_account
持つ代わりに、が、 が取り込むことができるさまざまな「バリアント」によってパラメータ化される可能性があります。たとえば、およびバリアントに固有の入力を取るおよびなどを使用します。実際にはこれがより優れている理由は、証明器の効率性が回路サイズに依存し、1 つの回路ですべてのバリアントをサポートすると、回路が不必要に大きくなるためです。
トランザクションの更新ノート
最後に、ユーザーがメモを更新するために送信したトランザクションの擬似コードを書くことができます。
transaction update_note
inputs:
- op_pub: OpPub,
- proof: ZkProof,
- h_nullifier_old: Scalar,
- merkle_root: Scalar,
- h_note_new: Scalar,
execution:
- shielder.public_exec(op_pub)
- assert: merkle_root is the current or historical root of shielder.notes
- assert: h_nullifier_old not in shielder.nullifier_set
- v = ZK-Verifier(R_update_note) // initialize verifier for the relation R_update_note
- assert: v.verify(proof; (op_pub, h_note_new, merkle_root, h_nullifier_old))
- shielder.notes.add_leaf(h_note_new)
- shielder.nullifier_set.add(h_nullifier_old)
上記は、ZCash のようなプライバシー システムを研究した人にとっては馴染みのある内容のはずです。
最初の命令は、shielder.public_exec(op_pub)
操作にop_pub
必要な「パブリック状態」に対するすべての操作を実行します。以下にいくつかの例を示します。public_exec(op_pub)
トランザクションの最初の操作としてトークン転送などを実行する場合でも、後の操作 (証明の検証など) が失敗した場合はロールバックされるため、実際にはこの命令が関数本体のどこに配置されているかはそれほど重要ではないことに注意してください。
例: depositFT 操作
// Below implementation for the depositFT variant
fn public_exec(op_pub: OpPub) {
let depositFT { amount, token_id, user } = op_pub;
assert allowance(user, shielder) >= amount;
transfer amount of token_id token from user to shielder;
}
上記において、ユーザーがshielder
契約に十分な許可を与えていない場合、 はassert
失敗し、したがって の実行update_note
も失敗することに注意してください。
上記は、操作の「公開」部分が行うことです。前述のように、部分と組み合わせるop: Operation
と発生する「完全」バージョンは、ユーザーのプライベート アカウントを更新するために使用されます。 この場合に行うべきことは、詳細に応じて、非常に簡単です。op_pub
op_priv
Account::update(acc, op)
Account
- ハードコードされたフィールドの1つ(
AccountSimple
)を増分するか、 acc.other
( )内のセルの1つにトークンを追加しますAccountAdvanced
。
例: withdrawFT 操作
// Below implementation for the withdrawFT variant
fn public_exec(op: Operation) {
let withdrawFT { amount, token_id, user } = op_pub;
transfer amount of token_id token from shielder to user;
}
ZK-IDとレジストラ
Shielder の各ユーザーは、均一にランダムな要素である ZK-ID を生成する必要がありますid: Scalar
。これは、Shielder でのユーザーのすべてのアクションの匿名性を解除できるため、ユーザーが誰にも明かしてはならない秘密です。
オプションで、特別な当事者であるレジストラが存在します。レジストラの役割は、ユーザーをシールダーに登録し、アカウントを作成できるようにすることです。レジストラは、検証されたユーザーのオフチェーン データベースを維持します。設定に応じて、レジストラは 1 つ、複数、または 1 つも存在しない場合があります。
各レジストラは、公開されている対応する鍵がよく知られている ECDSA「レジストラ キー」を保持しています (レジストラが発行した署名を検証するために、たとえば、Shielder スマート コントラクトに保存されます)。このキーはローテーションできますが、以下の簡略化された設計ではそれが考慮されていません。
レジストラへの登録
ZK-ID を持つユーザーは、id
オンボーディング プロセス (特定のレジストラによって異なります) を経て、レジストラCom(id)
に へのコミットメントを提供することで、オフチェーンでレジストラに登録id
します。最も単純なケースでは、コミットメントは だけです。c = Com(id, r) = hash(id, r)
は、ユーザーが生成したランダム ソルトです (ユーザーはこれを保存する必要があります)。レジストラは、内部データベースでr
特定の で登録の事実をマークします。Com(id)
デフォルトでは、登録は一定期間有効です。その期間が経過すると、ユーザーはレジストラで更新手順を実行する必要があります。レジストラによっては、いくつかのチェックを繰り返す必要がある場合があります。
登録の証明
ユーザーは、次の手順でレジストラから登録証明書(特定の有効期限付き)を受け取ることができます。
- 保有ユーザーは、
id
以前に登録したレジストラに連絡します。 - レジストラは 、ユーザーが作成した
c=Com(id, r)
ものに対するコミットメントを保持しますが、ランダム性は知りません。id
r
date
登録の有効期限のタイムスタンプで示します。- レジストラは、次の手順でユーザーの証明書を生成します。
- ランダム性を生成する
r'
(オプションで、このランダム性はユーザーによって提供される場合もあります) - 計算
reg_payload = hash(c, date, r')
- 「レジストラキー」を使用して
s
ECDSA署名を計算するpayload
- ランダム性を生成する
- レジストラはユーザーに を提供します
s, date, r'
。 それ以外では、ユーザーc
は最初にそれを生成したのでそれを知っています。 - ユーザーは、署名の形式の「証明書」
s
とreg_payload
(その内容は を使用して隠されていますr'
) を使用して、特定の有効期限が設定された登録済みの を保持していることをオンチェーンで証明できますid
。実際には、ペイロードは特定の zk リレーションへの公開入力として使用され、署名はs
プレーンテキストで検証されます。
新しいノートを作成する
ユーザーがレジストラに ZK-ID を登録すると、Shielder でノートを作成 (空のアカウントを初期化) できるようになります。
relation R_new_note
inputs:
- reg_payload: Scalar, // payload from the Registrar
- h_note: Scalar,
- nullifier_create: Scalar,
witnesses:
- note: Note,
- trapdoor, nullifier: Scalar,
- id: Scalar,
- r: Scalar,
- r': Scalar,
- date: Scalar
constraints:
1. reg_payload = hash(hash(id, r), date, r')
2. h_note = hash(note)
3. note = Note { id, trapdoor, nullifier, h_acc }
4. acc = Account::new(date)
5. h_acc = Account::hash(acc)
6. nullifier_create = hash(id, NULL) // NULL is a special field element
上記を踏まえて、new_note
取引について説明する準備が整いました。
transaction new_note
inputs:
- proof_new: ZkProof,
- h_note: Scalar,
- proof_id: ZkProof,
- reg_payload: Scalar,
- s: EcdsaSignature,
- root: Scalar, // should be a root of the tree in SC_Registrar
- nullifier_create: Scalar,
execution:
- assert: the signature s under reg_payload verifies with Registrar's key
- v_new = ZK-Verifier(R_new_note) // initialize verifier for the relation R_new_note
- assert: v_new.verify(proof_new; (reg_payload, h_note, nullifier_create))
- assert: root is current or historical Merkle root in SC_Registrar
- assert: nullifier_create is not in shielder.nullifier_create_set
- v_id = ZK-Verifier(R_verify_identity)
- shielder.notes.add_leaf(h_note)
- shielder.nullifier_create_set.add(nullifier_create)
メモが最初に作成されると、ユーザーはそれを更新し続け (それを使用したり、新しいメモを作成したり)、ユーザーの情報はメモ内に保持されます。上記では、各ユーザーが作成したメモが 1 つだけであることを確認できる新しいストレージ アイテムid
も導入しました。nullifier_create_set
shielder
id
「Anonymity Revokers」ではid
、メモ内の情報を使用して、悪意のある行為者の匿名性を解除するセキュリティ メカニズムを追加する方法について説明します。
ZK-IDの有効期限と更新
zk id では、 Notes and AccountsR_update_note
の関係に導入する必要がある追加のチェックが 1 つあります。つまり、id が期限切れになっていないこと (メモ内) です。メモの有効期限が切れている場合、ユーザーはシールダー内で取引できなくなります。その場合、2 つのオプションがあります。current_timestamp < date
id
レジストラで更新します。これはrefresh_id
、レジストラからの新しい証明書に基づいて有効期限を延長できるトランザクションを送信することによって行われます。- ユーザーがそれを望まない場合、またはレジストラが の更新を拒否した場合
id
、ユーザーはすべての資産を引き出すことができますが、可能なのは引き出しのみです。このような引き出しは、匿名性取り消しメカニズムを使用した強制的な匿名化も引き起こします。これについては、匿名性取り消し者で説明されています。
匿名性取り消し者
匿名リボーカー (略して AR) は、Shielder を悪意のある人物から保護する役割です。主なアイデアは、AR が認識された悪意のある人物の Shielder 内のすべてのアクションを匿名化解除することです。ここで想定している典型的なシナリオは次のとおりです。
- Shielder とのやり取りに使用された不正な資金がチェーン上で検出されました。たとえば、資金はよく知られているハッキングから来ています。
- 特定の Shielder 取引 (たとえば、入金) が不正な資金を使用していると判断されます。
- ガバナンス プロセスにより、このデポジットの背後にあるユーザーの匿名性を解除するかどうかが決定されます。YES と決定された場合は、匿名性を解除する要求が AR に送信されます。
- AR はトランザクションを明らかにし、その結果 (ソリューションが技術的に構築されているため)、不正ユーザーが Shielder をまったく使用していないかのように、不正ユーザーが実行した他のすべてのトランザクションの詳細を誰もが確認できるようになります。
匿名性取り消しキー
ARはAR_sk
非対称暗号化用の秘密鍵を保持し、対応する公開鍵はAR_pk
Shielderの既知のパラメータです。使用される暗号化方式は、皮肉を込めたものである必要があります。実際、各トランザクションには、次の形式のステートメントの証明を含める必要があります。
Enc(AR_pk, m) = c
ここで、m
はプライベート入力であり、 はc
パブリック入力です。
ユーザートランザクション
ZK-ID の場合、から対称暗号化キーを生成する手順id
によって を定義します。マップは一方向である必要があります。最も単純な例は を使用することですが、別のキー導出を使用するように強制する他の制約がある可能性があります。key(id)
key: Scalar -> Scalar
id
key
key(id) = hash(id)
ユーザーがシールダー内でトランザクションを実行するたびにtx
(それ以外の場合は完全に匿名)、次の 2 つの追加の暗号化されたデータが含まれます。
mac = (r, hash(r, key(id))) : (Scalar, Scalar)
— ランダムな nonce によるユーザーの HMAC「署名」。これにより、すべてのトランザクションの中からユーザーのトランザクションを識別できるようになりますkey(id)
。e_key = Enc(AR_pk, key(id))
— これはkey
ユーザーの暗号化されたものですe_op = SymEnc(key(id), op_priv)
— これは、ユーザーがアカウントで実行している操作のプライベートな部分の暗号化でありop_priv: OpPriv
、対称(SNARKフレンドリー)暗号化方式(SNARKフレンドリー対称暗号化を参照)を使用して暗号化されています。key(id)
取引の変更
トランザクションに暗号化を追加するには、 Notes and Accountsで紹介した内容にいくつかの変更を加える必要があることに注意してください。完全性を保つために、必要な修正を含めて変更された部分を繰り返します。ただし、基本的な考え方は単純です。update_note
トランザクションでは、暗号化の正確性をチェックし、MAC を形成する制約を設定する必要があります。
relation R_update_note
inputs:
- op_pub: OpPub // the public part of the operation to be performed
- h_note_new: Scalar,
- merkle_root: Scalar,
- h_nullifier_old: Scalar,
- mac: (Scalar, Scalar),
- e_key: Ciphertext,
- e_op: Scalar^n
witnesses:
- note_new, note_old: Note,
- trapdoor_new, trapdor_old: Scalar
- nullifier_new, nullifier_old: Scalar,
- proof: MerkleProof
- op_priv: OpPriv
- id: Scalar
constraints:
1. h_note_new = hash(note_new)
2. note_new = Note { id, trapdoor_new, nullifier_new, h_acc_new }
3. h_note_old = hash(note_old)
4. note_old = Note { id, trapdoor_old, nullifier_old, h_acc_old }
5. h_nullifier_old = hash(nullifier_old)
6. verify_merkle_proof(merkle_root, h_note_old, proof)
7. op = combine(op_pub, op_priv)
8. R_update_account(op, h_acc_old, h_acc_new)
9. k = key(id)
10. mac = (r, hash(r, k))
11. e_key = Enc(AR_pk, k)
12. e_op = SymEnc(k, op_priv)
transaction update_note
inputs:
- op_pub: OpPub,
- proof: ZkProof,
- h_nullifier_old: Scalar,
- merkle_root: Scalar,
- h_note_new: Scalar,
- mac: (Scalar, Scalar),
- e_key: Ciphertext,
- e_op: Scalar^n
execution:
- shielder.public_exec(op_pub)
- assert: merkle_root is the current or historical root of shielder.notes
- assert: h_nullifier_old not in shielder.nullifier_set
- v = ZK-Verifier(R_update_note) // initialize verifier for the relation R_update_note
- assert: v.verify(proof; (op_pub, id, h_note_new, merkle_root, h_nullifier_old))
- shielder.notes.add_leaf(h_note_new)
- shielder.nullifier_set.add(h_nullifier_old)
匿名性の取り消し
トランザクションで匿名性取り消し手順がトリガーされた場合tx
、AR はe_key
トランザクションのフィールドを公開的に復号化し、ユーザーがアカウントに関連するすべての操作を暗号化するために使用する をtx
明らかにします。これで、すべてのシールド トランザクションを最初からスキャンすることで、各ユーザーは を作成したユーザーからのすべてのトランザクションを復号化できます。これを行う方法は、各トランザクションに添付されている を検査し、 条件をチェックすることで、 トランザクションが匿名化解除されたユーザーからのものであるかどうかを確認することです。このアイデアを使用すると、key
tx
mac
mac = (m_0, m_1)
m_1 == hash(m_0, key)
- このユーザーの最初のメモを作成したトランザクション
op
このユーザーのアカウントに適用された各操作を見つけることができます。これはop_pub
、公開されているものとop_priv
暗号化されているものを組み合わせることで行えます。key
上記を使用すると、このユーザーのアカウントの完全な履歴を復元でき、特に現在の状態を復元し、ユーザーのすべての新しいトランザクションをプレーンテキストで確認できます。
リレー
shielder
は Aleph Zero 上のスマート コントラクトに過ぎないため、Shielder 内の各トランザクションAccountId
は Aleph Zero 上の誰かによって送信されなければなりません。ただし、これをユーザー自身によってのみ行うと、プライバシーが失われます。このため、通常のユーザーに代わって、Shielder トランザクションをチェーンに送信する当事者であるリレーを導入します。
リレーヤーをサポートするために、と というOperation
2つの新しいフィールドを追加して s を強化します。操作の例を考えてみましょう。relayer_address
relayer_fee
WithdrawETH
struct WithdrawETH {
relayer_address: AccountId,
relayer_fee: u128,
withdraw_address: AccountId,
amount_eth: u128,
}
対応するメソッドは次のように実装されます。
fn update(acc: Account, op: WithdrawETH) -> Account {
decrease balance of AZERO in acc by op.relayer_fee
decrease balance of ETH in acc by op.amount_eth
return acc;
}
の対応する実装public_exec
は
fn public_exec(op: WithdrawETH) {
transfer op.relayer_fee AZERO from shielder to op.relayer_address
transfer op.amount_eth ETH from shielder to op.withdraw_address
}
ユーザーがトランザクションを送信する一般的なフローは次のようになります。
- 中継業者に連絡して料金を交渉します。
- フィールドにリレーヤーのアドレス
relayer_address
、(AZERO で)に交渉された料金を入れてトランザクションを作成しますrelayer_fee
。対応するスナーク証明を生成します。プライバシーを最大限に高めるには、履歴のない新しいアカウントにする必要があることに注意してくださいwithdraw_address
。 - すべてのデータをリレーヤーに渡します。
- リレーヤーは、
relayer_address
およびがrelayer_fee
正しいことを検証し、トランザクションが成功するかどうか(証明が正しいかどうかなど)をシミュレートします。 - リレーヤーはトランザクションを送信し、ガス料金を負担しますが、
relayer_fee
ガス料金よりも大きい利益を得るはずです。
決定論的秘密管理
トランザクションを行う際、ユーザーはさまざまな秘密を用意する必要があります。シールダーを使い続けるにはtrapdoor
、nullifier
これらすべて (または少なくとも最新のもの) を保存する必要があります。これほど多くの秘密をバックアップするのは問題があるため、ユーザーが 1 つの秘密シードだけを保持し、残りのすべての秘密をそこから導出できる決定論的な秘密管理スキームを導入します。
このようなスキームの最も単純な試みは次のようになります。
- ユーザーはまずマスターシードを生成する。
seed
例えば256ビットで、 - ユーザーは ZK-ID を生成します。
id = hash(seed, "id")
- 新しい秘密、例えばnullifierが必要とされるときは、次のように導出される。
nullifier = hash(seed, "nullifier-X")
ここで、X
は一意のナンス、例えばトランザクションのインデックスなどです。trapdoor = hash(seed, "trapdoor-X")
- 同様に、暗号化やのためにランダム性を生成する場合も、
mac
同じランダム性を再利用しないように特別な注意を払う必要がありますmac = (r, hash(r, k))
。なぜなら、ランダム性はすぐに明らかになるからです (を思い出してください)。生成用の nonce がr
トランザクション番号のみに依存する場合、ユーザーがトランザクションを再送信するときに同じ nonce を使用することがありr
、プライバシーが侵害され、場合によってはセキュリティが侵害される可能性があります。
上記では、ナンスの選択がセキュリティにとって非常に重要であることに注意してください。
シールド転送
注記とアカウントに記載されているシールドでは、ユーザー間での資産の送信は許可されません。入金と引き出しのみ可能です。
SNARK対応の対称暗号化
問題設定
暗号化スキームを設計したい。このスキームは算術回路(SNARK)で効率的に動作する必要があり、暗号化のキーと入力は両方ともベクトル (∈Fn vectors ) (ここで ( F ) は体)とする。
解決策
キー生成 (Keygen):
キー (x∈F ) をランダムに生成する。
暗号化 (Encrypt):
- 入力: メッセージ (m∈Fn, key x∈Fx∈F)
- ノンス (k∈Fk∈F ) をランダムに生成する。=hash(k,x)∈F
- ( a = \text{hash}(k, x) \in F ) を計算する。
- ( r_i = \text{hash}(a, i) ) を ( i = 1, 2, …, n ) に対して計算し、ベクトル (r∈Fnr∈Fn ) を得る。
- 計算する。e=m+r (note e∈Fne∈Fn)
- 出力:(k,e)(k,e)
復号 (Decrypt):
- 入力: 暗号文 ( (k, e) ), キー ( x \in F )
- 上記と同様に ( k, x ) を用いて ( r \in F^n ) を計算する。
- ( m = e – r ) を計算する。
- 出力: 復号されたメッセージ ( m )。
計算コスト:
暗号化と復号化の総コストはおよそ ( \approx n \cdot G_{\text{hash}} ) (ここで ( G_{\text{hash}} ) はハッシュ1回のゲート数)。
暗号化
このページでは、Shielder での暗号化に関する選択について説明します。これらの選択の一部は 100% 最終的なものではなく、変更される可能性があります。
証明システム
KZG コミットメント スキームを使用してインスタンス化された Ultraplonk 証明システムを使用します。具体的な実装は Halo2 のバリアントになります ( https://github.com/zcash/halo2およびhttps://github.com/privacy-scaling-explorations/halo2を参照)。理由:
- これらはトランザクションの一部として一般ユーザーによって提出されるため、検証者のガス効率が重要な役割を果たすため、適度に小さな証明と高速な検証者が必要です。検証者の作業は多対数ではないため、IPA を多項式コミットメントとするオリジナルの Halo2 ( https://github.com/zcash/halo2 ) は除外されます。同様に、ハッシュベースの証明システムは、検証と証明のサイズに関しては依然として非常に高価であるため、私たちは採用しないことにしました。
- 一般ユーザーはブラウザ(またはモバイル)で証明を生成することになっているため、証明者も効率的である必要があります。
- Ultraplonk の本格的な代替手段は、間違いなく Groth16 です。これは今でも最先端の技術であり、多くのプロジェクトで使用されています。私たちは主に以下の理由から、これを選択しませんでした。
- plonk ベースの証明システムには多くの革新と進歩があると感じていますが、Groth16 は完全に最適化された局所最適解にすぎないようです。
- Groth16 は Plonk よりも柔軟性が低く、特定の機能に必要なカスタム調整を追加するのが難しくなります。
- 回路ごとの信頼できるセットアップでは、システムを更新するたびに新しいセレモニーが必要になるため、Groth16 は実際には少し問題があります。一方、plonk ベースのシステムにはユニバーサル セットアップがあります。
楕円曲線
KZG コミットメントを使用するには、ペアリングをサポートする楕円曲線が必要です。当面は、少なくとも Halo2 に BLS12 ファミリーの曲線のサポートが追加されるまでは、BN256 を使用することに決めました。最終的な選択は、BN* と比較してセキュリティ レベルが向上し、BLS12-377 の再帰が部分的にサポートされているため、BLS12-381 または BLS12-377 のいずれかになります。BLS12-377 は、いつか役に立つかもしれません。楕円曲線の選択は、おそらく唯一の最終的な選択であり、システムの展開後は変更できないことに注意してください。これは、曲線によって、算術 (回路、安っぽいテーブル) が定義されるフィールドが決定されるためです。特定のフィールドを使用して表現するチェーンにコミットされた状態があると、それを別のフィールドに移動することはほぼ不可能になります。そのため、証明システムの選択はいつでも変更できますが、曲線は一度選択すると永久に選択されます。
皮肉なハッシュ
私たちの初期実装では、 Poseidon Hash https://eprint.iacr.org/2019/458またはその最新バージョンの 1 つhttps://eprint.iacr.org/2023/323を使用します。
ユーザーウォレット
このセクションでは、ユーザー ウォレットがどのように機能するか、つまり、どのような状態を保存する必要があるか、状態を取得する方法、新しいトランザクションを作成する方法について説明します。決定論的秘密管理が使用されていると想定します。
現在の状態とトランザクションの作成
ユーザーの現在の状態は次のようになります。
seed
— すべての秘密を導き出すために使われるマスターシードid
— ユーザーのZK-ID- 現在のメモ:
trapdoor
nullifier
account
— 現在のアカウントの状態
上記のすべてを考慮すると、Notes and Accounts (またはAnonymity Revokersの調整バージョン)update_note
で説明されているように、トランザクションで状態を更新することが可能であり、実際に、読者は、新しいトランザクションを作成するために必要なすべての証明が、上記のデータを使用して生成できることを確認することが推奨されます。
マスターシードからの状態の復元
単一のマスターを使用する前提はseed
、ユーザーがseed
チェーン上のデータからのみ、上記の状態全体を回復できることです。もちろん、回復プロセスは長くなる可能性がありますが、可能であるはずです。一方、日常的な操作では、ユーザーは効率性のために現在の状態を維持し、保存する必要があります。
ユーザーの現在の状態を回復するには、いくつかの手順を実行します。
id
ユーザーのZK-IDを決定論的に生成するseed
- キーを計算する
key=key(id)
tx_1, tx_2, ... tx_n
チェーン上で確定されたすべてのトランザクションの時系列リストを取得します。- この目的のために、すべてのユーザーからシールダーに送信されたすべてのトランザクションのリストをフィルタリングします。フィルタリング ルールは次のとおりです。
tx
トランザクションとするmac = (m_0, m_1)
マックのtx
- その場合は、ユーザーの所有物として
m_1 = hash(m_0, key)
保持します。tx
- (この手順は、書かれているとおりには非常に非効率的であることに注意してください。以下では、効率を改善する方法について説明します)。
- この目的のために、すべてのユーザーからシールダーに送信されたすべてのトランザクションのリストをフィルタリングします。フィルタリング ルールは次のとおりです。
tx_1
はトランザクションなnew_note
ので無視できます。残りのものについては、現在のを回復するために以下の手順を実行しますaccount
。- セット
account := Account::new()
- の場合、次
tx
の[tx_2, tx_3, ..., tx_n]
操作を実行します。- 抽出
op_pub
するtx
- 鍵を使って復号化する
op_priv
ことから抜け出すe_op
key
- 組み合わせる
op = combine(op_pub, op_priv)
- アカウントを更新する
account = Account::update(account, op)
- 抽出
- セット
- 最後の音符の
trapdoor
andを決定論的に導出するnullifier
seed
収集フェーズの改善
アカウントに関連するすべてのトランザクションを収集するには、すべてのシールダー トランザクションをフィルターする必要がありますが、これは非常に時間がかかります。これを改善する方法については、いくつかのアイデアがあります。
n
ユーザーの 番目のシールドトランザクションに、番目のトランザクションが到着したブロック番号である暗号化された(SymEnc
キーを使用してkey(id)
)番号を添付します。この方法では、ユーザーが行う必要があるのは、最後のトランザクションを見つけて、ブロック全体をスキャンすることだけです。k
n-1
n
- あいまいなメッセージ検出技術を使用します。
概念実証
最初のバージョンでは、これらすべてがどのように見えるのでしょうか?
テストネットにのみ展開される初期 PoC バージョンの説明。
IDと秘密
Shielder に資金を入金するには、オンチェーン トランザクションを送信する必要があります。したがって、ユーザーは対応する秘密鍵 (基本的に 32 バイトのランダムなバイト) を持つネイティブ アカウントをすでに持っているものと想定します。簡単にするために、このキーを Shielder システム内のユーザー ID として使用します。対応するオンチェーン アカウントに関するチェックは実行されないことに注意してください。基本的に、任意の 32 バイトを使用できます。
その他のすべての(操作上の)秘密、つまりヌルファイアとトラップドアは、 の Keccak256 ハッシュとして生成されますid || nonce || label
。
id
上で述べたように、秘密鍵であるnonce
シールド操作がすでに何回実行されたかを示すカウンタですid
label
バイト文字列、b"nullifier"
またはb"trapdoor"
アカウント
PoC バージョンでは、簡略化のため、ネイティブ AZERO トークンのシールドのみを許可しています。したがって、ノートに保存される唯一の情報 (運用上の秘密を除く) は、現在のシールドされた AZERO 残高です。
回復とアカウント追跡
シールドされた状態を失った場合でも(たとえば、デバイスを紛失したり、誤って状態ファイルを削除したりした場合)、ID があれば資金を回復できます。
すべての Shielder トランザクションには共通の特性があります。つまり、すべてのアクションは何らかの nullifier を無効化します。入金と出金の場合、現在のノートの nullifier のハッシュを公開するだけです。新規アカウント アクションの場合、特別な事前 nullifierと見なすことができる ID のハッシュを公開します。Shielder コントラクトは、二重支払いや同じ ID で複数のアカウントを作成することを防ぐために、使用されたすべての nullifier (のハッシュ) のレジストリを維持します。これは、使用された nullifier ハッシュから、公開されたときのブロック番号へのマッピングとして実装されます。
この設計のおかげで、次のように簡単に回復手順を導き出すことができます。ノンス 0 から始めて、対応するヌルファイアがすでに使用されているかどうかをコントラクトに繰り返し問い合わせ、使用されている場合は適切なブロックを取得し、トランザクションを見つけてローカル状態を更新できます。
これはアカウントの追跡にも役立ちます。ユーザーはさまざまなデバイスから Shielder とやり取りできるため、ローカル状態が常に最新であることを確認する必要があります。したがって、アプリがオンになっているときはいつでも、現在の紙幣のヌルファイアがすでに使用されているかどうかを確認します。使用されている場合は、最新のトランザクションを取得して状態を更新するだけです。
予選 / 総括
Scalar
いくつかの固定された有限体要素の型です。
hash
関数は、一連のScalar
要素を単一のScalar
値に変換するハッシュ関数です。
AMOUNT_BOUND
Shielder が扱う量には制限があり、算術演算がフィールド内でオーバーフローしないようにします。例: 2^128
MERKLE_HEIGHT
契約書に記されたマークルツリーの高さです。
MERKLE_ARITY
契約に保持される Merkle ツリーのアリティです。
新規アカウント
relation PoC::NewAccount
inputs:
- h_note: Scalar
- h_id: Scalar
- initial_deposit: Scalar
witnesses:
- id: Scalar
- nullifier: Scalar
- trapdoor: Scalar
assumptions:
- initial_deposit <= AMOUNT_BOUND
constraints:
// id is correctly exposed as a public input
- h_id = hash(id)
// note is well-formed
- h_note = hash(id, nullifier, trapdoor, hash(initial_deposit))
suggestions:
- nullifier = hash(id, 0, 0)
- trapdoor = hash(id, 0, 1)
デポジット
relation PoC::Deposit
inputs:
- merkle_root: Scalar
- h_nullifier_old: Scalar
- h_note_new: Scalar
- value: Scalar
witnesses:
- id: Scalar
- nullifier_old: Scalar
- trapdoor_old: Scalar
- account_balance_old: Scalar
- merkle_path: [[Scalar; MERKLE_ARITY]; MERKLE_HEIGHT]
- nullifier_new: Scalar
- trapdoor_new: Scalar
assumptions:
- value <= AMOUNT_BOUND
- account_balance_old <= AMOUNT_BOUND
constraints:
// new value satisfies the bound
- account_balance_old + value <= AMOUNT_BOUND
// old nullifier is correctly exposed as a public input
- h_nullifier_old = hash(nullifier_old)
// new note is well-formed
- h_note_new = hash(id, nullifier_new, trapdoor_new, hash(account_balance_old + value))
// membership proof is valid
- merkle_path is a valid path to merkle_root
// membership proof relates to the old note
- hash(id, nullifier_old, trapdoor_old, account_balance_old) is belongs to the first layer of merkle_path
suggestions:
- nullifier_old = hash(id, n, 0)
- nullifier_new = hash(id, n+1, 0)
- trapdoor_old = hash(id, n, 1)
- trapdoor_new = hash(id, n+1, 1)
撤回する
relation PoC::Withdraw
inputs:
- merkle_root: Scalar
- h_nullifier_old: Scalar
- h_note_new: Scalar
- value: Scalar
- relayer: Scalar
- fee: Scalar
- withdrawal_address: Scalar
witnesses:
- id: Scalar
- nullifier_old: Scalar
- trapdoor_old: Scalar
- account_balance_old: Scalar
- merkle_path: [[Scalar; MERKLE_ARITY]; MERKLE_HEIGHT]
- nullifier_new: Scalar
- trapdoor_new: Scalar
assumptions:
- value <= AMOUNT_BOUND
- fee <= AMOUNT_BOUND
- account_balance_old <= AMOUNT_BOUND
constraints:
// new value satisfies the bound
- account_balance_old >= value + fee
// old nullifier is correctly exposed as a public input
- h_nullifier_old = hash(nullifier_old)
// new note is well-formed
- h_note_new = hash(id, nullifier_new, trapdoor_new, hash(account_balance_old - value - fee))
// membership proof is valid
- merkle_path is a valid path to merkle_root
// membership proof relates to the old note
- hash(id, nullifier_old, trapdoor_old, account_balance_old) is belongs to the first layer of merkle_path
suggestions:
- nullifier_old = hash(id, n, 0)
- nullifier_new = hash(id, n+1, 0)
- trapdoor_old = hash(id, n, 1)
- trapdoor_new = hash(id, n+1, 1)
この記事は以下サイトの機械翻訳です。
Shielderは現在テストネット稼働中で、今後はメインネットに統合される予定です。
こちらからデモ体験してください。
https://zk-demo.common.fi/