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

レッスン10 — build_payload が pending fill を drain する

問い

pending_fills に溜まった約定を、いつ・どう payload に移すか? submit 時か、build 時か?

原理(最小モデル)

  • std::mem::take は O(1) — 1000 個の Vec<Fill> でも (ptr, len, cap) を 1 代入で swap。drain(..).collect() は O(N) + iterator オーバーヘッド。
  • drain は submit でなく build_payload で。 Fill は「どの payload に乗るか」でグループ化する(submit 順でない)。submit 時に drain すると payload 割り当てを別チャネルで追う羽目に。
  • forward-only drain は block の不変性を反映。 payload N は「前回 build から今まで」の fill を受け取り、以前の payload は遡って更新されない(commit 済み block と同じ — 一度組んだら凍結)。
  • fill loss 失敗モードは現実だが v0 許容。 drain 後に build_payload が error で戻ると fill が消える(buffer からも payload からも)。production は recovery queue で保護、v0 single-validator は許容。

具体例

submit(o_a) submit(o_b)  build_payload(id=1)      submit(o_c)  build_payload(id=2)
pending:[F_a] [F_a,F_b]   mem::take → id=1 が       [F_c]        mem::take → id=2 が
                          [F_a,F_b] を所有、                     [F_c] を所有、
                          buffer 空に                            buffer 空に
pending HashMap: {id=1: (.., [F_a,F_b])}  →  {id=1: (.., [F_a,F_b]), id=2: (.., [F_c])}  ← 遡及書き込みなし

失敗例(誤解)

guard.iter().copied().collect() してから guard.clear()」は誤り — (a) O(N) copy + O(N) clear(mem::take の O(1) swap に対し)、(b) 2 step 版は pending_fill_count を読む誰かが半 drain 状態を見る窓ができる。mem::take は外側から atomic。


ここまでで「mem::take・build 時 drain・forward-only」は着地した。ここから 1 箇所を変更する。コードは完全形。

🛑 予測。 std::mem::take(&mut v)v の中身を奪い vDefault に置く。v.drain(..).collect::<Vec<_>>() で同じ効果は出せるが、実用上の違いは?(答え: drain は要素ごとに取り除く iterator で O(N) + オーバーヘッド。mem::take は Vec 全体を pointer swap で O(1)。「全部取って default」なら mem::take が速く意図も明確。)

ステップで組み立てる

Step 1: build_payload の placeholder を置換

レッスン9 の s.pending.insert(id, (hash, header, Vec::new())); を次に変える:

        let hash = header.hash_slow();

        // Drain whatever fills the CLOB has accumulated since the last
        // build_payload call. The fills attach to this payload so the bridge
        // can route them downstream (encode as EVM txs, return via
        // payload_fills, etc.). 8d keeps them as a parallel list; future
        // stages encode them into the block body.
        let drained_fills = std::mem::take(
            &mut *self
                .pending_fills
                .lock()
                .expect("pending_fills mutex poisoned"),
        );

        s.pending.insert(id, (hash, header, drained_fills));
        Ok(PayloadId(id))
    }

.lock().expect(...)MutexGuard<Vec<Fill>>DerefMut を持つ)→ &mut *guard&mut Vec<Fill>std::mem::take が Vec を drained_fills に move し guard 側を Vec::default()(空)に置換。一時 MutexGuard は statement の ; で drop → 次行の s.pending.insert 時点で pending_fills lock は解放済み(lock ordering 安全)。mem::take は lock 下で atomic(半 drain 状態は他から見えない)。

答え合わせ

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

本レッスン後、bridge は 428cc26 と機能的に等価(doc 以外)。唯一の差は integration test(clob_fills_flow_into_payload、レッスン11)。

合格基準

cargo test -p openhl-evm --release
grep -n "std::mem::take" crates/evm/src/live_node.rs   # build_payload で 1 行
grep -n "Vec::new()" crates/evm/src/live_node.rs       # build_payload の行は消えている

38 テスト pass。よくあるミス: &*guard&mut *guard が必要、DerefMut)/ mem::take(guard)&mut * なしで(take&mut T を取る)。

まとめ(3行)

  • build_payloadstd::mem::take を使い、前回 build 以降に溜まった fill を O(1) で drain して payload に attach する。
  • drain は build 時に行い payload 単位でグループ化、forward-only(以前の payload を遡及更新しない)。
  • mem::take は lock 下で atomic — 半 drain 状態を他に見せない(collect+clear より速く correct)。