理論から実践へ:イーサリアム Rollup による検閲耐性取引のメカニズムの解析
原文タイトル:《Rollup の Force Inclusion 機構紹介》
著者:NIC Lin、台北 Ethereum Meetup の責任者
昨日、数多くの人々を驚かせる出来事がありました:Metamask の親会社である Consensys が提供する Ethereum の Layer 2 Linea が自主的に停止され、公式には Velocore のハッキング事件の影響を軽減するためだとされています。これは、以前 BSC チェーン(BNB Chain)がハッキングの損失を軽減するために公式に調整して停止したことを思い起こさせます。このようなことが話題になるたびに、人々は Web3 が提唱する非中央集権の価値に疑念を抱くことになります。
もちろん、上記の事件が発生した核心的な理由は、インフラ自体の不完全さ、つまり非中央集権が不十分であることにあります:もしチェーンが十分に非中央集権であれば、停止することはできないはずです。Ethereum の Layer 2 の独特な構造により、ほとんどの Layer 2 は中央集権的な Sequencer に依存しています。近年、非中央集権的なシーケンサーの議論が増えてきましたが、Layer 2 の存在目的とその構造を考慮すると、Layer 2 のシーケンサーは多くの場合、あまり非中央集権的ではなく、最終的には BSC チェーンの非中央集権の程度にも及ばない可能性があります。もし事実が本当にそうであれば、私たちはどうすればよいのでしょうか?
実際、Layer 2 にとって、シーケンサーの非中央集権性がもたらす最も直接的な危害は、検閲耐性と活性に関わります。取引を処理する実体(シーケンサー)が少ない場合、彼らはあなたにサービスを提供するかどうかに絶対的な権力を持つことになります:拒否したいと思えば拒否でき、あなたはどうすることもできません。Layer 2 の検閲問題をどのように解決するかは、明らかに重要なテーマです。
過去数年間、各大手 Ethereum Layer 2 は検閲問題に対してさまざまな解決策を提案してきました。例えば、Loopring や Degate、StarkEx の強制引き出しおよびエスケープポッド機能、Arbitrum や他の OP Rollup の Force Inclusion 機能など、これらの方法は一定の条件下でシーケンサーに対して抑制をかけ、恣意的に任意のユーザーの取引リクエストを拒否することを防ぐことができます。
今日の記事では、台北 Ethereum 協会の NIC Lin が登場し、4つの主流 Rollup の検閲耐性取引機能を実際に実験し、ワークフローや操作方法などの観点から Force Inclusion のメカニズム設計を深く分析しました。これは Ethereum コミュニティや巨額の資産を持つ大口投資家にとって特に参考価値があります。
取引の検閲と Force Inclusion
取引の検閲耐性(Censorship Resistance)は、ブロックチェーンにとって非常に重要です。もしブロックチェーンが任意に検閲し、ユーザーが発起した取引を拒否できるのであれば、それは Web2 サーバーと何ら変わりありません。Ethereum の現在の取引検閲能力は、多数のバリデーターによって支えられています。もし誰かが Bob の取引を検閲し、彼の取引をブロックチェーンに載せたくない場合、ネットワーク内の大多数のバリデーターを買収するか、全ネットワークにスパム取引を送り続けて手数料を Bob より高くしてブロックスペースを奪う必要があります。どちらの方法でも、コストは非常に高くなります。
注:Ethereum の現在の PBS 構造では、取引を検閲するコストがかなり低下しています。OFAC による Tornado Cash 取引の検閲に関連するブロックの割合を参照できます。現在の検閲能力は、OFAC および政府の管轄範囲外の独立したバリデーターおよびリレーに依存しています。
しかし Rollup はどうでしょうか?Rollup は安全性を確保するために大量のバリデーターを必要としません。たとえ Rollup に中央集権的な役割(シーケンサー)が1つだけあっても、L1 と同じように安全です。しかし、安全性と検閲耐性は別の問題です。たとえ Rollup が Ethereum と同じくらい安全であっても、中央集権的なシーケンサーが1つしかない場合、任意のユーザーの取引を検閲することは可能です。
シーケンサーはユーザーの取引を拒否することができ、ユーザーの資金が拘束されて Rollup から出られなくなる
Force Inclusion 機構
Rollup に大量の非中央集権的なシーケンサーを要求するよりも、L1 の検閲耐性を直接利用した方が良いです:
本来シーケンサーは取引データをパッケージ化して L1 の Rollup コントラクトに送信する役割を果たしますが、コントラクトに設計を追加してユーザーが取引を Rollup コントラクトに自分で挿入できるようにすることができます。このメカニズムを「Force Inclusion」と呼びます。シーケンサーが L1 レベルでユーザーを検閲できない限り、ユーザーは L1 で取引を強制的に挿入することを妨げられません。こうすることで、Rollup は L1 の検閲耐性を引き継ぐことができます。
シーケンサーはユーザーの L1 取引を検閲できません、非常に高いコストを支払わない限り
強制取引はどのように有効にすべきか?
もし Force Inclusion を通じて取引を直接 Rollup コントラクトに書き込むことを許可すれば(つまり即時に有効にする)、Rollup の状態はすぐに変わります。例えば、Bob が Force Inclusion 機構を通じて「Carol に 1000 DAI を送る」という取引を挿入した場合、取引が即時に有効になると、最新の状態では Bob の残高が 1000 DAI 減り、Carol の残高が 1000 DAI 増えます。
もし Force Inclusion が取引を直接 Rollup コントラクトに書き込んで即時に有効にできるなら、状態はすぐに変わります
この時、シーケンサーがオフチェーンで取引を収集し、次のバッチの取引を Rollup コントラクトに送信している場合、Bob によって強制的に挿入され即時に有効になる取引に影響を与える可能性があります。このような問題は極力避けるべきであるため、Rollup は一般的に Force Inclusion 取引を即時に有効にすることはせず、まずユーザーが取引を L1 の待機キューに挿入し、「準備中」状態にします。
シーケンサーがオフチェーン取引をパッケージ化して Rollup コントラクトに送信する際、前述の取引を取引シーケンスに挿入するかどうかを選択します。もしシーケンサーが「準備中」状態の取引を無視し続け、ウィンドウ期間が終了した後、ユーザーはこれらの取引を強制的に Rollup コントラクトに挿入することができます。
シーケンサーは「待機キュー」の取引をいつ「ついでに収入」するかを決定できます
シーケンサーは待機キューの取引を処理することを拒否することもできます
もしシーケンサーが長期間拒否し続けると、一定の時間後に誰でも Force Inclusion 機能を使って取引を強制的に Rollup コントラクトに挿入できます
次に、Optimism、Arbitrum、StarkNet、zkSync の4つの有名な Rollup の Force Inclusion 機構の実装を順に紹介します。
Optimism の Force Inclusion 機構
まず、Optimism の Deposit プロセスを紹介します。この Deposit は、単にお金を Optimism に預けることだけでなく、「ユーザーから L2 に送信される情報」を L2 に送ることも含まれます。L2 ノードは新しく預けられたメッセージを受け取ると、そのメッセージを L2 取引に変換して実行し、指定された受取人に送信します。
ユーザーが L1 から L2 に Deposit するメッセージ
L1CrossDomainMessenger コントラクト
ユーザーが ETH または ERC-20 トークンを Optimism に預けるとき、彼はフロントエンドのウェブページを介して L1 上の L1StandardBridge コントラクトと対話し、どれだけの金額を預けるか、どの L2 アドレスがこれらの資産を受け取るかを指定します。
L1StandardBridge コントラクトはメッセージを次のレイヤーの L1CrossDomainMessenger コントラクトに送信します。このコントラクトは主に L1 と L2 の間の相互通信のコンポーネントとして機能します。L1StandardBridge はこの汎用の通信コンポーネントを介して L2 上の L2StandardBridge と通信し、誰が L2 でトークンを発行できるか、または誰が L1 からトークンを解除できるかを決定します。
もし開発者が L1 と L2 の間で相互に通信し、状態を同期するコントラクトを開発する必要がある場合、彼は L1CrossDomainMessenger コントラクトの上に構築できます。
ユーザーのメッセージは CrossDomainMessenger コントラクトを介して L1 から L2 に送信されます
注:この記事の一部の画像では CrossDomainMessager が CrossChainMessager と記載されています。
OptimismPortal コントラクト
L1CrossDomainMessenger コントラクトは、メッセージを最下層の OptimismPortal コントラクトに送信します。OptimismPortal コントラクトが処理を終えると、TransactionDeposited という名前のイベントが発生し、パラメータには「メッセージを送信した人」、「メッセージを受け取った人」、および関連する実行パラメータが含まれます。
次に、L2 の Optimism ノードは OptimismPortal コントラクトが発生させた Transaction Deposited イベントをリッスンし、イベント内のパラメータを L2 取引に変換します。この取引の発起者は Transaction Deposited イベントのパラメータで指定された「メッセージを送信した人」であり、取引の受取人はイベントパラメータの「メッセージを受け取った人」です。他の取引パラメータも上記のイベントのパラメータから来ています。
L2 ノードは OptimismPortal emit の Transaction Deposited イベントパラメータを L2 取引に変換します
例えば、これはあるユーザーが L1StandardBridge コントラクトを介して 0.01 ETH を預ける取引です。このメッセージと ETH は OptimismPortal コントラクト(アドレスは 0xbEb5…06Ed)に送信され、数分後に L2 取引に変換されます:
メッセージの発起者は L1CrossDomainMessenger コントラクトであり、受取人は L2 上の L2CrossDomainMessenger コントラクトです。メッセージ内容は L1StandardBridge が Bob の 0.01 ETH の預金を受け取ったことです。その後、いくつかのプロセスがトリガーされます。例えば、L2StandardBridge に 0.01 ETH を追加発行し、その後、後者が Bob に転送します。
具体的にどうトリガーするか
取引を Optimism の Rollup コントラクトに強制的に収納する場合、達成したい効果は「あなたの L2 アドレスから L2 で発起され、実行される取引」がスムーズに実行されることです。この時、あなたは自分の L2 アドレスを使ってメッセージを直接 OptimismPortal コントラクトに提出する必要があります(注意:OptimismPortal コントラクトは実際には L1 上にありますが、OP のアドレス形式は L1 アドレス形式と一致しているため、あなたは L2 アカウントと同じアドレスの L1 アカウントを使って上記のコントラクトを呼び出すことができます)。
その後、コントラクトが発生させた Transaction Deposited イベントから変換された L2 取引の「発起者」はあなたの L2 アカウントになります。この時、取引形式は通常の L2 取引と一致します。
Transaction Deposited イベントから変換された L2 取引では、発起者は Bob 自身になります。受取人は Uniswap コントラクトであり、指定された ETH が添付されます。まるで Bob 自身が L2 取引を発起したかのようです。
Optimism の Force Inclusion 機能を呼び出すには、OptimismPortal コントラクトの depositTransaction 関数を直接呼び出し、L2 で実行したい取引のパラメータを入力する必要があります。
私は簡単な Force Inclusion 実験を行いました。この取引は、私のアドレスから自分に送金(0xeDc1…6909)し、「force inclusion」というテキストメッセージを添付することを目的としています。
これは私が OptimismPortal コントラクトで depositTransaction 関数を実行した L1 取引であり、その発生させた Transaction Deposited イベントでは、from と to が私自身であることがわかります。
残りの opaque Data 欄の値は、「deposit Transaction 関数を呼び出した人がどれだけの ETH を添付したか」、「L2 取引の発起者が受取人にどれだけの ETH を送るか」、「L2 取引の GasLimit」、「L2 受取人への Data」などの情報をエンコードしています。
上記の情報をデコードすると、それぞれ次のようになります:
「deposit Transaction を呼び出した人が追加した ETH の量」:0、なぜなら私は L1 から L2 に ETH を預けていないからです;
「L2 取引の発起者が受取人に送る ETH の量」:5566(wei)
「L2 取引の GasLimit」:50000
「L2 受取人への Data」:0x666f72636520696e636c7573696f6e、つまり「force inclusion」という文字列の16進数エンコードです。
その後、変換された L2 取引が現れました:私が自分にお金を送る L2 取引で、金額は 5566 wei、Data は「force inclusion」文字列です。そして、図の中の倒数第二行の Other Attributes の TxnType(取引タイプ)は、システム取引126(System)であり、この取引は私自身が L2 で発起したものではなく、L1 取引の Deposited イベントから変換されたものであることが示されています。
変換された L2 取引
もし Force Inclusion を通じて L2 コントラクトを呼び出し、異なる Data を送信したい場合は、単に前述の deposit Transaction 関数にパラメータを一つ一つ入力すればよいだけです。ただし、必ず自分の L2 アカウントと同じ L1 アドレスを使って deposit Transaction 関数を呼び出すことを忘れないでください。そうすれば、Deposited Event が L2 取引に変換されたとき、発起者はあなたの L2 アカウントになります。
SequencerWindow
前述の Optimism L2 ノードが Transaction Deposited イベントを L2 取引に変換する際、実際にこの Optimism ノードはシーケンサーを指します。取引の順序に関わるため、シーケンサーだけが前述のイベントを L2 取引に変換するタイミングを決定できます。
Transaction Deposited イベントを監視しているとき、シーケンサーは必ずしもすぐにイベントを L2 取引に変換するわけではなく、遅延が生じる可能性があります。この遅延の最大値を SequencerWindow と呼びます。
現在、Optimism メインネットの Sequencer Window は 24 時間です。つまり、ユーザーが L1 からお金を預けたり Force Inclusion で取引を行った場合、最悪のシナリオでは 24 時間後に初めて L2 取引履歴に記録されることになります。
Arbitrum の Force Inclusion 機構
Optimism では L1 の Deposit 操作が Transaction Deposited イベントを発生させ、その後はシーケンサーが上記の操作を取り込むのを待つだけですが、Arbitrum では L1 で発生する操作(預金や L2 へのメッセージ送信など)は L1 上のキューに保存され、単にイベントを発生させるだけではありません。
シーケンサーには、上記のキュー内の取引を L2 取引履歴に取り込むための時間が与えられます。もし時間が経過してもシーケンサーが何も行動しなければ、誰でもシーケンサーの代わりにそれを完了させることができます。
Arbitrum は L1 コントラクトでキューを維持します。シーケンサーがキュー内の取引を積極的に処理しない場合、時間が経過すると誰でもキュー内の取引を強制的に L2 取引履歴に取り込むことができます。
Arbitrum の設計では、L1 で発生する預金などの操作は Delayed Inbox コントラクトを経由する必要があります。名の通り、ここでの操作は遅延して有効になります。もう一つのコントラクトは Sequencer Inbox で、シーケンサーが L2 取引を L1 にアップロードする際の直接の場所です。シーケンサーが L2 取引をアップロードするたびに、Delayed Inbox からいくつかの未処理の取引を一緒に取出して取引履歴に書き込むことができます。
シーケンサーが新しい取引を書き込む際に、Delayed Inbox から取引を一緒に取り出すことができます。
複雑な設計と参考資料の不足
もし読者が Arbitrum の公式文書でシーケンサーおよび Force Inclusion の章を直接参照すると、Force Inclusion が大まかにどのように機能するか、いくつかのパラメータ名や関数名が記載されているのがわかります:
ユーザーはまず Delayed Inbox コントラクトで sendUnsignedTransaction 関数を呼び出します。もしシーケンサーが約 24 時間以内に取り込まなければ、ユーザーは SequencerInbox コントラクトの forceInclusion 関数を呼び出すことができます。しかし、Arbitrum の公式は関数のリンクを公式文書に添付しておらず、コントラクトコード内の対応する関数を自分で確認する必要があります。
sendUnsignedTransaction 関数を見つけると、nonce 値や maxFeePerGas 值を自分で入力する必要があることに気づきます。それはどのアドレスの nonce ですか?どのネットワークの maxFeePerGas ですか?どのように入力すればよいのでしょうか?文書の参考がなく、Natpsec もありません。さらに、Arbitrum コントラクト内には似たような関数がたくさんあります:
sendL1FundedUnsignedTransaction、sendUnsignedTransactionToFork、sendContractTransaction、sendL1FundedContractTransaction、これらの関数の違いや使い方、パラメータの入力方法についての文書もありません。Natpsec もありません。
あなたは試行錯誤の心構えでパラメータを入力して取引を送信し、正しい使い方を見つけようとしますが、これらの関数はすべてあなたの L1 アドレスを AddressAliasing してしまい、最終的に L2 で取引を発起する際の Sender がまったく異なるアドレスになってしまい、あなたの L2 アドレスは動かなくなります。
sendL2Message
その後、偶然 Google 検索を開くと、Arbitrum 自体にチュートリアルライブラリがあり、L1 から L2 取引を送信する方法(つまり Force Inclusion の意味)を示すスクリプトがありました。そして、そこに示されている関数は上記のどれでもなく、sendL2Message という名前のもので、message パラメータには L2 アカウントで署名された取引を入力する必要があることがわかりました。
誰が「Force Inclusion のために L2 に送るメッセージ」が「署名された L2 取引」であることを知っているでしょうか?しかも、いつこの関数を使うべきか、どのように使うべきかについての文書や Natspec の説明はありません。
結論:Arbitrum の強制取引を手動で生成するのはかなり面倒です。公式のチュートリアルに従って Arbitrum SDK を実行することをお勧めします。Arbitrum は他の Rollup のように明確な開発者文書やコード注釈がなく、多くの関数の用途やパラメータが説明不足であるため、開発者は予想以上に多くの時間を接続および使用に費やす必要があります。私も Arbitrum の Discord で Arbitrum の人に尋ねましたが、満足のいく答えは得られませんでした。
Discord で尋ねると、相手は私に sendL2Message を見に行くように言うだけで、他の関数の機能(さらには Force Inclusion 文書で言及されている sendUnsignedTransaction の用途、使い方、使用時期について)を説明しようとはしません。
StarkNet の Force Inclusion 機構
残念ながら、StarkNet には現在 Force Inclusion 機構がありません。公式フォーラムで検閲および Force Inclusion に関する議論が行われた記事が2つあるだけです。
証明できない失敗した取引
上記の理由は、StarkNet のゼロ知識証明システムが失敗した取引を証明できないため、Force Inclusion を許可できないからです。もし誰かが悪意を持って(または無意識に)失敗した取引を Force Include した場合、StarkNet は直接停止します。なぜなら、取引が強制的に収入された後、Prover はその失敗した取引を証明しなければならず、しかしそれを証明することができないからです。
StarkNet は v0.15.0 版で失敗した取引を証明する機能を導入する予定であり、その後、Force Inclusion 機構をさらに実装できるようになるでしょう。
zkSync の Force Inclusion 機構
zkSync の L1->L2 メッセージ送信および Force Inclusion 機構は、MailBox コントラクトの requestL2Transaction 関数を介して行われます。ユーザーは L2 アドレス、calldata、追加の ETH 数量、L2GasLimit 値などを指定し、requestL2Transaction はこれらのパラメータを組み合わせて L2 取引を生成し、優先キュー(PriorityQueue)に入れます。シーケンサーは取引を L1 にパッケージ化してアップロードする際(commitBatches 関数を介して)、優先キューからどれだけの取引を一緒に取り込むかを指定します。
zkSync の Force Inclusion の形式は Optimism と非常に似ており、発起者の L2 アドレス(L1 アドレスと一致)を使って関連関数を呼び出し、データ(呼び出し先、calldata など)を入力します。Arbitrum のように署名された L2 取引を入力するのではなく、設計上は Arbitrum と同様に、L1 にキューを維持し、シーケンサーがユーザーが直接提出した未処理の取引をキューから取り出して取引履歴に書き込む形です。
もしあなたが zkSync の公式ブリッジを通じて ETH を Deposit すると、例えばこの取引は MailBox コントラクトの requestL2Transaction 関数を呼び出します。この取引は Deposit ETH の L2 取引を優先キューに入れ、NewPriorityRequest イベントを発生させます。コントラクトが L2 取引データを一連の bytes 文字列にエンコードするため、読み取りにくくなっていますが、この L1 取引のパラメータを見ると、L2 の受取人も取引の発起人であることがわかります(自分に Deposit するため)。その後、しばらくするとこの L2 取引がシーケンサーによって優先キューから取り出され、取引履歴に記録されると、L2 上で自分に自分を送金する取引に変換され、送金額は取引発起人が L1 の Deposit ETH 取引で持参した ETH 金額になります。
L1Deposit 取引では、取引発起人と受取人がどちらも 0xeDc1…6909 で、金額は 0.03 ETH、calldata は空です。
L2 では 0xeDc1…6909 が自分に送金する取引が現れ、取引タイプ(TxnType)は 255 で、システム取引であることを示しています。
その後、私は以前の OP の強制取引機能の実験と同様に、zkSync の requestL2Transaction 関数を呼び出し、自己送金の取引を行いました:ETH を持参せず、calldata に「force inclusion」文字列の HEX エンコードを入力しました。
その後、これは L2 上で自分に自分を送金する取引に変換され、calldata には「force inclusion」の16進数文字列が含まれます:0x666f72636520696e636c7573696f6e。
シーケンサーが取引を PriorityQueue から取り出し、取引履歴に書き込むと、L2 上で対応する L2 取引に変換されます。
requestL2Transaction 関数を通じて、ユーザーは L2 アドレスと同じ L1 アカウントを使用して L1 にデータを提出し、L2 受取人、添付の ETH 金額、calldata を指定できます。もしユーザーが他のコントラクトを呼び出し、異なる Data を持参したい場合は、同様にパラメータを一つ一つ requestL2Transaction 関数に入力すればよいのです。
まだユーザーが強制的に収録する機能はない
L2 取引が優先キューに入った後、その L2 取引がシーケンサーに収録される待機期限が計算されますが、現在の zkSync の設計には、ユーザーが強制的に実行できる Force Inclusion 関数はありません。つまり、半分の機能しか実装されていないのです。つまり、「収録待機期限」があるものの、実際には「シーケンサーが収録するかどうかを見るだけ」です。シーケンサーは期限が過ぎた後に収録することもあれば、優先キュー内の取引を一切収録しないこともできます。
将来的に zkSync は関連する関数を追加し、ユーザーが収録の有効期限が過ぎたがまだシーケンサーに収録されていない場合に、取引を強制的に L2 取引履歴に含めることができるようにするべきです。これこそが真の Force Inclusion 機構です。
まとめ
L1 は多数のバリデーターによってネットワークの「安全性」と「検閲耐性」を確保していますが、Rollup は少数または単一のシーケンサーによって取引が書き込まれるため、検閲耐性はさらに弱くなります。したがって、Rollup には Force Inclusion 機構が必要であり、ユーザーがシーケンサーを回避して取引を履歴に書き込むことができるようにし、シーケンサーによる検閲によって使用できなくなったり、資金を Rollup から引き出せなくなったりすることを避ける必要があります。
Force Inclusion により、ユーザーは取引を強制的に履歴に書き込むことができますが、設計上は「取引が即座に履歴に挿入され、即座に有効になるかどうか」を選択する必要があります。もし取引が即座に有効になることを許可すれば、シーケンサーに悪影響を及ぼすことになります。なぜなら、L2 上で収録を待っている取引は、L1 で強制的に収録される取引によって影響を受ける可能性があるからです。
したがって、現在の Rollup の Force Inclusion 機構は、L1 上に挿入された取引をまず待機状態にし、シーケンサーに反応するための時間ウィンドウを与え、待機中の取引を収録するかどうかを選択させる形をとっています。
zkSync と Arbitrum は、L1 にキューを維持して、ユーザーが L1 から送信した L2 取引や L2 へのメッセージを管理しています。Arbitrum は DelayedInbox と呼び、zkSync は PriorityQueue と呼びます。
しかし、zkSync の L2 取引送信方法は Optimism に似ており、L2 アドレスを使って L1 にメッセージを送信します。こうして L2 取引に変換された後、発起者はその L2 アドレスになります。Optimism の L2 取引送信関数は depositTransaction と呼ばれ、zkSync は requestL2Transaction と呼ばれます。一方、Arbitrum は完全な L2 取引を生成し署名し、sendL2Message 関数を介して送信します。Arbitrum では L2 上で署名を通じて署名者を復元し、L2 取引の発起者とします。
StarkNet には現在 Force Inclusion 機構がなく、zkSync は半分の Force Inclusion を実装しています。---PriorityQueue があり、各キュー内の L2 取引には収録の有効期限がありますが、この有効期限は現在装飾的なものであり、実際にはシーケンサーが PriorityQueue 内の L2 取引を一切収録しないことを選択できます。