,
) -> Result<(), ProviderError> {
- for result in results {
- self.process_simulation_task_result(result)?;
- }
+ results.retain_mut(|result| {
+ let (skip, cancel) = self.handle_ace_interaction(result).unwrap_or_else(|err| {
+ error!(?err, "Failed to handle ACE interaction");
+ (false, None)
+ });
+
+ if let Some(id) = cancel {
+ let _ = cancellation_sender.try_send(SimulatedOrderCommand::Cancellation(id));
+ }
+
+ if !skip {
+ if let Err(err) = self.process_simulation_task_result(result.clone()) {
+ error!(?err, "Failed to process simulation result");
+ }
+ }
+
+ !skip
+ });
+
Ok(())
}
}
@@ -318,6 +580,8 @@ pub fn simulate_all_orders_with_sim_tree(
ctx: &BlockBuildingContext,
orders: &[Order],
randomize_insertion: bool,
+ ace_config: Vec,
+ cancellation_sender: &mpsc::Sender,
) -> Result<(Vec>, Vec), CriticalCommitOrderError>
where
P: StateProviderFactory + Clone,
@@ -326,7 +590,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);
@@ -369,6 +633,7 @@ where
ctx,
&mut local_ctx,
&mut block_state,
+ sim_tree.ace_configs(),
)?;
let (_, provider) = block_state.into_parts();
state_for_sim = provider;
@@ -383,22 +648,31 @@ where
continue;
}
OrderSimResult::Success(sim_order, nonces) => {
+ let mut dependencies_satisfied: Vec = nonces
+ .into_iter()
+ .map(|(address, nonce)| DependencyKey::Nonce(NonceKey { address, nonce }))
+ .collect();
+
+ // If this is an unlocking ACE order, add the ACE dependency
+ if let Some(AceInteraction::Unlocking {
+ contract_address, ..
+ }) = sim_order.ace_interaction
+ {
+ dependencies_satisfied.push(DependencyKey::AceUnlock(contract_address));
+ }
+
let result = SimulatedResult {
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)?;
+ sim_tree.submit_simulation_tasks_results(&mut sim_results, cancellation_sender)?;
}
Ok((
@@ -418,14 +692,21 @@ pub fn simulate_order(
ctx: &BlockBuildingContext,
local_ctx: &mut ThreadBlockBuildingContext,
state: &mut BlockState,
+ ace_configs: &HashMap,
) -> 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,
+ );
fork.rollback(rollback_point);
let sim_res = sim_res?;
+
Ok(OrderSimResultWithGas {
result: sim_res,
gas_used: tracer.used_gas,
@@ -438,8 +719,11 @@ pub fn simulate_order_using_fork(
order: Order,
fork: &mut PartialBlockFork<'_, '_, '_, '_, Tracer, NullPartialBlockForkExecutionTracer>,
mempool_tx_detector: &MempoolTxsDetector,
+ ace_configs: &HashMap,
) -> Result {
let start = Instant::now();
+ let has_parents = !parent_orders.is_empty();
+
// simulate parents
let mut space_state = BlockBuildingSpaceState::ZERO;
// We use empty combined refunds because the value of the bundle will
@@ -461,7 +745,37 @@ 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_ref()
+ .and_then(|t| t.get_used_state_tracer())
+ .cloned();
+
+ // Detect ACE interaction from the state trace using config
+ // Get function selector and tx.to from order's first transaction
+ let (selector, tx_to): (Option, Option) =
+ order.list_txs().first().map_or((None, None), |(tx, _)| {
+ let input = tx.internal_tx_unsecure().input();
+ let sel = if input.len() >= 4 {
+ Some(Selector::from_slice(&input[..4]))
+ } else {
+ None
+ };
+ (sel, tx.to())
+ });
+
+ let ace_interaction = used_state_trace.as_ref().and_then(|trace| {
+ ace_configs.iter().find_map(|(_, config)| {
+ if !config.enabled {
+ return None;
+ }
+ classify_ace_interaction(trace, sim_success, config, selector, tx_to)
+ })
+ });
match result {
Ok(res) => {
@@ -475,10 +789,44 @@ pub fn simulate_order_using_fork(
order,
sim_value,
used_state_trace: res.used_state_trace,
+ ace_interaction,
}),
new_nonces,
))
}
- Err(err) => Ok(OrderSimResult::Failed(err)),
+ Err(err) => {
+ // Check if failed order accessed ACE - if so, treat as successful with zero profit
+ if let Some(interaction @ AceInteraction::NonUnlocking { contract_address }) =
+ ace_interaction
+ {
+ // ACE can inject parent orders, we want to ignore these.
+ if !has_parents {
+ tracing::debug!(
+ order = ?order.id(),
+ ?err,
+ ?contract_address,
+ "Failed order accessed ACE - treating as successful non-unlocking ACE order"
+ );
+ // For failed-but-ACE orders, we use 0 gas since the order
+ // didn't actually succeed - it's just marked as a non-unlocking ACE interaction
+ let gas_used = 0;
+ return Ok(OrderSimResult::Success(
+ Arc::new(SimulatedOrder {
+ order,
+ sim_value: SimValue::new(
+ U256::ZERO,
+ U256::ZERO,
+ BlockSpace::new(gas_used, 0, 0),
+ Vec::new(),
+ ),
+ used_state_trace,
+ ace_interaction: Some(interaction),
+ }),
+ Vec::new(),
+ ));
+ }
+ }
+ Ok(OrderSimResult::Failed(err))
+ }
}
}
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..de4d45018
--- /dev/null
+++ b/crates/rbuilder/src/building/testing/ace_tests/mod.rs
@@ -0,0 +1,313 @@
+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 {
+ enabled: true,
+ 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_interaction: Some(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_interaction: Some(AceInteraction::Unlocking {
+ contract_address: contract,
+ source: AceUnlockSource::ProtocolOptional,
+ }),
+ })
+}
+
+/// Create a mock non-unlocking order (accesses ACE slot but doesn't unlock)
+fn create_non_unlocking_order(contract: Address, gas_used: u64) -> Arc {
+ let slot = b256!("0000000000000000000000000000000000000000000000000000000000000003");
+ Arc::new(SimulatedOrder {
+ order: create_test_order(),
+ sim_value: SimValue::new(
+ U256::from(15),
+ U256::from(15),
+ BlockSpace::new(gas_used, 0, 0),
+ Vec::new(),
+ ),
+ used_state_trace: Some(mock_state_trace_with_ace_slot(contract, slot)),
+ ace_interaction: Some(AceInteraction::NonUnlocking {
+ contract_address: contract,
+ }),
+ })
+}
+
+#[test]
+fn test_ace_exchange_state_get_unlock_order_force_only() {
+ let order = create_force_unlock_order(
+ address!("0000000aa232009084Bd71A5797d089AA4Edfad4"),
+ 100_000,
+ );
+ let mut state = AceExchangeState::default();
+ state.force_unlock_order = Some(order.clone());
+
+ 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 mut state = AceExchangeState::default();
+ state.optional_unlock_order = Some(order.clone());
+
+ 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 mut state = AceExchangeState::default();
+ state.force_unlock_order = Some(expensive_order.clone());
+ state.optional_unlock_order = Some(cheap_order.clone());
+
+ // 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 mut state = AceExchangeState::default();
+ state.force_unlock_order = Some(force_order.clone());
+ state.optional_unlock_order = Some(optional_order.clone());
+
+ // 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_interaction_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 mut result = SimulatedResult {
+ 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 interaction
+ let (skip_forwarding, cancellation) = sim_tree.handle_ace_interaction(&mut result)?;
+
+ // Unlocking orders are not skipped (they go downstream), but should be cancelled
+ assert!(!skip_forwarding);
+ assert_eq!(cancellation, Some(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"),
+ }
+}
diff --git a/crates/rbuilder/src/building/testing/bundle_tests/setup.rs b/crates/rbuilder/src/building/testing/bundle_tests/setup.rs
index 40395227f..c0338f2e9 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_interaction: None,
};
// 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/live_builder/base_config.rs b/crates/rbuilder/src/live_builder/base_config.rs
index a82ebd2fd..892e0ff24 100644
--- a/crates/rbuilder/src/live_builder/base_config.rs
+++ b/crates/rbuilder/src/live_builder/base_config.rs
@@ -171,6 +171,9 @@ pub struct BaseConfig {
pub orderflow_tracing_store_path: Option,
/// Max number of blocks to keep in disk.
pub orderflow_tracing_max_blocks: usize,
+
+ /// Ace Configurations
+ pub ace_protocols: Vec,
}
pub fn default_ip() -> Ipv4Addr {
@@ -271,6 +274,7 @@ impl BaseConfig {
simulation_use_random_coinbase: self.simulation_use_random_coinbase,
faster_finalize: self.faster_finalize,
order_flow_tracer_manager,
+ ace_config: self.ace_protocols.clone(),
})
}
@@ -488,6 +492,7 @@ pub const DEFAULT_TIME_TO_KEEP_MEMPOOL_TXS_SECS: u64 = 60;
impl Default for BaseConfig {
fn default() -> Self {
Self {
+ 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..a46f7b4cf 100644
--- a/crates/rbuilder/src/live_builder/building/mod.rs
+++ b/crates/rbuilder/src/live_builder/building/mod.rs
@@ -46,6 +46,7 @@ pub struct BlockBuildingPool {
sbundle_merger_selected_signers: Arc>,
order_flow_tracer_manager: Box,
built_block_id_source: Arc,
+ ace_config: Vec,
}
impl BlockBuildingPool
@@ -62,6 +63,7 @@ where
run_sparse_trie_prefetcher: bool,
sbundle_merger_selected_signers: Arc>,
order_flow_tracer_manager: Box,
+ ace_config: Vec,
) -> Self {
BlockBuildingPool {
provider,
@@ -73,6 +75,7 @@ where
sbundle_merger_selected_signers,
order_flow_tracer_manager,
built_block_id_source: Arc::new(BuiltBlockIdSource::new()),
+ ace_config,
}
}
@@ -149,6 +152,7 @@ where
orders_for_block,
block_cancellation.clone(),
sim_tracer,
+ 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..85c0e02d6 100644
--- a/crates/rbuilder/src/live_builder/mod.rs
+++ b/crates/rbuilder/src/live_builder/mod.rs
@@ -135,6 +135,8 @@ where
pub simulation_use_random_coinbase: bool,
pub order_flow_tracer_manager: Box,
+
+ pub ace_config: Vec,
}
impl LiveBuilder
@@ -233,6 +235,7 @@ where
self.run_sparse_trie_prefetcher,
self.sbundle_merger_selected_signers.clone(),
self.order_flow_tracer_manager,
+ self.ace_config.clone(),
);
ready_to_build.store(true, Ordering::Relaxed);
diff --git a/crates/rbuilder/src/live_builder/simulation/mod.rs b/crates/rbuilder/src/live_builder/simulation/mod.rs
index 1dfa36514..d06991408 100644
--- a/crates/rbuilder/src/live_builder/simulation/mod.rs
+++ b/crates/rbuilder/src/live_builder/simulation/mod.rs
@@ -39,6 +39,8 @@ pub struct SimulationContext {
pub requests: flume::Receiver,
/// Simulation results go out through this channel.
pub results: mpsc::Sender,
+ /// ACE configuration for this simulation context.
+ pub ace_configs: ahash::HashMap,
}
/// All active SimulationContexts
@@ -117,6 +119,7 @@ where
input: OrdersForBlock,
block_cancellation: CancellationToken,
sim_tracer: Arc,
+ ace_config: Vec,
) -> SlotOrderSimResults {
let (slot_sim_results_sender, slot_sim_results_receiver) = mpsc::channel(10_000);
@@ -153,7 +156,14 @@ where
NonceCache::new(state.into())
};
- let sim_tree = SimTree::new(nonces);
+ // Convert ace_config Vec to HashMap for efficient lookup
+ let ace_configs_map: ahash::HashMap<_, _> = ace_config
+ .iter()
+ .filter(|c| c.enabled)
+ .map(|c| (c.contract_address, c.clone()))
+ .collect();
+
+ let sim_tree = SimTree::new(nonces, ace_config);
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 +173,7 @@ where
block_ctx: ctx,
requests: sim_req_receiver,
results: sim_results_sender,
+ ace_configs: ace_configs_map,
};
contexts.contexts.insert(block_context, sim_context);
}
@@ -236,6 +247,7 @@ mod tests {
orders_for_block,
cancel.clone(),
Arc::new(NullSimulationJobTracer {}),
+ 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..c96931c8a 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,
@@ -70,19 +71,33 @@ pub fn run_sim_worker(
¤t_sim_context.block_ctx,
&mut local_ctx,
&mut block_state,
+ ¤t_sim_context.ace_configs,
);
let sim_ok = match sim_result {
Ok(sim_result) => {
let sim_ok = match sim_result.result {
OrderSimResult::Success(simulated_order, nonces_after) => {
+ let mut dependencies_satisfied: Vec = nonces_after
+ .into_iter()
+ .map(|(address, nonce)| {
+ DependencyKey::Nonce(NonceKey { address, nonce })
+ })
+ .collect();
+
+ // If this is an unlocking ACE order, add the ACE dependency
+ if let Some(AceInteraction::Unlocking {
+ contract_address, ..
+ }) = simulated_order.ace_interaction
+ {
+ dependencies_satisfied
+ .push(DependencyKey::AceUnlock(contract_address));
+ }
+
let result = SimulatedResult {
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
diff --git a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs
index b1bff3c57..7ce0541e2 100644
--- a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs
+++ b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs
@@ -229,10 +229,10 @@ impl SimulationJob {
}
}
// update simtree
- if let Err(err) = self
- .sim_tree
- .submit_simulation_tasks_results(valid_simulated_orders)
- {
+ if let Err(err) = self.sim_tree.submit_simulation_tasks_results(
+ &mut valid_simulated_orders,
+ &self.slot_sim_results_sender,
+ ) {
error!(?err, "Failed to push order sim results into the sim tree");
// @Metric
return false;
diff --git a/crates/rbuilder/src/mev_boost/mod.rs b/crates/rbuilder/src/mev_boost/mod.rs
index 38fbd0e5d..970d9de78 100644
--- a/crates/rbuilder/src/mev_boost/mod.rs
+++ b/crates/rbuilder/src/mev_boost/mod.rs
@@ -1,6 +1,8 @@
use crate::telemetry::{add_gzip_compression_time, add_ssz_encoding_time};
use super::utils::u256decimal_serde_helper;
+use itertools::Itertools;
+
use alloy_primitives::{utils::parse_ether, Address, BlockHash, U256};
use alloy_rpc_types_beacon::BlsPublicKey;
use flate2::{write::GzEncoder, Compression};
@@ -794,6 +796,29 @@ impl RelayClient {
if let Some(top_competitor_bid) = metadata.value.top_competitor_bid {
builder = builder.header(TOP_BID_HEADER, top_competitor_bid.to_string());
}
+ if !metadata.order_ids.is_empty() {
+ const MAX_BUNDLE_IDS: usize = 150;
+ let bundle_ids: Vec<_> = metadata
+ .order_ids
+ .iter()
+ .filter_map(|order| match order {
+ rbuilder_primitives::OrderId::Tx(_fixed_bytes) => None,
+ rbuilder_primitives::OrderId::Bundle(uuid) => Some(uuid),
+ rbuilder_primitives::OrderId::ShareBundle(_fixed_bytes) => None,
+ })
+ .collect();
+ let total_bundles = bundle_ids.len();
+ let mut bundle_ids = bundle_ids
+ .iter()
+ .take(MAX_BUNDLE_IDS)
+ .map(|uuid| format!("{uuid:?}"));
+ let bundle_ids = if total_bundles > MAX_BUNDLE_IDS {
+ bundle_ids.join(",") + ",CAPPED"
+ } else {
+ bundle_ids.join(",")
+ };
+ builder = builder.header(BUNDLE_HASHES_HEADER, bundle_ids);
+ }
const MAX_BUNDLE_HASHES: usize = 150;
if !metadata.bundle_hashes.is_empty() {
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"
-