FABRKNT
Inside Alloy — Rust Ethereum ライブラリを読む
Alloyの内側
レッスン 5 / 15·CONTENT12 分25 XP
コース
Inside Alloy — Rust Ethereum ライブラリを読む
レッスンの役割
CONTENT
順序
5 / 15

レッスン3 — ドリル: ログ Provider ラッパーを作る

問い

読むだけではリハーサル、手を動かすことで記憶。任意の Provider をラップし、RPC 呼び出しを内側へ転送する前にログ出力する LoggingProvider を書く。本番のインデクサ / MEV パイプラインが実際に動かしているコード相当 — alloy をフォークせず観測可能性を層として積む。どう実装する?

原理(最小モデル)

  • セットアップ 3 要素. Foundry / Anvil(ローカルノード)+ 新規 cargo プロジェクト + Cargo.toml に alloy + tokio + tracing。
  • FillProvider のオーバーライド数. 30 以上のうち 3-5 個のみ(send_transaction + ガス充填まわりの call/estimate_gas)、残り ~25 個はデフォルト実装。
  • LoggingProvider の最小実装. inner: P + PhantomData<N> + root() 委譲 + 各 RPC メソッドで log + 委譲。
  • メソッド単位 opt-in. 明示的に override しないメソッドはデフォルト実装に落ち、ログを出さない。
  • 積層は自動. LoggingProvider<FillProvider<NonceFiller, FillProvider<GasFiller, FillProvider<ChainIdFiller, RootProvider>>>> のタワー = 各ラッパーの root() が 1 段内側に転送、トレイトのデフォルト実装が self.root() 経由で root にアクセス → N 層でも実行時は 1 本の間接化チェーン。
  • ProviderBuilder.with_recommended_fillers() で nonce / gas / chain-id 自動.

具体例

セットアップ:

# 1. Foundry / Anvil
curl -L https://foundry.paradigm.xyz | bash
foundryup

# 2. 新規 cargo プロジェクト
cargo new alloy-logging-drill --bin
cd alloy-logging-drill

Cargo.toml:

[dependencies]
alloy = { version = "0.x", features = ["full", "provider-http", "node-bindings"] }
tokio = { version = "1", features = ["full"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

LoggingProvider 実装:

use alloy::network::{Ethereum, Network};
use alloy::primitives::Address;
use alloy::providers::{Provider, RootProvider};
use std::marker::PhantomData;

pub struct LoggingProvider<P, N: Network = Ethereum> {
    inner: P,
    _network: PhantomData<N>,
}

impl<P, N: Network> LoggingProvider<P, N> {
    pub fn new(inner: P) -> Self {
        Self { inner, _network: PhantomData }
    }
}

impl<P, N> Provider<N> for LoggingProvider<P, N>
where
    P: Provider<N>,
    N: Network,
{
    fn root(&self) -> &RootProvider<N> {
        self.inner.root()
    }

    fn get_balance(&self, address: Address) -> alloy::providers::RpcWithBlock<Address, alloy::primitives::U256> {
        tracing::info!(?address, "LoggingProvider: get_balance called");
        self.inner.get_balance(address)
    }
}

メソッド本体 2 つ書いた: root + get_balance

get_block_number を呼んでも ログは出ない — トレイトのデフォルト実装に落ち、self.client() 経由で基盤トランスポートへ直接ルーティング。メソッド単位 opt-in、明示的にインターセプトしたものしか目にしない。

Anvil + main 配線:

# 別ターミナル
anvil
use alloy::providers::ProviderBuilder;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt()
        .with_env_filter("info")
        .init();

    let inner = ProviderBuilder::new()
        .on_http("http://localhost:8545".parse()?);

    let provider = LoggingProvider::new(inner);

    // 最初のプリファンド Anvil アカウント
    let addr: Address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".parse()?;
    let balance = provider.get_balance(addr).await?;
    println!("balance: {balance}");

    // こちらはログされないはず(get_block_number はインターセプトしていない)
    let block_number = provider.get_block_number().await?;
    println!("block: {block_number}");

    Ok(())
}

期待出力:

INFO LoggingProvider: get_balance called address=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
balance: 10000000000000000000000
block: 0

FillProvider と積層:

let inner = ProviderBuilder::new()
    .with_recommended_fillers()  // nonce + gas + chain-id filler を追加
    .on_http("http://localhost:8545".parse()?);

let provider = LoggingProvider::new(inner);

let bal = provider.get_balance(addr).await?;

積層タワー: LoggingProvider<FillProvider<NonceFiller, FillProvider<GasFiller, FillProvider<ChainIdFiller, RootProvider>>>>

自動合成の仕組み: 各ラッパーの root() が 1 段内側に転送 → トレイトのデフォルト実装は self.root() で root に到達 → 塔全体がトレイトレベルで平坦化 → N 層でも実行時は 1 本の間接化チェーン。

失敗例(誤解)

FillProvider は 30 メソッド全部 override」— 間違い。3-5 個のみ(send_transaction + ガス充填まわり)、残りはデフォルト実装が self.root() 経由でルーティング。

「ログを全 RPC メソッドに入れる」— 不適切。本番観測性層は重要メソッド(send_transaction / call / get_logs / get_balance)のみ override、30 個全部ではない。

「ラッパーの積層は手動配線が必要」— 間違いroot() + デフォルト実装の合成で 自動、各ラッパーは互いを知らずに済む。新規ラッパーは純粋にコードを足すだけ。

ステップで組み立てる

Step 1: FillProvider を読む

crates/provider/src/fillers/mod.rsimpl Provider for FillProviderroot()self.inner.root() 転送、override は 3-5 個。

Step 2: LoggingProvider スケッチ

inner: P + PhantomData<N> + root() 委譲 + log メソッド。

Step 3: Anvil で動作確認

get_balance でログ + get_block_number でログなしを観察。

Step 4: FillProvider と積層

with_recommended_fillers() + .wallet(signer) でタワー構築。

Step 5: 積層自動合成の理解

root() + デフォルト実装で各ラッパー独立、N 層でも実行時 1 本の間接化。

答え合わせ

  • FillProvider が 30 メソッド中 3-5 個しか override しない理由: トレイトのデフォルト実装が self.client() を呼ぶ → これは self.root().client() に落ちる → FillProvider::root()self.inner.root() 返す → 各デフォルト実装メソッドが 自動的に内側プロバイダのトランスポート経由でルーティング。書くコードゼロ。
  • LoggingProviderget_block_number がログを出さない理由: get_balance のみインターセプト。get_block_number はデフォルト実装に落ち、そこで self.client() を使い、self.root() 経由で基盤トランスポートへ直接ルーティング。ログはメソッド単位の opt-in
  • LoggingProvider<FillProvider<...>> の自動合成: 各ラッパーの root() がさらに 1 段内側に転送 → トレイトのデフォルト実装は self.root() 経由で root にアクセス → 塔全体がトレイトレベルで平坦化 → N 層あっても実行時は 1 本の間接化チェーン。ラッパーの組み合わせ任意、互いに無知、新規追加は純粋にコード足すだけ。

合格基準

  • FillProvider の override 数(3-5 個)を即答できる。
  • LoggingProvider の最小実装(root + 1 メソッド)を書ける。
  • メソッド単位 opt-in を 1 文で説明できる。
  • with_recommended_fillers() で nonce / gas / chain-id 自動追加を理解している。
  • 積層自動合成(root() + デフォルト実装)の仕組みを言える。

まとめ(3行)

  • LoggingProvider = inner: P + PhantomData<N> + root() 委譲 + 各 RPC メソッドで log + 委譲、本番観測性層の標準パターン。
  • メソッド単位 opt-in で重要メソッドのみ intercept、残りはデフォルト実装が self.root() 経由で動く → 書くコードゼロ。
  • 積層タワー(LoggingProvider<FillProvider<...>>)は root() + デフォルト実装で自動合成、N 層でも実行時 1 本の間接化、ラッパー互いに無知。