レッスン0 — OpenHL Precompile を作る(CLOB state をスマートコントラクトに接続する)
問い
前コース(CLOB)は bridge が matching engine を所有する地点で終わった。だが 約定はまだ並列リストにすぎず、同じ Reth node 上で動くスマートコントラクトからは見えない。CLOB の状態と EVM の状態は別世界にある。どうやってスマートコントラクトが CLOB を read/write できるようにするか?
注: OpenHL コースのコードブロックは原則として手元で実行可能な形で示す。ただし
<file>などのプレースホルダや答え合わせ用コマンドは、各レッスンの指示に従って置換してから実行すること。
原理(最小モデル)
- custom EVM precompile = EVM 内の固定 address で native Rust を実行する。 間に Solidity bytecode は挟まらない。caller からは固定 address への external call に見えるが、実装は自分が選んだ state にフルアクセスできる native Rust。「もう 1 つのスマートコントラクト」でなく「EVM から呼べる native 関数」。
- custom range は
0x0c00以降。 Ethereum は0x01〜0x0aを標準 precompile(ECDSA recovery / SHA-256 等)に予約済み。CLOB precompile は衝突しない並列 range に置く。 - read + write の 2 パス。
0x...0c1bで best bid を read、0x...0c1cで order を発注。これで CLOB は EVM の横の独立構造から、EVM が対話できる state 拡張 になる。 - precompile は bridge が所有するのと同じ CLOB instance に触れる。 これがチェーンを「Hyperliquid 型」にする本質 — perp matching engine が同じチェーン上のコントラクトから呼び出せる。
具体例
Solidity contract ──call(0x...0c1b)──► clob_read_best_bid → (price, qty) を 64-byte で返す
Solidity contract ──call(0x...0c1c, calldata)──► clob_place_order → decode → book.submit → 約定要約
ゴールは cargo test -p openhl-evm bridge_against_custom_evm_node_shares_clob_with_precompile --release — コントラクトの call が precompile 経由で order を発注し、既存 book とマッチし、約定が bridge へ流れる。
失敗例(誤解)
「Rust の HTTP サービスを並列に立てて、コントラクトから call() で best_bid を読めばいい」は誤り — consensus パスで破綻する。EVM 実行は決定的でなければならず、全 validator が同じ入力から同じ結果を計算する必要がある。外部 HTTP read は非決定的(ネットワーク遅延・サービス状態で結果が変わる)で、validator 間の合意を壊す。read は EVM 実行内で決定的に完結する native precompile でなければならない。「precompile を Solidity 関数のように考える」も誤り(precompile は固定 address の native Rust)。
ここまでで「precompile は EVM 内の決定的 native 関数」は着地した。ここから先はスコープ・前提・12レッスンのロードマップに入る。L1 以降は実際に Rust を書く。
🛑 予測。 Solidity から CLOB の
best_bidを読みたい。素朴な「Rust の HTTP サービスを並列に立ててcall()」がなぜ consensus パスで破綻するか — そこから read メカニズムはどんな形でなければならないと結論できるか?(答え: EVM 実行は決定的でないと validator が合意できない。外部 read は非決定的。→ read は EVM 内で決定的に完結する native precompile であるべき。)
終了時に手にするもの
新規 crates/evm/src/precompiles/ モジュール:
- 既知 EVM address に登録された custom precompile 2 個:
clob_read_best_bid(read、(price,qty)を 64-byte で返す)/clob_place_order(write、calldata から order を decode → CLOB submit → 約定要約を返す) - custom EVM machinery(
openhl_evm.rs)— Reth executor に precompile を組み込むEvmFactory+ExecutorBuilder - Bridge 統合 —
LiveRethEvmBridgeが custom EVM 付き Reth node を spawn し、precompile への call が bridge 所有の同じ CLOB instance に触れる
end-to-end test は ~3 秒(Reth bootstrap → precompile trigger → 約定 assert)。
終了時にも手にしないもの(意図的な scope cut)
- 約定を実 EVM tx として block body に encode(約定は payload に attach された並列リストのまま。read/write から 見える が block body の一部ではない)
- Funding state machine(= Step 4)
- Liquidation / oracle / perp 固有 math
- Multi-market precompile(Stage 9 は CLOB 1 つ。production は market ごと 1 precompile か market-id calldata)
「チェーンのどこかに orderbook がある」→「チェーンそのものが orderbook + EVM」への大きな capability ジャンプ。ループを完全に閉じる(約定を tx として block body に戻す)のは下流。
前提
- CLOB(Step 2)完了 —
LiveRethEvmBridge<P>にclob/pending_fills/submit_order/payload_fills/pending_fill_countが揃っていること。 - Rust 1.95+。
- REVM に trait レベルで慣れていること(
Precompile/PrecompileFn/Precompiles型。未見なら revm-precompile docs を一読)。 - スレッド境界を越えた共有 state に
Arc<Mutex<T>>。
不要: EvmFactory/ExecutorBuilder 予備知識(L1-2 で説明)/ Solidity(raw calldata でテスト)。
セットアップ確認(今やる)
cd ~/code/openhl-reference
git fetch origin
git log --oneline | grep -E "(1761d4d|b635ef7|a8823a1|d19ba1b|2f796c3|2ba97c6)"
# Stage 9a〜9e の SHA が見えるはず
cd ~/code/my-openhl
cargo test -p openhl-evm clob_fills_flow_into_payload --release 2>&1 | tail -5
# 期待: Step 2 のマイルストーンテストが pass
12レッスンのロードマップ
| # | build するもの | 終了時テスト |
|---|---|---|
| 0 | Orientation(本レッスン) | セットアップ確認 |
| 1 | openhl_evm.rs — EvmFactory パターン + 依存 | cargo check -p openhl-evm |
| 2 | precompiles/mod.rs — hardcoded read precompile + registry | precompile がコンパイル |
| 3 | OpenHlExecutorBuilder + NodeBuilder 統合; call する smoke test | precompile_is_callable_via_registry |
| 4 | install_clob() — Arc-shared CLOB state | bridge が shared state でコンパイル |
| 5 | read precompile を live CLOB state に接続 | precompile が実 best_bid を返す |
| 6 | end-to-end: read が bridge.submit_order の結果を反映 | integration test pass |
| 7 | clob_place_order signature + calldata decode | precompile が正しく decode |
| 8 | book.submit + 約定要約を返す | precompile が正しく write |
| 9 | install_fill_sink() — precompile 約定が bridge の pending_fills に流れる | precompile 約定が bridge に届く |
| 10 | bridge が custom-EVM Reth node に spawn | full pipeline test pass |
| 11 | Capstone | (recap) |
マイルストーンは レッスン10 — live Reth node 上で EVM から呼べる CLOB(コントラクトが precompile を call → matching → 約定が bridge を経由して payload に現れる)。レッスン11 で「まだ何が足りないか(約定はまだ EVM tx でない)」を名指す。
答え合わせの規律
| Lessons | Stage | SHA |
|---|---|---|
| L1〜3 | 9a + 9e | 1761d4d / 2ba97c6 |
| L4〜6 | 9b | b635ef7 |
| L7〜8 | 9c | a8823a1 |
| L9 | 9c+ | d19ba1b |
| L10 | 9d | 2f796c3 |
# レッスン範囲に応じて checkout:
# 1761d4d / b635ef7 / a8823a1 / d19ba1b / 2f796c3 / 2ba97c6
cd ~/code/openhl-reference && git checkout 1761d4d
diff -u ~/code/my-openhl/crates/evm/src/precompiles/mod.rs ./crates/evm/src/precompiles/mod.rs
本質(型・制御フロー)が一致していればよい。
合格基準
cargo test ... bridge_against_custom_evm_node_shares_clob_with_precompileを通せる(コース完走時)。- precompile が「EVM から呼べる native 関数」であることを説明できる。
- なぜ外部 HTTP read が consensus で破綻するか(非決定性)を 1 文で言える。
まとめ(3行)
- custom EVM precompile は固定 address(
0x0c00+)で native Rust を決定的に実行する — Solidity bytecode なし、external call と同じ呼び出し shape。 - read(
clob_read_best_bid)+ write(clob_place_order)で CLOB を EVM の state 拡張にする — これがチェーンを「Hyperliquid 型」にする本質。 - precompile は bridge 所有の同じ CLOB instance に触れる。約定の EVM-tx 化(block body への encode)はまだ scope 外。