レッスン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 失敗:
- Optimism: Tx envelope に
mintフィールド + レシートにl1_gas_used+l1_block_number - Anvil / Hardhat:
impersonateAccountで標準型に存在しないデバッグフィールド - カスタム 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 | 署名済み | ブロードキャスト対象 |
TransactionResponse | RPC 戻り値 | 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 ルート対象) |
| ReceiptResponse | RPC 戻り値(transaction_hash / block_hash / block_number / index 装飾) |
| Header | コンセンサスヘッダー |
| HeaderResponse | RPC 整形済み(ハッシュ計算済、JSON 化のため gas_used が文字列) |
| BlockResponse | RPC 完全ブロックペイロード |
| TxType | tx 分類用 enum タグ(Legacy / EIP-1559 / EIP-4844 / OP-Deposit) |
Step 4(関連型 vs ジェネリックパラメータ):
ジェネリックパラメータ 10 個:
struct Provider<TxRequest, TxEnvelope, Receipt, Block, ...> { ... }
3 問題:
- 一貫性なし:
Provider<EthereumTxRequest, OptimismTxEnvelope, ...>がそのままコンパイル = 混ぜ合わせ自由 - 呼び出し側冗長:
Providerの全 signature に 10 パラメータ必要 - 型レベル同一性なし:
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 + 'static → Arc<Provider<N>> パターン成立。
答え合わせ
TransactionRequestとTxEnvelopeを別関連型にする型システムの強制: 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 + 'staticでArc<Provider<N>>パターン成立、次のレッスンで本物の Ethereum + Optimism 実装を並列比較する。