レッスン6 — 読み出しマイルストーン — ラウンドトリップを証明する
問い
read チェーン全体(Solidity → STATICCALL → EVM dispatch → precompile → live Book → encode → コントラクト)が end-to-end で動くことを、どう証明するか?
原理(最小モデル)
- read チェーンを end-to-end で繋ぐ。
CLOB に bid 発注 → bridge が Mutex 経由で書く → precompile が global 経由で read → 64-byte ABI encode → 呼び出し元に返す。チェーン全体を一度に走査する最初のテスト。 - 敵対的テストデータ > ランダム。 order 2 個を意図的に:
(250, 7)(正しい答え)と(240, 99)(反復順を間違えたら取る larger-qty の罠)。50 個のランダム order より価値が高い。 - dispatch test と behavior test を分割。 レッスン5 は
Precompile::execute経由で到達可能を証明。レッスン6 はread_best_bidを直接呼び live state を読むことを証明。混ぜると失敗時のデバッグが難しくなる。 - assertion メッセージは保守者へのドキュメント。
"best bid is the 250 order, not 240"はどの不変条件が壊れたか伝える(素のleft=240 right=250は値しか伝えない)。
具体例
(250, 7) と (240, 99) を install して read_best_bid を呼ぶと:
素朴「最大 qty」 → (240, 99) ✗
素朴「最後 submit」 → (240, 99) ✗
正しい「最高価格」 → (250, 7) ✓ ← best bid は最高価格であって最大数量ではない
market sell は最高価格 250-bid に最初にぶつかり、250 を使い切ってから 240 に下りる。
失敗例(誤解)
「order 1 つでテストすれば十分」は誤り — (250,7) 1 つだけなら素朴な実装も全 pass し、正しさと偶然を切り分けられない。(240,99) を加えて初めて「best=最高価格」を証明できる(最小 order 数は 2)。「dispatch+behavior+state を 1 テストに束ねる」も誤り(失敗時にどこが壊れたか覆い隠す)。
ここまでで「e2e チェーン・敵対的データ」は着地した。ここからテストを追加する(production コード変更ゼロ)。コードは完全形。これが読み出しマイルストーン。
🛑 予測。
(price=250, qty=7)と(price=240, qty=99)を install。read_best_bidは何を返すか?(答え:(250, 7)。best bid = 最高価格であって最大数量でない。qty=99 は悪い価格 240 にあり候補にすら入らない。market sell が最初にぶつかる先 = 250。)
ステップで組み立てる
テストモジュールの import に Order, OrderId, AccountId, OrderType, Price, Qty, Side が含まれることを確認(レッスン5 を通して残しておいた)し、read_best_bid_returns_zero_when_no_clob_installed と openhl_precompiles_registers_clob_address の間に追加:
/// **Stage 9b end-to-end**: install a CLOB with a known bid, call the
/// precompile, observe the live data flow through to the EVM-visible
/// response. This is the moment custom EVM execution reads real
/// orderbook state.
#[test]
fn read_best_bid_returns_live_state_when_clob_installed() {
let _g = TEST_SERIALIZER.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let book = Arc::new(Mutex::new(Book::new()));
// Rest a buy @ 250 with qty 7
book.lock().unwrap().submit(Order {
id: OrderId(1),
account: AccountId(42),
side: Side::Buy,
qty: Qty(7),
order_type: OrderType::Limit { price: Price(250) },
});
// Rest another buy @ 240 (lower; shouldn't be picked as best bid)
book.lock().unwrap().submit(Order {
id: OrderId(2),
account: AccountId(43),
side: Side::Buy,
qty: Qty(99),
order_type: OrderType::Limit { price: Price(240) },
});
install_clob(book);
let result = read_best_bid(&[], 100_000, 0).expect("precompile must not error");
let price = U256::from_be_slice(&result.bytes[0..32]);
let qty = U256::from_be_slice(&result.bytes[32..64]);
assert_eq!(price, U256::from(250u64), "best bid is the 250 order, not 240");
assert_eq!(qty, U256::from(7u64), "qty at the best level is 7");
uninstall_clob();
}
要点:
- 冒頭で
TEST_SERIALIZER取得(uninstall_clob()は呼ばない — すぐ自分の CLOB を install するから、install_clobが原子的に置き換える)。 - order 2 個の 敵対的データ(
(240,99)が罠)。OrderIdは別々に(submitは OrderId をキーにするので使い回すと上書き)。 install_clob(book)でbookを move(Arc::cloneでない — install 後使わないので)。read_best_bid(&[], ...)を 直接呼ぶ(dispatch はレッスン5 で証明済み、ここは behavior を 1 assertion に絞る)。- assertion メッセージがドキュメント。
- 末尾で
uninstall_clob()(このモジュールで明示 cleanup するのはこのテストだけ — 空でない CLOB を install したまま終わるので、次の zero-output テストが拾わないように)。
答え合わせ
cd ~/code/openhl-reference && git checkout b635ef7
diff -u ~/code/my-openhl/crates/evm/src/precompiles/mod.rs ./crates/evm/src/precompiles/mod.rs
git checkout main
本レッスン後、precompiles/mod.rs は Stage 9b と バイト単位で同一(doc を変えていなければ)。git diff b635ef7 -- crates/evm が空になる。
合格基準
cargo test -p openhl-evm --release returns_live_state
cargo test -p openhl-evm --release # 43 個(既存 42 + 本レッスン 1)
→ pass。この ok 行が読み出しマイルストーン — custom EVM precompile が live matching engine の state を read し、データが EVM から見える出力 bytes までラウンドトリップする。よくあるミス: best_bid_with_qty が .iter().next_back()(最高価格は .next())/ 全レベルを sum(best level の queue.iter() のみ)/ install_clob(book) 後に book を使う(move 済み)。
まとめ(3行)
- read チェーン全体(Solidity→dispatch→precompile→live Book→encode→コントラクト)を 1 本の assert_eq! で e2e 証明する。
- 敵対的データ(
(250,7)+ 罠の(240,99))が「best=最高価格、最大数量でない」を証明 — 正しさと偶然を切り分ける最小 order 数は 2。 - dispatch(レッスン5)と behavior(レッスン6)を別テストに分割 — 片方の失敗がもう片方を覆い隠さない。