FABRKNT
Inside Alloy — Reading the Rust Ethereum Library
Inside Alloy
Lesson 9 of 15·CONTENT12 min25 XP

Treat this page as a workbench, not a blog post. The goal is to extract a reusable mental model from the source and carry it into the rest of the Fabrknt stack.

Course
Inside Alloy — Reading the Rust Ethereum Library
Lesson role
CONTENT
Sequence
9 / 15

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; OptimismProvider with <N=Optimism>. Works without modification.
  • Compile-time mismatch. Try block_summary::<Optimism, _>(&ethereum_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 Optimism block in op-alloy. The path varies — try crates/network/src/lib.rs or similar.

Read each associated type's RHS:

SlotEthereum's valueOptimism's valueSame?
TxTypealloy_consensus::TxTypeop_alloy_consensus::OpTxType
TxEnvelopealloy_consensus::TxEnvelopeop_alloy_consensus::OpTxEnvelope
UnsignedTxalloy_consensus::TypedTransactionOP analog
ReceiptEnvelopealloy_consensus::ReceiptEnvelopeOP analog
Headeralloy_consensus::Headeralloy_consensus::Header
TransactionRequestalloy_rpc_types_eth::TransactionRequestOP analog
TransactionResponsealloy_rpc_types_eth::TransactionOP analog
ReceiptResponsealloy_rpc_types_eth::TransactionReceiptOP analog
HeaderResponsealloy_rpc_types_eth::Headeralloy_rpc_types_eth::Header (likely)
BlockResponsealloy_rpc_types_eth::BlockOP 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, _>(&eth_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_summary is written once, instantiated with two different N parameters, 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, _>(&eth_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:

  1. block_summary is 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?
  2. Header is reused between Ethereum and Optimism, but BlockResponse differs. What does Drill 1's table reveal about which kinds of data force overrides vs allow sharing?
  3. The compiler rejects block_summary::<Optimism>(&eth_provider, ...). Trace through: which trait bound is violated, and which associated type's mismatch produces the error?
  4. 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 + 'static bounds, type-level dictionary, compile-time specialization. One Provider body 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.