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

レッスン2 — 本物の Provider トレイトを読む

問い

素朴な RPC から本物のトレイトの形まで Provider を組み立ててきた。今度はソースを開く — 各行は組み立てステップにどう対応するか + 戻り値型の機構(ProviderCall / RpcWithBlock / EthCall / PendingTransactionBuilder)の存在理由は?

原理(最小モデル)

  • Send + Sync スーパートレイト. 本番ユーザーは Arc<P> をワーカータスクへクローン、これがないとコンパイル不可。
  • auto_impl(&, &mut, Box, Rc, Arc) の 5 種. Provider は状態読み取りに &self のみ → 5 種すべてが動く。Database(revm)の &mut self 制約とは非対称。
  • root() だけが必須. ほかは全部デフォルト実装、ラッパーは self.inner.root() の 1 行で済む。
  • client()weak_client() の 2 種. 短命 RPC は client(強参照、ライフタイム束縛)/ 長寿命タスク(サブスクリプション、tokio::spawn)は weak_client(Weak 参照、drop を妨げない)。
  • 3 戻り値型パターン. ProviderCall(パラメータなし)/ RpcWithBlock(ブロック選択)/ EthCall(多次元オプション)。各々が対応 RPC のオプション構造に合うビルダー。
  • SendTransaction の複数ステップ. 1. 送信 → 2. 確認数設定 → 3. await でレシート。「送信 → 採掘 → 確定」ステートマシン。

具体例

トレイトヘッダー:

#[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()
    }
    // ...多数の RPC メソッド、すべてデフォルト実装
}

Database vs Provider の auto_impl 非対称:

トレイトauto_implメソッド signature
Database(revm)&mut, Box&mut self — キャッシュその場書き換え
Provider(alloy)&, &mut, Box, Rc, Arc&self — キャッシュは内部可変性で

Database&mut self&/Rc/Arc 使えない(&mut T 取り出せない)。Provider&self → 5 種全部 OK。

3 戻り値型パターン:

get_block_number — パラメータなし、シンプル結果:

fn get_block_number(&self) -> ProviderCall<NoParams, U64, BlockNumber> {
    self.client().request_noparams("eth_blockNumber").into()
}

ProviderCall<P, R, F> = future ビルダー、P = パラメータ型、R = 生応答型、F = ユーザー型。impl Future ではなく builder にすることで全 RPC メソッドが同じ形 + カスタマイズの仕組み常備。

get_balance — ビルダー経由でブロック選択:

fn get_balance(&self, address: Address) -> RpcWithBlock<Address, U256> {
    RpcWithBlock::new_provider(move |block| {
        self.client().request("eth_getBalance", (address, block)).into()
    })
}

RpcWithBlock がブロック選択ビルダー:

provider.get_balance(addr).await                            // latest(デフォルト)
provider.get_balance(addr).block_id(1_000_000.into()).await // 過去
provider.get_balance(addr).hash(some_hash).await            // ブロックハッシュ
provider.get_balance(addr).pending().await                  // pending ブロック

call — カスタマイズの幅大:

fn call(&self, tx: N::TransactionRequest) -> EthCall<N> {
    EthCall::new(self.client(), tx)
}

EthCall チェーン可能メソッド:

  • .block(BlockId) — 特定ブロックに対する eth_call
  • .overrides(state_overrides) — 状態オーバーライド付き(アカウントなりすまし、残高 / コード上書き)
  • .gas(...).value(...).from(...) — tx 補正

3 戻り値型と RPC オプション構造の対応:

RPC メソッドオプションパラメータ戻り値型
eth_blockNumberなしProviderCall(await するだけ)
eth_getBalanceblockRpcWithBlock(オプション次元 1 つ)
eth_callblock, from, gas, value, stateEthCall(オプション次元が多数)

デフォルト実装パターン:

fn get_X(&self, args...) -> SomeReturnType {
    self.client().request("rpc_methodName", args).into()
}

クライアント経由でリクエスト構築 → .into() で適切なビルダー / future 型へ変換 → ラッパーが変えたい部分以外をオーバーライドしなくて良い 理由。

send_transaction の複数ステップ:

fn send_transaction(&self, tx: N::TransactionRequest) -> SendTransaction<N>

ステートマシン:

provider.send_transaction(tx).await?                       // tx ハッシュのみ、待たない
provider.send_transaction(tx).with_required_confirmations(3).get_receipt().await? // 3 ブロック待つ

失敗例(誤解)

ProviderDatabase も同じ auto_impl リスト」— 間違いDatabase = &mut, Box(2 種)/ Provider = &, &mut, Box, Rc, Arc(5 種)。差異は &mut self vs &self のメソッド signature、auto_impl リストは契約の厳密宣言。

get_balanceimpl Future<Output = U256> を返せば十分」— 間違いRpcWithBlock で await 前にブロック選択カスタマイズ可能、全 RPC が同じビルダー形 → カスタマイズ仕組みが常備。

weak_client()client() の劣化版」— 間違い。長寿命タスク(サブスクリプション、tokio::spawn)用、Weak 参照は drop を妨げない → プロバイダ drop でタスクが気づいてシャットダウン。

ステップで組み立てる

Step 1: トレイトヘッダーの 3 注目点

Send + Sync + auto_impl(5 種) + root() のみ必須。

Step 2: Database vs Provider 非対称の理由

&mut self vs &self のメソッド signature。

Step 3: client()weak_client() の使い分け

短命 RPC = client / 長寿命タスク = weak_client。

Step 4: 3 戻り値型パターン

ProviderCall(オプション 0)/ RpcWithBlock(オプション 1 次元)/ EthCall(多次元)。

Step 5: デフォルト実装パターン

self.client().request(name, args).into() の 1 行、ラッパーは変える部分のみ override。

Step 6: send_transaction ステートマシン

送信 → 確認数設定 → await でレシート。

答え合わせ

  • ProviderDatabase の auto_impl 非対称の決め手: メソッド signature の違い。Database::basic(&mut self, addr)&mut&/Rc/Arc から取り出せない。Provider::get_balance(&self, addr)&self → 5 種すべて OK。キャッシュ戦略の選択(その場書き換え vs 内部可変性)が auto_impl 幅を決める
  • get_balanceimpl Future でなく RpcWithBlock を返す理由: ほとんどの呼び出しは latest だが、ビルダーパターンで .block_id(N) / .hash(h) / .pending() で過去 / ハッシュ / pending を選択可能 → メソッド 1 つで問い合わせ方多数。関数引数で block_id を渡す設計だと全メソッドが煩雑化。
  • SignerProvider ラッパーが書くメソッド数: send_transaction1 つだけ(tx 署名 + inner に転送)。残り 29 はトレイトのデフォルト実装に任せる → それらは self.root() 経由でトランスポート取得。30 メソッド本体書く代わりに 1 個

合格基準

  • auto_impl の 5 種を即答できる。
  • Database vs Provider の非対称を signature 差で説明できる。
  • client()weak_client() の使い分けを言える。
  • 3 戻り値型と RPC オプション構造の対応を言える。
  • send_transaction ステートマシン 3 ステップを順に言える。

まとめ(3行)

  • Provider ヘッダー = Send + Sync + auto_impl(5 種、Database の 2 種と非対称) + root() のみ必須 + デフォルト実装 30 メソッド。
  • 3 戻り値型(ProviderCall / RpcWithBlock / EthCall)が各 RPC のオプション構造に合うビルダー、await 前カスタマイズで「1 メソッド + 問い合わせ方多数」を実現。
  • SignerProvidersend_transaction 1 つだけ override、残り 29 はデフォルト実装が self.root() 経由で動く = ラッパー作者の 30 メソッド本体書きが 1 個に。