レッスン4 — install_clob() — EVM の state をマッチングエンジンに橋渡しする
問い
precompile は関数ポインタ(fn(&[u8], u64, u64) -> PrecompileResult)で、環境をキャプチャできない(move クロージャが書けない)。では bridge が所有する live CLOB state を、どうやって precompile に届けるか?
原理(最小モデル)
- 関数ポインタ → プロセスグローバル state が回避策。 共有 state を
staticに置き、precompile が呼び出し時にそこを読む。bridge がinstall_clobで書き、precompile がcurrent_best_bid()で読む。 RwLock<Option<Arc<Mutex<Book>>>>— アクセスパターンが違えばロックの種類も違う。 外RwLockは installed/uninstalled の区別(write 稀)、内Mutexは matching engine(write 頻繁)。Mutex<Option<...>>1 個だと全 read が 1 箇所のボトルネックを通る。Arc<Mutex<Book>>で bridge/precompile 境界を越えて所有を共有。 別々の caller が同じBookを見る。Arc =「所有者は複数、データは同じ」。- install は replace(error にしない)/ 配管は通すが電流は流さない。 test が install/uninstall を反復する。レッスン4 は配管(static / install 関数 / bridge フィールド型)を繋ぐが
read_best_bidはハードコードのまま — スイッチはレッスン5。
具体例
4 層ラッパー RwLock<Option<Arc<Mutex<Book>>>> の責務分担:
① RwLock : install 済みか否か(write=install/uninstall 超レア、read=precompile 毎回・並列OK → RwLock)
② Option : install 前(None)/後(Some) を型で表現(None→precompile はゼロ encode)
③ Arc : bridge と static が同じ Book を強参照(所有共有)
④ Mutex<Book>: matching engine 本体の排他保護(submit/best_bid_with_qty、write 頻繁 → Mutex)
bridge: submit_order → .lock().submit/precompile: current_best_bid → .lock().best_bid_with_qty — 同じ Book。
失敗例(誤解)
「OnceLock/lazy_static! を使えばいい」は誤り — OnceLock は 1 回しか set できず、test 分離のための install/uninstall 反復に不向き。Rust 1.63+ の static RwLock = RwLock::new(None)(const fn)が標準イディオム。「Mutex<Option<...>> 1 個に統合」も誤り(全 read がボトルネック)。
ここまでで「process-global static・4 層の責務分担」は着地した。ここから配管を組み立てる(read_best_bid 本体はレッスン5)。コードは完全形。
🛑 予測。
PrecompileFnは関数ポインタで環境キャプチャ不可。precompile にインスタンスごとの state を渡す唯一の方法は?(答え:Arc<Mutex<Book>>を引数で渡せない(シグネチャ固定)→staticから読む。bridge が install で書き、precompile が読む。トレードオフ: プロセスあたり CLOB は 1 つに固定 — 単一バリデータでは受容可能。)
ステップで組み立てる
Step 1: Book に best_bid_with_qty + best_ask_with_qty(crates/clob/src/book.rs)
/// Best bid price + total qty resting at that price level (sum of every
/// resting order in the level's FIFO queue). Returns `None` if there
/// are no bids.
#[must_use]
pub fn best_bid_with_qty(&self) -> Option<(Price, Qty)> {
self.bids.iter().next().map(|(rev_price, queue)| {
let qty: u64 = queue.iter().map(|o| o.qty.0).sum();
(rev_price.0, Qty(qty))
})
}
/// Best ask price + total qty resting at that price level.
#[must_use]
pub fn best_ask_with_qty(&self) -> Option<(Price, Qty)> {
self.asks.iter().next().map(|(price, queue)| {
let qty: u64 = queue.iter().map(|o| o.qty.0).sum();
(*price, Qty(qty))
})
}
best_bid()(価格のみ)と違い (price, そのレベルの FIFO キュー内 qty 合計) を返す — precompile の 64-byte 2 値レスポンス用。depth_bid()(全 bid の注文本数)とは別メトリクス。
Step 2: precompiles/mod.rs の import
use openhl_clob::Book;
use std::sync::{Arc, Mutex, RwLock};
RwLock は read(precompile 毎回)が write(install プロセス 1 回)より圧倒的多なので並行 read を許す。
Step 3: static CLOB_STATE
/// Process-global handle to the CLOB the precompile reads from.
///
/// `None` until [`install_clob`] is called (typically by `LiveRethEvmBridge::new`).
/// While `None`, `read_best_bid` returns zero-encoded output rather than
/// erroring — this keeps existing tests deterministic and matches what an
/// uninitialised perp market would return on mainnet.
static CLOB_STATE: RwLock<Option<Arc<Mutex<Book>>>> = RwLock::new(None);
RwLock::new(None) は const fn でコンパイル時評価 → 実行時の初期化レースが起きない。None は「未インストール」= ゼロ encode(メインネットの未初期化 perp market と同じ契約)。
Step 4: 3 つのモジュール関数
/// Install the CLOB instance the precompile should read from. The bridge
/// shares its `Arc<Mutex<Book>>` with the global so every EVM-side
/// `staticcall` to `CLOB_READ_BEST_BID` sees the same book the application
/// writes to via `submit_order`.
///
/// Calling this replaces any previously-installed CLOB. Production deployments
/// should call it exactly once at bridge construction.
pub fn install_clob(clob: Arc<Mutex<Book>>) {
*CLOB_STATE.write().expect("CLOB_STATE rwlock poisoned") = Some(clob);
}
/// Clear the installed CLOB. Used by tests that need a clean slate; rare in
/// production. Idempotent — uninstalling when nothing is installed is a no-op.
pub fn uninstall_clob() {
*CLOB_STATE.write().expect("CLOB_STATE rwlock poisoned") = None;
}
/// Read the currently-installed CLOB's best bid. Returns `None` if no CLOB
/// is installed or if the book has no bids. Public so tests can verify
/// install/uninstall without going through the precompile dispatch.
#[must_use]
pub fn current_best_bid() -> Option<(openhl_clob::Price, openhl_clob::Qty)> {
let state = CLOB_STATE.read().expect("CLOB_STATE rwlock poisoned");
let clob = state.as_ref()?;
let book = clob.lock().expect("clob mutex poisoned");
book.best_bid_with_qty()
}
install_clob は直前を置き換え(idempotent)、uninstall_clob は主に test 用、current_best_bid は EVM を経由せず直接テストできるよう公開。ロック順序の不変条件: 常に外(CLOB_STATE RwLock)→内(Book Mutex) の順(逆順を作らない限り deadlock しない)。
Step 5: bridge の clob を Arc<Mutex<Book>> に(live_node.rs)
struct フィールドと new() を変更:
pub struct LiveRethEvmBridge<P> {
provider: P,
chain_spec: Arc<ChainSpec>,
validator: EthBeaconConsensus<ChainSpec>,
/// `Arc<Mutex<Book>>` rather than `Mutex<Book>` so the bridge can share
/// its CLOB with the precompile module's process-global state. The bridge
/// writes via `submit_order`; smart contracts read via the
/// `clob_read_best_bid` precompile — both touch the same `Book`.
clob: Arc<Mutex<Book>>,
pending_fills: Mutex<Vec<Fill>>,
state: Mutex<State>,
}
impl<P> LiveRethEvmBridge<P> {
#[must_use]
pub fn new(provider: P, chain_spec: Arc<ChainSpec>) -> Self {
let validator = EthBeaconConsensus::new(Arc::clone(&chain_spec));
let clob = Arc::new(Mutex::new(Book::new()));
// Make our CLOB visible to the `clob_read_best_bid` precompile so
// smart contracts can query live orderbook state. The bridge writes
// (submit_order), the EVM reads (precompile); they share the same Arc.
crate::precompiles::install_clob(Arc::clone(&clob));
Self {
provider,
chain_spec,
validator,
clob,
pending_fills: Mutex::new(Vec::new()),
state: Mutex::new(State::default()),
}
}
Arc::clone(&clob) で refcount をインクリメント(bridge と static の両方が強参照)。submit_order の self.clob.lock() は Arc<Mutex<Book>> が &Mutex<Book> に deref するのでそのまま動く(他の callsite 変更不要)。
答え合わせ
cd ~/code/openhl-reference && git checkout b635ef7
diff -u ~/code/my-openhl/crates/clob/src/book.rs ./crates/clob/src/book.rs
diff -u ~/code/my-openhl/crates/evm/src/precompiles/mod.rs ./crates/evm/src/precompiles/mod.rs
git checkout main
本レッスン後、Stage 9b に 部分的に 一致(新メソッド/static/関数3/bridge フィールド)。残る差: read_best_bid がまだハードコード(レッスン5)+ レッスン3 の unit test がまだハードコード値を期待。
合格基準
cargo test -p openhl-evm --release
→ 42 テスト pass(レッスン3 の unit test は依然ハードコード値を期待 — read_best_bid 未変更だから)。配管は通したが電流はまだ流れない。よくあるミス: self.clob.deref().lock() と書く(self.clob.lock() が正)/ struct リテラルで clob を使い忘れ。
まとめ(3行)
- 関数ポインタ制約への定石は process-global
static— bridge が install で書き、precompile が読む(コストは CLOB 1 つ/プロセス)。 RwLock<Option<Arc<Mutex<Book>>>>の 4 層は別々の責務(installed 区別 / 型表現 / 所有共有 / engine 保護)— 重ねるでなく分担。- レッスン4 は配管(static/install/Arc 共有)を通すが
read_best_bidはハードコードのまま — スイッチはレッスン5。