FABRKNT
Step 2. CLOB — マッチングエンジンの追加とステートマシンの統合
Bridge 統合
レッスン 10 / 13·CONTENT40 分70 XP
コース
Step 2. CLOB — マッチングエンジンの追加とステートマシンの統合
レッスンの役割
CONTENT
順序
10 / 13

レッスン9 — LiveRethEvmBridge に CLOB + submit_order を持たせる

問い

完成した matching engine を consensus 側の LiveRethEvmBridge にどう接続するか? CLOB は Reth EVM の に置くのか、 に置くのか?

原理(最小モデル)

  • CLOB は EVM の に置く。 clob: Mutex<Book>provider/state と並ぶ bridge のフィールド。約定は payload に併走する parallel データレーンで、まだ EVM transaction ではない(それは Step 3 の precompile)。
  • Mutex は 1 つでなく 2 つ。 clobpending_fills は別タイミングで別 caller が変更する。lock を分ければ pending_fill_count を読むスレッドが book を触る submitter を block しない。
  • interior mutability + &self が async 共有 state の idiom。 submit_order(&self, ...) だから bridge を Arc で共有できる。RwLock<Bridge> を上に載せると全アクセスが直列化する。
  • lock を取る API は lock 越しに参照を返さない。 payload_fills&[Fill] でなく Vec<Fill>(clone) を返す(borrow を返すと caller が lock guard を抱え続けて deadlock)。

具体例

bridge 内部トポロジー(3 レーン):

order in → submit_order → [Mutex<Book>] match → fills → [Mutex<Vec<Fill>>] append
                                                              ↓ build_payload が drain(レッスン10)
                          [Mutex<State> pending: HashMap<id, (hash, header, Vec<Fill>)>]
                                                              ↓ build_payload → PayloadId
                                                          EVM レーン(state/header/forkchoice)

clobpending_fills を別 Mutex にするのが load-bearing — 2 レーンが直列化しない。

失敗例(誤解)

Mutex<(Book, Vec<Fill>)> 1 個でいい」は誤り — submit ごとに matching と fill-buffer mutation の両方で lock を保持し、submit せず count を読むだけのコードが block される。「submit_order&mut self に」も誤り — Arc<RwLock<Bridge>> が必要になり全 access を 1 グローバル lock で直列化する。


ここまでで「横に置く・Mutex 2 個・&self」は着地した。ここから組み立てる。コードは完全形(build_payload の drain はレッスン10)。

🛑 予測。 本レッスン後、order を submit して fill が蓄積したあと build_payload を呼ぶと、その payload への payload_fills(id) は何を返すか?(答え: Some(vec![]) — 空。本レッスンはデータフローを接続するが build_payload はまだ drain せず空 Vec を挿入。レッスン10 でこれが Some(vec![fill...]) になる。)

ステップで組み立てる

Step 1: crates/evm/Cargo.toml に dep

[dependencies]
openhl-consensus         = { workspace = true }
openhl-types             = { workspace = true }
openhl-clob              = { workspace = true }      # NEW
async-trait              = { workspace = true }
# ... rest unchanged ...

Step 2: live_node.rs に import

use openhl_clob::{Book, Fill, FillResult, Order};                     // NEW

Book(matching engine)/ Fill(output)/ FillResultBook::submit の wrapper)/ Order(input)を pull in。

Step 3: struct にフィールド 2 個(validatorstate の間)

#[derive(Debug)]
pub struct LiveRethEvmBridge<P> {
    provider: P,
    chain_spec: Arc<ChainSpec>,
    validator: EthBeaconConsensus<ChainSpec>,
    clob: Mutex<Book>,                                            // NEW
    pending_fills: Mutex<Vec<Fill>>,                              // NEW
    engine_handle: Option<ConsensusEngineHandle<EthEngineTypes>>,
    state: Mutex<State>,
}

clob は matching engine(Book は内部 thread-safe でないので Mutex で包む)。pending_fillssubmit_order が push しレッスン10 の build_payload が drain する buffer。別 Mutex にするのは lock 粒度(submit は matching で clob を、append で pending_fills を短く保持。分ければ 2 submit が全 chain を直列化しない)。

Step 4: Statepending を 3-tuple に

#[derive(Debug, Default)]
struct State {
    next_payload_id: u64,
    /// Pending payloads keyed by `PayloadId.0`. Value is (`block_hash`, `header`,
    /// fills drained from the CLOB at `build_payload` time).
    pending: HashMap<u64, (B256, Header, Vec<Fill>)>,
    chain: HashMap<B256, Header>,
    head: Option<B256>,
}

3 番目要素が payload ごとの約定リスト。chain は据え置き(commit 後の fill track は本コース範囲外)。

Step 5: new() を更新

    pub fn new(provider: P, chain_spec: Arc<ChainSpec>) -> Self {
        let validator = EthBeaconConsensus::new(Arc::clone(&chain_spec));
        Self {
            provider,
            chain_spec,
            validator,
            clob: Mutex::new(Book::new()),                        // NEW
            pending_fills: Mutex::new(Vec::new()),                // NEW
            engine_handle: None,
            state: Mutex::new(State::default()),
        }
    }

Step 6: メソッド 3 個

    /// Submit an order to the CLOB. Resulting fills are buffered in
    /// `pending_fills` until the next `build_payload` drains them.
    pub fn submit_order(&self, order: Order) -> FillResult {
        let mut book = self.clob.lock().expect("clob mutex poisoned");
        let result = book.submit(order);
        if !result.fills.is_empty() {
            self.pending_fills
                .lock()
                .expect("pending_fills mutex poisoned")
                .extend(result.fills.iter().copied());
        }
        result
    }

    /// Inspect (read-only) the fills attached to a built payload. Returns
    /// `None` if the payload id is unknown. Production code would encode
    /// these as EVM-executable transactions before they reach the block
    /// body; v0 keeps them as a parallel list for test inspection.
    #[must_use]
    pub fn payload_fills(&self, id: PayloadId) -> Option<Vec<Fill>> {
        let s = self.state.lock().expect("state mutex poisoned");
        s.pending.get(&id.0).map(|(_, _, fills)| fills.clone())
    }

    /// Number of fills currently buffered, waiting for the next `build_payload`.
    #[must_use]
    pub fn pending_fill_count(&self) -> usize {
        self.pending_fills
            .lock()
            .expect("pending_fills mutex poisoned")
            .len()
    }

3 つとも &self(interior mutability)。submit_order は write path — Rust の NLL により book.submit(order) 直後に clob guard が drop され、pending_fills.lock() 時点で clob は解放済み(2 lock は同時に保持しない、deadlock なし)。payload_fills&[Fill] でなく Vec を clone して返す(lock 越しに参照を返さない)。

Step 7: destructuring の波及更新(5 サイト)

build_payload/payload_ready/validate_payload/commit/payload_fills の pending tuple pattern を 3 要素に揃える:

// build_payload: s.pending.insert(id, (hash, header, Vec::new()));  // 今は空 Vec; レッスン10 で drain
// payload_ready: let (hash, header, _fills) = s.pending.get(&n).cloned()...
// validate_payload / commit: .find(|(h, _, _)| *h == ...).map(|(_, h, _)| h.clone())

cargo check が「pattern length 2 but expected 3」で見逃しを教える。build_payloadVec::new() は placeholder(レッスン10 で std::mem::take に置換)— TODO コメントより discoverable。

答え合わせ

cd ~/code/openhl-reference && git checkout 428cc26
diff -u ~/code/my-openhl/crates/evm/src/live_node.rs ./crates/evm/src/live_node.rs
git checkout main

本レッスン後、フィールド/メソッド/3-tuple は参照と一致。build_payload の drain(参照は std::mem::take)と integration test はレッスン10/11。

合格基準

cargo test -p openhl-evm --release

38 テスト pass(Consensus 由来、新規テストなし — 構造変更のみ)。bridge.pending_fill_count() == 0(fresh bridge)を smoke check できる。よくあるミス: dep 追加忘れ / 波及 site 見逃し(compiler が name する)/ Book.await 跨ぎで保持(Send エラー)。

まとめ(3行)

  • CLOB は EVM の横の parallel レーン(clob: Mutex<Book> + pending_fills: Mutex<Vec<Fill>>、別 Mutex で lock 粒度を確保)。
  • submit_order&self + interior mutability で Arc 共有可能、lock 越しに参照を返さない。
  • build_payload は今は空 Vec placeholder — drain はレッスン10、証明はレッスン11。