レッスン12 — ドリル: ZeroDb を実装して Revm の状態読みを観測する
問い
トレイトの形と 3 参照実装は読んだ。自分のものを作る — 「残高ゼロ、スロットゼロ、コードなし」と常に答える Database 最小版を 4 メソッドスタブ、Revm に差し込み、tx 走らせ、EVM が実際に発行する読みを観察。何が見えるか?(ネタバレ: 想像より少ない、EVM は状態に対して非常に怠惰。)
原理(最小モデル)
ZeroDb実装.type Error = std::convert::Infallible(失敗できない慣用型)+ 4 メソッド全部Ok(default())。- 4 メソッド戻り値.
basic→Some(AccountInfo::default())(残高 0 / nonce 0 / code hash 空)、code_by_hash→Bytecode::default()(長さ 0)、storage→StorageValue::ZERO、block_hash→B256::ZERO。 basicがOk(None)でなくOk(Some(AccountInfo::default()))の理由.None= アカウント不存在シグナル、Some(default)= 存在するが空 = Ethereum 上の新アカウント挙動と一致。- 5 EVM 操作の予測.
BALANCE= 0 /SLOAD= 0 /EXTCODESIZE= 0 / コードなしCALL= 実行なし成功 /BLOCKHASH(N)=B256::ZERO。 SSTOREがZeroDbメソッドを呼ばない理由. 書きはDatabaseCommit経由(意図的に未実装)、既存値読みはstorage(0 を返す)、新値はジャーナリング層にステージ、ZeroDbには届かない。- 計装で観察するもの. 標準 tx で
basic(tx.from)1 回 +storage(tx.to, 0)1 回 = 2 読み、それで全部、Revm 周りハーネス全体が見えた。 - tx revert vs
Database::Errorの違い. Revert = コンセンサス、Database エラー = インフラ、Revm はSelf::Errorを呼び出し元に bubble up(revert 変換せず)、ハーネスがリトライ / ログ / 伝播を選ぶ。
具体例
ZeroDb:
struct ZeroDb;
impl Database for ZeroDb {
type Error = std::convert::Infallible;
fn basic(&mut self, _: Address) -> Result<Option<AccountInfo>, Self::Error> {
Ok(Some(AccountInfo::default()))
}
fn code_by_hash(&mut self, _: B256) -> Result<Bytecode, Self::Error> {
Ok(Bytecode::default())
}
fn storage(&mut self, _: Address, _: StorageKey) -> Result<StorageValue, Self::Error> {
Ok(StorageValue::ZERO)
}
fn block_hash(&mut self, _: u64) -> Result<B256, Self::Error> {
Ok(B256::ZERO)
}
}
Revm に繋ぐ:
use revm::{database_interface::Database, Evm, primitives::*};
fn main() {
let mut evm = Evm::builder()
.with_db(ZeroDb)
.build();
// PUSH1 0x42 PUSH1 0x00 SSTORE STOP
let bytecode = hex::decode("604260005500").unwrap();
let result = evm.transact(&bytecode);
println!("{:?}", result);
}
計装版:
fn basic(&mut self, addr: Address) -> Result<Option<AccountInfo>, Self::Error> {
println!("[ZeroDb] basic({addr})");
Ok(Some(AccountInfo::default()))
}
fn storage(&mut self, addr: Address, key: StorageKey) -> Result<StorageValue, Self::Error> {
println!("[ZeroDb] storage({addr}, {key})");
Ok(StorageValue::ZERO)
}
PUSH1 0x42 PUSH1 0x00 SSTORE STOP で観測:
- 送信者の nonce/残高検証で
basic(tx.from)1 回 - SSTORE 返金会計のためのスロット読み込みで
storage(tx.to, 0)1 回
合計 2 読み。
失敗例(誤解)
「SSTORE は storage を呼ぶ」— 間違い。書きは DatabaseCommit 経由、ZeroDb は DatabaseCommit 未実装 → 書きは到達しない。既存値読みのみ storage、新値はジャーナリング層にステージ。
「コードなし CALL は revert する」— 間違い。EOA への CALL は 有効な Ethereum 操作、値転送して return、revert なし。
「Database::Error は revert に変換される」— 間違い。Revert = コンセンサス、Database エラー = インフラ、Revm は Self::Error を呼び出し元に bubble up(revert 変換せず)、ハーネスがリトライ / ログ / 伝播を選ぶ。だから Error はあなたの型、Revm のではない。
ステップで組み立てる
Step 1: ZeroDb 4 メソッドスタブ
全部 Ok(default())、type Error = Infallible。
Step 2: 5 EVM 操作の予測
BALANCE / SLOAD / EXTCODESIZE / コードなし CALL / BLOCKHASH。
Step 3: 1-tx ブロック実行
PUSH1 0x42 PUSH1 0x00 SSTORE STOP = 604260005500、成功確認。
Step 4: println! で計装
標準 tx で basic(tx.from) 1 + storage(tx.to, 0) 1 = 2 読み観察。
Step 5: 失敗ドリル(オプション)
Infallible をカスタムエラー型に置換、特定キーで Err 返す PickyDb で revert vs Database エラーの違いを観察。
答え合わせ
basicがSome(default)を返す理由:None= アカウント不存在シグナル、Some(default)= 存在するが空 = Ethereum 上の新アカウント挙動と一致。EXTCODEHASH が未知アカウントに特殊な意味を持つので、不存在ではなく空アカウントを返すことが正しいセマンティクス。SSTOREがZeroDbメソッドを呼ばない理由: SSTORE = 書きで、読みではない、書きはDatabaseCommit経由(ZeroDbは意図的未実装)。スロット既存値はstorage(0 返す、問題なし)で読まれる、新値 0x42 は Revm のジャーナリング層にステージ →ZeroDbには届かない、tx 無事 commit。- revert vs
Database::Errorの違い: Revert = コンセンサス(プロトコル定義の失敗、ガス消費、状態ロールバック)、Database エラー = インフラ(RPC タイムアウト / ロック poison / ディスク I/O)。Revm はSelf::Errorを呼び出し元に bubble up(revert 変換せず)、ハーネスがリトライ / ログ / 伝播を選ぶ。だからErrorはあなたの型、Revm のではない。
合格基準
ZeroDb4 メソッドの戻り値を即書ける。- 5 EVM 操作の予測(
BALANCE/SLOAD/EXTCODESIZE/ CALL /BLOCKHASH)を即答できる。 SSTOREがZeroDbのstorageを呼ばない理由を 1 文で説明できる。- 標準 tx で観測される 2 読み(
basic(tx.from)+storage(tx.to, 0))を即答できる。 - revert と
Database::Errorの違い(コンセンサス vs インフラ)を即答できる。
まとめ(3行)
ZeroDb= 「残高ゼロ、スロットゼロ、コードなし」最小実装、4 メソッド全部Ok(default())+type Error = Infallible。- 計装で観測 = 標準 tx 2 読み(
basic(tx.from)+storage(tx.to, 0))、SSTORE は書きなのでDatabaseCommit経由(未実装)=ZeroDbに届かない、EVM は状態に対して非常に怠惰。 - Revert(コンセンサス)vs
Database::Error(インフラ)の違い = Revm が bubble up(変換せず)、ハーネスが対応決定 = だからErrorはあなたの型。