), ProviderError> {
+ let mut cancellations = Vec::new();
+ let mut successful_results = Vec::with_capacity(results.len());
+
for result in results {
- self.process_simulation_task_result(result)?;
+ match result {
+ SimulatedResult::Success { .. } => {
+ cancellations.extend(self.handle_ace_unlock(&result)?);
+ // All successful results need to be processed for dependency tracking
+ self.process_simulation_task_result(&result)?;
+ successful_results.push(result);
+ }
+ SimulatedResult::Failed {
+ order,
+ failure:
+ SimulationFailure {
+ ace_dependency: Some(contract_address),
+ ..
+ },
+ ace_unlock_contracts,
+ ..
+ } => {
+ // Order failed but needs ACE unlock - queue for re-simulation
+ // Pass existing ace_unlock_contracts to support progressive multi-ACE discovery
+ self.add_ace_dependency_for_order(
+ order,
+ contract_address,
+ ace_unlock_contracts,
+ )?;
+ }
+ SimulatedResult::Failed { .. } => {
+ // Permanent failure - nothing to do
+ }
+ }
}
- Ok(())
+
+ Ok((successful_results, cancellations))
}
}
@@ -318,6 +699,7 @@ pub fn simulate_all_orders_with_sim_tree(
ctx: &BlockBuildingContext,
orders: &[Order],
randomize_insertion: bool,
+ ace_config: Vec,
) -> Result<(Vec>, Vec), CriticalCommitOrderError>
where
P: StateProviderFactory + Clone,
@@ -326,7 +708,7 @@ where
let state = provider.history_by_block_hash(ctx.attributes.parent)?;
NonceCache::new(state.into())
};
- let mut sim_tree = SimTree::new(nonces);
+ let mut sim_tree = SimTree::new(nonces, ace_config);
let mut orders = orders.to_vec();
let random_insert_size = max(orders.len() / 20, 1);
@@ -335,7 +717,7 @@ where
// shuffle orders
orders.shuffle(&mut rng);
} else {
- sim_tree.push_orders(orders.clone())?;
+ sim_tree.push_orders(std::mem::take(&mut orders))?;
}
let mut sim_errors = Vec::new();
@@ -369,36 +751,61 @@ where
ctx,
&mut local_ctx,
&mut block_state,
+ sim_tree.ace_configs(),
+ &sim_task.ace_unlock_contracts,
)?;
let (_, provider) = block_state.into_parts();
state_for_sim = provider;
match sim_result.result {
- OrderSimResult::Failed(err) => {
- trace!(
- order = sim_task.order.id().to_string(),
- ?err,
- "Order simulation failed"
- );
- sim_errors.push(err);
- continue;
+ OrderSimResult::Failed(failure) => {
+ if let Some(contract_address) = failure.ace_dependency {
+ // Order failed but needs ACE unlock - queue for re-simulation
+ // Pass existing ace_unlock_contracts for progressive multi-ACE discovery
+ sim_tree.add_ace_dependency_for_order(
+ sim_task.order,
+ contract_address,
+ sim_task.ace_unlock_contracts,
+ )?;
+ } else {
+ // Permanent failure
+ trace!(
+ order = sim_task.order.id().to_string(),
+ ?failure,
+ "Order simulation failed"
+ );
+ sim_errors.push(failure.error);
+ }
}
OrderSimResult::Success(sim_order, nonces) => {
- let result = SimulatedResult {
+ let mut dependencies_satisfied: Vec = nonces
+ .into_iter()
+ .map(|(address, nonce)| DependencyKey::Nonce(NonceKey { address, nonce }))
+ .collect();
+
+ // Add ACE dependencies for all unlocking interactions
+ for interaction in &sim_order.ace_interactions {
+ if let AceInteraction::Unlocking {
+ contract_address, ..
+ } = interaction
+ {
+ dependencies_satisfied
+ .push(DependencyKey::AceUnlock(*contract_address));
+ }
+ }
+
+ let result = SimulatedResult::Success {
id: sim_task.id,
simulated_order: sim_order,
previous_orders: sim_task.parents,
- nonces_after: nonces
- .into_iter()
- .map(|(address, nonce)| NonceKey { address, nonce })
- .collect(),
-
+ dependencies_satisfied,
simulation_time: start_time.elapsed(),
};
sim_results.push(result);
}
}
}
- sim_tree.submit_simulation_tasks_results(sim_results)?;
+ // For batch simulation, we ignore cancellations since there's no live processing
+ let (_, _cancellations) = sim_tree.submit_simulation_tasks_results(sim_results)?;
}
Ok((
@@ -418,14 +825,23 @@ pub fn simulate_order(
ctx: &BlockBuildingContext,
local_ctx: &mut ThreadBlockBuildingContext,
state: &mut BlockState,
+ ace_configs: &HashMap,
+ ace_unlock_contracts: &HashSet,
) -> Result {
let mut tracer = AccumulatorSimulationTracer::new();
let mut fork = PartialBlockFork::new(state, ctx, local_ctx).with_tracer(&mut tracer);
let rollback_point = fork.rollback_point();
- let sim_res =
- simulate_order_using_fork(parent_orders, order, &mut fork, &ctx.mempool_tx_detector);
+ let sim_res = simulate_order_using_fork(
+ parent_orders,
+ order,
+ &mut fork,
+ &ctx.mempool_tx_detector,
+ ace_configs,
+ ace_unlock_contracts,
+ );
fork.rollback(rollback_point);
let sim_res = sim_res?;
+
Ok(OrderSimResultWithGas {
result: sim_res,
gas_used: tracer.used_gas,
@@ -438,8 +854,11 @@ pub fn simulate_order_using_fork(
order: Order,
fork: &mut PartialBlockFork<'_, '_, '_, '_, Tracer, NullPartialBlockForkExecutionTracer>,
mempool_tx_detector: &MempoolTxsDetector,
+ ace_configs: &HashMap,
+ ace_unlock_contracts: &HashSet,
) -> Result {
let start = Instant::now();
+
// simulate parents
let mut space_state = BlockBuildingSpaceState::ZERO;
// We use empty combined refunds because the value of the bundle will
@@ -453,7 +872,10 @@ pub fn simulate_order_using_fork(
}
Err(err) => {
tracing::trace!(parent_order = ?parent.id(), ?err, "failed to simulate parent order");
- return Ok(OrderSimResult::Failed(err));
+ return Ok(OrderSimResult::Failed(SimulationFailure {
+ error: err,
+ ace_dependency: None,
+ }));
}
}
}
@@ -461,13 +883,62 @@ pub fn simulate_order_using_fork(
// simulate
let result = fork.commit_order(&order, space_state, true, &combined_refunds)?;
let sim_time = start.elapsed();
- add_order_simulation_time(sim_time, "sim", result.is_ok()); // we count parent sim time + order sim time time here
+ let sim_success = result.is_ok();
+ add_order_simulation_time(sim_time, "sim", sim_success); // we count parent sim time + order sim time time here
+
+ // Get the used_state_trace from tracer (available regardless of success/failure)
+ let used_state_trace = fork.tracer.as_mut().and_then(|t| t.take_used_state_trace());
+
+ // Detect ACE interactions from the state trace using config
+ // Check ALL transactions in the order and collect ALL ACE interactions (one per contract)
+ // For each contract, keep the highest priority classification:
+ // Priority: ProtocolForce > ProtocolOptional > User > NonUnlocking
+ let ace_interactions: Vec = if let Some(trace) = used_state_trace.as_ref() {
+ // Use HashMap to track best interaction per contract
+ let mut per_contract: HashMap = HashMap::default();
+
+ for (tx, _) in order.list_txs() {
+ let input = tx.internal_tx_unsecure().input();
+ let selector = if input.len() >= 4 {
+ Some(Selector::from_slice(&input[..4]))
+ } else {
+ None
+ };
+ let tx_to = tx.to();
+ let tx_from = Some(tx.signer());
+
+ // Check this transaction against all ACE configs
+ for (_, config) in ace_configs.iter() {
+ if let Some(interaction) =
+ classify_ace_interaction(trace, sim_success, config, selector, tx_to, tx_from)
+ {
+ let contract = interaction.get_contract_address();
+ // Update if new interaction has higher priority for this contract
+ per_contract
+ .entry(contract)
+ .and_modify(|existing| {
+ if interaction_priority(&interaction) > interaction_priority(existing) {
+ *existing = interaction;
+ }
+ })
+ .or_insert(interaction);
+ }
+ }
+ }
+
+ per_contract.into_values().collect()
+ } else {
+ Vec::new()
+ };
match result {
Ok(res) => {
let sim_value = create_sim_value(&order, &res, mempool_tx_detector);
if let Err(err) = order_is_worth_executing(&sim_value) {
- return Ok(OrderSimResult::Failed(err));
+ return Ok(OrderSimResult::Failed(SimulationFailure {
+ error: err,
+ ace_dependency: None,
+ }));
}
let new_nonces = res.nonces_updated.into_iter().collect::>();
Ok(OrderSimResult::Success(
@@ -475,10 +946,64 @@ pub fn simulate_order_using_fork(
order,
sim_value,
used_state_trace: res.used_state_trace,
+ ace_interactions,
}),
new_nonces,
))
}
- Err(err) => Ok(OrderSimResult::Failed(err)),
+ Err(err) => {
+ // Check if failed order accessed ACE - may need re-simulation with unlock parent
+ // Find the first NonUnlocking contract we don't already have an unlock for
+ let ace_dependency = ace_interactions.iter().find_map(|interaction| {
+ if let AceInteraction::NonUnlocking { contract_address } = interaction {
+ if ace_unlock_contracts.contains(contract_address) {
+ // Already had unlock for this contract but still failed - skip
+ tracing::debug!(
+ order = ?order.id(),
+ ?err,
+ ?contract_address,
+ "Order failed despite having ACE unlock for this contract"
+ );
+ None
+ } else {
+ // Need unlock for this contract
+ tracing::debug!(
+ order = ?order.id(),
+ ?err,
+ ?contract_address,
+ existing_unlocks = ?ace_unlock_contracts,
+ "Order needs additional ACE unlock"
+ );
+ Some(*contract_address)
+ }
+ } else {
+ None
+ }
+ });
+
+ Ok(OrderSimResult::Failed(SimulationFailure {
+ error: err,
+ ace_dependency,
+ }))
+ }
+ }
+}
+
+/// Returns priority score for ACE interaction (higher = more important)
+fn interaction_priority(interaction: &AceInteraction) -> u8 {
+ match interaction {
+ AceInteraction::Unlocking {
+ source: AceUnlockSource::ProtocolForce,
+ ..
+ } => 4,
+ AceInteraction::Unlocking {
+ source: AceUnlockSource::ProtocolOptional,
+ ..
+ } => 3,
+ AceInteraction::Unlocking {
+ source: AceUnlockSource::User,
+ ..
+ } => 2,
+ AceInteraction::NonUnlocking { .. } => 1,
}
}
diff --git a/crates/rbuilder/src/building/testing/ace_tests/mod.rs b/crates/rbuilder/src/building/testing/ace_tests/mod.rs
new file mode 100644
index 000000000..ea9e6d405
--- /dev/null
+++ b/crates/rbuilder/src/building/testing/ace_tests/mod.rs
@@ -0,0 +1,629 @@
+use super::test_chain_state::{BlockArgs, TestChainState};
+use crate::building::sim::{AceExchangeState, DependencyKey, NonceKey, SimTree, SimulatedResult};
+use crate::utils::NonceCache;
+use alloy_primitives::{address, b256, Address, B256, U256};
+use rbuilder_primitives::ace::{AceConfig, AceInteraction, AceUnlockSource, Selector};
+use rbuilder_primitives::evm_inspector::{SlotKey, UsedStateTrace};
+use rbuilder_primitives::{BlockSpace, Bundle, BundleVersion, Order, SimValue, SimulatedOrder};
+use std::collections::HashSet;
+use std::sync::Arc;
+use uuid::Uuid;
+
+/// Create a minimal order for testing (empty bundle with unique ID)
+fn create_test_order() -> Order {
+ Order::Bundle(Bundle {
+ version: BundleVersion::V1,
+ block: None,
+ min_timestamp: None,
+ max_timestamp: None,
+ txs: Vec::new(),
+ reverting_tx_hashes: Vec::new(),
+ dropping_tx_hashes: Vec::new(),
+ hash: B256::ZERO,
+ uuid: Uuid::new_v4(),
+ replacement_data: None,
+ signer: None,
+ refund_identity: None,
+ metadata: Default::default(),
+ refund: None,
+ external_hash: None,
+ })
+}
+
+/// Create the real ACE config for testing
+fn test_ace_config() -> AceConfig {
+ AceConfig {
+ contract_address: address!("0000000aa232009084Bd71A5797d089AA4Edfad4"),
+ from_addresses: HashSet::from([address!("c41ae140ca9b281d8a1dc254c50e446019517d04")]),
+ to_addresses: HashSet::from([address!("0000000aa232009084Bd71A5797d089AA4Edfad4")]),
+ detection_slots: HashSet::from([b256!(
+ "0000000000000000000000000000000000000000000000000000000000000003"
+ )]),
+ unlock_signatures: HashSet::from([Selector::from_slice(&[0x18, 0x28, 0xe0, 0xe7])]),
+ force_signatures: HashSet::from([Selector::from_slice(&[0x09, 0xc5, 0xea, 0xbe])]),
+ }
+}
+
+/// Create a mock state trace with ACE detection slot accessed
+fn mock_state_trace_with_ace_slot(contract: Address, slot: B256) -> UsedStateTrace {
+ let mut trace = UsedStateTrace::default();
+ trace.read_slot_values.insert(
+ SlotKey {
+ address: contract,
+ key: slot,
+ },
+ Default::default(),
+ );
+ trace
+}
+
+/// Create a mock force unlock order
+fn create_force_unlock_order(contract: Address, gas_used: u64) -> Arc {
+ let slot = b256!("0000000000000000000000000000000000000000000000000000000000000003");
+ Arc::new(SimulatedOrder {
+ order: create_test_order(),
+ sim_value: SimValue::new(
+ U256::from(10),
+ U256::from(10),
+ BlockSpace::new(gas_used, 0, 0),
+ Vec::new(),
+ ),
+ used_state_trace: Some(mock_state_trace_with_ace_slot(contract, slot)),
+ ace_interactions: vec![AceInteraction::Unlocking {
+ contract_address: contract,
+ source: AceUnlockSource::ProtocolForce,
+ }],
+ })
+}
+
+/// Create a mock optional unlock order
+fn create_optional_unlock_order(contract: Address, gas_used: u64) -> Arc {
+ let slot = b256!("0000000000000000000000000000000000000000000000000000000000000003");
+ Arc::new(SimulatedOrder {
+ order: create_test_order(),
+ sim_value: SimValue::new(
+ U256::from(5),
+ U256::from(5),
+ BlockSpace::new(gas_used, 0, 0),
+ Vec::new(),
+ ),
+ used_state_trace: Some(mock_state_trace_with_ace_slot(contract, slot)),
+ ace_interactions: vec![AceInteraction::Unlocking {
+ contract_address: contract,
+ source: AceUnlockSource::ProtocolOptional,
+ }],
+ })
+}
+
+#[test]
+fn test_ace_exchange_state_get_unlock_order_force_only() {
+ let order = create_force_unlock_order(
+ address!("0000000aa232009084Bd71A5797d089AA4Edfad4"),
+ 100_000,
+ );
+ let state = AceExchangeState {
+ force_unlock_order: Some(order.clone()),
+ ..Default::default()
+ };
+
+ let result = state.get_unlock_order();
+ assert_eq!(result, Some(&order));
+}
+
+#[test]
+fn test_ace_exchange_state_get_unlock_order_optional_only() {
+ let order =
+ create_optional_unlock_order(address!("0000000aa232009084Bd71A5797d089AA4Edfad4"), 50_000);
+ let state = AceExchangeState {
+ optional_unlock_order: Some(order.clone()),
+ ..Default::default()
+ };
+
+ let result = state.get_unlock_order();
+ assert_eq!(result, Some(&order));
+}
+
+#[test]
+fn test_cheapest_unlock_selected() {
+ let contract = address!("0000000aa232009084Bd71A5797d089AA4Edfad4");
+ let expensive_order = create_force_unlock_order(contract, 100_000);
+ let cheap_order = create_optional_unlock_order(contract, 50_000);
+
+ let state = AceExchangeState {
+ force_unlock_order: Some(expensive_order.clone()),
+ optional_unlock_order: Some(cheap_order.clone()),
+ ..Default::default()
+ };
+
+ // Should select the cheaper one (50k < 100k)
+ let result = state.get_unlock_order();
+ assert_eq!(result.unwrap().sim_value.gas_used(), 50_000);
+}
+
+#[test]
+fn test_equal_gas_prefers_force() {
+ let contract = address!("0000000aa232009084Bd71A5797d089AA4Edfad4");
+ let force_order = create_force_unlock_order(contract, 100_000);
+ let optional_order = create_optional_unlock_order(contract, 100_000);
+
+ let state = AceExchangeState {
+ force_unlock_order: Some(force_order.clone()),
+ optional_unlock_order: Some(optional_order.clone()),
+ ..Default::default()
+ };
+
+ // When equal gas, should prefer force (d comparison)
+ let result = state.get_unlock_order();
+ assert_eq!(result, Some(&force_order));
+}
+
+#[test]
+fn test_ace_exchange_state_get_unlock_order_none() {
+ let state = AceExchangeState::default();
+ assert_eq!(state.get_unlock_order(), None);
+}
+
+#[test]
+fn test_sim_tree_ace_config_registration() -> eyre::Result<()> {
+ let test_chain = TestChainState::new(BlockArgs::default())?;
+ let state = test_chain.provider_factory().latest()?;
+ let nonce_cache = NonceCache::new(state.into());
+
+ let ace_config = test_ace_config();
+ let contract_addr = ace_config.contract_address;
+
+ let sim_tree = SimTree::new(nonce_cache, vec![ace_config.clone()]);
+
+ // Verify config is registered
+ assert!(sim_tree.ace_configs().contains_key(&contract_addr));
+
+ // Verify state is initialized
+ assert!(sim_tree.get_ace_state(&contract_addr).is_some());
+
+ Ok(())
+}
+
+#[test]
+fn test_multiple_ace_contracts() -> eyre::Result<()> {
+ let test_chain = TestChainState::new(BlockArgs::default())?;
+ let state = test_chain.provider_factory().latest()?;
+ let nonce_cache = NonceCache::new(state.into());
+
+ let contract1 = address!("0000000aa232009084Bd71A5797d089AA4Edfad4");
+ let contract2 = address!("1111111aa232009084Bd71A5797d089AA4Edfad4");
+
+ let mut config1 = test_ace_config();
+ config1.contract_address = contract1;
+
+ let mut config2 = test_ace_config();
+ config2.contract_address = contract2;
+
+ let sim_tree = SimTree::new(nonce_cache, vec![config1, config2]);
+
+ // Both contracts should be registered
+ assert!(sim_tree.ace_configs().contains_key(&contract1));
+ assert!(sim_tree.ace_configs().contains_key(&contract2));
+
+ // Both should have state
+ assert!(sim_tree.get_ace_state(&contract1).is_some());
+ assert!(sim_tree.get_ace_state(&contract2).is_some());
+
+ Ok(())
+}
+
+#[test]
+fn test_mark_mempool_unlock_basic() -> eyre::Result<()> {
+ let test_chain = TestChainState::new(BlockArgs::default())?;
+ let state = test_chain.provider_factory().latest()?;
+ let nonce_cache = NonceCache::new(state.into());
+
+ let ace_config = test_ace_config();
+ let contract_addr = ace_config.contract_address;
+
+ let mut sim_tree = SimTree::new(nonce_cache, vec![ace_config]);
+
+ // Mark mempool unlock (first time)
+ let cancelled1 = sim_tree.mark_mempool_unlock(contract_addr);
+ // No optional order yet, so nothing to cancel
+ assert_eq!(cancelled1, None);
+
+ // Mark again (should be idempotent)
+ let cancelled2 = sim_tree.mark_mempool_unlock(contract_addr);
+ assert_eq!(cancelled2, None);
+
+ // Verify state shows mempool unlock was marked
+ let ace_state = sim_tree.get_ace_state(&contract_addr).unwrap();
+ assert!(ace_state.has_mempool_unlock);
+
+ Ok(())
+}
+
+// Note: More detailed cancellation tests require access to SimTree internals
+// or integration with handle_ace_interaction which we'll test separately
+
+#[test]
+fn test_handle_ace_unlock_with_mempool_unlock() -> eyre::Result<()> {
+ let test_chain = TestChainState::new(BlockArgs::default())?;
+ let state = test_chain.provider_factory().latest()?;
+ let nonce_cache = NonceCache::new(state.into());
+
+ let ace_config = test_ace_config();
+ let contract_addr = ace_config.contract_address;
+
+ let mut sim_tree = SimTree::new(nonce_cache, vec![ace_config]);
+
+ let cancelled = sim_tree.mark_mempool_unlock(contract_addr);
+ assert_eq!(cancelled, None); // Nothing to cancel yet
+
+ let optional_order = create_optional_unlock_order(contract_addr, 50_000);
+
+ let result = SimulatedResult::Success {
+ id: rand::random(),
+ simulated_order: optional_order.clone(),
+ previous_orders: Vec::new(),
+ dependencies_satisfied: Vec::new(),
+ simulation_time: std::time::Duration::from_millis(10),
+ };
+
+ // Handle the ACE unlock
+ let cancellations = sim_tree.handle_ace_unlock(&result)?;
+
+ // Unlocking orders should be cancelled since mempool unlock was already marked
+ assert!(cancellations.contains(&optional_order.order.id()));
+
+ // Optional order should NOT be stored
+ let ace_state = sim_tree.get_ace_state(&contract_addr).unwrap();
+ assert!(ace_state.optional_unlock_order.is_none());
+
+ Ok(())
+}
+
+#[test]
+fn test_optional_ace_not_stored_without_pending_orders() -> eyre::Result<()> {
+ let test_chain = TestChainState::new(BlockArgs::default())?;
+ let state = test_chain.provider_factory().latest()?;
+ let nonce_cache = NonceCache::new(state.into());
+
+ let ace_config = test_ace_config();
+ let contract_addr = ace_config.contract_address;
+
+ let mut sim_tree = SimTree::new(nonce_cache, vec![ace_config]);
+
+ // Create optional unlock order but NO pending orders waiting on it
+ let optional_order = create_optional_unlock_order(contract_addr, 50_000);
+
+ let result = SimulatedResult::Success {
+ id: rand::random(),
+ simulated_order: optional_order.clone(),
+ previous_orders: Vec::new(),
+ dependencies_satisfied: Vec::new(),
+ simulation_time: std::time::Duration::from_millis(10),
+ };
+
+ // Handle the ACE unlock - should be cancelled because no orders need it
+ let cancellations = sim_tree.handle_ace_unlock(&result)?;
+
+ // Optional should be cancelled because no orders are waiting
+ assert!(cancellations.contains(&optional_order.order.id()));
+
+ // Optional order should NOT be stored
+ let ace_state = sim_tree.get_ace_state(&contract_addr).unwrap();
+ assert!(ace_state.optional_unlock_order.is_none());
+
+ Ok(())
+}
+
+#[test]
+fn test_dependency_key_from_nonce() {
+ let nonce_key = NonceKey {
+ address: address!("0000000000000000000000000000000000000001"),
+ nonce: 5,
+ };
+ let nonce_key_clone = nonce_key.clone();
+ let dep_key: DependencyKey = nonce_key.into();
+ assert_eq!(dep_key, DependencyKey::Nonce(nonce_key_clone));
+}
+
+#[test]
+fn test_dependency_key_ace_unlock() {
+ let contract_addr = address!("0000000aa232009084Bd71A5797d089AA4Edfad4");
+ let dep_key = DependencyKey::AceUnlock(contract_addr);
+
+ match dep_key {
+ DependencyKey::AceUnlock(addr) => assert_eq!(addr, contract_addr),
+ _ => panic!("Expected AceUnlock dependency"),
+ }
+}
+
+// ============================================================================
+// Multi-ACE Unlock Tests
+// ============================================================================
+
+use crate::building::sim::SimulationRequest;
+use ahash::HashSet as AHashSet;
+
+#[test]
+fn test_simulation_request_ace_unlock_contracts_empty_default() {
+ // New orders should start with empty ace_unlock_contracts
+ let request = SimulationRequest {
+ id: rand::random(),
+ order: create_test_order(),
+ parents: Vec::new(),
+ ace_unlock_contracts: AHashSet::default(),
+ };
+
+ assert!(request.ace_unlock_contracts.is_empty());
+}
+
+#[test]
+fn test_simulation_request_ace_unlock_contracts_single() {
+ let contract_a = address!("0000000aa232009084Bd71A5797d089AA4Edfad4");
+ let mut ace_contracts = AHashSet::default();
+ ace_contracts.insert(contract_a);
+
+ let request = SimulationRequest {
+ id: rand::random(),
+ order: create_test_order(),
+ parents: Vec::new(),
+ ace_unlock_contracts: ace_contracts.clone(),
+ };
+
+ assert!(request.ace_unlock_contracts.contains(&contract_a));
+ assert_eq!(request.ace_unlock_contracts.len(), 1);
+}
+
+#[test]
+fn test_simulation_request_ace_unlock_contracts_multiple() {
+ // Test that SimulationRequest can track multiple ACE contracts
+ let contract_a = address!("0000000aa232009084Bd71A5797d089AA4Edfad4");
+ let contract_b = address!("1111111aa232009084Bd71A5797d089AA4Edfad4");
+ let contract_c = address!("2222222aa232009084Bd71A5797d089AA4Edfad4");
+
+ let mut ace_contracts = AHashSet::default();
+ ace_contracts.insert(contract_a);
+ ace_contracts.insert(contract_b);
+ ace_contracts.insert(contract_c);
+
+ let request = SimulationRequest {
+ id: rand::random(),
+ order: create_test_order(),
+ parents: Vec::new(),
+ ace_unlock_contracts: ace_contracts.clone(),
+ };
+
+ assert!(request.ace_unlock_contracts.contains(&contract_a));
+ assert!(request.ace_unlock_contracts.contains(&contract_b));
+ assert!(request.ace_unlock_contracts.contains(&contract_c));
+ assert_eq!(request.ace_unlock_contracts.len(), 3);
+}
+
+#[test]
+fn test_ace_unlock_contracts_genuine_failure_detection() {
+ // When ace_unlock_contracts contains the failing contract,
+ // it should be treated as genuine failure (not re-queued)
+ let contract_a = address!("0000000aa232009084Bd71A5797d089AA4Edfad4");
+
+ let mut ace_contracts = AHashSet::default();
+ ace_contracts.insert(contract_a);
+
+ // If the order already had unlock for contract_a but still failed,
+ // checking contains() should return true
+ assert!(ace_contracts.contains(&contract_a));
+
+ // A different contract should NOT be in the set
+ let contract_b = address!("1111111aa232009084Bd71A5797d089AA4Edfad4");
+ assert!(!ace_contracts.contains(&contract_b));
+}
+
+#[test]
+fn test_ace_unlock_contracts_progressive_accumulation() {
+ // Simulate progressive discovery: start empty, add contracts one by one
+ let mut ace_contracts = AHashSet::default();
+
+ let contract_a = address!("0000000aa232009084Bd71A5797d089AA4Edfad4");
+ let contract_b = address!("1111111aa232009084Bd71A5797d089AA4Edfad4");
+
+ // First failure - add contract A
+ assert!(!ace_contracts.contains(&contract_a));
+ ace_contracts.insert(contract_a);
+ assert!(ace_contracts.contains(&contract_a));
+ assert_eq!(ace_contracts.len(), 1);
+
+ // Second failure (different contract) - add contract B
+ assert!(!ace_contracts.contains(&contract_b));
+ ace_contracts.insert(contract_b);
+ assert!(ace_contracts.contains(&contract_b));
+ assert_eq!(ace_contracts.len(), 2);
+
+ // Third failure with contract A - should be genuine failure (already in set)
+ assert!(ace_contracts.contains(&contract_a));
+}
+
+#[test]
+fn test_add_ace_dependency_preserves_existing_contracts() -> eyre::Result<()> {
+ let test_chain = TestChainState::new(BlockArgs::default())?;
+ let state = test_chain.provider_factory().latest()?;
+ let nonce_cache = NonceCache::new(state.into());
+
+ let contract_a = address!("0000000aa232009084Bd71A5797d089AA4Edfad4");
+ let contract_b = address!("1111111aa232009084Bd71A5797d089AA4Edfad4");
+
+ let mut config_a = test_ace_config();
+ config_a.contract_address = contract_a;
+
+ let mut config_b = test_ace_config();
+ config_b.contract_address = contract_b;
+
+ let mut sim_tree = SimTree::new(nonce_cache, vec![config_a, config_b]);
+
+ // Create an order that already has contract_a in its ace_unlock_contracts
+ let order = create_test_order();
+ let mut existing_contracts = AHashSet::default();
+ existing_contracts.insert(contract_a);
+
+ // Add ACE dependency for contract_b (simulating second failure)
+ sim_tree.add_ace_dependency_for_order(order, contract_b, existing_contracts)?;
+
+ // The order should now be pending for contract_b
+ // (We can't easily check internal state, but the function should succeed)
+
+ Ok(())
+}
+
+#[test]
+fn test_multi_ace_config_registration() -> eyre::Result<()> {
+ let test_chain = TestChainState::new(BlockArgs::default())?;
+ let state = test_chain.provider_factory().latest()?;
+ let nonce_cache = NonceCache::new(state.into());
+
+ let contract_a = address!("0000000aa232009084Bd71A5797d089AA4Edfad4");
+ let contract_b = address!("1111111aa232009084Bd71A5797d089AA4Edfad4");
+ let contract_c = address!("2222222aa232009084Bd71A5797d089AA4Edfad4");
+
+ let mut config_a = test_ace_config();
+ config_a.contract_address = contract_a;
+
+ let mut config_b = test_ace_config();
+ config_b.contract_address = contract_b;
+
+ let mut config_c = test_ace_config();
+ config_c.contract_address = contract_c;
+
+ let sim_tree = SimTree::new(nonce_cache, vec![config_a, config_b, config_c]);
+
+ // All three contracts should be registered
+ assert!(sim_tree.ace_configs().contains_key(&contract_a));
+ assert!(sim_tree.ace_configs().contains_key(&contract_b));
+ assert!(sim_tree.ace_configs().contains_key(&contract_c));
+
+ // All three should have state
+ assert!(sim_tree.get_ace_state(&contract_a).is_some());
+ assert!(sim_tree.get_ace_state(&contract_b).is_some());
+ assert!(sim_tree.get_ace_state(&contract_c).is_some());
+
+ Ok(())
+}
+
+// ============================================================================
+// Multi-Transaction Bundle Classification Tests
+// ============================================================================
+
+use rbuilder_primitives::ace::classify_ace_interaction;
+
+#[test]
+fn test_classify_ace_interaction_priority_force_beats_optional() {
+ let config = test_ace_config();
+ let contract = config.contract_address;
+ let slot = b256!("0000000000000000000000000000000000000000000000000000000000000003");
+ let whitelisted_from = *config.from_addresses.iter().next().unwrap();
+ let force_selector = *config.force_signatures.iter().next().unwrap();
+ let unlock_selector = *config.unlock_signatures.iter().next().unwrap();
+
+ let trace = mock_state_trace_with_ace_slot(contract, slot);
+
+ // ProtocolForce classification
+ let force_result = classify_ace_interaction(
+ &trace,
+ true,
+ &config,
+ Some(force_selector),
+ Some(contract),
+ Some(whitelisted_from),
+ );
+ assert!(matches!(
+ force_result,
+ Some(AceInteraction::Unlocking {
+ source: AceUnlockSource::ProtocolForce,
+ ..
+ })
+ ));
+
+ // ProtocolOptional classification
+ let optional_result = classify_ace_interaction(
+ &trace,
+ true,
+ &config,
+ Some(unlock_selector),
+ Some(contract),
+ Some(whitelisted_from),
+ );
+ assert!(matches!(
+ optional_result,
+ Some(AceInteraction::Unlocking {
+ source: AceUnlockSource::ProtocolOptional,
+ ..
+ })
+ ));
+
+ // User classification (non-whitelisted from)
+ let non_whitelisted = address!("1111111111111111111111111111111111111111");
+ let user_result = classify_ace_interaction(
+ &trace,
+ true,
+ &config,
+ Some(unlock_selector),
+ Some(contract),
+ Some(non_whitelisted),
+ );
+ assert!(matches!(
+ user_result,
+ Some(AceInteraction::Unlocking {
+ source: AceUnlockSource::User,
+ ..
+ })
+ ));
+
+ // NonUnlocking classification (no unlock signature)
+ let non_unlocking_result = classify_ace_interaction(
+ &trace,
+ true,
+ &config,
+ None, // no selector
+ Some(contract),
+ Some(whitelisted_from),
+ );
+ assert!(matches!(
+ non_unlocking_result,
+ Some(AceInteraction::NonUnlocking { .. })
+ ));
+}
+
+#[test]
+fn test_ace_interaction_priority_ordering() {
+ // Test that the priority ordering is correct:
+ // ProtocolForce > ProtocolOptional > User > NonUnlocking
+ let contract = address!("0000000aa232009084Bd71A5797d089AA4Edfad4");
+
+ let force = AceInteraction::Unlocking {
+ contract_address: contract,
+ source: AceUnlockSource::ProtocolForce,
+ };
+ let optional = AceInteraction::Unlocking {
+ contract_address: contract,
+ source: AceUnlockSource::ProtocolOptional,
+ };
+ let user = AceInteraction::Unlocking {
+ contract_address: contract,
+ source: AceUnlockSource::User,
+ };
+ let non_unlocking = AceInteraction::NonUnlocking {
+ contract_address: contract,
+ };
+
+ // Verify the classification methods work as expected for priority
+ assert!(force.is_force());
+ assert!(force.is_protocol_tx());
+ assert!(force.is_unlocking());
+
+ assert!(!optional.is_force());
+ assert!(optional.is_protocol_tx());
+ assert!(optional.is_unlocking());
+
+ assert!(!user.is_force());
+ assert!(!user.is_protocol_tx());
+ assert!(user.is_unlocking());
+
+ assert!(!non_unlocking.is_force());
+ assert!(!non_unlocking.is_protocol_tx());
+ assert!(!non_unlocking.is_unlocking());
+}
diff --git a/crates/rbuilder/src/building/testing/bundle_tests/setup.rs b/crates/rbuilder/src/building/testing/bundle_tests/setup.rs
index 40395227f..3fcfeb7dd 100644
--- a/crates/rbuilder/src/building/testing/bundle_tests/setup.rs
+++ b/crates/rbuilder/src/building/testing/bundle_tests/setup.rs
@@ -233,6 +233,7 @@ impl TestSetup {
order: self.order_builder.build_order(),
sim_value: Default::default(),
used_state_trace: Default::default(),
+ ace_interactions: Vec::new(),
};
// we commit order twice to test evm caching
diff --git a/crates/rbuilder/src/building/testing/mod.rs b/crates/rbuilder/src/building/testing/mod.rs
index 38c649890..0bde21593 100644
--- a/crates/rbuilder/src/building/testing/mod.rs
+++ b/crates/rbuilder/src/building/testing/mod.rs
@@ -1,4 +1,6 @@
#[cfg(test)]
+pub mod ace_tests;
+#[cfg(test)]
pub mod bundle_tests;
#[cfg(test)]
pub mod evm_inspector_tests;
diff --git a/crates/rbuilder/src/building/tracers.rs b/crates/rbuilder/src/building/tracers.rs
index cbbf3fb39..44a7211e9 100644
--- a/crates/rbuilder/src/building/tracers.rs
+++ b/crates/rbuilder/src/building/tracers.rs
@@ -16,6 +16,11 @@ pub trait SimulationTracer {
fn get_used_state_tracer(&self) -> Option<&UsedStateTrace> {
None
}
+
+ /// Take ownership of the used state trace, leaving a default in its place.
+ fn take_used_state_trace(&mut self) -> Option {
+ None
+ }
}
impl SimulationTracer for () {}
@@ -69,4 +74,8 @@ impl SimulationTracer for AccumulatorSimulationTracer {
fn get_used_state_tracer(&self) -> Option<&UsedStateTrace> {
Some(&self.used_state_trace)
}
+
+ fn take_used_state_trace(&mut self) -> Option {
+ Some(std::mem::take(&mut self.used_state_trace))
+ }
}
diff --git a/crates/rbuilder/src/live_builder/base_config.rs b/crates/rbuilder/src/live_builder/base_config.rs
index a82ebd2fd..053d5baf1 100644
--- a/crates/rbuilder/src/live_builder/base_config.rs
+++ b/crates/rbuilder/src/live_builder/base_config.rs
@@ -171,6 +171,17 @@ pub struct BaseConfig {
pub orderflow_tracing_store_path: Option,
/// Max number of blocks to keep in disk.
pub orderflow_tracing_max_blocks: usize,
+
+ /// Global ACE kill switch - when false, all ACE logic is disabled
+ #[serde(default = "default_ace_enabled")]
+ pub ace_enabled: bool,
+
+ /// Ace Configurations
+ pub ace_protocols: Vec,
+}
+
+fn default_ace_enabled() -> bool {
+ true
}
pub fn default_ip() -> Ipv4Addr {
@@ -271,6 +282,8 @@ impl BaseConfig {
simulation_use_random_coinbase: self.simulation_use_random_coinbase,
faster_finalize: self.faster_finalize,
order_flow_tracer_manager,
+ ace_enabled: self.ace_enabled,
+ ace_config: self.ace_protocols.clone(),
})
}
@@ -488,6 +501,8 @@ pub const DEFAULT_TIME_TO_KEEP_MEMPOOL_TXS_SECS: u64 = 60;
impl Default for BaseConfig {
fn default() -> Self {
Self {
+ ace_enabled: true,
+ ace_protocols: vec![],
full_telemetry_server_port: 6069,
full_telemetry_server_ip: default_ip(),
redacted_telemetry_server_port: 6070,
diff --git a/crates/rbuilder/src/live_builder/building/mod.rs b/crates/rbuilder/src/live_builder/building/mod.rs
index a650776d1..a93221acb 100644
--- a/crates/rbuilder/src/live_builder/building/mod.rs
+++ b/crates/rbuilder/src/live_builder/building/mod.rs
@@ -46,6 +46,8 @@ pub struct BlockBuildingPool {
sbundle_merger_selected_signers: Arc>,
order_flow_tracer_manager: Box,
built_block_id_source: Arc,
+ ace_enabled: bool,
+ ace_config: Vec,
}
impl BlockBuildingPool
@@ -62,6 +64,8 @@ where
run_sparse_trie_prefetcher: bool,
sbundle_merger_selected_signers: Arc>,
order_flow_tracer_manager: Box,
+ ace_enabled: bool,
+ ace_config: Vec,
) -> Self {
BlockBuildingPool {
provider,
@@ -73,6 +77,8 @@ where
sbundle_merger_selected_signers,
order_flow_tracer_manager,
built_block_id_source: Arc::new(BuiltBlockIdSource::new()),
+ ace_enabled,
+ ace_config,
}
}
@@ -149,6 +155,8 @@ where
orders_for_block,
block_cancellation.clone(),
sim_tracer,
+ self.ace_enabled,
+ self.ace_config.clone(),
);
self.start_building_job(
block_ctx,
diff --git a/crates/rbuilder/src/live_builder/config.rs b/crates/rbuilder/src/live_builder/config.rs
index cd55f4b17..235410dc5 100644
--- a/crates/rbuilder/src/live_builder/config.rs
+++ b/crates/rbuilder/src/live_builder/config.rs
@@ -61,6 +61,7 @@ use eyre::Context;
use lazy_static::lazy_static;
use rbuilder_config::EnvOrValue;
use rbuilder_primitives::mev_boost::{MevBoostRelayID, RelayMode};
+pub use rbuilder_primitives::AceConfig;
use reth_chainspec::{Chain, ChainSpec, NamedChain};
use reth_db::DatabaseEnv;
use reth_node_api::NodeTypesWithDBAdapter;
@@ -69,8 +70,9 @@ use reth_primitives::StaticFileSegment;
use reth_provider::StaticFileProviderFactory;
use serde::Deserialize;
use serde_with::{serde_as, OneOrMany};
+use std::collections::HashSet;
use std::{
- collections::{HashMap, HashSet},
+ collections::HashMap,
fmt::Debug,
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
path::{Path, PathBuf},
diff --git a/crates/rbuilder/src/live_builder/mod.rs b/crates/rbuilder/src/live_builder/mod.rs
index 48555cafc..fe9f71bf3 100644
--- a/crates/rbuilder/src/live_builder/mod.rs
+++ b/crates/rbuilder/src/live_builder/mod.rs
@@ -135,6 +135,9 @@ where
pub simulation_use_random_coinbase: bool,
pub order_flow_tracer_manager: Box,
+
+ pub ace_enabled: bool,
+ pub ace_config: Vec,
}
impl LiveBuilder
@@ -233,6 +236,8 @@ where
self.run_sparse_trie_prefetcher,
self.sbundle_merger_selected_signers.clone(),
self.order_flow_tracer_manager,
+ self.ace_enabled,
+ self.ace_config.clone(),
);
ready_to_build.store(true, Ordering::Relaxed);
diff --git a/crates/rbuilder/src/live_builder/order_flow_tracing/order_flow_tracer.rs b/crates/rbuilder/src/live_builder/order_flow_tracing/order_flow_tracer.rs
index b8361aab0..71f058518 100644
--- a/crates/rbuilder/src/live_builder/order_flow_tracing/order_flow_tracer.rs
+++ b/crates/rbuilder/src/live_builder/order_flow_tracing/order_flow_tracer.rs
@@ -84,24 +84,30 @@ impl OrderFlowTracer {
impl SimulationJobTracer for OrderFlowTracer {
fn update_simulation_sent(&self, sim_result: &SimulatedResult) {
+ let SimulatedResult::Success {
+ simulation_time,
+ simulated_order,
+ ..
+ } = sim_result
+ else {
+ // Only Success variants are traced
+ return;
+ };
let event = SimulationEvent::SimulatedOrder(SimulatedOrderData {
- simulation_time: sim_result.simulation_time,
- order_id: sim_result.simulated_order.order.id(),
- replacement_key_and_sequence_number: sim_result
- .simulated_order
+ simulation_time: *simulation_time,
+ order_id: simulated_order.order.id(),
+ replacement_key_and_sequence_number: simulated_order
.order
.replacement_key_and_sequence_number(),
- full_profit: sim_result
- .simulated_order
+ full_profit: simulated_order
.sim_value
.full_profit_info()
.coinbase_profit(),
- non_mempool_profit: sim_result
- .simulated_order
+ non_mempool_profit: simulated_order
.sim_value
.non_mempool_profit_info()
.coinbase_profit(),
- gas_used: sim_result.simulated_order.sim_value.gas_used(),
+ gas_used: simulated_order.sim_value.gas_used(),
});
self.sim_events
.lock()
diff --git a/crates/rbuilder/src/live_builder/simulation/mod.rs b/crates/rbuilder/src/live_builder/simulation/mod.rs
index 1dfa36514..df2e86bf7 100644
--- a/crates/rbuilder/src/live_builder/simulation/mod.rs
+++ b/crates/rbuilder/src/live_builder/simulation/mod.rs
@@ -39,6 +39,10 @@ pub struct SimulationContext {
pub requests: flume::Receiver,
/// Simulation results go out through this channel.
pub results: mpsc::Sender,
+ /// ACE configuration for this simulation context (empty if ACE is disabled).
+ pub ace_configs: ahash::HashMap,
+ /// Whether ACE is enabled globally.
+ pub ace_enabled: bool,
}
/// All active SimulationContexts
@@ -117,6 +121,8 @@ where
input: OrdersForBlock,
block_cancellation: CancellationToken,
sim_tracer: Arc,
+ ace_enabled: bool,
+ ace_config: Vec,
) -> SlotOrderSimResults {
let (slot_sim_results_sender, slot_sim_results_receiver) = mpsc::channel(10_000);
@@ -153,7 +159,20 @@ where
NonceCache::new(state.into())
};
- let sim_tree = SimTree::new(nonces);
+ // Convert ace_config Vec to HashMap for efficient lookup
+ // When ace_enabled is false, we pass empty configs to disable all ACE logic
+ let ace_configs_map: ahash::HashMap<_, _> = if ace_enabled {
+ ace_config
+ .iter()
+ .map(|c| (c.contract_address, c.clone()))
+ .collect()
+ } else {
+ ahash::HashMap::default()
+ };
+
+ // Pass empty configs to SimTree when ACE is disabled
+ let sim_tree_configs = if ace_enabled { ace_config } else { Vec::new() };
+ let sim_tree = SimTree::new(nonces, sim_tree_configs);
let new_order_sub = input.new_order_sub;
let (sim_req_sender, sim_req_receiver) = flume::unbounded();
let (sim_results_sender, sim_results_receiver) = mpsc::channel(1024);
@@ -163,6 +182,8 @@ where
block_ctx: ctx,
requests: sim_req_receiver,
results: sim_results_sender,
+ ace_configs: ace_configs_map,
+ ace_enabled,
};
contexts.contexts.insert(block_context, sim_context);
}
@@ -236,6 +257,8 @@ mod tests {
orders_for_block,
cancel.clone(),
Arc::new(NullSimulationJobTracer {}),
+ false, // ace_enabled
+ vec![],
);
// Create a simple tx that sends to coinbase 5 wei.
let coinbase_profit = 5;
diff --git a/crates/rbuilder/src/live_builder/simulation/sim_worker.rs b/crates/rbuilder/src/live_builder/simulation/sim_worker.rs
index 838ad3c6c..2f34c0ce6 100644
--- a/crates/rbuilder/src/live_builder/simulation/sim_worker.rs
+++ b/crates/rbuilder/src/live_builder/simulation/sim_worker.rs
@@ -1,6 +1,6 @@
use crate::{
building::{
- sim::{NonceKey, OrderSimResult, SimulatedResult},
+ sim::{DependencyKey, NonceKey, OrderSimResult, SimulatedResult},
simulate_order, BlockState, ThreadBlockBuildingContext,
},
live_builder::simulation::CurrentSimulationContexts,
@@ -8,6 +8,7 @@ use crate::{
telemetry::{self, add_sim_thread_utilisation_timings, mark_order_simulation_end},
};
use parking_lot::Mutex;
+use rbuilder_primitives::ace::AceInteraction;
use std::{
sync::Arc,
thread::sleep,
@@ -66,32 +67,69 @@ pub fn run_sim_worker(
let mut block_state = BlockState::new_arc(state_provider.clone());
let sim_result = simulate_order(
task.parents.clone(),
- task.order,
+ task.order.clone(),
¤t_sim_context.block_ctx,
&mut local_ctx,
&mut block_state,
+ ¤t_sim_context.ace_configs,
+ &task.ace_unlock_contracts,
);
let sim_ok = match sim_result {
Ok(sim_result) => {
let sim_ok = match sim_result.result {
OrderSimResult::Success(simulated_order, nonces_after) => {
- let result = SimulatedResult {
+ let mut dependencies_satisfied: Vec = nonces_after
+ .into_iter()
+ .map(|(address, nonce)| {
+ DependencyKey::Nonce(NonceKey { address, nonce })
+ })
+ .collect();
+
+ // Add ACE dependencies for all unlocking interactions
+ for interaction in &simulated_order.ace_interactions {
+ if let AceInteraction::Unlocking {
+ contract_address, ..
+ } = interaction
+ {
+ dependencies_satisfied
+ .push(DependencyKey::AceUnlock(*contract_address));
+ }
+ }
+
+ let result = SimulatedResult::Success {
id: task.id,
simulated_order,
previous_orders: task.parents,
- nonces_after: nonces_after
- .into_iter()
- .map(|(address, nonce)| NonceKey { address, nonce })
- .collect(),
+ dependencies_satisfied,
simulation_time: start_time.elapsed(),
};
- current_sim_context
- .results
- .try_send(result)
- .unwrap_or_default();
+ if current_sim_context.results.try_send(result).is_err() {
+ error!(
+ ?order_id,
+ "Failed to send simulation result - channel full or closed"
+ );
+ }
true
}
- OrderSimResult::Failed(_) => false,
+ OrderSimResult::Failed(failure) => {
+ // Only send to SimTree if there's an ACE dependency to handle
+ if failure.ace_dependency.is_some() {
+ let result = SimulatedResult::Failed {
+ id: task.id,
+ order: task.order,
+ failure,
+ ace_unlock_contracts: task.ace_unlock_contracts.clone(),
+ simulation_time: start_time.elapsed(),
+ };
+ if current_sim_context.results.try_send(result).is_err() {
+ error!(
+ ?order_id,
+ "Failed to send Failed result with ACE dependency"
+ );
+ }
+ }
+ false
+ }
};
telemetry::inc_simulated_orders(sim_ok);
telemetry::inc_simulation_gas_used(sim_result.gas_used);
@@ -99,7 +137,6 @@ pub fn run_sim_worker(
}
Err(err) => {
error!(?err, ?order_id, "Critical error while simulating order");
- // @Metric
break;
}
};
diff --git a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs
index b1bff3c57..616cacbcb 100644
--- a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs
+++ b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs
@@ -1,7 +1,7 @@
use std::{fmt, sync::Arc};
use crate::{
- building::sim::{SimTree, SimulatedResult, SimulationRequest},
+ building::sim::{SimTree, SimulatedResult, SimulationFailure, SimulationRequest},
live_builder::{
order_input::order_sink::OrderPoolCommand,
simulation::simulation_job_tracer::SimulationJobTracer,
@@ -190,53 +190,80 @@ impl SimulationJob {
&mut self,
new_sim_results: &mut Vec,
) -> bool {
- // send results
- let mut valid_simulated_orders = Vec::new();
- for sim_result in new_sim_results {
- trace!(order_id=?sim_result.simulated_order.order.id(),
- sim_duration_mus = sim_result.simulation_time.as_micros(),
- profit = format_ether(sim_result.simulated_order.sim_value.full_profit_info().coinbase_profit()), "Order simulated");
- self.orders_simulated_ok
- .accumulate(&sim_result.simulated_order.order);
- if let Some(repl_key) = sim_result.simulated_order.order.replacement_key() {
- self.unique_replacement_key_bundles_sim_ok.insert(repl_key);
- self.orders_with_replacement_key_sim_ok += 1;
- }
- // Skip cancelled orders and remove from in_flight_orders
- if self
- .in_flight_orders
- .remove(&sim_result.simulated_order.id())
- {
- valid_simulated_orders.push(sim_result.clone());
- // Only send if it's the first time.
- if self
- .not_cancelled_sent_simulated_orders
- .insert(sim_result.simulated_order.id())
- {
- if self
- .slot_sim_results_sender
- .send(SimulatedOrderCommand::Simulation(
- sim_result.simulated_order.clone(),
- ))
- .await
- .is_err()
- {
- return false; //receiver closed :(
- } else {
- self.sim_tracer.update_simulation_sent(sim_result);
+ // Results to pass to sim_tree: successful sims and failed ones needing ACE re-queue
+ let mut sim_tree_results = Vec::new();
+ for sim_result in new_sim_results.drain(..) {
+ match &sim_result {
+ SimulatedResult::Success {
+ simulated_order,
+ simulation_time,
+ ..
+ } => {
+ trace!(order_id=?simulated_order.order.id(),
+ sim_duration_mus = simulation_time.as_micros(),
+ profit = format_ether(simulated_order.sim_value.full_profit_info().coinbase_profit()), "Order simulated");
+ self.orders_simulated_ok.accumulate(&simulated_order.order);
+ if let Some(repl_key) = simulated_order.order.replacement_key() {
+ self.unique_replacement_key_bundles_sim_ok.insert(repl_key);
+ self.orders_with_replacement_key_sim_ok += 1;
+ }
+ // Skip cancelled orders and remove from in_flight_orders
+ if self.in_flight_orders.remove(&simulated_order.id()) {
+ // Only send if it's the first time.
+ if self
+ .not_cancelled_sent_simulated_orders
+ .insert(simulated_order.id())
+ {
+ if self
+ .slot_sim_results_sender
+ .send(SimulatedOrderCommand::Simulation(simulated_order.clone()))
+ .await
+ .is_err()
+ {
+ return false; //receiver closed :(
+ } else {
+ self.sim_tracer.update_simulation_sent(&sim_result);
+ }
+ }
+ sim_tree_results.push(sim_result);
}
}
+ SimulatedResult::Failed {
+ failure:
+ SimulationFailure {
+ ace_dependency: Some(_),
+ ..
+ },
+ ..
+ } => {
+ // Failed with ACE dependency - pass to sim_tree for re-queuing with unlock parent
+ sim_tree_results.push(sim_result);
+ }
+ SimulatedResult::Failed { .. } => {
+ // Permanent failure without ACE dependency - nothing to do
+ }
}
}
// update simtree
- if let Err(err) = self
+ let (_, cancellations) = match self
.sim_tree
- .submit_simulation_tasks_results(valid_simulated_orders)
+ .submit_simulation_tasks_results(sim_tree_results)
{
- error!(?err, "Failed to push order sim results into the sim tree");
- // @Metric
- return false;
+ Ok(result) => result,
+ Err(err) => {
+ error!(?err, "Failed to push order sim results into the sim tree");
+ // @Metric
+ return false;
+ }
+ };
+
+ // Send any cancellations generated by the sim tree (e.g., optional ACE unlocks superseded by mempool)
+ for cancel_id in cancellations {
+ if !self.send_cancel(&cancel_id).await {
+ return false;
+ }
}
+
true
}
diff --git a/crates/rbuilder/src/live_builder/wallet_balance_watcher.rs b/crates/rbuilder/src/live_builder/wallet_balance_watcher.rs
index c076b240a..016e9f07c 100644
--- a/crates/rbuilder/src/live_builder/wallet_balance_watcher.rs
+++ b/crates/rbuilder/src/live_builder/wallet_balance_watcher.rs
@@ -3,7 +3,7 @@ use std::time::Duration;
use alloy_primitives::{utils::format_ether, Address, BlockNumber, U256};
use reth::providers::ProviderError;
use time::{error, OffsetDateTime};
-use tracing::{error, info, warn};
+use tracing::{info, warn};
use crate::{
provider::StateProviderFactory,
diff --git a/crates/rbuilder/src/mev_boost/mod.rs b/crates/rbuilder/src/mev_boost/mod.rs
index 38fbd0e5d..bc5ccc90d 100644
--- a/crates/rbuilder/src/mev_boost/mod.rs
+++ b/crates/rbuilder/src/mev_boost/mod.rs
@@ -1,6 +1,7 @@
use crate::telemetry::{add_gzip_compression_time, add_ssz_encoding_time};
use super::utils::u256decimal_serde_helper;
+
use alloy_primitives::{utils::parse_ether, Address, BlockHash, U256};
use alloy_rpc_types_beacon::BlsPublicKey;
use flate2::{write::GzEncoder, Compression};
diff --git a/examples/config/rbuilder/config-live-example.toml b/examples/config/rbuilder/config-live-example.toml
index 790a6af4d..4d03c5081 100644
--- a/examples/config/rbuilder/config-live-example.toml
+++ b/examples/config/rbuilder/config-live-example.toml
@@ -38,7 +38,7 @@ enabled_relays = ["flashbots"]
subsidy = "0.01"
[[subsidy_overrides]]
-relay = "flashbots_test2"
+relay = "flashbots_test2"
value = "0.05"
# This can be used with test-relay
@@ -57,7 +57,6 @@ mode = "full"
max_bid_eth = "0.05"
-
[[builders]]
name = "mgp-ordering"
algo = "ordering-builder"
@@ -81,6 +80,22 @@ discard_txs = true
num_threads = 25
safe_sorting_only = false
+[[ace_protocols]]
+# Contract address serves as unique identifier for this ACE protocol
+contract_address = "0x0000000aa232009084Bd71A5797d089AA4Edfad4"
+from_addresses = [
+ "0xc41ae140ca9b281d8a1dc254c50e446019517d04",
+ "0xd437f3372f3add2c2bc3245e6bd6f9c202e61bb3",
+ "0x693ca5c6852a7d212dabc98b28e15257465c11f3",
+]
+to_addresses = ["0x0000000aa232009084Bd71A5797d089AA4Edfad4"]
+# _lastBlockUpdated storage slot (slot 3)
+detection_slots = ["0x0000000000000000000000000000000000000000000000000000000000000003"]
+# unlockWithEmptyAttestation(address,bytes) nonpayable
+unlock_signatures = ["0x1828e0e7"]
+# execute(bytes) nonpayable
+force_signatures = ["0x09c5eabe"]
+
[[relay_bid_scrapers]]
type = "ultrasound-ws"
name = "ultrasound-ws-eu"
@@ -92,4 +107,3 @@ type = "ultrasound-ws"
name = "ultrasound-ws-us"
ultrasound_url = "ws://relay-builders-us.ultrasound.money/ws/v1/top_bid"
relay_name = "ultrasound-money-us"
-