flashbots / flashbots/rbuilder

[Bug]: Cached state not loaded properly during conflict resolution

Open
#773 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
567
Forks
209
PR merge metrics
No merged PRs in 30d

Description

## Description

In `process_sequence_of_orders`, when a sequence has a cached sub-sequence in `simulation_cache`, the code fetches a `CachedSimulationState` (including a `bundle_state`) but does not restore it into the new `BlockState`. Instead, a fresh `BlockState` is created. This causes the remaining (non-cached) orders of the sequence to be simulated on top of the wrong state.

Additionally, I think the `simulation_cache` should persist and restore counters for `gas_used` and `blob_gas_used`. On a cache hit, `PartialBlock` should be initialised using these cached values. Otherwise the remaining orders are simulated as if more resources were available than the cached orders actually left. For example, if the blob gas limit was already exhausted, remaining blob transactions should revert. Without restoring the counters, certain transactions can appear to succeed, producing incorrect results.

## Current flow (cache hit)

1. `get_cached_state(&full_sequence_of_orders)` returns `(Some(cached_state), cached_up_to_index > 0)`.
2. `initialize_block_state(state_provider)` is called:
* It uses `BlockState::new_arc(state_provider)`, which always sets a fresh `BundleState::default()`.
3. `pre_block_call` is invoked unconditionally.

Result: the cached sequence is not restored and the simulation effectively starts from an empty state again.

## Expected

* If a cached sequence exists:
* Restore `cached_state.bundle_state` into `BlockState` (using `with_bundle_state`).
* Skip `pre_block_call`
* Initialise `PartialBlock` counters (gas, blob gas) from the cached values.

## Minimal fix

Pass the cached state into the initialiser and use it when present:
```rust
fn initialize_block_state(
&self,
cached_state_option: &Option>,
state_provider: Arc,
) -> BlockState {
let initial_state = BlockState::new_arc(state_provider);
if let Some(cached_state) = cached_state_option {
initial_state.with_bundle_state(cached_state.bundle_state.clone())
} else {
initial_state
}
}
```

Update the call site in `process_sequence_of_orders`:
```rust
let (cached_state_option, cached_up_to_index) =
self.simulation_cache.get_cached_state(&full_sequence_of_orders);

let mut partial_block = PartialBlock::new(true);
let mut state = self.initialize_block_state(&cached_state_option, state_provider);

if cached_up_to_index == 0 {
partial_block.pre_block_call(&self.ctx, &mut local_ctx, &mut state)?;
} else if let Some(cached) = &cached_state_option {
partial_block.gas_used = cached.cumulative_gas_used;
partial_block.blob_gas_used = cached.cumulative_blob_gas_used;
}
```

Note that updates to `CachedSimulationState`and `store_simulation_state` would also be needed to include the gas counters.

I'm happy to open a PR with this change. If there's intended behaviour I'm missing (e.g., a different place where the cached `BundleState` is supposed to be re-applied), please let me know. Thanks!

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.