Lesson 13 — Drill: ship a custom pool builder
Question
Build a custom transaction pool that filters out high-gas-price spam. Replace Reth's default pool with yours; node still works.
Principle (minimum model)
- Pool trait impl.
impl Pool for SpamFilteringPool. ~80 lines. - Filter logic. On
add(tx), reject iftx.max_priority_fee > MAX_TIP_GAS_PRICE. Otherwise accept. - NodeBuilder integration.
NodeBuilder::new().pool(SpamFilteringPool::default()).launch(). One line. - Test. Boot the node; submit a high-gas-price tx; assert rejected. Submit a normal tx; assert accepted.
- Why this matters. Production chains differ in tx policy: rollups have L1-fee considerations; payment chains have specific filters. Each is a custom Pool.
- Composability. A custom Pool composes with default consensus, executor, validator, RPC. Only the slot you customise changes.
- Production parallel. Tempo uses a custom Pool for stable-coin tx prioritisation.
Worked example + steps
Drill: ship a custom pool builder
Reading is rehearsal. Doing is memory. This drill takes you from "I've read about the SDK" to "I have written a custom pool builder, swapped it in, and watched my code run inside a node binary."
Setup
git clone https://github.com/paradigmxyz/reth
cd reth/examples/custom-node-components
The example builds standalone — no need to build the rest of Reth.
Drill 1 — Read CustomPoolBuilder
Open src/main.rs. The example overrides exactly one component: pool. Find the CustomPoolBuilder struct and its PoolBuilder impl.
Skim the impl. Three sections:
- Validators — sets up the transaction validator (signature checks, nonce checks, etc.).
- Ordering — picks a tx ordering strategy (
CoinbaseTipOrderingis the default). - Construction — builds an
EthTransactionPoolwith those choices and anInMemoryBlobStore.
If your guess missed any of those three, scroll back and re-read the impl. The pool isn't a single piece — it's a composition of (validator, ordering, blob store).
Drill 2 — Add a log on every transaction
You want to see your custom code actually run. Add a log on every transaction the validator accepts.
The cleanest approach: wrap the validator in your own validator that logs and then delegates:
struct LoggingValidator<V> {
inner: V,
}
impl<V: TransactionValidator> TransactionValidator for LoggingValidator<V> {
type Transaction = V::Transaction;
async fn validate_transaction(
&self,
origin: TransactionOrigin,
transaction: Self::Transaction,
) -> TransactionValidationOutcome<Self::Transaction> {
info!(
tx_hash = %transaction.hash(),
gas_price = ?transaction.gas_price(),
"Pool: validating transaction"
);
self.inner.validate_transaction(origin, transaction).await
}
}
(Adapt to the exact TransactionValidator trait shape in your local Reth — the API drifts. The point is the structure.)
Then in CustomPoolBuilder::build_pool, wrap the underlying validator:
let inner_validator = TransactionValidationTaskExecutor::eth_builder(...)
/* ...existing setup... */
.build_with_tasks(...);
let validator = LoggingValidator { inner: inner_validator };
// pass `validator` instead of `inner_validator` to the pool
🔧 The wiring is left as the drill. The exact incantation depends on the version of
reth-poolyou're on. The point is to put your code on the path of every tx that enters the pool.
Drill 3 — Run a dev chain and watch your log fire
cargo run -- --dev
--dev starts a single-node ephemeral chain that mines blocks fast. Send a tx (any way — cast send, MetaMask pointed at localhost:8545, your own script):
cast send \
--rpc-url http://localhost:8545 \
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
--value 1ether \
0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
(Use the dev account's private key — --dev ships a known one; the one above is the standard Anvil/Reth dev key.)
Drill 4 — One more swap, sketched
You've owned pool. Now sketch what changes for one more component.
You swap executor. The custom precompile lives inside the EVM configuration. The skeleton:
.with_components(
EthereumNode::components()
.executor(CustomExecutorBuilder { extra_precompiles: vec![ed25519_verify] })
)
You'd implement ExecutorBuilder analogously to how CustomPoolBuilder implements PoolBuilder. The pattern transfers. That's the whole point of the SDK — once you've swapped one component, swapping another is mechanical.
🔍 Open
examples/custom-node-precompiles/(if it exists in your version of the repo) for a worked example of swapping the executor.
End-of-lesson recall
Without scrolling, in your own words:
- What three pieces does
CustomPoolBuilder::build_poolcompose? - Why is wrapping the validator the cleanest way to inject logging into the pool?
- If you wanted to add a custom precompile, which component would you swap?
- What's the one line change in
main.rsto use a different component builder?
After this drill, you've shipped a 1-line component swap. Scale this pattern to consensus or executor and you're building HyperEVM-class infra.
📺 Further watching
cc45Rcmrro4 | The Future of Reth (Frontiers 2025)
Summary (3 lines)
- Drill: SpamFilteringPool rejects high-tip txs. ~80-line trait impl + 1-line NodeBuilder wiring.
- Tests verify reject + accept paths. Other slots (consensus / executor / validator / RPC) unchanged.
- Production parallel: Tempo's stable-coin pool. Next: testing.