From 3a0d46375e95b8314de4e5a6627b921c14ef6d53 Mon Sep 17 00:00:00 2001 From: Peponks9 Date: Tue, 18 Nov 2025 12:44:52 -0600 Subject: [PATCH 1/4] implement simulated execution for bundles in Executable::simulate --- src/payload/exec.rs | 106 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/src/payload/exec.rs b/src/payload/exec.rs index 6137d05..e80b446 100644 --- a/src/payload/exec.rs +++ b/src/payload/exec.rs @@ -275,7 +275,7 @@ impl Executable

{ DB: DatabaseRef + 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), } @@ -312,6 +312,110 @@ impl Executable

{ 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, but changes are not committed or + /// 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 skipped for simulation, as no state changes + /// are persisted. + fn simulate_bundle( + bundle: types::Bundle

, + block: &BlockContext

, + db: &DB, + ) -> Result, ExecutionError

> + where + DB: DatabaseRef + 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, .. }) + if result.is_success() || allowed_to_fail => + { + results.push(result); + // Note: No db.commit(state) for simulation + } + // 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)); + + // Return ExecutionResult with simulated results and default state (no + // persistence) + Ok(ExecutionResult { + source: Executable::Bundle(bundle), + results, + state: BundleState::default(), + }) + } } impl Executable

{ From 1f8da66fd4d4c43736d486e9a6a7b04445a3c257 Mon Sep 17 00:00:00 2001 From: Peponks9 Date: Tue, 18 Nov 2025 13:41:16 -0600 Subject: [PATCH 2/4] fix ci checks --- src/payload/exec.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/payload/exec.rs b/src/payload/exec.rs index e80b446..61de5d7 100644 --- a/src/payload/exec.rs +++ b/src/payload/exec.rs @@ -339,7 +339,7 @@ impl Executable

{ /// [`Bundle::is_allowed_to_fail`], and failure outcomes are handled by /// including them if allowed. /// - /// See truth table (same as execute_bundle): + /// See truth table (same as `execute_bundle`): /// | success | `allowed_to_fail` | optional | Action | /// | ------: | :---------------: | :------: | :------ | /// | true | *don’t care* | *any* | include | @@ -347,8 +347,8 @@ impl Executable

{ /// | false | false | true | discard | /// | false | false | false | error | /// - /// - Post-execution validation is skipped for simulation, as no state changes - /// are persisted. + /// Post-execution validation is skipped for simulation, as no state changes + /// are persisted. fn simulate_bundle( bundle: types::Bundle

, block: &BlockContext

, From 8e15f3765f68b63aac566eeed6b95bc7d0a16ac6 Mon Sep 17 00:00:00 2001 From: Peponks9 Date: Wed, 19 Nov 2025 11:19:16 -0600 Subject: [PATCH 3/4] commit in-memory state changes in simulate_bundle and update docs --- src/payload/exec.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/payload/exec.rs b/src/payload/exec.rs index 61de5d7..4efb964 100644 --- a/src/payload/exec.rs +++ b/src/payload/exec.rs @@ -325,8 +325,8 @@ impl Executable

{ /// were defined in the bundle. /// /// - Each transaction is simulated on the in-memory state produced by the - /// previous transaction in the bundle, but changes are not committed or - /// persisted. + /// 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 @@ -380,11 +380,13 @@ impl Executable

{ match result { // Valid transaction or allowed to fail: include it in the bundle - Ok(ExecResultAndState { result, .. }) - if result.is_success() || allowed_to_fail => - { + Ok(ExecResultAndState { + result, + state: tx_state, + }) if result.is_success() || allowed_to_fail => { results.push(result); - // Note: No db.commit(state) for simulation + // 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 From cf780f8dc8faf4c0c9a9f95d2338b036174134ae Mon Sep 17 00:00:00 2001 From: Peponks9 Date: Thu, 20 Nov 2025 11:23:06 -0600 Subject: [PATCH 4/4] add post-execution validation to simulate_bundl --- src/payload/exec.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/payload/exec.rs b/src/payload/exec.rs index 4efb964..41a7e78 100644 --- a/src/payload/exec.rs +++ b/src/payload/exec.rs @@ -347,8 +347,8 @@ impl Executable

{ /// | false | false | true | discard | /// | false | false | false | error | /// - /// Post-execution validation is skipped for simulation, as no state changes - /// are persisted. + /// Post-execution validation is performed on the simulated state, but the + /// state is not persisted. fn simulate_bundle( bundle: types::Bundle

, block: &BlockContext

, @@ -410,6 +410,14 @@ impl Executable

{ .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 {