レッスン12 — 作ったもの、まだ stub のもの、次に行く先
問い
11 レッスンで何を作り、何がまだ stub か? そして「price-time-priority CLOB を作った」という選択は、perp DEX 設計の広い landscape のどこに位置するのか?
原理(最小モデル)
- CLOB は pure crate。
openhl-clobは EVM/consensus/async runtime に何も依存しない(レッスン1-8 でその境界を引いた)。約 800 行の Rust で、実 Reth-backed bridge に組み込まれた動く matching engine。 - bridge が 2 つの非対称世界を仲介。 pure な matching engine と、Reth が回す async + I/O ヘビーな EVM 基盤を
Mutex<...>越しに繋ぐ(レッスン9-11)。 - fill はまだ EVM 本流を横切らない parallel レーン。
pendingHashMap に座り payload に attach されるだけで、BlockExecutorは fill を知らない。この破線が Step 3(precompile) で実線になる。 - 最難関は matching でなく determinism。 submit の任意順序で同じ答えを生む保証。レッスン8 の proptest がテストしようと思わないバグを catch し、それが consensus に plug して safe な理由になる。
具体例
操作は 2 つ・観察可能な結果は 1 つ:
| 操作 | method | 内部 |
|---|---|---|
| Limit submit | Book::submit(Limit) | 反対側を price 以下/以上で辿りマッチ、残りを rest |
| Market submit | Book::submit(Market) | 反対側を任意価格で辿りマッチ、残りを破棄 |
| cancel | Book::cancel(id) | 両 side を scan、削除、空 level を drop |
| inspect | best_bid/best_ask/depth_* | read-only |
データフロー: submit_order → clob.submit → pending_fills.push(caller に FillResult)/ build_payload → drain pending_fills → payload に attach → payload_fills(id)。drain は forward-only。
失敗例(誤解)
「動く matching engine = 製品」は誤り。EVM-encoded fill / precompile / multi-market / 永続 state / cancel index / pre-trade risk / multi-validator agreement / liquidation — どれも未着手。動く matching engine は基礎であって製品ではない。
ここまでで「pure crate・bridge 仲介・determinism」は着地した。ここから全体像・市場構造・残った placeholder を振り返る。
🛑 セルフチェック。 次に進む前に 1 文で言えるか:maker bid@100 と taker sell@95 が cross したとき、約定価格はいくらで、それはなぜか?(答え: 100。fill は常に resting=maker の価格。price-time priority は resting 側に決定権がある。即答できなければ レッスン4・11 を読み直す。)
作ったシステム(全体像)
~/code/my-openhl/crates/
├── clob/ ← NEW crate(EVM/consensus/async 非依存の pure state machine)
│ └── src/
│ ├── types.rs L1-L2: newtype + record(~109 LOC)
│ └── book.rs L3-L8: Book + submit/cancel + unit×9 + proptest×3
└── evm/src/live_node.rs L9-L11: bridge が clob/pending_fills を持ち build で drain
[Reth EthereumNode] Engine API / Payload Builder / BlockExecutor(EVM 本流レーン)
▲(まだ未接続 — Step 3 で precompile 接続)
[LiveRethEvmBridge] submit_order → Mutex<Book> → Mutex<Vec<Fill>> → pending HashMap → payload_fills(id)
▲
[openhl-clob] Side/Price/Qty/Order/Fill + Book(BTreeMap<Reverse<Price>,VecDeque>) — pure
新規テスト 15 個(hand-trace 9 + proptest 3=768 シナリオ + integration 1)。workspace 計 39 テスト。fill は EVM 本流の 隣を並走するレーンで、BlockExecutor は Vec<Fill> を知らない(同じ PayloadId で identifier 紐づけのみ)。Step 3 がこの並走レーンを EVM execution path に交差させる。
まだ placeholder のもの(意図的な scope cut)
- EVM-executable transaction encoding — fill は parallel
Vec<Fill>のまま、block body の tx でない(= Step 3)。 - Custom EVM precompile — contract が CLOB state を read(
clob_read_best_bid)/ on-chain submit(clob_place_order)(= Step 3)。 - Funding rate state machine — mark vs index、定期 rebalancing(= Step 4)。
- 複数 market — 現状 orderbook 1 個。
HashMap<MarketId, Book>で機械的に拡張可。 - 永続 CLOB state — in-memory のみ。再起動で resting order 消失(snapshot/event-sourcing 要)。
- Cancel-by-id index — O(n) scan(レッスン6 で明示的に simplicity を選択)。~10k order 超で
HashMap<OrderId,(Side,Price)>を入れる。
市場構造 — あなたが本当に作ったもの
price-time-priority CLOB は perp DEX の 3 モデルの 1 つ:
- CLOB(作ったもの): MM が resting order を置き taker が約定。価格はこの venue 上の需給で決まる。銘柄ごとに継続 quoting が要る → retail flow がある銘柄で機能。
- RFQ(Variational, Paradigm): taker が quote request、dealer が JIT 応答し primary venue(CME/NYSE/別 CLOB)で hedge。価格を source venue から 持ってくる。idle 時の quoting コストがないので long tail でも economics が成立。
- AMM(GMX, 旧 dYdX vAMM): 流動性を curve に集約。cold-start に強いが tail で破綻。
CLOB が勝つのはローカルな需給が存在する場合に限る。 BTC/ETH/SOL/HYPE のような TradFi 的 primary venue を持たない資産では HL の CLOB が真に価格を決める。WTI/NVDA/SPY perp は取引時間中 primary 市場の arbitrage shadow にすぎない(RFQ も同様、両者とも CME/NYSE order book の downstream)。cold-start 非対称は構造的 — 200 RWA 銘柄のうち 10 銘柄にしか flow がなければ残り 190 は薄い板になる。RFQ は dealer が需要時だけ quote するのでこれを回避する。
「last look」: 「RFQ には last look があり CLOB にはない」は半分正しい。CLOB でも MM は taker が hit するより速く quote を cancel できる(HL docs の cancel prioritization)。あなたの CLOB はこれを実装していない(submit/cancel は first-come-first-served)。追加したいなら変更箇所は BFT engine の ordering rule であって matching engine でない。
位置づけ: あなたが作ったのは HL が crypto-native の top tier 銘柄 を pricing する engine — 現実に存在し経済的に重要な slice。RWA perp の long tail 用ではない(そちらは RFQ が構造的に向く)。問いは「CLOB か RFQ か」でなく「どの asset class のどの slice を、どの liquidity source で」。
Production-readiness チェックリスト
- 各
Fillを EVM tx として encode し BlockExecutor に route - custom precompile(
clob_read_best_bid/clob_place_order) - multi-market(
HashMap<MarketId, Book>) - 永続 state(snapshot + replay)
- cancel index(
HashMap<OrderId,(Side,Price)>で O(1)) - order-id 衝突チェック(現状 caller の unique 性を信頼)
- pre-trade risk チェック(margin 以下に追い込む order を matching 前に拒否)
- telemetry(throughput / fill latency / depth-of-book)
- multi-validator agreement(proptest determinism はローカル証明、ネット上は integration test が要る)
- liquidation engine(= Step 4 領域)
次に行く先
- rethlab 内: Step 3(precompile —
clob_read_best_bid+clob_place_order)/ Step 4(funding state machine)。 - rethlab 外:
psyto/openhlStage 9 ソース(crates/evm/src/precompiles/mod.rs)/ 参考 production engine(Project Serum, dYdX v4)/ proptest + QuickCheck 文献。
まとめ(3行)
- 約 800 行で、実 Reth-backed bridge に組み込まれた pure な price-time-priority CLOB を作った(EVM/consensus 非依存)。
- 最難関は matching でなく determinism — proptest がそれを守り、consensus に plug して safe な理由になる。
- fill はまだ EVM 本流を並走する parallel レーン。precompile で交差させるのが Step 3 — 動く matching engine は基礎であって製品ではない。