Skip to content

feat: top-of-block pre-simulation to filter reverting tx spam#465

Draft
dmarzzz wants to merge 5 commits intomainfrom
feat/presim-revert-filter
Draft

feat: top-of-block pre-simulation to filter reverting tx spam#465
dmarzzz wants to merge 5 commits intomainfrom
feat/presim-revert-filter

Conversation

@dmarzzz
Copy link
Copy Markdown
Member

@dmarzzz dmarzzz commented Apr 16, 2026

Summary

  • Pre-simulate all pending pool transactions once per block at the top-of-block state, before the flashblock loop begins
  • Reverting txs are marked in FlashblockTxCache as excluded so every flashblock iteration skips them — zero wasted EVM time on the critical path
  • Uses a random coinbase during simulation so adversaries can't detect the sim environment via COINBASE (same technique as rbuilder on L1)

Motivation and Context

The builder is spending too much time executing transactions that revert during flashblock production. The existing per-address gas limiter helps, but adversarial reverting txs still burn simulation time on every flashblock iteration. This PR moves that cost to a single pre-pass before the loop starts.

Design choices:

  • Each tx is simulated independently against parent state (no cross-tx state committed). This means future-nonce txs hit an EVM error and are kept, not falsely excluded.
  • Only loose pool txs are pre-simulated. Bundles use existing RemoveRevertedTransactions / exclude_reverts_between_flashblocks logic.
  • The pre-sim runs in a run_blocking_task to avoid blocking the tokio runtime.

New flags

Flag Default Env Description
--builder.enable-presim false BUILDER_ENABLE_PRESIM Enable top-of-block pre-simulation
--builder.presim-random-coinbase true BUILDER_PRESIM_RANDOM_COINBASE Randomize coinbase during pre-sim

New metrics (op_rbuilder scope)

  • presim_duration — histogram
  • presim_txs_simulated — histogram
  • presim_txs_excluded — counter
  • presim_gas_saved — counter

Checklist

  • cargo check -p op-rbuilder — clean
  • cargo check -p op-rbuilder --tests — clean
  • cargo fmt — clean
  • make test — integration tests added, need to run full suite
  • Manual: playground + adversarial reverting txs, confirm presim metrics emitted

Test plan

Tests in src/tests/presim.rs:

  • presim_filters_reverting_tx_without_revert_protectionkey test: presim ON, revert protection OFF, reverting tx excluded
  • presim_keeps_valid_transactions — no false positives
  • presim_disabled_by_default_includes_reverts — default behavior unchanged
  • presim_without_random_coinbase — filter still works with deterministic coinbase

🤖 Generated with Claude Code

dmarzzz and others added 4 commits April 16, 2026 17:56
Pre-simulate pending pool transactions once per block before the
flashblock loop begins. Transactions that revert are marked excluded
via FlashblockTxCache so they're skipped during all subsequent
flashblock iterations — moving the simulation cost of adversarial
reverting txs off the critical path of flashblock production.

Uses a random coinbase during simulation (configurable) to prevent
adversaries from detecting the simulation environment by checking the
COINBASE opcode. Same anti-gaming technique as rbuilder on L1.

New CLI flags (both default false/true respectively):
  --builder.enable-presim
  --builder.presim-random-coinbase

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove `with_bundle_update()` — presim is read-only, no bundle
  tracking needed
- Remove `derive(Clone)` on BlockEvmFactory — pass evm_config and
  evm_env by ref/clone instead of cloning the whole factory
- Add MAX_PRESIM_TXS (4096) cap to bound worst-case pool flooding
- Rename presim_duration → presim_pass_duration to avoid confusion
  with existing tx_simulation_duration metric
- Document known limitations: parent-state-vs-post-deposit divergence
  and coinbase balance detection bypass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fail-open: presim errors are logged and skipped instead of aborting
  the entire block build. Presim is an optimization, not required for
  correctness.
- Add PRESIM_DEADLINE (500ms) time budget to prevent adversarial txs
  from turning presim into a self-inflicted DoS on the first
  flashblock.
- Document gas revenue tradeoff: excluded reverting txs don't pay the
  builder, which is an explicit opt-in tradeoff.
- Document defense-in-depth: mid-block pool arrivals after presim are
  caught by exclude_reverts_between_flashblocks during building.
- Document independent simulation limitation for multi-tx flows.
- Make presim_txs_excluded a Histogram (consistent with siblings).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Build an initial empty block before presim tests so parent state has
  fully committed genesis state (funded accounts). Presim runs against
  parent state, so block 1 wouldn't see genesis funding without this.
- Add info-level logs for presim completion and exclusion count so CI
  output shows whether presim actually ran.
- Promote presim summary log from debug to info for observability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
// warmed fallback cache. Txs arriving mid-block after presim are
// caught by the existing exclude_reverts_between_flashblocks
// mechanism during building (defense in depth).
if self.config.presim_enabled {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this comment is not correct - looking in context.rs where exclude_reverts_between_flashblocks is actually used, it's only used for bundle txs, normal non-bundle txs don't touch that path at all

//!
//! - **Reverting txs forfeit gas revenue.** Excluded txs are never
//! included in the block, so the builder does not collect their gas
//! fees. This is an explicit tradeoff: faster flashblock production
Copy link
Copy Markdown
Contributor

@noot noot Apr 17, 2026

Choose a reason for hiding this comment

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

how does this allow for faster flashblock production? we're still executing the tx either way. do you mean faster flashblock syncing?

Comment on lines +30 to +33
//! - **Independent simulation.** Each tx is simulated against the same
//! top-of-block state without committing prior results. Multi-tx
//! flows from one sender (approve → transferFrom) may be falsely
//! excluded if the second tx depends on the first's state changes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

just flagging this again - seems like an issue

/// Maximum wall-clock time for the presim pass. Prevents adversarial
/// txs that maximize EVM compute from turning presim into a
/// self-inflicted DoS on the first flashblock.
const PRESIM_DEADLINE: Duration = Duration::from_millis(500);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

seems long considering flashblock time is 200ms

Comment on lines +112 to +144
while let Some(tx) = best_txs.next(()) {
if simulated >= MAX_PRESIM_TXS {
debug!(target: "payload_builder", limit = MAX_PRESIM_TXS, "presim: tx limit reached");
break;
}
if Instant::now() >= deadline {
debug!(target: "payload_builder", budget_ms = PRESIM_DEADLINE.as_millis(), "presim: time budget exceeded");
break;
}

let tx_hash = *tx.hash();
let recovered = tx.into_consensus();
simulated += 1;

let sim = evm_config
.evm_with_env(&mut state, env.clone())
.transact(&recovered);

match sim {
Ok(exec) if !exec.result.is_success() => {
let gas = exec.result.gas_used();
debug!(
target: "payload_builder",
%tx_hash,
gas_used = gas,
"presim: excluding reverting transaction"
);
result.excluded.push(tx_hash);
gas_saved = gas_saved.saturating_add(gas);
}
Err(_) | Ok(_) => {}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this can be run in parallel since they all run on top of the same parent-state independently - spawn a tokio task for each, use a JoinSet or something to await for them all to finish (or drop once deadline/limit is reached)

Presim was creating State without with_bundle_update(), which may cause
transact() to return incorrect results for CREATE txs with reverting
init code. CI showed simulated=2 excluded=0 — both txs appeared to
succeed in presim despite one being a reverting tx.

Restoring with_bundle_update() to match the State construction used
by the flashblock builder (context.rs). Also adds per-tx debug logs
for EVM results to aid future debugging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants