FABRKNT
Reth Expert — 本番エンジニアリング
本番エンジニアリング
レッスン 6 / 25·CONTENT15 分35 XP
コース
Reth Expert — 本番エンジニアリング
レッスンの役割
CONTENT
順序
6 / 25

レッスン5 — カスタム Precompile

問い

EVM の中で SHA-256 を使いたい。道は 2 つ: 新しい Opcode を足す(命令ストリームに新しいバイト)、あるいは Precompile を足す(コントラクトが CALL 0x00...02 した瞬間に EVM が呼び出すネイティブ Rust 関数)。カスタム Opcode はコンセンサスを破るがカスタム Precompile は破らない — なぜか?

原理(最小モデル)

  • Opcode vs Precompile. Opcode はバイトコード命令 → Solidity / ABI / インデクサ全部に影響、コンセンサス破壊。Precompile は特定アドレスへの CALL → ツーリングほぼ透過、レジストリに登録するだけ。
  • Precompile(input: &[u8], gas_limit: u64) -> PrecompileResult の関数. アドレスは Precompile::new の一部、コンパイル時に固定。
  • ガス計算は base + per_word × ceil(len / 32). Yellow Paper の標準形。
  • Precompiles::extend で登録. optimized_access[short_idx] 配列にも書く → 短アドレス(0x01-0x0a)はディスパッチが単一インデックス参照。
  • PrecompileHalt::OutOfGas はフレーム全体停止. リファンドなし、通常 revert とは別物。
  • Foundry の cheatcode は本番カスタム precompile. vm.deal / vm.warp / vm.prank は全部 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D への CALL。

具体例

ディスパッチフロー:

sequenceDiagram
    participant C as コントラクト bytecode
    participant I as Revm interpreter
    participant Reg as Precompiles レジストリ
    participant Fn as カスタム precompile fn

    C->>I: CALL 0x00...ff
    I->>Reg: lookup アドレス
    Reg-->>I: 発見 — Precompile
    I->>Fn: run(input, gas_limit)
    Fn-->>I: Ok(gas_used, output)
    I->>C: returndata + gas refund

本物の identity precompile(crates/precompile/src/identity.rs):

use super::calc_linear_cost;
use crate::{
    eth_precompile_fn, EthPrecompileOutput, EthPrecompileResult, Precompile, PrecompileHalt,
    PrecompileId,
};
use primitives::Bytes;

eth_precompile_fn!(identity_precompile, identity_run);

/// Address of the identity precompile.
pub const FUN: Precompile = Precompile::new(
    PrecompileId::Identity,
    crate::u64_to_address(4),
    identity_precompile,
);

/// 操作の基本コスト
pub const IDENTITY_BASE: u64 = 15;
/// ワードあたりのコスト
pub const IDENTITY_PER_WORD: u64 = 3;

/// 入力バイトをコピーして出力として返す。
pub fn identity_run(input: &[u8], gas_limit: u64) -> EthPrecompileResult {
    let gas_used = calc_linear_cost(input.len(), IDENTITY_BASE, IDENTITY_PER_WORD);
    if gas_used > gas_limit {
        return Err(PrecompileHalt::OutOfGas);
    }
    Ok(EthPrecompileOutput::new(
        gas_used,
        Bytes::copy_from_slice(input),
    ))
}

カスタム登録(crates/precompile/src/lib.rs):

pub fn extend(&mut self, other: impl IntoIterator<Item = Precompile>) {
    let iter = other.into_iter();
    let (lower, _) = iter.size_hint();
    self.addresses.reserve(lower);
    self.inner.reserve(lower);
    for item in iter {
        let address = *item.address();
        if let Some(short_idx) = short_address(&address) {
            self.optimized_access[short_idx] = Some(item.clone());
        }
        self.addresses.insert(address);
        self.inner.insert(address, item);
    }
}

登録例:

let my_pre = Precompile::new(
    PrecompileId::Custom("my_thing"),
    address!("00000000000000000000000000000000000000ff"),
    my_function,
);
precompiles.extend([my_pre]);

Foundry の cheatcode アドレス:

address internal constant VM_ADDRESS = 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D;
// = address(uint160(uint256(keccak256("hevm cheat code"))))

失敗例(誤解)

「カスタム Opcode を入れればコンセンサスを破らない」— 間違い。Opcode はバイトコードの 1 バイトを新しい意味に再定義する → Solidity コンパイラ / Wallet / インデクサ / 他クライアントすべてとコンセンサスを破る。Precompile は executor のレジストリにあなたのビルドだけが持つ追加エントリ → mainnet との合意は維持される。

「ガスコストは適当でよい、後で調整」— 間違い。precompile のガスは ChainSpec によって決まる。安すぎる precompile が攻撃者の DoS ベクターになり、後手の価格改定(EIP-2929 のような)はハードフォーク。現実的に最も遅い入力でベンチ → 悪用係数 2-5 倍 → ガス決定 が最初から必要。

vm.deal のような状態変更は precompile では無理」— 間違い。Foundry はカスタム precompile を Rust で実装、calldata をデコードして Revm DB を直接書き換える。標準 precompile(identity 等)が純粋関数なのは標準仕様の制約で、カスタム precompile はホスト側の状態に何でもできる。

🛑 予測。 カスタム Opcode はメインネットとのコンセンサスを破る(中級で見た)。カスタム Precompile は破らない — 同じく vanilla EVM になかった新コード。なぜ答えが違うか?(答え: Opcode はバイトコードの命令ストリームに新しい意味を導入 → 既存コードに影響 + ツール / インデクサ / コンパイラ全部が知らない命令を見る → コンセンサス分裂。Precompile は 特定アドレスへの CALL という既存メカニズム上の動作 → mainnet がそのアドレスに何もない / 別のものを返すだけ、他のすべては変わらない。あなたのビルドだけが追加挙動を持つ。)

ステップで組み立てる

Step 1: 4 比較項目を即答

呼び出し方 / 追加方法 / ツール影響 / 用途。

Step 2: identity precompile の構造を読める

Precompile::new(id, address, fn) + fn(input, gas_limit) -> Result の 50 行未満。

Step 3: extend で登録

Precompiles::extend([my_pre]) の 1 行。あとは Evm ビルダーに通す。

Step 4: ガス設計

base + per_word × ceil(len / 32) の線形コスト。価格設定は ① 現実的最遅入力でベンチ → ② 悪用係数 2-5 倍 → ③ チェーンのガス / CPU 比率に変換 → ④ 敵対的入力で再ベンチ。

Step 5: Foundry の cheatcode を読む

0x7109... への CALL を Foundry の Rust precompile がインターセプト → cheatcode dispatch(calldata セレクタマッチ)→ Revm DB を Rust から直接書き換え。カスタム precompile の本番ケーススタディ

答え合わせ

  • 1 KB 入力での identity ガス計算: ceil(1024 / 32) = 32 ワード。15 + 3 × 32 = 111 ガス。
  • vm.deal の実装: Foundry の Rust precompile が calldata から deal(address, uint256) セレクタをマッチ → Revm DB を直接書き換えてアカウントの残高を更新 → 通常実行を継続。標準 precompile が純粋関数なのは「Ethereum 標準仕様」の選択で、ホスト側の Rust 実装は何でもできる。
  • precompile ガス 100、攻撃者が 10 倍 CPU 時間の入力を発見: 攻撃者は 100 ガスで通常の 10 倍 CPU を消費 → ノード 1 秒あたり 10 倍多くの CPU を払う → DoS。ガス / CPU 比率が崩れる。EIP-2929 が cold/warm reset で同様問題に対処(後手で痛いハードフォーク)。

合格基準

  • Opcode vs Precompile の 4 比較を即答できる。
  • Precompile::new(id, address, fn) の構造を言える。
  • Precompiles::extend で登録、optimized_access で短アドレスが速い理由を説明できる。
  • ガス価格設計の 4 ステップを言える。
  • Foundry の cheatcode が「カスタム precompile + Rust 側 state 書き換え」と分かる。

まとめ(3行)

  • カスタム Precompile = アドレス + Rust 関数((input, gas_limit) -> Result)を Revm レジストリに登録、Opcode と違いコンセンサスを破らない。
  • ガス計算は base + per_word × ceil(len / 32)、価格設計は「最遅入力ベンチ → 悪用係数 → 敵対的再ベンチ」の 4 ステップ。
  • Foundry の cheatcode(vm.deal / vm.warp)は本番カスタム precompile の最広デプロイ例 — 同じパターンを自分のチェーンに転用できる。