[WIP] OP Stack Specificationを読む
目的
- OP Stackを使用してL2を構築しようとしている事業者が把握しておかないといけない基礎をまとめる
Network Participants
L2構築に必要なSequencer(Client群)にフォーカスする
Sequencers
概要
シーケンサーは主要なブロック生産者である。シーケンサーは1つの場合もあれば、コンセンサス・プロトコルを用いた多数の場合もある。1.0.0では、シーケンサーは1つだけである(現在はオプティミズム財団の監督下で運営されている)。一般に、仕様書では「シーケンサー」を複数のシーケンサーが運用するコンセンサス・プロトコルの代用語として使用することがある。
- ユーザーのオフチェーン取引を受け入れる -> Execution Engine
- オンチェーン取引(主にL1からの入金イベント)を監視。-> Rollup Node
- 両方のトランザクションを特定の順序でL2ブロックに統合。-> Execution Engine
- L1にcalldataまたはblobとして2つのものを提出することで、統合されたL2ブロックをL1に伝播する -> Batch Submitter, Output Submitter
構成要素
- Rollup Node:
- スタンドアロンのステートレスバイナリ。
- ユーザーから L2 トランザクションを受信する。
- L1上のロールアップデータを同期し、検証する。
- ロールアップ固有のブロック生成ルールを適用して、L1 からブロックを合成する。
- Engine APIを使用してL2チェーンにブロックを追加する。
- L1が再編成された場合の処理を行う。
- 未提出のブロックを他のロールアップ・ノードに配布する。
- Execution Engine(EE):
- Optimismをサポートするために若干の変更が加えられたGethノード。
- L2の状態を維持する。
- 他のL2ノードと状態を同期し、オンボーディングを高速化する。
- エンジンAPIをロールアップノードに提供する。
- Batch Submitter
- トランザクションバッチを BatchInbox アドレスに送信するバックグラウンドプロセス。
- Output Submitter
- L2OutputOracle に L2 出力のコミットメントをサブミットするバックグラウンドプロセス。
Block Derivation
Epochs and the Sequencing Window
ロールアップチェーンはEpochに細分化される。 L1ブロック番号とEpoch番号は1:1で対応する。 L1ブロック番号nに対して、対応するRollup Epoch nが存在する
Epochはブロック分のSequencing Windowが経過した後、つまり(n + SEQUENCING_WINDOW_SIZE)のBlockがL1チェーンに追加された後に、Blockを導出することができる。
たとえばL1ブロックを5つ含むように Sequencing Window を設定した場合、このウィンドウの総時間は60秒となる(12秒/ブロック×5ブロック=60秒)。 この60秒の時間枠の中で、理論的には30個のL2ブロックを生成することができる(60秒/2秒=30個)。
- トランザクションバッチはSequencing Window内のどのタイミングでも提出可能
- Sequencing Windowがあることで「シーケンサが古いEpochに遡ってブロックを追加すること」を防ぐことができ、ValidatorはいつEpochを確定できるかを知ることができる
- Sequencing Windowは、現在のデフォルトは3600 Epoch
- L1ブロックで行われた入金は、最悪ケースでもSequencing Window分 L1ブロックが経過した後にL2チェーンに含まれる
若干この文章のみだとSequencing Windowの存在理由の理解が難しかったので、より詳細な記述を見ていく
- 「トランザクションバッチはSequencing Window内のどのタイミングでも提出可能」とは?
- Sequencing Windowがあることで「シーケンサが古いEpochに遡ってブロックを追加すること」を防ぐことができ、ValidatorはいつEpochを確定できるかを知ることができる とは?
Batcher
Batcherは、データ・アベイラビリティ・プロバイダでチャネルを利用可能にする役割を担うソフトウェア・コンポーネント(独立したプログラム)です。 Batcherは、チャネルを取得するためにRollup Nodeと通信します。
BatcherはDAプロバイダにバッチャートランザクションを提出する。 これらのトランザクションには、チャネルに属するデータの塊である1つまたは複数のチャネル・フレームが含まれる。
用語が統一されてなくて混乱しがちだが、Batch Submitterと同義らしい
Channel, Frame
L1 Txの容量(最大128kB)に対して、柔軟にL2のtx内容をパッキングするためにChannelやFrameを使用する
- Channelは、圧縮されたトランザクションバッチ(任意のL2ブロック用)のシーケンスである(つまりトランザクションバッチのバッチ)。 複数のバッチをグループ化する理由は、単純に圧縮率を向上させ、データ利用コストを削減するためである。
- FrameはChannelをチャンクとして分解したもので、128kB以下に分解される
- Frameを最適配置して一回のL1 Txで含めることができるデータ量を最大化する
疑問の解消
「トランザクションバッチはSequencing Window内のどのタイミングでも提出可能」とは?
Frameを最適配置して一回のL1 Txで含めることができるデータ量を最大化する。つまりSequencing Windowは、Frameをプールして、最適配置の組み合わせを増やすためにも使用される
なので「トランザクションバッチはSequencing Window内のどのタイミングでも提出可能」ということらしい
この図では、使用されているシーケンス・ウィンドウのサイズは指定されていないが、チャンネルAの最後のフレームはブロック102に表示されているが、エポック99に属していることから、少なくとも4ブロックでなければならないと推測できる
ある過去のL2のトランザクションT1のデータを見たいとすると、T1を含むトランザクションバッチがFrame分解されてすべてL1に含まれる状態になるまで、最悪でもSequencing Window分の時間はかかる(逆に言えばSequencing Window分待てばT1のデータがDAから取り出せる状態になるということは保証されている)ということになる
Sequencing Windowがあることで「シーケンサが古いEpochに遡ってブロックを追加すること」を防ぐことができ、ValidatorはいつEpochを確定できるかを知ることができる とは?
- Sequencing Windowがない=Sequencing Windowが無限だという仮定を置くと、Frameの最適配置の柔軟性は高くなるが、その代わりトランザクションバッチのフラグメントがどのタイミングでも提出可能になってしまうため、Validatorが無限のSequencing WindowをVerifyする必要が出てくる。
- また、シーケンサが古いEpochに遡ってブロックを追加できる点に関して、Channelの容量に制限がないとすれば無限に過去のChannelにL2のBlockを増やし、そのFrameを現在のHeadに追加できてしまう
たとえばAliceがL2で1/1 00:00にトランザクションT1を投げてそのトランザクションシーケンスがその後すぐL1のコミットされたとすると、次にAliceがT1のデータを使えるのはデフォルトだと12秒 * 3600 Epochなので12時間後の1/1 12:00ということになる。じゃあその間AliceがT1のデータを使えないかというとそういうわけではない
Unsafe L2 Blockは、Rollup Nodeだけが知っている、L1チェーンから導出していないL2ブロックである
シーケンサモードでは、これはシーケンサ自身がシーケンス化したブロックである。(単一シーケンサー仮定) バリデータモードでは、これは安全でない同期を経由してシーケンサーから取得したブロックとなる。なので12時間経ってないL2のBlockはこれにあたる
逆にSafe L2 Blockとは、Rollup NodeがL1から完全に導出できるL2ブロックのことである。
Rollup Nodeはステートレスマシンと書いてあったので、Execution Engine側にステートがあるのではないかと思う。
Block Derivation Loop
Rollup Driverと呼ばれるRollup Nodeのサブコンポーネントが、実際にBlock導出の実行を担当する。 Rollup Driverは実際にはBlockを作成しない。 代わりに、Engine APIを介してExecution Engineに指示する。Rollup Driverは基本的に、Block導出関数を実行する無限ループである。 各Epochごとに、Block導出関数は以下のステップを実行する:
- Sequencing Window内の各ブロックの預金および取引バッチデータをダウンロード
- デポジットおよびトランザクション・バッチ・データをEngine API用のペイロード属性に変換
- ペイロード属性をEngine APIに提出することでブロックに変換して正規チェーンに追加
3に関しての詳しいシーケンス図
- ペイロード属性オブジェクトで更新された fork choice を呼び出し、Engine API はペイロード ID を返す
- ステップ 1 で返されたペイロード ID を指定して get payload を呼び出します。 エンジンAPIは、ブロック・ハッシュをフィールドの1つとして含むペイロード・オブジェクトを返す
- ステップ 2 で返されたペイロードを使用して新しいペイロードを呼び出す
- フォーク選択パラメータの headBlockHash をステップ 2 で返されたブロックハッシュに設定して更新されたフォーク選択を呼び出す。 L2 チェーンのHeadがステップ 1 で作成されたブロックになる
L2 Output Root Proposals
1つまたは複数のブロックを処理した後の出力は、出金などのL2-to-L1メッセージングをトラストレスなく実行するために、決済レイヤー(L1)と同期させる必要がある。 これらのOutput Proposalsは、L2の状態に対するブリッジのビューとして機能します。 Proposersと呼ばれるアクターは、Output Rootをセトルメント・レイヤー(L1)に提出し、フォールト・プルーフで争うことができる。 このようなプロポーザの実装のひとつがop-proposerである。
Proposerの役割は、L1のL2OutputOracleコントラクト(決済レイヤ)に、L2の状態へのコミットメントである出力ルートを構築して提出することである。 これを行うために、Proposerは、最新の確定L1ブロックに由来する最新の出力ルートを、定期的にRollup Nodeに問い合わせる。 そして、その出力ルートを受け取り、決済レイヤ(L1)のL2OutputOracleコントラクトに提出する。
ここから見てわかる通りProposer = Output Submitterとして読み替えて良さそう