FABRKNT
Inside Revm — EVM エンジンを読む
Revmの心臓部
レッスン 10 / 17·CONTENT10 分25 XP
コース
Inside Revm — EVM エンジンを読む
レッスンの役割
CONTENT
順序
10 / 17

レッスン9 — Database トレイトを組み立てる — 読み API

問い

EVM が SLOAD を実行したとき、値はどこから来るか? Revm からではない — Revm は 実行エンジンであり状態は持たない。Database トレイトの実装が Revm を何にでも繋ぐ方法 — テスト用 HashMap、フォークメインネット JSON-RPC、本物 Reth の MDBX、エキゾチック L1 のシャード網。同じ 4 メソッド、4 種バックエンド。どう組み立てるか?

原理(最小モデル)

  • 本物の形. #[auto_impl(&mut, Box)] pub trait Database { type Error: DBErrorMarker; fn basic(&mut self, address: Address) -> Result<Option<AccountInfo>, Self::Error>; fn code_by_hash(&mut self, code_hash: B256) -> ...; fn storage(&mut self, address, index) -> ...; fn block_hash(&mut self, number: u64) -> ...; }
  • 4 ステップ積み上げ. 素朴版(状態を内部所有)→ トレイト後ろに押し出す(依存性逆転)→ メソッド正しくグループ化(basic で AccountInfo 束、code_by_hash 別出し)→ Result + 関連型 Error#[auto_impl(&mut, Box)]
  • &mut self の理由. キャッシュ変更(ネットワーク実装が読み込み結果をキャッシュ)、&self だと RwLock / RefCell ラップ強制 = オーバーヘッド。
  • basicAccountInfo を返す理由. balance + nonce + code hash 束 = 1 ラウンドトリップで 3 データOption で未知アカウントを綺麗にシグナル。
  • code_by_hash 別出しの理由. コントラクトコードは コンテンツアドレス指定、人気バイトコード(DEX ルーター)が多アドレスで共有 → ハッシュキャッシュで自動デデュプ、実行必要時のみバイト実体化。
  • type Error: DBErrorMarker の理由. 各実装が独自エラー型を選べる + Revm が後から制約厳格化(Send / Sync)するフック、固定 enum は狭すぎ or 広すぎ。
  • auto_impl リスト 2 種(&mut, Box). Database メソッドは &mut selfArc<T>&T しか出さないので不可 → DatabaseRef 仲間トレイトで解決(次レッスン)。

具体例

ステップ 0 — 素朴版:

pub struct Revm {
    stack: Vec<U256>,
    storage: HashMap<(Address, U256), U256>,
    accounts: HashMap<Address, AccountInfo>,
}

問題: フォークメインネット(リモート RPC)/ 本番 MDBX / 独自スキーマ各々で状態取得コード違う → Revm を 3 通りにフォークしたくない。

ステップ 1 — 依存性逆転:

pub trait Database {
    fn storage(&mut self, address: Address, key: U256) -> U256;
    fn balance(&mut self, address: Address) -> U256;
    fn code(&mut self, address: Address) -> Vec<u8>;
    fn block_hash(&mut self, number: u64) -> B256;
}

インタープリターが db: &mut dyn Database を取る、ストレージ非所有、誰でも実装可能。&mut self の理由 = キャッシュ変更(ネットワーク実装が読み込み結果をキャッシュ)、&self だと RwLock / RefCell ラップ強制。

ステップ 2 — メソッドを正しくグループ化:

fn basic(&mut self, address: Address) -> Result<Option<AccountInfo>, Self::Error>;
fn code_by_hash(&mut self, code_hash: B256) -> Result<Bytecode, Self::Error>;

balance / code / nonce 分離 → 1 アカウント 3 ラウンドトリップ。AccountInfo で束 → 1 ラウンドトリップで 3 データOption で未知アカウント綺麗にシグナル。コード本体は code_by_hashコンテンツアドレス指定、人気バイトコード(DEX ルーター)が多アドレスで共有 → ハッシュキャッシュで自動デデュプ。

ステップ 3 — Result + 関連型 Error:

fn basic(&mut self, ...) -> Result<Option<AccountInfo>, Self::Error>;
// ...
type Error: DBErrorMarker;

ネットワーク実装は失敗する(RPC タイムアウト / MDBX 古いロック / Arc poisoned)。Self::Error = 各実装が独自エラー型を選べる、DBErrorMarker は無内容な制約(拡張に開いた接点)、Revm が後から Send / Sync 追加するフック。固定 enum だと reqwest::Error / serde_json::Error / タイムアウト / パースエラーを潰す羽目 + 新失敗モードごとに Revm PR 必要。

ステップ 4 — #[auto_impl(&mut, Box)]:

impl<T: Database> Database for &mut T {
    type Error = T::Error;
    fn basic(&mut self, addr: Address) -> Result<Option<AccountInfo>, T::Error> {
        (**self).basic(addr)
    }
    // ... 残り 3 メソッドも全部同じパターン
}
impl<T: Database> Database for Box<T> { /* ... 同じ 4 メソッド ... */ }

属性なしだと 4 メソッド × 2 ラッパー = 8 個の転送ボイラープレート。#[auto_impl(&mut, Box)] がこれを自動生成。Arc<MyDb> 不可(Arc<T>&T のみ、&mut self メソッド不可)→ 仲間トレイト DatabaseRef で次レッスンに解決。

ステップ 5 — 本物:

#[auto_impl(&mut, Box)]
pub trait Database {
    type Error: DBErrorMarker;
    fn basic(&mut self, address: Address) -> Result<Option<AccountInfo>, Self::Error>;
    fn code_by_hash(&mut self, code_hash: B256) -> Result<Bytecode, Self::Error>;
    fn storage(&mut self, address: Address, index: StorageKey)
        -> Result<StorageValue, Self::Error>;
    fn block_hash(&mut self, number: u64) -> Result<B256, Self::Error>;
}

失敗例(誤解)

&self で十分、不変なら」— 間違い。本物実装(フォークメインネット、RPC バックエンド)はキャッシュ必要 → &mut self で素直、&self だと RwLock / RefCell 強制 = 各実装に余計オーバーヘッド。共有並列が本当に必要な場合は DatabaseRef(次レッスン)。

balance / code / nonce を別メソッドに」— 間違い1 アカウント = 1 ラウンドトリップ、ネットワーク実装で各々別 RPC = 3 倍レイテンシ。basicAccountInfo を束、code は code_by_hashコンテンツアドレス指定(自動デデュプ)。

type Error = DatabaseError の固定 enum で十分」— 致命的reqwest::Error / serde_json::Error / ネットワークタイムアウト / パースエラーを閉じた enum バリアントに 潰す 羽目 + 新失敗モードごとに Revm への PR 必要。関連型ならあなたのエラーはあなたのもの

ステップで組み立てる

Step 1: 素朴版(状態内部所有)の 3 問題

フォークメインネット + 本番 MDBX + 独自スキーマで状態取得コード違う、Revm 3 フォーク不可避。

Step 2: トレイトに押し出す(依存性逆転)

db: &mut dyn Database、Revm はストレージ非所有、誰でも実装可能。

Step 3: メソッドを正しくグループ化

basicAccountInfo 束(1 ラウンドトリップ 3 データ)、code_by_hash 別出し(コンテンツアドレス指定)。

Step 4: Result + 関連型 Error

Self::Error: DBErrorMarker、各実装独自エラー型、Revm が後から制約厳格化可能。

Step 5: #[auto_impl(&mut, Box)]

転送実装自動生成、MyDb: Database なら &mut MyDbBox<MyDb> も自動実装、ユーザー側ボイラープレートゼロ。

答え合わせ

  • &mut self を選ぶ理由: 本物実装はキャッシュ必要 = ネットワーク実装が storage(addr, key) 最初の呼び出しでネットワーク叩き、以降ローカルキャッシュから返す。&mut でキャッシュ変更を直接、&self だと RwLock / RefCell ラップ強制 = 各実装にオーバーヘッド。共有並列アクセスが必要なケース(並列 EVM、Arc 共有)は DatabaseRef 仲間トレイト(次レッスン)で別途。
  • basiccode_by_hash の分離: basic = balance + nonce + code hash 束 = 1 アカウント 1 ラウンドトリップOption で未知アカウント綺麗にシグナル(EXTCODEHASH 特殊意味)。code_by_hash = コンテンツアドレス指定、人気バイトコード(DEX ルーター)が多アドレスで共有 → ハッシュキャッシュで 自動デデュプ、実行必要時のみバイト実体化。
  • 関連型 Error の必要性: Revm にあなたのエラー形状は分からない(RPC エラー / ディスク I/O / ロック poison 全部形違う)。固定 enum は 狭すぎ(本物エラーを潰す)or 広すぎ(Revm が 50 バリアント処理)。マーカー DBErrorMarker = 意図文書化 + Revm が後から Send / Sync 制約追加するフック、実装を壊さずに拡張可能。

合格基準

  • 本物の Database トレイト 4 メソッド + 関連型 Error + auto_impl を即書ける。
  • &mut self の理由(キャッシュ + 余計ラップ回避)を 1 文で説明できる。
  • basic / code_by_hash 分離の 2 利点(1 ラウンドトリップ束 + コンテンツアドレス指定デデュプ)を即答できる。
  • 関連型 Error の必要性を即答できる。
  • auto_impl(&mut, Box)Arc を含まない理由(&mut self メソッド + Arc<T>&T のみ出す)を言える。

まとめ(3行)

  • 本物の Database トレイト = 4 メソッド(basic / code_by_hash / storage / block_hash)+ 関連型 Error: DBErrorMarker + #[auto_impl(&mut, Box)]
  • 5 ステップ積み上げ = 素朴版(状態内部)→ トレイトに押し出す → メソッドグループ化 → Result + 関連型 → auto_impl、各ステップに動機(フォーク / 1 ラウンドトリップ / 拡張可能エラー / 転送ボイラープレートゼロ)。
  • Arc 不可問題(&mut self + Arc<T>&T のみ)は次レッスンで DatabaseRef 仲間トレイトが解決。