Lesson 8 — Drill: write generic-over-N code that works on Ethereum and Optimism
Question
Build block_summary<N: Network, P: Provider<N>> — same function works on Ethereum and Optimism. Generic over N.
Principle (minimum model)
- Generic signature.
fn block_summary<N: Network, P: Provider<N>>(provider: &P) -> Result<String>. The Network type is propagated through. - Implementation.
let block = provider.get_block(BlockNumberOrTag::Latest).await?; format!("{}", block.header.number). No chain-specific code. - Ethereum test.
let provider = ProviderBuilder::new().on_http(MAINNET_HTTP)?; block_summary(&provider).await?. Works. - Optimism test. Same code;
OptimismProviderwith<N=Optimism>. Works without modification. - Compile-time mismatch. Try
block_summary::<Optimism, _>(ðereum_provider). Compiler refuses (type mismatch on TxEnvelope). The N-bound prevents wrong-chain bugs. - Why this is the win. One function; both chains; no runtime checks; compile-time safety. The pattern extends to any cross-chain library.
Worked example + steps
Drill: write generic-over-N code that works on Ethereum and Optimism
Reading is rehearsal. Doing is memory. This drill takes you from "I've read about Network as a type-level dictionary" to "I have written one function that runs against both Ethereum and Optimism with no per-chain code."
The payoff in production: a block explorer, an indexer, an MEV bot — anything that wants to support multiple EVM-compatible chains writes its core logic once, generic over N: Network. That's what you'll do here.
Setup
Two terminals:
Terminal 1 — Anvil (local Ethereum):
anvil
(If you don't have Foundry installed: curl -L https://foundry.paradigm.xyz | bash && foundryup.)
Terminal 2 — your project:
cargo new alloy-network-drill --bin
cd alloy-network-drill
Add to Cargo.toml:
[dependencies]
alloy = { version = "0.x", features = ["full", "provider-http"] }
op-alloy = { version = "0.x" } # for Optimism Network impl
tokio = { version = "1", features = ["full"] }
eyre = "0.6"
(Pin to current versions; the exact crate names of op-alloy may vary across releases — search crates.io for the current packaging.)
Drill 1 — Read op-alloy's Network impl
Before writing code, verify the walkthrough's claims against real source.
🔍 Find the
impl Network for Optimismblock in op-alloy. The path varies — trycrates/network/src/lib.rsor similar.
Read each associated type's RHS:
| Slot | Ethereum's value | Optimism's value | Same? |
|---|---|---|---|
TxType | alloy_consensus::TxType | op_alloy_consensus::OpTxType | ❌ |
TxEnvelope | alloy_consensus::TxEnvelope | op_alloy_consensus::OpTxEnvelope | ❌ |
UnsignedTx | alloy_consensus::TypedTransaction | OP analog | ❌ |
ReceiptEnvelope | alloy_consensus::ReceiptEnvelope | OP analog | ❌ |
Header | alloy_consensus::Header | alloy_consensus::Header | ✅ |
TransactionRequest | alloy_rpc_types_eth::TransactionRequest | OP analog | ❌ |
TransactionResponse | alloy_rpc_types_eth::Transaction | OP analog | ❌ |
ReceiptResponse | alloy_rpc_types_eth::TransactionReceipt | OP analog | ❌ |
HeaderResponse | alloy_rpc_types_eth::Header | alloy_rpc_types_eth::Header (likely) | ✅ |
BlockResponse | alloy_rpc_types_eth::Block | OP analog | ❌ |
The two slots that don't differ (Header and HeaderResponse) are the ones whose data is content-identical across the two chains. Everywhere a tx, receipt, or block payload is involved, the tx-list cohesion forces an override.
Drill 2 — Write a generic-over-N block summary
In src/main.rs, write a function that takes any chain's block and produces a summary string. The function is generic over N: Network:
use alloy::network::{primitives::BlockTransactionsKind, Network};
use alloy::providers::Provider;
use alloy::rpc::types::BlockId;
async fn block_summary<N, P>(provider: &P, block_id: BlockId) -> eyre::Result<String>
where
N: Network,
P: Provider<N>,
{
let block = provider
.get_block(block_id)
.kind(BlockTransactionsKind::Hashes)
.await?
.ok_or_else(|| eyre::eyre!("block not found"))?;
// BlockResponse trait gives us .header() and .transactions()
use alloy::network::BlockResponse;
let header = block.header();
use alloy::network::primitives::HeaderResponse;
Ok(format!(
"block {} on chain — hash={:?}",
header.number(),
header.hash(),
))
}
(Method names approximate to current alloy. Your IDE will show you the exact HeaderResponse and BlockResponse trait methods. The point is: block.header() and header.number() work generically, regardless of N.)
Same code, different type parameters. The call site instantiates N to either Ethereum or op_alloy::network::Optimism. The function body is unchanged.
Drill 3 — Run against Ethereum (Anvil)
Add the main:
use alloy::network::Ethereum;
use alloy::providers::ProviderBuilder;
#[tokio::main]
async fn main() -> eyre::Result<()> {
// Ethereum (Anvil)
let eth_provider = ProviderBuilder::new()
.on_http("http://localhost:8545".parse()?);
let s = block_summary::<Ethereum, _>(ð_provider, BlockId::latest()).await?;
println!("ETH: {s}");
Ok(())
}
Run: cargo run. You should see something like:
ETH: block 0 on chain — hash=0x...
(Anvil starts at block 0. Mine a few blocks via anvil_mine if you want a higher number; not required for the drill.)
Drill 4 — Same function, against op-mainnet (Optimism)
Add an Optimism call to main:
use op_alloy::network::Optimism;
// ... after the Ethereum call ...
let op_provider = ProviderBuilder::<_, _, Optimism>::default()
.on_http("https://mainnet.optimism.io".parse()?);
let s = block_summary::<Optimism, _>(&op_provider, BlockId::latest()).await?;
println!(" OP: {s}");
(The exact ProviderBuilder syntax for non-Ethereum networks may differ; check op-alloy's examples. The structural point is: same function, different N.)
Run again. You should see two block summaries — one for Anvil (block 0 or similar), one for op-mainnet (a real block number, e.g. 130 million-ish at time of writing).
🔧 The same function body produced both outputs. That's the type-level-dictionary payoff:
block_summaryis written once, instantiated with two differentNparameters, and the compiler emits two specialized copies that work against the appropriate types per chain.
Drill 5 — Anti-fluency: what happens if you mix?
Try (this should NOT compile):
let eth_block = eth_provider.get_block(BlockId::latest()).await?;
let s = block_summary::<Optimism, _>(ð_provider, BlockId::latest()).await?;
The compiler rejects the second call because eth_provider's associated types don't match Optimism's. The error will mention something like "expected Optimism::TransactionRequest, found Ethereum::TransactionRequest."
This is the cohesion property protecting you at compile time. You cannot accidentally feed Ethereum-typed responses into Optimism-typed code. That's what made associated types better than 10 raw generic parameters from the buildup's Step 4.
End-of-lesson recall
Without scrolling, in your own words:
block_summaryis one function body. How many specialized copies does the compiler emit when it's instantiated for both Ethereum and Optimism? Why does that matter for performance?Headeris reused between Ethereum and Optimism, butBlockResponsediffers. What does Drill 1's table reveal about which kinds of data force overrides vs allow sharing?- The compiler rejects
block_summary::<Optimism>(ð_provider, ...). Trace through: which trait bound is violated, and which associated type's mismatch produces the error? - If you wanted to add Polygon zkEVM as a third chain, what would you write? (Hint: a new
struct PolygonZkEvm; impl Network for PolygonZkEvm { ... }.)
After this drill, you've shipped the same shape multi-chain tooling production indexers and explorers ship: one core function, generic over N: Network, specialized at compile time per chain. Next chain: the Signer model — how alloy composes signing, gas, and nonce filling into layered Providers.
🧭 Where you are now in the stack: you've built the networking layer's chain abstraction — 10 associated types with
Send + Sync + 'staticbounds, type-level dictionary, compile-time specialization. OneProviderbody now safely covers Ethereum, Optimism, and any future L2. Next chain switches dimension: from "which chain" to "who signs and how nonces / gas get filled."
Summary (3 lines)
- Drill:
block_summary<N, P>works on Ethereum AND Optimism. Generic-over-N is the win. - Anvil tests verify both chains work; compiler refuses chain-mismatched calls.
- Pattern extends to any cross-chain library — one function, type-safe. Next module: Signer.