FABRKNT
Inside Reth — Sync, Extensions, and the SDK
The Reth Stack — Sync, Extensions, and the SDK
Lesson 14 of 17·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 Reth — Sync, Extensions, and the SDK
Lesson role
CONTENT
Sequence
14 / 17

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 if tx.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:

  1. Validators — sets up the transaction validator (signature checks, nonce checks, etc.).
  2. Ordering — picks a tx ordering strategy (CoinbaseTipOrdering is the default).
  3. Construction — builds an EthTransactionPool with those choices and an InMemoryBlobStore.

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-pool you'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:

  1. What three pieces does CustomPoolBuilder::build_pool compose?
  2. Why is wrapping the validator the cleanest way to inject logging into the pool?
  3. If you wanted to add a custom precompile, which component would you swap?
  4. What's the one line change in main.rs to 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.