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

レッスン1 — Provider トレイトをステップで組み立てる

問い

Ethereum ノードと通信する Rust プログラム — MEV ボット / インデクサ / dapp バックエンド / Reth-SDK アプリ — は、すべて alloy-rs/alloyProvider トレイトを経由する。生 JSON-RPC は直接叩かず、必ずこの 1 つのトレイトに集約 — 6 つの新概念が一度に降ってくる、組み立てると理由が見えるか?

原理(最小モデル)

  • 素朴な RPC クライアントの 3 失敗. URL ハードコード + トランスポートハードコード(HTTP のみ)+ チェーンハードコード(Ethereum 形式)。修正 = 3 軸をトレイトに抽象化
  • Step 1: メソッドだけのトレイト. Ethereum で動くが Optimism で破綻(tx envelope が違う)。
  • Step 2: Network トレイトで型レベル辞書. type TxEnvelope / type ReceiptEnvelope 等、関連型でチェーン固有型を一束に。Provider に N: Network ジェネリックパラメータ追加。
  • N: Network = Ethereum デフォルト. ユーザーの 99% は Ethereum、デフォルトで楽にし Optimism のみ明示。
  • Step 3: トランスポート抽象トレイト. プロバイダ実装 1 つで HTTP/WS/IPC 全対応、struct ごとに書かない。
  • Step 4: RootProvider + root(). ラッパープロバイダが 30 メソッドを再実装せずトランスポート委譲、書くのは 1 行(self.inner.root())。
  • Step 5: FillProvider / Filler. Signer / Nonce / Gas / ChainId を合成可能な層に、ビルダーで積層。
  • Step 6: #[auto_impl(&, &mut, Box, Rc, Arc)]. Arc<P> がそのまま Provider として動く、タスク間で安価共有。

具体例

最終的に組み立てる本物のトレイトヘッダー:

#[auto_impl(&, &mut, Box, Rc, Arc)]
pub trait Provider<N: Network = Ethereum>: Send + Sync {
    fn root(&self) -> &RootProvider<N>;

    fn client(&self) -> ClientRef<'_> { self.root().client() }
    fn weak_client(&self) -> WeakClient { self.root().weak_client() }

    fn get_block_number(&self) -> ProviderCall<NoParams, U64, BlockNumber> { /* ... */ }
    fn get_balance(&self, address: Address) -> RpcWithBlock<Address, U256> { /* ... */ }
    fn call(&self, tx: N::TransactionRequest) -> EthCall<N> { /* ... */ }
    fn send_transaction(&self, tx: N::TransactionRequest) -> SendTransaction<N> { /* ... */ }
    // ...他にも多数の RPC メソッド
}

素朴な RPC クライアント:

async fn get_balance(addr: Address) -> Result<U256, Box<dyn Error>> {
    let body = serde_json::json!({
        "jsonrpc": "2.0",
        "method": "eth_getBalance",
        "params": [addr, "latest"],
        "id": 1,
    });
    let client = reqwest::Client::new();
    let resp = client.post("http://localhost:8545").json(&body).send().await?;
    let parsed: serde_json::Value = resp.json().await?;
    Ok(U256::from_str_radix(&parsed["result"].as_str().unwrap()[2..], 16)?)
}

3 失敗:

  1. URL ハードコード — Anvil / Alchemy / プライベートノード / フォークを切り替え不可
  2. トランスポートハードコード — HTTP のみ、WebSocket / IPC は別関数
  3. チェーンハードコード — Optimism の L1Cost フィールド / カスタム L2 の独自エンベロープに対応不可

Step 1(メソッドだけ):

#[async_trait]
pub trait Provider {
    async fn get_balance(&self, address: Address) -> Result<U256>;
    async fn get_block_number(&self) -> Result<u64>;
    async fn call(&self, tx: TransactionRequest) -> Result<Bytes>;
    async fn send_transaction(&self, tx: TransactionRequest) -> Result<TxHash>;
    // ... Ethereum RPC 動詞ごとに 30+ メソッド
}

Step 2(Network ジェネリック追加):

pub trait Network: Send + Sync + 'static {
    type TxEnvelope: ...;
    type UnsignedTx: ...;
    type ReceiptEnvelope: ...;
    type Header: ...;
    type TransactionRequest: ...;
    type TransactionResponse: ...;
    type ReceiptResponse: ...;
    type HeaderResponse: ...;
    type BlockResponse: ...;
}

pub struct Ethereum;
impl Network for Ethereum { /* ...標準型... */ }

pub struct Optimism;
impl Network for Optimism { /* ...OP 固有型... */ }

ProviderN でジェネリック化:

pub trait Provider<N: Network> {
    async fn get_balance(&self, address: Address) -> Result<U256>;  // 全チェーン共通
    async fn call(&self, tx: N::TransactionRequest) -> Result<Bytes>;  // チェーン固有
    async fn send_transaction(&self, tx: N::TransactionRequest) -> Result<...>;  // チェーン固有
}

Step 3(トランスポート抽象):

pub trait Transport {
    async fn send(&self, request: RpcRequest) -> Result<RpcResponse>;
}

// 1 つの具象プロバイダ、トランスポートでパラメータ化
pub struct ProviderImpl<T: Transport, N: Network> {
    transport: T,
    _network: PhantomData<N>,
}

Step 4(RootProvider + root()):

pub trait Provider<N: Network = Ethereum> {
    fn root(&self) -> &RootProvider<N>;
    // すべての RPC メソッドのデフォルト impl は self.root() を経由
}

pub struct RootProvider<N: Network = Ethereum> {
    client: ClientRef<'_>,  // 実際のトランスポート
    _network: PhantomData<N>,
}

Step 5(FillProvider / Filler):

pub struct FillProvider<F: Filler<N>, P: Provider<N>, N: Network> {
    filler: F,
    inner: P,
    _network: PhantomData<N>,
}

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

    async fn send_transaction(&self, mut tx: N::TransactionRequest) -> Result<...> {
        self.filler.fill(&mut tx).await?;
        self.inner.send_transaction(tx).await
    }
}

ユーザービルダー:

let provider = ProviderBuilder::new()
    .filler(NonceFiller)
    .filler(GasFiller)
    .signer(my_signer)
    .on_http(url);

Step 6(auto_implArc<P> 対応):

#[auto_impl(&, &mut, Box, Rc, Arc)]
pub trait Provider<N: Network = Ethereum>: Send + Sync { /* ... */ }

Arc<P>Provider として動く → MEV ボット / インデクサが 1 プロバイダを多タスク共有可能。

失敗例(誤解)

「メソッドだけのトレイトで Ethereum 専用 → 後で Optimism 用に別トレイト」— 間違い。9 割同一になる、Network 抽象で型レベル辞書化が正解。

「Provider 実装ごとに HTTP / WS / IPC の 3 struct」— 間違い。同じトレイトメソッドの本体コピペ = 筋が悪い。Transport トレイトで 1 implementation。

「ラッパーは 30 メソッド全部オーバーライド」— 間違いroot() 1 行 + デフォルト実装が self.root() 経由 → ラッパー作者は 変えたいメソッドだけオーバーライド

ステップで組み立てる

Step 1: 素朴な RPC の 3 失敗

URL + トランスポート + チェーンがすべてハードコード。

Step 2: 3 軸をトレイトに抽象化

トランスポート抽象 + Network 抽象 + デフォルト Ethereum。

Step 3: N: Network = Ethereum デフォルト

ユーザー 99% は Ethereum、デフォルトで楽に + Optimism のみ明示。

Step 4: RootProvider + root() の間接化

ラッパーは self.inner.root() の 1 行で 30 メソッド委譲完了。

Step 5: FillProvider で層状合成

Signer / Nonce / Gas / ChainId を任意組み合わせ可能、ビルダーで積層。

Step 6: auto_impl で 5 種ラッパー

& / &mut / Box / Rc / ArcArc<P> がタスク共有プリミティブ。

答え合わせ

  • デフォルト N: Network = Ethereum の利点: ユーザーの 99% は Ethereum → Provider<Ethereum> ではなく Provider で済む。Optimism / カスタム L2 のユーザーだけ明示。一般ケースを楽にし、まれなケースを明示にする
  • root() が解決する問題: ラッパープロバイダ(SignerProvider / Filler)が 30 メソッドを再実装する代わりに 1 メソッド(self.inner.root())で委譲。トレイトのデフォルトメソッドが self.root() 経由でトランスポートにアクセス → ラッパー作者は変えたいメソッドだけオーバーライド。
  • auto_impl(Arc, ...) が本番で効く理由: MEV ボット / インデクサ / dapp サーバーは 1 プロバイダを多タスクで共有したい → Arc<Provider> が自然 → Arc は安価クローン + 1000 ワーカーへ配布 + 全員同接続プール。auto_implArc<P> の Provider 実装を自動生成。

合格基準

  • 素朴な RPC の 3 失敗を即答できる。
  • N: Network = Ethereum デフォルトの理由を言える。
  • root() + デフォルト実装パターンが解く問題を即答できる。
  • FillProvider で層状合成(Signer / Nonce / Gas / ChainId)を組める。
  • auto_impl の 5 種ラッパーと Arc の本番用途を言える。

まとめ(3行)

  • Provider トレイト = 素朴な RPC の 3 失敗(URL / トランスポート / チェーン)を解決する 6 設計判断の積み重ね。
  • N: Network = Ethereum ジェネリック + トランスポート抽象 + RootProvider / root() の間接化 + FillProvider 層状合成 + auto_impl(Arc, ...) でタスク共有。
  • 次のレッスンで alloy 本体の crates/provider/src/provider/trait.rs を 1 行ずつ読み、各行を組み立てステップに対応づける。