レッスン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.rs で impl Provider for FillProvider、root() が 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()返す → 各デフォルト実装メソッドが 自動的に内側プロバイダのトランスポート経由でルーティング。書くコードゼロ。LoggingProviderでget_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 本の間接化、ラッパー互いに無知。