Agaveに実装予定のスケジューラーバインディング
現在、Solanaネットワーク上の90%以上のバリデータが、報酬を増やすために、JitoやPaladinといった非デフォルトのスケジューラー実装を利用しています。まもなく、Anzaのagaveクライアントにも、標準クライアントを直接改変することなくブロックパッキングのロジックをカスタマイズできる新機能群が追加される予定です。
これらの変更により、トランザクション受信とブロックパッキングのための新しいモジュラーアーキテクチャが実現されます。このロジックはバリデータのコアバイナリから分離され、リプレイや投票といったバリデータの基本的な処理を妨げることなく、容易にスケジューラーのカスタマイズが可能になります。また、「drop on revert」や「all or nothing」といった一般的なブロック構築のプリミティブについても、標準実装を提供する予定です。
背景
SolanaにおけるMEV(最大抽出可能価値)が成熟するにつれ、カスタムスケジューラーの導入がますます一般的になってきています。こうしたスケジューラーは収益の増加に寄与する一方で、現状ではいくつかの課題があります。スケジューラーを変更しようとすると、完全にagaveとは別のバリデータクライアントを実行する必要がありますし、一部の実装は非公開で仕組みが不明瞭であるため、トランザクションの確実な実行が難しいこともあります。
モジュラーアーキテクチャへ移行することによって、以下のような状況と問題点を改善することができます:
- 透明性:改変されたスケジューラーが普及するにつれて、ネットワーク上で何が行われているかを理解しづらくなってきています。
- 安全性:Solanaのバリデータコードは複雑なので、コアバイナリへの変更はプロトコルの稼働性に影響を及ぼす可能性があります。
- 運用性:スケジューラーの変更にはバリデータバイナリそのものを差し替える必要があり、ネットワークのパッチ適用のたびにスケジューラーチームによるコードのリベース作業が発生します。
実装の概要
- 以下のようなCLIオプションが追加されます:
- QUICなどのプロトコルを通じて、バリデータを外部のブロック構築サービスに接続
- バリデータがTPUとしてゴシップ上で公開するアドレス/ポートの指定
- admin RPC経由で外部ブロックビルダーとの接続を管理
- 稼働中のバリデータから接続/切断を動的に切り替え可能
- ブロック構築コンポーネントは、リーダーウィンドウ中にブロック構築用のトランザクションをストリーミングでバリデータに送信
- リーダーバリデータは受け取ったトランザクションを、競合順(FIFO)でブロックにパック
- もし外部ブロック構築サービスとの接続が失われた場合、バリデータは通常のブロック生成ロジックにフォールバックし、ゴシップ上のコンタクト情報も更新
通信仕様
バリデータはTCP経由でブロック構築サービスに接続します。
ブロック構築サービスは、定期的にハートビートメッセージをバリデータに送信します。
リーダーウィンドウ中、ブロック構築サービスはメタデータ付きのトランザクションバッチをバリデータに送信し、リーダーバリデータはそれをブロックにパッキングします。
メタデータには「drop on revert」「all or nothing」など、特殊な処理条件を指定することができます。
ブロック構築サービスがバリデータに送るメッセージ構造は以下のようなコードになるでしょう:
#[derive(Serialize, Deserialize)]
#[repr(C)]
pub struct TransactionBatch {
id: u32,
flags: BatchFlags,
transactions: Vec<Vec<u8>>,
}
bitflags! {
#[derive(Serialize, Deserialize)]
pub struct BatchFlags: u8 {
const ALL_OR_NOTHING = 0b0000_0001;
const DROP_ON_REVERT = 0b0000_0010;
}
}
バリデータは以下のような情報をブロック構築サービスに返す必要があります:
- リーダースロットの開始/終了通知
- トランザクションバッチの結果
- ブロック制限に関する情報の更新
これらのメッセージも、コードで書けば次のような形式になります:
#[derive(Serialize, Deserialize)]
#[repr(C, u8)]
pub enum ValidatorMessage {
BeginLeaderSlot(Slot),
SlotUpdate({
percentage_through_slot: u64,
block_cost_units_used: u64,
}), // スロットの完了度合い(割合)
EndLeaderWindow,
TransactionResults(Vec<TransactionResult>),
InvalidBatch,
}
#[derive(Serialize, Deserialize)]
#[repr(C)]
pub struct TransactionUpdate {
batch_id: u32,
index: u32,
// None はトランザクションが含まれないことを意味する
committed_cus: Option<NonZeroU64>,
write_accounts: Vec<Pubkey>,
}
パッキングの保証
- トランザクション同士に競合がある場合、バリデータは受信した順序でパッキングを試みます。
- 受信したすべてのトランザクションがブロックに入りきらない場合、受信した順序でブロック制限に達するまで詰め込みます。
-
DROP_ON_REVERT
フラグ付きのバッチは、トランザクションの結果が成功した場合のみ含まれます。 -
ALL_OR_NOTHING
フラグ付きのバッチは、すべてのトランザクションがブロックに含まれる場合のみブロックに取り込まれます。
Discussion