Skip to content

Conversation

@defistar
Copy link

@defistar defistar commented Jan 29, 2026

PR: Add Cache Pre-Warming Infrastructure

Overview

Add two-level cache to reduce database queries during block building.

flowchart TB
    subgraph "Current Implementation"
        A[Block Building] --> B[cached_reads<br/>previous block]
        B --> C[Database]
        B -.->|cache miss| C
    end
    
    subgraph "Target Architecture"
        D[Background Job<br/>continuous loop] -.->|update| E[pre_warmed cache]
        D -->|get top N| F[Mempool]
        D -->|simulate| G[Pre-load state]
        
        H[Block Building] --> E
        E -->|L1: check| I{hit?}
        I -->|yes| J[Return]
        I -->|no| K[cached_reads<br/>previous block]
        K -->|L2: check| L{hit?}
        L -->|yes| J
        L -->|no| M[Database]
        M --> J
    end
Loading

Lookup order: pre_warmed (mempool) -> cached_reads (previous block) -> database


Sequence Diagram

Current Flow (Before This PR)

sequenceDiagram
    participant Builder as OpPayloadBuilder
    participant CacheDB as CachedReadsDbMut
    participant Base as cached_reads
    participant DB as Database
    
    Builder->>CacheDB: build(cached_reads.as_db_mut(state))
    CacheDB->>Base: basic(address)?
    alt Account in cache
        Base-->>CacheDB: Return cached account
    else Cache miss
        Base->>DB: Query database
        DB-->>Base: Account data
        Base-->>CacheDB: Return account
    end
Loading

New Flow (After This PR)

sequenceDiagram
    participant BG as Background Job<br/>(TODO)
    participant Sim as TransactionSimulator
    participant Gen as BasicPayloadJobGenerator
    participant Builder as OpPayloadBuilder
    participant PreDB as PreWarmedCachedReadsDbMut
    participant PreCache as pre_warmed
    participant BaseCache as cached_reads
    participant DB as Database
    
    Note over BG,Gen: Background Pre-warming (Every 6s)
    BG->>Gen: pool.best_transactions().take(50)
    BG->>Sim: simulate_transactions(txs, parent)
    Sim-->>BG: CachedReads
    BG->>Gen: set_pre_warmed(PreWarmedCache)
    Note over Gen: Merges with existing cache<br/>if not stale
    
    Note over Gen,DB: Block Building
    Builder->>Gen: new_payload_job(attributes)
    Gen->>Gen: maybe_pre_warmed(parent_hash)
    alt Cache valid
        Gen-->>Builder: Some(pre_warmed)
    else Cache stale/missing
        Gen-->>Builder: None
    end
    
    alt Pre-warmed available
        Builder->>PreDB: PreWarmedCachedReadsDbMut::new(pre_warmed, cached_reads, state)
        Builder->>PreDB: build(db)
        PreDB->>PreCache: basic(address)?
        alt L1: Hit in pre_warmed
            PreCache-->>PreDB: Return cached
        else L1 miss
            PreDB->>BaseCache: basic(address)?
            alt L2: Hit in cached_reads
                BaseCache-->>PreDB: Return cached
            else L2 miss
                BaseCache->>DB: Query database
                DB-->>BaseCache: Account data
                BaseCache-->>PreDB: Return data
            end
        end
    else Pre-warmed unavailable (fallback)
        Builder->>BaseCache: cached_reads.as_db_mut(state)
        Builder->>BaseCache: build(db)
        Note over BaseCache,DB: Same as current flow
    end
Loading

Core Changes

1. Two-Level Cache Lookup

Package: reth-revm
File: crates/revm/src/cached.rs

Added PreWarmedCachedReadsDbMut struct for two-level cache lookup:

  • Level 1: Pre-warmed cache (mempool simulations)
  • Level 2: Base cache (previous block state)
  • Fallback: Database

2. Transaction Simulator

Package: reth-basic-payload-builder
File: crates/payload/basic/src/simulator.rs (new)

Simulates transactions to pre-load state:

  • Loads sender/recipient accounts
  • Loads contract code if needed
  • Returns CachedReads for pre-warming

Why in reth-basic-payload-builder?

  • Purpose-built for pre-warming the payload builder cache
  • Only used by the background job in the same package
  • Already has all required dependencies (reth-provider, reth-revm, reth-evm, reth-chainspec)
  • Keeps pre-warming logic encapsulated in one package
  • If general-purpose transaction simulation is needed elsewhere later, it can be extracted to a separate package

3. Pre-Warming Configuration

Package: reth-basic-payload-builder
File: crates/payload/basic/src/lib.rs

Added configurable pre-warming:

pub struct PreWarmingConfig {
    pub enabled: bool,              // Feature flag - controls all pre-warming behavior
    pub tx_count: usize,            // Default: 50 (configurable)
    pub interval_secs: u64,         // Default: 6 seconds
    pub staleness_multiplier: u64,  // Default: 2 (cache timeout = interval * multiplier)
}

Feature Flag (enabled):

  • Master switch for pre-warming feature
  • When false: All pre-warming logic disabled (no cache updates, no lookups)
  • Default: false (safe for production until background job implemented)
  • Can be toggled at runtime via configuration
  • Allows safe rollback if issues detected

Added PreWarmedCache struct:

  • Holds simulated transaction state
  • Merges with existing cache on updates
  • Time-based staleness (not block-based)
  • No transaction deduplication tracking (keeps it simple)

4. Payload Builder Integration

Package: reth-optimism-payload-builder
File: crates/optimism/payload/src/builder.rs

Uses two-level cache when available:

if let Some(pre_warmed) = pre_warmed {
    let db = PreWarmedCachedReadsDbMut::new(&pre_warmed, &mut cached_reads, state);
    builder.build(db, ...)
} else {
    builder.build(cached_reads.as_db_mut(state), ...)
}

Graceful fallback if pre-warmed cache unavailable.


Other Files

Integration Updates

  • crates/payload/basic/src/stack.rs - Pass pre-warmed cache through
  • crates/ethereum/payload/src/lib.rs - Compatibility with new structure
  • examples/custom-engine-types/src/main.rs - Example compatibility

Dependencies

  • crates/payload/basic/Cargo.toml - Added 6 workspace dependencies (all internal)

Key Design Decisions

1. Time-Based Staleness (Not Block-Based)

Pre-warmed cache valid until timeout, not tied to specific parent block.

  • Mempool transactions valid across multiple blocks
  • Staleness threshold: interval_secs * 2 (derived from config)
  • Example: 6 second interval = 12 second max cache age
  • Maximizes cache reuse across blocks

2. No Transaction Deduplication

Background job re-simulates top N every cycle, no tracking.

  • Simple implementation: just simulate current top N
  • Mempool self-cleans (included txs removed automatically)
  • No HashSet overhead or deduplication logic
  • Cache merging handles duplicates naturally (via extend())

3. Cache Replacement (Not Merging)

New simulations replace existing cache entirely.

  • Prevents mixed state from different parent blocks
  • Each cache snapshot is consistent (all from same simulation cycle)
  • Simpler reasoning about cache correctness
  • Production-safe approach

4. Configurable Transaction Count

Default 50 based on X Layer reality (typical blocks: 1-50 tx).

  • Can be tuned without code changes
  • Based on actual block sizes observed

Configuration

Default Values (X Layer Optimized)

PreWarmingConfig {
    enabled: false,              // Feature flag - disabled by default
    tx_count: 50,                // Typical X Layer blocks: 1-50 tx
    interval_secs: 6,            // 15 blocks @ 400ms block time
    staleness_multiplier: 2,     // Cache timeout = interval * multiplier
}

Feature Control

enabled flag:

  • Master switch for entire pre-warming feature
  • When false (default):
    • maybe_pre_warmed() returns None immediately
    • set_pre_warmed() is no-op (ignores updates)
    • Block building falls back to single-cache behavior
    • Zero overhead, zero risk
  • When true:
    • Pre-warmed cache lookups enabled
    • Cache updates accepted from background job
    • Two-level cache active

Production safety:

  • Default false until background job implemented and validated
  • Can be enabled/disabled via node configuration
  • Allows safe experimentation and rollback
  • No code changes needed to toggle feature

How staleness works:

  • Cache timeout = interval_secs * staleness_multiplier
  • Default: 6 seconds * 2 = 12 seconds max cache age
  • When stale: entire cache discarded and replaced (not per-record expiry)
  • Fully configurable via PreWarmingConfig
  • Implemented in maybe_pre_warmed() and set_pre_warmed() methods

Testing

All tests passing:

  • reth-basic-payload-builder: 3/3 passed (including feature flag tests)
  • reth-revm (cache tests): 2/2 passed
  • reth-optimism-payload-builder: 26/26 passed

Feature flag tests verify:

  • Pre-warming disabled by default (enabled = false)
  • Default configuration values (tx_count=50, interval=6s, staleness_multiplier=2)

Files Changed

Package File What Changed Why
reth-revm crates/revm/src/cached.rs Added PreWarmedCachedReadsDbMut struct with Database trait impl for two-level lookup (pre-warmed → base → database) Core cache hierarchy
reth-basic-payload-builder crates/payload/basic/src/lib.rs Added PreWarmingConfig struct with enabled flag, PreWarmedCache struct, updated BasicPayloadJobGeneratorConfig with pre_warming field, added set_pre_warmed() and maybe_pre_warmed() methods with enabled checks Configuration, cache storage, and feature control
reth-basic-payload-builder crates/payload/basic/src/simulator.rs NEW FILE: Added TransactionSimulator struct with simulate_transaction() and simulate_transactions() methods to pre-load sender/recipient accounts Transaction simulation logic
reth-optimism-payload-builder crates/optimism/payload/src/builder.rs Modified build_payload() to use PreWarmedCachedReadsDbMut when pre_warmed is Some, else fallback to existing cached_reads.as_db_mut() Integrate two-level cache
reth-basic-payload-builder crates/payload/basic/src/stack.rs Updated BuildArguments destructuring to include pre_warmed field Pass-through compatibility
reth-ethereum-payload-builder crates/ethereum/payload/src/lib.rs Updated BuildArguments::new() call to include pre_warmed: None Struct compatibility
example-custom-engine-types examples/custom-engine-types/src/main.rs Updated BuildArguments destructuring to include pre_warmed field Example compatibility
reth-basic-payload-builder crates/payload/basic/Cargo.toml Added 6 workspace dependencies: reth-chainspec, reth-evm, reth-primitives, reth-provider, revm, thiserror Simulator dependencies

Total: 8 files (1 new, 7 modified)


Backward Compatibility

  • No breaking changes
  • Graceful fallback if pre-warmed cache unavailable
  • Disabled by default (enabled: false)
  • All existing tests pass

Production Safety Verification

When enabled = false (Verified):

  1. Cache Lookups Disabled

    • maybe_pre_warmed() returns None immediately (lib.rs:139)
    • Block building uses single-cache: cached_reads.as_db_mut(state) (builder.rs:246)
    • No two-level cache lookup performed
    • Zero overhead
  2. Cache Updates Disabled

    • set_pre_warmed() returns immediately without updating (lib.rs:168)
    • Even if background job calls it, cache not modified
    • No memory allocation
  3. Background Job (TODO - Next PR)

    • MUST check config.enabled in main loop
    • When false: sleep and skip simulation
    • No transaction simulation
    • No cache updates

Complete Shutdown Path:

enabled = false
    ↓
Background Job → Checks flag → Skips simulation
    ↓
set_pre_warmed() → Early return (no-op)
    ↓
maybe_pre_warmed() → Returns None
    ↓
Block Building → Uses base cache only

What Gets Disabled:

  • Transaction simulation (background job)
  • Cache population (set_pre_warmed)
  • Cache lookups (maybe_pre_warmed)
  • Two-level cache logic (PreWarmedCachedReadsDbMut)

What Stays Active:

  • Single-cache behavior (cached_reads → database)
  • Block building continues normally
  • Zero functional change from current behavior

Implementation Status

[DONE] Completed (This PR)

  • Two-level cache data structure (PreWarmedCachedReadsDbMut)
  • Transaction simulator (TransactionSimulator)
  • Configuration structure (PreWarmingConfig)
  • Cache storage and merging logic
  • Payload builder integration (Optimism)
  • All tests passing

[TODO] Pending

Background Job (MUST check enabled flag):

// Production implementation for background task
loop {
    // CRITICAL: Check if pre-warming is enabled
    if !config.enabled {
        // Feature disabled - sleep and check again
        sleep(config.interval_secs).await;
        continue;
    }
    
    let top_txs = pool.best_transactions().take(config.tx_count).collect();
    let cache = simulator.simulate_transactions(top_txs)?;
    generator.set_pre_warmed(PreWarmedCache { created_at: now(), cached: cache });
    sleep(config.interval_secs).await;
}

Node Integration:

  • Spawn background task on node startup
  • Background task MUST check config.enabled in loop
  • When disabled: task sleeps, no simulation, no cache updates

Complete shutdown when disabled:

  1. Background job: Checks enabled flag, skips simulation if false
  2. Cache updates: set_pre_warmed() is no-op if enabled = false
  3. Cache lookups: maybe_pre_warmed() returns None if enabled = false
  4. Block building: Falls back to single-cache (base → database)

Until background job implemented, enabled: false by default (safe).

@defistar defistar changed the base branch from main to dev January 29, 2026 06:29
@defistar defistar self-assigned this Jan 29, 2026
@defistar defistar added the enhancement New feature or request label Jan 29, 2026
) -> Result<CachedReads, SimulationError> {
let mut merged_cache = CachedReads::default();

for (idx, tx) in transactions.into_iter().enumerate() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be done concurrently?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is to be changed to happen concurrently. on it

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe can use rayon parallel iterator?

chain_spec: Arc<ChainSpec>,
}

impl<Client, Evm> TransactionSimulator<Client, Evm>
Copy link

@cliff0412 cliff0412 Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are several flaws with existing approach

  1. simulate set can differ from the actual txs executed
  2. Storage Slots Not Cached
  3. simulation is sequential

we can try a different strategy
Instead of caching account balances and storage values (which become stale), cache which addresses and storage slots
each transaction will touch. At build time, use these access patterns to batch-fetch fresh state from the database.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pool.add_transaction()
        ↓
    Transaction validated & added to pool
        ↓
    Returns hash to user ✓
        ↓
    Trigger simulation (fire-and-forget)
        ↓
    [mpsc::unbounded_channel]
        ↓
    ┌────┬────┬────┬────┐
Worker 1  Worker 2  Worker 3  Worker 4
    ↓       ↓       ↓       ↓
Simulate (EVM execution - read-only)
    ↓       ↓       ↓       ↓
Extract keys (accounts, storage slots)
    ↓       ↓       ↓       ↓
Store in PreWarmedCache

this is planned design

@cliff0412
Copy link

need to add an engine integration test to ensure it is working properly

@defistar defistar closed this Jan 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants