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

レッスン12 — ドリル: ZeroDb を実装して Revm の状態読みを観測する

問い

トレイトの形と 3 参照実装は読んだ。自分のものを作る — 「残高ゼロ、スロットゼロ、コードなし」と常に答える Database 最小版を 4 メソッドスタブ、Revm に差し込み、tx 走らせ、EVM が実際に発行する読みを観察。何が見えるか?(ネタバレ: 想像より少ない、EVM は状態に対して非常に怠惰。)

原理(最小モデル)

  • ZeroDb 実装. type Error = std::convert::Infallible(失敗できない慣用型)+ 4 メソッド全部 Ok(default())
  • 4 メソッド戻り値. basicSome(AccountInfo::default())(残高 0 / nonce 0 / code hash 空)、code_by_hashBytecode::default()(長さ 0)、storageStorageValue::ZEROblock_hashB256::ZERO
  • basicOk(None) でなく Ok(Some(AccountInfo::default())) の理由. None = アカウント不存在シグナル、Some(default) = 存在するが空 = Ethereum 上の新アカウント挙動と一致。
  • 5 EVM 操作の予測. BALANCE = 0 / SLOAD = 0 / EXTCODESIZE = 0 / コードなし CALL = 実行なし成功 / BLOCKHASH(N) = B256::ZERO
  • SSTOREZeroDb メソッドを呼ばない理由. 書きは 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 読み。

失敗例(誤解)

SSTOREstorage を呼ぶ」— 間違い。書きは DatabaseCommit 経由、ZeroDbDatabaseCommit 未実装 → 書きは到達しない。既存値読みのみ 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 エラーの違いを観察。

答え合わせ

  • basicSome(default) を返す理由: None = アカウント不存在シグナル、Some(default) = 存在するが空 = Ethereum 上の新アカウント挙動と一致。EXTCODEHASH が未知アカウントに特殊な意味を持つので、不存在ではなく空アカウントを返すことが正しいセマンティクス。
  • SSTOREZeroDb メソッドを呼ばない理由: 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 のではない。

合格基準

  • ZeroDb 4 メソッドの戻り値を即書ける。
  • 5 EVM 操作の予測(BALANCE / SLOAD / EXTCODESIZE / CALL / BLOCKHASH)を即答できる。
  • SSTOREZeroDbstorage を呼ばない理由を 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 はあなたの型