Skip to content
This repository was archived by the owner on Jan 12, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions crates/op-rbuilder/src/builders/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,19 @@ impl<ExtraCtx: Debug + Default> OpPayloadBuilderCtx<ExtraCtx> {
let ResultAndState { result, state } = match evm.transact(&consensus_tx) {
Ok(res) => res,
Err(err) => {
if err.as_invalid_tx_err().is_some() {
self.metrics.backrun_bundles_invalid_tx_total.increment(1);
info!(
target: "payload_builder",
target_tx = ?tx_hash,
failed_tx = ?backrun_tx.hash(),
bundle_id = ?stored_bundle.bundle_id,
error = %err,
"Backrun bundle failed with invalid tx error"
);
continue 'bundle_loop;
}
self.metrics.backrun_bundles_fatal_error_total.increment(1);
return Err(PayloadBuilderError::evm(err));
}
};
Expand Down
4 changes: 4 additions & 0 deletions crates/op-rbuilder/src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ pub struct OpRBuilderMetrics {
pub backrun_bundles_received_total: Counter,
/// Number of backrun bundles that reverted during execution (all-or-nothing)
pub backrun_bundles_reverted_total: Counter,
/// Number of backrun bundles skipped due to invalid tx errors (nonce too low, etc.)
pub backrun_bundles_invalid_tx_total: Counter,
/// Number of backrun bundles that caused fatal EVM errors
pub backrun_bundles_fatal_error_total: Counter,
/// Number of backrun bundles rejected due to priority fee below target tx
pub backrun_bundles_rejected_low_fee_total: Counter,
/// Number of backrun bundles rejected due to exceeding block limits
Expand Down
91 changes: 91 additions & 0 deletions crates/op-rbuilder/src/tests/backrun.rs
Original file line number Diff line number Diff line change
Expand Up @@ -560,3 +560,94 @@ async fn backrun_bundle_rejected_exceeds_da_limit(rbuilder: LocalInstance) -> ey

Ok(())
}

/// Tests that backrun bundles with invalid tx errors (e.g. nonce too low) are skipped gracefully
#[rb_test(flashblocks)]
async fn backrun_bundle_invalid_tx_skipped(rbuilder: LocalInstance) -> eyre::Result<()> {
let driver = rbuilder.driver().await?;
let accounts = driver.fund_accounts(3, ONE_ETH).await?;

let target_tx = driver
.create_transaction()
.with_signer(accounts[0])
.with_max_priority_fee_per_gas(20)
.build()
.await;
let target_tx_hash = target_tx.tx_hash().clone();

let provider = rbuilder.provider().await?;
let _ = provider
.send_raw_transaction(target_tx.encoded_2718().as_slice())
.await?;

let backrun_tx = driver
.create_transaction()
.with_signer(accounts[1])
.with_max_priority_fee_per_gas(50)
.with_nonce(0)
.build()
.await;
let backrun_tx_hash = backrun_tx.tx_hash().clone();

// Send a conflicting tx with same nonce but higher fee - it will be included first
let conflicting_tx = driver
.create_transaction()
.with_signer(accounts[1])
.with_max_priority_fee_per_gas(100)
.with_nonce(0)
.send()
.await?;
let conflicting_tx_hash = conflicting_tx.tx_hash().clone();

let bundle = AcceptedBundle {
uuid: Uuid::new_v4(),
txs: vec![target_tx, backrun_tx],
block_number: driver.latest().await?.header.number + 1,
flashblock_number_min: None,
flashblock_number_max: None,
min_timestamp: None,
max_timestamp: None,
reverting_tx_hashes: vec![],
replacement_uuid: None,
dropping_tx_hashes: vec![],
meter_bundle_response: MeterBundleResponse {
bundle_gas_price: U256::ZERO,
bundle_hash: TxHash::ZERO,
coinbase_diff: U256::ZERO,
eth_sent_to_coinbase: U256::ZERO,
gas_fees: U256::ZERO,
results: vec![],
state_block_number: 0,
state_flashblock_index: None,
total_gas_used: 0,
total_execution_time_us: 0,
},
};

rbuilder
.tx_data_store()
.insert_backrun_bundle(bundle)
.expect("Failed to insert backrun bundle");

driver.build_new_block().await?;

let block = driver.latest_full().await?;
let tx_hashes: Vec<_> = block.transactions.hashes().collect();

assert!(
tx_hashes.contains(&target_tx_hash),
"Target tx should be included in block"
);

assert!(
tx_hashes.contains(&conflicting_tx_hash),
"Conflicting tx should be included in block"
);

assert!(
!tx_hashes.contains(&backrun_tx_hash),
"Backrun tx should NOT be in block (nonce-too-low EVM error)"
);

Ok(())
}
Loading