
Solana Builders:ZK Compression
HeliusとLight ProtocolがSolana上のZero-Knowledge(ZK)Compressionプロジェクトを発表して以来、ZK Compressionについて多くの議論が交わされてきました。その多くは名称をめぐるものです。これはZKロールアップなのでしょうか?L2なのでしょうか?それとも、まったく別のものなのでしょうか?
なぜこれが重要なのでしょうか?Solanaエコシステムには、名称をめぐる議論は不要だと考える人もいます。何と呼ぶかよりも、何ができるかのほうが重要だという点には私も一部同意します。しかし、名称は特定の性質を持つ構成を指し、それらを分類するものなので、やはり重要です。つまり、XYZと呼ぶことで、その性質や信頼に関する前提が分かります。そして、それらは重視すべきことです。
特性
ZK-Compressionの文脈で評価する前に、注目したいSolanaの特性を挙げてみましょう。
- 同期的なアトミックコンポーザビリティ
- 並行性
- 安全性
- ライブネス
- 検閲耐性
初期の信頼に関する前提
ここでは、フルノードのセキュリティ上の前提を表すために「トラストレス」という用語を使います。この定義を基準とします。フルノードが単独で実行できないことには、追加の信頼に関する前提が伴います。
プロトコルとトラストレスにやり取りできる唯一の方法は、フルノードです。ブリッジ、委員会、マルチシグ、ZK、不正証明など、追加の信頼に関する前提を導入しながらトラストレスだと主張する人がいれば、それは誤りです。
背景
ZK-Compressionを理解するために必要なSolanaの背景を、箇条書きで簡潔に説明します。
- Solanaの状態は、フルノードのディスク上にある「AccountsDB」に保存されます
- ストレージの単位は「アカウント」と呼ばれます
- アカウントにはアドレスがあります(各32バイト)
- アカウントが保存できるデータ量は、0~最大10 MBです
- Solanaで10 MBを保存するには、アカウント作成者が約70 SOLを支払う必要があります。このコストはアカウント数ではなくストレージ容量に連動します。10MBのアカウント1個でも、10KBのアカウント1000個でも同じです
- 現在、Solana上の全アカウントのサイズは76GBです(圧縮後)
- すべてのSolanaトランザクションは、読み書きする全アカウントを指定する必要があります
- 現在、Solanaトランザクションの上限は1232バイトです(これを引き上げる提案があります)
- 各Solanaトランザクションでは、以下の情報を指定する必要があります
- 署名(各64バイト)
- アカウント(各32バイト)
- 命令データ(任意の長さ)
- 直近のブロックハッシュ(32バイト)
- プログラムアドレス(各32バイト)(CPI:クロスプログラム呼び出し)
- Solanaトランザクションには32バイトの「直近のブロックハッシュ」が含まれます。このトランザクションは直近150ブロック以内に取り込まれなければ無効とみなされ、再署名して再送信する必要があります
通常のトランザクションのライフサイクル
通常、トランザクションは次のライフサイクルで実行されます。
- まず、トランザクションに対して経過時間の確認(直近のトランザクションのみ有効)、重複排除、構造検証、手数料(ガス)、署名確認が行われます
- プログラムアドレスに基づいてプログラムのバイトコードが読み込まれ、Solana Virtual Machine(SVM)がインスタンス化されます
- トランザクションが参照するすべてのアカウントが検査され、ストレージからメモリに読み込まれてSVMに渡されます
- プログラムのバイトコードが実行されます
- 変更されたアカウントが、更新後の状態でストレージに同期されます
ZK-Compressionの主な目的は次のとおりです。
- オンチェーン状態は高コストです。たとえば、1000個のアカウントには70 SOLかかるため、Drip Hausのようなプロダクトはすぐに高コストになります
- 状態全体をマークル化しなくても、ディスクに保存するアカウントが増えれば、スナップショットやインデックスなども大きくなります
- すべてのアカウントが頻繁にアクセスされるわけではないため、継続的なリソースコストを負担する必要はありません
では、圧縮を実現する最も簡単な方法は何でしょうか?
アカウントをディスクに保存し、必要なときに読み込む代わりに(トランザクション実行ライフサイクルのステップ3)、トランザクションのペイロードの一部としてアカウントデータを渡せば、オンチェーンストレージに伴うコストを削減できます。しかし、これにより新たな問題が生じます。ユーザーが状態について虚偽の情報を提供していないことを、どうすれば保証できるのでしょうか?
たとえば、トークン残高を保存するアカウントのオフチェーン値が1200で、そのownerフィールドが「BYixJwV32DjeuyRww72PwZMyKcaedN533GBrv7CDh4n9」だとします。
そのデータを含むトランザクションをチェーンに送信した場合、チェーンはアドレス「BYixJwV32DjeuyRww72PwZMyKcaedN533GBrv7CDh4n9」が保有するトークン数について、あなたが虚偽の情報を提供していないとどう判断するのでしょうか?
結局のところ、トランザクションを処理するフルノードはオフチェーンデータにアクセスできず、トランザクションとともにデータが提供されることを前提としています。
これにはマークル証明を利用できます。マークル証明の詳細には踏み込みませんが、これはオンチェーンストレージの使用量を抑えながら、検証可能な形でデータに「コミット」する方法です。チェーンと同期しているすべてのフルノードがその小さな「コミットメント」を保存します。誰かがトランザクション内でデータを提供する際には、コミットメントに対して検証できる「証明」も同じトランザクション内で提供できます。この証明は暗号学的に安全です。
これには何か問題があるのでしょうか?
問題は、マークル証明が大きくなり得ることです。ツリーに100,000個のアカウントが含まれる場合、そのうち1つのアカウントの証明サイズは17 * 32 = 544バイトです。複数のアカウントに対する証明を提供する場合、最悪のケースでは証明サイズがその数だけ乗算されます。したがって、10個のアカウントでは、最悪の場合10 * 544 = 5440バイトになります。この容量の問題はSolana特有です。Solanaトランザクションは現在1232バイトに制限されていますが、他のチェーンでは通常、制限がそれほど厳しくありません。プログラム、署名、直近のブロックハッシュなど、各コンポーネントのサイズを示した前述のセクションを参照してください。最良のケースでも、マークル証明だけでトランザクションサイズの半分を使用します。
ここで、いくつかの疑問が生じます。
トランザクションサイズが1232バイトなら、完全なデータをトランザクションの一部としてどう送信するのでしょうか?
非常に良い質問です。ZK Compressionは、少量のデータで構成される非常に多数のアカウントに有効です。トークン残高(トークンごとに8バイト)やNFT用の少量のメタデータなど、100バイトのデータならトランザクションに容易に収まります。一方、ほかの情報も含める必要があるため、1000バイトは困難です。アカウントに大量のデータを保存する必要がある場合、この方法(およびZK Compression)は機能しません。
この説明には1つ補足があります。ZK Compressionで使われるのと同じロジックを、アカウント状態の一部に適用できます。つまり、アカウントの完全なデータを1つのトランザクションペイロードに収められなくても、回避策はあります。具体的には、部分的なデータにコミットし、その証明を提供できます。
これを実現する方法はマークル証明だけでしょうか?
いいえ。マークル証明は、ベクトルコミットメントの一種にすぎません。コミットメントサイズは32バイト、証明サイズはLog2(N) * 32バイトです。ここでNは、コミットするベクトルのサイズです。Log2(100000)が17なので、先ほど17という値を使いました。しかし、証明サイズが一定のコミットメントもあります(KZG、Pedersen)。実際、ZK-Compressionはそのような方式を使用しています。
通常は状態の一部としてフルノードに保存されるアカウントデータを、トランザクションとともに提供する必要があるとのことですが、そのデータはどこに保存されるのでしょうか?
非常に良い質問です。これは信頼に関する前提と特性に影響します。答えは、どこでも構いません。専用のRPCサーバーに保存することも、FilecoinやIPFSの一部として保存することも、ユーザー自身のマシンに保存することさえ可能です。重要なのは、ベクトルのすべての要素がどこかに保存されている限り、証明をオンデマンドで計算できるという点です。保存場所がもたらす影響については、特性のセクションで説明します。
他のチェーンでも実現できますか?
ZK-Compressionには、zk-SNARK検証が低コストであることが特に求められます。Solanaではストレージよりもコンピュートのほうが安いため、相性が良いのです。各トランザクションに提供されるデータにベクトルコミットメントと証明を使うという一般的な概念は、他のチェーンでも実現できます。しかし、コンピュートが高コストであるため、Solanaほどトレードオフの効果は大きくありません。実際、継続的に発生する検証のガスコストは、1回限りのSSTOREと比べると、EVMベースのチェーンではかえって高コストになります。
ZK Compressionとは?
- ZK Compressionを知らない場合は、このセクションで説明します
- 大まかには理解している場合でも、よくある誤解がいくつかあるため、このセクションを読むことをおすすめします
- 正確に理解している場合も、私の説明に誤りがあれば指摘していただきたいので、ぜひお読みください :)
前のセクションを理解できたなら、ZK Compressionの90%を理解したことになります。主な問題はマークル証明のサイズでした。つまり、ZK Compressionとは、計算を証明する仕組みを利用することにほかなりません。
ZKが何かを知らなくても、大きな問題ではありません。知っておく必要があるのは、計算を「正しく」実行したことを証明する方法だということだけです。簡単な例として、2つの数を掛けて3つ目の数を得たことを証明したいとします。つまり、4*3 = 12です。
これを「ZK方式」で証明するには、次の関数を用意します。
f(x,y) = x*y
上記のコードから回路を生成すると、証明者は計算が正しいことを示す証明を生成します。回路自体がコミットメントなので、実行する「計算」の内容は誰にでも分かります。しかし素晴らしいのは、入力を知る必要がない点です。f(3,4)を実行すると、12と「証明」が返されます。これで誰でも12と「証明」を受け取り、何らかの2つの数を掛けて12を得たことを検証できます。実行したのが4,3なのか、6,2なのか、あるいは12,1なのかは分かりません。それらの数を隠したままでも検証できることが、「ゼロ知識」と呼ばれる理由です。
なぜこの説明をしているのでしょうか?
この一般化された概念は、特定の計算を実行して特定の結果を得たことを証明するうえで、極めて強力です。結果と証明があれば、誰でもその計算を実際に実行することなく、正しく行われたかを検証できます。これは、任意のあらゆる計算に当てはまります。ここでは2つの数を掛けただけですが、「この10個の署名を検証し、すべて有効だった」と示すこともできます。これが2つ目の利点であり、何かを「隠す」必要がない場合でもゼロ知識証明が使われる大きな理由の1つです。1000ステップ、あるいは100万ステップの計算を実行する必要がある問題を、1つの証明を検証するだけで計算が正しく行われたと確認できる問題に変換できるからです。ただし、証明の生成にはある程度の時間がかかります。
ZK Compressionは同じ技術を使って、実際のマークルツリーのメンバーシップロジックを実行します。つまり、アカウントデータと証明(128バイト)を受け取り、そのデータが確かにチェーン上の「コミットメント」の一部であることを検証できる回路を備えています。(実際の証明は256バイトですが、楕円曲線と点の便利な性質として、曲線が分かっていれば、片方の点だけで2つ目の点を取得できます)。
これは主に、証明サイズを一定の128バイトまで縮小するために行われます。これにより、小さなアカウントのデータを格納する余地が(相対的に)多く残ります。通常のマークル証明はLog2(N)ですが、ZK Compressionは常に一定なので、1つのコミットメントの下に非常に多数のアカウントを持たせられます。(参考までに、100,000個のアカウントに対するマークル証明は約550バイトになり、トランザクションペイロードの半分を占めます)
この証明はオフチェーンで生成できますが、証明の検証はオンチェーンで行う必要があります。プログラムは実行を続行する前に、アカウントについて正しいデータが提供されたことを確認する必要があるためです。そのため、ZK証明を検証する基本的な仕組みが必要です。ZK-Compressionが使用する具体的な証明者システムはGroth16と呼ばれます。これはさらにalt_bn128 syscallに依存しており、現在mainnetでは機能ゲートの対象で、テスト中です。
興味深いのは、ZK-Compressionが使用する仕組みを、任意の計算の検証に利用できることです(単に「このリーフは、このルートを持つツリーに属しているか?」を検証するだけではありません)。
ZK Compressionの主な利点の1つは、開発者が「ZK」の部分を一切扱わなくて済むよう、必要な基盤をすべて提供することです。開発者の視点では、同じフィールドなどを持つ他のアカウントと同じように扱えるため、プログラム内では通常のアカウントとして処理できます。「ZKの魔法」の大部分を抽象化し、開発者が直接扱わなくて済むことには価値があります。
ZKロールアップ
詳しくは触れませんが、ZKロールアップは主にZK-Compressionと同じ概念を使用します。最大の共通点は、ロールアップの状態全体がベースレイヤー(Ethereum)上で単一のルートとして表現されることです。そのため、ZK-Compressionはロールアップだという主張もあります。しかし、重要な違いがあります。
100件のロールアップトランザクションを考えてみましょう。
ZKロールアップ全体が1つの回路として扱われます(例として使った乗算プログラムと同様です)。100件のトランザクションすべてが検証され(署名、コントラクトロジック、重複排除チェックなど)、次の内容に対して単一の証明が生成されます。
「100件のトランザクションを適用すると、状態ルートがAからBに変わる」。証明が検証されると、スマートコントラクトは状態ルートをAからBに更新します。
一方、ZK-Compressionでは、100件の各トランザクションに、単にアカウントデータが正しいことを示す証明が1つずつ含まれます。ただし、(トランザクションによって生成される)状態遷移自体は、SVMの一部としてオンチェーンで実際に実行されます。証明が検証されると、通常のアカウントとして扱われます。これは、次に説明するコンポーザビリティの特性において極めて重要です。
特性を再検討する
ここからが興味深い部分です。ZK-Compressionは、Solanaのどのような特性を維持するのでしょうか?
同期的なアトミックコンポーザビリティ
2つのZK圧縮アカウントと10個の「通常」アカウントを参照するトランザクションがあっても、コンポーザビリティは損なわれません。ZK圧縮アカウントを参照する命令から、「通常」の非圧縮アカウントを参照する別の命令やプログラムを呼び出せます。この機能は、2つのアカウントが異なるツリーで圧縮されている場合でも完全に維持されます。1つの命令が失敗するとトランザクション全体がロールバックされ(アトミック)、1行目で呼び出された命令による変更は2行目から確認できます(同期的)。
ロールアップではこれは成り立ちません。ZKロールアップ同士は同期的にもアトミックにも呼び出せないためです(グローバルロックを取得し、ロールアップ間のロールバックを許可する場合を除きます)。
並列性
この機能は並列性に一定の影響を与えるため、ケースごとに検討する必要があります。
同じツリー配下にある複数の圧縮アカウントへの書き込み
各ツリーは、それぞれ独立して並行処理できます。つまり、ユーザーが同じ状態ルート配下の2つの圧縮アカウントを読み書きする場合、処理を並行して実行し、状態ルートも並行して更新できます。ここでのロジックは、SolanaがcNFTの並行マークルツリー更新に使用しているものと同じです。
同じ圧縮アカウントへの書き込み
個々の圧縮アカウントは並行処理できません。2人のユーザーが同じ圧縮アカウントに書き込もうとすると、順序に関係なく一方のトランザクションが失敗します。通常の実行では、前の命令によるアカウントへの書き込み結果を次の命令で利用できます。しかしZK圧縮アカウントの場合、アカウントデータの証明は以前の状態に対するものなので無効になります。
もう1つ注意すべき点として、圧縮ではコンピュートユニット(CU)の使用量が多いため、ツリーごとの最大並行数が低下します。アカウントのCU上限により、各アカウントが1ブロックあたり使用できるコンピュートユニットは12Mまでだからです。
同期的なアトミックコンポーザビリティは多くのロールアップで問題になります。一方、並列性の特性について言及したのは、ZK Compressionを有効にするためにシーケンサーなどの追加コンポーネントが不要であることを強調するためです。シーケンサーがないため、順序付けはベースチェーンが行います。based rollupでも同じ特性が成立すると論じることはできますが、ほとんどのロールアップは順序付けに中央集権型シーケンサーを使用するため、同じことは当てはまりません。
信頼に関する前提
証明の生成とトランザクションの送信に必要なすべての生データは誰でも保存できますが、これは圧縮された状態のライブネスに影響する追加の信頼に関する前提です。何らかの理由でデータが「失われた」場合や遅延が生じた場合、自分でデータを保存していない限り、トランザクションを送信できません。幸い、これはビザンチンフォールトトレランスを必要とする3f+1問題ではなく、f+1問題として知られるものです。f+1問題では、1つの誠実なノードがデータを提供すれば十分です。また、証明は「自己検証可能」なので、「安全性」の問題はありません。主に「ライブネス」の問題と検閲経路が生じます。
通常のロールアップとZK-Compressionは、どちらも有効性証明を提供する必要があります。しかし、ロールアップは状態遷移関数全体を有効性証明にエンコードするのに対し、ZK-Compressionがエンコードするのは「アカウントデータは正しいか?」という点だけです。そのため、ここでは信頼に関する前提が若干異なります。圧縮の場合、信頼に関する前提は主に状態へのアクセスに関するものです(状態遷移は完全に実行されます)。ロールアップの場合、信頼に関する前提は、ベースレイヤーから見た状態遷移関数全体に関するものです。ビット数や基礎となる問題の困難性/計算困難性に関するセキュリティ上の前提は同じです(双線形Diffie-Hellman仮定)。しかし、そのセキュリティモデルを*何のために*信頼するかが異なります。つまり、状態へのアクセスか実行かという違いです。これに言及するのは、追加の信頼に関する前提がどこに加わり、どこには加わらないのかを把握することが重要だからです。
現在、ZK圧縮アカウントを検証するプログラムはアップグレード可能です。しかし、このプログラムは非常に限定的な操作(マークル証明を開くこと)しか行わず、継続的なアップグレードを実質的に必要としないため、将来的にはイミュータブル化または凍結できます。
さらに、状態圧縮は別の2つの方法でも実現できます。
- トランザクションサイズを拡大する提案(Solana Discordのproj-3x-txチャンネル)が開発中です。稼働後、サイズが許容範囲なら通常のマークル証明を使用できます
- alt_bn128 syscallが稼働すれば、証明サイズが一定の通常のベクトルコミットメントにも使用できます(KZGはalt_bn128を含む、ペアリングに適した任意の曲線で機能します)。これにはZK証明者回路は必要ありません
これは何と呼ぶべきでしょうか?
残念ながら、ロールアップ、L2、Validiumなどの用語は非常に曖昧に使われており、ロールアップと呼ばれるものの中には、実際にはロールアップですらないものもあります。それらはベースレイヤーのライブネス、安全性、検閲耐性を継承していません。Heliusは「マーケティング用語」を使っていると非難されてきましたが、同じ人々も、投資先のプロジェクトを指す際に同じ理由、つまりマーケティングのために「ロールアップ」という言葉を非常に曖昧に使ってきました。実際にはロールアップでないものにも異なるステージを付け、ロールアップを名乗り続けられるようにしています。
もちろん、すべての人がそうしているわけではありません。正確な用語を使うことに徹底して誠実であり、不正確な用語でユーザーを惑わせる人々との議論に何人月もの時間を費やしてきた人もいます(*全員*に一貫して正確な用語の使用を求めてきたToghrulに敬意を表します)。
ロールアップとは異なる特性や信頼に関する前提があることを考えると、これをロールアップと呼ぶことはユーザーの混乱を招く可能性があります。Validiumと呼ぶのも広すぎます。同期的なアトミックコンポーザビリティや並列性が損なわれておらず、データ可用性(DA)がオンチェーンにあり、さらに状態遷移関数自体がトラストレスであるという事実を無視するからです(フルノードは実行自体の有効性証明を単に検証するのではなく、実際のプログラムを完全に実行しています)。ZK証明はトラストレスだと主張する人もいるかもしれませんが、それは正しくありません。信頼は大幅に最小化されますが、数学的にはセキュリティ上の前提が同じではないからです(ユースケースの99%では十分かもしれませんが、フルノードよりも追加の信頼に関する前提が課されます。たとえば、ペアリング曲線ベースのzk-SNARKでは双線形Diffie-Hellman仮定があります)。もちろん、ZK-Compressionはsnarkを使ってアカウント自体の有効性を確認するため、ZK-Compressionはトラストレスではないと言うのが妥当です。「通常」のアカウントには存在しない信頼に関する前提が、ZK圧縮アカウントにはあります。したがって、トラストレスと本格的なZKロールアップの中間に位置します。
名称から特性を推測するのであれば、これを「ロールアップ」と呼んでも、それらの特性や信頼に関する前提の存在は伝わらないと私は考えます。新しい名前を考えるべきでしょうか?信頼に関する前提が明確である限り、ZK-Compressionでまったく問題ないでしょう。
関連記事
Heliusを購読
Solana開発の最新情報や新しい記事の公開通知を受け取れます


