レッスン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の中身を奪いvをDefaultに置く。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_payloadでstd::mem::takeを使い、前回 build 以降に溜まった fill を O(1) で drain して payload に attach する。- drain は build 時に行い payload 単位でグループ化、forward-only(以前の payload を遡及更新しない)。
mem::takeは lock 下で atomic — 半 drain 状態を他に見せない(collect+clear より速く correct)。