Skip to content

Commit c3c5f02

Browse files
committed
detect used priority update by rejecting reads in reverting call frames
1 parent 5c33287 commit c3c5f02

6 files changed

Lines changed: 517 additions & 103 deletions

File tree

crates/rbuilder/src/backtest/build_block/full_partial_block_execution_tracer.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,9 @@ impl PartialBlockExecutionTracer for FullPartialBlockExecutionTracer {
237237
ExecutionError::PriorityUpdateProducedDelayedRefund => {
238238
SimpleOrderExecutionResult::OrderError
239239
}
240+
ExecutionError::PriorityUpdatesNotUsed(_) => {
241+
SimpleOrderExecutionResult::OrderError
242+
}
240243
};
241244
(None, result)
242245
}

crates/rbuilder/src/building/mod.rs

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,14 @@ use eth_sparse_mpt::SparseTrieLocalCache;
3838
use evm::EthCachedEvmFactory;
3939
use jsonrpsee::core::Serialize;
4040
use parking_lot::Mutex;
41-
use priority_update::PriorityUpdatePool;
41+
use priority_update::{
42+
select_unwritten_slots,
43+
used_priority_update_tracer::{UsedPriorityStateTracer, UsedStorageSlotStatus},
44+
PriorityUpdatePool,
45+
};
4246
use rbuilder_primitives::{
43-
mev_boost::BidAdjustmentDataV3, BlockSpace, Order, SimValue, SimulatedOrder,
44-
TransactionSignedEcRecoveredWithBlobs,
47+
evm_inspector::SlotKey, mev_boost::BidAdjustmentDataV3, BlockSpace, Order, SimValue,
48+
SimulatedOrder, TransactionSignedEcRecoveredWithBlobs,
4549
};
4650
use reth::{
4751
payload::PayloadId,
@@ -586,6 +590,8 @@ pub enum ExecutionError {
586590
LowerInsertedValue { before: SimValue, inplace: SimValue },
587591
#[error("Priority update produced a delayed refund")]
588592
PriorityUpdateProducedDelayedRefund,
593+
#[error("Order did not use the committed priority update slots: {0:?}")]
594+
PriorityUpdatesNotUsed(Vec<(SlotKey, UsedStorageSlotStatus)>),
589595
}
590596

591597
impl ExecutionError {
@@ -749,24 +755,31 @@ impl<Tracer: SimulationTracer, PartialBlockExecutionTracerType: PartialBlockExec
749755
where
750756
DB: Database<Error = ProviderError>,
751757
{
758+
let unwritten_priorit_update_slots =
759+
select_unwritten_slots(state, &order.used_priority_updates);
752760
let priority_update_orders =
753-
priority_update_pool.get_updates(state, &order.used_priority_updates);
761+
priority_update_pool.get_updates(&unwritten_priorit_update_slots);
762+
763+
let pu_tracer = UsedPriorityStateTracer::new(std::iter::empty());
764+
let pu_storage_tracker = pu_tracer.storage_tracker();
754765

755766
let mut fork = PartialBlockFork::new_with_execution_tracer(
756767
state,
757768
ctx,
758769
local_ctx,
759770
&mut self.partial_block_execution_tracer,
760771
)
761-
.with_tracer(&mut self.tracer);
772+
.with_tracer(&mut self.tracer)
773+
.with_evm_inspector(pu_tracer.clone());
762774

763775
let rollback = fork.rollback_point();
764776
let initial_space_state = self.space_state;
765777

766778
let mut priority_updates: Vec<Result<ExecutionResult, ExecutionError>> =
767779
Vec::with_capacity(priority_update_orders.len());
768780

769-
for priority_update_order in priority_update_orders {
781+
for priority_update_sim in priority_update_orders {
782+
let priority_update_order = priority_update_sim.order.as_ref();
770783
let pu_rollback = fork.rollback_point();
771784
let exec_result = fork.commit_order(
772785
priority_update_order,
@@ -805,6 +818,13 @@ impl<Tracer: SimulationTracer, PartialBlockExecutionTracerType: PartialBlockExec
805818
}));
806819
}
807820

821+
// Register the unwritten PU slots only after every PU has committed,
822+
// so SLOADs done by the PUs themselves don't pollute the recording —
823+
// we only want to see what the order reads.
824+
for slot in &unwritten_priorit_update_slots {
825+
pu_storage_tracker.track_slot(slot.clone());
826+
}
827+
808828
let exec_result = fork.commit_order(
809829
&order.order,
810830
self.space_state,
@@ -836,6 +856,24 @@ impl<Tracer: SimulationTracer, PartialBlockExecutionTracerType: PartialBlockExec
836856
});
837857
}
838858

859+
// Verify every tracked PU slot was actually read on the surviving
860+
// call path. Slots only surfaced through reverted subcalls (or not
861+
// read at all) mean the owning PU is unused — fail the whole commit
862+
// so the caller can re-plan.
863+
let unused_slots: Vec<(SlotKey, UsedStorageSlotStatus)> = pu_tracer
864+
.classification()
865+
.into_iter()
866+
.filter(|(_, status)| !matches!(status, UsedStorageSlotStatus::Read))
867+
.collect();
868+
if !unused_slots.is_empty() {
869+
fork.rollback(rollback);
870+
self.space_state = initial_space_state;
871+
return Ok(OrderCommitResult {
872+
priority_updates,
873+
order: Err(ExecutionError::PriorityUpdatesNotUsed(unused_slots)),
874+
});
875+
}
876+
839877
for priority_update in priority_updates.iter().flatten() {
840878
self.coinbase_profit += priority_update.coinbase_profit;
841879
self.executed_tx_infos

crates/rbuilder/src/building/priority_update/mod.rs

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
use ahash::{HashMap, HashSet};
22
use alloy_primitives::U256;
3-
use rbuilder_primitives::{
4-
evm_inspector::SlotKey, Order, OrderId, PriorityUpdateClass, SimulatedOrder,
5-
};
3+
use rbuilder_primitives::{evm_inspector::SlotKey, OrderId, PriorityUpdateClass, SimulatedOrder};
64
use std::sync::Arc;
75
use tracing::error;
86

@@ -85,39 +83,42 @@ impl PriorityUpdatePool {
8583
orders
8684
}
8785

88-
/// Priority-update orders that touch any of the slots in `read_slots`.
89-
///
90-
/// For each read slot: if the slot was already written in the in-block
91-
/// bundle state, the PU is not needed for this slot — skip it. Otherwise
92-
/// surface the PU that owns the slot.
93-
pub fn get_updates<DB>(
94-
&self,
95-
current_block_state: &BlockState<DB>,
96-
read_slots: &[SlotKey],
97-
) -> Vec<&Order> {
86+
/// Priority-update orders owning any of the slots in `read_slots`. The
87+
/// caller is adviced to filter out slots already written in the current
88+
/// bundle state via [`select_unwritten_slots`] beforehand.
89+
pub fn get_updates(&self, read_slots: &[SlotKey]) -> Vec<Arc<SimulatedOrder>> {
9890
if read_slots.is_empty() || self.orders.is_empty() {
9991
return Vec::new();
10092
}
10193
let mut matched: HashSet<OrderId> = HashSet::default();
102-
let mut result: Vec<&Order> = Vec::new();
94+
let mut result: Vec<Arc<SimulatedOrder>> = Vec::new();
10395
for slot in read_slots {
104-
if slot_overwritten_in_bundle(current_block_state, slot) {
105-
continue;
106-
}
10796
let Some(order_id) = self.pending.order_for_slot(slot) else {
10897
continue;
10998
};
11099
if !matched.insert(order_id) {
111100
continue;
112101
}
113102
if let Some(sim) = self.orders.get(&order_id) {
114-
result.push(sim.order.as_ref());
103+
result.push(Arc::clone(sim));
115104
}
116105
}
117106
result
118107
}
119108
}
120109

110+
/// Returns the subset of `slots` whose address/key has not been written in
111+
/// the current in-block bundle state. Used as a prefilter before
112+
/// [`PriorityUpdatePool::get_updates`] so already-overwritten slots don't
113+
/// pull in PUs that are no longer needed.
114+
pub fn select_unwritten_slots<DB>(state: &BlockState<DB>, slots: &[SlotKey]) -> Vec<SlotKey> {
115+
slots
116+
.iter()
117+
.filter(|slot| !slot_overwritten_in_bundle(state, slot))
118+
.cloned()
119+
.collect()
120+
}
121+
121122
fn slot_overwritten_in_bundle<DB>(state: &BlockState<DB>, slot: &SlotKey) -> bool {
122123
let Some(account) = state.bundle_state().state.get(&slot.address) else {
123124
return false;

0 commit comments

Comments
 (0)