Skip to content
Open
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
116 changes: 115 additions & 1 deletion src/payload/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ impl<P: Platform> Executable<P> {
DB: DatabaseRef<Error = ProviderError> + Debug,
{
match self {
Self::Bundle(_) => unreachable!("asd"),
Self::Bundle(bundle) => Self::simulate_bundle(bundle, block, db),
Self::Transaction(tx) => Self::simulate_transaction(tx, block, db)
.map_err(ExecutionError::InvalidTransaction),
}
Expand Down Expand Up @@ -312,6 +312,120 @@ impl<P: Platform> Executable<P> {
state: BundleState::default(),
})
}

/// Simulates a bundle of transactions and returns the simulated execution
/// outcome of all transactions in the bundle. No state changes are
/// persisted.
///
/// Notes:
/// - Bundles that are not eligible for execution in the current block are
/// considered invalid, and no execution result will be produced.
///
/// - All transactions in the bundle are simulated in the order in which they
/// were defined in the bundle.
///
/// - Each transaction is simulated on the in-memory state produced by the
/// previous transaction in the bundle. State changes are applied in-memory
/// for subsequent transactions, but the final state is not persisted.
///
/// - Transactions that cause EVM errors will invalidate the bundle, and no
/// execution result will be produced. Bundle transactions can be marked
/// optional [`Bundle::is_optional`], and invalid outcomes are handled by
/// discarding them.
///
/// - Transactions that fail gracefully (revert or halt) and are not optional
/// will invalidate the bundle, and no execution result will be produced.
/// Bundle transactions can be marked as allowed to fail
/// [`Bundle::is_allowed_to_fail`], and failure outcomes are handled by
/// including them if allowed.
///
/// See truth table (same as `execute_bundle`):
/// | success | `allowed_to_fail` | optional | Action |
/// | ------: | :---------------: | :------: | :------ |
/// | true | *don’t care* | *any* | include |
/// | false | true | *any* | include |
/// | false | false | true | discard |
/// | false | false | false | error |
///
/// Post-execution validation is performed on the simulated state, but the
/// state is not persisted.
fn simulate_bundle<DB>(
bundle: types::Bundle<P>,
block: &BlockContext<P>,
db: &DB,
) -> Result<ExecutionResult<P>, ExecutionError<P>>
where
DB: DatabaseRef<Error = ProviderError> + Debug,
{
let eligible = bundle.is_eligible(block);
if !eligible {
return Err(ExecutionError::IneligibleBundle(eligible));
}

let evm_env = block.evm_env();
let evm_config = block.evm_config();
let mut state = State::builder().with_database(WrapDatabaseRef(db)).build();

let mut discarded = Vec::new();
let mut results = Vec::with_capacity(bundle.transactions().len());

for transaction in bundle.transactions_encoded() {
let tx_hash = *transaction.tx_hash();
let optional = bundle.is_optional(&tx_hash);
let allowed_to_fail = bundle.is_allowed_to_fail(&tx_hash);

let result = evm_config
.evm_with_env(&mut state, evm_env.clone())
.transact(&transaction);

match result {
// Valid transaction or allowed to fail: include it in the bundle
Ok(ExecResultAndState {
result,
state: tx_state,
}) if result.is_success() || allowed_to_fail => {
results.push(result);
// State changes committed in-memory but not returned (simulation)
state.commit(tx_state);
}
// Optional failing transaction, not allowed to fail
// or optional invalid transaction: discard it
Ok(_) | Err(_) if optional => {
discarded.push(tx_hash);
}
// Non-Optional failing transaction, not allowed to fail: invalidate the
// bundle
Ok(_) => {
return Err(ExecutionError::BundleTransactionReverted(tx_hash));
}
// Non-Optional invalid transaction: invalidate the bundle
Err(err) => {
return Err(ExecutionError::InvalidBundleTransaction(tx_hash, err));
}
}
}

// Reduce the bundle by removing discarded transactions
let bundle = discarded
.into_iter()
.fold(bundle, |b, tx| b.without_transaction(tx));

// Extract the simulated state for validation (but do not merge transitions)
let simulated_state = state.take_bundle();

// Run post-execution validation on the simulated state
bundle
.validate_post_execution(&simulated_state, block)
.map_err(ExecutionError::InvalidBundlePostExecutionState)?;

// Return ExecutionResult with simulated results and default state (no
// persistence)
Ok(ExecutionResult {
source: Executable::Bundle(bundle),
results,
state: BundleState::default(),
})
}
}

impl<P: Platform> Executable<P> {
Expand Down
Loading