diff --git a/crates/builder/op-rbuilder/src/args/op.rs b/crates/builder/op-rbuilder/src/args/op.rs index 6d17d93c..f8c704eb 100644 --- a/crates/builder/op-rbuilder/src/args/op.rs +++ b/crates/builder/op-rbuilder/src/args/op.rs @@ -113,6 +113,16 @@ pub struct FlashblocksArgs { env = "FLASHBLOCKS_DISABLE_STATE_ROOT" )] pub flashblocks_disable_state_root: bool, + + /// Whether to enforce priority fee ordering within flashblocks. + /// When enabled, transactions that would violate descending priority fee order are skipped + /// and deferred to the next flashblock. + #[arg( + long = "flashblocks.enforce-priority-fee-ordering", + default_value = "true", + env = "FLASHBLOCKS_ENFORCE_PRIORITY_FEE_ORDERING" + )] + pub flashblocks_enforce_priority_fee_ordering: bool, } impl Default for FlashblocksArgs { diff --git a/crates/builder/op-rbuilder/src/flashblocks/config.rs b/crates/builder/op-rbuilder/src/flashblocks/config.rs index 69d4d007..3eb87a1b 100644 --- a/crates/builder/op-rbuilder/src/flashblocks/config.rs +++ b/crates/builder/op-rbuilder/src/flashblocks/config.rs @@ -33,6 +33,9 @@ pub struct FlashblocksConfig { /// Should we disable state root calculation for each flashblock pub disable_state_root: bool, + + /// Whether to enforce priority fee ordering within flashblocks + pub enforce_priority_fee_ordering: bool, } impl Default for FlashblocksConfig { @@ -43,6 +46,7 @@ impl Default for FlashblocksConfig { leeway_time: Duration::from_millis(50), fixed: false, disable_state_root: false, + enforce_priority_fee_ordering: true, } } } @@ -64,7 +68,17 @@ impl TryFrom for FlashblocksConfig { let disable_state_root = args.flashblocks.flashblocks_disable_state_root; - Ok(Self { ws_addr, interval, leeway_time, fixed, disable_state_root }) + let enforce_priority_fee_ordering = + args.flashblocks.flashblocks_enforce_priority_fee_ordering; + + Ok(Self { + ws_addr, + interval, + leeway_time, + fixed, + disable_state_root, + enforce_priority_fee_ordering, + }) } } diff --git a/crates/builder/op-rbuilder/src/flashblocks/context.rs b/crates/builder/op-rbuilder/src/flashblocks/context.rs index 95ed07ba..7a1fecd8 100644 --- a/crates/builder/op-rbuilder/src/flashblocks/context.rs +++ b/crates/builder/op-rbuilder/src/flashblocks/context.rs @@ -38,7 +38,7 @@ use reth_revm::{State, context::Block}; use reth_transaction_pool::{BestTransactionsAttributes, PoolTransaction}; use revm::{DatabaseCommit, context::result::ResultAndState, interpreter::as_u64_saturated}; use tokio_util::sync::CancellationToken; -use tracing::{debug, info, trace}; +use tracing::{debug, info, trace, warn}; use crate::{ flashblocks::payload::FlashblocksExecutionInfo, @@ -117,6 +117,8 @@ pub struct OpPayloadBuilderCtx { pub address_gas_limiter: AddressGasLimiter, /// Unified transaction data store (backrun bundles + resource metering) pub tx_data_store: TxDataStore, + /// Whether to enforce priority fee ordering within flashblocks + pub enforce_priority_fee_ordering: bool, } impl OpPayloadBuilderCtx { @@ -442,6 +444,14 @@ impl OpPayloadBuilderCtx { timestamp: self.attributes().timestamp(), }; + // Track the last priority fee to enforce descending order. + // Due to a reth bug (https://github.com/paradigmxyz/reth/pull/19940), blocked + // transactions are discarded instead of saved, breaking nonce chain tracking. + // Later transactions from the same sender can't find the blocked one, so they + // get processed as if they have no dependencies and may be yielded out of order. + // This check skips out-of-order txs, deferring them to the next flashblock. + let mut last_priority_fee: Option = None; + while let Some(tx) = best_txs.next(()) { let interop = tx.interop_deadline(); let conditional = tx.conditional().cloned(); @@ -462,6 +472,27 @@ impl OpPayloadBuilderCtx { num_txs_considered += 1; + // Check priority fee ordering - skip if current tx has higher priority than last. + if self.enforce_priority_fee_ordering + && let Some(current_priority) = tx.effective_tip_per_gas(base_fee) + { + if let Some(last_priority) = last_priority_fee + && current_priority > last_priority + { + warn!( + target: "payload_builder", + tx_hash = ?tx_hash, + current_priority = current_priority, + last_priority = last_priority, + "Skipping transaction due to priority fee ordering violation" + ); + self.metrics.priority_fee_ordering_violations.increment(1); + best_txs.mark_invalid(tx.signer(), tx.nonce()); + continue; + } + last_priority_fee = Some(current_priority); + } + let TxData { metering: _resource_usage, backrun_bundles } = self.tx_data_store.get(&tx_hash); diff --git a/crates/builder/op-rbuilder/src/flashblocks/ctx.rs b/crates/builder/op-rbuilder/src/flashblocks/ctx.rs index 5e20b2ab..430a8e21 100644 --- a/crates/builder/op-rbuilder/src/flashblocks/ctx.rs +++ b/crates/builder/op-rbuilder/src/flashblocks/ctx.rs @@ -88,6 +88,7 @@ impl OpPayloadSyncerCtx { max_gas_per_txn: self.max_gas_per_txn, address_gas_limiter: AddressGasLimiter::new(GasLimiterArgs::default()), tx_data_store: self.tx_data_store, + enforce_priority_fee_ordering: true, // default to enabled for tests } } } diff --git a/crates/builder/op-rbuilder/src/flashblocks/payload.rs b/crates/builder/op-rbuilder/src/flashblocks/payload.rs index 48d2933a..aa4cfe72 100644 --- a/crates/builder/op-rbuilder/src/flashblocks/payload.rs +++ b/crates/builder/op-rbuilder/src/flashblocks/payload.rs @@ -201,6 +201,7 @@ where max_gas_per_txn: self.config.max_gas_per_txn, address_gas_limiter: self.address_gas_limiter.clone(), tx_data_store: self.config.tx_data_store.clone(), + enforce_priority_fee_ordering: self.config.flashblocks.enforce_priority_fee_ordering, }) } diff --git a/crates/builder/op-rbuilder/src/metrics.rs b/crates/builder/op-rbuilder/src/metrics.rs index 6c575aea..3dc2c0fd 100644 --- a/crates/builder/op-rbuilder/src/metrics.rs +++ b/crates/builder/op-rbuilder/src/metrics.rs @@ -181,6 +181,8 @@ pub struct OpRBuilderMetrics { pub backrun_bundle_insert_duration: Histogram, /// Duration of executing all backrun bundles for a target transaction pub backrun_bundle_execution_duration: Histogram, + /// Number of transactions skipped due to priority fee ordering violations + pub priority_fee_ordering_violations: Counter, } impl OpRBuilderMetrics { diff --git a/crates/builder/op-rbuilder/src/tests/flashblocks.rs b/crates/builder/op-rbuilder/src/tests/flashblocks.rs index 56270514..eb9fd1d3 100644 --- a/crates/builder/op-rbuilder/src/tests/flashblocks.rs +++ b/crates/builder/op-rbuilder/src/tests/flashblocks.rs @@ -228,6 +228,7 @@ async fn test_flashblocks_no_state_root_calculation() -> eyre::Result<()> { flashblocks_leeway_time: 100, flashblocks_fixed: false, flashblocks_disable_state_root: true, + flashblocks_enforce_priority_fee_ordering: true, }, ..Default::default() };