レッスン1 — Provider トレイトをステップで組み立てる
問い
Ethereum ノードと通信する Rust プログラム — MEV ボット / インデクサ / dapp バックエンド / Reth-SDK アプリ — は、すべて alloy-rs/alloy の Provider トレイトを経由する。生 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 失敗:
- URL ハードコード — Anvil / Alchemy / プライベートノード / フォークを切り替え不可
- トランスポートハードコード — HTTP のみ、WebSocket / IPC は別関数
- チェーンハードコード — 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 固有型... */ }
Provider を N でジェネリック化:
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_impl で Arc<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 / Arc → Arc<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_implがArc<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 行ずつ読み、各行を組み立てステップに対応づける。