レッスン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ラップ強制 = オーバーヘッド。basicがAccountInfoを返す理由. 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 self、Arc<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 倍レイテンシ。basic で AccountInfo を束、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: メソッドを正しくグループ化
basic で AccountInfo 束(1 ラウンドトリップ 3 データ)、code_by_hash 別出し(コンテンツアドレス指定)。
Step 4: Result + 関連型 Error
Self::Error: DBErrorMarker、各実装独自エラー型、Revm が後から制約厳格化可能。
Step 5: #[auto_impl(&mut, Box)]
転送実装自動生成、MyDb: Database なら &mut MyDb と Box<MyDb> も自動実装、ユーザー側ボイラープレートゼロ。
答え合わせ
&mut selfを選ぶ理由: 本物実装はキャッシュ必要 = ネットワーク実装がstorage(addr, key)最初の呼び出しでネットワーク叩き、以降ローカルキャッシュから返す。&mutでキャッシュ変更を直接、&selfだとRwLock/RefCellラップ強制 = 各実装にオーバーヘッド。共有並列アクセスが必要なケース(並列 EVM、Arc 共有)はDatabaseRef仲間トレイト(次レッスン)で別途。basicとcode_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仲間トレイトが解決。