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

レッスン11 — Capstone — 築いたもの、先送りしたもの、次にくるもの

問い

EVM ↔ CLOB のアーキテクチャを、記憶を頼りにホワイトボードへ描けるか? v0 で 意図的に 先送りした項目を名指し、なぜスコープ外かを説明できるか? このレッスンにコードはなし — メンタルモデルだけだ。

原理(最小モデル)

全体のテーゼは 1 行: Arc が物理的に 1 つしかない。 precompile も bridge も同じ Arc を握り、同じ Book / 同じ Vec<Fill> を read/write する。CLOB_STATEFILL_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_bidCLOB_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) — プラガブルなシーム: OpenHlEvmFactoryalloy_evm::EvmFactory)+ OpenHlExecutorBuilderreth_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 化済み:

  1. RPC eth_call ラウンドトリップ — Rust 直接呼びは証明、JSON-RPC→Solidity 経路は Reth の責務(RPC サーバ / tx simulation / dispatch)。見直し: Reth を大幅 fork する/registry インターフェースが変わるメジャーアップグレード時。
  2. マルチバリデータ deterministic OrderId — プロセスグローバルカウンタは 2 validator で別々の ID を採り book が silent に分岐(エラーも crash も出ない)。要 keccak(tx_hash, call_index) か block-scoped state。見直し: マルチバリデータ deployment 前(NEXT_ORDER_ID doc で名指し済み)。
  3. Transaction-scoped state shadowingplace_order 成功後に tx revert しても Book は rollback しない(Book は EVM storage 外の Arc 内)。要 journaling か virtual モード。single-actor なら OK、DeFi composability では必須。
  4. staticcall での mutation 拒否 — EVM は static フラグを precompile に渡さず、STATICCALL(0x...0c1c) でも抵抗なく book に書けてしまう。PrecompileFn シグネチャ拡張(revm fork)が必要。見直し: 監査が攻撃 vector としてフラグしたとき。

次に来るもの

複雑度順に 4 つ:

  1. best_ask precompile(1 日)read_best_bid の sell 側ミラー。形は同じ方向だけ逆、~30 行でほぼ機械的。
  2. clob_depth_at_price precompile(2-3 日)(side, price) → その価格レベルの合計 qty。スリッページ見積もり用。calldata に入力パラメータを含む新パターン。
  3. clob_cancel_order precompile(1 週間)(order_id, account) → 削除成否。認可問題が出る(EVM の msg.sender は呼び出しコントラクトで、元アカウントでない)。要署名スキームか事前登録の認可マッピング。
  4. 約定を 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 トレーディングプリミティブが手元にある。