feat: add strict priority fee ordering mode for backrun bundles#410
feat: add strict priority fee ordering mode for backrun bundles#410
Conversation
Introduces `--backruns.enforce_strict_priority_fee_ordering` flag that, when enabled, requires backrun priority fees to exactly match the target tx fee (instead of >=), sorts candidates by declared `coinbase_profit` rather than estimated priority fee, and rejects backruns whose actual coinbase profit is less than declared.
There was a problem hiding this comment.
Pull request overview
Adds a strict backrun-bundle ordering mode that enforces exact priority-fee matching and uses declared coinbase_profit for candidate ordering, with commit-time validation to prevent overstated profits.
Changes:
- Introduce
--backruns.enforce_strict_priority_fee_orderingand plumb it through builder config, global/payload pools, and RPC wiring. - Extend backrun bundle RPC/pool data model with optional
coinbase_profit, and sort by it in strict mode. - Add strict-mode tests and new execution result for coinbase-profit rejection.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/op-rbuilder/src/backrun_bundle/args.rs | Adds CLI flag for strict priority-fee ordering mode. |
| crates/op-rbuilder/src/backrun_bundle/mod.rs | Updates module docs to describe strict ordering behavior. |
| crates/op-rbuilder/src/backrun_bundle/rpc.rs | Adds coinbaseProfit RPC arg, enforces it in strict mode, and passes through to stored bundle. |
| crates/op-rbuilder/src/backrun_bundle/payload_pool.rs | Adds coinbase_profit to stored bundles; changes ordering to use coinbase profit in strict mode; adds strict fee prefiltering. |
| crates/op-rbuilder/src/backrun_bundle/global_pool.rs | Makes global pool aware of strict-mode setting when creating payload pools. |
| crates/op-rbuilder/src/backrun_bundle/test_utils.rs | Extends backrun bundle test builder to set coinbase_profit. |
| crates/op-rbuilder/src/builders/mod.rs | Wires strict-mode flag into BackrunBundleGlobalPool construction. |
| crates/op-rbuilder/src/builders/context.rs | Enforces strict fee equality and validates actual vs declared coinbase profit during backrun commit. |
| crates/op-rbuilder/src/launcher.rs | Passes strict-mode flag into BackrunBundleRpc::new. |
| crates/op-rbuilder/src/tests/framework/instance.rs | Passes strict-mode flag into test instance RPC wiring. |
| crates/op-rbuilder/src/tests/framework/txs.rs | Extends test bundle opts with coinbase_profit and sends it for backrun bundles. |
| crates/op-rbuilder/src/tests/backrun.rs | Adds integration tests covering strict mode rejection/acceptance and ordering by coinbase_profit. |
| crates/op-rbuilder/src/primitives/reth/execution.rs | Adds CoinbaseProfitTooLow execution result variant. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let priority = if use_coinbase_profit { | ||
| bundle.coinbase_profit.unwrap_or_default() | ||
| } else { | ||
| U256::from(bundle.estimated_effective_priority_fee) |
There was a problem hiding this comment.
are they reinserted every block? Or it would be static once bundle created?
There was a problem hiding this comment.
good question, I think it might change
on the builder side we can remove on hash collision just in case.
but I would expect simulator to send to the non-overlapping block ranges if re simulation happens
There was a problem hiding this comment.
my point is that on next block effective fee could change and this priority needs to be updated
There was a problem hiding this comment.
I think we can enforce that you can have only one (target tx, backrun tx) pair at the same time. I need to think a bit about it and if we add this this is IMO better to do as a separate PR since I need functions from the cancellation PR.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| impl PartialEq for OrderedBackrunBundle { | ||
| fn eq(&self, other: &Self) -> bool { | ||
| self.backrun_tx_hash() == other.backrun_tx_hash() | ||
| self.backrun_tx_hash() == other.backrun_tx_hash() && self.priority == other.priority | ||
| } |
There was a problem hiding this comment.
OrderedBackrunBundle equality/uniqueness is now based on (backrun_tx_hash, priority) rather than just backrun_tx_hash. Because the BTreeSet ordering also uses priority first, the same signed backrun tx can be inserted multiple times with different declared coinbase_profit/estimated fee, and remove_bundle / replacement cleanup will only remove the matching priority variant. Consider enforcing uniqueness by backrun tx hash (e.g., make Ord/Eq treat identical hashes as equal, or switch to a map keyed by hash and derive ordering at query time).
| tx_backruns | ||
| .bundles | ||
| .iter() | ||
| .take(max_iter) | ||
| .filter(|ordered| { | ||
| let backrun_tx = &ordered.0.backrun_tx; | ||
| let backrun_tx = &ordered.bundle.backrun_tx; |
There was a problem hiding this comment.
get_backruns truncates the candidate set with .iter().take(max_iter) before applying the strict priority-fee filter. In strict mode, bundles are ordered by untrusted coinbase_profit, so a large number of high-profit but fee-mismatched bundles can crowd out fee-matching ones beyond max_iter, causing valid backruns to be missed (and enabling cheap starvation/DoS). Consider restructuring selection to ensure fee-matching candidates are discoverable within the scan budget (e.g., filter by effective fee first, then rank by coinbase_profit, or increase/adapt the scan limit in strict mode).
| } | ||
|
|
||
| if self.backrun_ctx.args.enforce_strict_priority_fee_ordering { | ||
| let stated = bundle.coinbase_profit.unwrap_or_default(); |
There was a problem hiding this comment.
In strict mode, stated coinbase profit is derived via bundle.coinbase_profit.unwrap_or_default(), so a missing coinbase_profit silently becomes 0 and will always pass the actual < stated check. This contradicts the strict-mode contract enforced at the RPC layer and the module docs (“Required when enforce_strict_priority_fee_ordering is enabled”). Consider treating None as invalid here (log and skip) to keep the invariants consistent even if bundles enter the pool through non-RPC paths.
| let stated = bundle.coinbase_profit.unwrap_or_default(); | |
| let Some(stated) = bundle.coinbase_profit else { | |
| // In strict mode, a missing stated coinbase profit is invalid. | |
| // Log and skip to keep invariants consistent with RPC-layer checks. | |
| log_br_txn(TxnExecutionResult::CoinbaseProfitTooLow); | |
| continue; | |
| }; |
📝 Summary
Introduces
--backruns.enforce_strict_priority_fee_orderingflag that, when set, requires backrun priority fees to exactly match the target tx fee (instead of >=), sorts candidates by declaredcoinbase_profitrather than estimated priority fee, and rejects backruns whose actual coinbase profit is less than declared.💡 Motivation and Context
For some chains we need to strictly keep priority fee ordering so we
require backruns to match the target transaction's priority fee. To
select better backruns we use coinbase profit (it's expected that searchers will send ETH directly to coinbase). We don't want to simulate bundles to get their coinbase profit so we assume accurate values from
the RPC but verify them when we include the bundle to minimize abuse of ordering.
✅ I have completed the following steps:
make lintmake test