レッスン11 — Capstone — 築いたもの、先送りしたもの、次にくるもの
問い
EVM ↔ CLOB のアーキテクチャを、記憶を頼りにホワイトボードへ描けるか? v0 で 意図的に 先送りした項目を名指し、なぜスコープ外かを説明できるか? このレッスンにコードはなし — メンタルモデルだけだ。
原理(最小モデル)
全体のテーゼは 1 行: Arc が物理的に 1 つしかない。 precompile も bridge も同じ Arc を握り、同じ Book / 同じ Vec<Fill> を read/write する。CLOB_STATE と FILL_SINK は「その Arc をどこからでも取れる shared register」にすぎない。翻訳レイヤなし、シリアライゼーションの往復なし、メモリだけ。
具体例
┌─────────────────────────────────────────────┐
│ LiveRethEvmBridge │
│ clob: Arc<Mutex<Book>> │
│ pending_fills: Arc<Mutex<Vec<Fill>>> │
└──────┬───────────────┬───────────────────────┘
install_ │ │ install_
clob ▼ ▼ fill_sink
┌─────────────────────────────────────┐
│ precompiles module (process-global)│
│ CLOB_STATE: RwLock<Option<…>> │
│ FILL_SINK: RwLock<Option<…>> │
└──────┬───────────────┬──────────────┘
read_best_ ▼ ▼ place_order
bid ┌─────────────────────────────────────┐
│ Reth EVM (via OpenHlEvmFactory) │
│ registry: 0x...0c1b → read_best_bid│
│ 0x...0c1c → place_order │
└──────┬──────────────────────────────┘
▼
┌─────────────────────────────────────┐
│ Solidity: staticcall(0x...0c1b,"") │
│ call(0x...0c1c, abi…) │
└─────────────────────────────────────┘
上→下: bridge がデータを所有、precompile モジュールが process-global handle で公開、EVM が dispatch、Solidity は ecrecover と同じ感覚で同じアドレスを叩く。下→上: STATICCALL(0x...0c1b) → registry → read_best_bid → CLOB_STATE → bridge の submit_order が書くのと同じ Arc<Mutex<Book>>。このマップを記憶から再現できれば、precompile レイヤを脳内で再構築できている。
失敗例(誤解)
「precompile ごとに自前 state を持てばシンプル」は誤り — 機能的に切り離され、read precompile と write precompile が別々の Book を見てラウンドトリップが壊れる。「EVM と CLOB の間に翻訳/シリアライズ層がある」も誤り — 同一プロセスの同一 Arc を共有するだけで変換は一切ない。「Arc が物理的に 1 つ」を外すと、このアーキテクチャは成立しない。
各モジュールが届けたもの
- Custom EVM bootstrap(レッスン1〜3) — プラガブルなシーム:
OpenHlEvmFactory(alloy_evm::EvmFactory)+OpenHlExecutorBuilder(reth_node_builder::ExecutorBuilder)+openhl_precompiles(hardfork ごとに自アドレスを足す、OnceLockキャッシュ)。.with_components(EthereumNode::components().executor(OpenHlExecutorBuilder))で boot。 - Read precompile(レッスン4〜6) —
0x...0c1b、空 calldata → 64-byte(price, qty)。CLOB_STATE: RwLock<Option<Arc<Mutex<Book>>>>+ install/uninstall/current_best_bid。 - Write precompile(レッスン7〜8) —
0x...0c1c、128-byte(account, side, price, qty)→ 32-byte(order_id)。NEXT_ORDER_ID: AtomicU64(1 始まり、0=rejected sentinel)、4 つの rejection path。 - Bridge統合(レッスン9〜10) —
FILL_SINK: RwLock<Option<Arc<Mutex<Vec<Fill>>>>>、new()が両 global に install、約定が bridge の drain を通って次のbuild_payloadに届く。合計 48 tests(unit 47 + integration 1)。
正直に先送り(意図的な scope cut)
4 項目、どれも実プロダクションギャップでコード上に doc 化済み:
- RPC
eth_callラウンドトリップ — Rust 直接呼びは証明、JSON-RPC→Solidity 経路は Reth の責務(RPC サーバ / tx simulation / dispatch)。見直し: Reth を大幅 fork する/registry インターフェースが変わるメジャーアップグレード時。 - マルチバリデータ deterministic OrderId — プロセスグローバルカウンタは 2 validator で別々の ID を採り book が silent に分岐(エラーも crash も出ない)。要
keccak(tx_hash, call_index)か block-scoped state。見直し: マルチバリデータ deployment 前(NEXT_ORDER_IDdoc で名指し済み)。 - Transaction-scoped state shadowing —
place_order成功後に tx revert しても Book は rollback しない(Book は EVM storage 外の Arc 内)。要 journaling か virtual モード。single-actor なら OK、DeFi composability では必須。 staticcallでの mutation 拒否 — EVM は static フラグを precompile に渡さず、STATICCALL(0x...0c1c)でも抵抗なく book に書けてしまう。PrecompileFnシグネチャ拡張(revm fork)が必要。見直し: 監査が攻撃 vector としてフラグしたとき。
次に来るもの
複雑度順に 4 つ:
best_askprecompile(1 日) —read_best_bidの sell 側ミラー。形は同じ方向だけ逆、~30 行でほぼ機械的。clob_depth_at_priceprecompile(2-3 日) —(side, price)→ その価格レベルの合計 qty。スリッページ見積もり用。calldata に入力パラメータを含む新パターン。clob_cancel_orderprecompile(1 週間) —(order_id, account)→ 削除成否。認可問題が出る(EVM のmsg.senderは呼び出しコントラクトで、元アカウントでない)。要署名スキームか事前登録の認可マッピング。- 約定を EVM event として emit(2 週間) —
eth_getLogs/ event filter で subscribe 可能に(ERC-20 transfer と同じ要領)。precompile からの event emit は revm API が扱いづらく、PrecompileFn拡張=小さな fork が要りうる。インパクト大・摩擦大。
このコースの位置(L1 Architect トラック)
- コース1〜5(Reth internals): pipeline / payload building / NodeBuilder / evm crate / RPC。
- Step 1(Consensus)/ Step 2(CLOB): Malachite コンセンサス統合 → マッチングエンジン。
- Step 3(Precompiles、本コース): カスタム precompile で EVM ↔ CLOB を橋渡し。Reth のプラガブルな EVM シームに触れる最初のコース。
- Step 4(Funding): funding rate 機構。本コースの precompile パターンの上に積む。
- Step 5(Liquidation): capstone — 実行可能な openhl ノードとサンプルトレーディングコントラクトを出荷。
ここで内在化したパターンは CLOB precompile を超えて一般化する: カスタム EVM の「1 スロット差し替え」(EvmFactory + ExecutorBuilder + .with_components)/ precompile state の「プロセスグローバル Arc」(fn ポインタは closure を持てないから)/ schema-first プロトコル設計(契約は calldata layout にあり関数 body にない)/ 敵対的テストデータ / doc 上で正直に scope を切ること。
最終答え合わせ
cd ~/code/openhl-reference
git checkout d19ba1b
diff -u ~/code/my-openhl/crates/evm/ ./crates/evm/ --recursive
レッスン11 を終えると、crates/evm/ ディレクトリ全体が openhl の d19ba1b HEAD と byte-identical に一致する。5 つの commit(9a / 9b / 9c / 9c+ / 9d)を手で再現し、各行がなぜそこにあるかを完全に理解した上で。
main に戻す:
git checkout main
まとめ(3行)
- 全アーキテクチャは 1 行に集約: precompile も bridge も 物理的に 1 つの Arc を共有し、
CLOB_STATE/FILL_SINKはその Arc を取り出す shared register にすぎない(翻訳なし・シリアライズなし・メモリだけ)。 - 4 モジュールの成果: プラガブル EVM シーム → live state read → live state write → 約定の payload 還流。48 tests が部品 + 組み込みを証明する。
- 4 つの先送り(RPC roundtrip / マルチバリデータ OrderId / tx-scoped rollback / staticcall mutation 拒否)はすべて doc 化済みの 意図的 scope cut — 出荷可能な、Reth ベースのカスタム L1 トレーディングプリミティブが手元にある。