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

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

問い

Optimism の tx には l1_block_number + mint フィールド、レシートに l1_fee。Polygon zkEVM の tx エンベロープにはシーケンサ署名。各 L2 が独自 tx / レシート / ブロック形を持つ — それでも同じ Provider API がそのすべてで動く。どうやって?

原理(最小モデル)

  • 素朴な「Ethereum ハードコード」の 3 失敗. Optimism(l1_fee) + Anvil cheat + カスタム L2(独自エンベロープ)で破綻。
  • Network = 型レベル辞書. 1 トレイト + 関連型で「あるチェーンが使う型一式」を 1 か所で決まる。
  • 10 関連型の必要性. TxType / TxEnvelope / UnsignedTx / TransactionRequest / TransactionResponse / ReceiptEnvelope / ReceiptResponse / Header / HeaderResponse / BlockResponse。各々が具体的失敗モードに対して存在理由。
  • トランザクションのライフサイクル分割. Request(ビルダーで組み立て)→ Unsigned(フィールド全部埋まり、署名前)→ Envelope(署名済み)→ Response(block_hash / index 焼き込み)。各々別の型 → コンパイラが「Request はブロードキャスト不可」「Response に署名不可」を強制。
  • Receipt も Block も 2 種分割. Envelope(コンセンサス形、Merkle ルート対象)+ Response(RPC 装飾フィールド付き)。
  • 関連型 > ジェネリックパラメータ 10 個. 一貫性担保(混ぜ合わせ防止)+ 呼び出し側冗長性削減(Provider<N: Network> で 10 型引き込み)+ 型レベル同一性(N::TransactionRequest で関数書ける)。
  • Network: Send + Sync + 'static. Arc<Provider<N>> パターンに不可欠、'static で借用ライフタイム禁止。

具体例

最終形:

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

Ethereum ハードコードの 3 失敗:

  1. Optimism: Tx envelope に mint フィールド + レシートに l1_gas_used + l1_block_number
  2. Anvil / Hardhat: impersonateAccount で標準型に存在しないデバッグフィールド
  3. カスタム L2: Polygon zkEVM / Scroll / Linea が独自 tx エンベロープバリアント

Step 1(素朴な 3 型):

trait Network {
    type Transaction;
    type Receipt;
    type Block;
}

struct Ethereum;
impl Network for Ethereum {
    type Transaction = EthereumTx;
    type Receipt    = EthereumReceipt;
    type Block      = EthereumBlock;
}

struct Optimism;
impl Network for Optimism {
    type Transaction = OpTx;
    type Receipt    = OpReceipt;
    type Block      = OpBlock;
}

しかし「トランザクション」は 1 型ではなく 複数あり:

状態役割検証
TransactionRequestユーザーが組み立てフィールドの大半オプション
UnsignedTx全フィールド埋まりnonce / gas / chain_id 解決済、署名直前
TxEnvelope署名済みブロードキャスト対象
TransactionResponseRPC 戻り値block_hash / block_number / transaction_index 焼き込み

役割ごとにフィールド・検証・シリアライズが異なる → 1 つの和型に押し込むとランタイム検証が必要、分割すれば型システムが「Request はブロードキャスト不可」「Response に署名不可」を強制

Step 2(6 関連型):

trait Network {
    type TxEnvelope;
    type UnsignedTx;
    type TransactionRequest;
    type TransactionResponse;
    type Receipt;
    type Block;
}

Step 3(Receipt + Block も分割、+ TxType):

用途
ReceiptEnvelopeコンセンサス形(Merkle ルート対象)
ReceiptResponseRPC 戻り値(transaction_hash / block_hash / block_number / index 装飾)
Headerコンセンサスヘッダー
HeaderResponseRPC 整形済み(ハッシュ計算済、JSON 化のため gas_used が文字列)
BlockResponseRPC 完全ブロックペイロード
TxTypetx 分類用 enum タグ(Legacy / EIP-1559 / EIP-4844 / OP-Deposit)

Step 4(関連型 vs ジェネリックパラメータ):

ジェネリックパラメータ 10 個:

struct Provider<TxRequest, TxEnvelope, Receipt, Block, ...> { ... }

3 問題:

  1. 一貫性なし: Provider<EthereumTxRequest, OptimismTxEnvelope, ...> がそのままコンパイル = 混ぜ合わせ自由
  2. 呼び出し側冗長: Provider の全 signature に 10 パラメータ必要
  3. 型レベル同一性なし: Network 名がトレイト → fn for_network<N: Network>(...) -> N::TransactionRequest 書ける、素ジェネリックは書けない

関連型 = 「これらは組で動く」、ジェネリックパラメータ = 「どんな組み合わせでも有効」。チェーンプリミティブには前者。

Step 5(トレイト境界 Send + Sync + 'static):

pub trait Network: Send + Sync + 'static { ... }
  • Send + Sync: Arc<Provider<N>> パターンに不可欠、これがないとコンパイル不可
  • 'static: PhantomData<N> を持つ Provider が借用ライフタイム継承 → Arc も借用元設定より長生きできない → 'static で借用禁止 → Arc 自立

失敗例(誤解)

「『同じデータ、状態が違うだけ』の 1 型で十分」— 間違い。署名・block_hash がオプションの巨大 struct → broadcast(&tx) で「署名は存在するか? block_hash は不在か?」のランタイム検証必要。4 状態を別型にすれば型システムが強制

「関連型 10 個は冗長、ジェネリック 10 個と同じ」— 間違い。関連型は 一貫性 + 呼び出し側簡潔さ + 型レベル同一性 の 3 利点。素ジェネリックでは混ぜ合わせ可能 + Provider signature に毎回 10 パラメータ。

'static は『永遠に生きる』だけ」— 間違い。具体的に: MyNetwork<'a> だと Provider<MyNetwork<'a>> も継承 → Arc<Provider<MyNetwork<'a>>>'a に縛られ → Arc が借用元設定より長生き不可。'static で borrowed パターン排除 → Arc 自立

ステップで組み立てる

Step 1: 素朴な「Ethereum ハードコード」の 3 失敗

Optimism / Anvil cheat / カスタム L2。

Step 2: トランザクションを 4 状態に分割

Request / Unsigned / Envelope / Response、型システムが状態遷移強制。

Step 3: Receipt と Block も 2 種分割

Envelope(コンセンサス)+ Response(RPC 装飾)。

Step 4: 10 関連型を即答

TxType + TxEnvelope + UnsignedTx + TransactionRequest + TransactionResponse + ReceiptEnvelope + ReceiptResponse + Header + HeaderResponse + BlockResponse。

Step 5: 関連型 vs ジェネリックパラメータ

関連型 = これらは組で動く / ジェネリック = どんな組み合わせでも有効。チェーンには関連型。

Step 6: トレイト境界の意味

Send + Sync + 'staticArc<Provider<N>> パターン成立。

答え合わせ

  • TransactionRequestTxEnvelope を別関連型にする型システムの強制: 1 型 + オプションフィールドだと broadcast(&tx) で「署名は本当に存在するか?」のランタイム検証が必要 → TransactionRequest / UnsignedTx / TxEnvelope / TransactionResponse に分けると 不整合な状態はそもそも構築できない、各関数 signature が正しい状態だけ受け取る。
  • 関連型がジェネリックパラメータ 10 個より優れる 3 帰結: ① 一貫性担保(Provider<EthereumTxRequest, OptimismTxEnvelope> を構文的に禁止、チェーン型混ぜ防止)、② 呼び出し側冗長性削減(Provider<N: Network> で 10 型一括引き込み)、③ 型レベル同一性(N::TransactionRequest で N 上ジェネリックな関数書ける、素ジェネリックでは不可能)。
  • 'static 境界がないと壊れるパターン: MyNetwork<'a> のような借用ライフタイム → Provider<MyNetwork<'a>> も継承 → Arc<Provider<MyNetwork<'a>>>'a に縛られ → Arc が借用元の設定より長生き不可。Provider をグローバル Arc に置くパターンが壊れる。'static で borrowed パターン排除し Arc 自立。

合格基準

  • 素朴 Ethereum ハードコードの 3 失敗を即答できる。
  • トランザクション 4 状態(Request / Unsigned / Envelope / Response)を順に言える。
  • 10 関連型を即答できる。
  • 関連型 vs ジェネリックパラメータの 3 帰結を言える。
  • Send + Sync + 'static 各境界の意味を言える。

まとめ(3行)

  • Network = 型レベル辞書、10 関連型(Tx 4 状態 + Receipt 2 種 + Block 関連 3 種 + TxType)が各々失敗モードに対する存在理由。
  • 関連型は「これらは組で動く」(混ぜ合わせ防止 + 呼び出し側簡潔 + 型レベル同一性)、ジェネリックパラメータでは実現不可能。
  • Send + Sync + 'staticArc<Provider<N>> パターン成立、次のレッスンで本物の Ethereum + Optimism 実装を並列比較する。