FABRKNT
Step 3. Precompiles — EVM 拡張による CLOB ステートのスマートコントラクト連携
Read precompile
レッスン 7 / 12·CONTENT30 分60 XP
コース
Step 3. Precompiles — EVM 拡張による CLOB ステートのスマートコントラクト連携
レッスンの役割
CONTENT
順序
7 / 12

レッスン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_installedopenhl_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)bookmoveArc::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)を別テストに分割 — 片方の失敗がもう片方を覆い隠さない。