From d1ddf4253aaa2113176f9d82e6905d92ec2c63b1 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Wed, 15 Oct 2025 11:24:25 -0400 Subject: [PATCH 01/51] wip: ace cleanup --- Cargo.lock | 19 +- crates/rbuilder-primitives/Cargo.toml | 1 + crates/rbuilder-primitives/src/ace.rs | 90 +++++ crates/rbuilder-primitives/src/lib.rs | 44 ++- crates/rbuilder/src/building/ace_bundler.rs | 388 ++++++++++++++++++++ crates/rbuilder/src/building/mod.rs | 1 + 6 files changed, 527 insertions(+), 16 deletions(-) create mode 100644 crates/rbuilder-primitives/src/ace.rs create mode 100644 crates/rbuilder/src/building/ace_bundler.rs diff --git a/Cargo.lock b/Cargo.lock index e9e33c29d..09838dc67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,7 +105,7 @@ dependencies = [ "alloy-rlp", "num_enum", "serde", - "strum 0.27.1", + "strum 0.27.2", ] [[package]] @@ -801,7 +801,7 @@ dependencies = [ "jsonwebtoken", "rand 0.8.5", "serde", - "strum 0.27.1", + "strum 0.27.2", ] [[package]] @@ -9604,6 +9604,7 @@ dependencies = [ "serde_with", "sha2 0.10.9", "ssz_types 0.8.0", + "strum 0.27.2", "thiserror 1.0.69", "time", "toml 0.8.20", @@ -10235,7 +10236,7 @@ dependencies = [ "reth-storage-errors", "reth-tracing", "rustc-hash 2.1.1", - "strum 0.27.1", + "strum 0.27.2", "sysinfo 0.33.1", "tempfile", "thiserror 2.0.12", @@ -11363,7 +11364,7 @@ dependencies = [ "secp256k1 0.30.0", "serde", "shellexpand", - "strum 0.27.1", + "strum 0.27.2", "thiserror 2.0.12", "toml 0.8.20", "tracing", @@ -11658,7 +11659,7 @@ dependencies = [ "reth-trie-db", "revm-database", "revm-state", - "strum 0.27.1", + "strum 0.27.2", "tokio", "tracing", ] @@ -12095,7 +12096,7 @@ dependencies = [ "reth-errors", "reth-network-api", "serde", - "strum 0.27.1", + "strum 0.27.2", ] [[package]] @@ -12212,7 +12213,7 @@ dependencies = [ "clap 4.5.36", "derive_more 2.0.1", "serde", - "strum 0.27.1", + "strum 0.27.2", ] [[package]] @@ -14271,9 +14272,9 @@ dependencies = [ [[package]] name = "strum" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ "strum_macros 0.27.1", ] diff --git a/crates/rbuilder-primitives/Cargo.toml b/crates/rbuilder-primitives/Cargo.toml index 092d84003..87ec01521 100644 --- a/crates/rbuilder-primitives/Cargo.toml +++ b/crates/rbuilder-primitives/Cargo.toml @@ -54,6 +54,7 @@ eyre.workspace = true serde.workspace = true derive_more.workspace = true serde_json.workspace = true +strum = { version = "0.27.2", features = ["derive"] } [dev-dependencies] rand.workspace = true diff --git a/crates/rbuilder-primitives/src/ace.rs b/crates/rbuilder-primitives/src/ace.rs new file mode 100644 index 000000000..8f14df0b7 --- /dev/null +++ b/crates/rbuilder-primitives/src/ace.rs @@ -0,0 +1,90 @@ +use crate::evm_inspector::UsedStateTrace; +use alloy_primitives::{address, Address}; +use strum::EnumIter; + +/// What ace based exchanges that rbuilder supports. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)] +pub enum AceExchange { + Angstrom, +} + +impl AceExchange { + /// Get the Angstrom variant + pub const fn angstrom() -> Self { + Self::Angstrom + } + + /// Get the address for this exchange + pub fn address(&self) -> Address { + match self { + AceExchange::Angstrom => address!("0000000aa232009084Bd71A5797d089AA4Edfad4"), + } + } + + /// Get the number of blocks this ACE exchange's transactions should be valid for + pub fn blocks_to_live(&self) -> u64 { + match self { + AceExchange::Angstrom => 1, + } + } + + /// Classify an ACE transaction interaction type based on state trace and simulation success + pub fn classify_ace_interaction( + &self, + state_trace: &UsedStateTrace, + sim_success: bool, + ) -> Option { + match self { + AceExchange::Angstrom => { + Self::angstrom_classify_interaction(state_trace, sim_success, *self) + } + } + } + + /// Angstrom-specific classification logic + fn angstrom_classify_interaction( + state_trace: &UsedStateTrace, + sim_success: bool, + exchange: AceExchange, + ) -> Option { + let angstrom_address = exchange.address(); + + // We need to include read here as if it tries to reads the lastBlockUpdated on the pre swap + // hook. it will revert and not make any changes if the pools not unlocked. We want to capture + // this. + let accessed_exchange = state_trace + .read_slot_values + .keys() + .any(|k| k.address == angstrom_address) + || state_trace + .written_slot_values + .keys() + .any(|k| k.address == angstrom_address); + + accessed_exchange.then(|| { + if sim_success { + AceInteraction::Unlocking { exchange } + } else { + AceInteraction::NonUnlocking { exchange } + } + }) + } +} + +/// Type of ACE interaction for mempool transactions +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum AceInteraction { + /// Unlocking ACE tx, doesn't revert without an ACE tx, must be placed with ACE bundle + Unlocking { exchange: AceExchange }, + /// Requires an unlocking ACE tx, will revert otherwise + NonUnlocking { exchange: AceExchange }, +} + +/// Type of unlock for ACE protocol transactions (Order::Ace) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] +pub enum AceUnlockType { + /// Must unlock, transaction will fail if unlock conditions aren't met + Force, + /// Optional unlock, transaction can proceed with or without unlock + Optional, +} diff --git a/crates/rbuilder-primitives/src/lib.rs b/crates/rbuilder-primitives/src/lib.rs index 929f4c27e..edbfc2fe7 100644 --- a/crates/rbuilder-primitives/src/lib.rs +++ b/crates/rbuilder-primitives/src/lib.rs @@ -1,5 +1,6 @@ //! Order types used as elements for block building. +pub mod ace; pub mod built_block; pub mod evm_inspector; pub mod fmt; @@ -34,7 +35,8 @@ use reth_transaction_pool::{ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::{ - cmp::Ordering, collections::HashMap, fmt::Display, hash::Hash, mem, str::FromStr, sync::Arc, + cmp::Ordering, collections::HashMap, fmt::Display, hash::Hash, mem, ops::Deref, str::FromStr, + sync::Arc, }; pub use test_data_generator::TestDataGenerator; use thiserror::Error; @@ -1055,12 +1057,31 @@ impl InMemorySize for MempoolTx { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum AceTx { + /// A protocol level tx, + Protocol(MempoolTx), + /// A unlocking tx. + Unlocking(MempoolTx), +} + +impl Deref for AceTx { + type Target = MempoolTx; + fn deref(&self) -> &Self::Target { + match self { + Self::Protocol(m) => m, + Self::Unlocking(m) => m, + } + } +} + /// Main type used for block building, we build blocks as sequences of Orders #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Order { Bundle(Bundle), Tx(MempoolTx), ShareBundle(ShareBundle), + AceTx(AceTx), } /// Uniquely identifies a replaceable sbundle @@ -1107,6 +1128,7 @@ impl Order { Order::Bundle(bundle) => bundle.can_execute_with_block_base_fee(block_base_fee), Order::Tx(tx) => tx.tx_with_blobs.tx.max_fee_per_gas() >= block_base_fee, Order::ShareBundle(bundle) => bundle.can_execute_with_block_base_fee(block_base_fee), + Order::AceTx(tx) => tx.tx_with_blobs.tx.max_fee_per_gas() >= block_base_fee, } } @@ -1116,8 +1138,7 @@ impl Order { /// Non virtual orders should return self pub fn original_orders(&self) -> Vec<&Order> { match self { - Order::Bundle(_) => vec![self], - Order::Tx(_) => vec![self], + Order::Bundle(_) | Order::Tx(_) | Order::AceTx(_) => vec![self], Order::ShareBundle(sb) => { let res = sb.original_orders(); if res.is_empty() { @@ -1139,6 +1160,11 @@ impl Order { address: tx.tx_with_blobs.tx.signer(), optional: false, }], + Order::AceTx(tx) => vec![Nonce { + nonce: tx.tx_with_blobs.tx.nonce(), + address: tx.tx_with_blobs.tx.signer(), + optional: false, + }], Order::ShareBundle(bundle) => bundle.nonces(), } } @@ -1147,6 +1173,7 @@ impl Order { match self { Order::Bundle(bundle) => OrderId::Bundle(bundle.uuid), Order::Tx(tx) => OrderId::Tx(tx.tx_with_blobs.hash()), + Order::AceTx(tx) => OrderId::Tx(tx.tx_with_blobs.hash()), Order::ShareBundle(bundle) => OrderId::ShareBundle(bundle.hash), } } @@ -1160,6 +1187,7 @@ impl Order { match self { Order::Bundle(bundle) => bundle.list_txs(), Order::Tx(tx) => vec![(&tx.tx_with_blobs, true)], + Order::AceTx(tx) => vec![(&tx.tx_with_blobs, true)], Order::ShareBundle(bundle) => bundle.list_txs(), } } @@ -1170,6 +1198,7 @@ impl Order { match self { Order::Bundle(bundle) => bundle.list_txs_revert(), Order::Tx(tx) => vec![(&tx.tx_with_blobs, TxRevertBehavior::AllowedIncluded)], + Order::AceTx(tx) => vec![(&tx.tx_with_blobs, TxRevertBehavior::AllowedIncluded)], Order::ShareBundle(bundle) => bundle.list_txs_revert(), } } @@ -1178,7 +1207,7 @@ impl Order { pub fn list_txs_len(&self) -> usize { match self { Order::Bundle(bundle) => bundle.list_txs_len(), - Order::Tx(_) => 1, + Order::AceTx(_) | Order::Tx(_) => 1, Order::ShareBundle(bundle) => bundle.list_txs_len(), } } @@ -1196,7 +1225,7 @@ impl Order { r.sequence_number, ) }), - Order::Tx(_) => None, + Order::AceTx(_) | Order::Tx(_) => None, Order::ShareBundle(sbundle) => sbundle.replacement_data.as_ref().map(|r| { ( OrderReplacementKey::ShareBundle(r.clone().key), @@ -1213,7 +1242,7 @@ impl Order { pub fn target_block(&self) -> Option { match self { Order::Bundle(bundle) => bundle.block, - Order::Tx(_) => None, + Order::AceTx(_) | Order::Tx(_) => None, Order::ShareBundle(bundle) => Some(bundle.block), } } @@ -1223,7 +1252,7 @@ impl Order { match self { Order::Bundle(bundle) => bundle.signer, Order::ShareBundle(bundle) => bundle.signer, - Order::Tx(_) => None, + Order::AceTx(_) | Order::Tx(_) => None, } } @@ -1231,6 +1260,7 @@ impl Order { match self { Order::Bundle(bundle) => &bundle.metadata, Order::Tx(tx) => &tx.tx_with_blobs.metadata, + Order::AceTx(tx) => &tx.tx_with_blobs.metadata, Order::ShareBundle(bundle) => &bundle.metadata, } } diff --git a/crates/rbuilder/src/building/ace_bundler.rs b/crates/rbuilder/src/building/ace_bundler.rs new file mode 100644 index 000000000..ffce2cd68 --- /dev/null +++ b/crates/rbuilder/src/building/ace_bundler.rs @@ -0,0 +1,388 @@ +use alloy_primitives::U256; +use rbuilder_primitives::{ + ace::{AceExchange, AceInteraction, AceUnlockType}, + Order, OrderId, SimulatedOrder, +}; +use std::sync::Arc; +use tracing::{debug, trace}; + +/// The ACE bundler sits between the sim-tree and the builder itself. We put the bundler here as it +/// gives maximum flexibility for ACE protocols for defining ordering and handling cases were +/// certain tx's depend on other tx's. With this, a simple ace detection can be ran on incoming +/// orders. Before the orders get sent to the builders, Ace orders get intercepted here and then can +/// follow protocol specific ordering by leveraging the current bundling design. For example, if a +/// ace protocol wants to have a protocol transaction first and then sort everything greedly for there +/// protocol, there bundler can collect all the orders that interact with the protocol and then +/// generate a bundle with the protocol tx first with all other orders following and set to +/// droppable with a order that they want. +#[derive(Debug)] +pub struct AceBundler { + /// ACE bundles organized by exchange + exchanges: std::collections::HashMap, +} + +/// Data for a specific ACE exchange including all transaction types and logic +#[derive(Debug, Clone)] +pub struct AceExchangeData { + /// Force ACE protocol tx - always included + pub force_ace_tx: Option, + /// Optional ACE protocol tx - conditionally included + pub optional_ace_tx: Option, + /// Mempool txs that unlock ACE state + pub unlocking_mempool_txs: Vec, + /// Mempool txs that require ACE unlock + pub non_unlocking_mempool_txs: Vec, +} + +#[derive(Debug, Clone)] +pub struct AceOrderEntry { + pub order: Order, + pub simulated: Arc, + /// Profit after bundle simulation + pub bundle_profit: U256, +} + +impl AceExchangeData { + /// Add an ACE protocol transaction + pub fn add_ace_protocol_tx( + &mut self, + order: Order, + simulated: Arc, + unlock_type: AceUnlockType, + ) { + let entry = AceOrderEntry { + order, + bundle_profit: simulated.sim_value.full_profit_info().coinbase_profit(), + simulated, + }; + + match unlock_type { + AceUnlockType::Force => { + self.force_ace_tx = Some(entry); + trace!("Added forced ACE protocol unlock tx"); + } + AceUnlockType::Optional => { + self.optional_ace_tx = Some(entry); + trace!("Added optional ACE protocol unlock tx"); + } + } + } + + /// Add a mempool ACE transaction + pub fn add_mempool_tx( + &mut self, + order: Order, + simulated: Arc, + is_unlocking: bool, + ) { + let entry = AceOrderEntry { + order, + bundle_profit: simulated.sim_value.full_profit_info().coinbase_profit(), + simulated, + }; + + if is_unlocking { + self.unlocking_mempool_txs.push(entry); + trace!("Added unlocking mempool ACE tx"); + } else { + self.non_unlocking_mempool_txs.push(entry); + trace!("Added non-unlocking mempool ACE tx"); + } + } + + /// Check if we should include optional ACE protocol tx + /// Optional is included if we have non-unlocking txs and no other unlock source + fn should_include_optional(&self) -> bool { + !self.non_unlocking_mempool_txs.is_empty() + && self.force_ace_tx.is_none() + && self.unlocking_mempool_txs.is_empty() + } + + /// Check if we have an available unlock (either force ACE or mempool unlocking) + fn has_unlock(&self) -> bool { + self.force_ace_tx.is_some() || !self.unlocking_mempool_txs.is_empty() + } + + /// Get the ACE bundle to place at top of block + /// Returns all unlock txs (force ACE, optional ACE, mempool unlocks) followed by non-unlocking txs + pub fn get_ace_bundle(&self) -> Vec { + let mut orders = Vec::new(); + + // Priority 1: Force ACE unlock (always included) + if let Some(ref force_tx) = self.force_ace_tx { + orders.push(force_tx.order.clone()); + } + + // Priority 2: Optional ACE unlock (if needed and no force ACE) + if let Some(ref optional_tx) = self.optional_ace_tx { + if self.should_include_optional() { + orders.push(optional_tx.order.clone()); + } + } + + // Priority 3: Mempool unlocking txs + for entry in &self.unlocking_mempool_txs { + orders.push(entry.order.clone()); + } + + // Priority 4: Non-unlocking mempool txs (only if we have an unlock) + if self.has_unlock() || self.should_include_optional() { + for entry in &self.non_unlocking_mempool_txs { + orders.push(entry.order.clone()); + } + } + + orders + } + + /// Update profits and sort by profitability + pub fn update_profits(&mut self, order_id: &OrderId, profit: U256) -> bool { + if let Some(ref mut entry) = self.force_ace_tx { + if entry.order.id() == *order_id { + entry.bundle_profit = profit; + return true; + } + } + + if let Some(ref mut entry) = self.optional_ace_tx { + if entry.order.id() == *order_id { + entry.bundle_profit = profit; + return true; + } + } + + for entry in &mut self.unlocking_mempool_txs { + if entry.order.id() == *order_id { + entry.bundle_profit = profit; + return true; + } + } + + for entry in &mut self.non_unlocking_mempool_txs { + if entry.order.id() == *order_id { + entry.bundle_profit = profit; + return true; + } + } + + false + } + + /// Sort mempool transactions by profitability + pub fn sort_by_profit(&mut self) { + self.unlocking_mempool_txs + .sort_by(|a, b| b.bundle_profit.cmp(&a.bundle_profit)); + self.non_unlocking_mempool_txs + .sort_by(|a, b| b.bundle_profit.cmp(&a.bundle_profit)); + } + + /// Remove orders that builder wants to kick out + pub fn kick_out_orders(&mut self, order_ids: &[OrderId]) { + if let Some(ref force_tx) = self.force_ace_tx { + if order_ids.contains(&force_tx.order.id()) { + debug!("Attempted to kick out force ACE tx - ignoring"); + } + } + + self.unlocking_mempool_txs + .retain(|entry| !order_ids.contains(&entry.order.id())); + self.non_unlocking_mempool_txs + .retain(|entry| !order_ids.contains(&entry.order.id())); + } + + /// Get total profit + pub fn total_profit(&self) -> U256 { + let mut total = U256::ZERO; + + if let Some(ref entry) = self.force_ace_tx { + total = total.saturating_add(entry.bundle_profit); + } + if let Some(ref entry) = self.optional_ace_tx { + total = total.saturating_add(entry.bundle_profit); + } + + for entry in &self.unlocking_mempool_txs { + total = total.saturating_add(entry.bundle_profit); + } + for entry in &self.non_unlocking_mempool_txs { + total = total.saturating_add(entry.bundle_profit); + } + + total + } + + /// Check if empty + pub fn is_empty(&self) -> bool { + self.force_ace_tx.is_none() + && self.optional_ace_tx.is_none() + && self.unlocking_mempool_txs.is_empty() + && self.non_unlocking_mempool_txs.is_empty() + } + + /// Get count of orders + pub fn len(&self) -> usize { + let mut count = 0; + if self.force_ace_tx.is_some() { + count += 1; + } + if self.optional_ace_tx.is_some() { + count += 1; + } + count + self.unlocking_mempool_txs.len() + self.non_unlocking_mempool_txs.len() + } +} + +impl AceBundler { + pub fn new() -> Self { + Self { + exchanges: std::collections::HashMap::new(), + } + } + + /// Add an ACE protocol transaction (Order::Ace) + pub fn add_ace_protocol_tx( + &mut self, + order: Order, + simulated: Arc, + unlock_type: AceUnlockType, + exchange: AceExchange, + ) { + let data = self.exchanges.entry(exchange).or_default(); + data.add_ace_protocol_tx(order, simulated, unlock_type); + } + + /// Add a mempool ACE transaction or bundle containing ACE interactions + pub fn add_mempool_ace_tx( + &mut self, + order: Order, + simulated: Arc, + interaction: AceInteraction, + ) { + if matches!(order, Order::Bundle(_) | Order::ShareBundle(_)) { + trace!( + order_id = ?order.id(), + "Adding ACE bundle/share bundle - will be treated as atomic unit" + ); + } + + match interaction { + AceInteraction::Unlocking { exchange } => { + let data = self.exchanges.entry(exchange).or_default(); + data.add_mempool_tx(order, simulated, true); + } + AceInteraction::NonUnlocking { exchange } => { + let data = self.exchanges.entry(exchange).or_default(); + data.add_mempool_tx(order, simulated, false); + } + } + } + + /// Handle replacement of a mempool transaction + pub fn replace_mempool_tx( + &mut self, + old_order_id: &OrderId, + new_order: Order, + new_simulated: Arc, + interaction: AceInteraction, + ) -> bool { + let mut found = false; + for data in self.exchanges.values_mut() { + if let Some(pos) = data + .unlocking_mempool_txs + .iter() + .position(|e| e.order.id() == *old_order_id) + { + data.unlocking_mempool_txs.remove(pos); + found = true; + break; + } + if let Some(pos) = data + .non_unlocking_mempool_txs + .iter() + .position(|e| e.order.id() == *old_order_id) + { + data.non_unlocking_mempool_txs.remove(pos); + found = true; + break; + } + } + + if found { + self.add_mempool_ace_tx(new_order, new_simulated, interaction); + trace!( + "Replaced ACE mempool tx {:?} with new version", + old_order_id + ); + } + + found + } + + /// Get the ACE bundle for a specific exchange to place at top of block + pub fn get_ace_bundle(&self, exchange: &AceExchange) -> Vec { + self.exchanges + .get(exchange) + .map(|data| data.get_ace_bundle()) + .unwrap_or_default() + } + + /// Update profits after bundle simulation + pub fn update_after_simulation(&mut self, simulation_results: Vec<(OrderId, U256)>) { + for (order_id, profit) in simulation_results { + for data in self.exchanges.values_mut() { + if data.update_profits(&order_id, profit) { + break; + } + } + } + + // Sort all exchanges by profit + for data in self.exchanges.values_mut() { + data.sort_by_profit(); + } + } + + /// Remove specific ACE orders if builder has better alternatives + pub fn kick_out_orders(&mut self, exchange: &AceExchange, order_ids: &[OrderId]) { + if let Some(data) = self.exchanges.get_mut(exchange) { + data.kick_out_orders(order_ids); + } + } + + /// Get all configured exchanges + pub fn get_exchanges(&self) -> Vec { + self.exchanges.keys().cloned().collect() + } + + /// Clear all orders + pub fn clear(&mut self) { + self.exchanges.clear(); + } + + pub fn is_empty(&self) -> bool { + self.exchanges.is_empty() || self.exchanges.values().all(|d| d.is_empty()) + } + + pub fn len(&self) -> usize { + self.exchanges.values().map(|d| d.len()).sum() + } + + /// Get total profit for a specific exchange + pub fn total_profit(&self, exchange: &AceExchange) -> U256 { + self.exchanges + .get(exchange) + .map(|d| d.total_profit()) + .unwrap_or(U256::ZERO) + } +} + +impl Default for AceExchangeData { + fn default() -> Self { + Self { + force_ace_tx: None, + optional_ace_tx: None, + unlocking_mempool_txs: Vec::new(), + non_unlocking_mempool_txs: Vec::new(), + } + } +} diff --git a/crates/rbuilder/src/building/mod.rs b/crates/rbuilder/src/building/mod.rs index 6cfec1915..d4f9f72fc 100644 --- a/crates/rbuilder/src/building/mod.rs +++ b/crates/rbuilder/src/building/mod.rs @@ -75,6 +75,7 @@ use time::OffsetDateTime; use tracing::{error, trace}; use tx_sim_cache::TxExecutionCache; +pub mod ace_bundler; pub mod block_orders; pub mod builders; pub mod built_block_trace; From 725cf81b3d055653dd4686d229addb80420bb25b Mon Sep 17 00:00:00 2001 From: Will Smith Date: Wed, 15 Oct 2025 12:09:21 -0400 Subject: [PATCH 02/51] feat: get baseline building --- crates/rbuilder-primitives/src/fmt.rs | 1 + crates/rbuilder-primitives/src/lib.rs | 100 +++++++++++---- .../src/order_statistics.rs | 5 + crates/rbuilder-primitives/src/serialize.rs | 83 +++++++++++++ .../rbuilder/src/backtest/redistribute/mod.rs | 5 +- .../find_landed_orders.rs | 8 ++ crates/rbuilder/src/backtest/store.rs | 1 + .../src/building/built_block_trace.rs | 11 +- crates/rbuilder/src/building/order_commit.rs | 116 ++++++++++++++++++ .../src/live_builder/order_input/orderpool.rs | 3 +- .../live_builder/order_input/rpc_server.rs | 44 +++++++ .../live_builder/simulation/simulation_job.rs | 9 +- crates/rbuilder/src/mev_boost/mod.rs | 1 + .../src/telemetry/metrics/tracing_metrics.rs | 1 + 14 files changed, 356 insertions(+), 32 deletions(-) diff --git a/crates/rbuilder-primitives/src/fmt.rs b/crates/rbuilder-primitives/src/fmt.rs index 0cf1a6d31..5ac2a0b79 100644 --- a/crates/rbuilder-primitives/src/fmt.rs +++ b/crates/rbuilder-primitives/src/fmt.rs @@ -50,6 +50,7 @@ pub fn write_order( tx.tx_with_blobs.hash(), tx.tx_with_blobs.value() )), + Order::AceTx(ace) => buf.write_str(&format!("ace {}\n", ace.order_id())), Order::ShareBundle(sb) => { buf.write_str(&format!("ShB {:?}\n", sb.hash))?; write_share_bundle_inner(indent + 1, buf, &sb.inner_bundle) diff --git a/crates/rbuilder-primitives/src/lib.rs b/crates/rbuilder-primitives/src/lib.rs index edbfc2fe7..bcf1d3ea4 100644 --- a/crates/rbuilder-primitives/src/lib.rs +++ b/crates/rbuilder-primitives/src/lib.rs @@ -35,8 +35,7 @@ use reth_transaction_pool::{ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::{ - cmp::Ordering, collections::HashMap, fmt::Display, hash::Hash, mem, ops::Deref, str::FromStr, - sync::Arc, + cmp::Ordering, collections::HashMap, fmt::Display, hash::Hash, mem, str::FromStr, sync::Arc, }; pub use test_data_generator::TestDataGenerator; use thiserror::Error; @@ -1057,22 +1056,77 @@ impl InMemorySize for MempoolTx { } } +/// The application that is being executed. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum AceTx { - /// A protocol level tx, - Protocol(MempoolTx), - /// A unlocking tx. - Unlocking(MempoolTx), + Angstrom(AngstromTx), } -impl Deref for AceTx { - type Target = MempoolTx; - fn deref(&self) -> &Self::Target { +impl AceTx { + pub fn target_block(&self) -> Option { + match self { + Self::Angstrom(_) => None, + } + } + pub fn metadata(&self) -> &Metadata { + match self { + Self::Angstrom(ang) => &ang.meta, + } + } + + pub fn list_txs_len(&self) -> usize { match self { - Self::Protocol(m) => m, - Self::Unlocking(m) => m, + Self::Angstrom(_) => 1, } } + + pub fn nonces(&self) -> Vec { + match self { + Self::Angstrom(ang) => { + vec![Nonce { + nonce: ang.tx.nonce(), + address: ang.tx.signer(), + optional: false, + }] + } + } + } + + pub fn can_execute_with_block_base_fee(&self, base_fee: u128) -> bool { + match self { + Self::Angstrom(ang) => ang.tx.as_ref().max_fee_per_gas() >= base_fee, + } + } + + pub fn list_txs_revert( + &self, + ) -> Vec<(&TransactionSignedEcRecoveredWithBlobs, TxRevertBehavior)> { + match self { + Self::Angstrom(ang) => vec![(&ang.tx, TxRevertBehavior::NotAllowed)], + } + } + + pub fn order_id(&self) -> B256 { + match self { + Self::Angstrom(ang) => ang.tx.hash(), + } + } + + pub fn list_txs(&self) -> Vec<(&TransactionSignedEcRecoveredWithBlobs, bool)> { + match self { + Self::Angstrom(ang) => vec![(&ang.tx, false)], + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct AngstromTx { + pub tx: TransactionSignedEcRecoveredWithBlobs, + pub meta: Metadata, + pub unlock_data: Bytes, + pub max_priority_fee_per_gas: u128, + /// Whether this is a forced unlock or optional + pub unlock_type: ace::AceUnlockType, } /// Main type used for block building, we build blocks as sequences of Orders @@ -1128,7 +1182,7 @@ impl Order { Order::Bundle(bundle) => bundle.can_execute_with_block_base_fee(block_base_fee), Order::Tx(tx) => tx.tx_with_blobs.tx.max_fee_per_gas() >= block_base_fee, Order::ShareBundle(bundle) => bundle.can_execute_with_block_base_fee(block_base_fee), - Order::AceTx(tx) => tx.tx_with_blobs.tx.max_fee_per_gas() >= block_base_fee, + Order::AceTx(tx) => tx.can_execute_with_block_base_fee(block_base_fee), } } @@ -1160,11 +1214,7 @@ impl Order { address: tx.tx_with_blobs.tx.signer(), optional: false, }], - Order::AceTx(tx) => vec![Nonce { - nonce: tx.tx_with_blobs.tx.nonce(), - address: tx.tx_with_blobs.tx.signer(), - optional: false, - }], + Order::AceTx(tx) => tx.nonces(), Order::ShareBundle(bundle) => bundle.nonces(), } } @@ -1173,7 +1223,7 @@ impl Order { match self { Order::Bundle(bundle) => OrderId::Bundle(bundle.uuid), Order::Tx(tx) => OrderId::Tx(tx.tx_with_blobs.hash()), - Order::AceTx(tx) => OrderId::Tx(tx.tx_with_blobs.hash()), + Order::AceTx(ace) => OrderId::Tx(ace.order_id()), Order::ShareBundle(bundle) => OrderId::ShareBundle(bundle.hash), } } @@ -1187,7 +1237,7 @@ impl Order { match self { Order::Bundle(bundle) => bundle.list_txs(), Order::Tx(tx) => vec![(&tx.tx_with_blobs, true)], - Order::AceTx(tx) => vec![(&tx.tx_with_blobs, true)], + Order::AceTx(tx) => tx.list_txs(), Order::ShareBundle(bundle) => bundle.list_txs(), } } @@ -1198,7 +1248,7 @@ impl Order { match self { Order::Bundle(bundle) => bundle.list_txs_revert(), Order::Tx(tx) => vec![(&tx.tx_with_blobs, TxRevertBehavior::AllowedIncluded)], - Order::AceTx(tx) => vec![(&tx.tx_with_blobs, TxRevertBehavior::AllowedIncluded)], + Order::AceTx(tx) => tx.list_txs_revert(), Order::ShareBundle(bundle) => bundle.list_txs_revert(), } } @@ -1260,7 +1310,7 @@ impl Order { match self { Order::Bundle(bundle) => &bundle.metadata, Order::Tx(tx) => &tx.tx_with_blobs.metadata, - Order::AceTx(tx) => &tx.tx_with_blobs.metadata, + Order::AceTx(tx) => tx.metadata(), Order::ShareBundle(bundle) => &bundle.metadata, } } @@ -1404,12 +1454,13 @@ pub enum OrderId { Tx(B256), Bundle(Uuid), ShareBundle(B256), + Ace(B256), } impl OrderId { pub fn fixed_bytes(&self) -> B256 { match self { - Self::Tx(hash) | Self::ShareBundle(hash) => *hash, + Self::Tx(hash) | Self::ShareBundle(hash) | Self::Ace(hash) => *hash, Self::Bundle(uuid) => { let mut out = [0u8; 32]; out[0..16].copy_from_slice(uuid.as_bytes()); @@ -1440,6 +1491,9 @@ impl FromStr for OrderId { } else if let Some(hash_str) = s.strip_prefix("sbundle:") { let hash = B256::from_str(hash_str)?; Ok(Self::ShareBundle(hash)) + } else if let Some(hash_str) = s.strip_prefix("ace_tx:") { + let hash = B256::from_str(hash_str)?; + Ok(Self::Ace(hash)) } else { Err(eyre::eyre!("invalid order id")) } @@ -1453,6 +1507,7 @@ impl Display for OrderId { Self::Tx(hash) => write!(f, "tx:{hash:?}"), Self::Bundle(uuid) => write!(f, "bundle:{uuid:?}"), Self::ShareBundle(hash) => write!(f, "sbundle:{hash:?}"), + Self::Ace(hash) => write!(f, "ace_tx:{hash:?}"), } } } @@ -1467,6 +1522,7 @@ impl Ord for OrderId { fn cmp(&self, other: &Self) -> Ordering { fn rank(id: &OrderId) -> usize { match id { + OrderId::Ace(_) => 0, OrderId::Tx(_) => 1, OrderId::Bundle(_) => 2, OrderId::ShareBundle(_) => 3, diff --git a/crates/rbuilder-primitives/src/order_statistics.rs b/crates/rbuilder-primitives/src/order_statistics.rs index b091d2ddb..663e75334 100644 --- a/crates/rbuilder-primitives/src/order_statistics.rs +++ b/crates/rbuilder-primitives/src/order_statistics.rs @@ -7,6 +7,7 @@ pub struct OrderStatistics { tx_count: i32, bundle_count: i32, sbundle_count: i32, + ace_count: i32, } impl OrderStatistics { @@ -18,6 +19,7 @@ impl OrderStatistics { match order { Order::Bundle(_) => self.bundle_count += 1, Order::Tx(_) => self.tx_count += 1, + Order::AceTx(_) => self.ace_count += 1, Order::ShareBundle(_) => self.sbundle_count += 1, } } @@ -26,6 +28,7 @@ impl OrderStatistics { match order { Order::Bundle(_) => self.bundle_count -= 1, Order::Tx(_) => self.tx_count -= 1, + Order::AceTx(_) => self.ace_count -= 1, Order::ShareBundle(_) => self.sbundle_count -= 1, } } @@ -41,6 +44,7 @@ impl Add for OrderStatistics { fn add(self, other: Self) -> Self::Output { Self { tx_count: self.tx_count + other.tx_count, + ace_count: self.ace_count + other.ace_count, bundle_count: self.bundle_count + other.bundle_count, sbundle_count: self.sbundle_count + other.sbundle_count, } @@ -54,6 +58,7 @@ impl Sub for OrderStatistics { Self { tx_count: self.tx_count - other.tx_count, bundle_count: self.bundle_count - other.bundle_count, + ace_count: self.ace_count - other.ace_count, sbundle_count: self.sbundle_count - other.sbundle_count, } } diff --git a/crates/rbuilder-primitives/src/serialize.rs b/crates/rbuilder-primitives/src/serialize.rs index 474e2a5bc..796cd0e04 100644 --- a/crates/rbuilder-primitives/src/serialize.rs +++ b/crates/rbuilder-primitives/src/serialize.rs @@ -5,6 +5,7 @@ use super::{ TransactionSignedEcRecoveredWithBlobs, TxRevertBehavior, TxWithBlobsCreateError, LAST_BUNDLE_VERSION, }; +use crate::{ace::AceUnlockType, AceTx, AngstromTx, Metadata}; use alloy_consensus::constants::EIP4844_TX_TYPE_ID; use alloy_eips::eip2718::Eip2718Error; use alloy_primitives::{Address, Bytes, TxHash, B256, U64}; @@ -505,6 +506,78 @@ impl RawTx { } } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type")] +#[serde(rename_all = "camelCase")] +pub enum RawAce { + Angstrom(RawAngstromTx), +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RawAngstromTx { + pub tx: Bytes, + pub unlock_data: Bytes, + pub max_priority_fee_per_gas: u128, + pub unlock_type: AceUnlockType, +} + +impl RawAce { + pub fn from_tx(ace: AceTx) -> Self { + match ace { + AceTx::Angstrom(angstrom_tx) => { + let tx_bytes = angstrom_tx.tx.envelope_encoded_no_blobs(); + RawAce::Angstrom(RawAngstromTx { + tx: tx_bytes, + unlock_data: angstrom_tx.unlock_data, + max_priority_fee_per_gas: angstrom_tx.max_priority_fee_per_gas, + unlock_type: angstrom_tx.unlock_type, + }) + } + } + } +} + +/// Angstrom bundle submission structure +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)] +#[serde(rename_all = "camelCase")] +pub struct AngstromIntegrationSubmission { + pub tx: Bytes, + pub unlock_data: Bytes, + pub max_priority_fee_per_gas: u128, +} + +impl AngstromIntegrationSubmission { + /// Convert the submission to an AceTx order + pub fn to_ace_tx( + self, + received_at: time::OffsetDateTime, + ) -> Result { + let tx = + RawTransactionDecodable::new(self.tx, TxEncoding::WithBlobData).decode_enveloped()?; + + let unlock_type = if self.unlock_data.is_empty() { + AceUnlockType::Force + } else { + AceUnlockType::Optional + }; + + let angstrom_tx = AngstromTx { + tx, + meta: Metadata { + received_at_timestamp: received_at, + is_system: false, + refund_identity: None, + }, + unlock_data: self.unlock_data, + max_priority_fee_per_gas: self.max_priority_fee_per_gas, + unlock_type, + }; + + Ok(AceTx::Angstrom(angstrom_tx)) + } +} + /// Struct to de/serialize json Bundles from bundles APIs and from/db. /// Does not assume a particular format on txs. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -831,6 +904,7 @@ pub enum RawOrder { Bundle(RawBundle), Tx(RawTx), ShareBundle(RawShareBundle), + Ace(RawAce), } #[derive(Error, Debug)] @@ -843,6 +917,8 @@ pub enum RawOrderConvertError { FailedToDecodeShareBundle(RawShareBundleConvertError), #[error("Blobs not supported by RawOrder")] BlobsNotSupported, + #[error("{0}")] + UnsupportedOrderType(String), } impl RawOrder { @@ -863,6 +939,12 @@ impl RawOrder { .decode_new_bundle(encoding) .map_err(RawOrderConvertError::FailedToDecodeShareBundle)?, )), + RawOrder::Ace(_) => { + // ACE orders are not decoded from RawOrder - they come directly from RPC + Err(RawOrderConvertError::UnsupportedOrderType( + "ACE orders cannot be decoded from RawOrder".to_string(), + )) + } } } } @@ -872,6 +954,7 @@ impl From for RawOrder { match value { Order::Bundle(bundle) => Self::Bundle(RawBundle::encode_no_blobs(bundle)), Order::Tx(tx) => Self::Tx(RawTx::encode_no_blobs(tx)), + Order::AceTx(tx) => Self::Ace(RawAce::from_tx(tx)), Order::ShareBundle(bundle) => { Self::ShareBundle(RawShareBundle::encode_no_blobs(bundle)) } diff --git a/crates/rbuilder/src/backtest/redistribute/mod.rs b/crates/rbuilder/src/backtest/redistribute/mod.rs index d2cbd73b1..a95512169 100644 --- a/crates/rbuilder/src/backtest/redistribute/mod.rs +++ b/crates/rbuilder/src/backtest/redistribute/mod.rs @@ -67,6 +67,7 @@ pub enum InclusionChange { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ExtendedOrderId { Tx(B256), + AceTx(B256), Bundle { uuid: Uuid, hash: B256 }, ShareBundle(B256), } @@ -75,6 +76,7 @@ impl ExtendedOrderId { fn new(order_id: OrderId, bundle_hashes: &HashMap) -> Self { match order_id { OrderId::Tx(hash) => ExtendedOrderId::Tx(hash), + OrderId::Ace(hash) => ExtendedOrderId::AceTx(hash), OrderId::Bundle(uuid) => { let hash = bundle_hashes.get(&order_id).cloned().unwrap_or_default(); ExtendedOrderId::Bundle { uuid, hash } @@ -317,6 +319,7 @@ where Order::Bundle(_) => bundles += 1, Order::Tx(_) => txs += 1, Order::ShareBundle(_) => share_bundles += 1, + Order::AceTx(_) => txs += 1, } } let total = txs + bundles + share_bundles; @@ -1229,7 +1232,7 @@ fn order_redistribution_address( let (first_tx, _) = txs.first()?; Some((first_tx.signer(), true)) } - Order::Tx(_) => { + Order::AceTx(_) | Order::Tx(_) => { unreachable!("Mempool tx order can't have signer"); } } diff --git a/crates/rbuilder/src/backtest/restore_landed_orders/find_landed_orders.rs b/crates/rbuilder/src/backtest/restore_landed_orders/find_landed_orders.rs index 7ddd21a9d..7f2b55551 100644 --- a/crates/rbuilder/src/backtest/restore_landed_orders/find_landed_orders.rs +++ b/crates/rbuilder/src/backtest/restore_landed_orders/find_landed_orders.rs @@ -45,6 +45,14 @@ impl SimplifiedOrder { 0, )], ), + Order::AceTx(_) => { + let txs = order + .list_txs_revert() + .into_iter() + .map(|(tx, revert)| OrderTxData::new(tx.hash(), revert, 0)) + .collect(); + SimplifiedOrder::new(id, txs) + } Order::Bundle(bundle) => { let (refund_percent, receiver_hash) = if let Some(refund) = &bundle.refund { (refund.percent as usize, Some(refund.tx_hash)) diff --git a/crates/rbuilder/src/backtest/store.rs b/crates/rbuilder/src/backtest/store.rs index 90c4cca32..18e1f71b3 100644 --- a/crates/rbuilder/src/backtest/store.rs +++ b/crates/rbuilder/src/backtest/store.rs @@ -504,6 +504,7 @@ fn order_type(command: &RawReplaceableOrderPoolCommand) -> &'static str { RawOrder::Bundle(_) => "bundle", RawOrder::Tx(_) => "tx", RawOrder::ShareBundle(_) => "sbundle", + RawOrder::Ace(_) => "ace_tx", }, RawReplaceableOrderPoolCommand::CancelShareBundle(_) => "cancel_sbundle", RawReplaceableOrderPoolCommand::CancelBundle(_) => "cancel_bundle", diff --git a/crates/rbuilder/src/building/built_block_trace.rs b/crates/rbuilder/src/building/built_block_trace.rs index 9dbe433b1..caef8ef15 100644 --- a/crates/rbuilder/src/building/built_block_trace.rs +++ b/crates/rbuilder/src/building/built_block_trace.rs @@ -131,13 +131,14 @@ impl BuiltBlockTrace { } // txs, bundles, share bundles - pub fn used_order_count(&self) -> (usize, usize, usize) { + pub fn used_order_count(&self) -> (usize, usize, usize, usize) { self.included_orders .iter() - .fold((0, 0, 0), |acc, order| match order.order { - Order::Tx(_) => (acc.0 + 1, acc.1, acc.2), - Order::Bundle(_) => (acc.0, acc.1 + 1, acc.2), - Order::ShareBundle(_) => (acc.0, acc.1, acc.2 + 1), + .fold((0, 0, 0, 0), |acc, order| match order.order { + Order::Tx(_) => (acc.0 + 1, acc.1, acc.2, acc.3), + Order::Bundle(_) => (acc.0, acc.1 + 1, acc.2, acc.3), + Order::ShareBundle(_) => (acc.0, acc.1, acc.2 + 1, acc.3), + Order::AceTx(_) => (acc.0, acc.1, acc.2, acc.3 + 1), }) } diff --git a/crates/rbuilder/src/building/order_commit.rs b/crates/rbuilder/src/building/order_commit.rs index c45ab67a5..547294e3d 100644 --- a/crates/rbuilder/src/building/order_commit.rs +++ b/crates/rbuilder/src/building/order_commit.rs @@ -15,6 +15,8 @@ use alloy_evm::Database; use alloy_primitives::{Address, B256, I256, U256}; use alloy_rlp::Encodable; use itertools::Itertools; +use rbuilder_primitives::ace::AceExchange; +use rbuilder_primitives::AceTx; use rbuilder_primitives::{ evm_inspector::{RBuilderEVMInspector, UsedStateTrace}, BlockSpace, Bundle, Order, OrderId, RefundConfig, ShareBundle, ShareBundleBody, @@ -280,6 +282,19 @@ impl BundleOk { } } +/// Result of successfully executing an ACE transaction +#[derive(Debug, Clone)] +pub struct AceOk { + pub space_used: BlockSpace, + pub cumulative_space_used: BlockSpace, + pub tx_info: TransactionExecutionInfo, + pub nonces_updated: Vec<(Address, u64)>, + /// Whether the ACE transaction reverted (but is still included) + pub reverted: bool, + /// The ACE exchange this transaction interacted with + pub exchange: AceExchange, +} + #[derive(Error, Debug, PartialEq, Eq)] pub enum BundleErr { #[error("Invalid transaction, hash: {0:?}, err: {1}")] @@ -546,6 +561,25 @@ impl< res } + pub fn commit_ace( + &mut self, + tx: &AceTx, + space_state: BlockBuildingSpaceState, + ) -> Result, CriticalCommitOrderError> { + let current_block = self.ctx.block(); + // None is good for any block + if let Some(block) = tx.target_block() { + if block != current_block { + return Ok(Err(BundleErr::TargetBlockIncorrect { + block: current_block, + target_block: block, + target_max_block: block, + })); + } + } + self.execute_with_rollback(|state| state.commit_ace_no_rollback(tx, space_state)) + } + /// Checks if the tx can fit in the block by checking: /// - Gas left /// - Blob gas left @@ -840,6 +874,48 @@ impl< Ok(Ok(())) } + fn commit_ace_no_rollback( + &mut self, + ace_tx: &AceTx, + space_state: BlockBuildingSpaceState, + ) -> Result, CriticalCommitOrderError> { + match ace_tx { + AceTx::Angstrom(angstrom_tx) => { + let tx_hash = angstrom_tx.tx.hash(); + + // Use the constant Angstrom exchange address + let exchange = AceExchange::angstrom(); + + // Commit the ACE transaction - no rollback for ACE + let result = self.commit_tx(&angstrom_tx.tx, space_state)?; + + match result { + Ok(res) => { + // Check if the transaction reverted + if !res.tx_info.receipt.success { + // Reject reverted ACE transactions + return Ok(Err(BundleErr::TransactionReverted(tx_hash))); + } + + Ok(Ok(AceOk { + space_used: res.space_used(), + cumulative_space_used: res.cumulative_space_used, + tx_info: res.tx_info, + nonces_updated: vec![res.nonce_updated], + reverted: false, + exchange, + })) + } + Err(err) => { + // ACE transactions must not fail at the EVM level + // These are critical errors that prevent the bundle + Ok(Err(BundleErr::InvalidTransaction(tx_hash, err))) + } + } + } + } + } + fn commit_bundle_no_rollback( &mut self, bundle: &Bundle, @@ -1264,6 +1340,46 @@ impl< let res = self.commit_share_bundle(bundle, space_state, allow_tx_skip)?; self.bundle_to_order_result(res, coinbase_balance_before) } + Order::AceTx(ace) => { + let coinbase_balance_before = self.coinbase_balance()?; + let res = self.commit_ace(ace, space_state)?; + self.ace_to_order_result(res, coinbase_balance_before) + } + } + } + + fn ace_to_order_result( + &mut self, + ace_result: Result, + coinbase_balance_before: U256, + ) -> Result, CriticalCommitOrderError> { + match ace_result { + Ok(ok) => { + let coinbase_balance_after = self.coinbase_balance()?; + let coinbase_profit = if coinbase_balance_after >= coinbase_balance_before { + coinbase_balance_after - coinbase_balance_before + } else { + return Ok(Err(OrderErr::NegativeProfit( + coinbase_balance_before - coinbase_balance_after, + ))); + }; + + // Get the tx hash before moving tx_info + let tx_hash = ok.tx_info.tx.hash(); + + Ok(Ok(OrderOk { + coinbase_profit, + space_used: ok.space_used, + cumulative_space_used: ok.cumulative_space_used, + tx_infos: vec![ok.tx_info], + nonces_updated: ok.nonces_updated, + paid_kickbacks: Vec::new(), + delayed_kickback: None, + used_state_trace: self.get_used_state_trace(), + original_order_ids: vec![OrderId::Ace(tx_hash.into())], + })) + } + Err(err) => Ok(Err(err.into())), } } diff --git a/crates/rbuilder/src/live_builder/order_input/orderpool.rs b/crates/rbuilder/src/live_builder/order_input/orderpool.rs index 17d540768..ab6580c8c 100644 --- a/crates/rbuilder/src/live_builder/order_input/orderpool.rs +++ b/crates/rbuilder/src/live_builder/order_input/orderpool.rs @@ -112,7 +112,7 @@ impl OrderPool { trace!(?order_id, "Adding order"); let (order, target_block) = match &order { - Order::Tx(..) => { + Order::Tx(..) | Order::AceTx(_) => { self.mempool_txs.push((order.clone(), Instant::now())); self.mempool_txs_size += Self::measure_tx(order); (order, None) @@ -300,6 +300,7 @@ impl OrderPool { pub fn measure_tx(order: &Order) -> usize { match order { Order::Tx(tx) => tx.size(), + Order::AceTx(_) => 0, Order::Bundle(_) => { error!("measure_tx called on a bundle"); 0 diff --git a/crates/rbuilder/src/live_builder/order_input/rpc_server.rs b/crates/rbuilder/src/live_builder/order_input/rpc_server.rs index b58e0aeaa..2da3ddb25 100644 --- a/crates/rbuilder/src/live_builder/order_input/rpc_server.rs +++ b/crates/rbuilder/src/live_builder/order_input/rpc_server.rs @@ -9,6 +9,7 @@ use jsonrpsee::{ types::{ErrorObject, Params}, IntoResponse, RpcModule, }; +use rbuilder_primitives::serialize::AngstromIntegrationSubmission; use rbuilder_primitives::{ serialize::{ RawBundle, RawBundleDecodeResult, RawShareBundle, RawShareBundleDecodeResult, RawTx, @@ -36,6 +37,7 @@ const ETH_SEND_BUNDLE: &str = "eth_sendBundle"; const MEV_SEND_BUNDLE: &str = "mev_sendBundle"; const ETH_CANCEL_BUNDLE: &str = "eth_cancelBundle"; const ETH_SEND_RAW_TRANSACTION: &str = "eth_sendRawTransaction"; +const ANG_BUNDLE: &str = "angstrom_submitBundle"; /// Adds metrics to the callback and registers via module.register_async_method. pub fn register_metered_async_method<'a, R, Fun, Fut>( @@ -144,6 +146,10 @@ pub async fn start_server_accepting_bundles( Ok(hash) } })?; + let results_clone = results.clone(); + register_metered_async_method(&mut module, ANG_BUNDLE, move |params, _| { + handle_angstrom_bundle(results_clone.clone(), timeout, params) + })?; module.merge(extra_rpc)?; let handle = server.start(module); @@ -271,6 +277,44 @@ async fn handle_mev_send_bundle( }; } +/// Handles angstrom_submitBundle RPC call +async fn handle_angstrom_bundle( + results: mpsc::Sender, + timeout: Duration, + params: jsonrpsee::types::Params<'static>, +) { + let received_at = OffsetDateTime::now_utc(); + let start = Instant::now(); + + let submission: AngstromIntegrationSubmission = match params.one() { + Ok(submission) => submission, + Err(err) => { + warn!(?err, "Failed to parse Angstrom bundle"); + inc_order_input_rpc_errors(ANG_BUNDLE); + return; + } + }; + + let ace_tx = match submission.to_ace_tx(received_at) { + Ok(ace_tx) => ace_tx, + Err(err) => { + warn!(?err, "Failed to decode Angstrom bundle"); + inc_order_input_rpc_errors(ANG_BUNDLE); + return; + } + }; + + let order = Order::AceTx(ace_tx); + let parse_duration = start.elapsed(); + trace!( + order = ?order.id(), + parse_duration_mus = parse_duration.as_micros(), + "Received Angstrom ACE bundle from API" + ); + + send_order(order, &results, timeout, received_at).await; +} + async fn send_order( order: Order, channel: &mpsc::Sender, diff --git a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs index b1bff3c57..1daa4130d 100644 --- a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs +++ b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs @@ -310,17 +310,19 @@ struct OrderCounter { mempool_txs: usize, bundles: usize, share_bundles: usize, + ace_tx: usize, } impl fmt::Debug for OrderCounter { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, - "OrderCounter {{ total: {}, mempool_txs: {}, bundles {}, share_bundles {} }}", + "OrderCounter {{ total: {}, mempool_txs: {}, bundles {}, share_bundles {}, ace_txs {} }}", self.total(), self.mempool_txs, self.bundles, - self.share_bundles + self.share_bundles, + self.ace_tx ) } } @@ -331,9 +333,10 @@ impl OrderCounter { Order::Tx(_) => self.mempool_txs += 1, Order::Bundle(_) => self.bundles += 1, Order::ShareBundle(_) => self.share_bundles += 1, + Order::AceTx(_) => self.ace_tx += 1, } } fn total(&self) -> usize { - self.mempool_txs + self.bundles + self.share_bundles + self.mempool_txs + self.bundles + self.share_bundles + self.ace_tx } } diff --git a/crates/rbuilder/src/mev_boost/mod.rs b/crates/rbuilder/src/mev_boost/mod.rs index a7cd5d867..4b5b9670e 100644 --- a/crates/rbuilder/src/mev_boost/mod.rs +++ b/crates/rbuilder/src/mev_boost/mod.rs @@ -758,6 +758,7 @@ impl RelayClient { rbuilder_primitives::OrderId::Tx(_fixed_bytes) => None, rbuilder_primitives::OrderId::Bundle(uuid) => Some(uuid), rbuilder_primitives::OrderId::ShareBundle(_fixed_bytes) => None, + rbuilder_primitives::OrderId::Ace(_fixed_bytes) => None, }) .collect(); let total_bundles = bundle_ids.len(); diff --git a/crates/rbuilder/src/telemetry/metrics/tracing_metrics.rs b/crates/rbuilder/src/telemetry/metrics/tracing_metrics.rs index 0f14888fe..53bec6952 100644 --- a/crates/rbuilder/src/telemetry/metrics/tracing_metrics.rs +++ b/crates/rbuilder/src/telemetry/metrics/tracing_metrics.rs @@ -100,6 +100,7 @@ pub fn mark_command_received(command: &ReplaceableOrderPoolCommand, received_at: Order::Bundle(_) => "bundle", Order::Tx(_) => "tx", Order::ShareBundle(_) => "sbundle", + Order::AceTx(_) => "ace_tx", } } ReplaceableOrderPoolCommand::CancelShareBundle(_) From a0d45aa0a8579ee6bea17e6aaba9001715a1ba3c Mon Sep 17 00:00:00 2001 From: Will Smith Date: Wed, 15 Oct 2025 14:23:57 -0400 Subject: [PATCH 03/51] feat: get ace top of block commit added --- Cargo.lock | 1 + crates/rbuilder-primitives/src/lib.rs | 3 +- crates/rbuilder/Cargo.toml | 1 + .../rbuilder/src/bin/run-bundle-on-prefix.rs | 2 + crates/rbuilder/src/building/ace_bundler.rs | 4 +- .../block_orders/share_bundle_merger.rs | 1 + .../block_orders/test_data_generator.rs | 1 + .../src/building/builders/ordering_builder.rs | 26 ++++++- .../block_building_result_assembler.rs | 72 ++++++++++++++++++- crates/rbuilder/src/building/sim.rs | 62 +++++++++++++++- .../src/live_builder/order_input/orderpool.rs | 11 ++- .../live_builder/simulation/simulation_job.rs | 7 +- 12 files changed, 178 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 09838dc67..2bfc2d8e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9485,6 +9485,7 @@ dependencies = [ "sha2 0.10.9", "shellexpand", "sqlx", + "strum 0.27.2", "sysperf", "tempfile", "test_utils", diff --git a/crates/rbuilder-primitives/src/lib.rs b/crates/rbuilder-primitives/src/lib.rs index bcf1d3ea4..3f19ed386 100644 --- a/crates/rbuilder-primitives/src/lib.rs +++ b/crates/rbuilder-primitives/src/lib.rs @@ -41,7 +41,7 @@ pub use test_data_generator::TestDataGenerator; use thiserror::Error; use uuid::Uuid; -use crate::serialize::TxEncoding; +use crate::{ace::AceInteraction, serialize::TxEncoding}; /// Extra metadata for an order. #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -1435,6 +1435,7 @@ pub struct SimulatedOrder { pub sim_value: SimValue, /// Info about read/write slots during the simulation to help figure out what the Order is doing. pub used_state_trace: Option, + pub ace_interaction: Option, } impl SimulatedOrder { diff --git a/crates/rbuilder/Cargo.toml b/crates/rbuilder/Cargo.toml index 6f93cff91..1dd768919 100644 --- a/crates/rbuilder/Cargo.toml +++ b/crates/rbuilder/Cargo.toml @@ -132,6 +132,7 @@ schnellru = "0.2.4" # IPC state provider deps reipc = { git = "https://github.com/nethermindeth/reipc.git", rev = "3837f3539201f948dd1c2c96a85a60d589feb4c6" } quick_cache = "0.6.11" +strum = "0.27.2" [target.'cfg(not(target_env = "msvc"))'.dependencies] tikv-jemallocator = "0.6" diff --git a/crates/rbuilder/src/bin/run-bundle-on-prefix.rs b/crates/rbuilder/src/bin/run-bundle-on-prefix.rs index 01eb99b31..2679647fe 100644 --- a/crates/rbuilder/src/bin/run-bundle-on-prefix.rs +++ b/crates/rbuilder/src/bin/run-bundle-on-prefix.rs @@ -220,6 +220,7 @@ async fn main() -> eyre::Result<()> { order, sim_value: Default::default(), used_state_trace: Default::default(), + ace_interaction: None, }; let res = builder.commit_order(&mut block_info.local_ctx, &sim_order, &|_| Ok(()))?; println!("{:?} {:?}", tx.hash(), res.is_ok()); @@ -315,6 +316,7 @@ fn execute_orders_on_tob( order: order_ts.order.clone(), sim_value: Default::default(), used_state_trace: Default::default(), + ace_interaction: None, }; let res = builder.commit_order(&mut block_info.local_ctx, &sim_order, &|_| Ok(()))?; let profit = res diff --git a/crates/rbuilder/src/building/ace_bundler.rs b/crates/rbuilder/src/building/ace_bundler.rs index ffce2cd68..f8a89ecb0 100644 --- a/crates/rbuilder/src/building/ace_bundler.rs +++ b/crates/rbuilder/src/building/ace_bundler.rs @@ -18,7 +18,7 @@ use tracing::{debug, trace}; #[derive(Debug)] pub struct AceBundler { /// ACE bundles organized by exchange - exchanges: std::collections::HashMap, + exchanges: ahash::HashMap, } /// Data for a specific ACE exchange including all transaction types and logic @@ -235,7 +235,7 @@ impl AceExchangeData { impl AceBundler { pub fn new() -> Self { Self { - exchanges: std::collections::HashMap::new(), + exchanges: ahash::HashMap::default(), } } diff --git a/crates/rbuilder/src/building/block_orders/share_bundle_merger.rs b/crates/rbuilder/src/building/block_orders/share_bundle_merger.rs index 2c37e5e9c..59e2c484b 100644 --- a/crates/rbuilder/src/building/block_orders/share_bundle_merger.rs +++ b/crates/rbuilder/src/building/block_orders/share_bundle_merger.rs @@ -148,6 +148,7 @@ impl MultiBackrunManager { order: Order::ShareBundle(sbundle), sim_value: highest_payback_order.sim_order.sim_value.clone(), used_state_trace: highest_payback_order.sim_order.used_state_trace.clone(), + ace_interaction: highest_payback_order.sim_order.ace_interaction.clone(), })) } diff --git a/crates/rbuilder/src/building/block_orders/test_data_generator.rs b/crates/rbuilder/src/building/block_orders/test_data_generator.rs index c792a439d..f2a8fd4a7 100644 --- a/crates/rbuilder/src/building/block_orders/test_data_generator.rs +++ b/crates/rbuilder/src/building/block_orders/test_data_generator.rs @@ -31,6 +31,7 @@ impl TestDataGenerator { order, sim_value, used_state_trace: None, + ace_interaction: None, }) } } diff --git a/crates/rbuilder/src/building/builders/ordering_builder.rs b/crates/rbuilder/src/building/builders/ordering_builder.rs index f6f9f9770..1341b7daf 100644 --- a/crates/rbuilder/src/building/builders/ordering_builder.rs +++ b/crates/rbuilder/src/building/builders/ordering_builder.rs @@ -26,7 +26,7 @@ use crate::{ }; use ahash::{HashMap, HashSet}; use derivative::Derivative; -use rbuilder_primitives::{AccountNonce, OrderId, SimValue, SimulatedOrder}; +use rbuilder_primitives::{AccountNonce, Order, OrderId, SimValue, SimulatedOrder}; use reth_provider::StateProvider; use serde::Deserialize; use std::{ @@ -281,6 +281,18 @@ impl OrderingBuilderContext { self.failed_orders.clear(); self.order_attempts.clear(); + // Extract ACE protocol transactions (Order::AceTx) from block_orders + // These will be pre-committed at the top of the block + let all_orders = block_orders.get_all_orders(); + let mut ace_txs = Vec::new(); + for order in all_orders { + if matches!(order.order, Order::AceTx(_)) { + ace_txs.push(order.clone()); + // Remove from block_orders so they don't get processed in fill_orders + block_orders.remove_order(order.id()); + } + } + let mut block_building_helper = BlockBuildingHelperFromProvider::new_with_execution_tracer( built_block_id, self.state.clone(), @@ -293,6 +305,18 @@ impl OrderingBuilderContext { partial_block_execution_tracer, self.max_order_execution_duration_warning, )?; + + // Pre-commit ACE protocol transactions at the top of the block + for ace_tx in &ace_txs { + trace!(order_id = ?ace_tx.id(), "Pre-committing ACE protocol tx"); + if let Err(err) = block_building_helper.commit_order( + &mut self.local_ctx, + ace_tx, + &|_| Ok(()), // ACE protocol txs bypass profit validation + ) { + trace!(order_id = ?ace_tx.id(), ?err, "Failed to pre-commit ACE protocol tx"); + } + } self.fill_orders( &mut block_building_helper, &mut block_orders, diff --git a/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs b/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs index 78c5e2f78..301d9738f 100644 --- a/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs +++ b/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs @@ -27,7 +27,7 @@ use crate::{ telemetry::mark_builder_considers_order, utils::elapsed_ms, }; -use rbuilder_primitives::order_statistics::OrderStatistics; +use rbuilder_primitives::{order_statistics::OrderStatistics, Order}; /// Assembles block building results from the best orderings of order groups. pub struct BlockBuildingResultAssembler { @@ -186,6 +186,27 @@ impl BlockBuildingResultAssembler { ) -> eyre::Result> { let build_start = Instant::now(); + // Extract ACE protocol transactions (Order::AceTx) from all groups + // These will be pre-committed at the top of the block + let mut ace_txs = Vec::new(); + for (_, group) in best_orderings_per_group.iter() { + for order in group.orders.iter() { + if matches!(order.order, Order::AceTx(_)) { + ace_txs.push(order.clone()); + } + } + } + + // Remove ACE orders from groups so they don't get processed twice + for (resolution_result, group) in best_orderings_per_group.iter_mut() { + // Filter out ACE orders from the sequence + resolution_result + .sequence_of_orders + .retain(|(order_idx, _)| { + !matches!(group.orders[*order_idx].order, Order::AceTx(_)) + }); + } + let mut block_building_helper = BlockBuildingHelperFromProvider::new( self.built_block_id_source.get_new_id(), self.state.clone(), @@ -199,6 +220,18 @@ impl BlockBuildingResultAssembler { )?; block_building_helper.set_trace_orders_closed_at(orders_closed_at); + // Pre-commit ACE protocol transactions at the top of the block + for ace_tx in &ace_txs { + trace!(order_id = ?ace_tx.id(), "Pre-committing ACE protocol tx"); + if let Err(err) = block_building_helper.commit_order( + &mut self.local_ctx, + ace_tx, + &|_| Ok(()), // ACE protocol txs bypass profit validation + ) { + trace!(order_id = ?ace_tx.id(), ?err, "Failed to pre-commit ACE protocol tx"); + } + } + // Sort groups by total profit in descending order best_orderings_per_group.sort_by(|(a_ordering, _), (b_ordering, _)| { b_ordering.total_profit.cmp(&a_ordering.total_profit) @@ -261,6 +294,30 @@ impl BlockBuildingResultAssembler { best_results: HashMap, orders_closed_at: OffsetDateTime, ) -> eyre::Result> { + let mut best_orderings_per_group: Vec<(ResolutionResult, ConflictGroup)> = + best_results.into_values().collect(); + + // Extract ACE protocol transactions (Order::AceTx) from all groups + // These will be pre-committed at the top of the block + let mut ace_txs = Vec::new(); + for (_, group) in best_orderings_per_group.iter() { + for order in group.orders.iter() { + if matches!(order.order, Order::AceTx(_)) { + ace_txs.push(order.clone()); + } + } + } + + // Remove ACE orders from groups so they don't get processed twice + for (resolution_result, group) in best_orderings_per_group.iter_mut() { + // Filter out ACE orders from the sequence + resolution_result + .sequence_of_orders + .retain(|(order_idx, _)| { + !matches!(group.orders[*order_idx].order, Order::AceTx(_)) + }); + } + let mut block_building_helper = BlockBuildingHelperFromProvider::new( self.built_block_id_source.get_new_id(), self.state.clone(), @@ -275,8 +332,17 @@ impl BlockBuildingResultAssembler { block_building_helper.set_trace_orders_closed_at(orders_closed_at); - let mut best_orderings_per_group: Vec<(ResolutionResult, ConflictGroup)> = - best_results.into_values().collect(); + // Pre-commit ACE protocol transactions at the top of the block + for ace_tx in &ace_txs { + trace!(order_id = ?ace_tx.id(), "Pre-committing ACE protocol tx in backtest"); + if let Err(err) = block_building_helper.commit_order( + &mut self.local_ctx, + ace_tx, + &|_| Ok(()), // ACE protocol txs bypass profit validation + ) { + trace!(order_id = ?ace_tx.id(), ?err, "Failed to pre-commit ACE protocol tx in backtest"); + } + } // Sort groups by total profit in descending order best_orderings_per_group.sort_by(|(a_ordering, _), (b_ordering, _)| { diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index 2e70fd412..0deb0414e 100644 --- a/crates/rbuilder/src/building/sim.rs +++ b/crates/rbuilder/src/building/sim.rs @@ -15,7 +15,11 @@ use crate::{ }; use ahash::{HashMap, HashSet}; use alloy_primitives::Address; +use alloy_primitives::U256; use rand::seq::SliceRandom; +use rbuilder_primitives::ace::{AceExchange, AceInteraction}; +use rbuilder_primitives::BlockSpace; +use rbuilder_primitives::SimValue; use rbuilder_primitives::{Order, OrderId, SimulatedOrder}; use reth_errors::ProviderError; use reth_provider::StateProvider; @@ -25,6 +29,7 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; +use strum::IntoEnumIterator; use tracing::{error, trace}; #[derive(Debug)] @@ -422,10 +427,60 @@ pub fn simulate_order( 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 order_id = order.id(); + let sim_res = simulate_order_using_fork( + parent_orders, + order.clone(), + &mut fork, + &ctx.mempool_tx_detector, + ); fork.rollback(rollback_point); - let sim_res = sim_res?; + let mut sim_res = sim_res?; + + let sim_success = matches!(&sim_res, OrderSimResult::Success(_, _)); + let ace_interaction = AceExchange::iter().find_map(|exchange| { + exchange.classify_ace_interaction(&tracer.used_state_trace, sim_success) + }); + + match sim_res { + OrderSimResult::Failed(ref err) => { + // Check if failed order accessed ACE - if so, treat as successful with zero profit + if let Some(interaction @ AceInteraction::NonUnlocking { exchange }) = ace_interaction { + tracing::debug!( + order = ?order_id, + ?err, + ?exchange, + "Failed order accessed ACE - treating as successful non-unlocking ACE transaction" + ); + sim_res = OrderSimResult::Success( + Arc::new(SimulatedOrder { + order, + sim_value: SimValue::new( + U256::ZERO, + U256::ZERO, + BlockSpace::new(tracer.used_gas, 0, 0), + Vec::new(), + ), + used_state_trace: Some(tracer.used_state_trace.clone()), + ace_interaction: Some(interaction), + }), + Vec::new(), + ); + } + } + // If we have a sucessful simulation and we have detected an ace tx, this means that it is a + // unlocking mempool ace tx by default. + OrderSimResult::Success(..) => { + if let Some(interaction) = ace_interaction { + tracing::debug!( + order = ?order.id(), + ?interaction, + "Order has ACE interaction" + ); + } + } + } + Ok(OrderSimResultWithGas { result: sim_res, gas_used: tracer.used_gas, @@ -472,6 +527,7 @@ pub fn simulate_order_using_fork( order, sim_value, used_state_trace: res.used_state_trace, + ace_interaction: None, }), new_nonces, )) diff --git a/crates/rbuilder/src/live_builder/order_input/orderpool.rs b/crates/rbuilder/src/live_builder/order_input/orderpool.rs index ab6580c8c..1141c4332 100644 --- a/crates/rbuilder/src/live_builder/order_input/orderpool.rs +++ b/crates/rbuilder/src/live_builder/order_input/orderpool.rs @@ -112,7 +112,7 @@ impl OrderPool { trace!(?order_id, "Adding order"); let (order, target_block) = match &order { - Order::Tx(..) | Order::AceTx(_) => { + Order::Tx(..) => { self.mempool_txs.push((order.clone(), Instant::now())); self.mempool_txs_size += Self::measure_tx(order); (order, None) @@ -142,6 +142,10 @@ impl OrderPool { bundles_store.bundles.push(order.clone()); (order, Some(target_block)) } + Order::AceTx(ace_tx) => { + self.bundles_for_current_block.push(order.clone()); + (order, ace_tx.target_block()) + } }; self.known_orders .put((order.id(), target_block.unwrap_or_default()), ()); @@ -300,7 +304,10 @@ impl OrderPool { pub fn measure_tx(order: &Order) -> usize { match order { Order::Tx(tx) => tx.size(), - Order::AceTx(_) => 0, + Order::AceTx(_) => { + error!("measure_tx called on an ace"); + 0 + } Order::Bundle(_) => { error!("measure_tx called on a bundle"); 0 diff --git a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs index 1daa4130d..9b6701b46 100644 --- a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs +++ b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs @@ -1,7 +1,10 @@ use std::{fmt, sync::Arc}; use crate::{ - building::sim::{SimTree, SimulatedResult, SimulationRequest}, + building::{ + ace_bundler::AceBundler, + sim::{SimTree, SimulatedResult, SimulationRequest}, + }, live_builder::{ order_input::order_sink::OrderPoolCommand, simulation::simulation_job_tracer::SimulationJobTracer, @@ -38,6 +41,7 @@ pub struct SimulationJob { /// Output of the simulations slot_sim_results_sender: mpsc::Sender, sim_tree: SimTree, + ace_bundler: AceBundler, orders_received: OrderCounter, orders_simulated_ok: OrderCounter, @@ -78,6 +82,7 @@ impl SimulationJob { sim_tracer: Arc, ) -> Self { Self { + ace_bundler: AceBundler::new(), block_cancellation, new_order_sub, sim_req_sender, From 587f9e11eca6363179618bf4428fe62cea07660d Mon Sep 17 00:00:00 2001 From: Will Smith Date: Thu, 23 Oct 2025 14:08:37 -0400 Subject: [PATCH 04/51] fix: sim tree --- Cargo.lock | 2 ++ crates/rbuilder/src/building/sim.rs | 4 +++- .../rbuilder/src/live_builder/simulation/simulation_job.rs | 6 ++---- crates/rbuilder/src/mev_boost/mod.rs | 2 ++ 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4940515df..b82b5ea73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9296,6 +9296,7 @@ dependencies = [ "sha2 0.10.9", "shellexpand", "sqlx", + "strum 0.27.2", "sysperf", "tempfile", "test_utils", @@ -9418,6 +9419,7 @@ dependencies = [ "serde_with", "sha2 0.10.9", "ssz_types 0.8.0", + "strum 0.27.2", "thiserror 1.0.69", "time", "toml 0.8.23", diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index 0deb0414e..ba52359ab 100644 --- a/crates/rbuilder/src/building/sim.rs +++ b/crates/rbuilder/src/building/sim.rs @@ -470,13 +470,15 @@ pub fn simulate_order( } // If we have a sucessful simulation and we have detected an ace tx, this means that it is a // unlocking mempool ace tx by default. - OrderSimResult::Success(..) => { + OrderSimResult::Success(ref mut simulated_order, _) => { if let Some(interaction) = ace_interaction { tracing::debug!( order = ?order.id(), ?interaction, "Order has ACE interaction" ); + // Update the SimulatedOrder to include ace_interaction + Arc::make_mut(simulated_order).ace_interaction = Some(interaction); } } } diff --git a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs index 9b6701b46..a45ffae48 100644 --- a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs +++ b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs @@ -1,10 +1,7 @@ use std::{fmt, sync::Arc}; use crate::{ - building::{ - ace_bundler::AceBundler, - sim::{SimTree, SimulatedResult, SimulationRequest}, - }, + building::sim::{SimTree, SimulatedResult, SimulationRequest}, live_builder::{ order_input::order_sink::OrderPoolCommand, simulation::simulation_job_tracer::SimulationJobTracer, @@ -207,6 +204,7 @@ impl SimulationJob { 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 diff --git a/crates/rbuilder/src/mev_boost/mod.rs b/crates/rbuilder/src/mev_boost/mod.rs index fb00a6c6f..654f1bb1f 100644 --- a/crates/rbuilder/src/mev_boost/mod.rs +++ b/crates/rbuilder/src/mev_boost/mod.rs @@ -1,4 +1,6 @@ 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}; From 4d96ed45ea1df875f3d1f43d8b8b15205409b167 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Fri, 24 Oct 2025 16:06:07 -0400 Subject: [PATCH 05/51] wip: almost done cleaning up sequencing --- crates/rbuilder-primitives/src/ace.rs | 14 + crates/rbuilder/src/building/ace_bundler.rs | 330 ++++-------------- .../live_builder/simulation/simulation_job.rs | 20 ++ 3 files changed, 96 insertions(+), 268 deletions(-) diff --git a/crates/rbuilder-primitives/src/ace.rs b/crates/rbuilder-primitives/src/ace.rs index 8f14df0b7..10f3627a1 100644 --- a/crates/rbuilder-primitives/src/ace.rs +++ b/crates/rbuilder-primitives/src/ace.rs @@ -80,6 +80,20 @@ pub enum AceInteraction { NonUnlocking { exchange: AceExchange }, } +impl AceInteraction { + pub fn is_unlocking(&self) -> bool { + matches!(self, Self::Unlocking { .. }) + } + + pub fn get_exchange(&self) -> AceExchange { + match self { + AceInteraction::Unlocking { exchange } | AceInteraction::NonUnlocking { exchange } => { + *exchange + } + } + } +} + /// Type of unlock for ACE protocol transactions (Order::Ace) #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub enum AceUnlockType { diff --git a/crates/rbuilder/src/building/ace_bundler.rs b/crates/rbuilder/src/building/ace_bundler.rs index f8a89ecb0..57726b842 100644 --- a/crates/rbuilder/src/building/ace_bundler.rs +++ b/crates/rbuilder/src/building/ace_bundler.rs @@ -1,10 +1,13 @@ use alloy_primitives::U256; +use itertools::Itertools; use rbuilder_primitives::{ ace::{AceExchange, AceInteraction, AceUnlockType}, - Order, OrderId, SimulatedOrder, + Order, SimulatedOrder, }; use std::sync::Arc; -use tracing::{debug, trace}; +use tracing::trace; + +use crate::{building::sim::SimulationRequest, live_builder::simulation::SimulatedOrderCommand}; /// The ACE bundler sits between the sim-tree and the builder itself. We put the bundler here as it /// gives maximum flexibility for ACE protocols for defining ordering and handling cases were @@ -28,15 +31,14 @@ pub struct AceExchangeData { pub force_ace_tx: Option, /// Optional ACE protocol tx - conditionally included pub optional_ace_tx: Option, - /// Mempool txs that unlock ACE state - pub unlocking_mempool_txs: Vec, + /// weather or not we have pushed through an unlocking mempool tx. + pub has_unlocking: bool, /// Mempool txs that require ACE unlock pub non_unlocking_mempool_txs: Vec, } #[derive(Debug, Clone)] pub struct AceOrderEntry { - pub order: Order, pub simulated: Arc, /// Profit after bundle simulation pub bundle_profit: U256, @@ -46,12 +48,12 @@ impl AceExchangeData { /// Add an ACE protocol transaction pub fn add_ace_protocol_tx( &mut self, - order: Order, simulated: Arc, unlock_type: AceUnlockType, - ) { + ) -> Vec { + let sim_cpy = simulated.order.clone(); + let entry = AceOrderEntry { - order, bundle_profit: simulated.sim_value.full_profit_info().coinbase_profit(), simulated, }; @@ -66,169 +68,60 @@ impl AceExchangeData { trace!("Added optional ACE protocol unlock tx"); } } - } - /// Add a mempool ACE transaction - pub fn add_mempool_tx( - &mut self, - order: Order, - simulated: Arc, - is_unlocking: bool, - ) { - let entry = AceOrderEntry { - order, - bundle_profit: simulated.sim_value.full_profit_info().coinbase_profit(), - simulated, + // Take all non-unlocking orders and simulate them with parents so they will pass and inject + // them into the system. + self.non_unlocking_mempool_txs + .drain(..) + .map(|entry| SimulationRequest { + id: rand::random(), + order: entry.simulated.order.clone(), + parents: vec![sim_cpy.clone()], + }) + .collect_vec() + } + + pub fn try_generate_sim_request(&self, order: &Order) -> Option { + let Some(parent) = self + .optional_ace_tx + .as_ref() + .or_else(|| self.force_ace_tx.as_ref()) + else { + return None; }; - if is_unlocking { - self.unlocking_mempool_txs.push(entry); - trace!("Added unlocking mempool ACE tx"); - } else { - self.non_unlocking_mempool_txs.push(entry); - trace!("Added non-unlocking mempool ACE tx"); - } + Some(SimulationRequest { + id: rand::random(), + order: order.clone(), + parents: vec![parent.simulated.order.clone()], + }) } - /// Check if we should include optional ACE protocol tx - /// Optional is included if we have non-unlocking txs and no other unlock source - fn should_include_optional(&self) -> bool { - !self.non_unlocking_mempool_txs.is_empty() - && self.force_ace_tx.is_none() - && self.unlocking_mempool_txs.is_empty() - } + // If we have a regular mempool unlocking tx, we don't want to include the optional ace + // transaction ad will cancel it. + pub fn has_unlocking(&mut self) -> Option { + self.has_unlocking = true; - /// Check if we have an available unlock (either force ACE or mempool unlocking) - fn has_unlock(&self) -> bool { - self.force_ace_tx.is_some() || !self.unlocking_mempool_txs.is_empty() - } - - /// Get the ACE bundle to place at top of block - /// Returns all unlock txs (force ACE, optional ACE, mempool unlocks) followed by non-unlocking txs - pub fn get_ace_bundle(&self) -> Vec { - let mut orders = Vec::new(); - - // Priority 1: Force ACE unlock (always included) - if let Some(ref force_tx) = self.force_ace_tx { - orders.push(force_tx.order.clone()); - } - - // Priority 2: Optional ACE unlock (if needed and no force ACE) - if let Some(ref optional_tx) = self.optional_ace_tx { - if self.should_include_optional() { - orders.push(optional_tx.order.clone()); - } - } - - // Priority 3: Mempool unlocking txs - for entry in &self.unlocking_mempool_txs { - orders.push(entry.order.clone()); - } - - // Priority 4: Non-unlocking mempool txs (only if we have an unlock) - if self.has_unlock() || self.should_include_optional() { - for entry in &self.non_unlocking_mempool_txs { - orders.push(entry.order.clone()); - } - } - - orders - } - - /// Update profits and sort by profitability - pub fn update_profits(&mut self, order_id: &OrderId, profit: U256) -> bool { - if let Some(ref mut entry) = self.force_ace_tx { - if entry.order.id() == *order_id { - entry.bundle_profit = profit; - return true; - } - } - - if let Some(ref mut entry) = self.optional_ace_tx { - if entry.order.id() == *order_id { - entry.bundle_profit = profit; - return true; - } - } - - for entry in &mut self.unlocking_mempool_txs { - if entry.order.id() == *order_id { - entry.bundle_profit = profit; - return true; - } - } - - for entry in &mut self.non_unlocking_mempool_txs { - if entry.order.id() == *order_id { - entry.bundle_profit = profit; - return true; - } - } - - false - } - - /// Sort mempool transactions by profitability - pub fn sort_by_profit(&mut self) { - self.unlocking_mempool_txs - .sort_by(|a, b| b.bundle_profit.cmp(&a.bundle_profit)); - self.non_unlocking_mempool_txs - .sort_by(|a, b| b.bundle_profit.cmp(&a.bundle_profit)); - } - - /// Remove orders that builder wants to kick out - pub fn kick_out_orders(&mut self, order_ids: &[OrderId]) { - if let Some(ref force_tx) = self.force_ace_tx { - if order_ids.contains(&force_tx.order.id()) { - debug!("Attempted to kick out force ACE tx - ignoring"); - } - } - - self.unlocking_mempool_txs - .retain(|entry| !order_ids.contains(&entry.order.id())); - self.non_unlocking_mempool_txs - .retain(|entry| !order_ids.contains(&entry.order.id())); + self.optional_ace_tx + .take() + .map(|order| SimulatedOrderCommand::Cancellation(order.simulated.order.id())) } - /// Get total profit - pub fn total_profit(&self) -> U256 { - let mut total = U256::ZERO; - - if let Some(ref entry) = self.force_ace_tx { - total = total.saturating_add(entry.bundle_profit); - } - if let Some(ref entry) = self.optional_ace_tx { - total = total.saturating_add(entry.bundle_profit); + pub fn add_mempool_tx(&mut self, simulated: Arc) -> Option { + if let Some(req) = self.try_generate_sim_request(&simulated.order) { + return Some(req); } + // we don't have a way to sim this mempool tx yet, going to collect it instead. - for entry in &self.unlocking_mempool_txs { - total = total.saturating_add(entry.bundle_profit); - } - for entry in &self.non_unlocking_mempool_txs { - total = total.saturating_add(entry.bundle_profit); - } - - total - } + let entry = AceOrderEntry { + bundle_profit: simulated.sim_value.full_profit_info().coinbase_profit(), + simulated, + }; - /// Check if empty - pub fn is_empty(&self) -> bool { - self.force_ace_tx.is_none() - && self.optional_ace_tx.is_none() - && self.unlocking_mempool_txs.is_empty() - && self.non_unlocking_mempool_txs.is_empty() - } + trace!("Added non-unlocking mempool ACE tx"); + self.non_unlocking_mempool_txs.push(entry); - /// Get count of orders - pub fn len(&self) -> usize { - let mut count = 0; - if self.force_ace_tx.is_some() { - count += 1; - } - if self.optional_ace_tx.is_some() { - count += 1; - } - count + self.unlocking_mempool_txs.len() + self.non_unlocking_mempool_txs.len() + None } } @@ -242,111 +135,28 @@ impl AceBundler { /// Add an ACE protocol transaction (Order::Ace) pub fn add_ace_protocol_tx( &mut self, - order: Order, simulated: Arc, unlock_type: AceUnlockType, exchange: AceExchange, ) { let data = self.exchanges.entry(exchange).or_default(); - data.add_ace_protocol_tx(order, simulated, unlock_type); + data.add_ace_protocol_tx(simulated, unlock_type); + } + + pub fn have_unlocking(&mut self, exchange: AceExchange) -> Option { + self.exchanges.entry(exchange).or_default().has_unlocking() } /// Add a mempool ACE transaction or bundle containing ACE interactions pub fn add_mempool_ace_tx( &mut self, - order: Order, simulated: Arc, interaction: AceInteraction, - ) { - if matches!(order, Order::Bundle(_) | Order::ShareBundle(_)) { - trace!( - order_id = ?order.id(), - "Adding ACE bundle/share bundle - will be treated as atomic unit" - ); - } - - match interaction { - AceInteraction::Unlocking { exchange } => { - let data = self.exchanges.entry(exchange).or_default(); - data.add_mempool_tx(order, simulated, true); - } - AceInteraction::NonUnlocking { exchange } => { - let data = self.exchanges.entry(exchange).or_default(); - data.add_mempool_tx(order, simulated, false); - } - } - } - - /// Handle replacement of a mempool transaction - pub fn replace_mempool_tx( - &mut self, - old_order_id: &OrderId, - new_order: Order, - new_simulated: Arc, - interaction: AceInteraction, - ) -> bool { - let mut found = false; - for data in self.exchanges.values_mut() { - if let Some(pos) = data - .unlocking_mempool_txs - .iter() - .position(|e| e.order.id() == *old_order_id) - { - data.unlocking_mempool_txs.remove(pos); - found = true; - break; - } - if let Some(pos) = data - .non_unlocking_mempool_txs - .iter() - .position(|e| e.order.id() == *old_order_id) - { - data.non_unlocking_mempool_txs.remove(pos); - found = true; - break; - } - } - - if found { - self.add_mempool_ace_tx(new_order, new_simulated, interaction); - trace!( - "Replaced ACE mempool tx {:?} with new version", - old_order_id - ); - } - - found - } - - /// Get the ACE bundle for a specific exchange to place at top of block - pub fn get_ace_bundle(&self, exchange: &AceExchange) -> Vec { + ) -> Option { self.exchanges - .get(exchange) - .map(|data| data.get_ace_bundle()) - .unwrap_or_default() - } - - /// Update profits after bundle simulation - pub fn update_after_simulation(&mut self, simulation_results: Vec<(OrderId, U256)>) { - for (order_id, profit) in simulation_results { - for data in self.exchanges.values_mut() { - if data.update_profits(&order_id, profit) { - break; - } - } - } - - // Sort all exchanges by profit - for data in self.exchanges.values_mut() { - data.sort_by_profit(); - } - } - - /// Remove specific ACE orders if builder has better alternatives - pub fn kick_out_orders(&mut self, exchange: &AceExchange, order_ids: &[OrderId]) { - if let Some(data) = self.exchanges.get_mut(exchange) { - data.kick_out_orders(order_ids); - } + .entry(interaction.get_exchange()) + .or_default() + .add_mempool_tx(simulated) } /// Get all configured exchanges @@ -358,22 +168,6 @@ impl AceBundler { pub fn clear(&mut self) { self.exchanges.clear(); } - - pub fn is_empty(&self) -> bool { - self.exchanges.is_empty() || self.exchanges.values().all(|d| d.is_empty()) - } - - pub fn len(&self) -> usize { - self.exchanges.values().map(|d| d.len()).sum() - } - - /// Get total profit for a specific exchange - pub fn total_profit(&self, exchange: &AceExchange) -> U256 { - self.exchanges - .get(exchange) - .map(|d| d.total_profit()) - .unwrap_or(U256::ZERO) - } } impl Default for AceExchangeData { @@ -381,7 +175,7 @@ impl Default for AceExchangeData { Self { force_ace_tx: None, optional_ace_tx: None, - unlocking_mempool_txs: Vec::new(), + has_unlocking: false, non_unlocking_mempool_txs: Vec::new(), } } diff --git a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs index a45ffae48..387ed9650 100644 --- a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs +++ b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs @@ -1,3 +1,4 @@ +use crate::building::ace_bundler::AceBundler; use std::{fmt, sync::Arc}; use crate::{ @@ -185,6 +186,18 @@ impl SimulationJob { } } + /// Returns weather or not to continue the processing of this tx. + fn handle_ace_tx(&mut self, res: &SimulatedResult) -> bool { + let ace_interaction = res.simulated_order.ace_interaction.unwrap(); + if ace_interaction.is_unlocking() { + self.ace_bundler + .add_mempool_ace_tx(res.clone(), ace_interaction); + return true; + } + + false + } + /// updates the sim_tree and notifies new orders /// ONLY not cancelled are considered /// return if everything went OK @@ -200,11 +213,18 @@ impl SimulationJob { 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; } + // first we need to check if this interacted with a ace tx and if so what type. + if sim_result.simulated_order.ace_interaction.is_some() { + if !self.handle_ace_tx(&sim_result) { + continue; + } + } // Skip cancelled orders and remove from in_flight_orders if self .in_flight_orders From 0b29bc65e5a66f5a8d21325edc038e9c3faacd55 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Mon, 27 Oct 2025 11:18:06 -0400 Subject: [PATCH 06/51] feat: finish baseline impl --- crates/rbuilder-primitives/src/lib.rs | 17 ++++++- crates/rbuilder/src/building/ace_bundler.rs | 17 ++++++- crates/rbuilder/src/building/sim.rs | 46 +++++++++--------- .../live_builder/simulation/simulation_job.rs | 47 +++++++++++++++++-- 4 files changed, 100 insertions(+), 27 deletions(-) diff --git a/crates/rbuilder-primitives/src/lib.rs b/crates/rbuilder-primitives/src/lib.rs index 6f99e85cd..3d4e3610a 100644 --- a/crates/rbuilder-primitives/src/lib.rs +++ b/crates/rbuilder-primitives/src/lib.rs @@ -41,7 +41,10 @@ pub use test_data_generator::TestDataGenerator; use thiserror::Error; use uuid::Uuid; -use crate::{ace::AceInteraction, serialize::TxEncoding}; +use crate::{ + ace::{AceExchange, AceInteraction, AceUnlockType}, + serialize::TxEncoding, +}; /// Extra metadata for an order. #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -1117,6 +1120,18 @@ impl AceTx { Self::Angstrom(ang) => vec![(&ang.tx, false)], } } + + pub fn ace_unlock_type(&self) -> AceUnlockType { + match self { + AceTx::Angstrom(ang) => ang.unlock_type, + } + } + + pub fn exchange(&self) -> AceExchange { + match self { + AceTx::Angstrom(_) => AceExchange::Angstrom, + } + } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] diff --git a/crates/rbuilder/src/building/ace_bundler.rs b/crates/rbuilder/src/building/ace_bundler.rs index 57726b842..3442f2ae0 100644 --- a/crates/rbuilder/src/building/ace_bundler.rs +++ b/crates/rbuilder/src/building/ace_bundler.rs @@ -100,6 +100,11 @@ impl AceExchangeData { // If we have a regular mempool unlocking tx, we don't want to include the optional ace // transaction ad will cancel it. pub fn has_unlocking(&mut self) -> Option { + // we only want to send this once. + if self.has_unlocking { + return None; + } + self.has_unlocking = true; self.optional_ace_tx @@ -138,9 +143,17 @@ impl AceBundler { simulated: Arc, unlock_type: AceUnlockType, exchange: AceExchange, - ) { + ) -> Vec { let data = self.exchanges.entry(exchange).or_default(); - data.add_ace_protocol_tx(simulated, unlock_type); + + data.add_ace_protocol_tx(simulated, unlock_type) + } + + pub fn has_unlocking(&self, exchange: &AceExchange) -> bool { + self.exchanges + .get(&exchange) + .map(|e| e.has_unlocking) + .unwrap_or_default() } pub fn have_unlocking(&mut self, exchange: AceExchange) -> Option { diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index ba52359ab..7fab560fc 100644 --- a/crates/rbuilder/src/building/sim.rs +++ b/crates/rbuilder/src/building/sim.rs @@ -88,7 +88,7 @@ pub struct SimTree { pending_orders: HashMap, pending_nonces: HashMap>, - ready_orders: Vec, + pub(crate) ready_orders: Vec, } #[derive(Debug)] @@ -428,6 +428,7 @@ pub fn simulate_order( let mut fork = PartialBlockFork::new(state, ctx, local_ctx).with_tracer(&mut tracer); let rollback_point = fork.rollback_point(); let order_id = order.id(); + let has_parents = !parent_orders.is_empty(); let sim_res = simulate_order_using_fork( parent_orders, order.clone(), @@ -446,26 +447,29 @@ pub fn simulate_order( OrderSimResult::Failed(ref err) => { // Check if failed order accessed ACE - if so, treat as successful with zero profit if let Some(interaction @ AceInteraction::NonUnlocking { exchange }) = ace_interaction { - tracing::debug!( - order = ?order_id, - ?err, - ?exchange, - "Failed order accessed ACE - treating as successful non-unlocking ACE transaction" - ); - sim_res = OrderSimResult::Success( - Arc::new(SimulatedOrder { - order, - sim_value: SimValue::new( - U256::ZERO, - U256::ZERO, - BlockSpace::new(tracer.used_gas, 0, 0), - Vec::new(), - ), - used_state_trace: Some(tracer.used_state_trace.clone()), - ace_interaction: Some(interaction), - }), - Vec::new(), - ); + // Ace can inject parent orders, we want to ignore these. + if !has_parents { + tracing::debug!( + order = ?order_id, + ?err, + ?exchange, + "Failed order accessed ACE - treating as successful non-unlocking ACE transaction" + ); + sim_res = OrderSimResult::Success( + Arc::new(SimulatedOrder { + order, + sim_value: SimValue::new( + U256::ZERO, + U256::ZERO, + BlockSpace::new(tracer.used_gas, 0, 0), + Vec::new(), + ), + used_state_trace: Some(tracer.used_state_trace.clone()), + ace_interaction: Some(interaction), + }), + Vec::new(), + ); + } } } // If we have a sucessful simulation and we have detected an ace tx, this means that it is a diff --git a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs index 387ed9650..a6f5888d8 100644 --- a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs +++ b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs @@ -10,7 +10,7 @@ use crate::{ }; use ahash::HashSet; use alloy_primitives::utils::format_ether; -use rbuilder_primitives::{Order, OrderId, OrderReplacementKey}; +use rbuilder_primitives::{ace::AceUnlockType, Order, OrderId, OrderReplacementKey}; use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; use tracing::{debug, error, info, trace, warn}; @@ -188,11 +188,51 @@ impl SimulationJob { /// Returns weather or not to continue the processing of this tx. fn handle_ace_tx(&mut self, res: &SimulatedResult) -> bool { + // this means that we have frontran this with an ace unlocking tx in the simulator. + // We cannot do anything else at this point so we yield to the default flow. + if !res.previous_orders.is_empty() { + return true; + } + + // check to see if this is a ace specific tx. + if let Order::AceTx(ref ace) = res.simulated_order.order { + let unlock_type = ace.ace_unlock_type(); + let exchange = ace.exchange(); + + for sim_order in self.ace_bundler.add_ace_protocol_tx( + res.simulated_order.clone(), + unlock_type, + exchange, + ) { + self.sim_tree.ready_orders.push(sim_order); + } + + // If its a force, we pass through. If its a optional, we only want to have it be + // inlcuded if we don't have an unlocking tx. + return match unlock_type { + AceUnlockType::Force => true, + AceUnlockType::Optional => !self.ace_bundler.has_unlocking(&exchange), + }; + } + + // we need to know if this ace tx has already been simulated or not. let ace_interaction = res.simulated_order.ace_interaction.unwrap(); if ace_interaction.is_unlocking() { - self.ace_bundler - .add_mempool_ace_tx(res.clone(), ace_interaction); + if let Some(cmd) = self + .ace_bundler + .have_unlocking(ace_interaction.get_exchange()) + { + let _ = self.slot_sim_results_sender.try_send(cmd); + } + return true; + } else { + if let Some(order) = self.ace_bundler.add_mempool_ace_tx( + res.simulated_order.clone(), + res.simulated_order.ace_interaction.unwrap(), + ) { + self.sim_tree.ready_orders.push(order); + } } false @@ -225,6 +265,7 @@ impl SimulationJob { continue; } } + // Skip cancelled orders and remove from in_flight_orders if self .in_flight_orders From c72ef7e6f699eeb0150662c7b7b9c0ffa532abaa Mon Sep 17 00:00:00 2001 From: Will Smith Date: Mon, 27 Oct 2025 11:55:24 -0400 Subject: [PATCH 07/51] fix: linting --- crates/rbuilder-primitives/src/ace.rs | 2 +- crates/rbuilder/src/building/ace_bundler.rs | 28 ++++--------------- .../building/block_orders/order_priority.rs | 1 + .../block_orders/share_bundle_merger.rs | 2 +- .../src/building/block_orders/test_context.rs | 2 ++ .../parallel_builder/conflict_resolvers.rs | 1 + .../conflict_task_generator.rs | 1 + .../builders/parallel_builder/groups.rs | 1 + crates/rbuilder/src/building/order_commit.rs | 2 +- .../building/testing/bundle_tests/setup.rs | 1 + .../live_builder/simulation/simulation_job.rs | 20 ++++++------- 11 files changed, 25 insertions(+), 36 deletions(-) diff --git a/crates/rbuilder-primitives/src/ace.rs b/crates/rbuilder-primitives/src/ace.rs index 10f3627a1..dc9dbf3d9 100644 --- a/crates/rbuilder-primitives/src/ace.rs +++ b/crates/rbuilder-primitives/src/ace.rs @@ -61,7 +61,7 @@ impl AceExchange { .keys() .any(|k| k.address == angstrom_address); - accessed_exchange.then(|| { + accessed_exchange.then_some({ if sim_success { AceInteraction::Unlocking { exchange } } else { diff --git a/crates/rbuilder/src/building/ace_bundler.rs b/crates/rbuilder/src/building/ace_bundler.rs index 3442f2ae0..b798ee70b 100644 --- a/crates/rbuilder/src/building/ace_bundler.rs +++ b/crates/rbuilder/src/building/ace_bundler.rs @@ -18,14 +18,14 @@ use crate::{building::sim::SimulationRequest, live_builder::simulation::Simulate /// protocol, there bundler can collect all the orders that interact with the protocol and then /// generate a bundle with the protocol tx first with all other orders following and set to /// droppable with a order that they want. -#[derive(Debug)] +#[derive(Debug, Default)] pub struct AceBundler { /// ACE bundles organized by exchange exchanges: ahash::HashMap, } /// Data for a specific ACE exchange including all transaction types and logic -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct AceExchangeData { /// Force ACE protocol tx - always included pub force_ace_tx: Option, @@ -82,13 +82,10 @@ impl AceExchangeData { } pub fn try_generate_sim_request(&self, order: &Order) -> Option { - let Some(parent) = self + let parent = self .optional_ace_tx .as_ref() - .or_else(|| self.force_ace_tx.as_ref()) - else { - return None; - }; + .or(self.force_ace_tx.as_ref())?; Some(SimulationRequest { id: rand::random(), @@ -132,9 +129,7 @@ impl AceExchangeData { impl AceBundler { pub fn new() -> Self { - Self { - exchanges: ahash::HashMap::default(), - } + Self::default() } /// Add an ACE protocol transaction (Order::Ace) @@ -151,7 +146,7 @@ impl AceBundler { pub fn has_unlocking(&self, exchange: &AceExchange) -> bool { self.exchanges - .get(&exchange) + .get(exchange) .map(|e| e.has_unlocking) .unwrap_or_default() } @@ -182,14 +177,3 @@ impl AceBundler { self.exchanges.clear(); } } - -impl Default for AceExchangeData { - fn default() -> Self { - Self { - force_ace_tx: None, - optional_ace_tx: None, - has_unlocking: false, - non_unlocking_mempool_txs: Vec::new(), - } - } -} diff --git a/crates/rbuilder/src/building/block_orders/order_priority.rs b/crates/rbuilder/src/building/block_orders/order_priority.rs index 73c882c95..9c7ddeaf5 100644 --- a/crates/rbuilder/src/building/block_orders/order_priority.rs +++ b/crates/rbuilder/src/building/block_orders/order_priority.rs @@ -332,6 +332,7 @@ mod test { U256::from(non_mempool_profit), gas, ), + ace_interaction: None, used_state_trace: None, }) } diff --git a/crates/rbuilder/src/building/block_orders/share_bundle_merger.rs b/crates/rbuilder/src/building/block_orders/share_bundle_merger.rs index 59e2c484b..ef37ec1c5 100644 --- a/crates/rbuilder/src/building/block_orders/share_bundle_merger.rs +++ b/crates/rbuilder/src/building/block_orders/share_bundle_merger.rs @@ -148,7 +148,7 @@ impl MultiBackrunManager { order: Order::ShareBundle(sbundle), sim_value: highest_payback_order.sim_order.sim_value.clone(), used_state_trace: highest_payback_order.sim_order.used_state_trace.clone(), - ace_interaction: highest_payback_order.sim_order.ace_interaction.clone(), + ace_interaction: highest_payback_order.sim_order.ace_interaction, })) } diff --git a/crates/rbuilder/src/building/block_orders/test_context.rs b/crates/rbuilder/src/building/block_orders/test_context.rs index 371a5d1a2..0a7294ac3 100644 --- a/crates/rbuilder/src/building/block_orders/test_context.rs +++ b/crates/rbuilder/src/building/block_orders/test_context.rs @@ -169,6 +169,7 @@ impl TestContext { order, sim_value, used_state_trace: None, + ace_interaction: None, }) } @@ -213,6 +214,7 @@ impl TestContext { Order::Bundle(_) => panic!("Order::Bundle expecting ShareBundle"), Order::Tx(_) => panic!("Order::Tx expecting ShareBundle"), Order::ShareBundle(sb) => sb, + Order::AceTx(_) => panic!("Order::AceTx expecting ShareBundle"), } } diff --git a/crates/rbuilder/src/building/builders/parallel_builder/conflict_resolvers.rs b/crates/rbuilder/src/building/builders/parallel_builder/conflict_resolvers.rs index 8e06e596b..996d1a6c0 100644 --- a/crates/rbuilder/src/building/builders/parallel_builder/conflict_resolvers.rs +++ b/crates/rbuilder/src/building/builders/parallel_builder/conflict_resolvers.rs @@ -533,6 +533,7 @@ mod tests { order: Order::Bundle(bundle), used_state_trace: None, sim_value, + ace_interaction: None, }) } } diff --git a/crates/rbuilder/src/building/builders/parallel_builder/conflict_task_generator.rs b/crates/rbuilder/src/building/builders/parallel_builder/conflict_task_generator.rs index dfc6f62ed..381b634d9 100644 --- a/crates/rbuilder/src/building/builders/parallel_builder/conflict_task_generator.rs +++ b/crates/rbuilder/src/building/builders/parallel_builder/conflict_task_generator.rs @@ -496,6 +496,7 @@ mod tests { }), sim_value, used_state_trace: Some(trace), + ace_interaction: None, }) } } diff --git a/crates/rbuilder/src/building/builders/parallel_builder/groups.rs b/crates/rbuilder/src/building/builders/parallel_builder/groups.rs index a0b6d8800..be892f41a 100644 --- a/crates/rbuilder/src/building/builders/parallel_builder/groups.rs +++ b/crates/rbuilder/src/building/builders/parallel_builder/groups.rs @@ -479,6 +479,7 @@ mod tests { }), used_state_trace: Some(trace), sim_value: SimValue::default(), + ace_interaction: None, }) } } diff --git a/crates/rbuilder/src/building/order_commit.rs b/crates/rbuilder/src/building/order_commit.rs index 547294e3d..88b934148 100644 --- a/crates/rbuilder/src/building/order_commit.rs +++ b/crates/rbuilder/src/building/order_commit.rs @@ -1376,7 +1376,7 @@ impl< paid_kickbacks: Vec::new(), delayed_kickback: None, used_state_trace: self.get_used_state_trace(), - original_order_ids: vec![OrderId::Ace(tx_hash.into())], + original_order_ids: vec![OrderId::Ace(tx_hash)], })) } Err(err) => Ok(Err(err.into())), diff --git a/crates/rbuilder/src/building/testing/bundle_tests/setup.rs b/crates/rbuilder/src/building/testing/bundle_tests/setup.rs index 0f9a35628..3f9afbdd1 100644 --- a/crates/rbuilder/src/building/testing/bundle_tests/setup.rs +++ b/crates/rbuilder/src/building/testing/bundle_tests/setup.rs @@ -228,6 +228,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/live_builder/simulation/simulation_job.rs b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs index a6f5888d8..ac15619fe 100644 --- a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs +++ b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs @@ -226,13 +226,11 @@ impl SimulationJob { } return true; - } else { - if let Some(order) = self.ace_bundler.add_mempool_ace_tx( - res.simulated_order.clone(), - res.simulated_order.ace_interaction.unwrap(), - ) { - self.sim_tree.ready_orders.push(order); - } + } else if let Some(order) = self.ace_bundler.add_mempool_ace_tx( + res.simulated_order.clone(), + res.simulated_order.ace_interaction.unwrap(), + ) { + self.sim_tree.ready_orders.push(order); } false @@ -260,10 +258,10 @@ impl SimulationJob { } // first we need to check if this interacted with a ace tx and if so what type. - if sim_result.simulated_order.ace_interaction.is_some() { - if !self.handle_ace_tx(&sim_result) { - continue; - } + if sim_result.simulated_order.ace_interaction.is_some() + && !self.handle_ace_tx(sim_result) + { + continue; } // Skip cancelled orders and remove from in_flight_orders From 8f39ca804987e1c649bb69dda58b8737ee24f767 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Mon, 10 Nov 2025 12:06:15 -0500 Subject: [PATCH 08/51] chore: cleanup ace tx order type --- crates/rbuilder-primitives/src/fmt.rs | 1 - crates/rbuilder-primitives/src/lib.rs | 25 ++---- .../src/order_statistics.rs | 5 -- crates/rbuilder-primitives/src/serialize.rs | 81 ------------------- .../rbuilder/src/backtest/redistribute/mod.rs | 5 +- .../find_landed_orders.rs | 8 -- crates/rbuilder/src/backtest/store.rs | 1 - .../src/building/built_block_trace.rs | 11 ++- crates/rbuilder/src/building/order_commit.rs | 59 -------------- .../src/live_builder/order_input/orderpool.rs | 8 -- .../live_builder/order_input/rpc_server.rs | 43 ---------- .../live_builder/simulation/simulation_job.rs | 44 +++++----- crates/rbuilder/src/mev_boost/mod.rs | 1 - .../src/telemetry/metrics/tracing_metrics.rs | 1 - 14 files changed, 33 insertions(+), 260 deletions(-) diff --git a/crates/rbuilder-primitives/src/fmt.rs b/crates/rbuilder-primitives/src/fmt.rs index 5ac2a0b79..0cf1a6d31 100644 --- a/crates/rbuilder-primitives/src/fmt.rs +++ b/crates/rbuilder-primitives/src/fmt.rs @@ -50,7 +50,6 @@ pub fn write_order( tx.tx_with_blobs.hash(), tx.tx_with_blobs.value() )), - Order::AceTx(ace) => buf.write_str(&format!("ace {}\n", ace.order_id())), Order::ShareBundle(sb) => { buf.write_str(&format!("ShB {:?}\n", sb.hash))?; write_share_bundle_inner(indent + 1, buf, &sb.inner_bundle) diff --git a/crates/rbuilder-primitives/src/lib.rs b/crates/rbuilder-primitives/src/lib.rs index 3d4e3610a..a25c01d88 100644 --- a/crates/rbuilder-primitives/src/lib.rs +++ b/crates/rbuilder-primitives/src/lib.rs @@ -1150,7 +1150,6 @@ pub enum Order { Bundle(Bundle), Tx(MempoolTx), ShareBundle(ShareBundle), - AceTx(AceTx), } /// Uniquely identifies a replaceable sbundle @@ -1197,7 +1196,6 @@ impl Order { Order::Bundle(bundle) => bundle.can_execute_with_block_base_fee(block_base_fee), Order::Tx(tx) => tx.tx_with_blobs.tx.max_fee_per_gas() >= block_base_fee, Order::ShareBundle(bundle) => bundle.can_execute_with_block_base_fee(block_base_fee), - Order::AceTx(tx) => tx.can_execute_with_block_base_fee(block_base_fee), } } @@ -1207,7 +1205,7 @@ impl Order { /// Non virtual orders should return self pub fn original_orders(&self) -> Vec<&Order> { match self { - Order::Bundle(_) | Order::Tx(_) | Order::AceTx(_) => vec![self], + Order::Bundle(_) | Order::Tx(_) => vec![self], Order::ShareBundle(sb) => { let res = sb.original_orders(); if res.is_empty() { @@ -1229,7 +1227,6 @@ impl Order { address: tx.tx_with_blobs.tx.signer(), optional: false, }], - Order::AceTx(tx) => tx.nonces(), Order::ShareBundle(bundle) => bundle.nonces(), } } @@ -1238,7 +1235,6 @@ impl Order { match self { Order::Bundle(bundle) => OrderId::Bundle(bundle.uuid), Order::Tx(tx) => OrderId::Tx(tx.tx_with_blobs.hash()), - Order::AceTx(ace) => OrderId::Tx(ace.order_id()), Order::ShareBundle(bundle) => OrderId::ShareBundle(bundle.hash), } } @@ -1259,7 +1255,6 @@ impl Order { match self { Order::Bundle(bundle) => bundle.list_txs(), Order::Tx(tx) => vec![(&tx.tx_with_blobs, true)], - Order::AceTx(tx) => tx.list_txs(), Order::ShareBundle(bundle) => bundle.list_txs(), } } @@ -1270,7 +1265,6 @@ impl Order { match self { Order::Bundle(bundle) => bundle.list_txs_revert(), Order::Tx(tx) => vec![(&tx.tx_with_blobs, TxRevertBehavior::AllowedIncluded)], - Order::AceTx(tx) => tx.list_txs_revert(), Order::ShareBundle(bundle) => bundle.list_txs_revert(), } } @@ -1279,7 +1273,7 @@ impl Order { pub fn list_txs_len(&self) -> usize { match self { Order::Bundle(bundle) => bundle.list_txs_len(), - Order::AceTx(_) | Order::Tx(_) => 1, + Order::Tx(_) => 1, Order::ShareBundle(bundle) => bundle.list_txs_len(), } } @@ -1297,7 +1291,7 @@ impl Order { r.sequence_number, ) }), - Order::AceTx(_) | Order::Tx(_) => None, + Order::Tx(_) => None, Order::ShareBundle(sbundle) => sbundle.replacement_data.as_ref().map(|r| { ( OrderReplacementKey::ShareBundle(r.clone().key), @@ -1314,7 +1308,7 @@ impl Order { pub fn target_block(&self) -> Option { match self { Order::Bundle(bundle) => bundle.block, - Order::AceTx(_) | Order::Tx(_) => None, + Order::Tx(_) => None, Order::ShareBundle(bundle) => Some(bundle.block), } } @@ -1324,7 +1318,7 @@ impl Order { match self { Order::Bundle(bundle) => bundle.signer, Order::ShareBundle(bundle) => bundle.signer, - Order::AceTx(_) | Order::Tx(_) => None, + Order::Tx(_) => None, } } @@ -1332,7 +1326,6 @@ impl Order { match self { Order::Bundle(bundle) => &bundle.metadata, Order::Tx(tx) => &tx.tx_with_blobs.metadata, - Order::AceTx(tx) => tx.metadata(), Order::ShareBundle(bundle) => &bundle.metadata, } } @@ -1477,13 +1470,12 @@ pub enum OrderId { Tx(B256), Bundle(Uuid), ShareBundle(B256), - Ace(B256), } impl OrderId { pub fn fixed_bytes(&self) -> B256 { match self { - Self::Tx(hash) | Self::ShareBundle(hash) | Self::Ace(hash) => *hash, + Self::Tx(hash) | Self::ShareBundle(hash) => *hash, Self::Bundle(uuid) => { let mut out = [0u8; 32]; out[0..16].copy_from_slice(uuid.as_bytes()); @@ -1514,9 +1506,6 @@ impl FromStr for OrderId { } else if let Some(hash_str) = s.strip_prefix("sbundle:") { let hash = B256::from_str(hash_str)?; Ok(Self::ShareBundle(hash)) - } else if let Some(hash_str) = s.strip_prefix("ace_tx:") { - let hash = B256::from_str(hash_str)?; - Ok(Self::Ace(hash)) } else { Err(eyre::eyre!("invalid order id")) } @@ -1530,7 +1519,6 @@ impl Display for OrderId { Self::Tx(hash) => write!(f, "tx:{hash:?}"), Self::Bundle(uuid) => write!(f, "bundle:{uuid:?}"), Self::ShareBundle(hash) => write!(f, "sbundle:{hash:?}"), - Self::Ace(hash) => write!(f, "ace_tx:{hash:?}"), } } } @@ -1545,7 +1533,6 @@ impl Ord for OrderId { fn cmp(&self, other: &Self) -> Ordering { fn rank(id: &OrderId) -> usize { match id { - OrderId::Ace(_) => 0, OrderId::Tx(_) => 1, OrderId::Bundle(_) => 2, OrderId::ShareBundle(_) => 3, diff --git a/crates/rbuilder-primitives/src/order_statistics.rs b/crates/rbuilder-primitives/src/order_statistics.rs index 663e75334..b091d2ddb 100644 --- a/crates/rbuilder-primitives/src/order_statistics.rs +++ b/crates/rbuilder-primitives/src/order_statistics.rs @@ -7,7 +7,6 @@ pub struct OrderStatistics { tx_count: i32, bundle_count: i32, sbundle_count: i32, - ace_count: i32, } impl OrderStatistics { @@ -19,7 +18,6 @@ impl OrderStatistics { match order { Order::Bundle(_) => self.bundle_count += 1, Order::Tx(_) => self.tx_count += 1, - Order::AceTx(_) => self.ace_count += 1, Order::ShareBundle(_) => self.sbundle_count += 1, } } @@ -28,7 +26,6 @@ impl OrderStatistics { match order { Order::Bundle(_) => self.bundle_count -= 1, Order::Tx(_) => self.tx_count -= 1, - Order::AceTx(_) => self.ace_count -= 1, Order::ShareBundle(_) => self.sbundle_count -= 1, } } @@ -44,7 +41,6 @@ impl Add for OrderStatistics { fn add(self, other: Self) -> Self::Output { Self { tx_count: self.tx_count + other.tx_count, - ace_count: self.ace_count + other.ace_count, bundle_count: self.bundle_count + other.bundle_count, sbundle_count: self.sbundle_count + other.sbundle_count, } @@ -58,7 +54,6 @@ impl Sub for OrderStatistics { Self { tx_count: self.tx_count - other.tx_count, bundle_count: self.bundle_count - other.bundle_count, - ace_count: self.ace_count - other.ace_count, sbundle_count: self.sbundle_count - other.sbundle_count, } } diff --git a/crates/rbuilder-primitives/src/serialize.rs b/crates/rbuilder-primitives/src/serialize.rs index 796cd0e04..1a68a8a36 100644 --- a/crates/rbuilder-primitives/src/serialize.rs +++ b/crates/rbuilder-primitives/src/serialize.rs @@ -5,7 +5,6 @@ use super::{ TransactionSignedEcRecoveredWithBlobs, TxRevertBehavior, TxWithBlobsCreateError, LAST_BUNDLE_VERSION, }; -use crate::{ace::AceUnlockType, AceTx, AngstromTx, Metadata}; use alloy_consensus::constants::EIP4844_TX_TYPE_ID; use alloy_eips::eip2718::Eip2718Error; use alloy_primitives::{Address, Bytes, TxHash, B256, U64}; @@ -506,78 +505,6 @@ impl RawTx { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "type")] -#[serde(rename_all = "camelCase")] -pub enum RawAce { - Angstrom(RawAngstromTx), -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct RawAngstromTx { - pub tx: Bytes, - pub unlock_data: Bytes, - pub max_priority_fee_per_gas: u128, - pub unlock_type: AceUnlockType, -} - -impl RawAce { - pub fn from_tx(ace: AceTx) -> Self { - match ace { - AceTx::Angstrom(angstrom_tx) => { - let tx_bytes = angstrom_tx.tx.envelope_encoded_no_blobs(); - RawAce::Angstrom(RawAngstromTx { - tx: tx_bytes, - unlock_data: angstrom_tx.unlock_data, - max_priority_fee_per_gas: angstrom_tx.max_priority_fee_per_gas, - unlock_type: angstrom_tx.unlock_type, - }) - } - } - } -} - -/// Angstrom bundle submission structure -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)] -#[serde(rename_all = "camelCase")] -pub struct AngstromIntegrationSubmission { - pub tx: Bytes, - pub unlock_data: Bytes, - pub max_priority_fee_per_gas: u128, -} - -impl AngstromIntegrationSubmission { - /// Convert the submission to an AceTx order - pub fn to_ace_tx( - self, - received_at: time::OffsetDateTime, - ) -> Result { - let tx = - RawTransactionDecodable::new(self.tx, TxEncoding::WithBlobData).decode_enveloped()?; - - let unlock_type = if self.unlock_data.is_empty() { - AceUnlockType::Force - } else { - AceUnlockType::Optional - }; - - let angstrom_tx = AngstromTx { - tx, - meta: Metadata { - received_at_timestamp: received_at, - is_system: false, - refund_identity: None, - }, - unlock_data: self.unlock_data, - max_priority_fee_per_gas: self.max_priority_fee_per_gas, - unlock_type, - }; - - Ok(AceTx::Angstrom(angstrom_tx)) - } -} - /// Struct to de/serialize json Bundles from bundles APIs and from/db. /// Does not assume a particular format on txs. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -904,7 +831,6 @@ pub enum RawOrder { Bundle(RawBundle), Tx(RawTx), ShareBundle(RawShareBundle), - Ace(RawAce), } #[derive(Error, Debug)] @@ -939,12 +865,6 @@ impl RawOrder { .decode_new_bundle(encoding) .map_err(RawOrderConvertError::FailedToDecodeShareBundle)?, )), - RawOrder::Ace(_) => { - // ACE orders are not decoded from RawOrder - they come directly from RPC - Err(RawOrderConvertError::UnsupportedOrderType( - "ACE orders cannot be decoded from RawOrder".to_string(), - )) - } } } } @@ -954,7 +874,6 @@ impl From for RawOrder { match value { Order::Bundle(bundle) => Self::Bundle(RawBundle::encode_no_blobs(bundle)), Order::Tx(tx) => Self::Tx(RawTx::encode_no_blobs(tx)), - Order::AceTx(tx) => Self::Ace(RawAce::from_tx(tx)), Order::ShareBundle(bundle) => { Self::ShareBundle(RawShareBundle::encode_no_blobs(bundle)) } diff --git a/crates/rbuilder/src/backtest/redistribute/mod.rs b/crates/rbuilder/src/backtest/redistribute/mod.rs index aef67327e..4eb576a2b 100644 --- a/crates/rbuilder/src/backtest/redistribute/mod.rs +++ b/crates/rbuilder/src/backtest/redistribute/mod.rs @@ -67,7 +67,6 @@ pub enum InclusionChange { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ExtendedOrderId { Tx(B256), - AceTx(B256), Bundle { uuid: Uuid, hash: B256 }, ShareBundle(B256), } @@ -76,7 +75,6 @@ impl ExtendedOrderId { fn new(order_id: OrderId, bundle_hashes: &HashMap) -> Self { match order_id { OrderId::Tx(hash) => ExtendedOrderId::Tx(hash), - OrderId::Ace(hash) => ExtendedOrderId::AceTx(hash), OrderId::Bundle(uuid) => { let hash = bundle_hashes.get(&order_id).cloned().unwrap_or_default(); ExtendedOrderId::Bundle { uuid, hash } @@ -320,7 +318,6 @@ where Order::Bundle(_) => bundles += 1, Order::Tx(_) => txs += 1, Order::ShareBundle(_) => share_bundles += 1, - Order::AceTx(_) => txs += 1, } } let total = txs + bundles + share_bundles; @@ -1236,7 +1233,7 @@ fn order_redistribution_address( let (first_tx, _) = txs.first()?; Some((first_tx.signer(), true)) } - Order::AceTx(_) | Order::Tx(_) => { + Order::Tx(_) => { unreachable!("Mempool tx order can't have signer"); } } diff --git a/crates/rbuilder/src/backtest/restore_landed_orders/find_landed_orders.rs b/crates/rbuilder/src/backtest/restore_landed_orders/find_landed_orders.rs index 9abc1a202..2ab1c90ce 100644 --- a/crates/rbuilder/src/backtest/restore_landed_orders/find_landed_orders.rs +++ b/crates/rbuilder/src/backtest/restore_landed_orders/find_landed_orders.rs @@ -45,14 +45,6 @@ impl SimplifiedOrder { 0, )], ), - Order::AceTx(_) => { - let txs = order - .list_txs_revert() - .into_iter() - .map(|(tx, revert)| OrderTxData::new(tx.hash(), revert, 0)) - .collect(); - SimplifiedOrder::new(id, txs) - } Order::Bundle(bundle) => { let (refund_percent, refund_payer_hash) = if let Some(refund) = &bundle.refund { (refund.percent as usize, Some(refund.tx_hash)) diff --git a/crates/rbuilder/src/backtest/store.rs b/crates/rbuilder/src/backtest/store.rs index 18e1f71b3..90c4cca32 100644 --- a/crates/rbuilder/src/backtest/store.rs +++ b/crates/rbuilder/src/backtest/store.rs @@ -504,7 +504,6 @@ fn order_type(command: &RawReplaceableOrderPoolCommand) -> &'static str { RawOrder::Bundle(_) => "bundle", RawOrder::Tx(_) => "tx", RawOrder::ShareBundle(_) => "sbundle", - RawOrder::Ace(_) => "ace_tx", }, RawReplaceableOrderPoolCommand::CancelShareBundle(_) => "cancel_sbundle", RawReplaceableOrderPoolCommand::CancelBundle(_) => "cancel_bundle", diff --git a/crates/rbuilder/src/building/built_block_trace.rs b/crates/rbuilder/src/building/built_block_trace.rs index e1f426cda..0cb20628e 100644 --- a/crates/rbuilder/src/building/built_block_trace.rs +++ b/crates/rbuilder/src/building/built_block_trace.rs @@ -137,14 +137,13 @@ impl BuiltBlockTrace { } // txs, bundles, share bundles - pub fn used_order_count(&self) -> (usize, usize, usize, usize) { + pub fn used_order_count(&self) -> (usize, usize, usize) { self.included_orders .iter() - .fold((0, 0, 0, 0), |acc, order| match order.order { - Order::Tx(_) => (acc.0 + 1, acc.1, acc.2, acc.3), - Order::Bundle(_) => (acc.0, acc.1 + 1, acc.2, acc.3), - Order::ShareBundle(_) => (acc.0, acc.1, acc.2 + 1, acc.3), - Order::AceTx(_) => (acc.0, acc.1, acc.2, acc.3 + 1), + .fold((0, 0, 0), |acc, order| match order.order { + Order::Tx(_) => (acc.0 + 1, acc.1, acc.2), + Order::Bundle(_) => (acc.0, acc.1 + 1, acc.2), + Order::ShareBundle(_) => (acc.0, acc.1, acc.2 + 1), }) } diff --git a/crates/rbuilder/src/building/order_commit.rs b/crates/rbuilder/src/building/order_commit.rs index 88b934148..b0b562d83 100644 --- a/crates/rbuilder/src/building/order_commit.rs +++ b/crates/rbuilder/src/building/order_commit.rs @@ -561,25 +561,6 @@ impl< res } - pub fn commit_ace( - &mut self, - tx: &AceTx, - space_state: BlockBuildingSpaceState, - ) -> Result, CriticalCommitOrderError> { - let current_block = self.ctx.block(); - // None is good for any block - if let Some(block) = tx.target_block() { - if block != current_block { - return Ok(Err(BundleErr::TargetBlockIncorrect { - block: current_block, - target_block: block, - target_max_block: block, - })); - } - } - self.execute_with_rollback(|state| state.commit_ace_no_rollback(tx, space_state)) - } - /// Checks if the tx can fit in the block by checking: /// - Gas left /// - Blob gas left @@ -1340,46 +1321,6 @@ impl< let res = self.commit_share_bundle(bundle, space_state, allow_tx_skip)?; self.bundle_to_order_result(res, coinbase_balance_before) } - Order::AceTx(ace) => { - let coinbase_balance_before = self.coinbase_balance()?; - let res = self.commit_ace(ace, space_state)?; - self.ace_to_order_result(res, coinbase_balance_before) - } - } - } - - fn ace_to_order_result( - &mut self, - ace_result: Result, - coinbase_balance_before: U256, - ) -> Result, CriticalCommitOrderError> { - match ace_result { - Ok(ok) => { - let coinbase_balance_after = self.coinbase_balance()?; - let coinbase_profit = if coinbase_balance_after >= coinbase_balance_before { - coinbase_balance_after - coinbase_balance_before - } else { - return Ok(Err(OrderErr::NegativeProfit( - coinbase_balance_before - coinbase_balance_after, - ))); - }; - - // Get the tx hash before moving tx_info - let tx_hash = ok.tx_info.tx.hash(); - - Ok(Ok(OrderOk { - coinbase_profit, - space_used: ok.space_used, - cumulative_space_used: ok.cumulative_space_used, - tx_infos: vec![ok.tx_info], - nonces_updated: ok.nonces_updated, - paid_kickbacks: Vec::new(), - delayed_kickback: None, - used_state_trace: self.get_used_state_trace(), - original_order_ids: vec![OrderId::Ace(tx_hash)], - })) - } - Err(err) => Ok(Err(err.into())), } } diff --git a/crates/rbuilder/src/live_builder/order_input/orderpool.rs b/crates/rbuilder/src/live_builder/order_input/orderpool.rs index 1141c4332..17d540768 100644 --- a/crates/rbuilder/src/live_builder/order_input/orderpool.rs +++ b/crates/rbuilder/src/live_builder/order_input/orderpool.rs @@ -142,10 +142,6 @@ impl OrderPool { bundles_store.bundles.push(order.clone()); (order, Some(target_block)) } - Order::AceTx(ace_tx) => { - self.bundles_for_current_block.push(order.clone()); - (order, ace_tx.target_block()) - } }; self.known_orders .put((order.id(), target_block.unwrap_or_default()), ()); @@ -304,10 +300,6 @@ impl OrderPool { pub fn measure_tx(order: &Order) -> usize { match order { Order::Tx(tx) => tx.size(), - Order::AceTx(_) => { - error!("measure_tx called on an ace"); - 0 - } Order::Bundle(_) => { error!("measure_tx called on a bundle"); 0 diff --git a/crates/rbuilder/src/live_builder/order_input/rpc_server.rs b/crates/rbuilder/src/live_builder/order_input/rpc_server.rs index 2da3ddb25..856854a03 100644 --- a/crates/rbuilder/src/live_builder/order_input/rpc_server.rs +++ b/crates/rbuilder/src/live_builder/order_input/rpc_server.rs @@ -9,7 +9,6 @@ use jsonrpsee::{ types::{ErrorObject, Params}, IntoResponse, RpcModule, }; -use rbuilder_primitives::serialize::AngstromIntegrationSubmission; use rbuilder_primitives::{ serialize::{ RawBundle, RawBundleDecodeResult, RawShareBundle, RawShareBundleDecodeResult, RawTx, @@ -37,7 +36,6 @@ const ETH_SEND_BUNDLE: &str = "eth_sendBundle"; const MEV_SEND_BUNDLE: &str = "mev_sendBundle"; const ETH_CANCEL_BUNDLE: &str = "eth_cancelBundle"; const ETH_SEND_RAW_TRANSACTION: &str = "eth_sendRawTransaction"; -const ANG_BUNDLE: &str = "angstrom_submitBundle"; /// Adds metrics to the callback and registers via module.register_async_method. pub fn register_metered_async_method<'a, R, Fun, Fut>( @@ -147,9 +145,6 @@ pub async fn start_server_accepting_bundles( } })?; let results_clone = results.clone(); - register_metered_async_method(&mut module, ANG_BUNDLE, move |params, _| { - handle_angstrom_bundle(results_clone.clone(), timeout, params) - })?; module.merge(extra_rpc)?; let handle = server.start(module); @@ -277,44 +272,6 @@ async fn handle_mev_send_bundle( }; } -/// Handles angstrom_submitBundle RPC call -async fn handle_angstrom_bundle( - results: mpsc::Sender, - timeout: Duration, - params: jsonrpsee::types::Params<'static>, -) { - let received_at = OffsetDateTime::now_utc(); - let start = Instant::now(); - - let submission: AngstromIntegrationSubmission = match params.one() { - Ok(submission) => submission, - Err(err) => { - warn!(?err, "Failed to parse Angstrom bundle"); - inc_order_input_rpc_errors(ANG_BUNDLE); - return; - } - }; - - let ace_tx = match submission.to_ace_tx(received_at) { - Ok(ace_tx) => ace_tx, - Err(err) => { - warn!(?err, "Failed to decode Angstrom bundle"); - inc_order_input_rpc_errors(ANG_BUNDLE); - return; - } - }; - - let order = Order::AceTx(ace_tx); - let parse_duration = start.elapsed(); - trace!( - order = ?order.id(), - parse_duration_mus = parse_duration.as_micros(), - "Received Angstrom ACE bundle from API" - ); - - send_order(order, &results, timeout, received_at).await; -} - async fn send_order( order: Order, channel: &mpsc::Sender, diff --git a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs index ac15619fe..7465a2819 100644 --- a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs +++ b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs @@ -195,25 +195,26 @@ impl SimulationJob { } // check to see if this is a ace specific tx. - if let Order::AceTx(ref ace) = res.simulated_order.order { - let unlock_type = ace.ace_unlock_type(); - let exchange = ace.exchange(); - - for sim_order in self.ace_bundler.add_ace_protocol_tx( - res.simulated_order.clone(), - unlock_type, - exchange, - ) { - self.sim_tree.ready_orders.push(sim_order); - } - // If its a force, we pass through. If its a optional, we only want to have it be - // inlcuded if we don't have an unlocking tx. - return match unlock_type { - AceUnlockType::Force => true, - AceUnlockType::Optional => !self.ace_bundler.has_unlocking(&exchange), - }; - } + // if let Order::AceTx(ref ace) = res.simulated_order.order { + // let unlock_type = ace.ace_unlock_type(); + // let exchange = ace.exchange(); + // + // for sim_order in self.ace_bundler.add_ace_protocol_tx( + // res.simulated_order.clone(), + // unlock_type, + // exchange, + // ) { + // self.sim_tree.ready_orders.push(sim_order); + // } + // + // // If its a force, we pass through. If its a optional, we only want to have it be + // // inlcuded if we don't have an unlocking tx. + // return match unlock_type { + // AceUnlockType::Force => true, + // AceUnlockType::Optional => !self.ace_bundler.has_unlocking(&exchange), + // }; + // } // we need to know if this ace tx has already been simulated or not. let ace_interaction = res.simulated_order.ace_interaction.unwrap(); @@ -372,19 +373,17 @@ struct OrderCounter { mempool_txs: usize, bundles: usize, share_bundles: usize, - ace_tx: usize, } impl fmt::Debug for OrderCounter { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, - "OrderCounter {{ total: {}, mempool_txs: {}, bundles {}, share_bundles {}, ace_txs {} }}", + "OrderCounter {{ total: {}, mempool_txs: {}, bundles {}, share_bundles {} }}", self.total(), self.mempool_txs, self.bundles, self.share_bundles, - self.ace_tx ) } } @@ -395,10 +394,9 @@ impl OrderCounter { Order::Tx(_) => self.mempool_txs += 1, Order::Bundle(_) => self.bundles += 1, Order::ShareBundle(_) => self.share_bundles += 1, - Order::AceTx(_) => self.ace_tx += 1, } } fn total(&self) -> usize { - self.mempool_txs + self.bundles + self.share_bundles + self.ace_tx + self.mempool_txs + self.bundles + self.share_bundles } } diff --git a/crates/rbuilder/src/mev_boost/mod.rs b/crates/rbuilder/src/mev_boost/mod.rs index 33d79bab3..507be6ad7 100644 --- a/crates/rbuilder/src/mev_boost/mod.rs +++ b/crates/rbuilder/src/mev_boost/mod.rs @@ -780,7 +780,6 @@ impl RelayClient { rbuilder_primitives::OrderId::Tx(_fixed_bytes) => None, rbuilder_primitives::OrderId::Bundle(uuid) => Some(uuid), rbuilder_primitives::OrderId::ShareBundle(_fixed_bytes) => None, - rbuilder_primitives::OrderId::Ace(_fixed_bytes) => None, }) .collect(); let total_bundles = bundle_ids.len(); diff --git a/crates/rbuilder/src/telemetry/metrics/tracing_metrics.rs b/crates/rbuilder/src/telemetry/metrics/tracing_metrics.rs index 53bec6952..0f14888fe 100644 --- a/crates/rbuilder/src/telemetry/metrics/tracing_metrics.rs +++ b/crates/rbuilder/src/telemetry/metrics/tracing_metrics.rs @@ -100,7 +100,6 @@ pub fn mark_command_received(command: &ReplaceableOrderPoolCommand, received_at: Order::Bundle(_) => "bundle", Order::Tx(_) => "tx", Order::ShareBundle(_) => "sbundle", - Order::AceTx(_) => "ace_tx", } } ReplaceableOrderPoolCommand::CancelShareBundle(_) From 299dc5dc2b912b78a8ff4f6390e03e6342605b77 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Mon, 10 Nov 2025 13:16:44 -0500 Subject: [PATCH 09/51] feat: rename ace bundler --- .../{ace_bundler.rs => ace_collector.rs} | 56 ++++++++++++++----- crates/rbuilder/src/building/mod.rs | 2 +- 2 files changed, 44 insertions(+), 14 deletions(-) rename crates/rbuilder/src/building/{ace_bundler.rs => ace_collector.rs} (79%) diff --git a/crates/rbuilder/src/building/ace_bundler.rs b/crates/rbuilder/src/building/ace_collector.rs similarity index 79% rename from crates/rbuilder/src/building/ace_bundler.rs rename to crates/rbuilder/src/building/ace_collector.rs index b798ee70b..18e8bb7d1 100644 --- a/crates/rbuilder/src/building/ace_bundler.rs +++ b/crates/rbuilder/src/building/ace_collector.rs @@ -1,27 +1,57 @@ -use alloy_primitives::U256; +use ahash::HashSet; +use alloy_primitives::{Address, U256}; +use alloy_rpc_types::TransactionTrait; use itertools::Itertools; use rbuilder_primitives::{ ace::{AceExchange, AceInteraction, AceUnlockType}, - Order, SimulatedOrder, + Order, SimulatedOrder, TransactionSignedEcRecoveredWithBlobs, }; +use serde::Deserialize; use std::sync::Arc; use tracing::trace; use crate::{building::sim::SimulationRequest, live_builder::simulation::SimulatedOrderCommand}; -/// The ACE bundler sits between the sim-tree and the builder itself. We put the bundler here as it -/// gives maximum flexibility for ACE protocols for defining ordering and handling cases were -/// certain tx's depend on other tx's. With this, a simple ace detection can be ran on incoming -/// orders. Before the orders get sent to the builders, Ace orders get intercepted here and then can -/// follow protocol specific ordering by leveraging the current bundling design. For example, if a -/// ace protocol wants to have a protocol transaction first and then sort everything greedly for there -/// protocol, there bundler can collect all the orders that interact with the protocol and then -/// generate a bundle with the protocol tx first with all other orders following and set to -/// droppable with a order that they want. +/// Collects Ace Orders #[derive(Debug, Default)] -pub struct AceBundler { +pub struct AceCollector { /// ACE bundles organized by exchange exchanges: ahash::HashMap, + ace_tx_lookup: ahash::HashMap, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct AceTxData { + pub from_addresses: HashSet
, + pub to_addresses: HashSet
, + pub unlock_signatures: HashSet<[u8; 4]>, + pub force_signatures: HashSet<[u8; 4]>, +} + +impl AceTxData { + pub fn is_ace(&self, tx: &TransactionSignedEcRecoveredWithBlobs) -> bool { + let internal = tx.internal_tx_unsecure(); + self.from_addresses.contains(&internal.signer()) + && self + .to_addresses + .contains(&internal.inner().to().unwrap_or_default()) + } + + pub fn ace_type(&self, tx: &TransactionSignedEcRecoveredWithBlobs) -> Option { + if self + .force_signatures + .contains(&tx.internal_tx_unsecure().inner().input()[0..4]) + { + Some(AceUnlockType::Force) + } else if self + .unlock_signatures + .contains(&tx.internal_tx_unsecure().inner().input()[0..4]) + { + Some(AceUnlockType::Optional) + } else { + None + } + } } /// Data for a specific ACE exchange including all transaction types and logic @@ -127,7 +157,7 @@ impl AceExchangeData { } } -impl AceBundler { +impl AceCollector { pub fn new() -> Self { Self::default() } diff --git a/crates/rbuilder/src/building/mod.rs b/crates/rbuilder/src/building/mod.rs index 487e23cf3..fe513d798 100644 --- a/crates/rbuilder/src/building/mod.rs +++ b/crates/rbuilder/src/building/mod.rs @@ -74,7 +74,7 @@ use time::OffsetDateTime; use tracing::{error, trace}; use tx_sim_cache::TxExecutionCache; -pub mod ace_bundler; +pub mod ace_collector; pub mod bid_adjustments; pub mod block_orders; pub mod builders; From 6363250ddb02ad0ade83aebb37babcbe8f6c3f48 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Mon, 10 Nov 2025 15:57:46 -0500 Subject: [PATCH 10/51] feat: wire in ace collector config --- crates/rbuilder-primitives/src/ace.rs | 4 +- crates/rbuilder/src/building/ace_collector.rs | 42 +++++++++++-------- .../rbuilder/src/live_builder/base_config.rs | 2 + .../rbuilder/src/live_builder/building/mod.rs | 4 ++ crates/rbuilder/src/live_builder/config.rs | 25 ++++++++++- crates/rbuilder/src/live_builder/mod.rs | 3 ++ .../src/live_builder/simulation/mod.rs | 3 ++ .../live_builder/simulation/simulation_job.rs | 7 ++-- .../config/rbuilder/config-live-example.toml | 17 ++++++-- 9 files changed, 80 insertions(+), 27 deletions(-) diff --git a/crates/rbuilder-primitives/src/ace.rs b/crates/rbuilder-primitives/src/ace.rs index dc9dbf3d9..9c23ec304 100644 --- a/crates/rbuilder-primitives/src/ace.rs +++ b/crates/rbuilder-primitives/src/ace.rs @@ -1,9 +1,11 @@ use crate::evm_inspector::UsedStateTrace; use alloy_primitives::{address, Address}; +use derive_more::FromStr; +use serde::Deserialize; use strum::EnumIter; /// What ace based exchanges that rbuilder supports. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter, Deserialize, FromStr)] pub enum AceExchange { Angstrom, } diff --git a/crates/rbuilder/src/building/ace_collector.rs b/crates/rbuilder/src/building/ace_collector.rs index 18e8bb7d1..99bdb7e2e 100644 --- a/crates/rbuilder/src/building/ace_collector.rs +++ b/crates/rbuilder/src/building/ace_collector.rs @@ -1,34 +1,28 @@ -use ahash::HashSet; -use alloy_primitives::{Address, U256}; +use ahash::HashMap; +use alloy_primitives::U256; use alloy_rpc_types::TransactionTrait; use itertools::Itertools; use rbuilder_primitives::{ ace::{AceExchange, AceInteraction, AceUnlockType}, Order, SimulatedOrder, TransactionSignedEcRecoveredWithBlobs, }; -use serde::Deserialize; use std::sync::Arc; use tracing::trace; -use crate::{building::sim::SimulationRequest, live_builder::simulation::SimulatedOrderCommand}; +use crate::{ + building::sim::SimulationRequest, + live_builder::{config::AceConfig, simulation::SimulatedOrderCommand}, +}; /// Collects Ace Orders #[derive(Debug, Default)] pub struct AceCollector { /// ACE bundles organized by exchange exchanges: ahash::HashMap, - ace_tx_lookup: ahash::HashMap, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct AceTxData { - pub from_addresses: HashSet
, - pub to_addresses: HashSet
, - pub unlock_signatures: HashSet<[u8; 4]>, - pub force_signatures: HashSet<[u8; 4]>, + ace_tx_lookup: ahash::HashMap, } -impl AceTxData { +impl AceConfig { pub fn is_ace(&self, tx: &TransactionSignedEcRecoveredWithBlobs) -> bool { let internal = tx.internal_tx_unsecure(); self.from_addresses.contains(&internal.signer()) @@ -40,12 +34,12 @@ impl AceTxData { pub fn ace_type(&self, tx: &TransactionSignedEcRecoveredWithBlobs) -> Option { if self .force_signatures - .contains(&tx.internal_tx_unsecure().inner().input()[0..4]) + .contains(tx.internal_tx_unsecure().inner().input()) { Some(AceUnlockType::Force) } else if self .unlock_signatures - .contains(&tx.internal_tx_unsecure().inner().input()[0..4]) + .contains(tx.internal_tx_unsecure().inner().input()) { Some(AceUnlockType::Optional) } else { @@ -158,8 +152,20 @@ impl AceExchangeData { } impl AceCollector { - pub fn new() -> Self { - Self::default() + pub fn new(config: Vec) -> Self { + let mut lookup = HashMap::default(); + let mut exchanges = HashMap::default(); + + for ace in config { + let protocol = ace.protocol; + lookup.insert(protocol, ace); + exchanges.insert(protocol, Default::default()); + } + + Self { + exchanges, + ace_tx_lookup: lookup, + } } /// Add an ACE protocol transaction (Order::Ace) diff --git a/crates/rbuilder/src/live_builder/base_config.rs b/crates/rbuilder/src/live_builder/base_config.rs index 733121e74..675399ea2 100644 --- a/crates/rbuilder/src/live_builder/base_config.rs +++ b/crates/rbuilder/src/live_builder/base_config.rs @@ -219,6 +219,7 @@ impl BaseConfig { slot_source: MevBoostSlotDataGenerator, provider: P, blocklist_provider: Arc, + ace_config: Vec, ) -> eyre::Result> where P: StateProviderFactory, @@ -270,6 +271,7 @@ impl BaseConfig { simulation_use_random_coinbase: self.simulation_use_random_coinbase, faster_finalize: self.faster_finalize, order_flow_tracer_manager, + ace_config, }) } 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 14c1adb86..2db8d0195 100644 --- a/crates/rbuilder/src/live_builder/config.rs +++ b/crates/rbuilder/src/live_builder/config.rs @@ -50,6 +50,7 @@ use crate::{ utils::{build_info::rbuilder_version, ProviderFactoryReopener, Signer}, }; use alloy_chains::ChainKind; +use alloy_primitives::Bytes; use alloy_primitives::{ utils::{format_ether, parse_ether}, Address, FixedBytes, B256, U256, @@ -63,7 +64,10 @@ use ethereum_consensus::{ use eyre::Context; use lazy_static::lazy_static; use rbuilder_config::EnvOrValue; -use rbuilder_primitives::mev_boost::{MevBoostRelayID, RelayMode}; +use rbuilder_primitives::{ + ace::AceExchange, + mev_boost::{MevBoostRelayID, RelayMode}, +}; use reth_chainspec::{Chain, ChainSpec, NamedChain}; use reth_db::DatabaseEnv; use reth_node_api::NodeTypesWithDBAdapter; @@ -72,8 +76,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}, @@ -112,6 +117,15 @@ pub struct BuilderConfig { pub builder: SpecificBuilderConfig, } +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +pub struct AceConfig { + pub protocol: AceExchange, + pub from_addresses: HashSet

, + pub to_addresses: HashSet
, + pub unlock_signatures: HashSet, + pub force_signatures: HashSet, +} + #[derive(Debug, Clone, Deserialize, PartialEq, Default)] #[serde(default, deny_unknown_fields)] pub struct SubsidyConfig { @@ -132,6 +146,9 @@ pub struct Config { /// selected builder configurations pub builders: Vec, + /// Ace Configurations + pub ace_protocols: Vec, + /// When the sample bidder (see TrueBlockValueBiddingService) will start bidding. /// Usually a negative number. pub slot_delta_to_start_bidding_ms: Option, @@ -542,6 +559,7 @@ impl LiveBuilderConfig for Config { slot_info_provider, adjustment_fee_payers, cancellation_token, + self.ace_protocols.clone(), ) .await?; let builders = create_builders( @@ -735,6 +753,7 @@ impl Default for Config { }), }, ], + ace_protocols: vec![], slot_delta_to_start_bidding_ms: None, subsidy: None, subsidy_overrides: Vec::new(), @@ -1139,6 +1158,7 @@ pub async fn create_builder_from_sink

( slot_info_provider: Vec, adjustment_fee_payers: ahash::HashMap, cancellation_token: CancellationToken, + ace_config: Vec, ) -> eyre::Result> where P: StateProviderFactory, @@ -1162,6 +1182,7 @@ where payload_event, provider, blocklist_provider, + ace_config, ) .await } diff --git a/crates/rbuilder/src/live_builder/mod.rs b/crates/rbuilder/src/live_builder/mod.rs index 908dd6a18..8d6e77200 100644 --- a/crates/rbuilder/src/live_builder/mod.rs +++ b/crates/rbuilder/src/live_builder/mod.rs @@ -133,6 +133,8 @@ where pub simulation_use_random_coinbase: bool, pub order_flow_tracer_manager: Box, + + pub ace_config: Vec, } impl

LiveBuilder

@@ -200,6 +202,7 @@ where self.run_sparse_trie_prefetcher, self.sbundle_merger_selected_signers.clone(), self.order_flow_tracer_manager, + self.ace_config.clone(), ); let watchdog_sender = match self.watchdog_timeout { diff --git a/crates/rbuilder/src/live_builder/simulation/mod.rs b/crates/rbuilder/src/live_builder/simulation/mod.rs index 3132cf6b5..41eca1e7b 100644 --- a/crates/rbuilder/src/live_builder/simulation/mod.rs +++ b/crates/rbuilder/src/live_builder/simulation/mod.rs @@ -117,6 +117,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); @@ -174,6 +175,7 @@ where slot_sim_results_sender, sim_tree, sim_tracer, + ace_config, ); simulation_job.run().await; @@ -236,6 +238,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/simulation_job.rs b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs index 7465a2819..d59049f6b 100644 --- a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs +++ b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs @@ -1,4 +1,4 @@ -use crate::building::ace_bundler::AceBundler; +use crate::building::ace_collector::AceCollector; use std::{fmt, sync::Arc}; use crate::{ @@ -39,7 +39,7 @@ pub struct SimulationJob { /// Output of the simulations slot_sim_results_sender: mpsc::Sender, sim_tree: SimTree, - ace_bundler: AceBundler, + ace_bundler: AceCollector, orders_received: OrderCounter, orders_simulated_ok: OrderCounter, @@ -78,9 +78,10 @@ impl SimulationJob { slot_sim_results_sender: mpsc::Sender, sim_tree: SimTree, sim_tracer: Arc, + ace_config: Vec, ) -> Self { Self { - ace_bundler: AceBundler::new(), + ace_bundler: AceCollector::new(ace_config), block_cancellation, new_order_sub, sim_req_sender, diff --git a/examples/config/rbuilder/config-live-example.toml b/examples/config/rbuilder/config-live-example.toml index 3fbc2cef5..df39b2f5b 100644 --- a/examples/config/rbuilder/config-live-example.toml +++ b/examples/config/rbuilder/config-live-example.toml @@ -39,7 +39,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 @@ -58,7 +58,6 @@ mode = "full" max_bid_eth = "0.05" - [[builders]] name = "mgp-ordering" algo = "ordering-builder" @@ -82,6 +81,19 @@ discard_txs = true num_threads = 25 safe_sorting_only = false +[[ace_protocols]] +protocol = "Angstrom" +from_addresses = [ + "0xc41ae140ca9b281d8a1dc254c50e446019517d04", + "0xd437f3372f3add2c2bc3245e6bd6f9c202e61bb3", + "0x693ca5c6852a7d212dabc98b28e15257465c11f3", +] +to_addresses = ["0x0000000aa232009084Bd71A5797d089AA4Edfad4"] +# unlockWithEmptyAttestation(address,bytes) nonpayable +unlock_signatures = ["0x1828e0e7"] +# execute(bytes) nonpayable +force_signatures = ["0x09c5eabe"] + [[relay_bid_scrapers]] type = "ultrasound-ws" name = "ultrasound-ws-eu" @@ -93,4 +105,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" - From 4dfdb8a5df979155b6f6adaad17282ee7c3d8840 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Mon, 10 Nov 2025 17:42:09 -0500 Subject: [PATCH 11/51] feat: just missing new detection --- crates/rbuilder-primitives/src/lib.rs | 1 + crates/rbuilder/src/bin/run-bundle-on-prefix.rs | 2 ++ .../building/block_orders/share_bundle_merger.rs | 1 + .../building/block_orders/test_data_generator.rs | 1 + .../src/building/builders/ordering_builder.rs | 4 ++-- .../block_building_result_assembler.rs | 14 +++++--------- crates/rbuilder/src/building/sim.rs | 2 ++ crates/rbuilder/src/live_builder/base_config.rs | 7 +++++-- crates/rbuilder/src/live_builder/config.rs | 7 ------- .../src/live_builder/order_input/rpc_server.rs | 1 - .../src/live_builder/simulation/simulation_job.rs | 2 +- 11 files changed, 20 insertions(+), 22 deletions(-) diff --git a/crates/rbuilder-primitives/src/lib.rs b/crates/rbuilder-primitives/src/lib.rs index a25c01d88..c5ac852e7 100644 --- a/crates/rbuilder-primitives/src/lib.rs +++ b/crates/rbuilder-primitives/src/lib.rs @@ -1450,6 +1450,7 @@ pub struct SimulatedOrder { pub sim_value: SimValue, /// Info about read/write slots during the simulation to help figure out what the Order is doing. pub used_state_trace: Option, + pub is_ace: bool, pub ace_interaction: Option, } diff --git a/crates/rbuilder/src/bin/run-bundle-on-prefix.rs b/crates/rbuilder/src/bin/run-bundle-on-prefix.rs index 2679647fe..1e430c55d 100644 --- a/crates/rbuilder/src/bin/run-bundle-on-prefix.rs +++ b/crates/rbuilder/src/bin/run-bundle-on-prefix.rs @@ -220,6 +220,7 @@ async fn main() -> eyre::Result<()> { order, sim_value: Default::default(), used_state_trace: Default::default(), + is_ace: false, ace_interaction: None, }; let res = builder.commit_order(&mut block_info.local_ctx, &sim_order, &|_| Ok(()))?; @@ -317,6 +318,7 @@ fn execute_orders_on_tob( sim_value: Default::default(), used_state_trace: Default::default(), ace_interaction: None, + is_ace: false, }; let res = builder.commit_order(&mut block_info.local_ctx, &sim_order, &|_| Ok(()))?; let profit = res diff --git a/crates/rbuilder/src/building/block_orders/share_bundle_merger.rs b/crates/rbuilder/src/building/block_orders/share_bundle_merger.rs index ef37ec1c5..58887b4a2 100644 --- a/crates/rbuilder/src/building/block_orders/share_bundle_merger.rs +++ b/crates/rbuilder/src/building/block_orders/share_bundle_merger.rs @@ -149,6 +149,7 @@ impl MultiBackrunManager { sim_value: highest_payback_order.sim_order.sim_value.clone(), used_state_trace: highest_payback_order.sim_order.used_state_trace.clone(), ace_interaction: highest_payback_order.sim_order.ace_interaction, + is_ace: highest_payback_order.sim_order.is_ace, })) } diff --git a/crates/rbuilder/src/building/block_orders/test_data_generator.rs b/crates/rbuilder/src/building/block_orders/test_data_generator.rs index f2a8fd4a7..281c8ff55 100644 --- a/crates/rbuilder/src/building/block_orders/test_data_generator.rs +++ b/crates/rbuilder/src/building/block_orders/test_data_generator.rs @@ -32,6 +32,7 @@ impl TestDataGenerator { sim_value, used_state_trace: None, ace_interaction: None, + is_ace: false, }) } } diff --git a/crates/rbuilder/src/building/builders/ordering_builder.rs b/crates/rbuilder/src/building/builders/ordering_builder.rs index 577612f4c..ab91929ec 100644 --- a/crates/rbuilder/src/building/builders/ordering_builder.rs +++ b/crates/rbuilder/src/building/builders/ordering_builder.rs @@ -27,7 +27,7 @@ use crate::{ use ahash::{HashMap, HashSet}; use alloy_primitives::I256; use derivative::Derivative; -use rbuilder_primitives::{AccountNonce, Order, OrderId, SimValue, SimulatedOrder}; +use rbuilder_primitives::{AccountNonce, OrderId, SimValue, SimulatedOrder}; use reth_provider::StateProvider; use serde::Deserialize; use std::{ @@ -287,7 +287,7 @@ impl OrderingBuilderContext { let all_orders = block_orders.get_all_orders(); let mut ace_txs = Vec::new(); for order in all_orders { - if matches!(order.order, Order::AceTx(_)) { + if order.is_ace { ace_txs.push(order.clone()); // Remove from block_orders so they don't get processed in fill_orders block_orders.remove_order(order.id()); diff --git a/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs b/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs index 301d9738f..bf9331087 100644 --- a/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs +++ b/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs @@ -27,7 +27,7 @@ use crate::{ telemetry::mark_builder_considers_order, utils::elapsed_ms, }; -use rbuilder_primitives::{order_statistics::OrderStatistics, Order}; +use rbuilder_primitives::order_statistics::OrderStatistics; /// Assembles block building results from the best orderings of order groups. pub struct BlockBuildingResultAssembler { @@ -191,7 +191,7 @@ impl BlockBuildingResultAssembler { let mut ace_txs = Vec::new(); for (_, group) in best_orderings_per_group.iter() { for order in group.orders.iter() { - if matches!(order.order, Order::AceTx(_)) { + if order.is_ace { ace_txs.push(order.clone()); } } @@ -202,9 +202,7 @@ impl BlockBuildingResultAssembler { // Filter out ACE orders from the sequence resolution_result .sequence_of_orders - .retain(|(order_idx, _)| { - !matches!(group.orders[*order_idx].order, Order::AceTx(_)) - }); + .retain(|(order_idx, _)| !group.orders[*order_idx].is_ace); } let mut block_building_helper = BlockBuildingHelperFromProvider::new( @@ -302,7 +300,7 @@ impl BlockBuildingResultAssembler { let mut ace_txs = Vec::new(); for (_, group) in best_orderings_per_group.iter() { for order in group.orders.iter() { - if matches!(order.order, Order::AceTx(_)) { + if order.is_ace { ace_txs.push(order.clone()); } } @@ -313,9 +311,7 @@ impl BlockBuildingResultAssembler { // Filter out ACE orders from the sequence resolution_result .sequence_of_orders - .retain(|(order_idx, _)| { - !matches!(group.orders[*order_idx].order, Order::AceTx(_)) - }); + .retain(|(order_idx, _)| !group.orders[*order_idx].is_ace); } let mut block_building_helper = BlockBuildingHelperFromProvider::new( diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index 7fab560fc..a48a85604 100644 --- a/crates/rbuilder/src/building/sim.rs +++ b/crates/rbuilder/src/building/sim.rs @@ -464,6 +464,7 @@ pub fn simulate_order( BlockSpace::new(tracer.used_gas, 0, 0), Vec::new(), ), + is_ace: false, used_state_trace: Some(tracer.used_state_trace.clone()), ace_interaction: Some(interaction), }), @@ -534,6 +535,7 @@ pub fn simulate_order_using_fork( sim_value, used_state_trace: res.used_state_trace, ace_interaction: None, + is_ace: false, }), new_nonces, )) diff --git a/crates/rbuilder/src/live_builder/base_config.rs b/crates/rbuilder/src/live_builder/base_config.rs index 675399ea2..db22f53cf 100644 --- a/crates/rbuilder/src/live_builder/base_config.rs +++ b/crates/rbuilder/src/live_builder/base_config.rs @@ -170,6 +170,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 { @@ -219,7 +222,6 @@ impl BaseConfig { slot_source: MevBoostSlotDataGenerator, provider: P, blocklist_provider: Arc, - ace_config: Vec, ) -> eyre::Result> where P: StateProviderFactory, @@ -271,7 +273,7 @@ impl BaseConfig { simulation_use_random_coinbase: self.simulation_use_random_coinbase, faster_finalize: self.faster_finalize, order_flow_tracer_manager, - ace_config, + ace_config: self.ace_protocols.clone(), }) } @@ -489,6 +491,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/config.rs b/crates/rbuilder/src/live_builder/config.rs index 2db8d0195..4c6d9d01b 100644 --- a/crates/rbuilder/src/live_builder/config.rs +++ b/crates/rbuilder/src/live_builder/config.rs @@ -146,9 +146,6 @@ pub struct Config { /// selected builder configurations pub builders: Vec, - /// Ace Configurations - pub ace_protocols: Vec, - /// When the sample bidder (see TrueBlockValueBiddingService) will start bidding. /// Usually a negative number. pub slot_delta_to_start_bidding_ms: Option, @@ -559,7 +556,6 @@ impl LiveBuilderConfig for Config { slot_info_provider, adjustment_fee_payers, cancellation_token, - self.ace_protocols.clone(), ) .await?; let builders = create_builders( @@ -753,7 +749,6 @@ impl Default for Config { }), }, ], - ace_protocols: vec![], slot_delta_to_start_bidding_ms: None, subsidy: None, subsidy_overrides: Vec::new(), @@ -1158,7 +1153,6 @@ pub async fn create_builder_from_sink

( slot_info_provider: Vec, adjustment_fee_payers: ahash::HashMap, cancellation_token: CancellationToken, - ace_config: Vec, ) -> eyre::Result> where P: StateProviderFactory, @@ -1182,7 +1176,6 @@ where payload_event, provider, blocklist_provider, - ace_config, ) .await } diff --git a/crates/rbuilder/src/live_builder/order_input/rpc_server.rs b/crates/rbuilder/src/live_builder/order_input/rpc_server.rs index 856854a03..b58e0aeaa 100644 --- a/crates/rbuilder/src/live_builder/order_input/rpc_server.rs +++ b/crates/rbuilder/src/live_builder/order_input/rpc_server.rs @@ -144,7 +144,6 @@ pub async fn start_server_accepting_bundles( Ok(hash) } })?; - let results_clone = results.clone(); module.merge(extra_rpc)?; let handle = server.start(module); diff --git a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs index d59049f6b..0fb0e22b8 100644 --- a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs +++ b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs @@ -10,7 +10,7 @@ use crate::{ }; use ahash::HashSet; use alloy_primitives::utils::format_ether; -use rbuilder_primitives::{ace::AceUnlockType, Order, OrderId, OrderReplacementKey}; +use rbuilder_primitives::{Order, OrderId, OrderReplacementKey}; use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; use tracing::{debug, error, info, trace, warn}; From c461a8ab1c8267a65e1e4a5310eed844c803435f Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 11 Nov 2025 15:36:40 -0800 Subject: [PATCH 12/51] feat: add new detection --- crates/rbuilder/src/building/ace_collector.rs | 11 +++- crates/rbuilder/src/building/order_commit.rs | 43 -------------- crates/rbuilder/src/building/sim.rs | 6 +- .../live_builder/simulation/simulation_job.rs | 56 ++++++++----------- 4 files changed, 37 insertions(+), 79 deletions(-) diff --git a/crates/rbuilder/src/building/ace_collector.rs b/crates/rbuilder/src/building/ace_collector.rs index 99bdb7e2e..8e5dd117a 100644 --- a/crates/rbuilder/src/building/ace_collector.rs +++ b/crates/rbuilder/src/building/ace_collector.rs @@ -168,7 +168,16 @@ impl AceCollector { } } - /// Add an ACE protocol transaction (Order::Ace) + pub fn is_ace(&self, order: &Order) -> bool { + match order { + Order::Tx(tx) => self + .ace_tx_lookup + .values() + .any(|config| config.is_ace(&tx.tx_with_blobs)), + _ => false, + } + } + pub fn add_ace_protocol_tx( &mut self, simulated: Arc, diff --git a/crates/rbuilder/src/building/order_commit.rs b/crates/rbuilder/src/building/order_commit.rs index b0b562d83..7bb4ac814 100644 --- a/crates/rbuilder/src/building/order_commit.rs +++ b/crates/rbuilder/src/building/order_commit.rs @@ -16,7 +16,6 @@ use alloy_primitives::{Address, B256, I256, U256}; use alloy_rlp::Encodable; use itertools::Itertools; use rbuilder_primitives::ace::AceExchange; -use rbuilder_primitives::AceTx; use rbuilder_primitives::{ evm_inspector::{RBuilderEVMInspector, UsedStateTrace}, BlockSpace, Bundle, Order, OrderId, RefundConfig, ShareBundle, ShareBundleBody, @@ -855,48 +854,6 @@ impl< Ok(Ok(())) } - fn commit_ace_no_rollback( - &mut self, - ace_tx: &AceTx, - space_state: BlockBuildingSpaceState, - ) -> Result, CriticalCommitOrderError> { - match ace_tx { - AceTx::Angstrom(angstrom_tx) => { - let tx_hash = angstrom_tx.tx.hash(); - - // Use the constant Angstrom exchange address - let exchange = AceExchange::angstrom(); - - // Commit the ACE transaction - no rollback for ACE - let result = self.commit_tx(&angstrom_tx.tx, space_state)?; - - match result { - Ok(res) => { - // Check if the transaction reverted - if !res.tx_info.receipt.success { - // Reject reverted ACE transactions - return Ok(Err(BundleErr::TransactionReverted(tx_hash))); - } - - Ok(Ok(AceOk { - space_used: res.space_used(), - cumulative_space_used: res.cumulative_space_used, - tx_info: res.tx_info, - nonces_updated: vec![res.nonce_updated], - reverted: false, - exchange, - })) - } - Err(err) => { - // ACE transactions must not fail at the EVM level - // These are critical errors that prevent the bundle - Ok(Err(BundleErr::InvalidTransaction(tx_hash, err))) - } - } - } - } - } - fn commit_bundle_no_rollback( &mut self, bundle: &Bundle, diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index a48a85604..5e7447850 100644 --- a/crates/rbuilder/src/building/sim.rs +++ b/crates/rbuilder/src/building/sim.rs @@ -88,7 +88,7 @@ pub struct SimTree { pending_orders: HashMap, pending_nonces: HashMap>, - pub(crate) ready_orders: Vec, + ready_orders: Vec, } #[derive(Debug)] @@ -110,6 +110,10 @@ impl SimTree { } } + pub fn requeue_ace_order(&mut self, req: SimulationRequest) { + self.ready_orders.push(req); + } + fn push_order(&mut self, order: Order) -> Result<(), ProviderError> { if self.pending_orders.contains_key(&order.id()) { return Ok(()); diff --git a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs index 0fb0e22b8..41cf93d0f 100644 --- a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs +++ b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs @@ -188,38 +188,24 @@ impl SimulationJob { } /// Returns weather or not to continue the processing of this tx. - fn handle_ace_tx(&mut self, res: &SimulatedResult) -> bool { + fn handle_ace_tx(&mut self, mut res: SimulatedResult) -> Option { // this means that we have frontran this with an ace unlocking tx in the simulator. // We cannot do anything else at this point so we yield to the default flow. if !res.previous_orders.is_empty() { - return true; + return Some(res); } - // check to see if this is a ace specific tx. - - // if let Order::AceTx(ref ace) = res.simulated_order.order { - // let unlock_type = ace.ace_unlock_type(); - // let exchange = ace.exchange(); - // - // for sim_order in self.ace_bundler.add_ace_protocol_tx( - // res.simulated_order.clone(), - // unlock_type, - // exchange, - // ) { - // self.sim_tree.ready_orders.push(sim_order); - // } - // - // // If its a force, we pass through. If its a optional, we only want to have it be - // // inlcuded if we don't have an unlocking tx. - // return match unlock_type { - // AceUnlockType::Force => true, - // AceUnlockType::Optional => !self.ace_bundler.has_unlocking(&exchange), - // }; - // } + let is_ace = if self.ace_bundler.is_ace(&res.simulated_order.order) { + Arc::make_mut(&mut res.simulated_order).is_ace = true; + // assert that this order is fully correct. + true + } else { + false + }; // we need to know if this ace tx has already been simulated or not. let ace_interaction = res.simulated_order.ace_interaction.unwrap(); - if ace_interaction.is_unlocking() { + if is_ace { if let Some(cmd) = self .ace_bundler .have_unlocking(ace_interaction.get_exchange()) @@ -227,15 +213,15 @@ impl SimulationJob { let _ = self.slot_sim_results_sender.try_send(cmd); } - return true; + return Some(res); } else if let Some(order) = self.ace_bundler.add_mempool_ace_tx( res.simulated_order.clone(), res.simulated_order.ace_interaction.unwrap(), ) { - self.sim_tree.ready_orders.push(order); + self.sim_tree.requeue_ace_order(order); } - false + None } /// updates the sim_tree and notifies new orders @@ -259,12 +245,14 @@ impl SimulationJob { self.orders_with_replacement_key_sim_ok += 1; } - // first we need to check if this interacted with a ace tx and if so what type. - if sim_result.simulated_order.ace_interaction.is_some() - && !self.handle_ace_tx(sim_result) - { - continue; - } + let sim_result = if sim_result.simulated_order.ace_interaction.is_some() { + let Some(unlocking_ace) = self.handle_ace_tx(sim_result.clone()) else { + continue; + }; + unlocking_ace + } else { + sim_result.clone() + }; // Skip cancelled orders and remove from in_flight_orders if self @@ -287,7 +275,7 @@ impl SimulationJob { { return false; //receiver closed :( } else { - self.sim_tracer.update_simulation_sent(sim_result); + self.sim_tracer.update_simulation_sent(&sim_result); } } } From 330a808de092d59e0def5dcb42d132aa32c3c5e0 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 25 Nov 2025 11:13:02 -0500 Subject: [PATCH 13/51] fix: wiring + linting --- crates/rbuilder/src/building/ace_collector.rs | 30 +++++-------------- .../building/block_orders/order_priority.rs | 1 + .../src/building/block_orders/test_context.rs | 2 +- .../parallel_builder/conflict_resolvers.rs | 1 + .../conflict_task_generator.rs | 1 + .../builders/parallel_builder/groups.rs | 1 + crates/rbuilder/src/building/order_commit.rs | 14 --------- .../building/testing/bundle_tests/setup.rs | 1 + .../live_builder/simulation/simulation_job.rs | 23 ++++++++++++-- 9 files changed, 34 insertions(+), 40 deletions(-) diff --git a/crates/rbuilder/src/building/ace_collector.rs b/crates/rbuilder/src/building/ace_collector.rs index 8e5dd117a..507a270ad 100644 --- a/crates/rbuilder/src/building/ace_collector.rs +++ b/crates/rbuilder/src/building/ace_collector.rs @@ -23,29 +23,13 @@ pub struct AceCollector { } impl AceConfig { - pub fn is_ace(&self, tx: &TransactionSignedEcRecoveredWithBlobs) -> bool { + pub fn is_ace_force(&self, tx: &TransactionSignedEcRecoveredWithBlobs) -> bool { let internal = tx.internal_tx_unsecure(); self.from_addresses.contains(&internal.signer()) && self .to_addresses .contains(&internal.inner().to().unwrap_or_default()) } - - pub fn ace_type(&self, tx: &TransactionSignedEcRecoveredWithBlobs) -> Option { - if self - .force_signatures - .contains(tx.internal_tx_unsecure().inner().input()) - { - Some(AceUnlockType::Force) - } else if self - .unlock_signatures - .contains(tx.internal_tx_unsecure().inner().input()) - { - Some(AceUnlockType::Optional) - } else { - None - } - } } /// Data for a specific ACE exchange including all transaction types and logic @@ -70,7 +54,7 @@ pub struct AceOrderEntry { impl AceExchangeData { /// Add an ACE protocol transaction - pub fn add_ace_protocol_tx( + fn add_ace_protocol_tx( &mut self, simulated: Arc, unlock_type: AceUnlockType, @@ -105,7 +89,7 @@ impl AceExchangeData { .collect_vec() } - pub fn try_generate_sim_request(&self, order: &Order) -> Option { + fn try_generate_sim_request(&self, order: &Order) -> Option { let parent = self .optional_ace_tx .as_ref() @@ -120,7 +104,7 @@ impl AceExchangeData { // If we have a regular mempool unlocking tx, we don't want to include the optional ace // transaction ad will cancel it. - pub fn has_unlocking(&mut self) -> Option { + fn has_unlocking(&mut self) -> Option { // we only want to send this once. if self.has_unlocking { return None; @@ -133,7 +117,7 @@ impl AceExchangeData { .map(|order| SimulatedOrderCommand::Cancellation(order.simulated.order.id())) } - pub fn add_mempool_tx(&mut self, simulated: Arc) -> Option { + fn add_mempool_tx(&mut self, simulated: Arc) -> Option { if let Some(req) = self.try_generate_sim_request(&simulated.order) { return Some(req); } @@ -168,12 +152,12 @@ impl AceCollector { } } - pub fn is_ace(&self, order: &Order) -> bool { + pub fn is_ace_force(&self, order: &Order) -> bool { match order { Order::Tx(tx) => self .ace_tx_lookup .values() - .any(|config| config.is_ace(&tx.tx_with_blobs)), + .any(|config| config.is_ace_force(&tx.tx_with_blobs)), _ => false, } } diff --git a/crates/rbuilder/src/building/block_orders/order_priority.rs b/crates/rbuilder/src/building/block_orders/order_priority.rs index 9c7ddeaf5..dabf9c312 100644 --- a/crates/rbuilder/src/building/block_orders/order_priority.rs +++ b/crates/rbuilder/src/building/block_orders/order_priority.rs @@ -332,6 +332,7 @@ mod test { U256::from(non_mempool_profit), gas, ), + is_ace: false, ace_interaction: None, used_state_trace: None, }) diff --git a/crates/rbuilder/src/building/block_orders/test_context.rs b/crates/rbuilder/src/building/block_orders/test_context.rs index 0a7294ac3..3e20ff7e6 100644 --- a/crates/rbuilder/src/building/block_orders/test_context.rs +++ b/crates/rbuilder/src/building/block_orders/test_context.rs @@ -168,6 +168,7 @@ impl TestContext { Arc::new(SimulatedOrder { order, sim_value, + is_ace: false, used_state_trace: None, ace_interaction: None, }) @@ -214,7 +215,6 @@ impl TestContext { Order::Bundle(_) => panic!("Order::Bundle expecting ShareBundle"), Order::Tx(_) => panic!("Order::Tx expecting ShareBundle"), Order::ShareBundle(sb) => sb, - Order::AceTx(_) => panic!("Order::AceTx expecting ShareBundle"), } } diff --git a/crates/rbuilder/src/building/builders/parallel_builder/conflict_resolvers.rs b/crates/rbuilder/src/building/builders/parallel_builder/conflict_resolvers.rs index 996d1a6c0..6cec39ed0 100644 --- a/crates/rbuilder/src/building/builders/parallel_builder/conflict_resolvers.rs +++ b/crates/rbuilder/src/building/builders/parallel_builder/conflict_resolvers.rs @@ -532,6 +532,7 @@ mod tests { Arc::new(SimulatedOrder { order: Order::Bundle(bundle), used_state_trace: None, + is_ace: false, sim_value, ace_interaction: None, }) diff --git a/crates/rbuilder/src/building/builders/parallel_builder/conflict_task_generator.rs b/crates/rbuilder/src/building/builders/parallel_builder/conflict_task_generator.rs index 381b634d9..d05f527e2 100644 --- a/crates/rbuilder/src/building/builders/parallel_builder/conflict_task_generator.rs +++ b/crates/rbuilder/src/building/builders/parallel_builder/conflict_task_generator.rs @@ -496,6 +496,7 @@ mod tests { }), sim_value, used_state_trace: Some(trace), + is_ace: false, ace_interaction: None, }) } diff --git a/crates/rbuilder/src/building/builders/parallel_builder/groups.rs b/crates/rbuilder/src/building/builders/parallel_builder/groups.rs index be892f41a..0445ab999 100644 --- a/crates/rbuilder/src/building/builders/parallel_builder/groups.rs +++ b/crates/rbuilder/src/building/builders/parallel_builder/groups.rs @@ -479,6 +479,7 @@ mod tests { }), used_state_trace: Some(trace), sim_value: SimValue::default(), + is_ace: false, ace_interaction: None, }) } diff --git a/crates/rbuilder/src/building/order_commit.rs b/crates/rbuilder/src/building/order_commit.rs index 7bb4ac814..c45ab67a5 100644 --- a/crates/rbuilder/src/building/order_commit.rs +++ b/crates/rbuilder/src/building/order_commit.rs @@ -15,7 +15,6 @@ use alloy_evm::Database; use alloy_primitives::{Address, B256, I256, U256}; use alloy_rlp::Encodable; use itertools::Itertools; -use rbuilder_primitives::ace::AceExchange; use rbuilder_primitives::{ evm_inspector::{RBuilderEVMInspector, UsedStateTrace}, BlockSpace, Bundle, Order, OrderId, RefundConfig, ShareBundle, ShareBundleBody, @@ -281,19 +280,6 @@ impl BundleOk { } } -/// Result of successfully executing an ACE transaction -#[derive(Debug, Clone)] -pub struct AceOk { - pub space_used: BlockSpace, - pub cumulative_space_used: BlockSpace, - pub tx_info: TransactionExecutionInfo, - pub nonces_updated: Vec<(Address, u64)>, - /// Whether the ACE transaction reverted (but is still included) - pub reverted: bool, - /// The ACE exchange this transaction interacted with - pub exchange: AceExchange, -} - #[derive(Error, Debug, PartialEq, Eq)] pub enum BundleErr { #[error("Invalid transaction, hash: {0:?}, err: {1}")] diff --git a/crates/rbuilder/src/building/testing/bundle_tests/setup.rs b/crates/rbuilder/src/building/testing/bundle_tests/setup.rs index 3f9afbdd1..c7d69391c 100644 --- a/crates/rbuilder/src/building/testing/bundle_tests/setup.rs +++ b/crates/rbuilder/src/building/testing/bundle_tests/setup.rs @@ -228,6 +228,7 @@ impl TestSetup { order: self.order_builder.build_order(), sim_value: Default::default(), used_state_trace: Default::default(), + is_ace: false, ace_interaction: None, }; diff --git a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs index 41cf93d0f..7b528b558 100644 --- a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs +++ b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs @@ -70,6 +70,7 @@ pub struct SimulationJob { } impl SimulationJob { + #[allow(clippy::too_many_arguments)] pub fn new( block_cancellation: CancellationToken, new_order_sub: mpsc::UnboundedReceiver, @@ -195,9 +196,28 @@ impl SimulationJob { return Some(res); } - let is_ace = if self.ace_bundler.is_ace(&res.simulated_order.order) { + let is_ace = if self.ace_bundler.is_ace_force(&res.simulated_order.order) { Arc::make_mut(&mut res.simulated_order).is_ace = true; + + // Is a force tx given that it is being sent directly to the ace protocol tx + // but isn't reverting. + self.ace_bundler.add_ace_protocol_tx( + res.simulated_order.clone(), + rbuilder_primitives::ace::AceUnlockType::Force, + res.simulated_order.ace_interaction.unwrap().get_exchange(), + ); + // assert that this order is fully correct. + true + } else if let Some(ace) = res.simulated_order.ace_interaction { + if ace.is_unlocking() { + self.ace_bundler.add_ace_protocol_tx( + res.simulated_order.clone(), + rbuilder_primitives::ace::AceUnlockType::Optional, + res.simulated_order.ace_interaction.unwrap().get_exchange(), + ); + } + true } else { false @@ -212,7 +232,6 @@ impl SimulationJob { { let _ = self.slot_sim_results_sender.try_send(cmd); } - return Some(res); } else if let Some(order) = self.ace_bundler.add_mempool_ace_tx( res.simulated_order.clone(), From 34a7b595d01a920d0c42c0d5aa0d8a2231afc87f Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 25 Nov 2025 11:18:24 -0500 Subject: [PATCH 14/51] feat: more cleanup of old code --- crates/rbuilder-primitives/src/ace.rs | 14 +---- crates/rbuilder-primitives/src/lib.rs | 90 +-------------------------- 2 files changed, 2 insertions(+), 102 deletions(-) diff --git a/crates/rbuilder-primitives/src/ace.rs b/crates/rbuilder-primitives/src/ace.rs index 9c23ec304..3f868c54c 100644 --- a/crates/rbuilder-primitives/src/ace.rs +++ b/crates/rbuilder-primitives/src/ace.rs @@ -11,25 +11,13 @@ pub enum AceExchange { } impl AceExchange { - /// Get the Angstrom variant - pub const fn angstrom() -> Self { - Self::Angstrom - } - /// Get the address for this exchange - pub fn address(&self) -> Address { + fn address(&self) -> Address { match self { AceExchange::Angstrom => address!("0000000aa232009084Bd71A5797d089AA4Edfad4"), } } - /// Get the number of blocks this ACE exchange's transactions should be valid for - pub fn blocks_to_live(&self) -> u64 { - match self { - AceExchange::Angstrom => 1, - } - } - /// Classify an ACE transaction interaction type based on state trace and simulation success pub fn classify_ace_interaction( &self, diff --git a/crates/rbuilder-primitives/src/lib.rs b/crates/rbuilder-primitives/src/lib.rs index c5ac852e7..303910be7 100644 --- a/crates/rbuilder-primitives/src/lib.rs +++ b/crates/rbuilder-primitives/src/lib.rs @@ -41,10 +41,7 @@ pub use test_data_generator::TestDataGenerator; use thiserror::Error; use uuid::Uuid; -use crate::{ - ace::{AceExchange, AceInteraction, AceUnlockType}, - serialize::TxEncoding, -}; +use crate::{ace::AceInteraction, serialize::TxEncoding}; /// Extra metadata for an order. #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -1059,91 +1056,6 @@ impl InMemorySize for MempoolTx { } } -/// The application that is being executed. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum AceTx { - Angstrom(AngstromTx), -} - -impl AceTx { - pub fn target_block(&self) -> Option { - match self { - Self::Angstrom(_) => None, - } - } - pub fn metadata(&self) -> &Metadata { - match self { - Self::Angstrom(ang) => &ang.meta, - } - } - - pub fn list_txs_len(&self) -> usize { - match self { - Self::Angstrom(_) => 1, - } - } - - pub fn nonces(&self) -> Vec { - match self { - Self::Angstrom(ang) => { - vec![Nonce { - nonce: ang.tx.nonce(), - address: ang.tx.signer(), - optional: false, - }] - } - } - } - - pub fn can_execute_with_block_base_fee(&self, base_fee: u128) -> bool { - match self { - Self::Angstrom(ang) => ang.tx.as_ref().max_fee_per_gas() >= base_fee, - } - } - - pub fn list_txs_revert( - &self, - ) -> Vec<(&TransactionSignedEcRecoveredWithBlobs, TxRevertBehavior)> { - match self { - Self::Angstrom(ang) => vec![(&ang.tx, TxRevertBehavior::NotAllowed)], - } - } - - pub fn order_id(&self) -> B256 { - match self { - Self::Angstrom(ang) => ang.tx.hash(), - } - } - - pub fn list_txs(&self) -> Vec<(&TransactionSignedEcRecoveredWithBlobs, bool)> { - match self { - Self::Angstrom(ang) => vec![(&ang.tx, false)], - } - } - - pub fn ace_unlock_type(&self) -> AceUnlockType { - match self { - AceTx::Angstrom(ang) => ang.unlock_type, - } - } - - pub fn exchange(&self) -> AceExchange { - match self { - AceTx::Angstrom(_) => AceExchange::Angstrom, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct AngstromTx { - pub tx: TransactionSignedEcRecoveredWithBlobs, - pub meta: Metadata, - pub unlock_data: Bytes, - pub max_priority_fee_per_gas: u128, - /// Whether this is a forced unlock or optional - pub unlock_type: ace::AceUnlockType, -} - /// Main type used for block building, we build blocks as sequences of Orders #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Order { From 58cc5614aaa8dec43128b84a29fa7dc6497bbc42 Mon Sep 17 00:00:00 2001 From: Daniel Xifra Date: Wed, 26 Nov 2025 16:27:19 -0300 Subject: [PATCH 15/51] removed unused error --- crates/rbuilder-primitives/src/serialize.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/rbuilder-primitives/src/serialize.rs b/crates/rbuilder-primitives/src/serialize.rs index 1a68a8a36..474e2a5bc 100644 --- a/crates/rbuilder-primitives/src/serialize.rs +++ b/crates/rbuilder-primitives/src/serialize.rs @@ -843,8 +843,6 @@ pub enum RawOrderConvertError { FailedToDecodeShareBundle(RawShareBundleConvertError), #[error("Blobs not supported by RawOrder")] BlobsNotSupported, - #[error("{0}")] - UnsupportedOrderType(String), } impl RawOrder { From a6daa3a15e9e7539251b8aa77b87e9a7d1f65ef8 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Mon, 1 Dec 2025 16:35:31 -0500 Subject: [PATCH 16/51] wip: refactor to new model discussed --- crates/rbuilder-primitives/src/ace.rs | 127 +++-- crates/rbuilder-primitives/src/lib.rs | 4 +- crates/rbuilder/src/backtest/execute.rs | 2 +- .../rbuilder/src/bin/run-bundle-on-prefix.rs | 2 - crates/rbuilder/src/building/ace_collector.rs | 208 -------- .../building/block_orders/order_priority.rs | 1 - .../block_orders/share_bundle_merger.rs | 1 - .../src/building/block_orders/test_context.rs | 1 - .../block_orders/test_data_generator.rs | 1 - .../src/building/builders/ordering_builder.rs | 20 +- .../block_building_result_assembler.rs | 54 ++- .../parallel_builder/conflict_resolvers.rs | 1 - .../conflict_task_generator.rs | 1 - .../builders/parallel_builder/groups.rs | 1 - crates/rbuilder/src/building/mod.rs | 1 - crates/rbuilder/src/building/sim.rs | 458 ++++++++++++++---- .../building/testing/bundle_tests/setup.rs | 1 - crates/rbuilder/src/live_builder/config.rs | 10 +- .../src/live_builder/simulation/mod.rs | 14 +- .../src/live_builder/simulation/sim_worker.rs | 23 +- .../live_builder/simulation/simulation_job.rs | 96 +--- 21 files changed, 541 insertions(+), 486 deletions(-) delete mode 100644 crates/rbuilder/src/building/ace_collector.rs diff --git a/crates/rbuilder-primitives/src/ace.rs b/crates/rbuilder-primitives/src/ace.rs index 3f868c54c..149e09707 100644 --- a/crates/rbuilder-primitives/src/ace.rs +++ b/crates/rbuilder-primitives/src/ace.rs @@ -1,72 +1,116 @@ use crate::evm_inspector::UsedStateTrace; -use alloy_primitives::{address, Address}; +use alloy_primitives::{Address, Bytes}; use derive_more::FromStr; use serde::Deserialize; +use std::collections::HashSet; use strum::EnumIter; -/// What ace based exchanges that rbuilder supports. +/// Configuration for an ACE (Atomic Clearing Engine) protocol +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +pub struct AceConfig { + /// Whether this ACE config is enabled + #[serde(default = "default_enabled")] + pub enabled: bool, + /// Which ACE protocol this config is for + pub protocol: AceExchange, + /// Addresses that send ACE orders (used to identify force unlocks) + pub from_addresses: HashSet

, + /// Addresses that receive ACE orders (the ACE contract addresses) + pub to_addresses: HashSet
, + /// Function signatures that indicate an unlock operation + pub unlock_signatures: HashSet, + /// Function signatures that indicate a forced unlock operation + pub force_signatures: HashSet, +} + +fn default_enabled() -> bool { + true +} + +/// What ACE based exchanges that rbuilder supports. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter, Deserialize, FromStr)] pub enum AceExchange { Angstrom, } impl AceExchange { - /// Get the address for this exchange - fn address(&self) -> Address { - match self { - AceExchange::Angstrom => address!("0000000aa232009084Bd71A5797d089AA4Edfad4"), - } - } - - /// Classify an ACE transaction interaction type based on state trace and simulation success + /// Classify an ACE order interaction type based on state trace, simulation success, and config. + /// Uses both state trace (address access) AND function signatures to determine interaction type. pub fn classify_ace_interaction( &self, state_trace: &UsedStateTrace, sim_success: bool, + config: &AceConfig, + selector: Option<&[u8]>, ) -> Option { match self { - AceExchange::Angstrom => { - Self::angstrom_classify_interaction(state_trace, sim_success, *self) - } + AceExchange::Angstrom => Self::angstrom_classify_interaction( + state_trace, + sim_success, + *self, + config, + selector, + ), } } - /// Angstrom-specific classification logic + /// Angstrom-specific classification logic using both state trace and signatures fn angstrom_classify_interaction( state_trace: &UsedStateTrace, sim_success: bool, exchange: AceExchange, + config: &AceConfig, + selector: Option<&[u8]>, ) -> Option { - let angstrom_address = exchange.address(); - - // We need to include read here as if it tries to reads the lastBlockUpdated on the pre swap - // hook. it will revert and not make any changes if the pools not unlocked. We want to capture - // this. - let accessed_exchange = state_trace - .read_slot_values - .keys() - .any(|k| k.address == angstrom_address) - || state_trace - .written_slot_values + // Check state trace for ACE address access using config addresses + let accessed_exchange = config.to_addresses.iter().any(|addr| { + state_trace + .read_slot_values .keys() - .any(|k| k.address == angstrom_address); + .any(|k| &k.address == addr) + || state_trace + .written_slot_values + .keys() + .any(|k| &k.address == addr) + }); - accessed_exchange.then_some({ - if sim_success { - AceInteraction::Unlocking { exchange } - } else { - AceInteraction::NonUnlocking { exchange } - } - }) + if !accessed_exchange { + return None; + } + + // Check function signatures to determine if this is a force or regular unlock + let is_force = selector.map_or(false, |sel| { + config + .force_signatures + .iter() + .any(|sig| sig.starts_with(sel)) + }); + + let is_unlock = selector.map_or(false, |sel| { + config + .unlock_signatures + .iter() + .any(|sig| sig.starts_with(sel)) + }); + + if sim_success && (is_force || is_unlock) { + Some(AceInteraction::Unlocking { exchange, is_force }) + } else { + Some(AceInteraction::NonUnlocking { exchange }) + } } } -/// Type of ACE interaction for mempool transactions +/// Type of ACE interaction for orders #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum AceInteraction { - /// Unlocking ACE tx, doesn't revert without an ACE tx, must be placed with ACE bundle - Unlocking { exchange: AceExchange }, - /// Requires an unlocking ACE tx, will revert otherwise + /// Unlocking ACE order - doesn't revert without an ACE order, must be placed with ACE bundle. + /// `is_force` indicates if this is a forced unlock (must always be included) vs optional. + Unlocking { + exchange: AceExchange, + is_force: bool, + }, + /// Requires an unlocking ACE order, will revert otherwise NonUnlocking { exchange: AceExchange }, } @@ -75,11 +119,14 @@ impl AceInteraction { matches!(self, Self::Unlocking { .. }) } + pub fn is_force(&self) -> bool { + matches!(self, Self::Unlocking { is_force: true, .. }) + } + pub fn get_exchange(&self) -> AceExchange { match self { - AceInteraction::Unlocking { exchange } | AceInteraction::NonUnlocking { exchange } => { - *exchange - } + AceInteraction::Unlocking { exchange, .. } + | AceInteraction::NonUnlocking { exchange } => *exchange, } } } diff --git a/crates/rbuilder-primitives/src/lib.rs b/crates/rbuilder-primitives/src/lib.rs index 303910be7..bddc1a057 100644 --- a/crates/rbuilder-primitives/src/lib.rs +++ b/crates/rbuilder-primitives/src/lib.rs @@ -42,6 +42,7 @@ use thiserror::Error; use uuid::Uuid; use crate::{ace::AceInteraction, serialize::TxEncoding}; +pub use ace::AceConfig; /// Extra metadata for an order. #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -1362,7 +1363,8 @@ pub struct SimulatedOrder { pub sim_value: SimValue, /// Info about read/write slots during the simulation to help figure out what the Order is doing. pub used_state_trace: Option, - pub is_ace: bool, + /// ACE interaction classification - None if not an ACE interaction. + /// Use `ace_interaction.map(|a| a.is_force()).unwrap_or(false)` to check if force unlock. pub ace_interaction: Option, } diff --git a/crates/rbuilder/src/backtest/execute.rs b/crates/rbuilder/src/backtest/execute.rs index 3b677f94d..b2e5d5d9c 100644 --- a/crates/rbuilder/src/backtest/execute.rs +++ b/crates/rbuilder/src/backtest/execute.rs @@ -107,7 +107,7 @@ where } let (sim_orders, sim_errors) = - simulate_all_orders_with_sim_tree(provider, &ctx, &orders, false)?; + simulate_all_orders_with_sim_tree(provider, &ctx, &orders, false, vec![])?; // Apply bundle merging as in live building. let order_store = Rc::new(RefCell::new(SimulatedOrderStore::new())); diff --git a/crates/rbuilder/src/bin/run-bundle-on-prefix.rs b/crates/rbuilder/src/bin/run-bundle-on-prefix.rs index 1e430c55d..2679647fe 100644 --- a/crates/rbuilder/src/bin/run-bundle-on-prefix.rs +++ b/crates/rbuilder/src/bin/run-bundle-on-prefix.rs @@ -220,7 +220,6 @@ async fn main() -> eyre::Result<()> { order, sim_value: Default::default(), used_state_trace: Default::default(), - is_ace: false, ace_interaction: None, }; let res = builder.commit_order(&mut block_info.local_ctx, &sim_order, &|_| Ok(()))?; @@ -318,7 +317,6 @@ fn execute_orders_on_tob( sim_value: Default::default(), used_state_trace: Default::default(), ace_interaction: None, - is_ace: false, }; let res = builder.commit_order(&mut block_info.local_ctx, &sim_order, &|_| Ok(()))?; let profit = res diff --git a/crates/rbuilder/src/building/ace_collector.rs b/crates/rbuilder/src/building/ace_collector.rs deleted file mode 100644 index 507a270ad..000000000 --- a/crates/rbuilder/src/building/ace_collector.rs +++ /dev/null @@ -1,208 +0,0 @@ -use ahash::HashMap; -use alloy_primitives::U256; -use alloy_rpc_types::TransactionTrait; -use itertools::Itertools; -use rbuilder_primitives::{ - ace::{AceExchange, AceInteraction, AceUnlockType}, - Order, SimulatedOrder, TransactionSignedEcRecoveredWithBlobs, -}; -use std::sync::Arc; -use tracing::trace; - -use crate::{ - building::sim::SimulationRequest, - live_builder::{config::AceConfig, simulation::SimulatedOrderCommand}, -}; - -/// Collects Ace Orders -#[derive(Debug, Default)] -pub struct AceCollector { - /// ACE bundles organized by exchange - exchanges: ahash::HashMap, - ace_tx_lookup: ahash::HashMap, -} - -impl AceConfig { - pub fn is_ace_force(&self, tx: &TransactionSignedEcRecoveredWithBlobs) -> bool { - let internal = tx.internal_tx_unsecure(); - self.from_addresses.contains(&internal.signer()) - && self - .to_addresses - .contains(&internal.inner().to().unwrap_or_default()) - } -} - -/// Data for a specific ACE exchange including all transaction types and logic -#[derive(Debug, Clone, Default)] -pub struct AceExchangeData { - /// Force ACE protocol tx - always included - pub force_ace_tx: Option, - /// Optional ACE protocol tx - conditionally included - pub optional_ace_tx: Option, - /// weather or not we have pushed through an unlocking mempool tx. - pub has_unlocking: bool, - /// Mempool txs that require ACE unlock - pub non_unlocking_mempool_txs: Vec, -} - -#[derive(Debug, Clone)] -pub struct AceOrderEntry { - pub simulated: Arc, - /// Profit after bundle simulation - pub bundle_profit: U256, -} - -impl AceExchangeData { - /// Add an ACE protocol transaction - fn add_ace_protocol_tx( - &mut self, - simulated: Arc, - unlock_type: AceUnlockType, - ) -> Vec { - let sim_cpy = simulated.order.clone(); - - let entry = AceOrderEntry { - bundle_profit: simulated.sim_value.full_profit_info().coinbase_profit(), - simulated, - }; - - match unlock_type { - AceUnlockType::Force => { - self.force_ace_tx = Some(entry); - trace!("Added forced ACE protocol unlock tx"); - } - AceUnlockType::Optional => { - self.optional_ace_tx = Some(entry); - trace!("Added optional ACE protocol unlock tx"); - } - } - - // Take all non-unlocking orders and simulate them with parents so they will pass and inject - // them into the system. - self.non_unlocking_mempool_txs - .drain(..) - .map(|entry| SimulationRequest { - id: rand::random(), - order: entry.simulated.order.clone(), - parents: vec![sim_cpy.clone()], - }) - .collect_vec() - } - - fn try_generate_sim_request(&self, order: &Order) -> Option { - let parent = self - .optional_ace_tx - .as_ref() - .or(self.force_ace_tx.as_ref())?; - - Some(SimulationRequest { - id: rand::random(), - order: order.clone(), - parents: vec![parent.simulated.order.clone()], - }) - } - - // If we have a regular mempool unlocking tx, we don't want to include the optional ace - // transaction ad will cancel it. - fn has_unlocking(&mut self) -> Option { - // we only want to send this once. - if self.has_unlocking { - return None; - } - - self.has_unlocking = true; - - self.optional_ace_tx - .take() - .map(|order| SimulatedOrderCommand::Cancellation(order.simulated.order.id())) - } - - fn add_mempool_tx(&mut self, simulated: Arc) -> Option { - if let Some(req) = self.try_generate_sim_request(&simulated.order) { - return Some(req); - } - // we don't have a way to sim this mempool tx yet, going to collect it instead. - - let entry = AceOrderEntry { - bundle_profit: simulated.sim_value.full_profit_info().coinbase_profit(), - simulated, - }; - - trace!("Added non-unlocking mempool ACE tx"); - self.non_unlocking_mempool_txs.push(entry); - - None - } -} - -impl AceCollector { - pub fn new(config: Vec) -> Self { - let mut lookup = HashMap::default(); - let mut exchanges = HashMap::default(); - - for ace in config { - let protocol = ace.protocol; - lookup.insert(protocol, ace); - exchanges.insert(protocol, Default::default()); - } - - Self { - exchanges, - ace_tx_lookup: lookup, - } - } - - pub fn is_ace_force(&self, order: &Order) -> bool { - match order { - Order::Tx(tx) => self - .ace_tx_lookup - .values() - .any(|config| config.is_ace_force(&tx.tx_with_blobs)), - _ => false, - } - } - - pub fn add_ace_protocol_tx( - &mut self, - simulated: Arc, - unlock_type: AceUnlockType, - exchange: AceExchange, - ) -> Vec { - let data = self.exchanges.entry(exchange).or_default(); - - data.add_ace_protocol_tx(simulated, unlock_type) - } - - pub fn has_unlocking(&self, exchange: &AceExchange) -> bool { - self.exchanges - .get(exchange) - .map(|e| e.has_unlocking) - .unwrap_or_default() - } - - pub fn have_unlocking(&mut self, exchange: AceExchange) -> Option { - self.exchanges.entry(exchange).or_default().has_unlocking() - } - - /// Add a mempool ACE transaction or bundle containing ACE interactions - pub fn add_mempool_ace_tx( - &mut self, - simulated: Arc, - interaction: AceInteraction, - ) -> Option { - self.exchanges - .entry(interaction.get_exchange()) - .or_default() - .add_mempool_tx(simulated) - } - - /// Get all configured exchanges - pub fn get_exchanges(&self) -> Vec { - self.exchanges.keys().cloned().collect() - } - - /// Clear all orders - pub fn clear(&mut self) { - self.exchanges.clear(); - } -} diff --git a/crates/rbuilder/src/building/block_orders/order_priority.rs b/crates/rbuilder/src/building/block_orders/order_priority.rs index dabf9c312..9c7ddeaf5 100644 --- a/crates/rbuilder/src/building/block_orders/order_priority.rs +++ b/crates/rbuilder/src/building/block_orders/order_priority.rs @@ -332,7 +332,6 @@ mod test { U256::from(non_mempool_profit), gas, ), - is_ace: false, ace_interaction: None, used_state_trace: None, }) diff --git a/crates/rbuilder/src/building/block_orders/share_bundle_merger.rs b/crates/rbuilder/src/building/block_orders/share_bundle_merger.rs index 58887b4a2..ef37ec1c5 100644 --- a/crates/rbuilder/src/building/block_orders/share_bundle_merger.rs +++ b/crates/rbuilder/src/building/block_orders/share_bundle_merger.rs @@ -149,7 +149,6 @@ impl MultiBackrunManager { sim_value: highest_payback_order.sim_order.sim_value.clone(), used_state_trace: highest_payback_order.sim_order.used_state_trace.clone(), ace_interaction: highest_payback_order.sim_order.ace_interaction, - is_ace: highest_payback_order.sim_order.is_ace, })) } diff --git a/crates/rbuilder/src/building/block_orders/test_context.rs b/crates/rbuilder/src/building/block_orders/test_context.rs index 3e20ff7e6..3199cb3b2 100644 --- a/crates/rbuilder/src/building/block_orders/test_context.rs +++ b/crates/rbuilder/src/building/block_orders/test_context.rs @@ -168,7 +168,6 @@ impl TestContext { Arc::new(SimulatedOrder { order, sim_value, - is_ace: false, used_state_trace: None, ace_interaction: None, }) diff --git a/crates/rbuilder/src/building/block_orders/test_data_generator.rs b/crates/rbuilder/src/building/block_orders/test_data_generator.rs index 281c8ff55..f2a8fd4a7 100644 --- a/crates/rbuilder/src/building/block_orders/test_data_generator.rs +++ b/crates/rbuilder/src/building/block_orders/test_data_generator.rs @@ -32,7 +32,6 @@ impl TestDataGenerator { sim_value, used_state_trace: None, ace_interaction: None, - is_ace: false, }) } } diff --git a/crates/rbuilder/src/building/builders/ordering_builder.rs b/crates/rbuilder/src/building/builders/ordering_builder.rs index ab91929ec..f2a23601e 100644 --- a/crates/rbuilder/src/building/builders/ordering_builder.rs +++ b/crates/rbuilder/src/building/builders/ordering_builder.rs @@ -282,13 +282,13 @@ impl OrderingBuilderContext { self.failed_orders.clear(); self.order_attempts.clear(); - // Extract ACE protocol transactions (Order::AceTx) from block_orders + // Extract ACE protocol orders from block_orders // These will be pre-committed at the top of the block let all_orders = block_orders.get_all_orders(); - let mut ace_txs = Vec::new(); + let mut ace_orders = Vec::new(); for order in all_orders { - if order.is_ace { - ace_txs.push(order.clone()); + if order.ace_interaction.map(|a| a.is_force()).unwrap_or(false) { + ace_orders.push(order.clone()); // Remove from block_orders so they don't get processed in fill_orders block_orders.remove_order(order.id()); } @@ -307,15 +307,15 @@ impl OrderingBuilderContext { self.max_order_execution_duration_warning, )?; - // Pre-commit ACE protocol transactions at the top of the block - for ace_tx in &ace_txs { - trace!(order_id = ?ace_tx.id(), "Pre-committing ACE protocol tx"); + // Pre-commit ACE protocol orders at the top of the block + for ace_order in &ace_orders { + trace!(order_id = ?ace_order.id(), "Pre-committing ACE protocol order"); if let Err(err) = block_building_helper.commit_order( &mut self.local_ctx, - ace_tx, - &|_| Ok(()), // ACE protocol txs bypass profit validation + ace_order, + &|_| Ok(()), // ACE protocol orders bypass profit validation ) { - trace!(order_id = ?ace_tx.id(), ?err, "Failed to pre-commit ACE protocol tx"); + trace!(order_id = ?ace_order.id(), ?err, "Failed to pre-commit ACE protocol order"); } } self.fill_orders( diff --git a/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs b/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs index bf9331087..9f8cdaba7 100644 --- a/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs +++ b/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs @@ -186,13 +186,13 @@ impl BlockBuildingResultAssembler { ) -> eyre::Result> { let build_start = Instant::now(); - // Extract ACE protocol transactions (Order::AceTx) from all groups + // Extract ACE protocol orders from all groups // These will be pre-committed at the top of the block - let mut ace_txs = Vec::new(); + let mut ace_orders = Vec::new(); for (_, group) in best_orderings_per_group.iter() { for order in group.orders.iter() { - if order.is_ace { - ace_txs.push(order.clone()); + if order.ace_interaction.map(|a| a.is_force()).unwrap_or(false) { + ace_orders.push(order.clone()); } } } @@ -202,7 +202,12 @@ impl BlockBuildingResultAssembler { // Filter out ACE orders from the sequence resolution_result .sequence_of_orders - .retain(|(order_idx, _)| !group.orders[*order_idx].is_ace); + .retain(|(order_idx, _)| { + !group.orders[*order_idx] + .ace_interaction + .map(|a| a.is_force()) + .unwrap_or(false) + }); } let mut block_building_helper = BlockBuildingHelperFromProvider::new( @@ -218,15 +223,15 @@ impl BlockBuildingResultAssembler { )?; block_building_helper.set_trace_orders_closed_at(orders_closed_at); - // Pre-commit ACE protocol transactions at the top of the block - for ace_tx in &ace_txs { - trace!(order_id = ?ace_tx.id(), "Pre-committing ACE protocol tx"); + // Pre-commit ACE protocol orders at the top of the block + for ace_order in &ace_orders { + trace!(order_id = ?ace_order.id(), "Pre-committing ACE protocol order"); if let Err(err) = block_building_helper.commit_order( &mut self.local_ctx, - ace_tx, - &|_| Ok(()), // ACE protocol txs bypass profit validation + ace_order, + &|_| Ok(()), // ACE protocol orders bypass profit validation ) { - trace!(order_id = ?ace_tx.id(), ?err, "Failed to pre-commit ACE protocol tx"); + trace!(order_id = ?ace_order.id(), ?err, "Failed to pre-commit ACE protocol order"); } } @@ -295,13 +300,13 @@ impl BlockBuildingResultAssembler { let mut best_orderings_per_group: Vec<(ResolutionResult, ConflictGroup)> = best_results.into_values().collect(); - // Extract ACE protocol transactions (Order::AceTx) from all groups + // Extract ACE protocol orders from all groups // These will be pre-committed at the top of the block - let mut ace_txs = Vec::new(); + let mut ace_orders = Vec::new(); for (_, group) in best_orderings_per_group.iter() { for order in group.orders.iter() { - if order.is_ace { - ace_txs.push(order.clone()); + if order.ace_interaction.map(|a| a.is_force()).unwrap_or(false) { + ace_orders.push(order.clone()); } } } @@ -311,7 +316,12 @@ impl BlockBuildingResultAssembler { // Filter out ACE orders from the sequence resolution_result .sequence_of_orders - .retain(|(order_idx, _)| !group.orders[*order_idx].is_ace); + .retain(|(order_idx, _)| { + !group.orders[*order_idx] + .ace_interaction + .map(|a| a.is_force()) + .unwrap_or(false) + }); } let mut block_building_helper = BlockBuildingHelperFromProvider::new( @@ -328,15 +338,15 @@ impl BlockBuildingResultAssembler { block_building_helper.set_trace_orders_closed_at(orders_closed_at); - // Pre-commit ACE protocol transactions at the top of the block - for ace_tx in &ace_txs { - trace!(order_id = ?ace_tx.id(), "Pre-committing ACE protocol tx in backtest"); + // Pre-commit ACE protocol orders at the top of the block + for ace_order in &ace_orders { + trace!(order_id = ?ace_order.id(), "Pre-committing ACE protocol order in backtest"); if let Err(err) = block_building_helper.commit_order( &mut self.local_ctx, - ace_tx, - &|_| Ok(()), // ACE protocol txs bypass profit validation + ace_order, + &|_| Ok(()), // ACE protocol orders bypass profit validation ) { - trace!(order_id = ?ace_tx.id(), ?err, "Failed to pre-commit ACE protocol tx in backtest"); + trace!(order_id = ?ace_order.id(), ?err, "Failed to pre-commit ACE protocol order in backtest"); } } diff --git a/crates/rbuilder/src/building/builders/parallel_builder/conflict_resolvers.rs b/crates/rbuilder/src/building/builders/parallel_builder/conflict_resolvers.rs index 6cec39ed0..996d1a6c0 100644 --- a/crates/rbuilder/src/building/builders/parallel_builder/conflict_resolvers.rs +++ b/crates/rbuilder/src/building/builders/parallel_builder/conflict_resolvers.rs @@ -532,7 +532,6 @@ mod tests { Arc::new(SimulatedOrder { order: Order::Bundle(bundle), used_state_trace: None, - is_ace: false, sim_value, ace_interaction: None, }) diff --git a/crates/rbuilder/src/building/builders/parallel_builder/conflict_task_generator.rs b/crates/rbuilder/src/building/builders/parallel_builder/conflict_task_generator.rs index d05f527e2..381b634d9 100644 --- a/crates/rbuilder/src/building/builders/parallel_builder/conflict_task_generator.rs +++ b/crates/rbuilder/src/building/builders/parallel_builder/conflict_task_generator.rs @@ -496,7 +496,6 @@ mod tests { }), sim_value, used_state_trace: Some(trace), - is_ace: false, ace_interaction: None, }) } diff --git a/crates/rbuilder/src/building/builders/parallel_builder/groups.rs b/crates/rbuilder/src/building/builders/parallel_builder/groups.rs index 0445ab999..be892f41a 100644 --- a/crates/rbuilder/src/building/builders/parallel_builder/groups.rs +++ b/crates/rbuilder/src/building/builders/parallel_builder/groups.rs @@ -479,7 +479,6 @@ mod tests { }), used_state_trace: Some(trace), sim_value: SimValue::default(), - is_ace: false, ace_interaction: None, }) } diff --git a/crates/rbuilder/src/building/mod.rs b/crates/rbuilder/src/building/mod.rs index fe513d798..ba74d5a45 100644 --- a/crates/rbuilder/src/building/mod.rs +++ b/crates/rbuilder/src/building/mod.rs @@ -74,7 +74,6 @@ use time::OffsetDateTime; use tracing::{error, trace}; use tx_sim_cache::TxExecutionCache; -pub mod ace_collector; pub mod bid_adjustments; pub mod block_orders; pub mod builders; diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index 5e7447850..34bf0b7c5 100644 --- a/crates/rbuilder/src/building/sim.rs +++ b/crates/rbuilder/src/building/sim.rs @@ -16,8 +16,10 @@ use crate::{ use ahash::{HashMap, HashSet}; use alloy_primitives::Address; use alloy_primitives::U256; +use alloy_rpc_types::TransactionTrait; use rand::seq::SliceRandom; use rbuilder_primitives::ace::{AceExchange, AceInteraction}; +use rbuilder_primitives::AceConfig; use rbuilder_primitives::BlockSpace; use rbuilder_primitives::SimValue; use rbuilder_primitives::{Order, OrderId, SimulatedOrder}; @@ -29,7 +31,6 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use strum::IntoEnumIterator; use tracing::{error, trace}; #[derive(Debug)] @@ -52,10 +53,56 @@ pub struct NonceKey { pub nonce: u64, } +/// Generic dependency key - represents something an order needs before it can execute +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum DependencyKey { + /// Order needs a specific nonce to be filled + Nonce(NonceKey), + /// Order needs an ACE unlock transaction for the given exchange + AceUnlock(AceExchange), +} + +impl From for DependencyKey { + fn from(nonce: NonceKey) -> Self { + DependencyKey::Nonce(nonce) + } +} + +/// State for a specific ACE exchange +#[derive(Debug, Clone, Default)] +pub struct AceExchangeState { + /// Force ACE protocol order - always included + pub force_unlock_order: Option>, + /// Optional ACE protocol order - can be cancelled if mempool unlock arrives + pub optional_unlock_order: Option>, + /// Whether we've seen a mempool unlocking order (cancels optional) + pub has_mempool_unlock: bool, +} + +impl AceExchangeState { + /// Get the best available unlock order. + /// Selects the cheapest (lowest gas) for frontrunning when both are available. + pub fn get_unlock_order(&self) -> Option<&Arc> { + match (&self.force_unlock_order, &self.optional_unlock_order) { + (Some(force), Some(optional)) => { + // Select cheapest (lowest gas) for frontrunning + if force.sim_value.gas_used() <= optional.sim_value.gas_used() { + Some(force) + } else { + Some(optional) + } + } + (Some(force), None) => Some(force), + (None, Some(optional)) => Some(optional), + (None, None) => None, + } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] struct PendingOrder { order: Order, - unsatisfied_nonces: usize, + unsatisfied_dependencies: usize, } pub type SimulationId = u64; @@ -72,7 +119,8 @@ pub struct SimulatedResult { pub id: SimulationId, pub simulated_order: Arc, pub previous_orders: Vec, - pub nonces_after: Vec, + /// Dependencies this simulation satisfies (nonces updated, ACE unlocks provided) + pub dependencies_satisfied: Vec, pub simulation_time: Duration, } @@ -83,35 +131,61 @@ pub struct SimTree { nonces: NonceCache, sims: HashMap, - sims_that_update_one_nonce: HashMap, + /// Maps a dependency to the simulation that provides it (for single-dependency sims) + dependency_providers: HashMap, pending_orders: HashMap, - pending_nonces: HashMap>, + + /// Orders waiting on each dependency + pending_dependencies: HashMap>, ready_orders: Vec, + + // ACE state management + /// ACE configuration lookup by exchange + ace_config: HashMap, + /// ACE exchange state (force/optional unlocks, mempool unlock tracking) + ace_state: HashMap, } #[derive(Debug)] -enum OrderNonceState { +enum OrderDependencyState { Invalid, - PendingNonces(Vec), + Pending(Vec), Ready(Vec), } impl SimTree { - pub fn new(nonce_cache_ref: NonceCache) -> Self { + pub fn new(nonce_cache_ref: NonceCache, ace_configs: Vec) -> Self { + let mut ace_config = HashMap::default(); + let mut ace_state = HashMap::default(); + + for config in ace_configs { + let protocol = config.protocol; + ace_config.insert(protocol, config); + ace_state.insert(protocol, AceExchangeState::default()); + } + Self { nonces: nonce_cache_ref, sims: HashMap::default(), - sims_that_update_one_nonce: HashMap::default(), + dependency_providers: HashMap::default(), pending_orders: HashMap::default(), - pending_nonces: HashMap::default(), + pending_dependencies: HashMap::default(), ready_orders: Vec::default(), + ace_config, + ace_state, } } - pub fn requeue_ace_order(&mut self, req: SimulationRequest) { - self.ready_orders.push(req); + /// Get the ACE configs + pub fn ace_configs(&self) -> &HashMap { + &self.ace_config + } + + /// Get the ACE exchange state for a given exchange + pub fn get_ace_state(&self, exchange: &AceExchange) -> Option<&AceExchangeState> { + self.ace_state.get(exchange) } fn push_order(&mut self, order: Order) -> Result<(), ProviderError> { @@ -119,20 +193,20 @@ impl SimTree { return Ok(()); } - let order_nonce_state = self.get_order_nonce_state(&order)?; + let order_dep_state = self.get_order_dependency_state(&order)?; let order_id = order.id(); - match order_nonce_state { - OrderNonceState::Invalid => { + match order_dep_state { + OrderDependencyState::Invalid => { return Ok(()); } - OrderNonceState::PendingNonces(pending_nonces) => { + OrderDependencyState::Pending(pending_deps) => { mark_order_pending_nonce(order_id); - let unsatisfied_nonces = pending_nonces.len(); - for nonce in pending_nonces { - self.pending_nonces - .entry(nonce) + let unsatisfied_dependencies = pending_deps.len(); + for dep in pending_deps { + self.pending_dependencies + .entry(dep) .or_default() .push(order.id()); } @@ -140,11 +214,11 @@ impl SimTree { order.id(), PendingOrder { order, - unsatisfied_nonces, + unsatisfied_dependencies, }, ); } - OrderNonceState::Ready(parents) => { + OrderDependencyState::Ready(parents) => { self.ready_orders.push(SimulationRequest { id: rand::random(), order, @@ -155,17 +229,21 @@ impl SimTree { Ok(()) } - fn get_order_nonce_state(&mut self, order: &Order) -> Result { + fn get_order_dependency_state( + &mut self, + order: &Order, + ) -> Result { let mut onchain_nonces_incremented = HashSet::default(); - let mut pending_nonces = Vec::new(); + let mut pending_deps = Vec::new(); let mut parent_orders = Vec::new(); + // Check nonce dependencies for nonce in order.nonces() { let onchain_nonce = self.nonces.nonce(nonce.address)?; match onchain_nonce.cmp(&nonce.nonce) { Ordering::Equal => { - // nonce, valid + // nonce valid onchain_nonces_incremented.insert(nonce.address); continue; } @@ -178,7 +256,7 @@ impl SimTree { ?nonce, "Dropping order because of nonce" ); - return Ok(OrderNonceState::Invalid); + return Ok(OrderDependencyState::Invalid); } else { // we can ignore this tx continue; @@ -196,8 +274,9 @@ impl SimTree { address: nonce.address, nonce: nonce.nonce, }; + let dep_key = DependencyKey::Nonce(nonce_key); - if let Some(sim_id) = self.sims_that_update_one_nonce.get(&nonce_key) { + if let Some(sim_id) = self.dependency_providers.get(&dep_key) { // we have something that fills this nonce let sim = self.sims.get(sim_id).expect("we never delete sims"); parent_orders.extend_from_slice(&sim.previous_orders); @@ -205,18 +284,56 @@ impl SimTree { continue; } - pending_nonces.push(nonce_key); + pending_deps.push(dep_key); } } } - if pending_nonces.is_empty() { - Ok(OrderNonceState::Ready(parent_orders)) + if pending_deps.is_empty() { + Ok(OrderDependencyState::Ready(parent_orders)) } else { - Ok(OrderNonceState::PendingNonces(pending_nonces)) + Ok(OrderDependencyState::Pending(pending_deps)) } } + /// Check if an order needs ACE unlock and add that dependency. + /// Called after initial simulation when we detect a NonUnlocking ACE interaction. + fn add_ace_dependency_for_order( + &mut self, + order: Order, + exchange: AceExchange, + ) -> Result<(), ProviderError> { + let dep_key = DependencyKey::AceUnlock(exchange); + + // Check if we already have an unlock provider + if let Some(sim_id) = self.dependency_providers.get(&dep_key) { + let sim = self.sims.get(sim_id).expect("we never delete sims"); + let mut parents = sim.previous_orders.clone(); + parents.push(sim.simulated_order.order.clone()); + + // Order is ready with the unlock tx as parent + self.ready_orders.push(SimulationRequest { + id: rand::random(), + order, + parents, + }); + } else { + // No unlock yet - add to pending + self.pending_dependencies + .entry(dep_key) + .or_default() + .push(order.id()); + self.pending_orders.insert( + order.id(), + PendingOrder { + order, + unsatisfied_dependencies: 1, + }, + ); + } + Ok(()) + } + pub fn push_orders(&mut self, orders: Vec) -> Result<(), ProviderError> { for order in orders { self.push_order(order)?; @@ -236,11 +353,14 @@ impl SimTree { ) -> Result<(), ProviderError> { self.sims.insert(result.id, result.clone()); let mut orders_ready = Vec::new(); - if result.nonces_after.len() == 1 { - let updated_nonce = result.nonces_after.first().unwrap().clone(); - match self.sims_that_update_one_nonce.entry(updated_nonce.clone()) { + // Process each dependency this simulation satisfies + if result.dependencies_satisfied.len() == 1 { + let dep_key = result.dependencies_satisfied.first().unwrap().clone(); + + match self.dependency_providers.entry(dep_key.clone()) { Entry::Occupied(mut entry) => { + // Already have a provider - check if this one is more profitable let current_sim_profit = { let sim_id = entry.get_mut(); self.sims @@ -262,15 +382,17 @@ impl SimTree { } } Entry::Vacant(entry) => { + // First provider for this dependency entry.insert(result.id); - if let Some(pending_orders) = self.pending_nonces.remove(&updated_nonce) { - for order in pending_orders { - match self.pending_orders.entry(order) { + // Unblock orders waiting on this dependency + if let Some(pending_order_ids) = self.pending_dependencies.remove(&dep_key) { + for order_id in pending_order_ids { + match self.pending_orders.entry(order_id) { Entry::Occupied(mut entry) => { let pending_order = entry.get_mut(); - pending_order.unsatisfied_nonces -= 1; - if pending_order.unsatisfied_nonces == 0 { + pending_order.unsatisfied_dependencies -= 1; + if pending_order.unsatisfied_dependencies == 0 { orders_ready.push(entry.remove().order); } } @@ -286,20 +408,20 @@ impl SimTree { } for ready_order in orders_ready { - let pending_state = self.get_order_nonce_state(&ready_order)?; + let pending_state = self.get_order_dependency_state(&ready_order)?; match pending_state { - OrderNonceState::Ready(parents) => { + OrderDependencyState::Ready(parents) => { self.ready_orders.push(SimulationRequest { id: rand::random(), order: ready_order, parents, }); } - OrderNonceState::Invalid => { + OrderDependencyState::Invalid => { // @Metric bug counter error!("SimTree bug order became invalid"); } - OrderNonceState::PendingNonces(_) => { + OrderDependencyState::Pending(_) => { // @Metric bug counter error!("SimTree bug order became pending again"); } @@ -308,6 +430,102 @@ impl SimTree { Ok(()) } + /// Handle ACE interaction after simulation. + /// Returns (was_handled, optional_cancellation_order_id) + /// - For Unlocking interactions: registers as force or optional unlock provider + /// - For NonUnlocking interactions: adds order as pending on ACE unlock dependency + /// - Returns cancellation OrderId if a mempool unlock cancels an optional ACE tx + pub fn handle_ace_interaction( + &mut self, + result: &mut SimulatedResult, + ) -> Result<(bool, Option), ProviderError> { + let Some(interaction) = result.simulated_order.ace_interaction else { + return Ok((false, None)); + }; + + // If this order already has parents, it was re-simulated with unlock - just pass through + if !result.previous_orders.is_empty() { + return Ok((false, None)); + } + + let mut cancellation = None; + + match interaction { + AceInteraction::Unlocking { exchange, is_force } => { + // Register the unlock in ACE state + let state = self.ace_state.entry(exchange).or_default(); + + if is_force { + state.force_unlock_order = Some(result.simulated_order.clone()); + trace!("Added forced ACE protocol unlock order for {:?}", exchange); + } else { + state.optional_unlock_order = Some(result.simulated_order.clone()); + trace!( + "Added optional ACE protocol unlock order for {:?}", + exchange + ); + } + + // Check if we should cancel the optional ACE order (mempool unlock arrived first) + if state.has_mempool_unlock { + if let Some(optional) = state.optional_unlock_order.take() { + cancellation = Some(optional.order.id()); + } + } + + // Make sure the ACE unlock dependency is in dependencies_satisfied + let dep_key = DependencyKey::AceUnlock(exchange); + if !result.dependencies_satisfied.contains(&dep_key) { + result.dependencies_satisfied.push(dep_key); + } + + // Process this result to unblock pending orders + self.process_simulation_task_result(result.clone())?; + } + AceInteraction::NonUnlocking { exchange } => { + // This is a mempool order that needs ACE unlock + let state = self.ace_state.entry(exchange).or_default(); + + // Check if we have an unlock order to use as parent + if let Some(unlock_order) = state.get_unlock_order().cloned() { + // Re-queue with the unlock as parent + self.ready_orders.push(SimulationRequest { + id: rand::random(), + order: result.simulated_order.order.clone(), + parents: vec![unlock_order.order.clone()], + }); + } else { + // No unlock yet - add as pending on ACE dependency + self.add_ace_dependency_for_order( + result.simulated_order.order.clone(), + exchange, + )?; + } + return Ok((true, None)); + } + } + + Ok((true, cancellation)) + } + + /// Mark that a mempool unlocking order has been seen for an exchange. + /// Returns the OrderId of the optional ACE order to cancel, if any. + pub fn mark_mempool_unlock(&mut self, exchange: AceExchange) -> Option { + let state = self.ace_state.entry(exchange).or_default(); + + // Only cancel once + if state.has_mempool_unlock { + return None; + } + state.has_mempool_unlock = true; + + // Cancel the optional ACE order if present + state + .optional_unlock_order + .take() + .map(|order| order.order.id()) + } + pub fn submit_simulation_tasks_results( &mut self, results: Vec, @@ -327,6 +545,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, @@ -335,7 +554,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); @@ -378,6 +597,7 @@ where ctx, &mut local_ctx, &mut block_state, + sim_tree.ace_configs(), )?; let (_, provider) = block_state.into_parts(); state_for_sim = provider; @@ -392,15 +612,23 @@ 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 { exchange, .. }) = + sim_order.ace_interaction + { + dependencies_satisfied.push(DependencyKey::AceUnlock(exchange)); + } + 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); @@ -427,70 +655,20 @@ 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 order_id = order.id(); - let has_parents = !parent_orders.is_empty(); let sim_res = simulate_order_using_fork( parent_orders, - order.clone(), + order, &mut fork, &ctx.mempool_tx_detector, + ace_configs, ); fork.rollback(rollback_point); - let mut sim_res = sim_res?; - - let sim_success = matches!(&sim_res, OrderSimResult::Success(_, _)); - let ace_interaction = AceExchange::iter().find_map(|exchange| { - exchange.classify_ace_interaction(&tracer.used_state_trace, sim_success) - }); - - match sim_res { - OrderSimResult::Failed(ref err) => { - // Check if failed order accessed ACE - if so, treat as successful with zero profit - if let Some(interaction @ AceInteraction::NonUnlocking { exchange }) = ace_interaction { - // Ace can inject parent orders, we want to ignore these. - if !has_parents { - tracing::debug!( - order = ?order_id, - ?err, - ?exchange, - "Failed order accessed ACE - treating as successful non-unlocking ACE transaction" - ); - sim_res = OrderSimResult::Success( - Arc::new(SimulatedOrder { - order, - sim_value: SimValue::new( - U256::ZERO, - U256::ZERO, - BlockSpace::new(tracer.used_gas, 0, 0), - Vec::new(), - ), - is_ace: false, - used_state_trace: Some(tracer.used_state_trace.clone()), - ace_interaction: Some(interaction), - }), - Vec::new(), - ); - } - } - } - // If we have a sucessful simulation and we have detected an ace tx, this means that it is a - // unlocking mempool ace tx by default. - OrderSimResult::Success(ref mut simulated_order, _) => { - if let Some(interaction) = ace_interaction { - tracing::debug!( - order = ?order.id(), - ?interaction, - "Order has ACE interaction" - ); - // Update the SimulatedOrder to include ace_interaction - Arc::make_mut(simulated_order).ace_interaction = Some(interaction); - } - } - } + let sim_res = sim_res?; Ok(OrderSimResultWithGas { result: sim_res, @@ -504,8 +682,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 @@ -527,7 +708,40 @@ 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 from order's first transaction + let selector: Option<[u8; 4]> = order.list_txs().first().and_then(|(tx, _)| { + let input = tx.tx.input(); + if input.len() >= 4 { + Some([input[0], input[1], input[2], input[3]]) + } else { + None + } + }); + + let ace_interaction = used_state_trace.as_ref().and_then(|trace| { + ace_configs.iter().find_map(|(exchange, config)| { + if !config.enabled { + return None; + } + exchange.classify_ace_interaction( + trace, + sim_success, + config, + selector.as_ref().map(|s| s.as_slice()), + ) + }) + }); match result { Ok(res) => { @@ -538,12 +752,42 @@ pub fn simulate_order_using_fork( order, sim_value, used_state_trace: res.used_state_trace, - ace_interaction: None, - is_ace: false, + 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 { exchange }) = ace_interaction { + // ACE can inject parent orders, we want to ignore these. + if !has_parents { + tracing::debug!( + order = ?order.id(), + ?err, + ?exchange, + "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/bundle_tests/setup.rs b/crates/rbuilder/src/building/testing/bundle_tests/setup.rs index c7d69391c..3f9afbdd1 100644 --- a/crates/rbuilder/src/building/testing/bundle_tests/setup.rs +++ b/crates/rbuilder/src/building/testing/bundle_tests/setup.rs @@ -228,7 +228,6 @@ impl TestSetup { order: self.order_builder.build_order(), sim_value: Default::default(), used_state_trace: Default::default(), - is_ace: false, ace_interaction: None, }; diff --git a/crates/rbuilder/src/live_builder/config.rs b/crates/rbuilder/src/live_builder/config.rs index 4c6d9d01b..d81554962 100644 --- a/crates/rbuilder/src/live_builder/config.rs +++ b/crates/rbuilder/src/live_builder/config.rs @@ -64,6 +64,7 @@ use ethereum_consensus::{ use eyre::Context; use lazy_static::lazy_static; use rbuilder_config::EnvOrValue; +pub use rbuilder_primitives::AceConfig; use rbuilder_primitives::{ ace::AceExchange, mev_boost::{MevBoostRelayID, RelayMode}, @@ -117,15 +118,6 @@ pub struct BuilderConfig { pub builder: SpecificBuilderConfig, } -#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] -pub struct AceConfig { - pub protocol: AceExchange, - pub from_addresses: HashSet

, - pub to_addresses: HashSet
, - pub unlock_signatures: HashSet, - pub force_signatures: HashSet, -} - #[derive(Debug, Clone, Deserialize, PartialEq, Default)] #[serde(default, deny_unknown_fields)] pub struct SubsidyConfig { diff --git a/crates/rbuilder/src/live_builder/simulation/mod.rs b/crates/rbuilder/src/live_builder/simulation/mod.rs index 41eca1e7b..a35846dad 100644 --- a/crates/rbuilder/src/live_builder/simulation/mod.rs +++ b/crates/rbuilder/src/live_builder/simulation/mod.rs @@ -39,6 +39,9 @@ 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 @@ -154,7 +157,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.protocol, 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); @@ -164,6 +174,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); } @@ -175,7 +186,6 @@ where slot_sim_results_sender, sim_tree, sim_tracer, - ace_config, ); simulation_job.run().await; diff --git a/crates/rbuilder/src/live_builder/simulation/sim_worker.rs b/crates/rbuilder/src/live_builder/simulation/sim_worker.rs index 838ad3c6c..28e5ac13e 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,31 @@ 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 { exchange, .. }) = + simulated_order.ace_interaction + { + dependencies_satisfied.push(DependencyKey::AceUnlock(exchange)); + } + 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 7b528b558..5709c8c5c 100644 --- a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs +++ b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs @@ -1,4 +1,3 @@ -use crate::building::ace_collector::AceCollector; use std::{fmt, sync::Arc}; use crate::{ @@ -21,7 +20,7 @@ use super::SimulatedOrderCommand; /// Create and call run() /// The flow is: /// 1 New orders are polled from new_order_sub and inserted en the SimTree. -/// 2 SimTree is polled for nonce-ready orders and are sent to be simulated (sent to sim_req_sender). +/// 2 SimTree is polled for dependency-ready orders and are sent to be simulated (sent to sim_req_sender). /// 3 Simulation results are polled from sim_results_receiver and sent to slot_sim_results_sender. /// Cancellation flow: we add every order we start to process to in_flight_orders. /// If we get a cancellation and the order is not in in_flight_orders we forward the cancellation. @@ -39,7 +38,6 @@ pub struct SimulationJob { /// Output of the simulations slot_sim_results_sender: mpsc::Sender, sim_tree: SimTree, - ace_bundler: AceCollector, orders_received: OrderCounter, orders_simulated_ok: OrderCounter, @@ -70,7 +68,6 @@ pub struct SimulationJob { } impl SimulationJob { - #[allow(clippy::too_many_arguments)] pub fn new( block_cancellation: CancellationToken, new_order_sub: mpsc::UnboundedReceiver, @@ -79,10 +76,8 @@ impl SimulationJob { slot_sim_results_sender: mpsc::Sender, sim_tree: SimTree, sim_tracer: Arc, - ace_config: Vec, ) -> Self { Self { - ace_bundler: AceCollector::new(ace_config), block_cancellation, new_order_sub, sim_req_sender, @@ -188,61 +183,6 @@ impl SimulationJob { } } - /// Returns weather or not to continue the processing of this tx. - fn handle_ace_tx(&mut self, mut res: SimulatedResult) -> Option { - // this means that we have frontran this with an ace unlocking tx in the simulator. - // We cannot do anything else at this point so we yield to the default flow. - if !res.previous_orders.is_empty() { - return Some(res); - } - - let is_ace = if self.ace_bundler.is_ace_force(&res.simulated_order.order) { - Arc::make_mut(&mut res.simulated_order).is_ace = true; - - // Is a force tx given that it is being sent directly to the ace protocol tx - // but isn't reverting. - self.ace_bundler.add_ace_protocol_tx( - res.simulated_order.clone(), - rbuilder_primitives::ace::AceUnlockType::Force, - res.simulated_order.ace_interaction.unwrap().get_exchange(), - ); - - // assert that this order is fully correct. - true - } else if let Some(ace) = res.simulated_order.ace_interaction { - if ace.is_unlocking() { - self.ace_bundler.add_ace_protocol_tx( - res.simulated_order.clone(), - rbuilder_primitives::ace::AceUnlockType::Optional, - res.simulated_order.ace_interaction.unwrap().get_exchange(), - ); - } - - true - } else { - false - }; - - // we need to know if this ace tx has already been simulated or not. - let ace_interaction = res.simulated_order.ace_interaction.unwrap(); - if is_ace { - if let Some(cmd) = self - .ace_bundler - .have_unlocking(ace_interaction.get_exchange()) - { - let _ = self.slot_sim_results_sender.try_send(cmd); - } - return Some(res); - } else if let Some(order) = self.ace_bundler.add_mempool_ace_tx( - res.simulated_order.clone(), - res.simulated_order.ace_interaction.unwrap(), - ) { - self.sim_tree.requeue_ace_order(order); - } - - None - } - /// updates the sim_tree and notifies new orders /// ONLY not cancelled are considered /// return if everything went OK @@ -264,14 +204,30 @@ impl SimulationJob { self.orders_with_replacement_key_sim_ok += 1; } - let sim_result = if sim_result.simulated_order.ace_interaction.is_some() { - let Some(unlocking_ace) = self.handle_ace_tx(sim_result.clone()) else { - continue; - }; - unlocking_ace - } else { - sim_result.clone() - }; + // Handle ACE interactions through the SimTree's dependency system + // NonUnlocking ACE orders get added as pending, Unlocking orders provide the dependency + match self.sim_tree.handle_ace_interaction(sim_result) { + Ok((handled, cancellation)) => { + // Send cancellation for optional ACE tx if needed + if let Some(cancel_id) = cancellation { + let _ = self + .slot_sim_results_sender + .try_send(SimulatedOrderCommand::Cancellation(cancel_id)); + } + // If this was a non-unlocking ACE tx that got queued for re-sim, skip forwarding + if handled + && sim_result + .simulated_order + .ace_interaction + .is_some_and(|i| !i.is_unlocking()) + { + continue; + } + } + Err(err) => { + error!(?err, "Failed to handle ACE interaction"); + } + } // Skip cancelled orders and remove from in_flight_orders if self @@ -294,7 +250,7 @@ impl SimulationJob { { return false; //receiver closed :( } else { - self.sim_tracer.update_simulation_sent(&sim_result); + self.sim_tracer.update_simulation_sent(sim_result); } } } From 47e4853abf7792d2fed2a6337c4b62c549cbca56 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 2 Dec 2025 12:19:55 -0500 Subject: [PATCH 17/51] feat: wip --- crates/rbuilder-primitives/src/ace.rs | 4 ++-- crates/rbuilder/src/building/sim.rs | 8 ++++++-- crates/rbuilder/src/live_builder/config.rs | 6 +----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/rbuilder-primitives/src/ace.rs b/crates/rbuilder-primitives/src/ace.rs index 149e09707..d1a4cc30b 100644 --- a/crates/rbuilder-primitives/src/ace.rs +++ b/crates/rbuilder-primitives/src/ace.rs @@ -79,14 +79,14 @@ impl AceExchange { } // Check function signatures to determine if this is a force or regular unlock - let is_force = selector.map_or(false, |sel| { + let is_force = selector.is_some_and(|sel| { config .force_signatures .iter() .any(|sig| sig.starts_with(sel)) }); - let is_unlock = selector.map_or(false, |sel| { + let is_unlock = selector.is_some_and(|sel| { config .unlock_signatures .iter() diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index 34bf0b7c5..7c890e59f 100644 --- a/crates/rbuilder/src/building/sim.rs +++ b/crates/rbuilder/src/building/sim.rs @@ -356,7 +356,11 @@ impl SimTree { // Process each dependency this simulation satisfies if result.dependencies_satisfied.len() == 1 { - let dep_key = result.dependencies_satisfied.first().unwrap().clone(); + let dep_key = result + .dependencies_satisfied + .first() + .expect("checked len == 1") + .clone(); match self.dependency_providers.entry(dep_key.clone()) { Entry::Occupied(mut entry) => { @@ -721,7 +725,7 @@ pub fn simulate_order_using_fork( // Detect ACE interaction from the state trace using config // Get function selector from order's first transaction let selector: Option<[u8; 4]> = order.list_txs().first().and_then(|(tx, _)| { - let input = tx.tx.input(); + let input = tx.internal_tx_unsecure().input(); if input.len() >= 4 { Some([input[0], input[1], input[2], input[3]]) } else { diff --git a/crates/rbuilder/src/live_builder/config.rs b/crates/rbuilder/src/live_builder/config.rs index d81554962..1ffa6b26f 100644 --- a/crates/rbuilder/src/live_builder/config.rs +++ b/crates/rbuilder/src/live_builder/config.rs @@ -50,7 +50,6 @@ use crate::{ utils::{build_info::rbuilder_version, ProviderFactoryReopener, Signer}, }; use alloy_chains::ChainKind; -use alloy_primitives::Bytes; use alloy_primitives::{ utils::{format_ether, parse_ether}, Address, FixedBytes, B256, U256, @@ -64,11 +63,8 @@ use ethereum_consensus::{ 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 rbuilder_primitives::{ - ace::AceExchange, - mev_boost::{MevBoostRelayID, RelayMode}, -}; use reth_chainspec::{Chain, ChainSpec, NamedChain}; use reth_db::DatabaseEnv; use reth_node_api::NodeTypesWithDBAdapter; From 85dd20a1b5a191ef94ff696e5e41c2f66d9d75fc Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 2 Dec 2025 12:57:37 -0500 Subject: [PATCH 18/51] wip: some featur cleanup --- crates/rbuilder-primitives/src/ace.rs | 79 ++++++++++++++----- .../src/building/builders/ordering_builder.rs | 8 +- .../block_building_result_assembler.rs | 20 +++-- crates/rbuilder/src/building/sim.rs | 27 ++++--- .../config/rbuilder/config-live-example.toml | 2 + 5 files changed, 98 insertions(+), 38 deletions(-) diff --git a/crates/rbuilder-primitives/src/ace.rs b/crates/rbuilder-primitives/src/ace.rs index d1a4cc30b..7119e9abe 100644 --- a/crates/rbuilder-primitives/src/ace.rs +++ b/crates/rbuilder-primitives/src/ace.rs @@ -1,5 +1,5 @@ use crate::evm_inspector::UsedStateTrace; -use alloy_primitives::{Address, Bytes}; +use alloy_primitives::{Address, Bytes, B256}; use derive_more::FromStr; use serde::Deserialize; use std::collections::HashSet; @@ -17,6 +17,8 @@ pub struct AceConfig { pub from_addresses: HashSet

, /// Addresses that receive ACE orders (the ACE contract addresses) pub to_addresses: HashSet
, + /// Storage slots that must be read to detect ACE interaction (e.g., _lastBlockUpdated at slot 3) + pub detection_slots: HashSet, /// Function signatures that indicate an unlock operation pub unlock_signatures: HashSet, /// Function signatures that indicate a forced unlock operation @@ -42,6 +44,7 @@ impl AceExchange { sim_success: bool, config: &AceConfig, selector: Option<&[u8]>, + tx_to: Option
, ) -> Option { match self { AceExchange::Angstrom => Self::angstrom_classify_interaction( @@ -50,6 +53,7 @@ impl AceExchange { *self, config, selector, + tx_to, ), } } @@ -61,54 +65,77 @@ impl AceExchange { exchange: AceExchange, config: &AceConfig, selector: Option<&[u8]>, + tx_to: Option
, ) -> Option { - // Check state trace for ACE address access using config addresses - let accessed_exchange = config.to_addresses.iter().any(|addr| { - state_trace - .read_slot_values - .keys() - .any(|k| &k.address == addr) - || state_trace - .written_slot_values + // Check that ALL detection slots are read or written from any of the ACE contract addresses + let all_slots_accessed = config.detection_slots.iter().all(|slot| { + config.to_addresses.iter().any(|addr| { + state_trace + .read_slot_values .keys() - .any(|k| &k.address == addr) + .any(|k| &k.address == addr && &k.key == slot) + || state_trace + .written_slot_values + .keys() + .any(|k| &k.address == addr && &k.key == slot) + }) }); - if !accessed_exchange { + if !all_slots_accessed { return None; } + // Check if this is a direct call to the protocol + let is_direct_protocol_call = tx_to.is_some_and(|to| config.to_addresses.contains(&to)); + // Check function signatures to determine if this is a force or regular unlock - let is_force = selector.is_some_and(|sel| { + let is_force_sig = selector.is_some_and(|sel| { config .force_signatures .iter() .any(|sig| sig.starts_with(sel)) }); - let is_unlock = selector.is_some_and(|sel| { + let is_unlock_sig = selector.is_some_and(|sel| { config .unlock_signatures .iter() .any(|sig| sig.starts_with(sel)) }); - if sim_success && (is_force || is_unlock) { - Some(AceInteraction::Unlocking { exchange, is_force }) + if sim_success && (is_force_sig || is_unlock_sig) { + let source = if is_direct_protocol_call && is_force_sig { + AceUnlockSource::ProtocolForce + } else if is_direct_protocol_call && is_unlock_sig { + AceUnlockSource::ProtocolOptional + } else { + AceUnlockSource::User + }; + Some(AceInteraction::Unlocking { exchange, source }) } else { Some(AceInteraction::NonUnlocking { exchange }) } } } +/// Source of an ACE unlock order +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum AceUnlockSource { + /// Direct call to protocol with force signature - must always be included + ProtocolForce, + /// Direct call to protocol with optional unlock signature + ProtocolOptional, + /// Indirect interaction (user tx that interacts with ACE contract) + User, +} + /// Type of ACE interaction for orders #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum AceInteraction { /// Unlocking ACE order - doesn't revert without an ACE order, must be placed with ACE bundle. - /// `is_force` indicates if this is a forced unlock (must always be included) vs optional. Unlocking { exchange: AceExchange, - is_force: bool, + source: AceUnlockSource, }, /// Requires an unlocking ACE order, will revert otherwise NonUnlocking { exchange: AceExchange }, @@ -119,8 +146,24 @@ impl AceInteraction { matches!(self, Self::Unlocking { .. }) } + pub fn is_protocol_tx(&self) -> bool { + matches!( + self, + Self::Unlocking { + source: AceUnlockSource::ProtocolForce | AceUnlockSource::ProtocolOptional, + .. + } + ) + } + pub fn is_force(&self) -> bool { - matches!(self, Self::Unlocking { is_force: true, .. }) + matches!( + self, + Self::Unlocking { + source: AceUnlockSource::ProtocolForce, + .. + } + ) } pub fn get_exchange(&self) -> AceExchange { diff --git a/crates/rbuilder/src/building/builders/ordering_builder.rs b/crates/rbuilder/src/building/builders/ordering_builder.rs index f2a23601e..3675a9560 100644 --- a/crates/rbuilder/src/building/builders/ordering_builder.rs +++ b/crates/rbuilder/src/building/builders/ordering_builder.rs @@ -282,12 +282,16 @@ impl OrderingBuilderContext { self.failed_orders.clear(); self.order_attempts.clear(); - // Extract ACE protocol orders from block_orders + // Extract ACE protocol orders (direct calls to protocol) from block_orders // These will be pre-committed at the top of the block let all_orders = block_orders.get_all_orders(); let mut ace_orders = Vec::new(); for order in all_orders { - if order.ace_interaction.map(|a| a.is_force()).unwrap_or(false) { + if order + .ace_interaction + .map(|a| a.is_protocol_tx()) + .unwrap_or(false) + { ace_orders.push(order.clone()); // Remove from block_orders so they don't get processed in fill_orders block_orders.remove_order(order.id()); diff --git a/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs b/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs index 9f8cdaba7..d4da950ec 100644 --- a/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs +++ b/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs @@ -186,12 +186,16 @@ impl BlockBuildingResultAssembler { ) -> eyre::Result> { let build_start = Instant::now(); - // Extract ACE protocol orders from all groups + // Extract ACE protocol orders (direct calls to protocol) from all groups // These will be pre-committed at the top of the block let mut ace_orders = Vec::new(); for (_, group) in best_orderings_per_group.iter() { for order in group.orders.iter() { - if order.ace_interaction.map(|a| a.is_force()).unwrap_or(false) { + if order + .ace_interaction + .map(|a| a.is_protocol_tx()) + .unwrap_or(false) + { ace_orders.push(order.clone()); } } @@ -205,7 +209,7 @@ impl BlockBuildingResultAssembler { .retain(|(order_idx, _)| { !group.orders[*order_idx] .ace_interaction - .map(|a| a.is_force()) + .map(|a| a.is_protocol_tx()) .unwrap_or(false) }); } @@ -300,12 +304,16 @@ impl BlockBuildingResultAssembler { let mut best_orderings_per_group: Vec<(ResolutionResult, ConflictGroup)> = best_results.into_values().collect(); - // Extract ACE protocol orders from all groups + // Extract ACE protocol orders (direct calls to protocol) from all groups // These will be pre-committed at the top of the block let mut ace_orders = Vec::new(); for (_, group) in best_orderings_per_group.iter() { for order in group.orders.iter() { - if order.ace_interaction.map(|a| a.is_force()).unwrap_or(false) { + if order + .ace_interaction + .map(|a| a.is_protocol_tx()) + .unwrap_or(false) + { ace_orders.push(order.clone()); } } @@ -319,7 +327,7 @@ impl BlockBuildingResultAssembler { .retain(|(order_idx, _)| { !group.orders[*order_idx] .ace_interaction - .map(|a| a.is_force()) + .map(|a| a.is_protocol_tx()) .unwrap_or(false) }); } diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index 7c890e59f..69d9666da 100644 --- a/crates/rbuilder/src/building/sim.rs +++ b/crates/rbuilder/src/building/sim.rs @@ -18,7 +18,7 @@ use alloy_primitives::Address; use alloy_primitives::U256; use alloy_rpc_types::TransactionTrait; use rand::seq::SliceRandom; -use rbuilder_primitives::ace::{AceExchange, AceInteraction}; +use rbuilder_primitives::ace::{AceExchange, AceInteraction, AceUnlockSource}; use rbuilder_primitives::AceConfig; use rbuilder_primitives::BlockSpace; use rbuilder_primitives::SimValue; @@ -455,11 +455,11 @@ impl SimTree { let mut cancellation = None; match interaction { - AceInteraction::Unlocking { exchange, is_force } => { + AceInteraction::Unlocking { exchange, source } => { // Register the unlock in ACE state let state = self.ace_state.entry(exchange).or_default(); - if is_force { + if source == AceUnlockSource::ProtocolForce { state.force_unlock_order = Some(result.simulated_order.clone()); trace!("Added forced ACE protocol unlock order for {:?}", exchange); } else { @@ -723,15 +723,17 @@ pub fn simulate_order_using_fork( .cloned(); // Detect ACE interaction from the state trace using config - // Get function selector from order's first transaction - let selector: Option<[u8; 4]> = order.list_txs().first().and_then(|(tx, _)| { - let input = tx.internal_tx_unsecure().input(); - if input.len() >= 4 { - Some([input[0], input[1], input[2], input[3]]) - } else { - None - } - }); + // Get function selector and tx.to from order's first transaction + let (selector, tx_to): (Option<[u8; 4]>, Option
) = + order.list_txs().first().map_or((None, None), |(tx, _)| { + let input = tx.internal_tx_unsecure().input(); + let sel = if input.len() >= 4 { + Some([input[0], input[1], input[2], input[3]]) + } else { + None + }; + (sel, tx.to()) + }); let ace_interaction = used_state_trace.as_ref().and_then(|trace| { ace_configs.iter().find_map(|(exchange, config)| { @@ -743,6 +745,7 @@ pub fn simulate_order_using_fork( sim_success, config, selector.as_ref().map(|s| s.as_slice()), + tx_to, ) }) }); diff --git a/examples/config/rbuilder/config-live-example.toml b/examples/config/rbuilder/config-live-example.toml index df39b2f5b..920172bc9 100644 --- a/examples/config/rbuilder/config-live-example.toml +++ b/examples/config/rbuilder/config-live-example.toml @@ -89,6 +89,8 @@ from_addresses = [ "0x693ca5c6852a7d212dabc98b28e15257465c11f3", ] to_addresses = ["0x0000000aa232009084Bd71A5797d089AA4Edfad4"] +# _lastBlockUpdated storage slot (slot 3) +detection_slots = ["0x0000000000000000000000000000000000000000000000000000000000000003"] # unlockWithEmptyAttestation(address,bytes) nonpayable unlock_signatures = ["0x1828e0e7"] # execute(bytes) nonpayable From ce6e06c3fd0e7938715405288188c8c447a6e50e Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 2 Dec 2025 13:42:33 -0500 Subject: [PATCH 19/51] feat: refactor angstrom out of the codebase, only ace --- crates/rbuilder-primitives/src/ace.rs | 157 +++++++----------- crates/rbuilder/src/building/sim.rs | 93 ++++++----- .../src/live_builder/simulation/mod.rs | 5 +- .../src/live_builder/simulation/sim_worker.rs | 8 +- .../config/rbuilder/config-live-example.toml | 3 +- 5 files changed, 120 insertions(+), 146 deletions(-) diff --git a/crates/rbuilder-primitives/src/ace.rs b/crates/rbuilder-primitives/src/ace.rs index 7119e9abe..0c4b6c31b 100644 --- a/crates/rbuilder-primitives/src/ace.rs +++ b/crates/rbuilder-primitives/src/ace.rs @@ -1,9 +1,10 @@ use crate::evm_inspector::UsedStateTrace; -use alloy_primitives::{Address, Bytes, B256}; -use derive_more::FromStr; +use alloy_primitives::{Address, FixedBytes, B256}; use serde::Deserialize; use std::collections::HashSet; -use strum::EnumIter; + +/// 4-byte function selector +pub type Selector = FixedBytes<4>; /// Configuration for an ACE (Atomic Clearing Engine) protocol #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] @@ -11,110 +12,74 @@ pub struct AceConfig { /// Whether this ACE config is enabled #[serde(default = "default_enabled")] pub enabled: bool, - /// Which ACE protocol this config is for - pub protocol: AceExchange, + /// The primary contract address for this ACE protocol (used as unique identifier) + pub contract_address: Address, /// Addresses that send ACE orders (used to identify force unlocks) pub from_addresses: HashSet
, /// Addresses that receive ACE orders (the ACE contract addresses) pub to_addresses: HashSet
, /// Storage slots that must be read to detect ACE interaction (e.g., _lastBlockUpdated at slot 3) pub detection_slots: HashSet, - /// Function signatures that indicate an unlock operation - pub unlock_signatures: HashSet, - /// Function signatures that indicate a forced unlock operation - pub force_signatures: HashSet, + /// Function selectors (4 bytes) that indicate an unlock operation + pub unlock_signatures: HashSet, + /// Function selectors (4 bytes) that indicate a forced unlock operation + pub force_signatures: HashSet, } fn default_enabled() -> bool { true } -/// What ACE based exchanges that rbuilder supports. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter, Deserialize, FromStr)] -pub enum AceExchange { - Angstrom, -} +/// Classify an ACE order interaction type based on state trace, simulation success, and config. +/// Uses both state trace (address access) AND function signatures to determine interaction type. +pub fn classify_ace_interaction( + state_trace: &UsedStateTrace, + sim_success: bool, + config: &AceConfig, + selector: Option, + tx_to: Option
, +) -> Option { + // Check that ALL detection slots are read or written from any of the ACE contract addresses + let all_slots_accessed = config.detection_slots.iter().all(|slot| { + config.to_addresses.iter().any(|addr| { + state_trace + .read_slot_values + .keys() + .any(|k| &k.address == addr && &k.key == slot) + || state_trace + .written_slot_values + .keys() + .any(|k| &k.address == addr && &k.key == slot) + }) + }); -impl AceExchange { - /// Classify an ACE order interaction type based on state trace, simulation success, and config. - /// Uses both state trace (address access) AND function signatures to determine interaction type. - pub fn classify_ace_interaction( - &self, - state_trace: &UsedStateTrace, - sim_success: bool, - config: &AceConfig, - selector: Option<&[u8]>, - tx_to: Option
, - ) -> Option { - match self { - AceExchange::Angstrom => Self::angstrom_classify_interaction( - state_trace, - sim_success, - *self, - config, - selector, - tx_to, - ), - } + if !all_slots_accessed { + return None; } - /// Angstrom-specific classification logic using both state trace and signatures - fn angstrom_classify_interaction( - state_trace: &UsedStateTrace, - sim_success: bool, - exchange: AceExchange, - config: &AceConfig, - selector: Option<&[u8]>, - tx_to: Option
, - ) -> Option { - // Check that ALL detection slots are read or written from any of the ACE contract addresses - let all_slots_accessed = config.detection_slots.iter().all(|slot| { - config.to_addresses.iter().any(|addr| { - state_trace - .read_slot_values - .keys() - .any(|k| &k.address == addr && &k.key == slot) - || state_trace - .written_slot_values - .keys() - .any(|k| &k.address == addr && &k.key == slot) - }) - }); - - if !all_slots_accessed { - return None; - } + // Check if this is a direct call to the protocol + let is_direct_protocol_call = tx_to.is_some_and(|to| config.to_addresses.contains(&to)); + + // Check function selectors with direct HashSet lookup + let is_force_sig = selector.is_some_and(|sel| config.force_signatures.contains(&sel)); + let is_unlock_sig = selector.is_some_and(|sel| config.unlock_signatures.contains(&sel)); - // Check if this is a direct call to the protocol - let is_direct_protocol_call = tx_to.is_some_and(|to| config.to_addresses.contains(&to)); - - // Check function signatures to determine if this is a force or regular unlock - let is_force_sig = selector.is_some_and(|sel| { - config - .force_signatures - .iter() - .any(|sig| sig.starts_with(sel)) - }); - - let is_unlock_sig = selector.is_some_and(|sel| { - config - .unlock_signatures - .iter() - .any(|sig| sig.starts_with(sel)) - }); - - if sim_success && (is_force_sig || is_unlock_sig) { - let source = if is_direct_protocol_call && is_force_sig { - AceUnlockSource::ProtocolForce - } else if is_direct_protocol_call && is_unlock_sig { - AceUnlockSource::ProtocolOptional - } else { - AceUnlockSource::User - }; - Some(AceInteraction::Unlocking { exchange, source }) + let contract_address = config.contract_address; + + if sim_success && (is_force_sig || is_unlock_sig) { + let source = if is_direct_protocol_call && is_force_sig { + AceUnlockSource::ProtocolForce + } else if is_direct_protocol_call && is_unlock_sig { + AceUnlockSource::ProtocolOptional } else { - Some(AceInteraction::NonUnlocking { exchange }) - } + AceUnlockSource::User + }; + Some(AceInteraction::Unlocking { + contract_address, + source, + }) + } else { + Some(AceInteraction::NonUnlocking { contract_address }) } } @@ -134,11 +99,11 @@ pub enum AceUnlockSource { pub enum AceInteraction { /// Unlocking ACE order - doesn't revert without an ACE order, must be placed with ACE bundle. Unlocking { - exchange: AceExchange, + contract_address: Address, source: AceUnlockSource, }, /// Requires an unlocking ACE order, will revert otherwise - NonUnlocking { exchange: AceExchange }, + NonUnlocking { contract_address: Address }, } impl AceInteraction { @@ -166,10 +131,12 @@ impl AceInteraction { ) } - pub fn get_exchange(&self) -> AceExchange { + pub fn get_contract_address(&self) -> Address { match self { - AceInteraction::Unlocking { exchange, .. } - | AceInteraction::NonUnlocking { exchange } => *exchange, + AceInteraction::Unlocking { + contract_address, .. + } + | AceInteraction::NonUnlocking { contract_address } => *contract_address, } } } diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index 69d9666da..38db23906 100644 --- a/crates/rbuilder/src/building/sim.rs +++ b/crates/rbuilder/src/building/sim.rs @@ -18,7 +18,9 @@ use alloy_primitives::Address; use alloy_primitives::U256; use alloy_rpc_types::TransactionTrait; use rand::seq::SliceRandom; -use rbuilder_primitives::ace::{AceExchange, AceInteraction, AceUnlockSource}; +use rbuilder_primitives::ace::{ + classify_ace_interaction, AceInteraction, AceUnlockSource, Selector, +}; use rbuilder_primitives::AceConfig; use rbuilder_primitives::BlockSpace; use rbuilder_primitives::SimValue; @@ -58,8 +60,8 @@ pub struct NonceKey { pub enum DependencyKey { /// Order needs a specific nonce to be filled Nonce(NonceKey), - /// Order needs an ACE unlock transaction for the given exchange - AceUnlock(AceExchange), + /// Order needs an ACE unlock transaction for the given contract address + AceUnlock(Address), } impl From for DependencyKey { @@ -142,10 +144,10 @@ pub struct SimTree { ready_orders: Vec, // ACE state management - /// ACE configuration lookup by exchange - ace_config: HashMap, - /// ACE exchange state (force/optional unlocks, mempool unlock tracking) - ace_state: HashMap, + /// ACE configuration lookup by contract address + ace_config: HashMap, + /// ACE state (force/optional unlocks, mempool unlock tracking) by contract address + ace_state: HashMap, } #[derive(Debug)] @@ -161,9 +163,9 @@ impl SimTree { let mut ace_state = HashMap::default(); for config in ace_configs { - let protocol = config.protocol; - ace_config.insert(protocol, config); - ace_state.insert(protocol, AceExchangeState::default()); + let contract_address = config.contract_address; + ace_config.insert(contract_address, config); + ace_state.insert(contract_address, AceExchangeState::default()); } Self { @@ -179,13 +181,13 @@ impl SimTree { } /// Get the ACE configs - pub fn ace_configs(&self) -> &HashMap { + pub fn ace_configs(&self) -> &HashMap { &self.ace_config } - /// Get the ACE exchange state for a given exchange - pub fn get_ace_state(&self, exchange: &AceExchange) -> Option<&AceExchangeState> { - self.ace_state.get(exchange) + /// Get the ACE state for a given contract address + pub fn get_ace_state(&self, contract_address: &Address) -> Option<&AceExchangeState> { + self.ace_state.get(contract_address) } fn push_order(&mut self, order: Order) -> Result<(), ProviderError> { @@ -301,9 +303,9 @@ impl SimTree { fn add_ace_dependency_for_order( &mut self, order: Order, - exchange: AceExchange, + contract_address: Address, ) -> Result<(), ProviderError> { - let dep_key = DependencyKey::AceUnlock(exchange); + let dep_key = DependencyKey::AceUnlock(contract_address); // Check if we already have an unlock provider if let Some(sim_id) = self.dependency_providers.get(&dep_key) { @@ -455,18 +457,24 @@ impl SimTree { let mut cancellation = None; match interaction { - AceInteraction::Unlocking { exchange, source } => { + AceInteraction::Unlocking { + contract_address, + source, + } => { // Register the unlock in ACE state - let state = self.ace_state.entry(exchange).or_default(); + let state = self.ace_state.entry(contract_address).or_default(); if source == AceUnlockSource::ProtocolForce { state.force_unlock_order = Some(result.simulated_order.clone()); - trace!("Added forced ACE protocol unlock order for {:?}", exchange); + trace!( + "Added forced ACE protocol unlock order for {:?}", + contract_address + ); } else { state.optional_unlock_order = Some(result.simulated_order.clone()); trace!( "Added optional ACE protocol unlock order for {:?}", - exchange + contract_address ); } @@ -478,7 +486,7 @@ impl SimTree { } // Make sure the ACE unlock dependency is in dependencies_satisfied - let dep_key = DependencyKey::AceUnlock(exchange); + let dep_key = DependencyKey::AceUnlock(contract_address); if !result.dependencies_satisfied.contains(&dep_key) { result.dependencies_satisfied.push(dep_key); } @@ -486,9 +494,9 @@ impl SimTree { // Process this result to unblock pending orders self.process_simulation_task_result(result.clone())?; } - AceInteraction::NonUnlocking { exchange } => { + AceInteraction::NonUnlocking { contract_address } => { // This is a mempool order that needs ACE unlock - let state = self.ace_state.entry(exchange).or_default(); + let state = self.ace_state.entry(contract_address).or_default(); // Check if we have an unlock order to use as parent if let Some(unlock_order) = state.get_unlock_order().cloned() { @@ -502,7 +510,7 @@ impl SimTree { // No unlock yet - add as pending on ACE dependency self.add_ace_dependency_for_order( result.simulated_order.order.clone(), - exchange, + contract_address, )?; } return Ok((true, None)); @@ -512,10 +520,10 @@ impl SimTree { Ok((true, cancellation)) } - /// Mark that a mempool unlocking order has been seen for an exchange. + /// Mark that a mempool unlocking order has been seen for a contract address. /// Returns the OrderId of the optional ACE order to cancel, if any. - pub fn mark_mempool_unlock(&mut self, exchange: AceExchange) -> Option { - let state = self.ace_state.entry(exchange).or_default(); + pub fn mark_mempool_unlock(&mut self, contract_address: Address) -> Option { + let state = self.ace_state.entry(contract_address).or_default(); // Only cancel once if state.has_mempool_unlock { @@ -622,10 +630,11 @@ where .collect(); // If this is an unlocking ACE order, add the ACE dependency - if let Some(AceInteraction::Unlocking { exchange, .. }) = - sim_order.ace_interaction + if let Some(AceInteraction::Unlocking { + contract_address, .. + }) = sim_order.ace_interaction { - dependencies_satisfied.push(DependencyKey::AceUnlock(exchange)); + dependencies_satisfied.push(DependencyKey::AceUnlock(contract_address)); } let result = SimulatedResult { @@ -659,7 +668,7 @@ pub fn simulate_order( ctx: &BlockBuildingContext, local_ctx: &mut ThreadBlockBuildingContext, state: &mut BlockState, - ace_configs: &HashMap, + ace_configs: &HashMap, ) -> Result { let mut tracer = AccumulatorSimulationTracer::new(); let mut fork = PartialBlockFork::new(state, ctx, local_ctx).with_tracer(&mut tracer); @@ -686,7 +695,7 @@ pub fn simulate_order_using_fork( order: Order, fork: &mut PartialBlockFork<'_, '_, '_, '_, Tracer, NullPartialBlockForkExecutionTracer>, mempool_tx_detector: &MempoolTxsDetector, - ace_configs: &HashMap, + ace_configs: &HashMap, ) -> Result { let start = Instant::now(); let has_parents = !parent_orders.is_empty(); @@ -724,11 +733,11 @@ pub fn simulate_order_using_fork( // 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<[u8; 4]>, Option
) = + 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([input[0], input[1], input[2], input[3]]) + Some(Selector::from_slice(&input[..4])) } else { None }; @@ -736,17 +745,11 @@ pub fn simulate_order_using_fork( }); let ace_interaction = used_state_trace.as_ref().and_then(|trace| { - ace_configs.iter().find_map(|(exchange, config)| { + ace_configs.iter().find_map(|(_, config)| { if !config.enabled { return None; } - exchange.classify_ace_interaction( - trace, - sim_success, - config, - selector.as_ref().map(|s| s.as_slice()), - tx_to, - ) + classify_ace_interaction(trace, sim_success, config, selector, tx_to) }) }); @@ -766,13 +769,15 @@ pub fn simulate_order_using_fork( } Err(err) => { // Check if failed order accessed ACE - if so, treat as successful with zero profit - if let Some(interaction @ AceInteraction::NonUnlocking { exchange }) = ace_interaction { + 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, - ?exchange, + ?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 diff --git a/crates/rbuilder/src/live_builder/simulation/mod.rs b/crates/rbuilder/src/live_builder/simulation/mod.rs index a35846dad..11382dea1 100644 --- a/crates/rbuilder/src/live_builder/simulation/mod.rs +++ b/crates/rbuilder/src/live_builder/simulation/mod.rs @@ -40,8 +40,7 @@ pub struct SimulationContext { /// Simulation results go out through this channel. pub results: mpsc::Sender, /// ACE configuration for this simulation context. - pub ace_configs: - ahash::HashMap, + pub ace_configs: ahash::HashMap, } /// All active SimulationContexts @@ -161,7 +160,7 @@ where let ace_configs_map: ahash::HashMap<_, _> = ace_config .iter() .filter(|c| c.enabled) - .map(|c| (c.protocol, c.clone())) + .map(|c| (c.contract_address, c.clone())) .collect(); let sim_tree = SimTree::new(nonces, ace_config); diff --git a/crates/rbuilder/src/live_builder/simulation/sim_worker.rs b/crates/rbuilder/src/live_builder/simulation/sim_worker.rs index 28e5ac13e..c96931c8a 100644 --- a/crates/rbuilder/src/live_builder/simulation/sim_worker.rs +++ b/crates/rbuilder/src/live_builder/simulation/sim_worker.rs @@ -85,10 +85,12 @@ pub fn run_sim_worker

( .collect(); // If this is an unlocking ACE order, add the ACE dependency - if let Some(AceInteraction::Unlocking { exchange, .. }) = - simulated_order.ace_interaction + if let Some(AceInteraction::Unlocking { + contract_address, .. + }) = simulated_order.ace_interaction { - dependencies_satisfied.push(DependencyKey::AceUnlock(exchange)); + dependencies_satisfied + .push(DependencyKey::AceUnlock(contract_address)); } let result = SimulatedResult { diff --git a/examples/config/rbuilder/config-live-example.toml b/examples/config/rbuilder/config-live-example.toml index 920172bc9..f826c126c 100644 --- a/examples/config/rbuilder/config-live-example.toml +++ b/examples/config/rbuilder/config-live-example.toml @@ -82,7 +82,8 @@ num_threads = 25 safe_sorting_only = false [[ace_protocols]] -protocol = "Angstrom" +# Contract address serves as unique identifier for this ACE protocol +contract_address = "0x0000000aa232009084Bd71A5797d089AA4Edfad4" from_addresses = [ "0xc41ae140ca9b281d8a1dc254c50e446019517d04", "0xd437f3372f3add2c2bc3245e6bd6f9c202e61bb3", From 6c751aa34b89d39fe039c4f14d9ea121ba03ada0 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 2 Dec 2025 13:45:36 -0500 Subject: [PATCH 20/51] fix: comment --- crates/rbuilder-primitives/src/ace.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rbuilder-primitives/src/ace.rs b/crates/rbuilder-primitives/src/ace.rs index 0c4b6c31b..b3e67e061 100644 --- a/crates/rbuilder-primitives/src/ace.rs +++ b/crates/rbuilder-primitives/src/ace.rs @@ -6,7 +6,7 @@ use std::collections::HashSet; /// 4-byte function selector pub type Selector = FixedBytes<4>; -/// Configuration for an ACE (Atomic Clearing Engine) protocol +/// Configuration for an ACE (Application Controlled Execution) protocol #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub struct AceConfig { /// Whether this ACE config is enabled From 05b1be2697bc845550a14fa28e5ed43bf0658b83 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Wed, 3 Dec 2025 14:30:36 -0500 Subject: [PATCH 21/51] feat: add tests for simtree --- crates/rbuilder-primitives/src/ace.rs | 474 ++++++++++++++++++ .../src/building/testing/ace_tests/mod.rs | 336 +++++++++++++ crates/rbuilder/src/building/testing/mod.rs | 2 + 3 files changed, 812 insertions(+) create mode 100644 crates/rbuilder/src/building/testing/ace_tests/mod.rs diff --git a/crates/rbuilder-primitives/src/ace.rs b/crates/rbuilder-primitives/src/ace.rs index b3e67e061..b38e9617e 100644 --- a/crates/rbuilder-primitives/src/ace.rs +++ b/crates/rbuilder-primitives/src/ace.rs @@ -149,3 +149,477 @@ pub enum AceUnlockType { /// Optional unlock, transaction can proceed with or without unlock Optional, } + +#[cfg(test)] +mod tests { + use super::*; + use crate::evm_inspector::{SlotKey, UsedStateTrace}; + use alloy_primitives::hex; + use alloy_primitives::{address, b256}; + + /// Create the real ACE config from the provided TOML configuration + fn real_ace_config() -> AceConfig { + AceConfig { + enabled: true, + contract_address: address!("0000000aa232009084Bd71A5797d089AA4Edfad4"), + from_addresses: HashSet::from([ + address!("c41ae140ca9b281d8a1dc254c50e446019517d04"), + address!("d437f3372f3add2c2bc3245e6bd6f9c202e61bb3"), + address!("693ca5c6852a7d212dabc98b28e15257465c11f3"), + ]), + to_addresses: HashSet::from([address!("0000000aa232009084Bd71A5797d089AA4Edfad4")]), + // _lastBlockUpdated storage slot (slot 3) + detection_slots: HashSet::from([b256!( + "0000000000000000000000000000000000000000000000000000000000000003" + )]), + // unlockWithEmptyAttestation(address,bytes) nonpayable - 0x1828e0e7 + unlock_signatures: HashSet::from([Selector::from_slice(&[0x18, 0x28, 0xe0, 0xe7])]), + // execute(bytes) nonpayable - 0x09c5eabe + force_signatures: HashSet::from([Selector::from_slice(&[0x09, 0xc5, 0xea, 0xbe])]), + } + } + + /// Create a mock state trace with the detection slot accessed + fn mock_state_trace_with_slot(addr: Address, slot: B256) -> UsedStateTrace { + let mut trace = UsedStateTrace::default(); + trace.read_slot_values.insert( + SlotKey { + address: addr, + key: slot, + }, + Default::default(), + ); + trace + } + + #[test] + fn test_real_ace_force_order_classification() { + // Test with real force order calldata + let config = real_ace_config(); + let contract = config.contract_address; + let detection_slot = *config.detection_slots.iter().next().unwrap(); + let force_selector = *config.force_signatures.iter().next().unwrap(); + + // Mock state trace with detection slot accessed + let trace = mock_state_trace_with_slot(contract, detection_slot); + + // Direct call to ACE contract with force signature should be ProtocolForce + let result = + classify_ace_interaction(&trace, true, &config, Some(force_selector), Some(contract)); + + assert_eq!( + result, + Some(AceInteraction::Unlocking { + contract_address: contract, + source: AceUnlockSource::ProtocolForce + }) + ); + + // Verify it's detected as force + assert!(result.unwrap().is_force()); + assert!(result.unwrap().is_protocol_tx()); + } + + #[test] + fn test_real_ace_unlock_order_classification() { + // Test with real unlock signature from config + let config = real_ace_config(); + let contract = config.contract_address; + let detection_slot = *config.detection_slots.iter().next().unwrap(); + let unlock_selector = *config.unlock_signatures.iter().next().unwrap(); + + // Mock state trace with detection slot accessed + let trace = mock_state_trace_with_slot(contract, detection_slot); + + // Direct call to ACE contract with unlock signature should be ProtocolOptional + let result = + classify_ace_interaction(&trace, true, &config, Some(unlock_selector), Some(contract)); + + assert_eq!( + result, + Some(AceInteraction::Unlocking { + contract_address: contract, + source: AceUnlockSource::ProtocolOptional + }) + ); + + // Verify it's protocol tx but not force + assert!(result.unwrap().is_protocol_tx()); + assert!(!result.unwrap().is_force()); + } + + #[test] + fn test_ace_user_unlock_indirect_call() { + // User transaction that calls ACE contract indirectly (not tx.to = contract) + let config = real_ace_config(); + let contract = config.contract_address; + let detection_slot = *config.detection_slots.iter().next().unwrap(); + let unlock_selector = *config.unlock_signatures.iter().next().unwrap(); + + let trace = mock_state_trace_with_slot(contract, detection_slot); + + // tx.to is NOT the ACE contract (indirect call via user tx) = User unlock + let result = classify_ace_interaction(&trace, true, &config, Some(unlock_selector), None); + + assert_eq!( + result, + Some(AceInteraction::Unlocking { + contract_address: contract, + source: AceUnlockSource::User + }) + ); + + // Verify it's unlocking but not a protocol tx + assert!(result.unwrap().is_unlocking()); + assert!(!result.unwrap().is_protocol_tx()); + } + + #[test] + fn test_ace_non_unlocking_interaction() { + // Transaction that accesses ACE slot but doesn't have unlock signature + let config = real_ace_config(); + let contract = config.contract_address; + let detection_slot = *config.detection_slots.iter().next().unwrap(); + + let trace = mock_state_trace_with_slot(contract, detection_slot); + + // No unlock/force signature = NonUnlocking + let result = classify_ace_interaction(&trace, true, &config, None, Some(contract)); + + assert_eq!( + result, + Some(AceInteraction::NonUnlocking { + contract_address: contract + }) + ); + + // Verify it's not an unlocking interaction + assert!(!result.unwrap().is_unlocking()); + } + + #[test] + fn test_ace_failed_sim_becomes_non_unlocking() { + // Even with unlock signature, failed simulation = NonUnlocking + let config = real_ace_config(); + let contract = config.contract_address; + let detection_slot = *config.detection_slots.iter().next().unwrap(); + let unlock_selector = *config.unlock_signatures.iter().next().unwrap(); + + let trace = mock_state_trace_with_slot(contract, detection_slot); + + // sim_success = false turns unlock into NonUnlocking + let result = classify_ace_interaction( + &trace, + false, + &config, + Some(unlock_selector), + Some(contract), + ); + + assert_eq!( + result, + Some(AceInteraction::NonUnlocking { + contract_address: contract + }) + ); + + // Failed sim should not be considered unlocking + assert!(!result.unwrap().is_unlocking()); + } + + #[test] + fn test_ace_no_slot_access_returns_none() { + // If detection slot is not accessed, no ACE interaction detected + let config = real_ace_config(); + let empty_trace = UsedStateTrace::default(); + let force_selector = *config.force_signatures.iter().next().unwrap(); + + // Even with valid force signature, no slot access = None + let result = classify_ace_interaction( + &empty_trace, + true, + &config, + Some(force_selector), + Some(config.contract_address), + ); + + assert_eq!( + result, None, + "Should return None when detection slot is not accessed" + ); + } + + #[test] + fn test_ace_wrong_slot_returns_none() { + // Accessing wrong slot should return None + let config = real_ace_config(); + let wrong_slot = b256!("0000000000000000000000000000000000000000000000000000000000000099"); + let force_selector = *config.force_signatures.iter().next().unwrap(); + + let trace = mock_state_trace_with_slot(config.contract_address, wrong_slot); + + // Wrong slot accessed = None (even with valid signature) + let result = classify_ace_interaction( + &trace, + true, + &config, + Some(force_selector), + Some(config.contract_address), + ); + + assert_eq!( + result, None, + "Should return None when wrong slot is accessed" + ); + } + + #[test] + fn test_ace_disabled_config() { + let mut config = real_ace_config(); + config.enabled = false; + + let contract = config.contract_address; + let detection_slot = *config.detection_slots.iter().next().unwrap(); + let force_selector = *config.force_signatures.iter().next().unwrap(); + + let trace = mock_state_trace_with_slot(contract, detection_slot); + + // Classification still works even if disabled - filtering happens at higher level + let result = + classify_ace_interaction(&trace, true, &config, Some(force_selector), Some(contract)); + + assert_eq!( + result, + Some(AceInteraction::Unlocking { + contract_address: contract, + source: AceUnlockSource::ProtocolForce + }) + ); + } + + #[test] + fn test_ace_interaction_is_unlocking() { + let contract = address!("0000000aa232009084Bd71A5797d089AA4Edfad4"); + + let force = AceInteraction::Unlocking { + contract_address: contract, + source: AceUnlockSource::ProtocolForce, + }; + assert!(force.is_unlocking()); + + let optional = AceInteraction::Unlocking { + contract_address: contract, + source: AceUnlockSource::ProtocolOptional, + }; + assert!(optional.is_unlocking()); + + let user = AceInteraction::Unlocking { + contract_address: contract, + source: AceUnlockSource::User, + }; + assert!(user.is_unlocking()); + + let non_unlocking = AceInteraction::NonUnlocking { + contract_address: contract, + }; + assert!(!non_unlocking.is_unlocking()); + } + + #[test] + fn test_ace_interaction_is_protocol_tx() { + let contract = address!("0000000aa232009084Bd71A5797d089AA4Edfad4"); + + let force = AceInteraction::Unlocking { + contract_address: contract, + source: AceUnlockSource::ProtocolForce, + }; + assert!(force.is_protocol_tx()); + + let optional = AceInteraction::Unlocking { + contract_address: contract, + source: AceUnlockSource::ProtocolOptional, + }; + assert!(optional.is_protocol_tx()); + + let user = AceInteraction::Unlocking { + contract_address: contract, + source: AceUnlockSource::User, + }; + assert!(!user.is_protocol_tx()); + } + + #[test] + fn test_ace_interaction_is_force() { + let contract = address!("0000000aa232009084Bd71A5797d089AA4Edfad4"); + + let force = AceInteraction::Unlocking { + contract_address: contract, + source: AceUnlockSource::ProtocolForce, + }; + assert!(force.is_force()); + + let optional = AceInteraction::Unlocking { + contract_address: contract, + source: AceUnlockSource::ProtocolOptional, + }; + assert!(!optional.is_force()); + + let user = AceInteraction::Unlocking { + contract_address: contract, + source: AceUnlockSource::User, + }; + assert!(!user.is_force()); + } + + #[test] + fn test_ace_interaction_get_contract_address() { + let contract = address!("0000000aa232009084Bd71A5797d089AA4Edfad4"); + + let unlocking = AceInteraction::Unlocking { + contract_address: contract, + source: AceUnlockSource::ProtocolForce, + }; + assert_eq!(unlocking.get_contract_address(), contract); + + let non_unlocking = AceInteraction::NonUnlocking { + contract_address: contract, + }; + assert_eq!(non_unlocking.get_contract_address(), contract); + } + + #[test] + fn test_force_signature_from_real_calldata() { + // The provided calldata starts with 0x09c5eabe (execute function) + let calldata = hex::decode("09c5eabe000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002950000cca0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000003e6d1e500000000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000003675af200000000000000000000008cff47e70a0000000000000000006f8f0c22bbdf00dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000000000000000000000000000000000000001f548eb0000000000000000000000000000000000004c00000001000000000000000000000000000000000000003d82768a1dd582a9887587911fe9180001000200010000000000000000000000000000000000000000000000002b708452feb67dac0000660200000000000000000000004a458c968ab800000000000000000000000000000003ba0000000000000000010e8724bdb79c980300010000000000000000002548f28ce93ff600000000000000000000008cff47e70a000000000000000000225d82a810177e000108090000000000000000004a458c968ab80000000000000000000000000003e6ce2b000000000000000000000000000000010000000000000000000000000000000000001c2514149461c050689a85a1a293766501a00feab18c79d5b3cacb8c4052c9c0ea432416a6b9b672896d6596a3fa25fb765ab8a0245e2ebacfde1ed5a42786a6d60b00000000000000000025497f8c31270000000000000000000000000001f548eb00000000000000000000000003675af200000000000000000000000003675af200011c833917577a24b35aca558dcee9b4ab547c419f53a6b8b4e353e23ba811b956c35ef19655e4695da96e6e85f36c84db41d860eb7c267466cd1c0ebe581196086a0000000000000000000000000000").unwrap(); + + // Extract first 4 bytes + let selector = Selector::from_slice(&calldata[..4]); + + // Should match the force signature + let expected_force_sig = Selector::from_slice(&[0x09, 0xc5, 0xea, 0xbe]); + assert_eq!(selector, expected_force_sig); + + // Verify it's in the config + let config = real_ace_config(); + assert!(config.force_signatures.contains(&selector)); + } + + #[test] + fn test_optional_unlock_signature_from_real_transaction() { + let calldata = hex::decode("1828e0e7000000000000000000000000c41ae140ca9b281d8a1dc254c50e446019517d0400000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000041c28cfd9fd7ffdce92022dcd0116088a1a0b1a9fb2124f55dce50ec39a10b9ad819f4ca93c677b0952c90389a4e1af98f9770fe4f3cdfa7b2fa30ecbd2c01a9bf1c00000000000000000000000000000000000000000000000000000000000000").unwrap(); + + // Extract first 4 bytes (function selector) + let selector = Selector::from_slice(&calldata[..4]); + + // Should match the optional unlock signature: unlockWithEmptyAttestation(address,bytes) + let expected_unlock_sig = Selector::from_slice(&[0x18, 0x28, 0xe0, 0xe7]); + assert_eq!(selector, expected_unlock_sig); + + // Verify it's in the config as an unlock signature + let config = real_ace_config(); + assert!(config.unlock_signatures.contains(&selector)); + assert!(!config.force_signatures.contains(&selector)); // Should NOT be in force + } + + #[test] + fn test_optional_unlock_with_real_config() { + // Test complete optional unlock classification with real config + let config = real_ace_config(); + let contract = config.contract_address; + let slot = b256!("0000000000000000000000000000000000000000000000000000000000000003"); + + // Optional unlock signature from real transaction + let unlock_selector = Selector::from_slice(&[0x18, 0x28, 0xe0, 0xe7]); + + // Mock state trace showing slot 3 was accessed + let trace = mock_state_trace_with_slot(contract, slot); + + // Test 1: Direct call to ACE contract = ProtocolOptional + let result = + classify_ace_interaction(&trace, true, &config, Some(unlock_selector), Some(contract)); + + assert_eq!( + result, + Some(AceInteraction::Unlocking { + contract_address: contract, + source: AceUnlockSource::ProtocolOptional + }) + ); + + // Test 2: Indirect call (user tx) = User unlock + let result_indirect = + classify_ace_interaction(&trace, true, &config, Some(unlock_selector), None); + + assert_eq!( + result_indirect, + Some(AceInteraction::Unlocking { + contract_address: contract, + source: AceUnlockSource::User + }) + ); + + // Test 3: Failed simulation with unlock signature = NonUnlocking + let result_failed = classify_ace_interaction( + &trace, + false, + &config, + Some(unlock_selector), + Some(contract), + ); + + assert_eq!( + result_failed, + Some(AceInteraction::NonUnlocking { + contract_address: contract + }) + ); + } + + #[test] + fn test_distinguish_force_vs_optional_signatures() { + // Verify that force and optional signatures are distinct + let config = real_ace_config(); + + let force_sig = Selector::from_slice(&[0x09, 0xc5, 0xea, 0xbe]); // execute + let unlock_sig = Selector::from_slice(&[0x18, 0x28, 0xe0, 0xe7]); // unlockWithEmptyAttestation + + // Force signature should only be in force_signatures + assert!(config.force_signatures.contains(&force_sig)); + assert!(!config.unlock_signatures.contains(&force_sig)); + + // Unlock signature should only be in unlock_signatures + assert!(config.unlock_signatures.contains(&unlock_sig)); + assert!(!config.force_signatures.contains(&unlock_sig)); + } + + #[test] + fn test_slot_written_also_detected() { + // Test that writing to the detection slot is also detected (not just reading) + let config = real_ace_config(); + let contract = config.contract_address; + let detection_slot = *config.detection_slots.iter().next().unwrap(); + let force_selector = *config.force_signatures.iter().next().unwrap(); + + let mut trace = UsedStateTrace::default(); + // Write to slot instead of reading + trace.written_slot_values.insert( + SlotKey { + address: contract, + key: detection_slot, + }, + Default::default(), + ); + + // Writing to detection slot should still trigger classification + let result = + classify_ace_interaction(&trace, true, &config, Some(force_selector), Some(contract)); + + assert_eq!( + result, + Some(AceInteraction::Unlocking { + contract_address: contract, + source: AceUnlockSource::ProtocolForce + }) + ); + } +} 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..7dca37888 --- /dev/null +++ b/crates/rbuilder/src/building/testing/ace_tests/mod.rs @@ -0,0 +1,336 @@ +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; + +// ============================================================================ +// Test Infrastructure & Helper Functions +// ============================================================================ + +/// 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, + }), + }) +} + +// ============================================================================ +// 1. Dependency Tracking Tests +// ============================================================================ + +#[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); +} + +// ============================================================================ +// 2. SimTree Initialization Tests +// ============================================================================ + +#[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(()) +} + +// ============================================================================ +// 3. Cancellation Tests +// ============================================================================ + +#[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]); + + // Mark mempool unlock BEFORE optional order arrives + let cancelled = sim_tree.mark_mempool_unlock(contract_addr); + assert_eq!(cancelled, None); // Nothing to cancel yet + + // Now add optional unlock order + let optional_order = create_optional_unlock_order(contract_addr, 50_000); + + // Create SimulatedResult for handle_ace_interaction + 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 (handled, cancellation) = sim_tree.handle_ace_interaction(&mut result)?; + + // Should be handled and immediately cancelled + assert!(handled); + 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(()) +} + +// ============================================================================ +// 4. Dependency Key Tests +// ============================================================================ + +#[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/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; From caea1e205f4209a3a322114191dd29622dcd0a93 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Wed, 3 Dec 2025 14:31:49 -0500 Subject: [PATCH 22/51] some cleanup --- .../src/building/testing/ace_tests/mod.rs | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/crates/rbuilder/src/building/testing/ace_tests/mod.rs b/crates/rbuilder/src/building/testing/ace_tests/mod.rs index 7dca37888..a09031b3e 100644 --- a/crates/rbuilder/src/building/testing/ace_tests/mod.rs +++ b/crates/rbuilder/src/building/testing/ace_tests/mod.rs @@ -9,10 +9,6 @@ use std::collections::HashSet; use std::sync::Arc; use uuid::Uuid; -// ============================================================================ -// Test Infrastructure & Helper Functions -// ============================================================================ - /// Create a minimal order for testing (empty bundle with unique ID) fn create_test_order() -> Order { Order::Bundle(Bundle { @@ -118,10 +114,6 @@ fn create_non_unlocking_order(contract: Address, gas_used: u64) -> Arc eyre::Result<()> { let test_chain = TestChainState::new(BlockArgs::default())?; @@ -234,10 +222,6 @@ fn test_multiple_ace_contracts() -> eyre::Result<()> { Ok(()) } -// ============================================================================ -// 3. Cancellation Tests -// ============================================================================ - #[test] fn test_mark_mempool_unlock_basic() -> eyre::Result<()> { let test_chain = TestChainState::new(BlockArgs::default())?; @@ -279,14 +263,11 @@ fn test_handle_ace_interaction_with_mempool_unlock() -> eyre::Result<()> { let mut sim_tree = SimTree::new(nonce_cache, vec![ace_config]); - // Mark mempool unlock BEFORE optional order arrives let cancelled = sim_tree.mark_mempool_unlock(contract_addr); assert_eq!(cancelled, None); // Nothing to cancel yet - // Now add optional unlock order let optional_order = create_optional_unlock_order(contract_addr, 50_000); - // Create SimulatedResult for handle_ace_interaction let mut result = SimulatedResult { id: rand::random(), simulated_order: optional_order.clone(), @@ -309,10 +290,6 @@ fn test_handle_ace_interaction_with_mempool_unlock() -> eyre::Result<()> { Ok(()) } -// ============================================================================ -// 4. Dependency Key Tests -// ============================================================================ - #[test] fn test_dependency_key_from_nonce() { let nonce_key = NonceKey { From 0eff0aa1b2fd614e2e721df464a501973912a887 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Wed, 3 Dec 2025 15:45:15 -0500 Subject: [PATCH 23/51] cleanup --- crates/rbuilder-primitives/src/ace.rs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/crates/rbuilder-primitives/src/ace.rs b/crates/rbuilder-primitives/src/ace.rs index b38e9617e..2b9495267 100644 --- a/crates/rbuilder-primitives/src/ace.rs +++ b/crates/rbuilder-primitives/src/ace.rs @@ -575,23 +575,6 @@ mod tests { ); } - #[test] - fn test_distinguish_force_vs_optional_signatures() { - // Verify that force and optional signatures are distinct - let config = real_ace_config(); - - let force_sig = Selector::from_slice(&[0x09, 0xc5, 0xea, 0xbe]); // execute - let unlock_sig = Selector::from_slice(&[0x18, 0x28, 0xe0, 0xe7]); // unlockWithEmptyAttestation - - // Force signature should only be in force_signatures - assert!(config.force_signatures.contains(&force_sig)); - assert!(!config.unlock_signatures.contains(&force_sig)); - - // Unlock signature should only be in unlock_signatures - assert!(config.unlock_signatures.contains(&unlock_sig)); - assert!(!config.force_signatures.contains(&unlock_sig)); - } - #[test] fn test_slot_written_also_detected() { // Test that writing to the detection slot is also detected (not just reading) From 2c291285c190b4cbc690987a9d4d0422c7fc76f9 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Fri, 5 Dec 2025 12:55:35 -0500 Subject: [PATCH 24/51] fix: slot access logic --- crates/rbuilder-primitives/src/ace.rs | 32 +++++++++++++++------------ 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/crates/rbuilder-primitives/src/ace.rs b/crates/rbuilder-primitives/src/ace.rs index 2b9495267..61a83e0df 100644 --- a/crates/rbuilder-primitives/src/ace.rs +++ b/crates/rbuilder-primitives/src/ace.rs @@ -1,4 +1,4 @@ -use crate::evm_inspector::UsedStateTrace; +use crate::evm_inspector::{SlotKey, UsedStateTrace}; use alloy_primitives::{Address, FixedBytes, B256}; use serde::Deserialize; use std::collections::HashSet; @@ -39,21 +39,25 @@ pub fn classify_ace_interaction( selector: Option, tx_to: Option

, ) -> Option { - // Check that ALL detection slots are read or written from any of the ACE contract addresses - let all_slots_accessed = config.detection_slots.iter().all(|slot| { - config.to_addresses.iter().any(|addr| { - state_trace - .read_slot_values - .keys() - .any(|k| &k.address == addr && &k.key == slot) - || state_trace - .written_slot_values - .keys() - .any(|k| &k.address == addr && &k.key == slot) + let any_ace_slots_accessed = config + .to_addresses + .iter() + .map(|address| { + config.detection_slots.iter().map(|slot| SlotKey { + address: *address, + key: *slot, + }) + }) + .flatten() + .flat_map(|key| { + [ + state_trace.read_slot_values.get(&key).is_some(), + state_trace.written_slot_values.get(&key).is_some(), + ] }) - }); + .any(|read_slot_of_interest| read_slot_of_interest); - if !all_slots_accessed { + if !any_ace_slots_accessed { return None; } From 850ea7ac971ee898ef95fc6af1f819b7588e09b2 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Fri, 5 Dec 2025 15:10:28 -0500 Subject: [PATCH 25/51] feat: refactor sim_job --- Cargo.lock | 3 +- crates/rbuilder/src/backtest/execute.rs | 16 +++- crates/rbuilder/src/building/sim.rs | 44 +++++++--- .../src/building/testing/ace_tests/mod.rs | 6 +- .../live_builder/simulation/simulation_job.rs | 82 +++++++------------ 5 files changed, 81 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f20fb1522..66993f7cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9645,9 +9645,8 @@ dependencies = [ "serde_json", "serde_with", "sha2 0.10.9", - "ssz_types 0.8.0", - "strum 0.27.2", "ssz_types", + "strum 0.27.2", "thiserror 1.0.69", "time", "tracing", diff --git a/crates/rbuilder/src/backtest/execute.rs b/crates/rbuilder/src/backtest/execute.rs index b2e5d5d9c..eb8cf8379 100644 --- a/crates/rbuilder/src/backtest/execute.rs +++ b/crates/rbuilder/src/backtest/execute.rs @@ -6,7 +6,9 @@ use crate::{ NullPartialBlockExecutionTracer, OrderErr, SimulatedOrderSink, SimulatedOrderStore, TransactionErr, }, - live_builder::{block_list_provider::BlockList, cli::LiveBuilderConfig}, + live_builder::{ + block_list_provider::BlockList, cli::LiveBuilderConfig, simulation::SimulatedOrderCommand, + }, provider::StateProviderFactory, utils::{clean_extradata, mevblocker::get_mevblocker_price, Signer}, }; @@ -16,6 +18,7 @@ use rbuilder_primitives::{OrderId, SimulatedOrder}; use reth_chainspec::ChainSpec; use serde::{Deserialize, Serialize}; use std::{cell::RefCell, rc::Rc, sync::Arc}; +use tokio::sync::mpsc; use super::OrdersWithTimestamp; @@ -106,8 +109,15 @@ where ctx.mempool_tx_detector.add_tx(order); } - let (sim_orders, sim_errors) = - simulate_all_orders_with_sim_tree(provider, &ctx, &orders, false, vec![])?; + let (cancellation_sender, _) = mpsc::channel::(1); + let (sim_orders, sim_errors) = simulate_all_orders_with_sim_tree( + provider, + &ctx, + &orders, + false, + vec![], + &cancellation_sender, + )?; // Apply bundle merging as in live building. let order_store = Rc::new(RefCell::new(SimulatedOrderStore::new())); diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index 026b417b4..bd3ea64f6 100644 --- a/crates/rbuilder/src/building/sim.rs +++ b/crates/rbuilder/src/building/sim.rs @@ -8,7 +8,9 @@ use crate::{ order_is_worth_executing, BlockBuildingContext, BlockBuildingSpaceState, BlockState, CriticalCommitOrderError, NullPartialBlockForkExecutionTracer, }, - live_builder::order_input::mempool_txs_detector::MempoolTxsDetector, + live_builder::{ + order_input::mempool_txs_detector::MempoolTxsDetector, simulation::SimulatedOrderCommand, + }, provider::StateProviderFactory, telemetry::{add_order_simulation_time, mark_order_pending_nonce}, utils::NonceCache, @@ -33,6 +35,7 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; +use tokio::sync::mpsc; use tracing::{error, trace}; #[derive(Debug)] @@ -437,9 +440,9 @@ impl SimTree { } /// Handle ACE interaction after simulation. - /// Returns (was_handled, optional_cancellation_order_id) - /// - For Unlocking interactions: registers as force or optional unlock provider - /// - For NonUnlocking interactions: adds order as pending on ACE unlock dependency + /// Returns (skip_forwarding, optional_cancellation_order_id) + /// - For Unlocking interactions: registers as force or optional unlock provider, skip_forwarding=false + /// - For NonUnlocking interactions: adds order as pending on ACE unlock dependency, skip_forwarding=true /// - Returns cancellation OrderId if a mempool unlock cancels an optional ACE tx pub fn handle_ace_interaction( &mut self, @@ -517,7 +520,7 @@ impl SimTree { } } - Ok((true, cancellation)) + Ok((false, cancellation)) } /// Mark that a mempool unlocking order has been seen for a contract address. @@ -538,13 +541,33 @@ impl SimTree { .map(|order| order.order.id()) } + /// Process simulation results, handling ACE interactions and updating dependencies. + /// Filters out orders that should not be forwarded (e.g., ACE orders queued for re-sim). + /// Sends cancellation commands via the provided sender. pub fn submit_simulation_tasks_results( &mut self, - results: Vec, + results: &mut Vec, + cancellation_sender: &mpsc::Sender, ) -> 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(()) } } @@ -558,6 +581,7 @@ pub fn simulate_all_orders_with_sim_tree

( orders: &[Order], randomize_insertion: bool, ace_config: Vec, + cancellation_sender: &mpsc::Sender, ) -> Result<(Vec>, Vec), CriticalCommitOrderError> where P: StateProviderFactory + Clone, @@ -648,7 +672,7 @@ where } } } - sim_tree.submit_simulation_tasks_results(sim_results)?; + sim_tree.submit_simulation_tasks_results(&mut sim_results, cancellation_sender)?; } Ok(( diff --git a/crates/rbuilder/src/building/testing/ace_tests/mod.rs b/crates/rbuilder/src/building/testing/ace_tests/mod.rs index a09031b3e..de4d45018 100644 --- a/crates/rbuilder/src/building/testing/ace_tests/mod.rs +++ b/crates/rbuilder/src/building/testing/ace_tests/mod.rs @@ -277,10 +277,10 @@ fn test_handle_ace_interaction_with_mempool_unlock() -> eyre::Result<()> { }; // Handle the ACE interaction - let (handled, cancellation) = sim_tree.handle_ace_interaction(&mut result)?; + let (skip_forwarding, cancellation) = sim_tree.handle_ace_interaction(&mut result)?; - // Should be handled and immediately cancelled - assert!(handled); + // 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 diff --git a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs index 5709c8c5c..36efdb027 100644 --- a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs +++ b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs @@ -190,9 +190,10 @@ 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 { + + // Collect stats and filter to in-flight orders + for sim_result in new_sim_results.iter() { 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"); @@ -204,66 +205,43 @@ impl SimulationJob { self.orders_with_replacement_key_sim_ok += 1; } - // Handle ACE interactions through the SimTree's dependency system - // NonUnlocking ACE orders get added as pending, Unlocking orders provide the dependency - match self.sim_tree.handle_ace_interaction(sim_result) { - Ok((handled, cancellation)) => { - // Send cancellation for optional ACE tx if needed - if let Some(cancel_id) = cancellation { - let _ = self - .slot_sim_results_sender - .try_send(SimulatedOrderCommand::Cancellation(cancel_id)); - } - // If this was a non-unlocking ACE tx that got queued for re-sim, skip forwarding - if handled - && sim_result - .simulated_order - .ace_interaction - .is_some_and(|i| !i.is_unlocking()) - { - continue; - } - } - Err(err) => { - error!(?err, "Failed to handle ACE interaction"); - } - } - - // 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); - } - } } } - // 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; } + + // Send filtered results downstream + for sim_result in &valid_simulated_orders { + 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; + } else { + self.sim_tracer.update_simulation_sent(sim_result); + } + } + } + true } From 5ed27855cdb290ddc5616a93120f539383ccf12c Mon Sep 17 00:00:00 2001 From: Will Smith Date: Fri, 5 Dec 2025 15:17:05 -0500 Subject: [PATCH 26/51] feat: cleanup simjob --- .../live_builder/simulation/simulation_job.rs | 55 +++++++++---------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs index 36efdb027..7ce0541e2 100644 --- a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs +++ b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs @@ -20,7 +20,7 @@ use super::SimulatedOrderCommand; /// Create and call run() /// The flow is: /// 1 New orders are polled from new_order_sub and inserted en the SimTree. -/// 2 SimTree is polled for dependency-ready orders and are sent to be simulated (sent to sim_req_sender). +/// 2 SimTree is polled for nonce-ready orders and are sent to be simulated (sent to sim_req_sender). /// 3 Simulation results are polled from sim_results_receiver and sent to slot_sim_results_sender. /// Cancellation flow: we add every order we start to process to in_flight_orders. /// If we get a cancellation and the order is not in in_flight_orders we forward the cancellation. @@ -190,58 +190,53 @@ impl SimulationJob { &mut self, new_sim_results: &mut Vec, ) -> bool { + // send results let mut valid_simulated_orders = Vec::new(); - - // Collect stats and filter to in-flight orders - for sim_result in new_sim_results.iter() { + 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); + } + } } } - + // update simtree 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; } - - // Send filtered results downstream - for sim_result in &valid_simulated_orders { - 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; - } else { - self.sim_tracer.update_simulation_sent(sim_result); - } - } - } - true } @@ -325,7 +320,7 @@ impl fmt::Debug for OrderCounter { self.total(), self.mempool_txs, self.bundles, - self.share_bundles, + self.share_bundles ) } } From a643176aafab59d0807d625c35cf48565b35d9bb Mon Sep 17 00:00:00 2001 From: Will Smith Date: Mon, 8 Dec 2025 13:07:48 -0500 Subject: [PATCH 27/51] fix: clippy --- Cargo.lock | 1 - crates/rbuilder-primitives/src/ace.rs | 7 +++---- crates/rbuilder/Cargo.toml | 1 - 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 66993f7cb..a39f4e751 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9532,7 +9532,6 @@ dependencies = [ "sha2 0.10.9", "shellexpand", "sqlx", - "strum 0.27.2", "sysperf", "tempfile", "test_utils", diff --git a/crates/rbuilder-primitives/src/ace.rs b/crates/rbuilder-primitives/src/ace.rs index 61a83e0df..7308b21ed 100644 --- a/crates/rbuilder-primitives/src/ace.rs +++ b/crates/rbuilder-primitives/src/ace.rs @@ -42,17 +42,16 @@ pub fn classify_ace_interaction( let any_ace_slots_accessed = config .to_addresses .iter() - .map(|address| { + .flat_map(|address| { config.detection_slots.iter().map(|slot| SlotKey { address: *address, key: *slot, }) }) - .flatten() .flat_map(|key| { [ - state_trace.read_slot_values.get(&key).is_some(), - state_trace.written_slot_values.get(&key).is_some(), + state_trace.read_slot_values.contains_key(&key), + state_trace.written_slot_values.contains_key(&key), ] }) .any(|read_slot_of_interest| read_slot_of_interest); diff --git a/crates/rbuilder/Cargo.toml b/crates/rbuilder/Cargo.toml index e5fd8b156..35210f2af 100644 --- a/crates/rbuilder/Cargo.toml +++ b/crates/rbuilder/Cargo.toml @@ -129,7 +129,6 @@ schnellru = "0.2.4" reipc = { git = "https://github.com/nethermindeth/reipc.git", rev = "b0b70735cda6273652212d1591188642e3449ed7" } quick_cache = "0.6.11" -strum = "0.27.2" [target.'cfg(not(target_env = "msvc"))'.dependencies] tikv-jemallocator = "0.6" From e947f1afc1638b6b1e949455745a05bda61731cd Mon Sep 17 00:00:00 2001 From: Will Smith Date: Mon, 5 Jan 2026 15:57:10 -0400 Subject: [PATCH 28/51] feat: add ability to disable ace + remove hacky code --- crates/rbuilder/src/backtest/execute.rs | 16 +- crates/rbuilder/src/building/sim.rs | 408 ++++++++++-------- .../src/building/testing/ace_tests/mod.rs | 11 +- .../rbuilder/src/live_builder/base_config.rs | 10 + .../rbuilder/src/live_builder/building/mod.rs | 4 + crates/rbuilder/src/live_builder/mod.rs | 2 + .../order_flow_tracing/order_flow_tracer.rs | 24 +- .../src/live_builder/simulation/mod.rs | 26 +- .../src/live_builder/simulation/sim_worker.rs | 29 +- .../live_builder/simulation/simulation_job.rs | 95 ++-- 10 files changed, 368 insertions(+), 257 deletions(-) diff --git a/crates/rbuilder/src/backtest/execute.rs b/crates/rbuilder/src/backtest/execute.rs index eb8cf8379..b2e5d5d9c 100644 --- a/crates/rbuilder/src/backtest/execute.rs +++ b/crates/rbuilder/src/backtest/execute.rs @@ -6,9 +6,7 @@ use crate::{ NullPartialBlockExecutionTracer, OrderErr, SimulatedOrderSink, SimulatedOrderStore, TransactionErr, }, - live_builder::{ - block_list_provider::BlockList, cli::LiveBuilderConfig, simulation::SimulatedOrderCommand, - }, + live_builder::{block_list_provider::BlockList, cli::LiveBuilderConfig}, provider::StateProviderFactory, utils::{clean_extradata, mevblocker::get_mevblocker_price, Signer}, }; @@ -18,7 +16,6 @@ use rbuilder_primitives::{OrderId, SimulatedOrder}; use reth_chainspec::ChainSpec; use serde::{Deserialize, Serialize}; use std::{cell::RefCell, rc::Rc, sync::Arc}; -use tokio::sync::mpsc; use super::OrdersWithTimestamp; @@ -109,15 +106,8 @@ where ctx.mempool_tx_detector.add_tx(order); } - let (cancellation_sender, _) = mpsc::channel::(1); - let (sim_orders, sim_errors) = simulate_all_orders_with_sim_tree( - provider, - &ctx, - &orders, - false, - vec![], - &cancellation_sender, - )?; + let (sim_orders, sim_errors) = + simulate_all_orders_with_sim_tree(provider, &ctx, &orders, false, vec![])?; // Apply bundle merging as in live building. let order_store = Rc::new(RefCell::new(SimulatedOrderStore::new())); diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index bd3ea64f6..25d892f1c 100644 --- a/crates/rbuilder/src/building/sim.rs +++ b/crates/rbuilder/src/building/sim.rs @@ -8,24 +8,19 @@ use crate::{ order_is_worth_executing, BlockBuildingContext, BlockBuildingSpaceState, BlockState, CriticalCommitOrderError, NullPartialBlockForkExecutionTracer, }, - live_builder::{ - order_input::mempool_txs_detector::MempoolTxsDetector, simulation::SimulatedOrderCommand, - }, + live_builder::order_input::mempool_txs_detector::MempoolTxsDetector, provider::StateProviderFactory, telemetry::{add_order_simulation_time, mark_order_pending_nonce}, utils::NonceCache, }; use ahash::{HashMap, HashSet}; use alloy_primitives::Address; -use alloy_primitives::U256; use alloy_rpc_types::TransactionTrait; use rand::seq::SliceRandom; use rbuilder_primitives::ace::{ classify_ace_interaction, AceInteraction, AceUnlockSource, Selector, }; use rbuilder_primitives::AceConfig; -use rbuilder_primitives::BlockSpace; -use rbuilder_primitives::SimValue; use rbuilder_primitives::{Order, OrderId, SimulatedOrder}; use reth_errors::ProviderError; use reth_provider::StateProvider; @@ -35,14 +30,21 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use tokio::sync::mpsc; use tracing::{error, trace}; #[derive(Debug)] #[allow(clippy::large_enum_variant)] pub enum OrderSimResult { + /// Order simulated successfully Success(Arc, Vec<(Address, u64)>), + /// Order simulation failed Failed(OrderErr), + /// Order failed but accessed an ACE exchange that wasn't unlocked. + /// This order should be queued for re-simulation once the unlock tx is available. + NonUnlockingAce { + order: Order, + contract_address: Address, + }, } #[derive(Debug)] @@ -117,16 +119,27 @@ pub struct SimulationRequest { pub id: SimulationId, pub order: Order, pub parents: Vec, + /// Whether any parent is an ACE unlock tx (vs just nonce dependencies) + pub has_ace_unlock_parent: bool, } #[derive(Debug, Clone, PartialEq, Eq)] -pub struct SimulatedResult { - pub id: SimulationId, - pub simulated_order: Arc, - pub previous_orders: Vec, - /// Dependencies this simulation satisfies (nonces updated, ACE unlocks provided) - pub dependencies_satisfied: Vec, - pub simulation_time: Duration, +#[allow(clippy::large_enum_variant)] +pub enum SimulatedResult { + /// Successful simulation + Success { + id: SimulationId, + simulated_order: Arc, + previous_orders: Vec, + /// Dependencies this simulation satisfies (nonces updated, ACE unlocks provided) + dependencies_satisfied: Vec, + simulation_time: Duration, + }, + /// Order failed due to locked ACE - needs re-simulation with unlock parent + NonUnlockingAce { + order: Order, + contract_address: Address, + }, } // @Feat replaceable orders @@ -228,6 +241,7 @@ impl SimTree { id: rand::random(), order, parents, + has_ace_unlock_parent: false, }); } } @@ -284,8 +298,15 @@ impl SimTree { if let Some(sim_id) = self.dependency_providers.get(&dep_key) { // we have something that fills this nonce let sim = self.sims.get(sim_id).expect("we never delete sims"); - parent_orders.extend_from_slice(&sim.previous_orders); - parent_orders.push(sim.simulated_order.order.clone()); + if let SimulatedResult::Success { + previous_orders, + simulated_order, + .. + } = sim + { + parent_orders.extend_from_slice(previous_orders); + parent_orders.push(simulated_order.order.clone()); + } continue; } @@ -313,29 +334,38 @@ impl SimTree { // Check if we already have an unlock provider if let Some(sim_id) = self.dependency_providers.get(&dep_key) { let sim = self.sims.get(sim_id).expect("we never delete sims"); - let mut parents = sim.previous_orders.clone(); - parents.push(sim.simulated_order.order.clone()); + if let SimulatedResult::Success { + previous_orders, + simulated_order, + .. + } = sim + { + let mut parents = previous_orders.clone(); + parents.push(simulated_order.order.clone()); - // Order is ready with the unlock tx as parent - self.ready_orders.push(SimulationRequest { - id: rand::random(), - order, - parents, - }); - } else { - // No unlock yet - add to pending - self.pending_dependencies - .entry(dep_key) - .or_default() - .push(order.id()); - self.pending_orders.insert( - order.id(), - PendingOrder { + // Order is ready with the unlock tx as parent + self.ready_orders.push(SimulationRequest { + id: rand::random(), order, - unsatisfied_dependencies: 1, - }, - ); + parents, + has_ace_unlock_parent: true, + }); + return Ok(()); + } } + + // No unlock yet - add to pending + self.pending_dependencies + .entry(dep_key) + .or_default() + .push(order.id()); + self.pending_orders.insert( + order.id(), + PendingOrder { + order, + unsatisfied_dependencies: 1, + }, + ); Ok(()) } @@ -356,13 +386,24 @@ impl SimTree { &mut self, result: SimulatedResult, ) -> Result<(), ProviderError> { - self.sims.insert(result.id, result.clone()); + let SimulatedResult::Success { + id, + ref simulated_order, + ref previous_orders, + ref dependencies_satisfied, + .. + } = result + else { + // Only Success variants should be processed here + return Ok(()); + }; + + self.sims.insert(id, result.clone()); let mut orders_ready = Vec::new(); // Process each dependency this simulation satisfies - if result.dependencies_satisfied.len() == 1 { - let dep_key = result - .dependencies_satisfied + if dependencies_satisfied.len() == 1 { + let dep_key = dependencies_satisfied .first() .expect("checked len == 1") .clone(); @@ -372,27 +413,31 @@ impl SimTree { // Already have a provider - check if this one is more profitable let current_sim_profit = { let sim_id = entry.get_mut(); - self.sims - .get(sim_id) - .expect("we never delete sims") - .simulated_order - .sim_value - .full_profit_info() - .coinbase_profit() + if let Some(SimulatedResult::Success { + simulated_order: existing_order, + .. + }) = self.sims.get(sim_id) + { + existing_order + .sim_value + .full_profit_info() + .coinbase_profit() + } else { + return Ok(()); + } }; - if result - .simulated_order + if simulated_order .sim_value .full_profit_info() .coinbase_profit() > current_sim_profit { - entry.insert(result.id); + entry.insert(id); } } Entry::Vacant(entry) => { // First provider for this dependency - entry.insert(result.id); + entry.insert(id); // Unblock orders waiting on this dependency if let Some(pending_order_ids) = self.pending_dependencies.remove(&dep_key) { @@ -416,14 +461,26 @@ impl SimTree { } } + // Determine if the satisfied dependency was an ACE unlock + let is_ace_dependency = matches!( + dependencies_satisfied.first(), + Some(DependencyKey::AceUnlock(_)) + ); + for ready_order in orders_ready { let pending_state = self.get_order_dependency_state(&ready_order)?; match pending_state { - OrderDependencyState::Ready(parents) => { + OrderDependencyState::Ready(mut parents) => { + // If this order became ready due to ACE unlock, add the unlock tx as parent + if is_ace_dependency { + parents.extend(previous_orders.iter().cloned()); + parents.push(simulated_order.order.clone()); + } self.ready_orders.push(SimulationRequest { id: rand::random(), order: ready_order, parents, + has_ace_unlock_parent: is_ace_dependency, }); } OrderDependencyState::Invalid => { @@ -439,88 +496,70 @@ impl SimTree { Ok(()) } - /// Handle ACE interaction after simulation. - /// Returns (skip_forwarding, optional_cancellation_order_id) - /// - For Unlocking interactions: registers as force or optional unlock provider, skip_forwarding=false - /// - For NonUnlocking interactions: adds order as pending on ACE unlock dependency, skip_forwarding=true - /// - Returns cancellation OrderId if a mempool unlock cancels an optional ACE tx - pub fn handle_ace_interaction( + /// Handle ACE unlocking interaction after successful simulation. + /// Returns optional cancellation OrderId if a mempool unlock cancels an optional ACE tx. + /// Note: NonUnlocking ACE interactions are handled at the OrderSimResult level. + pub fn handle_ace_unlock( &mut self, result: &mut SimulatedResult, - ) -> Result<(bool, Option), ProviderError> { - let Some(interaction) = result.simulated_order.ace_interaction else { - return Ok((false, None)); + ) -> Result, ProviderError> { + let SimulatedResult::Success { + simulated_order, + previous_orders, + dependencies_satisfied, + .. + } = result + else { + return Ok(None); }; - // If this order already has parents, it was re-simulated with unlock - just pass through - if !result.previous_orders.is_empty() { - return Ok((false, None)); - } - - let mut cancellation = None; - - match interaction { - AceInteraction::Unlocking { - contract_address, - source, - } => { - // Register the unlock in ACE state - let state = self.ace_state.entry(contract_address).or_default(); - - if source == AceUnlockSource::ProtocolForce { - state.force_unlock_order = Some(result.simulated_order.clone()); - trace!( - "Added forced ACE protocol unlock order for {:?}", - contract_address - ); - } else { - state.optional_unlock_order = Some(result.simulated_order.clone()); - trace!( - "Added optional ACE protocol unlock order for {:?}", - contract_address - ); - } + let Some(AceInteraction::Unlocking { + contract_address, + source, + }) = simulated_order.ace_interaction + else { + return Ok(None); + }; - // Check if we should cancel the optional ACE order (mempool unlock arrived first) - if state.has_mempool_unlock { - if let Some(optional) = state.optional_unlock_order.take() { - cancellation = Some(optional.order.id()); - } - } + // If this order already has parents, it was re-simulated - just pass through + if !previous_orders.is_empty() { + return Ok(None); + } - // Make sure the ACE unlock dependency is in dependencies_satisfied - let dep_key = DependencyKey::AceUnlock(contract_address); - if !result.dependencies_satisfied.contains(&dep_key) { - result.dependencies_satisfied.push(dep_key); - } + // Register the unlock in ACE state + let state = self.ace_state.entry(contract_address).or_default(); - // Process this result to unblock pending orders - self.process_simulation_task_result(result.clone())?; + let cancellation = if source == AceUnlockSource::ProtocolForce { + state.force_unlock_order = Some(simulated_order.clone()); + trace!( + "Added forced ACE protocol unlock order for {:?}", + contract_address + ); + None + } else { + state.optional_unlock_order = Some(simulated_order.clone()); + trace!( + "Added optional ACE protocol unlock order for {:?}", + contract_address + ); + // Check if we should cancel the optional ACE order (mempool unlock arrived first) + if state.has_mempool_unlock { + state.optional_unlock_order.take().map(|o| o.order.id()) + } else { + None } - AceInteraction::NonUnlocking { contract_address } => { - // This is a mempool order that needs ACE unlock - let state = self.ace_state.entry(contract_address).or_default(); + }; - // Check if we have an unlock order to use as parent - if let Some(unlock_order) = state.get_unlock_order().cloned() { - // Re-queue with the unlock as parent - self.ready_orders.push(SimulationRequest { - id: rand::random(), - order: result.simulated_order.order.clone(), - parents: vec![unlock_order.order.clone()], - }); - } else { - // No unlock yet - add as pending on ACE dependency - self.add_ace_dependency_for_order( - result.simulated_order.order.clone(), - contract_address, - )?; - } - return Ok((true, None)); - } + // Make sure the ACE unlock dependency is in dependencies_satisfied + let dep_key = DependencyKey::AceUnlock(contract_address); + if !dependencies_satisfied.contains(&dep_key) { + dependencies_satisfied.push(dep_key); } - Ok((false, cancellation)) + // Process this result to unblock pending orders + self.process_simulation_task_result(result.clone())?; + + Ok(cancellation) } /// Mark that a mempool unlocking order has been seen for a contract address. @@ -541,34 +580,40 @@ impl SimTree { .map(|order| order.order.id()) } - /// Process simulation results, handling ACE interactions and updating dependencies. - /// Filters out orders that should not be forwarded (e.g., ACE orders queued for re-sim). - /// Sends cancellation commands via the provided sender. + /// Process simulation results, handling ACE unlocks and updating dependencies. + /// Returns: + /// - `Vec`: Successful results (to be forwarded to builder) + /// - `Vec`: Order IDs that should be cancelled (e.g., optional ACE unlocks superseded by mempool) pub fn submit_simulation_tasks_results( &mut self, - results: &mut Vec, - cancellation_sender: &mpsc::Sender, - ) -> Result<(), ProviderError> { - 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"); + results: Vec, + ) -> Result<(Vec, Vec), ProviderError> { + let mut cancellations = Vec::new(); + let mut successful_results = Vec::with_capacity(results.len()); + + for result in results { + match result { + SimulatedResult::Success { .. } => { + let mut result = result; + if let Some(id) = self.handle_ace_unlock(&mut result)? { + cancellations.push(id); + } else { + // Non-ACE-unlock results still need to be processed for nonce dependencies + self.process_simulation_task_result(result.clone())?; + } + successful_results.push(result); + } + SimulatedResult::NonUnlockingAce { + order, + contract_address, + } => { + // Queue this order for re-simulation with ACE unlock parent + self.add_ace_dependency_for_order(order, contract_address)?; } } + } - !skip - }); - - Ok(()) + Ok((successful_results, cancellations)) } } @@ -581,7 +626,6 @@ pub fn simulate_all_orders_with_sim_tree

( orders: &[Order], randomize_insertion: bool, ace_config: Vec, - cancellation_sender: &mpsc::Sender, ) -> Result<(Vec>, Vec), CriticalCommitOrderError> where P: StateProviderFactory + Clone, @@ -634,6 +678,7 @@ where &mut local_ctx, &mut block_state, sim_tree.ace_configs(), + sim_task.has_ace_unlock_parent, )?; let (_, provider) = block_state.into_parts(); state_for_sim = provider; @@ -645,7 +690,13 @@ where "Order simulation failed" ); sim_errors.push(err); - continue; + } + OrderSimResult::NonUnlockingAce { + order, + contract_address, + } => { + // Queue this order to be re-simulated once ACE unlock is available + sim_tree.add_ace_dependency_for_order(order, contract_address)?; } OrderSimResult::Success(sim_order, nonces) => { let mut dependencies_satisfied: Vec = nonces @@ -661,7 +712,7 @@ where dependencies_satisfied.push(DependencyKey::AceUnlock(contract_address)); } - let result = SimulatedResult { + let result = SimulatedResult::Success { id: sim_task.id, simulated_order: sim_order, previous_orders: sim_task.parents, @@ -672,14 +723,24 @@ where } } } - sim_tree.submit_simulation_tasks_results(&mut sim_results, cancellation_sender)?; + // For batch simulation, we ignore cancellations since there's no live processing + let (_, _cancellations) = sim_tree.submit_simulation_tasks_results(sim_results)?; } Ok(( sim_tree .sims .into_values() - .map(|sim| sim.simulated_order) + .filter_map(|sim| { + if let SimulatedResult::Success { + simulated_order, .. + } = sim + { + Some(simulated_order) + } else { + None + } + }) .collect(), sim_errors, )) @@ -693,6 +754,7 @@ pub fn simulate_order( local_ctx: &mut ThreadBlockBuildingContext, state: &mut BlockState, ace_configs: &HashMap, + has_ace_unlock_parent: bool, ) -> Result { let mut tracer = AccumulatorSimulationTracer::new(); let mut fork = PartialBlockFork::new(state, ctx, local_ctx).with_tracer(&mut tracer); @@ -703,6 +765,7 @@ pub fn simulate_order( &mut fork, &ctx.mempool_tx_detector, ace_configs, + has_ace_unlock_parent, ); fork.rollback(rollback_point); let sim_res = sim_res?; @@ -720,9 +783,9 @@ pub fn simulate_order_using_fork( fork: &mut PartialBlockFork<'_, '_, '_, '_, Tracer, NullPartialBlockForkExecutionTracer>, mempool_tx_detector: &MempoolTxsDetector, ace_configs: &HashMap, + has_ace_unlock_parent: bool, ) -> Result { let start = Instant::now(); - let has_parents = !parent_orders.is_empty(); // simulate parents let mut space_state = BlockBuildingSpaceState::ZERO; @@ -795,35 +858,28 @@ pub fn simulate_order_using_fork( )) } 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 { + // Check if failed order accessed ACE - queue for re-simulation with unlock parent + if let Some(AceInteraction::NonUnlocking { contract_address }) = ace_interaction { + if has_ace_unlock_parent { + // Order had ACE unlock parent but still failed - genuine failure + tracing::debug!( + order = ?order.id(), + ?err, + ?contract_address, + "Order failed despite having ACE unlock parent - treating as genuine failure" + ); + } else { + // No ACE unlock parent - queue for re-simulation with unlock tracing::debug!( order = ?order.id(), ?err, ?contract_address, - "Failed order accessed ACE - treating as successful non-unlocking ACE order" + "Order failed due to locked ACE - queueing for re-simulation with unlock" ); - // 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(), - )); + return Ok(OrderSimResult::NonUnlockingAce { + order, + contract_address, + }); } } 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 index de4d45018..136bc9885 100644 --- a/crates/rbuilder/src/building/testing/ace_tests/mod.rs +++ b/crates/rbuilder/src/building/testing/ace_tests/mod.rs @@ -253,7 +253,7 @@ fn test_mark_mempool_unlock_basic() -> eyre::Result<()> { // or integration with handle_ace_interaction which we'll test separately #[test] -fn test_handle_ace_interaction_with_mempool_unlock() -> eyre::Result<()> { +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()); @@ -268,7 +268,7 @@ fn test_handle_ace_interaction_with_mempool_unlock() -> eyre::Result<()> { let optional_order = create_optional_unlock_order(contract_addr, 50_000); - let mut result = SimulatedResult { + let mut result = SimulatedResult::Success { id: rand::random(), simulated_order: optional_order.clone(), previous_orders: Vec::new(), @@ -276,11 +276,10 @@ fn test_handle_ace_interaction_with_mempool_unlock() -> eyre::Result<()> { simulation_time: std::time::Duration::from_millis(10), }; - // Handle the ACE interaction - let (skip_forwarding, cancellation) = sim_tree.handle_ace_interaction(&mut result)?; + // Handle the ACE unlock + let cancellation = sim_tree.handle_ace_unlock(&mut result)?; - // Unlocking orders are not skipped (they go downstream), but should be cancelled - assert!(!skip_forwarding); + // Unlocking orders should be cancelled since mempool unlock was already marked assert_eq!(cancellation, Some(optional_order.order.id())); // Optional order should NOT be stored diff --git a/crates/rbuilder/src/live_builder/base_config.rs b/crates/rbuilder/src/live_builder/base_config.rs index 892e0ff24..053d5baf1 100644 --- a/crates/rbuilder/src/live_builder/base_config.rs +++ b/crates/rbuilder/src/live_builder/base_config.rs @@ -172,10 +172,18 @@ pub struct BaseConfig { /// 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 { Ipv4Addr::new(0, 0, 0, 0) } @@ -274,6 +282,7 @@ 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(), }) } @@ -492,6 +501,7 @@ 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(), diff --git a/crates/rbuilder/src/live_builder/building/mod.rs b/crates/rbuilder/src/live_builder/building/mod.rs index a46f7b4cf..a93221acb 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_enabled: bool, ace_config: Vec, } @@ -63,6 +64,7 @@ where run_sparse_trie_prefetcher: bool, sbundle_merger_selected_signers: Arc>, order_flow_tracer_manager: Box, + ace_enabled: bool, ace_config: Vec, ) -> Self { BlockBuildingPool { @@ -75,6 +77,7 @@ where sbundle_merger_selected_signers, order_flow_tracer_manager, built_block_id_source: Arc::new(BuiltBlockIdSource::new()), + ace_enabled, ace_config, } } @@ -152,6 +155,7 @@ where orders_for_block, block_cancellation.clone(), sim_tracer, + self.ace_enabled, self.ace_config.clone(), ); self.start_building_job( diff --git a/crates/rbuilder/src/live_builder/mod.rs b/crates/rbuilder/src/live_builder/mod.rs index 85c0e02d6..fe9f71bf3 100644 --- a/crates/rbuilder/src/live_builder/mod.rs +++ b/crates/rbuilder/src/live_builder/mod.rs @@ -136,6 +136,7 @@ where pub order_flow_tracer_manager: Box, + pub ace_enabled: bool, pub ace_config: Vec, } @@ -235,6 +236,7 @@ where self.run_sparse_trie_prefetcher, self.sbundle_merger_selected_signers.clone(), self.order_flow_tracer_manager, + self.ace_enabled, self.ace_config.clone(), ); 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 d06991408..3938e2b2c 100644 --- a/crates/rbuilder/src/live_builder/simulation/mod.rs +++ b/crates/rbuilder/src/live_builder/simulation/mod.rs @@ -39,8 +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. + /// 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 @@ -119,6 +121,7 @@ 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); @@ -157,13 +160,20 @@ where }; // 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(); + // 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() + .filter(|c| c.enabled) + .map(|c| (c.contract_address, c.clone())) + .collect() + } else { + ahash::HashMap::default() + }; - let sim_tree = SimTree::new(nonces, ace_config); + // 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); @@ -174,6 +184,7 @@ where requests: sim_req_receiver, results: sim_results_sender, ace_configs: ace_configs_map, + ace_enabled, }; contexts.contexts.insert(block_context, sim_context); } @@ -247,6 +258,7 @@ mod tests { orders_for_block, cancel.clone(), Arc::new(NullSimulationJobTracer {}), + false, // ace_enabled vec![], ); // Create a simple tx that sends to coinbase 5 wei. diff --git a/crates/rbuilder/src/live_builder/simulation/sim_worker.rs b/crates/rbuilder/src/live_builder/simulation/sim_worker.rs index c96931c8a..f8dd8889a 100644 --- a/crates/rbuilder/src/live_builder/simulation/sim_worker.rs +++ b/crates/rbuilder/src/live_builder/simulation/sim_worker.rs @@ -67,11 +67,12 @@ 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.has_ace_unlock_parent, ); let sim_ok = match sim_result { Ok(sim_result) => { @@ -93,19 +94,34 @@ pub fn run_sim_worker

( .push(DependencyKey::AceUnlock(contract_address)); } - let result = SimulatedResult { + let result = SimulatedResult::Success { id: task.id, simulated_order, previous_orders: task.parents, 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::NonUnlockingAce { + order, + contract_address, + } => { + let result = SimulatedResult::NonUnlockingAce { + order, + contract_address, + }; + if current_sim_context.results.try_send(result).is_err() { + error!(?order_id, "Failed to send NonUnlockingAce result"); + } + false + } OrderSimResult::Failed(_) => false, }; telemetry::inc_simulated_orders(sim_ok); @@ -114,7 +130,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 7ce0541e2..248fc998d 100644 --- a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs +++ b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs @@ -192,51 +192,68 @@ impl SimulationJob { ) -> 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); + 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); + } + } + valid_simulated_orders.push(sim_result); } } + SimulatedResult::NonUnlockingAce { .. } => { + // Pass through to sim_tree for re-queuing with ACE unlock parent + valid_simulated_orders.push(sim_result); + } } } // update simtree - 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; + let (_, cancellations) = match self + .sim_tree + .submit_simulation_tasks_results(valid_simulated_orders) + { + 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 } From feda589e6750433384e6936cbb0dc6c3985f22b5 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 6 Jan 2026 11:03:27 -0400 Subject: [PATCH 29/51] make sure that clippy happy --- .../rbuilder-operator/src/flashbots_config.rs | 2 +- .../blocks_processor_backend.rs | 1 - .../true_block_value_push/redis_backend.rs | 1 - .../backtest/build_block/synthetic_orders.rs | 2 +- .../block_orders/multi_share_bundle_merger.rs | 10 +++---- .../block_orders/share_bundle_merger.rs | 6 ++-- .../builders/block_building_helper.rs | 2 +- .../src/building/testing/ace_tests/mod.rs | 29 ++++++++++++------- .../live_builder/wallet_balance_watcher.rs | 2 +- 9 files changed, 31 insertions(+), 24 deletions(-) diff --git a/crates/rbuilder-operator/src/flashbots_config.rs b/crates/rbuilder-operator/src/flashbots_config.rs index 4dbb75edd..f48556258 100644 --- a/crates/rbuilder-operator/src/flashbots_config.rs +++ b/crates/rbuilder-operator/src/flashbots_config.rs @@ -36,7 +36,7 @@ use serde::Deserialize; use serde_with::serde_as; use time::OffsetDateTime; use tokio_util::sync::CancellationToken; -use tracing::{error, warn}; +use tracing::warn; use url::Url; use crate::{ diff --git a/crates/rbuilder-operator/src/true_block_value_push/blocks_processor_backend.rs b/crates/rbuilder-operator/src/true_block_value_push/blocks_processor_backend.rs index 6d10761da..76e7132fc 100644 --- a/crates/rbuilder-operator/src/true_block_value_push/blocks_processor_backend.rs +++ b/crates/rbuilder-operator/src/true_block_value_push/blocks_processor_backend.rs @@ -5,7 +5,6 @@ use crate::{ use alloy_signer_local::PrivateKeySigner; use jsonrpsee::core::client::ClientT; use tokio::runtime::Runtime; -use tracing::error; use super::best_true_value_pusher::{Backend, BuiltBlockInfo}; diff --git a/crates/rbuilder-operator/src/true_block_value_push/redis_backend.rs b/crates/rbuilder-operator/src/true_block_value_push/redis_backend.rs index be3e4bf85..6977ff303 100644 --- a/crates/rbuilder-operator/src/true_block_value_push/redis_backend.rs +++ b/crates/rbuilder-operator/src/true_block_value_push/redis_backend.rs @@ -1,5 +1,4 @@ use redis::Commands; -use tracing::error; use super::best_true_value_pusher::{Backend, BuiltBlockInfo}; diff --git a/crates/rbuilder/src/backtest/build_block/synthetic_orders.rs b/crates/rbuilder/src/backtest/build_block/synthetic_orders.rs index 6d3ecdfb4..22cbe4578 100644 --- a/crates/rbuilder/src/backtest/build_block/synthetic_orders.rs +++ b/crates/rbuilder/src/backtest/build_block/synthetic_orders.rs @@ -1,5 +1,5 @@ use alloy_primitives::B256; -use clap::{command, Parser}; +use clap::Parser; use rbuilder_config::load_toml_config; use rbuilder_primitives::{ Bundle, MempoolTx, Metadata, Order, TransactionSignedEcRecoveredWithBlobs, LAST_BUNDLE_VERSION, diff --git a/crates/rbuilder/src/building/block_orders/multi_share_bundle_merger.rs b/crates/rbuilder/src/building/block_orders/multi_share_bundle_merger.rs index 41ccfa8ed..bf89030f7 100644 --- a/crates/rbuilder/src/building/block_orders/multi_share_bundle_merger.rs +++ b/crates/rbuilder/src/building/block_orders/multi_share_bundle_merger.rs @@ -121,7 +121,7 @@ mod test { // first order generates a megabundle with it context.insert_order(br_hi.clone()); let generated_order = context.pop_insert(); - context.assert_concatenated_sbundles_ok(&generated_order, &[br_hi.clone()]); + context.assert_concatenated_sbundles_ok(&generated_order, std::slice::from_ref(&br_hi)); // for second expect a cancellation and a new megabundle with both context.insert_order(br_low.clone()); @@ -141,17 +141,17 @@ mod test { // first order generates a megabundle with it context.insert_order(br_1.clone()); let generated_order = context.pop_insert(); - context.assert_concatenated_sbundles_ok(&generated_order, &[br_1.clone()]); + context.assert_concatenated_sbundles_ok(&generated_order, std::slice::from_ref(&br_1)); // for second expect a new megabundle with it context.insert_order(br_2.clone()); let generated_order = context.pop_insert(); - context.assert_concatenated_sbundles_ok(&generated_order, &[br_2.clone()]); + context.assert_concatenated_sbundles_ok(&generated_order, std::slice::from_ref(&br_2)); // for an unknown signer expect a new megabundle with it context.insert_order(br_3.clone()); let generated_order = context.pop_insert(); - context.assert_concatenated_sbundles_ok(&generated_order, &[br_3.clone()]); + context.assert_concatenated_sbundles_ok(&generated_order, std::slice::from_ref(&br_3)); } #[test] @@ -163,7 +163,7 @@ mod test { // first order generates a megabundle with it context.insert_order(br_hi.clone()); let generated_order = context.pop_insert(); - context.assert_concatenated_sbundles_ok(&generated_order, &[br_hi.clone()]); + context.assert_concatenated_sbundles_ok(&generated_order, std::slice::from_ref(&br_hi)); // for second expect a cancellation and a new megabundle with both context.insert_order(br_low.clone()); diff --git a/crates/rbuilder/src/building/block_orders/share_bundle_merger.rs b/crates/rbuilder/src/building/block_orders/share_bundle_merger.rs index ef37ec1c5..7d19cfc62 100644 --- a/crates/rbuilder/src/building/block_orders/share_bundle_merger.rs +++ b/crates/rbuilder/src/building/block_orders/share_bundle_merger.rs @@ -472,7 +472,7 @@ mod test { // Insert hi expect an order with only br_hi context.insert_order(br_hi.clone()); let generated_order = context.pop_insert(); - context.assert_concatenated_sbundles_ok(&generated_order, &[br_hi.clone()]); + context.assert_concatenated_sbundles_ok(&generated_order, std::slice::from_ref(&br_hi)); // Insert low expect a cancellation for prev order and hi+low context.insert_order(br_low.clone()); @@ -484,7 +484,7 @@ mod test { context.remove_order(br_hi.id()); assert_eq!(context.pop_remove(), generated_order.id()); let generated_order = context.pop_insert(); - context.assert_concatenated_sbundles_ok(&generated_order, &[br_low.clone()]); + context.assert_concatenated_sbundles_ok(&generated_order, std::slice::from_ref(&br_low)); // Remove low order expect a cancellation for prev order and nothing more (shoudn't insert an empty sbundle!) context.remove_order(br_low.id()); @@ -493,7 +493,7 @@ mod test { // We expect an order with only br_low context.insert_order(br_low.clone()); let generated_order = context.pop_insert(); - context.assert_concatenated_sbundles_ok(&generated_order, &[br_low.clone()]); + context.assert_concatenated_sbundles_ok(&generated_order, std::slice::from_ref(&br_low)); // Insert hi expect a cancellation for prev order and hi+low context.insert_order(br_hi.clone()); diff --git a/crates/rbuilder/src/building/builders/block_building_helper.rs b/crates/rbuilder/src/building/builders/block_building_helper.rs index c9dfbe7a8..5bbd70d34 100644 --- a/crates/rbuilder/src/building/builders/block_building_helper.rs +++ b/crates/rbuilder/src/building/builders/block_building_helper.rs @@ -7,7 +7,7 @@ use std::{ }; use time::OffsetDateTime; use tokio_util::sync::CancellationToken; -use tracing::{debug, error, trace, warn}; +use tracing::{debug, trace, warn}; use crate::{ building::{ diff --git a/crates/rbuilder/src/building/testing/ace_tests/mod.rs b/crates/rbuilder/src/building/testing/ace_tests/mod.rs index 136bc9885..34a39e976 100644 --- a/crates/rbuilder/src/building/testing/ace_tests/mod.rs +++ b/crates/rbuilder/src/building/testing/ace_tests/mod.rs @@ -97,6 +97,7 @@ fn create_optional_unlock_order(contract: Address, gas_used: u64) -> Arc Arc { let slot = b256!("0000000000000000000000000000000000000000000000000000000000000003"); Arc::new(SimulatedOrder { @@ -120,8 +121,10 @@ fn test_ace_exchange_state_get_unlock_order_force_only() { address!("0000000aa232009084Bd71A5797d089AA4Edfad4"), 100_000, ); - let mut state = AceExchangeState::default(); - state.force_unlock_order = Some(order.clone()); + let state = AceExchangeState { + force_unlock_order: Some(order.clone()), + ..Default::default() + }; let result = state.get_unlock_order(); assert_eq!(result, Some(&order)); @@ -131,8 +134,10 @@ fn test_ace_exchange_state_get_unlock_order_force_only() { 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 state = AceExchangeState { + optional_unlock_order: Some(order.clone()), + ..Default::default() + }; let result = state.get_unlock_order(); assert_eq!(result, Some(&order)); @@ -144,9 +149,11 @@ fn test_cheapest_unlock_selected() { 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()); + 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(); @@ -159,9 +166,11 @@ fn test_equal_gas_prefers_force() { 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()); + 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(); 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, From e819c74219a50f8576b66721c5c2b8517b91667b Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 6 Jan 2026 12:11:04 -0400 Subject: [PATCH 30/51] bug fixes --- crates/rbuilder-primitives/src/ace.rs | 9 --- crates/rbuilder/src/building/sim.rs | 96 ++++++++++++++++----------- 2 files changed, 56 insertions(+), 49 deletions(-) diff --git a/crates/rbuilder-primitives/src/ace.rs b/crates/rbuilder-primitives/src/ace.rs index 7308b21ed..9eb49b2e7 100644 --- a/crates/rbuilder-primitives/src/ace.rs +++ b/crates/rbuilder-primitives/src/ace.rs @@ -144,15 +144,6 @@ impl AceInteraction { } } -/// Type of unlock for ACE protocol transactions (Order::Ace) -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] -pub enum AceUnlockType { - /// Must unlock, transaction will fail if unlock conditions aren't met - Force, - /// Optional unlock, transaction can proceed with or without unlock - Optional, -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index 25d892f1c..7df432978 100644 --- a/crates/rbuilder/src/building/sim.rs +++ b/crates/rbuilder/src/building/sim.rs @@ -297,7 +297,11 @@ impl SimTree { if let Some(sim_id) = self.dependency_providers.get(&dep_key) { // we have something that fills this nonce - let sim = self.sims.get(sim_id).expect("we never delete sims"); + let Some(sim) = self.sims.get(sim_id) else { + error!("SimTree bug: dependency provider sim not found"); + pending_deps.push(dep_key); + continue; + }; if let SimulatedResult::Success { previous_orders, simulated_order, @@ -333,7 +337,11 @@ impl SimTree { // Check if we already have an unlock provider if let Some(sim_id) = self.dependency_providers.get(&dep_key) { - let sim = self.sims.get(sim_id).expect("we never delete sims"); + let Some(sim) = self.sims.get(sim_id) else { + error!("SimTree bug: ACE unlock provider sim not found"); + // Fall through to pending logic below + return self.add_order_to_pending(order, dep_key); + }; if let SimulatedResult::Success { previous_orders, simulated_order, @@ -355,6 +363,15 @@ impl SimTree { } // No unlock yet - add to pending + self.add_order_to_pending(order, dep_key) + } + + /// Helper to add an order to pending state with a single dependency + fn add_order_to_pending( + &mut self, + order: Order, + dep_key: DependencyKey, + ) -> Result<(), ProviderError> { self.pending_dependencies .entry(dep_key) .or_default() @@ -400,13 +417,14 @@ impl SimTree { self.sims.insert(id, result.clone()); let mut orders_ready = Vec::new(); + let mut is_ace_dependency = false; // Process each dependency this simulation satisfies - if dependencies_satisfied.len() == 1 { - let dep_key = dependencies_satisfied - .first() - .expect("checked len == 1") - .clone(); + for dep_key in dependencies_satisfied.iter().cloned() { + // Track if any dependency is an ACE unlock + if matches!(dep_key, DependencyKey::AceUnlock(_)) { + is_ace_dependency = true; + } match self.dependency_providers.entry(dep_key.clone()) { Entry::Occupied(mut entry) => { @@ -423,7 +441,7 @@ impl SimTree { .full_profit_info() .coinbase_profit() } else { - return Ok(()); + continue; } }; if simulated_order @@ -461,12 +479,6 @@ impl SimTree { } } - // Determine if the satisfied dependency was an ACE unlock - let is_ace_dependency = matches!( - dependencies_satisfied.first(), - Some(DependencyKey::AceUnlock(_)) - ); - for ready_order in orders_ready { let pending_state = self.get_order_dependency_state(&ready_order)?; match pending_state { @@ -526,28 +538,36 @@ impl SimTree { return Ok(None); } - // Register the unlock in ACE state - let state = self.ace_state.entry(contract_address).or_default(); - - let cancellation = if source == AceUnlockSource::ProtocolForce { - state.force_unlock_order = Some(simulated_order.clone()); - trace!( - "Added forced ACE protocol unlock order for {:?}", - contract_address - ); - None - } else { - state.optional_unlock_order = Some(simulated_order.clone()); - trace!( - "Added optional ACE protocol unlock order for {:?}", - contract_address - ); - // Check if we should cancel the optional ACE order (mempool unlock arrived first) - if state.has_mempool_unlock { - state.optional_unlock_order.take().map(|o| o.order.id()) - } else { + // Register the unlock in ACE state based on source type + let cancellation = match source { + AceUnlockSource::ProtocolForce => { + let state = self.ace_state.entry(contract_address).or_default(); + state.force_unlock_order = Some(simulated_order.clone()); + trace!( + "Added forced ACE protocol unlock order for {:?}", + contract_address + ); None } + AceUnlockSource::ProtocolOptional => { + let state = self.ace_state.entry(contract_address).or_default(); + state.optional_unlock_order = Some(simulated_order.clone()); + trace!( + "Added optional ACE protocol unlock order for {:?}", + contract_address + ); + // Check if we should cancel the optional ACE order (mempool unlock arrived first) + if state.has_mempool_unlock { + state.optional_unlock_order.take().map(|o| o.order.id()) + } else { + None + } + } + AceUnlockSource::User => { + // A user unlocked ACE via mempool - mark it and cancel any optional protocol order + trace!("User mempool unlock detected for {:?}", contract_address); + self.mark_mempool_unlock(contract_address) + } }; // Make sure the ACE unlock dependency is in dependencies_satisfied @@ -556,9 +576,6 @@ impl SimTree { dependencies_satisfied.push(dep_key); } - // Process this result to unblock pending orders - self.process_simulation_task_result(result.clone())?; - Ok(cancellation) } @@ -597,10 +614,9 @@ impl SimTree { let mut result = result; if let Some(id) = self.handle_ace_unlock(&mut result)? { cancellations.push(id); - } else { - // Non-ACE-unlock results still need to be processed for nonce dependencies - self.process_simulation_task_result(result.clone())?; } + // All successful results need to be processed for dependency tracking + self.process_simulation_task_result(result.clone())?; successful_results.push(result); } SimulatedResult::NonUnlockingAce { From a61b91f7178838c36d5588ad22cfaafd8bcb61fe Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 6 Jan 2026 12:16:11 -0400 Subject: [PATCH 31/51] cleanup ace enabled flag --- crates/rbuilder-primitives/src/ace.rs | 32 ------------------- crates/rbuilder/src/building/sim.rs | 3 -- .../src/building/testing/ace_tests/mod.rs | 1 - .../src/live_builder/simulation/mod.rs | 1 - 4 files changed, 37 deletions(-) diff --git a/crates/rbuilder-primitives/src/ace.rs b/crates/rbuilder-primitives/src/ace.rs index 9eb49b2e7..012ddf288 100644 --- a/crates/rbuilder-primitives/src/ace.rs +++ b/crates/rbuilder-primitives/src/ace.rs @@ -9,9 +9,6 @@ pub type Selector = FixedBytes<4>; /// Configuration for an ACE (Application Controlled Execution) protocol #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub struct AceConfig { - /// Whether this ACE config is enabled - #[serde(default = "default_enabled")] - pub enabled: bool, /// The primary contract address for this ACE protocol (used as unique identifier) pub contract_address: Address, /// Addresses that send ACE orders (used to identify force unlocks) @@ -26,10 +23,6 @@ pub struct AceConfig { pub force_signatures: HashSet, } -fn default_enabled() -> bool { - true -} - /// Classify an ACE order interaction type based on state trace, simulation success, and config. /// Uses both state trace (address access) AND function signatures to determine interaction type. pub fn classify_ace_interaction( @@ -154,7 +147,6 @@ mod tests { /// Create the real ACE config from the provided TOML configuration fn real_ace_config() -> AceConfig { AceConfig { - enabled: true, contract_address: address!("0000000aa232009084Bd71A5797d089AA4Edfad4"), from_addresses: HashSet::from([ address!("c41ae140ca9b281d8a1dc254c50e446019517d04"), @@ -367,30 +359,6 @@ mod tests { ); } - #[test] - fn test_ace_disabled_config() { - let mut config = real_ace_config(); - config.enabled = false; - - let contract = config.contract_address; - let detection_slot = *config.detection_slots.iter().next().unwrap(); - let force_selector = *config.force_signatures.iter().next().unwrap(); - - let trace = mock_state_trace_with_slot(contract, detection_slot); - - // Classification still works even if disabled - filtering happens at higher level - let result = - classify_ace_interaction(&trace, true, &config, Some(force_selector), Some(contract)); - - assert_eq!( - result, - Some(AceInteraction::Unlocking { - contract_address: contract, - source: AceUnlockSource::ProtocolForce - }) - ); - } - #[test] fn test_ace_interaction_is_unlocking() { let contract = address!("0000000aa232009084Bd71A5797d089AA4Edfad4"); diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index 7df432978..2dc64ea9e 100644 --- a/crates/rbuilder/src/building/sim.rs +++ b/crates/rbuilder/src/building/sim.rs @@ -849,9 +849,6 @@ pub fn simulate_order_using_fork( 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) }) }); diff --git a/crates/rbuilder/src/building/testing/ace_tests/mod.rs b/crates/rbuilder/src/building/testing/ace_tests/mod.rs index 34a39e976..8c204613e 100644 --- a/crates/rbuilder/src/building/testing/ace_tests/mod.rs +++ b/crates/rbuilder/src/building/testing/ace_tests/mod.rs @@ -33,7 +33,6 @@ fn create_test_order() -> Order { /// 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")]), diff --git a/crates/rbuilder/src/live_builder/simulation/mod.rs b/crates/rbuilder/src/live_builder/simulation/mod.rs index 3938e2b2c..df2e86bf7 100644 --- a/crates/rbuilder/src/live_builder/simulation/mod.rs +++ b/crates/rbuilder/src/live_builder/simulation/mod.rs @@ -164,7 +164,6 @@ where let ace_configs_map: ahash::HashMap<_, _> = if ace_enabled { ace_config .iter() - .filter(|c| c.enabled) .map(|c| (c.contract_address, c.clone())) .collect() } else { From 1be6dd57e637b292b9b043375549b8638ef9738b Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 6 Jan 2026 12:21:24 -0400 Subject: [PATCH 32/51] fix: remove unused code --- .../src/building/testing/ace_tests/mod.rs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/crates/rbuilder/src/building/testing/ace_tests/mod.rs b/crates/rbuilder/src/building/testing/ace_tests/mod.rs index 8c204613e..7304911cd 100644 --- a/crates/rbuilder/src/building/testing/ace_tests/mod.rs +++ b/crates/rbuilder/src/building/testing/ace_tests/mod.rs @@ -95,25 +95,6 @@ fn create_optional_unlock_order(contract: Address, gas_used: u64) -> Arc 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( From 91d48f103eddf5b6b58a6c10690d9bed0eb31877 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 6 Jan 2026 13:06:45 -0400 Subject: [PATCH 33/51] more cleanup --- crates/rbuilder/src/building/sim.rs | 235 ++++++++++-------- .../src/building/testing/ace_tests/mod.rs | 35 +++ crates/rbuilder/src/building/tracers.rs | 9 + .../src/live_builder/simulation/sim_worker.rs | 26 +- .../live_builder/simulation/simulation_job.rs | 9 +- 5 files changed, 191 insertions(+), 123 deletions(-) diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index 2dc64ea9e..ad2a77d99 100644 --- a/crates/rbuilder/src/building/sim.rs +++ b/crates/rbuilder/src/building/sim.rs @@ -32,19 +32,23 @@ use std::{ }; use tracing::{error, trace}; +/// Information about a simulation failure +#[derive(Debug)] +pub struct SimulationFailure { + /// The error that caused the failure + pub error: OrderErr, + /// If Some, this order needs an ACE unlock from this contract before it can succeed. + /// The order should be queued for re-simulation once the unlock tx is available. + pub ace_dependency: Option

, +} + #[derive(Debug)] #[allow(clippy::large_enum_variant)] pub enum OrderSimResult { /// Order simulated successfully Success(Arc, Vec<(Address, u64)>), /// Order simulation failed - Failed(OrderErr), - /// Order failed but accessed an ACE exchange that wasn't unlocked. - /// This order should be queued for re-simulation once the unlock tx is available. - NonUnlockingAce { - order: Order, - contract_address: Address, - }, + Failed(SimulationFailure), } #[derive(Debug)] @@ -123,7 +127,7 @@ pub struct SimulationRequest { pub has_ace_unlock_parent: bool, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug)] #[allow(clippy::large_enum_variant)] pub enum SimulatedResult { /// Successful simulation @@ -135,20 +139,29 @@ pub enum SimulatedResult { dependencies_satisfied: Vec, simulation_time: Duration, }, - /// Order failed due to locked ACE - needs re-simulation with unlock parent - NonUnlockingAce { + /// Order simulation failed + Failed { + id: SimulationId, order: Order, - contract_address: Address, + failure: SimulationFailure, + simulation_time: Duration, }, } +/// Minimal data stored for completed simulations (to avoid Clone on full SimulatedResult) +#[derive(Debug, Clone)] +struct StoredSimulation { + previous_orders: Vec, + simulated_order: Arc, +} + // @Feat replaceable orders #[derive(Debug)] pub struct SimTree { // fields for nonce management nonces: NonceCache, - sims: HashMap, + sims: HashMap, /// Maps a dependency to the simulation that provides it (for single-dependency sims) dependency_providers: HashMap, @@ -302,15 +315,8 @@ impl SimTree { pending_deps.push(dep_key); continue; }; - if let SimulatedResult::Success { - previous_orders, - simulated_order, - .. - } = sim - { - parent_orders.extend_from_slice(previous_orders); - parent_orders.push(simulated_order.order.clone()); - } + parent_orders.extend_from_slice(&sim.previous_orders); + parent_orders.push(sim.simulated_order.order.clone()); continue; } @@ -342,24 +348,17 @@ impl SimTree { // Fall through to pending logic below return self.add_order_to_pending(order, dep_key); }; - if let SimulatedResult::Success { - previous_orders, - simulated_order, - .. - } = sim - { - let mut parents = previous_orders.clone(); - parents.push(simulated_order.order.clone()); + let mut parents = sim.previous_orders.clone(); + parents.push(sim.simulated_order.order.clone()); - // Order is ready with the unlock tx as parent - self.ready_orders.push(SimulationRequest { - id: rand::random(), - order, - parents, - has_ace_unlock_parent: true, - }); - return Ok(()); - } + // Order is ready with the unlock tx as parent + self.ready_orders.push(SimulationRequest { + id: rand::random(), + order, + parents, + has_ace_unlock_parent: true, + }); + return Ok(()); } // No unlock yet - add to pending @@ -401,13 +400,13 @@ impl SimTree { // we don't really need state here because nonces are cached but its smaller if we reuse pending state fn fn process_simulation_task_result( &mut self, - result: SimulatedResult, + result: &SimulatedResult, ) -> Result<(), ProviderError> { let SimulatedResult::Success { id, - ref simulated_order, - ref previous_orders, - ref dependencies_satisfied, + simulated_order, + previous_orders, + dependencies_satisfied, .. } = result else { @@ -415,7 +414,13 @@ impl SimTree { return Ok(()); }; - self.sims.insert(id, result.clone()); + self.sims.insert( + *id, + StoredSimulation { + previous_orders: previous_orders.clone(), + simulated_order: simulated_order.clone(), + }, + ); let mut orders_ready = Vec::new(); let mut is_ace_dependency = false; @@ -431,12 +436,9 @@ impl SimTree { // Already have a provider - check if this one is more profitable let current_sim_profit = { let sim_id = entry.get_mut(); - if let Some(SimulatedResult::Success { - simulated_order: existing_order, - .. - }) = self.sims.get(sim_id) - { - existing_order + if let Some(existing_sim) = self.sims.get(sim_id) { + existing_sim + .simulated_order .sim_value .full_profit_info() .coinbase_profit() @@ -450,12 +452,12 @@ impl SimTree { .coinbase_profit() > current_sim_profit { - entry.insert(id); + entry.insert(*id); } } Entry::Vacant(entry) => { // First provider for this dependency - entry.insert(id); + entry.insert(*id); // Unblock orders waiting on this dependency if let Some(pending_order_ids) = self.pending_dependencies.remove(&dep_key) { @@ -518,7 +520,6 @@ impl SimTree { let SimulatedResult::Success { simulated_order, previous_orders, - dependencies_satisfied, .. } = result else { @@ -551,17 +552,33 @@ impl SimTree { } AceUnlockSource::ProtocolOptional => { let state = self.ace_state.entry(contract_address).or_default(); + + // Check if user unlock already available - cancel optional + if state.has_mempool_unlock { + trace!( + "Cancelling optional ACE unlock for {:?} - user unlock exists", + contract_address + ); + return Ok(Some(simulated_order.order.id())); + } + + // Only include optional if there are orders waiting on this unlock + let dep_key = DependencyKey::AceUnlock(contract_address); + if !self.pending_dependencies.contains_key(&dep_key) { + trace!( + "Cancelling optional ACE unlock for {:?} - no pending orders need it", + contract_address + ); + return Ok(Some(simulated_order.order.id())); + } + + // Store optional unlock - there are orders waiting for it state.optional_unlock_order = Some(simulated_order.clone()); trace!( "Added optional ACE protocol unlock order for {:?}", contract_address ); - // Check if we should cancel the optional ACE order (mempool unlock arrived first) - if state.has_mempool_unlock { - state.optional_unlock_order.take().map(|o| o.order.id()) - } else { - None - } + None } AceUnlockSource::User => { // A user unlocked ACE via mempool - mark it and cancel any optional protocol order @@ -570,12 +587,6 @@ impl SimTree { } }; - // Make sure the ACE unlock dependency is in dependencies_satisfied - let dep_key = DependencyKey::AceUnlock(contract_address); - if !dependencies_satisfied.contains(&dep_key) { - dependencies_satisfied.push(dep_key); - } - Ok(cancellation) } @@ -616,16 +627,24 @@ impl SimTree { cancellations.push(id); } // All successful results need to be processed for dependency tracking - self.process_simulation_task_result(result.clone())?; + self.process_simulation_task_result(&result)?; successful_results.push(result); } - SimulatedResult::NonUnlockingAce { + SimulatedResult::Failed { order, - contract_address, + failure: + SimulationFailure { + ace_dependency: Some(contract_address), + .. + }, + .. } => { - // Queue this order for re-simulation with ACE unlock parent + // Order failed but needs ACE unlock - queue for re-simulation self.add_ace_dependency_for_order(order, contract_address)?; } + SimulatedResult::Failed { .. } => { + // Permanent failure - nothing to do + } } } @@ -699,20 +718,19 @@ where 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); - } - OrderSimResult::NonUnlockingAce { - order, - contract_address, - } => { - // Queue this order to be re-simulated once ACE unlock is available - sim_tree.add_ace_dependency_for_order(order, contract_address)?; + OrderSimResult::Failed(failure) => { + if let Some(contract_address) = failure.ace_dependency { + // Order failed but needs ACE unlock - queue for re-simulation + sim_tree.add_ace_dependency_for_order(sim_task.order, contract_address)?; + } 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 mut dependencies_satisfied: Vec = nonces @@ -747,16 +765,7 @@ where sim_tree .sims .into_values() - .filter_map(|sim| { - if let SimulatedResult::Success { - simulated_order, .. - } = sim - { - Some(simulated_order) - } else { - None - } - }) + .map(|sim| sim.simulated_order) .collect(), sim_errors, )) @@ -816,7 +825,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, + })); } } } @@ -828,11 +840,7 @@ pub fn simulate_order_using_fork( 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(); + let used_state_trace = fork.tracer.as_mut().and_then(|t| t.take_used_state_trace()); // Detect ACE interaction from the state trace using config // Get function selector and tx.to from order's first transaction @@ -857,7 +865,10 @@ pub fn simulate_order_using_fork( 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( @@ -871,8 +882,10 @@ pub fn simulate_order_using_fork( )) } Err(err) => { - // Check if failed order accessed ACE - queue for re-simulation with unlock parent - if let Some(AceInteraction::NonUnlocking { contract_address }) = ace_interaction { + // Check if failed order accessed ACE - may need re-simulation with unlock parent + let ace_dependency = if let Some(AceInteraction::NonUnlocking { contract_address }) = + ace_interaction + { if has_ace_unlock_parent { // Order had ACE unlock parent but still failed - genuine failure tracing::debug!( @@ -881,21 +894,25 @@ pub fn simulate_order_using_fork( ?contract_address, "Order failed despite having ACE unlock parent - treating as genuine failure" ); + None } else { - // No ACE unlock parent - queue for re-simulation with unlock + // No ACE unlock parent - needs re-simulation with unlock tracing::debug!( order = ?order.id(), ?err, ?contract_address, - "Order failed due to locked ACE - queueing for re-simulation with unlock" + "Order failed due to locked ACE - needs re-simulation with unlock" ); - return Ok(OrderSimResult::NonUnlockingAce { - order, - contract_address, - }); + Some(contract_address) } - } - Ok(OrderSimResult::Failed(err)) + } else { + None + }; + + Ok(OrderSimResult::Failed(SimulationFailure { + error: err, + ace_dependency, + })) } } } diff --git a/crates/rbuilder/src/building/testing/ace_tests/mod.rs b/crates/rbuilder/src/building/testing/ace_tests/mod.rs index 7304911cd..4110bafbe 100644 --- a/crates/rbuilder/src/building/testing/ace_tests/mod.rs +++ b/crates/rbuilder/src/building/testing/ace_tests/mod.rs @@ -278,6 +278,41 @@ fn test_handle_ace_unlock_with_mempool_unlock() -> eyre::Result<()> { 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 mut 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 cancellation = sim_tree.handle_ace_unlock(&mut result)?; + + // Optional should be cancelled because no orders are waiting + 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 { 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/simulation/sim_worker.rs b/crates/rbuilder/src/live_builder/simulation/sim_worker.rs index f8dd8889a..011042862 100644 --- a/crates/rbuilder/src/live_builder/simulation/sim_worker.rs +++ b/crates/rbuilder/src/live_builder/simulation/sim_worker.rs @@ -109,20 +109,24 @@ pub fn run_sim_worker

( } true } - OrderSimResult::NonUnlockingAce { - order, - contract_address, - } => { - let result = SimulatedResult::NonUnlockingAce { - order, - contract_address, - }; - if current_sim_context.results.try_send(result).is_err() { - error!(?order_id, "Failed to send NonUnlockingAce result"); + 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, + 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 } - OrderSimResult::Failed(_) => false, }; telemetry::inc_simulated_orders(sim_ok); telemetry::inc_simulation_gas_used(sim_result.gas_used); diff --git a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs index 248fc998d..cfdc755ed 100644 --- a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs +++ b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs @@ -228,9 +228,12 @@ impl SimulationJob { valid_simulated_orders.push(sim_result); } } - SimulatedResult::NonUnlockingAce { .. } => { - // Pass through to sim_tree for re-queuing with ACE unlock parent - valid_simulated_orders.push(sim_result); + SimulatedResult::Failed { ref failure, .. } => { + if failure.ace_dependency.is_some() { + // Pass through to sim_tree for re-queuing with ACE unlock parent + valid_simulated_orders.push(sim_result); + } + // Permanent failures are dropped } } } From 32247d024f534551857066f44a28ee4d942a155f Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 6 Jan 2026 13:56:19 -0400 Subject: [PATCH 34/51] feat: test and cases --- crates/rbuilder/src/building/sim.rs | 125 ++++++++----- .../src/building/testing/ace_tests/mod.rs | 169 ++++++++++++++++++ .../src/live_builder/simulation/sim_worker.rs | 3 +- 3 files changed, 250 insertions(+), 47 deletions(-) diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index ad2a77d99..b2eb941f9 100644 --- a/crates/rbuilder/src/building/sim.rs +++ b/crates/rbuilder/src/building/sim.rs @@ -114,17 +114,21 @@ impl AceExchangeState { struct PendingOrder { order: Order, unsatisfied_dependencies: usize, + /// ACE contracts already provided as unlock parents (for progressive multi-ACE discovery) + ace_unlock_contracts: HashSet

, } pub type SimulationId = u64; -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct SimulationRequest { pub id: SimulationId, pub order: Order, pub parents: Vec, - /// Whether any parent is an ACE unlock tx (vs just nonce dependencies) - pub has_ace_unlock_parent: bool, + /// ACE contracts for which we've already provided unlock parents. + /// Used to determine if a failure is genuine (contract already unlocked) or needs retry. + /// Supports multiple ACE contracts - order can progressively discover needed unlocks. + pub ace_unlock_contracts: HashSet
, } #[derive(Debug)] @@ -144,6 +148,8 @@ pub enum SimulatedResult { id: SimulationId, order: Order, failure: SimulationFailure, + /// ACE contracts that were already provided as unlock parents (preserved for re-queuing) + ace_unlock_contracts: HashSet
, simulation_time: Duration, }, } @@ -246,6 +252,7 @@ impl SimTree { PendingOrder { order, unsatisfied_dependencies, + ace_unlock_contracts: HashSet::default(), }, ); } @@ -254,7 +261,7 @@ impl SimTree { id: rand::random(), order, parents, - has_ace_unlock_parent: false, + ace_unlock_contracts: HashSet::default(), }); } } @@ -333,43 +340,53 @@ impl SimTree { } /// Check if an order needs ACE unlock and add that dependency. - /// Called after initial simulation when we detect a NonUnlocking ACE interaction. - fn add_ace_dependency_for_order( + /// Called after simulation when we detect a NonUnlocking ACE interaction. + /// Supports progressive multi-ACE discovery - existing_ace_unlock_contracts contains + /// contracts we've already provided unlocks for in previous sim attempts. + pub fn add_ace_dependency_for_order( &mut self, order: Order, - contract_address: Address, + new_contract: Address, + mut existing_ace_unlock_contracts: HashSet
, ) -> Result<(), ProviderError> { - let dep_key = DependencyKey::AceUnlock(contract_address); - - // Check if we already have an unlock provider - if let Some(sim_id) = self.dependency_providers.get(&dep_key) { - let Some(sim) = self.sims.get(sim_id) else { - error!("SimTree bug: ACE unlock provider sim not found"); - // Fall through to pending logic below - return self.add_order_to_pending(order, dep_key); - }; - let mut parents = sim.previous_orders.clone(); - parents.push(sim.simulated_order.order.clone()); + // Add new contract to the set + existing_ace_unlock_contracts.insert(new_contract); + let dep_key = DependencyKey::AceUnlock(new_contract); + + // Check if we already have an unlock provider for the new contract + if self.dependency_providers.contains_key(&dep_key) { + // Build parents from ALL ACE unlock contracts we need + let mut parents = Vec::new(); + for contract in &existing_ace_unlock_contracts { + let key = DependencyKey::AceUnlock(*contract); + if let Some(sim_id) = self.dependency_providers.get(&key) { + if let Some(sim) = self.sims.get(sim_id) { + parents.extend(sim.previous_orders.clone()); + parents.push(sim.simulated_order.order.clone()); + } + } + } - // Order is ready with the unlock tx as parent + // Order is ready with all unlock txs as parents self.ready_orders.push(SimulationRequest { id: rand::random(), order, parents, - has_ace_unlock_parent: true, + ace_unlock_contracts: existing_ace_unlock_contracts, }); return Ok(()); } - // No unlock yet - add to pending - self.add_order_to_pending(order, dep_key) + // New unlock not yet available - add to pending + self.add_order_to_pending_with_ace(order, dep_key, existing_ace_unlock_contracts) } - /// Helper to add an order to pending state with a single dependency - fn add_order_to_pending( + /// Helper to add an order to pending state with ACE unlock tracking + fn add_order_to_pending_with_ace( &mut self, order: Order, dep_key: DependencyKey, + ace_unlock_contracts: HashSet
, ) -> Result<(), ProviderError> { self.pending_dependencies .entry(dep_key) @@ -380,6 +397,7 @@ impl SimTree { PendingOrder { order, unsatisfied_dependencies: 1, + ace_unlock_contracts, }, ); Ok(()) @@ -421,14 +439,15 @@ impl SimTree { simulated_order: simulated_order.clone(), }, ); - let mut orders_ready = Vec::new(); - let mut is_ace_dependency = false; + // Track orders that become ready along with their ACE state + let mut orders_ready: Vec = Vec::new(); + let mut ace_unlock_contract: Option
= None; // Process each dependency this simulation satisfies for dep_key in dependencies_satisfied.iter().cloned() { - // Track if any dependency is an ACE unlock - if matches!(dep_key, DependencyKey::AceUnlock(_)) { - is_ace_dependency = true; + // Track if this dependency is an ACE unlock and which contract + if let DependencyKey::AceUnlock(contract) = dep_key { + ace_unlock_contract = Some(contract); } match self.dependency_providers.entry(dep_key.clone()) { @@ -467,7 +486,7 @@ impl SimTree { let pending_order = entry.get_mut(); pending_order.unsatisfied_dependencies -= 1; if pending_order.unsatisfied_dependencies == 0 { - orders_ready.push(entry.remove().order); + orders_ready.push(entry.remove()); } } Entry::Vacant(_) => { @@ -481,20 +500,22 @@ impl SimTree { } } - for ready_order in orders_ready { - let pending_state = self.get_order_dependency_state(&ready_order)?; + for mut ready_pending_order in orders_ready { + let pending_state = self.get_order_dependency_state(&ready_pending_order.order)?; match pending_state { OrderDependencyState::Ready(mut parents) => { // If this order became ready due to ACE unlock, add the unlock tx as parent - if is_ace_dependency { + // and track the contract in ace_unlock_contracts + if let Some(contract) = ace_unlock_contract { + ready_pending_order.ace_unlock_contracts.insert(contract); parents.extend(previous_orders.iter().cloned()); parents.push(simulated_order.order.clone()); } self.ready_orders.push(SimulationRequest { id: rand::random(), - order: ready_order, + order: ready_pending_order.order, parents, - has_ace_unlock_parent: is_ace_dependency, + ace_unlock_contracts: ready_pending_order.ace_unlock_contracts, }); } OrderDependencyState::Invalid => { @@ -637,10 +658,16 @@ impl SimTree { ace_dependency: Some(contract_address), .. }, + ace_unlock_contracts, .. } => { // Order failed but needs ACE unlock - queue for re-simulation - self.add_ace_dependency_for_order(order, contract_address)?; + // 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 @@ -713,7 +740,7 @@ where &mut local_ctx, &mut block_state, sim_tree.ace_configs(), - sim_task.has_ace_unlock_parent, + &sim_task.ace_unlock_contracts, )?; let (_, provider) = block_state.into_parts(); state_for_sim = provider; @@ -721,7 +748,12 @@ where OrderSimResult::Failed(failure) => { if let Some(contract_address) = failure.ace_dependency { // Order failed but needs ACE unlock - queue for re-simulation - sim_tree.add_ace_dependency_for_order(sim_task.order, contract_address)?; + // 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!( @@ -779,7 +811,7 @@ pub fn simulate_order( local_ctx: &mut ThreadBlockBuildingContext, state: &mut BlockState, ace_configs: &HashMap, - has_ace_unlock_parent: bool, + ace_unlock_contracts: &HashSet
, ) -> Result { let mut tracer = AccumulatorSimulationTracer::new(); let mut fork = PartialBlockFork::new(state, ctx, local_ctx).with_tracer(&mut tracer); @@ -790,7 +822,7 @@ pub fn simulate_order( &mut fork, &ctx.mempool_tx_detector, ace_configs, - has_ace_unlock_parent, + ace_unlock_contracts, ); fork.rollback(rollback_point); let sim_res = sim_res?; @@ -808,7 +840,7 @@ pub fn simulate_order_using_fork( fork: &mut PartialBlockFork<'_, '_, '_, '_, Tracer, NullPartialBlockForkExecutionTracer>, mempool_tx_detector: &MempoolTxsDetector, ace_configs: &HashMap, - has_ace_unlock_parent: bool, + ace_unlock_contracts: &HashSet
, ) -> Result { let start = Instant::now(); @@ -886,22 +918,23 @@ pub fn simulate_order_using_fork( let ace_dependency = if let Some(AceInteraction::NonUnlocking { contract_address }) = ace_interaction { - if has_ace_unlock_parent { - // Order had ACE unlock parent but still failed - genuine failure + if ace_unlock_contracts.contains(&contract_address) { + // Already had unlock for this contract but still failed - genuine failure tracing::debug!( order = ?order.id(), ?err, ?contract_address, - "Order failed despite having ACE unlock parent - treating as genuine failure" + "Order failed despite having ACE unlock for this contract - genuine failure" ); None } else { - // No ACE unlock parent - needs re-simulation with unlock + // Need unlock for this contract (might already have others) tracing::debug!( order = ?order.id(), ?err, ?contract_address, - "Order failed due to locked ACE - needs re-simulation with unlock" + existing_unlocks = ?ace_unlock_contracts, + "Order needs additional ACE unlock" ); Some(contract_address) } diff --git a/crates/rbuilder/src/building/testing/ace_tests/mod.rs b/crates/rbuilder/src/building/testing/ace_tests/mod.rs index 4110bafbe..954985664 100644 --- a/crates/rbuilder/src/building/testing/ace_tests/mod.rs +++ b/crates/rbuilder/src/building/testing/ace_tests/mod.rs @@ -334,3 +334,172 @@ fn test_dependency_key_ace_unlock() { _ => 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(()) +} diff --git a/crates/rbuilder/src/live_builder/simulation/sim_worker.rs b/crates/rbuilder/src/live_builder/simulation/sim_worker.rs index 011042862..dbe609fc5 100644 --- a/crates/rbuilder/src/live_builder/simulation/sim_worker.rs +++ b/crates/rbuilder/src/live_builder/simulation/sim_worker.rs @@ -72,7 +72,7 @@ pub fn run_sim_worker

( &mut local_ctx, &mut block_state, ¤t_sim_context.ace_configs, - task.has_ace_unlock_parent, + &task.ace_unlock_contracts, ); let sim_ok = match sim_result { Ok(sim_result) => { @@ -116,6 +116,7 @@ pub fn run_sim_worker

( 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() { From 1ccbd75c99a528e878ebcdce767fec1ca90a0a7f Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 6 Jan 2026 14:08:42 -0400 Subject: [PATCH 35/51] cleanup from bad merge --- crates/rbuilder/src/mev_boost/mod.rs | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/crates/rbuilder/src/mev_boost/mod.rs b/crates/rbuilder/src/mev_boost/mod.rs index 970d9de78..bc5ccc90d 100644 --- a/crates/rbuilder/src/mev_boost/mod.rs +++ b/crates/rbuilder/src/mev_boost/mod.rs @@ -1,7 +1,6 @@ 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; @@ -796,29 +795,6 @@ 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() { From 930a89273311301a289f390c4b8a1babc1dcbb8a Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 6 Jan 2026 14:19:48 -0400 Subject: [PATCH 36/51] wip --- .../live_builder/simulation/simulation_job.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs index cfdc755ed..99811a568 100644 --- a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs +++ b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs @@ -190,8 +190,8 @@ impl SimulationJob { &mut self, new_sim_results: &mut Vec, ) -> bool { - // send results - let mut valid_simulated_orders = Vec::new(); + // 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 { @@ -225,22 +225,20 @@ impl SimulationJob { self.sim_tracer.update_simulation_sent(&sim_result); } } - valid_simulated_orders.push(sim_result); + sim_tree_results.push(sim_result); } } - SimulatedResult::Failed { ref failure, .. } => { - if failure.ace_dependency.is_some() { - // Pass through to sim_tree for re-queuing with ACE unlock parent - valid_simulated_orders.push(sim_result); - } - // Permanent failures are dropped + SimulatedResult::Failed { .. } => { + // Failed with ACE dependency - pass to sim_tree for re-queuing with unlock parent + // Note: sim_worker only sends Failed results when ace_dependency.is_some() + sim_tree_results.push(sim_result); } } } // update simtree let (_, cancellations) = match self .sim_tree - .submit_simulation_tasks_results(valid_simulated_orders) + .submit_simulation_tasks_results(sim_tree_results) { Ok(result) => result, Err(err) => { From d9f3e80853fc8dbb306b0e5af14b8492791265d5 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 6 Jan 2026 15:18:35 -0400 Subject: [PATCH 37/51] bug fixes --- crates/rbuilder-primitives/src/ace.rs | 211 +++++++++++++++++++++++--- crates/rbuilder/src/building/sim.rs | 19 +-- 2 files changed, 204 insertions(+), 26 deletions(-) diff --git a/crates/rbuilder-primitives/src/ace.rs b/crates/rbuilder-primitives/src/ace.rs index 012ddf288..d25122763 100644 --- a/crates/rbuilder-primitives/src/ace.rs +++ b/crates/rbuilder-primitives/src/ace.rs @@ -25,12 +25,20 @@ pub struct AceConfig { /// Classify an ACE order interaction type based on state trace, simulation success, and config. /// Uses both state trace (address access) AND function signatures to determine interaction type. +/// +/// For `ProtocolForce` and `ProtocolOptional` classification, the transaction must: +/// 1. Be a direct call to the ACE contract (`tx_to` in `config.to_addresses`) +/// 2. Have the appropriate signature (`force_signatures` or `unlock_signatures`) +/// 3. Be from a whitelisted address (`tx_from` in `config.from_addresses`) +/// +/// All other unlocking transactions are classified as `User`. pub fn classify_ace_interaction( state_trace: &UsedStateTrace, sim_success: bool, config: &AceConfig, selector: Option, tx_to: Option

, + tx_from: Option
, ) -> Option { let any_ace_slots_accessed = config .to_addresses @@ -56,6 +64,9 @@ pub fn classify_ace_interaction( // Check if this is a direct call to the protocol let is_direct_protocol_call = tx_to.is_some_and(|to| config.to_addresses.contains(&to)); + // Check if transaction is from a whitelisted address (required for protocol orders) + let is_from_whitelisted = tx_from.is_some_and(|from| config.from_addresses.contains(&from)); + // Check function selectors with direct HashSet lookup let is_force_sig = selector.is_some_and(|sel| config.force_signatures.contains(&sel)); let is_unlock_sig = selector.is_some_and(|sel| config.unlock_signatures.contains(&sel)); @@ -63,11 +74,13 @@ pub fn classify_ace_interaction( let contract_address = config.contract_address; if sim_success && (is_force_sig || is_unlock_sig) { - let source = if is_direct_protocol_call && is_force_sig { + // Protocol orders require: direct call + correct signature + whitelisted sender + let source = if is_direct_protocol_call && is_force_sig && is_from_whitelisted { AceUnlockSource::ProtocolForce - } else if is_direct_protocol_call && is_unlock_sig { + } else if is_direct_protocol_call && is_unlock_sig && is_from_whitelisted { AceUnlockSource::ProtocolOptional } else { + // Any unlock without all three requirements is a User unlock AceUnlockSource::User }; Some(AceInteraction::Unlocking { @@ -185,13 +198,20 @@ mod tests { let contract = config.contract_address; let detection_slot = *config.detection_slots.iter().next().unwrap(); let force_selector = *config.force_signatures.iter().next().unwrap(); + let whitelisted_from = *config.from_addresses.iter().next().unwrap(); // Mock state trace with detection slot accessed let trace = mock_state_trace_with_slot(contract, detection_slot); - // Direct call to ACE contract with force signature should be ProtocolForce - let result = - classify_ace_interaction(&trace, true, &config, Some(force_selector), Some(contract)); + // Direct call to ACE contract with force signature FROM WHITELISTED ADDRESS should be ProtocolForce + let result = classify_ace_interaction( + &trace, + true, + &config, + Some(force_selector), + Some(contract), + Some(whitelisted_from), + ); assert_eq!( result, @@ -213,13 +233,20 @@ mod tests { let contract = config.contract_address; let detection_slot = *config.detection_slots.iter().next().unwrap(); let unlock_selector = *config.unlock_signatures.iter().next().unwrap(); + let whitelisted_from = *config.from_addresses.iter().next().unwrap(); // Mock state trace with detection slot accessed let trace = mock_state_trace_with_slot(contract, detection_slot); - // Direct call to ACE contract with unlock signature should be ProtocolOptional - let result = - classify_ace_interaction(&trace, true, &config, Some(unlock_selector), Some(contract)); + // Direct call to ACE contract with unlock signature FROM WHITELISTED ADDRESS should be ProtocolOptional + let result = classify_ace_interaction( + &trace, + true, + &config, + Some(unlock_selector), + Some(contract), + Some(whitelisted_from), + ); assert_eq!( result, @@ -245,7 +272,16 @@ mod tests { let trace = mock_state_trace_with_slot(contract, detection_slot); // tx.to is NOT the ACE contract (indirect call via user tx) = User unlock - let result = classify_ace_interaction(&trace, true, &config, Some(unlock_selector), None); + // Even with whitelisted from address, indirect call is User + let whitelisted_from = *config.from_addresses.iter().next().unwrap(); + let result = classify_ace_interaction( + &trace, + true, + &config, + Some(unlock_selector), + None, + Some(whitelisted_from), + ); assert_eq!( result, @@ -266,11 +302,19 @@ mod tests { let config = real_ace_config(); let contract = config.contract_address; let detection_slot = *config.detection_slots.iter().next().unwrap(); + let whitelisted_from = *config.from_addresses.iter().next().unwrap(); let trace = mock_state_trace_with_slot(contract, detection_slot); // No unlock/force signature = NonUnlocking - let result = classify_ace_interaction(&trace, true, &config, None, Some(contract)); + let result = classify_ace_interaction( + &trace, + true, + &config, + None, + Some(contract), + Some(whitelisted_from), + ); assert_eq!( result, @@ -290,6 +334,7 @@ mod tests { let contract = config.contract_address; let detection_slot = *config.detection_slots.iter().next().unwrap(); let unlock_selector = *config.unlock_signatures.iter().next().unwrap(); + let whitelisted_from = *config.from_addresses.iter().next().unwrap(); let trace = mock_state_trace_with_slot(contract, detection_slot); @@ -300,6 +345,7 @@ mod tests { &config, Some(unlock_selector), Some(contract), + Some(whitelisted_from), ); assert_eq!( @@ -319,6 +365,7 @@ mod tests { let config = real_ace_config(); let empty_trace = UsedStateTrace::default(); let force_selector = *config.force_signatures.iter().next().unwrap(); + let whitelisted_from = *config.from_addresses.iter().next().unwrap(); // Even with valid force signature, no slot access = None let result = classify_ace_interaction( @@ -327,6 +374,7 @@ mod tests { &config, Some(force_selector), Some(config.contract_address), + Some(whitelisted_from), ); assert_eq!( @@ -341,6 +389,7 @@ mod tests { let config = real_ace_config(); let wrong_slot = b256!("0000000000000000000000000000000000000000000000000000000000000099"); let force_selector = *config.force_signatures.iter().next().unwrap(); + let whitelisted_from = *config.from_addresses.iter().next().unwrap(); let trace = mock_state_trace_with_slot(config.contract_address, wrong_slot); @@ -351,6 +400,7 @@ mod tests { &config, Some(force_selector), Some(config.contract_address), + Some(whitelisted_from), ); assert_eq!( @@ -489,6 +539,7 @@ mod tests { let config = real_ace_config(); let contract = config.contract_address; let slot = b256!("0000000000000000000000000000000000000000000000000000000000000003"); + let whitelisted_from = *config.from_addresses.iter().next().unwrap(); // Optional unlock signature from real transaction let unlock_selector = Selector::from_slice(&[0x18, 0x28, 0xe0, 0xe7]); @@ -496,9 +547,15 @@ mod tests { // Mock state trace showing slot 3 was accessed let trace = mock_state_trace_with_slot(contract, slot); - // Test 1: Direct call to ACE contract = ProtocolOptional - let result = - classify_ace_interaction(&trace, true, &config, Some(unlock_selector), Some(contract)); + // Test 1: Direct call to ACE contract FROM WHITELISTED ADDRESS = ProtocolOptional + let result = classify_ace_interaction( + &trace, + true, + &config, + Some(unlock_selector), + Some(contract), + Some(whitelisted_from), + ); assert_eq!( result, @@ -509,8 +566,14 @@ mod tests { ); // Test 2: Indirect call (user tx) = User unlock - let result_indirect = - classify_ace_interaction(&trace, true, &config, Some(unlock_selector), None); + let result_indirect = classify_ace_interaction( + &trace, + true, + &config, + Some(unlock_selector), + None, + Some(whitelisted_from), + ); assert_eq!( result_indirect, @@ -527,6 +590,7 @@ mod tests { &config, Some(unlock_selector), Some(contract), + Some(whitelisted_from), ); assert_eq!( @@ -544,6 +608,7 @@ mod tests { let contract = config.contract_address; let detection_slot = *config.detection_slots.iter().next().unwrap(); let force_selector = *config.force_signatures.iter().next().unwrap(); + let whitelisted_from = *config.from_addresses.iter().next().unwrap(); let mut trace = UsedStateTrace::default(); // Write to slot instead of reading @@ -556,8 +621,14 @@ mod tests { ); // Writing to detection slot should still trigger classification - let result = - classify_ace_interaction(&trace, true, &config, Some(force_selector), Some(contract)); + let result = classify_ace_interaction( + &trace, + true, + &config, + Some(force_selector), + Some(contract), + Some(whitelisted_from), + ); assert_eq!( result, @@ -567,4 +638,110 @@ mod tests { }) ); } + + #[test] + fn test_non_whitelisted_from_address_becomes_user() { + // Force call from non-whitelisted address should become User, not ProtocolForce + let config = real_ace_config(); + let contract = config.contract_address; + let detection_slot = *config.detection_slots.iter().next().unwrap(); + let force_selector = *config.force_signatures.iter().next().unwrap(); + + // Use an address NOT in the whitelist + let non_whitelisted = address!("1111111111111111111111111111111111111111"); + assert!( + !config.from_addresses.contains(&non_whitelisted), + "Address should not be in whitelist for this test" + ); + + let trace = mock_state_trace_with_slot(contract, detection_slot); + + // Direct call with force signature but from non-whitelisted address = User + let result = classify_ace_interaction( + &trace, + true, + &config, + Some(force_selector), + Some(contract), + Some(non_whitelisted), + ); + + assert_eq!( + result, + Some(AceInteraction::Unlocking { + contract_address: contract, + source: AceUnlockSource::User // NOT ProtocolForce! + }) + ); + + // Should still be unlocking, but not a protocol tx + assert!(result.unwrap().is_unlocking()); + assert!(!result.unwrap().is_protocol_tx()); + } + + #[test] + fn test_non_whitelisted_optional_unlock_becomes_user() { + // Optional unlock from non-whitelisted address should become User, not ProtocolOptional + let config = real_ace_config(); + let contract = config.contract_address; + let detection_slot = *config.detection_slots.iter().next().unwrap(); + let unlock_selector = *config.unlock_signatures.iter().next().unwrap(); + + // Use an address NOT in the whitelist + let non_whitelisted = address!("2222222222222222222222222222222222222222"); + assert!(!config.from_addresses.contains(&non_whitelisted)); + + let trace = mock_state_trace_with_slot(contract, detection_slot); + + // Direct call with unlock signature but from non-whitelisted address = User + let result = classify_ace_interaction( + &trace, + true, + &config, + Some(unlock_selector), + Some(contract), + Some(non_whitelisted), + ); + + assert_eq!( + result, + Some(AceInteraction::Unlocking { + contract_address: contract, + source: AceUnlockSource::User // NOT ProtocolOptional! + }) + ); + + // Should be unlocking but not a protocol tx + assert!(result.unwrap().is_unlocking()); + assert!(!result.unwrap().is_protocol_tx()); + } + + #[test] + fn test_none_from_address_becomes_user() { + // When tx_from is None, should classify as User even with correct signature and direct call + let config = real_ace_config(); + let contract = config.contract_address; + let detection_slot = *config.detection_slots.iter().next().unwrap(); + let force_selector = *config.force_signatures.iter().next().unwrap(); + + let trace = mock_state_trace_with_slot(contract, detection_slot); + + // Direct call with force signature but tx_from = None = User + let result = classify_ace_interaction( + &trace, + true, + &config, + Some(force_selector), + Some(contract), + None, // No from address + ); + + assert_eq!( + result, + Some(AceInteraction::Unlocking { + contract_address: contract, + source: AceUnlockSource::User + }) + ); + } } diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index b2eb941f9..aefcfd591 100644 --- a/crates/rbuilder/src/building/sim.rs +++ b/crates/rbuilder/src/building/sim.rs @@ -536,7 +536,7 @@ impl SimTree { /// Note: NonUnlocking ACE interactions are handled at the OrderSimResult level. pub fn handle_ace_unlock( &mut self, - result: &mut SimulatedResult, + result: &SimulatedResult, ) -> Result, ProviderError> { let SimulatedResult::Success { simulated_order, @@ -643,8 +643,7 @@ impl SimTree { for result in results { match result { SimulatedResult::Success { .. } => { - let mut result = result; - if let Some(id) = self.handle_ace_unlock(&mut result)? { + if let Some(id) = self.handle_ace_unlock(&result)? { cancellations.push(id); } // All successful results need to be processed for dependency tracking @@ -705,7 +704,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(); @@ -875,21 +874,23 @@ pub fn simulate_order_using_fork( let used_state_trace = fork.tracer.as_mut().and_then(|t| t.take_used_state_trace()); // 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, _)| { + // Get function selector, tx.to, and tx.from from order's first transaction + let (selector, tx_to, tx_from): (Option, Option
, Option
) = order + .list_txs() + .first() + .map_or((None, 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()) + (sel, tx.to(), Some(tx.signer())) }); let ace_interaction = used_state_trace.as_ref().and_then(|trace| { ace_configs.iter().find_map(|(_, config)| { - classify_ace_interaction(trace, sim_success, config, selector, tx_to) + classify_ace_interaction(trace, sim_success, config, selector, tx_to, tx_from) }) }); From b03de11dd48f522d038182568f2b77f50c5790aa Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 6 Jan 2026 16:14:54 -0400 Subject: [PATCH 38/51] fix edgecases --- crates/rbuilder-primitives/src/lib.rs | 6 +- .../rbuilder/src/bin/run-bundle-on-prefix.rs | 4 +- .../building/block_orders/order_priority.rs | 2 +- .../block_orders/share_bundle_merger.rs | 2 +- .../src/building/block_orders/test_context.rs | 2 +- .../block_orders/test_data_generator.rs | 2 +- .../src/building/builders/ordering_builder.rs | 6 +- .../block_building_result_assembler.rs | 24 +- .../parallel_builder/conflict_resolvers.rs | 2 +- .../conflict_task_generator.rs | 2 +- .../builders/parallel_builder/groups.rs | 2 +- crates/rbuilder/src/building/sim.rs | 249 +++++++++++------- .../src/building/testing/ace_tests/mod.rs | 144 +++++++++- .../building/testing/bundle_tests/setup.rs | 2 +- .../src/live_builder/simulation/sim_worker.rs | 16 +- 15 files changed, 318 insertions(+), 147 deletions(-) diff --git a/crates/rbuilder-primitives/src/lib.rs b/crates/rbuilder-primitives/src/lib.rs index bddc1a057..f95678b25 100644 --- a/crates/rbuilder-primitives/src/lib.rs +++ b/crates/rbuilder-primitives/src/lib.rs @@ -1363,9 +1363,9 @@ pub struct SimulatedOrder { pub sim_value: SimValue, /// Info about read/write slots during the simulation to help figure out what the Order is doing. pub used_state_trace: Option, - /// ACE interaction classification - None if not an ACE interaction. - /// Use `ace_interaction.map(|a| a.is_force()).unwrap_or(false)` to check if force unlock. - pub ace_interaction: Option, + /// ACE interactions - one per ACE contract this order interacts with. + /// Empty if no ACE interactions. + pub ace_interactions: Vec, } impl SimulatedOrder { diff --git a/crates/rbuilder/src/bin/run-bundle-on-prefix.rs b/crates/rbuilder/src/bin/run-bundle-on-prefix.rs index 2679647fe..2c7c99d7a 100644 --- a/crates/rbuilder/src/bin/run-bundle-on-prefix.rs +++ b/crates/rbuilder/src/bin/run-bundle-on-prefix.rs @@ -220,7 +220,7 @@ async fn main() -> eyre::Result<()> { order, sim_value: Default::default(), used_state_trace: Default::default(), - ace_interaction: None, + ace_interactions: Vec::new(), }; let res = builder.commit_order(&mut block_info.local_ctx, &sim_order, &|_| Ok(()))?; println!("{:?} {:?}", tx.hash(), res.is_ok()); @@ -316,7 +316,7 @@ fn execute_orders_on_tob( order: order_ts.order.clone(), sim_value: Default::default(), used_state_trace: Default::default(), - ace_interaction: None, + ace_interactions: Vec::new(), }; let res = builder.commit_order(&mut block_info.local_ctx, &sim_order, &|_| Ok(()))?; let profit = res diff --git a/crates/rbuilder/src/building/block_orders/order_priority.rs b/crates/rbuilder/src/building/block_orders/order_priority.rs index 9c7ddeaf5..c4f8343c5 100644 --- a/crates/rbuilder/src/building/block_orders/order_priority.rs +++ b/crates/rbuilder/src/building/block_orders/order_priority.rs @@ -332,7 +332,7 @@ mod test { U256::from(non_mempool_profit), gas, ), - ace_interaction: None, + ace_interactions: Vec::new(), used_state_trace: None, }) } diff --git a/crates/rbuilder/src/building/block_orders/share_bundle_merger.rs b/crates/rbuilder/src/building/block_orders/share_bundle_merger.rs index 7d19cfc62..c5515fb60 100644 --- a/crates/rbuilder/src/building/block_orders/share_bundle_merger.rs +++ b/crates/rbuilder/src/building/block_orders/share_bundle_merger.rs @@ -148,7 +148,7 @@ impl MultiBackrunManager { order: Order::ShareBundle(sbundle), sim_value: highest_payback_order.sim_order.sim_value.clone(), used_state_trace: highest_payback_order.sim_order.used_state_trace.clone(), - ace_interaction: highest_payback_order.sim_order.ace_interaction, + ace_interactions: highest_payback_order.sim_order.ace_interactions.clone(), })) } diff --git a/crates/rbuilder/src/building/block_orders/test_context.rs b/crates/rbuilder/src/building/block_orders/test_context.rs index 3199cb3b2..0e2ce0ddf 100644 --- a/crates/rbuilder/src/building/block_orders/test_context.rs +++ b/crates/rbuilder/src/building/block_orders/test_context.rs @@ -169,7 +169,7 @@ impl TestContext { order, sim_value, used_state_trace: None, - ace_interaction: None, + ace_interactions: Vec::new(), }) } diff --git a/crates/rbuilder/src/building/block_orders/test_data_generator.rs b/crates/rbuilder/src/building/block_orders/test_data_generator.rs index f2a8fd4a7..c3bd4e7f6 100644 --- a/crates/rbuilder/src/building/block_orders/test_data_generator.rs +++ b/crates/rbuilder/src/building/block_orders/test_data_generator.rs @@ -31,7 +31,7 @@ impl TestDataGenerator { order, sim_value, used_state_trace: None, - ace_interaction: None, + ace_interactions: Vec::new(), }) } } diff --git a/crates/rbuilder/src/building/builders/ordering_builder.rs b/crates/rbuilder/src/building/builders/ordering_builder.rs index 0876072ee..6baa819cb 100644 --- a/crates/rbuilder/src/building/builders/ordering_builder.rs +++ b/crates/rbuilder/src/building/builders/ordering_builder.rs @@ -294,11 +294,7 @@ impl OrderingBuilderContext { let all_orders = block_orders.get_all_orders(); let mut ace_orders = Vec::new(); for order in all_orders { - if order - .ace_interaction - .map(|a| a.is_protocol_tx()) - .unwrap_or(false) - { + if order.ace_interactions.iter().any(|a| a.is_protocol_tx()) { ace_orders.push(order.clone()); // Remove from block_orders so they don't get processed in fill_orders block_orders.remove_order(order.id()); diff --git a/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs b/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs index d4da950ec..358cb9a35 100644 --- a/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs +++ b/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs @@ -191,11 +191,7 @@ impl BlockBuildingResultAssembler { let mut ace_orders = Vec::new(); for (_, group) in best_orderings_per_group.iter() { for order in group.orders.iter() { - if order - .ace_interaction - .map(|a| a.is_protocol_tx()) - .unwrap_or(false) - { + if order.ace_interactions.iter().any(|a| a.is_protocol_tx()) { ace_orders.push(order.clone()); } } @@ -208,9 +204,9 @@ impl BlockBuildingResultAssembler { .sequence_of_orders .retain(|(order_idx, _)| { !group.orders[*order_idx] - .ace_interaction - .map(|a| a.is_protocol_tx()) - .unwrap_or(false) + .ace_interactions + .iter() + .any(|a| a.is_protocol_tx()) }); } @@ -309,11 +305,7 @@ impl BlockBuildingResultAssembler { let mut ace_orders = Vec::new(); for (_, group) in best_orderings_per_group.iter() { for order in group.orders.iter() { - if order - .ace_interaction - .map(|a| a.is_protocol_tx()) - .unwrap_or(false) - { + if order.ace_interactions.iter().any(|a| a.is_protocol_tx()) { ace_orders.push(order.clone()); } } @@ -326,9 +318,9 @@ impl BlockBuildingResultAssembler { .sequence_of_orders .retain(|(order_idx, _)| { !group.orders[*order_idx] - .ace_interaction - .map(|a| a.is_protocol_tx()) - .unwrap_or(false) + .ace_interactions + .iter() + .any(|a| a.is_protocol_tx()) }); } diff --git a/crates/rbuilder/src/building/builders/parallel_builder/conflict_resolvers.rs b/crates/rbuilder/src/building/builders/parallel_builder/conflict_resolvers.rs index 996d1a6c0..07781e94b 100644 --- a/crates/rbuilder/src/building/builders/parallel_builder/conflict_resolvers.rs +++ b/crates/rbuilder/src/building/builders/parallel_builder/conflict_resolvers.rs @@ -533,7 +533,7 @@ mod tests { order: Order::Bundle(bundle), used_state_trace: None, sim_value, - ace_interaction: None, + ace_interactions: Vec::new(), }) } } diff --git a/crates/rbuilder/src/building/builders/parallel_builder/conflict_task_generator.rs b/crates/rbuilder/src/building/builders/parallel_builder/conflict_task_generator.rs index 381b634d9..ab7b2993a 100644 --- a/crates/rbuilder/src/building/builders/parallel_builder/conflict_task_generator.rs +++ b/crates/rbuilder/src/building/builders/parallel_builder/conflict_task_generator.rs @@ -496,7 +496,7 @@ mod tests { }), sim_value, used_state_trace: Some(trace), - ace_interaction: None, + ace_interactions: Vec::new(), }) } } diff --git a/crates/rbuilder/src/building/builders/parallel_builder/groups.rs b/crates/rbuilder/src/building/builders/parallel_builder/groups.rs index be892f41a..691245664 100644 --- a/crates/rbuilder/src/building/builders/parallel_builder/groups.rs +++ b/crates/rbuilder/src/building/builders/parallel_builder/groups.rs @@ -479,7 +479,7 @@ mod tests { }), used_state_trace: Some(trace), sim_value: SimValue::default(), - ace_interaction: None, + ace_interactions: Vec::new(), }) } } diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index aefcfd591..a251097b8 100644 --- a/crates/rbuilder/src/building/sim.rs +++ b/crates/rbuilder/src/building/sim.rs @@ -537,78 +537,93 @@ impl SimTree { pub fn handle_ace_unlock( &mut self, result: &SimulatedResult, - ) -> Result, ProviderError> { + ) -> Result, ProviderError> { let SimulatedResult::Success { simulated_order, previous_orders, .. } = result else { - return Ok(None); - }; - - let Some(AceInteraction::Unlocking { - contract_address, - source, - }) = simulated_order.ace_interaction - else { - return Ok(None); + return Ok(Vec::new()); }; // If this order already has parents, it was re-simulated - just pass through if !previous_orders.is_empty() { - return Ok(None); + return Ok(Vec::new()); } - // Register the unlock in ACE state based on source type - let cancellation = match source { - AceUnlockSource::ProtocolForce => { - let state = self.ace_state.entry(contract_address).or_default(); - state.force_unlock_order = Some(simulated_order.clone()); - trace!( - "Added forced ACE protocol unlock order for {:?}", - contract_address - ); - None - } - AceUnlockSource::ProtocolOptional => { - let state = self.ace_state.entry(contract_address).or_default(); + // Get all unlocking interactions + let unlocking_interactions: Vec<_> = simulated_order + .ace_interactions + .iter() + .filter_map(|i| match i { + AceInteraction::Unlocking { + contract_address, + source, + } => Some((*contract_address, *source)), + AceInteraction::NonUnlocking { .. } => None, + }) + .collect(); + + if unlocking_interactions.is_empty() { + return Ok(Vec::new()); + } - // Check if user unlock already available - cancel optional - if state.has_mempool_unlock { + let mut cancellations = Vec::new(); + + // Process each unlocking interaction + for (contract_address, source) in unlocking_interactions { + match source { + AceUnlockSource::ProtocolForce => { + let state = self.ace_state.entry(contract_address).or_default(); + state.force_unlock_order = Some(simulated_order.clone()); trace!( - "Cancelling optional ACE unlock for {:?} - user unlock exists", + "Added forced ACE protocol unlock order for {:?}", contract_address ); - return Ok(Some(simulated_order.order.id())); } + AceUnlockSource::ProtocolOptional => { + let state = self.ace_state.entry(contract_address).or_default(); - // Only include optional if there are orders waiting on this unlock - let dep_key = DependencyKey::AceUnlock(contract_address); - if !self.pending_dependencies.contains_key(&dep_key) { + // Check if user unlock already available - cancel optional + if state.has_mempool_unlock { + trace!( + "Cancelling optional ACE unlock for {:?} - user unlock exists", + contract_address + ); + cancellations.push(simulated_order.order.id()); + continue; + } + + // Only include optional if there are orders waiting on this unlock + let dep_key = DependencyKey::AceUnlock(contract_address); + if !self.pending_dependencies.contains_key(&dep_key) { + trace!( + "Cancelling optional ACE unlock for {:?} - no pending orders need it", + contract_address + ); + cancellations.push(simulated_order.order.id()); + continue; + } + + // Store optional unlock - there are orders waiting for it + state.optional_unlock_order = Some(simulated_order.clone()); trace!( - "Cancelling optional ACE unlock for {:?} - no pending orders need it", + "Added optional ACE protocol unlock order for {:?}", contract_address ); - return Ok(Some(simulated_order.order.id())); } - - // Store optional unlock - there are orders waiting for it - state.optional_unlock_order = Some(simulated_order.clone()); - trace!( - "Added optional ACE protocol unlock order for {:?}", - contract_address - ); - None - } - AceUnlockSource::User => { - // A user unlocked ACE via mempool - mark it and cancel any optional protocol order - trace!("User mempool unlock detected for {:?}", contract_address); - self.mark_mempool_unlock(contract_address) + AceUnlockSource::User => { + // A user unlocked ACE via mempool - mark it and cancel any optional protocol order + trace!("User mempool unlock detected for {:?}", contract_address); + if let Some(cancelled_id) = self.mark_mempool_unlock(contract_address) { + cancellations.push(cancelled_id); + } + } } - }; + } - Ok(cancellation) + Ok(cancellations) } /// Mark that a mempool unlocking order has been seen for a contract address. @@ -643,9 +658,7 @@ impl SimTree { for result in results { match result { SimulatedResult::Success { .. } => { - if let Some(id) = self.handle_ace_unlock(&result)? { - cancellations.push(id); - } + 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); @@ -769,12 +782,15 @@ where .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)); + // 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 { @@ -873,26 +889,47 @@ pub fn simulate_order_using_fork( // 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 interaction from the state trace using config - // Get function selector, tx.to, and tx.from from order's first transaction - let (selector, tx_to, tx_from): (Option, Option
, Option
) = order - .list_txs() - .first() - .map_or((None, None, None), |(tx, _)| { + // 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 sel = if input.len() >= 4 { + let selector = if input.len() >= 4 { Some(Selector::from_slice(&input[..4])) } else { None }; - (sel, tx.to(), Some(tx.signer())) - }); + 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); + } + } + } - let ace_interaction = used_state_trace.as_ref().and_then(|trace| { - ace_configs.iter().find_map(|(_, config)| { - classify_ace_interaction(trace, sim_success, config, selector, tx_to, tx_from) - }) - }); + per_contract.into_values().collect() + } else { + Vec::new() + }; match result { Ok(res) => { @@ -909,39 +946,40 @@ pub fn simulate_order_using_fork( order, sim_value, used_state_trace: res.used_state_trace, - ace_interaction, + ace_interactions, }), new_nonces, )) } Err(err) => { // Check if failed order accessed ACE - may need re-simulation with unlock parent - let ace_dependency = if let Some(AceInteraction::NonUnlocking { contract_address }) = - ace_interaction - { - if ace_unlock_contracts.contains(&contract_address) { - // Already had unlock for this contract but still failed - genuine failure - tracing::debug!( - order = ?order.id(), - ?err, - ?contract_address, - "Order failed despite having ACE unlock for this contract - genuine failure" - ); - None + // 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 { - // Need unlock for this contract (might already have others) - tracing::debug!( - order = ?order.id(), - ?err, - ?contract_address, - existing_unlocks = ?ace_unlock_contracts, - "Order needs additional ACE unlock" - ); - Some(contract_address) + None } - } else { - None - }; + }); Ok(OrderSimResult::Failed(SimulationFailure { error: err, @@ -950,3 +988,22 @@ pub fn simulate_order_using_fork( } } } + +/// 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 index 954985664..ea9e6d405 100644 --- a/crates/rbuilder/src/building/testing/ace_tests/mod.rs +++ b/crates/rbuilder/src/building/testing/ace_tests/mod.rs @@ -69,10 +69,10 @@ fn create_force_unlock_order(contract: Address, gas_used: u64) -> Arc Arc eyre::Result<()> { let optional_order = create_optional_unlock_order(contract_addr, 50_000); - let mut result = SimulatedResult::Success { + let result = SimulatedResult::Success { id: rand::random(), simulated_order: optional_order.clone(), previous_orders: Vec::new(), @@ -266,10 +266,10 @@ fn test_handle_ace_unlock_with_mempool_unlock() -> eyre::Result<()> { }; // Handle the ACE unlock - let cancellation = sim_tree.handle_ace_unlock(&mut result)?; + let cancellations = sim_tree.handle_ace_unlock(&result)?; // Unlocking orders should be cancelled since mempool unlock was already marked - assert_eq!(cancellation, Some(optional_order.order.id())); + assert!(cancellations.contains(&optional_order.order.id())); // Optional order should NOT be stored let ace_state = sim_tree.get_ace_state(&contract_addr).unwrap(); @@ -292,7 +292,7 @@ fn test_optional_ace_not_stored_without_pending_orders() -> eyre::Result<()> { // Create optional unlock order but NO pending orders waiting on it let optional_order = create_optional_unlock_order(contract_addr, 50_000); - let mut result = SimulatedResult::Success { + let result = SimulatedResult::Success { id: rand::random(), simulated_order: optional_order.clone(), previous_orders: Vec::new(), @@ -301,10 +301,10 @@ fn test_optional_ace_not_stored_without_pending_orders() -> eyre::Result<()> { }; // Handle the ACE unlock - should be cancelled because no orders need it - let cancellation = sim_tree.handle_ace_unlock(&mut result)?; + let cancellations = sim_tree.handle_ace_unlock(&result)?; // Optional should be cancelled because no orders are waiting - assert_eq!(cancellation, Some(optional_order.order.id())); + assert!(cancellations.contains(&optional_order.order.id())); // Optional order should NOT be stored let ace_state = sim_tree.get_ace_state(&contract_addr).unwrap(); @@ -503,3 +503,127 @@ fn test_multi_ace_config_registration() -> eyre::Result<()> { 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 c0338f2e9..3fcfeb7dd 100644 --- a/crates/rbuilder/src/building/testing/bundle_tests/setup.rs +++ b/crates/rbuilder/src/building/testing/bundle_tests/setup.rs @@ -233,7 +233,7 @@ impl TestSetup { order: self.order_builder.build_order(), sim_value: Default::default(), used_state_trace: Default::default(), - ace_interaction: None, + ace_interactions: Vec::new(), }; // we commit order twice to test evm caching diff --git a/crates/rbuilder/src/live_builder/simulation/sim_worker.rs b/crates/rbuilder/src/live_builder/simulation/sim_worker.rs index dbe609fc5..2f34c0ce6 100644 --- a/crates/rbuilder/src/live_builder/simulation/sim_worker.rs +++ b/crates/rbuilder/src/live_builder/simulation/sim_worker.rs @@ -85,13 +85,15 @@ pub fn run_sim_worker

( }) .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)); + // 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 { From 3cb20c3364f1a4666d5f719dc18e72184dbf90b4 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Tue, 6 Jan 2026 16:36:14 -0400 Subject: [PATCH 39/51] fix: remove comment thats misleading --- .../src/live_builder/simulation/simulation_job.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs index 99811a568..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, @@ -228,11 +228,20 @@ impl SimulationJob { sim_tree_results.push(sim_result); } } - SimulatedResult::Failed { .. } => { + SimulatedResult::Failed { + failure: + SimulationFailure { + ace_dependency: Some(_), + .. + }, + .. + } => { // Failed with ACE dependency - pass to sim_tree for re-queuing with unlock parent - // Note: sim_worker only sends Failed results when ace_dependency.is_some() sim_tree_results.push(sim_result); } + SimulatedResult::Failed { .. } => { + // Permanent failure without ACE dependency - nothing to do + } } } // update simtree From 8c3d0f3ae6e97c6ce23cc568131a0fb868b6a6e3 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Mon, 12 Jan 2026 15:34:42 -0500 Subject: [PATCH 40/51] wip: refactor --- crates/rbuilder-primitives/src/ace.rs | 3 + crates/rbuilder/src/building/sim.rs | 322 +++++++++++++------------- 2 files changed, 159 insertions(+), 166 deletions(-) diff --git a/crates/rbuilder-primitives/src/ace.rs b/crates/rbuilder-primitives/src/ace.rs index d25122763..e2e33c524 100644 --- a/crates/rbuilder-primitives/src/ace.rs +++ b/crates/rbuilder-primitives/src/ace.rs @@ -116,6 +116,9 @@ pub enum AceInteraction { } impl AceInteraction { + pub fn needs_unlock(&self) -> bool { + matches!(self, Self::NonUnlocking { .. }) + } pub fn is_unlocking(&self) -> bool { matches!(self, Self::Unlocking { .. }) } diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index a251097b8..57544bf36 100644 --- a/crates/rbuilder/src/building/sim.rs +++ b/crates/rbuilder/src/building/sim.rs @@ -16,6 +16,8 @@ use crate::{ use ahash::{HashMap, HashSet}; use alloy_primitives::Address; use alloy_rpc_types::TransactionTrait; +use itertools::Itertools; +use quick_cache::Equivalent; use rand::seq::SliceRandom; use rbuilder_primitives::ace::{ classify_ace_interaction, AceInteraction, AceUnlockSource, Selector, @@ -39,7 +41,7 @@ pub struct SimulationFailure { pub error: OrderErr, /// If Some, this order needs an ACE unlock from this contract before it can succeed. /// The order should be queued for re-simulation once the unlock tx is available. - pub ace_dependency: Option

, + pub ace_state: AceSimulationState, } #[derive(Debug)] @@ -94,19 +96,56 @@ impl AceExchangeState { /// Get the best available unlock order. /// Selects the cheapest (lowest gas) for frontrunning when both are available. pub fn get_unlock_order(&self) -> Option<&Arc> { - match (&self.force_unlock_order, &self.optional_unlock_order) { - (Some(force), Some(optional)) => { - // Select cheapest (lowest gas) for frontrunning - if force.sim_value.gas_used() <= optional.sim_value.gas_used() { - Some(force) - } else { - Some(optional) - } - } - (Some(force), None) => Some(force), - (None, Some(optional)) => Some(optional), - (None, None) => None, - } + // Because we only expect one or the other, and force tx will always be at the top of the + // block. we want to ensure we always select the correct order for what we expect in the + // block builder. + self.force_unlock_order + .as_ref() + .or_else(|| self.optional_unlock_order.as_ref()) + } +} + +/// Tracks ACE simulation state for an order through iterative re-simulations. +/// +/// Key concepts: +/// - NonUnlocking interactions = need unlock parents (revert without) +/// - Unlocking interactions = ARE the unlock parents (never need parents themselves) +/// +/// Flow: +/// 1) Simulate order + collect possible ACE interactions +/// 2) If NonUnlocking interactions exist, wait for unlock parents +/// 3) Re-simulate with ACE context, compute symmetric difference of +/// new NonUnlocking dependencies vs already-accounted-for interactions +/// 4) If unhandled set is empty -> done, else -> repeat from 3 +#[derive(Debug, Clone, Default, PartialEq, Eq)] +struct AceSimulationState { + /// ACE interactions detected + /// Includes both Unlocking (parent providers) and NonUnlocking (need parents). + detected_interactions: HashSet, + + /// ACE interactions (by contract address) for which we've already provided + /// unlock parents in previous simulation attempts. + accounted_for_interactions: HashSet, +} + +impl AceSimulationState { + pub fn dependencies_to_handle(&self) -> HashSet { + todo!() + // self.detected_interactions + // .iter() + // .filter(|interaction| interaction.needs_unlock()) + // .copied() + // .collect::>() + // .symmetric_difference(&self.accounted_for_interactions) + // .collect() + } + + pub fn all_dependencies_accounted(&self) -> bool { + todo!() + } + + pub fn add_accounted_interactions(&mut self, actions: impl Iterator) { + self.accounted_for_interactions.extend(actions) } } @@ -115,7 +154,7 @@ struct PendingOrder { order: Order, unsatisfied_dependencies: usize, /// ACE contracts already provided as unlock parents (for progressive multi-ACE discovery) - ace_unlock_contracts: HashSet
, + ace_state: AceSimulationState, } pub type SimulationId = u64; @@ -128,7 +167,7 @@ pub struct SimulationRequest { /// ACE contracts for which we've already provided unlock parents. /// Used to determine if a failure is genuine (contract already unlocked) or needs retry. /// Supports multiple ACE contracts - order can progressively discover needed unlocks. - pub ace_unlock_contracts: HashSet
, + pub ace_state: AceSimulationState, } #[derive(Debug)] @@ -148,16 +187,22 @@ pub enum SimulatedResult { id: SimulationId, order: Order, failure: SimulationFailure, - /// ACE contracts that were already provided as unlock parents (preserved for re-queuing) - ace_unlock_contracts: HashSet
, simulation_time: Duration, }, } +impl SimulatedResult { + pub fn is_success(&self) -> bool { + matches!(self, Self::Success { .. }) + } +} + /// Minimal data stored for completed simulations (to avoid Clone on full SimulatedResult) #[derive(Debug, Clone)] struct StoredSimulation { - previous_orders: Vec, + // parents + parent_orders: Vec, + // result simulated_order: Arc, } @@ -252,7 +297,7 @@ impl SimTree { PendingOrder { order, unsatisfied_dependencies, - ace_unlock_contracts: HashSet::default(), + ace_state: AceSimulationState::default(), }, ); } @@ -261,7 +306,8 @@ impl SimTree { id: rand::random(), order, parents, - ace_unlock_contracts: HashSet::default(), + // we don't have a state for it yet. + ace_state: AceSimulationState::default(), }); } } @@ -322,7 +368,7 @@ impl SimTree { pending_deps.push(dep_key); continue; }; - parent_orders.extend_from_slice(&sim.previous_orders); + parent_orders.extend_from_slice(&sim.parent_orders); parent_orders.push(sim.simulated_order.order.clone()); continue; } @@ -339,68 +385,74 @@ impl SimTree { } } - /// Check if an order needs ACE unlock and add that dependency. - /// Called after simulation when we detect a NonUnlocking ACE interaction. - /// Supports progressive multi-ACE discovery - existing_ace_unlock_contracts contains - /// contracts we've already provided unlocks for in previous sim attempts. - pub fn add_ace_dependency_for_order( + /// Takes a failed ace dependency order and adds parents / puts into holding for parents to + /// arrive. + pub fn handle_ace_dependencies_for_order( &mut self, order: Order, - new_contract: Address, - mut existing_ace_unlock_contracts: HashSet
, - ) -> Result<(), ProviderError> { - // Add new contract to the set - existing_ace_unlock_contracts.insert(new_contract); - let dep_key = DependencyKey::AceUnlock(new_contract); - - // Check if we already have an unlock provider for the new contract - if self.dependency_providers.contains_key(&dep_key) { - // Build parents from ALL ACE unlock contracts we need - let mut parents = Vec::new(); - for contract in &existing_ace_unlock_contracts { - let key = DependencyKey::AceUnlock(*contract); - if let Some(sim_id) = self.dependency_providers.get(&key) { - if let Some(sim) = self.sims.get(sim_id) { - parents.extend(sim.previous_orders.clone()); - parents.push(sim.simulated_order.order.clone()); + mut ace_state: AceSimulationState, + ) { + let difference = ace_state.dependencies_to_handle(); + // If we have handled all dependencies, this order will not be valid and thus we will + // ignore it. + if difference.is_empty() { + return; + } + + let mut is_ready = true; + + let keys = difference + .into_iter() + .filter_map(|dep| { + let dep_key = DependencyKey::AceUnlock(dep.get_contract_address()); + + match self.dependency_providers.get(&dep_key) { + Some(key) => Some(key), + + None => { + is_ready |= false; + self.pending_dependencies + .entry(dep_key) + .or_default() + .push(order.id()); + None } } - } + }) + .collect_vec(); - // Order is ready with all unlock txs as parents - self.ready_orders.push(SimulationRequest { - id: rand::random(), - order, - parents, - ace_unlock_contracts: existing_ace_unlock_contracts, - }); - return Ok(()); + // this is fine as we add the order to pending_dependencies + if !is_ready { + return; } - // New unlock not yet available - add to pending - self.add_order_to_pending_with_ace(order, dep_key, existing_ace_unlock_contracts) - } + let parents = keys + .into_iter() + .filter_map(|sim_id| { + // for each parent, we want to track it now. + if let Some(sim) = self.sims.get(sim_id) { + ace_state.add_accounted_interactions( + sim.simulated_order.ace_interactions.iter().copied(), + ); - /// Helper to add an order to pending state with ACE unlock tracking - fn add_order_to_pending_with_ace( - &mut self, - order: Order, - dep_key: DependencyKey, - ace_unlock_contracts: HashSet
, - ) -> Result<(), ProviderError> { - self.pending_dependencies - .entry(dep_key) - .or_default() - .push(order.id()); - self.pending_orders.insert( - order.id(), - PendingOrder { - order, - unsatisfied_dependencies: 1, - ace_unlock_contracts, - }, - ); - Ok(()) + let mut parents = sim.parent_orders.clone(); + parents.push(sim.simulated_order.order.clone()); + + Some(parents) + } else { + None + } + }) + .flatten() + .collect::>(); + + // Order is ready with all unlock txs as parents + self.ready_orders.push(SimulationRequest { + id: rand::random(), + order, + parents, + ace_state, + }); } pub fn push_orders(&mut self, orders: Vec) -> Result<(), ProviderError> { @@ -420,11 +472,13 @@ impl SimTree { &mut self, result: &SimulatedResult, ) -> Result<(), ProviderError> { + if result.is_success() { + return Ok(()); + } let SimulatedResult::Success { - id, - simulated_order, previous_orders, - dependencies_satisfied, + simulated_order, + id, .. } = result else { @@ -435,7 +489,7 @@ impl SimTree { self.sims.insert( *id, StoredSimulation { - previous_orders: previous_orders.clone(), + parent_orders: previous_orders.clone(), simulated_order: simulated_order.clone(), }, ); @@ -656,34 +710,11 @@ impl SimTree { let mut successful_results = Vec::with_capacity(results.len()); for result in results { - 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 - } + if let SimulatedResult::Success { .. } = result { + 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); } } @@ -752,29 +783,16 @@ where &mut local_ctx, &mut block_state, sim_tree.ace_configs(), - &sim_task.ace_unlock_contracts, + &sim_task.ace_state, )?; let (_, provider) = block_state.into_parts(); state_for_sim = provider; match sim_result.result { 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); - } + // if we have a failure, we will handle the case were its ace by either putting + // it into pending or requeing if we have the deps. Otherwise, no action is + // taken and flow handles as normal. + sim_tree.handle_ace_dependencies_for_order(sim_task.order, failure.ace_state); } OrderSimResult::Success(sim_order, nonces) => { let mut dependencies_satisfied: Vec = nonces @@ -826,7 +844,8 @@ pub fn simulate_order( local_ctx: &mut ThreadBlockBuildingContext, state: &mut BlockState, ace_configs: &HashMap, - ace_unlock_contracts: &HashSet
, + // we have parents for these ace addresses. + current_ace_state: &AceSimulationState, ) -> Result { let mut tracer = AccumulatorSimulationTracer::new(); let mut fork = PartialBlockFork::new(state, ctx, local_ctx).with_tracer(&mut tracer); @@ -837,7 +856,7 @@ pub fn simulate_order( &mut fork, &ctx.mempool_tx_detector, ace_configs, - ace_unlock_contracts, + current_ace_state, ); fork.rollback(rollback_point); let sim_res = sim_res?; @@ -855,7 +874,7 @@ pub fn simulate_order_using_fork( fork: &mut PartialBlockFork<'_, '_, '_, '_, Tracer, NullPartialBlockForkExecutionTracer>, mempool_tx_detector: &MempoolTxsDetector, ace_configs: &HashMap, - ace_unlock_contracts: &HashSet
, + current_ace_state: &AceSimulationState, ) -> Result { let start = Instant::now(); @@ -874,7 +893,9 @@ pub fn simulate_order_using_fork( tracing::trace!(parent_order = ?parent.id(), ?err, "failed to simulate parent order"); return Ok(OrderSimResult::Failed(SimulationFailure { error: err, - ace_dependency: None, + // Given a parent failure. We will return a empty ace simulation state as this + // signals to treat it as a regular order. + ace_state: AceSimulationState::default(), })); } } @@ -937,7 +958,7 @@ pub fn simulate_order_using_fork( if let Err(err) = order_is_worth_executing(&sim_value) { return Ok(OrderSimResult::Failed(SimulationFailure { error: err, - ace_dependency: None, + ace_dependency: ace_interactions, })); } let new_nonces = res.nonces_updated.into_iter().collect::>(); @@ -951,41 +972,10 @@ pub fn simulate_order_using_fork( new_nonces, )) } - 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, - })) - } + Err(err) => Ok(OrderSimResult::Failed(SimulationFailure { + error: err, + ace_dependency: ace_interactions, + })), } } From 30f45266fedbd8f6b48f7bf900830284d6292cfa Mon Sep 17 00:00:00 2001 From: Will Smith Date: Mon, 12 Jan 2026 16:27:09 -0500 Subject: [PATCH 41/51] cleanup --- crates/rbuilder/src/building/sim.rs | 144 +++++++------ .../src/building/testing/ace_tests/mod.rs | 192 +++++++++++------- .../src/live_builder/simulation/sim_worker.rs | 5 +- .../live_builder/simulation/simulation_job.rs | 15 +- 4 files changed, 210 insertions(+), 146 deletions(-) diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index 57544bf36..6d25dcb07 100644 --- a/crates/rbuilder/src/building/sim.rs +++ b/crates/rbuilder/src/building/sim.rs @@ -17,7 +17,6 @@ use ahash::{HashMap, HashSet}; use alloy_primitives::Address; use alloy_rpc_types::TransactionTrait; use itertools::Itertools; -use quick_cache::Equivalent; use rand::seq::SliceRandom; use rbuilder_primitives::ace::{ classify_ace_interaction, AceInteraction, AceUnlockSource, Selector, @@ -118,30 +117,40 @@ impl AceExchangeState { /// new NonUnlocking dependencies vs already-accounted-for interactions /// 4) If unhandled set is empty -> done, else -> repeat from 3 #[derive(Debug, Clone, Default, PartialEq, Eq)] -struct AceSimulationState { +pub struct AceSimulationState { /// ACE interactions detected /// Includes both Unlocking (parent providers) and NonUnlocking (need parents). - detected_interactions: HashSet, + pub detected_interactions: HashSet, /// ACE interactions (by contract address) for which we've already provided /// unlock parents in previous simulation attempts. - accounted_for_interactions: HashSet, + pub accounted_for_interactions: HashSet, } impl AceSimulationState { + /// Returns NonUnlocking interactions that still need unlock parents. + /// Filters out interactions where we already have an Unlocking interaction + /// for the same contract in accounted_for_interactions. pub fn dependencies_to_handle(&self) -> HashSet { - todo!() - // self.detected_interactions - // .iter() - // .filter(|interaction| interaction.needs_unlock()) - // .copied() - // .collect::>() - // .symmetric_difference(&self.accounted_for_interactions) - // .collect() + // Get contract addresses for which we have unlocks accounted for + let accounted_contracts: HashSet
= self + .accounted_for_interactions + .iter() + .filter(|i| i.is_unlocking()) + .map(|i| i.get_contract_address()) + .collect(); + + // Return NonUnlocking interactions whose contracts aren't yet accounted for + self.detected_interactions + .iter() + .filter(|i| i.needs_unlock()) + .filter(|i| !accounted_contracts.contains(&i.get_contract_address())) + .copied() + .collect() } pub fn all_dependencies_accounted(&self) -> bool { - todo!() + self.dependencies_to_handle().is_empty() } pub fn add_accounted_interactions(&mut self, actions: impl Iterator) { @@ -152,8 +161,7 @@ impl AceSimulationState { #[derive(Debug, Clone, PartialEq, Eq)] struct PendingOrder { order: Order, - unsatisfied_dependencies: usize, - /// ACE contracts already provided as unlock parents (for progressive multi-ACE discovery) + /// ACE state tracking detected and accounted-for interactions ace_state: AceSimulationState, } @@ -285,7 +293,6 @@ impl SimTree { } OrderDependencyState::Pending(pending_deps) => { mark_order_pending_nonce(order_id); - let unsatisfied_dependencies = pending_deps.len(); for dep in pending_deps { self.pending_dependencies .entry(dep) @@ -296,7 +303,6 @@ impl SimTree { order.id(), PendingOrder { order, - unsatisfied_dependencies, ace_state: AceSimulationState::default(), }, ); @@ -393,8 +399,8 @@ impl SimTree { mut ace_state: AceSimulationState, ) { let difference = ace_state.dependencies_to_handle(); - // If we have handled all dependencies, this order will not be valid and thus we will - // ignore it. + // If we have handled all dependencies, this order will not be valid and thus we will + // ignore it. if difference.is_empty() { return; } @@ -410,7 +416,7 @@ impl SimTree { Some(key) => Some(key), None => { - is_ready |= false; + is_ready = false; self.pending_dependencies .entry(dep_key) .or_default() @@ -421,8 +427,10 @@ impl SimTree { }) .collect_vec(); - // this is fine as we add the order to pending_dependencies + // Order needs to wait for ACE unlock dependencies if !is_ready { + self.pending_orders + .insert(order.id(), PendingOrder { order, ace_state }); return; } @@ -472,12 +480,10 @@ impl SimTree { &mut self, result: &SimulatedResult, ) -> Result<(), ProviderError> { - if result.is_success() { - return Ok(()); - } let SimulatedResult::Success { previous_orders, simulated_order, + dependencies_satisfied, id, .. } = result @@ -493,17 +499,12 @@ impl SimTree { simulated_order: simulated_order.clone(), }, ); - // Track orders that become ready along with their ACE state + + // Track orders that become ready (all deps satisfied) let mut orders_ready: Vec = Vec::new(); - let mut ace_unlock_contract: Option
= None; // Process each dependency this simulation satisfies for dep_key in dependencies_satisfied.iter().cloned() { - // Track if this dependency is an ACE unlock and which contract - if let DependencyKey::AceUnlock(contract) = dep_key { - ace_unlock_contract = Some(contract); - } - match self.dependency_providers.entry(dep_key.clone()) { Entry::Occupied(mut entry) => { // Already have a provider - check if this one is more profitable @@ -532,21 +533,25 @@ impl SimTree { // First provider for this dependency entry.insert(*id); - // Unblock orders waiting on this dependency + // Update orders waiting on this dependency if let Some(pending_order_ids) = self.pending_dependencies.remove(&dep_key) { for order_id in pending_order_ids { - match self.pending_orders.entry(order_id) { - Entry::Occupied(mut entry) => { - let pending_order = entry.get_mut(); - pending_order.unsatisfied_dependencies -= 1; - if pending_order.unsatisfied_dependencies == 0 { - orders_ready.push(entry.remove()); - } + if let Entry::Occupied(mut entry) = self.pending_orders.entry(order_id) + { + let pending_order = entry.get_mut(); + + // Add the unlock interactions to the order's accounted_for set + if matches!(dep_key, DependencyKey::AceUnlock(_)) { + pending_order.ace_state.add_accounted_interactions( + simulated_order.ace_interactions.iter().copied(), + ); } - Entry::Vacant(_) => { - error!("SimTree bug order not found"); - // @Metric bug counter + + // Check if all ACE deps are now accounted for + if pending_order.ace_state.all_dependencies_accounted() { + orders_ready.push(entry.remove()); } + // Otherwise order stays pending, waiting for more deps } } } @@ -554,30 +559,38 @@ impl SimTree { } } - for mut ready_pending_order in orders_ready { + // Process orders that are now fully ready + for ready_pending_order in orders_ready { let pending_state = self.get_order_dependency_state(&ready_pending_order.order)?; match pending_state { OrderDependencyState::Ready(mut parents) => { - // If this order became ready due to ACE unlock, add the unlock tx as parent - // and track the contract in ace_unlock_contracts - if let Some(contract) = ace_unlock_contract { - ready_pending_order.ace_unlock_contracts.insert(contract); - parents.extend(previous_orders.iter().cloned()); - parents.push(simulated_order.order.clone()); + let ace_state = ready_pending_order.ace_state; + + // Collect ALL ACE parent orders from dependency_providers + for interaction in ace_state.detected_interactions.iter() { + if interaction.needs_unlock() { + let dep_key = + DependencyKey::AceUnlock(interaction.get_contract_address()); + if let Some(sim_id) = self.dependency_providers.get(&dep_key) { + if let Some(sim) = self.sims.get(sim_id) { + parents.extend(sim.parent_orders.iter().cloned()); + parents.push(sim.simulated_order.order.clone()); + } + } + } } + self.ready_orders.push(SimulationRequest { id: rand::random(), order: ready_pending_order.order, parents, - ace_unlock_contracts: ready_pending_order.ace_unlock_contracts, + ace_state, }); } OrderDependencyState::Invalid => { - // @Metric bug counter error!("SimTree bug order became invalid"); } OrderDependencyState::Pending(_) => { - // @Metric bug counter error!("SimTree bug order became pending again"); } } @@ -751,7 +764,7 @@ where sim_tree.push_orders(std::mem::take(&mut orders))?; } - let mut sim_errors = Vec::new(); + let sim_errors = Vec::new(); let mut state_for_sim = Arc::::from(provider.history_by_block_hash(ctx.attributes.parent)?); let mut local_ctx = ThreadBlockBuildingContext::default(); @@ -956,9 +969,10 @@ pub fn simulate_order_using_fork( Ok(res) => { let sim_value = create_sim_value(&order, &res, mempool_tx_detector); if let Err(err) = order_is_worth_executing(&sim_value) { + // Not an ACE-related failure, use default state return Ok(OrderSimResult::Failed(SimulationFailure { error: err, - ace_dependency: ace_interactions, + ace_state: AceSimulationState::default(), })); } let new_nonces = res.nonces_updated.into_iter().collect::>(); @@ -972,10 +986,24 @@ pub fn simulate_order_using_fork( new_nonces, )) } - Err(err) => Ok(OrderSimResult::Failed(SimulationFailure { - error: err, - ace_dependency: ace_interactions, - })), + Err(err) => { + // Build ACE state with only NonUnlocking interactions (they need unlock parents) + let non_unlocking: HashSet = ace_interactions + .iter() + .filter(|i| i.needs_unlock()) + .copied() + .collect(); + + Ok(OrderSimResult::Failed(SimulationFailure { + error: err, + ace_state: AceSimulationState { + detected_interactions: non_unlocking, + accounted_for_interactions: current_ace_state + .accounted_for_interactions + .clone(), + }, + })) + } } } diff --git a/crates/rbuilder/src/building/testing/ace_tests/mod.rs b/crates/rbuilder/src/building/testing/ace_tests/mod.rs index ea9e6d405..57daebbf8 100644 --- a/crates/rbuilder/src/building/testing/ace_tests/mod.rs +++ b/crates/rbuilder/src/building/testing/ace_tests/mod.rs @@ -1,5 +1,8 @@ use super::test_chain_state::{BlockArgs, TestChainState}; -use crate::building::sim::{AceExchangeState, DependencyKey, NonceKey, SimTree, SimulatedResult}; +use crate::building::sim::{ + AceExchangeState, AceSimulationState, DependencyKey, NonceKey, SimTree, SimulatedResult, + SimulationRequest, +}; use crate::utils::NonceCache; use alloy_primitives::{address, b256, Address, B256, U256}; use rbuilder_primitives::ace::{AceConfig, AceInteraction, AceUnlockSource, Selector}; @@ -124,20 +127,20 @@ fn test_ace_exchange_state_get_unlock_order_optional_only() { } #[test] -fn test_cheapest_unlock_selected() { +fn test_force_unlock_always_preferred() { 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 force_order = create_force_unlock_order(contract, 100_000); + let optional_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()), + force_unlock_order: Some(force_order.clone()), + optional_unlock_order: Some(optional_order.clone()), ..Default::default() }; - // Should select the cheaper one (50k < 100k) + // Force is always preferred over optional, regardless of gas cost let result = state.get_unlock_order(); - assert_eq!(result.unwrap().sim_value.gas_used(), 50_000); + assert_eq!(result, Some(&force_order)); } #[test] @@ -336,111 +339,131 @@ fn test_dependency_key_ace_unlock() { } // ============================================================================ -// Multi-ACE Unlock Tests +// AceSimulationState 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 +fn test_simulation_request_ace_state_empty_default() { + // New orders should start with empty ace_state let request = SimulationRequest { id: rand::random(), order: create_test_order(), parents: Vec::new(), - ace_unlock_contracts: AHashSet::default(), + ace_state: AceSimulationState::default(), }; - assert!(request.ace_unlock_contracts.is_empty()); + assert!(request.ace_state.all_dependencies_accounted()); } #[test] -fn test_simulation_request_ace_unlock_contracts_single() { +fn test_ace_simulation_state_single_dependency() { 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(), + // Order detected a NonUnlocking interaction (needs unlock) + let mut detected = AHashSet::default(); + detected.insert(AceInteraction::NonUnlocking { + contract_address: contract_a, + }); + + let ace_state = AceSimulationState { + detected_interactions: detected, + accounted_for_interactions: AHashSet::default(), }; - assert!(request.ace_unlock_contracts.contains(&contract_a)); - assert_eq!(request.ace_unlock_contracts.len(), 1); + // Should have unhandled dependencies + assert!(!ace_state.all_dependencies_accounted()); + + let deps = ace_state.dependencies_to_handle(); + assert_eq!(deps.len(), 1); } #[test] -fn test_simulation_request_ace_unlock_contracts_multiple() { - // Test that SimulationRequest can track multiple ACE contracts +fn test_ace_simulation_state_multiple_dependencies() { 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(), + let mut detected = AHashSet::default(); + detected.insert(AceInteraction::NonUnlocking { + contract_address: contract_a, + }); + detected.insert(AceInteraction::NonUnlocking { + contract_address: contract_b, + }); + detected.insert(AceInteraction::NonUnlocking { + contract_address: contract_c, + }); + + let ace_state = AceSimulationState { + detected_interactions: detected, + accounted_for_interactions: AHashSet::default(), }; - 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); + let deps = ace_state.dependencies_to_handle(); + assert_eq!(deps.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) +fn test_ace_simulation_state_partial_unlock() { + // Test that partial unlocks are tracked correctly let contract_a = address!("0000000aa232009084Bd71A5797d089AA4Edfad4"); + let contract_b = address!("1111111aa232009084Bd71A5797d089AA4Edfad4"); - let mut ace_contracts = AHashSet::default(); - ace_contracts.insert(contract_a); + let mut detected = AHashSet::default(); + detected.insert(AceInteraction::NonUnlocking { + contract_address: contract_a, + }); + detected.insert(AceInteraction::NonUnlocking { + contract_address: contract_b, + }); + + // We've accounted for unlock on contract_a + let mut accounted = AHashSet::default(); + accounted.insert(AceInteraction::Unlocking { + contract_address: contract_a, + source: AceUnlockSource::User, + }); - // If the order already had unlock for contract_a but still failed, - // checking contains() should return true - assert!(ace_contracts.contains(&contract_a)); + let ace_state = AceSimulationState { + detected_interactions: detected, + accounted_for_interactions: accounted, + }; - // A different contract should NOT be in the set - let contract_b = address!("1111111aa232009084Bd71A5797d089AA4Edfad4"); - assert!(!ace_contracts.contains(&contract_b)); + // Should still have one unhandled dependency (contract_b) + assert!(!ace_state.all_dependencies_accounted()); + let deps = ace_state.dependencies_to_handle(); + assert_eq!(deps.len(), 1); } #[test] -fn test_ace_unlock_contracts_progressive_accumulation() { - // Simulate progressive discovery: start empty, add contracts one by one - let mut ace_contracts = AHashSet::default(); - +fn test_ace_simulation_state_all_accounted() { + // When all unlocks are accounted for, should be fully resolved 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); + let mut detected = AHashSet::default(); + detected.insert(AceInteraction::NonUnlocking { + contract_address: contract_a, + }); - // 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); + let mut accounted = AHashSet::default(); + accounted.insert(AceInteraction::Unlocking { + contract_address: contract_a, + source: AceUnlockSource::User, + }); + + let ace_state = AceSimulationState { + detected_interactions: detected, + accounted_for_interactions: accounted, + }; - // Third failure with contract A - should be genuine failure (already in set) - assert!(ace_contracts.contains(&contract_a)); + assert!(ace_state.all_dependencies_accounted()); + assert!(ace_state.dependencies_to_handle().is_empty()); } #[test] -fn test_add_ace_dependency_preserves_existing_contracts() -> eyre::Result<()> { +fn test_handle_ace_dependencies_with_existing_accounted() -> eyre::Result<()> { let test_chain = TestChainState::new(BlockArgs::default())?; let state = test_chain.provider_factory().latest()?; let nonce_cache = NonceCache::new(state.into()); @@ -456,16 +479,33 @@ fn test_add_ace_dependency_preserves_existing_contracts() -> eyre::Result<()> { 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 + // Create an order with ACE state: detected both contracts, accounted for contract_a 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)?; + let mut detected = AHashSet::default(); + detected.insert(AceInteraction::NonUnlocking { + contract_address: contract_a, + }); + detected.insert(AceInteraction::NonUnlocking { + contract_address: contract_b, + }); + + let mut accounted = AHashSet::default(); + accounted.insert(AceInteraction::Unlocking { + contract_address: contract_a, + source: AceUnlockSource::User, + }); + + let ace_state = AceSimulationState { + detected_interactions: detected, + accounted_for_interactions: accounted, + }; + + // Handle ACE dependencies - should add order to pending for contract_b + sim_tree.handle_ace_dependencies_for_order(order, ace_state); - // The order should now be pending for contract_b - // (We can't easily check internal state, but the function should succeed) + // The order should now be pending for contract_b unlock + // (Function should succeed without error) Ok(()) } diff --git a/crates/rbuilder/src/live_builder/simulation/sim_worker.rs b/crates/rbuilder/src/live_builder/simulation/sim_worker.rs index 2f34c0ce6..246cee07e 100644 --- a/crates/rbuilder/src/live_builder/simulation/sim_worker.rs +++ b/crates/rbuilder/src/live_builder/simulation/sim_worker.rs @@ -72,7 +72,7 @@ pub fn run_sim_worker

( &mut local_ctx, &mut block_state, ¤t_sim_context.ace_configs, - &task.ace_unlock_contracts, + &task.ace_state, ); let sim_ok = match sim_result { Ok(sim_result) => { @@ -113,12 +113,11 @@ pub fn run_sim_worker

( } OrderSimResult::Failed(failure) => { // Only send to SimTree if there's an ACE dependency to handle - if failure.ace_dependency.is_some() { + if !failure.ace_state.all_dependencies_accounted() { 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() { diff --git a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs index dea5a8bb0..d67e38ded 100644 --- a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs +++ b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs @@ -229,17 +229,14 @@ impl SimulationJob { } } SimulatedResult::Failed { - failure: - SimulationFailure { - ace_dependency: Some(_), - .. - }, + failure: SimulationFailure { ref ace_state, .. }, .. } => { - // Failed with ACE dependency - pass to sim_tree for re-queuing with unlock parent - sim_tree_results.push(sim_result); - } - SimulatedResult::Failed { .. } => { + // Check if there are unhandled ACE dependencies + if !ace_state.all_dependencies_accounted() { + // Failed with ACE dependency - pass to sim_tree for re-queuing with unlock parent + sim_tree_results.push(sim_result); + } // Permanent failure without ACE dependency - nothing to do } } From d01eb8e1382e8dfc610bb1a03ffe3c78c2c75642 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Fri, 16 Jan 2026 14:13:26 -0500 Subject: [PATCH 42/51] feat: update config examples --- .../build_block/backtest_build_block.rs | 9 ++++++++- crates/rbuilder/src/backtest/execute.rs | 12 ++++++++++-- .../rbuilder/src/bin/run-bundle-on-prefix.rs | 16 ++++++++++++---- .../rbuilder/config-backtest-example.toml | 18 +++++++++++++++++- .../config/rbuilder/config-playground.toml | 16 ++++++++++++++++ 5 files changed, 63 insertions(+), 8 deletions(-) diff --git a/crates/rbuilder/src/backtest/build_block/backtest_build_block.rs b/crates/rbuilder/src/backtest/build_block/backtest_build_block.rs index 228435c22..20ec9abbf 100644 --- a/crates/rbuilder/src/backtest/build_block/backtest_build_block.rs +++ b/crates/rbuilder/src/backtest/build_block/backtest_build_block.rs @@ -17,7 +17,7 @@ use crate::{ builders::BacktestSimulateBlockInput, BlockBuildingContext, ExecutionResult, NullPartialBlockExecutionTracer, }, - live_builder::cli::LiveBuilderConfig, + live_builder::{cli::LiveBuilderConfig, config::AceConfig}, provider::StateProviderFactory, }; use clap::Parser; @@ -102,10 +102,17 @@ where let provider_factory = orders_source.create_provider_factory()?; orders_source.print_custom_stats(provider_factory.clone())?; + let base_config = config.base_config(); + let ace_configs: Vec = if base_config.ace_enabled { + base_config.ace_protocols.clone() + } else { + Vec::new() + }; let BacktestBlockInput { sim_orders, .. } = backtest_prepare_orders_from_building_context( ctx.clone(), available_orders.clone(), provider_factory.clone(), + ace_configs, )?; if let Some(tx_hash) = build_block_cfg.show_tx_extra_data { diff --git a/crates/rbuilder/src/backtest/execute.rs b/crates/rbuilder/src/backtest/execute.rs index 9e0b4fd68..2f638eb02 100644 --- a/crates/rbuilder/src/backtest/execute.rs +++ b/crates/rbuilder/src/backtest/execute.rs @@ -4,7 +4,7 @@ use crate::{ builders::BacktestSimulateBlockInput, sim::simulate_all_orders_with_sim_tree, BlockBuildingContext, BundleErr, NullPartialBlockExecutionTracer, OrderErr, TransactionErr, }, - live_builder::{block_list_provider::BlockList, cli::LiveBuilderConfig}, + live_builder::{block_list_provider::BlockList, cli::LiveBuilderConfig, config::AceConfig}, provider::StateProviderFactory, utils::{clean_extradata, mevblocker::get_mevblocker_price, Signer}, }; @@ -91,6 +91,7 @@ pub fn backtest_prepare_orders_from_building_context

( ctx: BlockBuildingContext, available_orders: Vec, provider: P, + ace_configs: Vec, ) -> eyre::Result where P: StateProviderFactory + Clone + 'static, @@ -104,7 +105,7 @@ where } let (sim_orders, sim_errors) = - simulate_all_orders_with_sim_tree(provider, &ctx, &orders, false, vec![])?; + simulate_all_orders_with_sim_tree(provider, &ctx, &orders, false, ace_configs)?; Ok(BacktestBlockInput { sim_orders, sim_errors, @@ -147,6 +148,12 @@ where P: StateProviderFactory + Clone + 'static, ConfigType: LiveBuilderConfig, { + let base_config = config.base_config(); + let ace_configs = if base_config.ace_enabled { + base_config.ace_protocols.clone() + } else { + Vec::new() + }; let BacktestBlockInput { sim_orders, sim_errors, @@ -154,6 +161,7 @@ where ctx.clone(), block_data.available_orders, provider.clone(), + ace_configs, )?; let filtered_orders_blocklist_count = sim_errors diff --git a/crates/rbuilder/src/bin/run-bundle-on-prefix.rs b/crates/rbuilder/src/bin/run-bundle-on-prefix.rs index d4fd558f6..399e7959b 100644 --- a/crates/rbuilder/src/bin/run-bundle-on-prefix.rs +++ b/crates/rbuilder/src/bin/run-bundle-on-prefix.rs @@ -19,7 +19,10 @@ use rbuilder::{ BlockBuildingContext, ExecutionError, MockRootHasher, NullPartialBlockExecutionTracer, OrderPriority, ThreadBlockBuildingContext, }, - live_builder::{cli::LiveBuilderConfig, config::Config}, + live_builder::{ + cli::LiveBuilderConfig, + config::{AceConfig, Config}, + }, provider::StateProviderFactory, utils::{extract_onchain_block_txs, find_suggested_fee_recipient}, }; @@ -109,12 +112,17 @@ impl LandedBlockInfo { orders: Vec, use_original_coinbase: bool, ) -> eyre::Result>> { + let base_config = self.config.base_config(); + let ace_configs: Vec = if base_config.ace_enabled { + base_config.ace_protocols.clone() + } else { + Vec::new() + }; let BacktestBlockInput { sim_orders, .. } = backtest_prepare_orders_from_building_context( self.get_context(use_original_coinbase), orders, - self.config - .base_config() - .create_reth_provider_factory(true)?, + base_config.create_reth_provider_factory(true)?, + ace_configs, )?; Ok(sim_orders) } diff --git a/examples/config/rbuilder/config-backtest-example.toml b/examples/config/rbuilder/config-backtest-example.toml index 9cb531735..f67589471 100644 --- a/examples/config/rbuilder/config-backtest-example.toml +++ b/examples/config/rbuilder/config-backtest-example.toml @@ -31,4 +31,20 @@ name = "parallel" algo = "parallel-builder" discard_txs = true num_threads = 25 -safe_sorting_only = false \ No newline at end of file +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"] \ No newline at end of file diff --git a/examples/config/rbuilder/config-playground.toml b/examples/config/rbuilder/config-playground.toml index 9520e3f31..10c963177 100644 --- a/examples/config/rbuilder/config-playground.toml +++ b/examples/config/rbuilder/config-playground.toml @@ -10,3 +10,19 @@ coinbase_secret_key = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf root_hash_use_sparse_trie=true root_hash_compare_sparse_trie=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"] From 520b31f2ee84058ace1b652ce2d2f144b49b37ca Mon Sep 17 00:00:00 2001 From: Will Smith Date: Fri, 16 Jan 2026 14:32:30 -0500 Subject: [PATCH 43/51] add logging --- crates/rbuilder/src/backtest/execute.rs | 39 +++++++++++++ crates/rbuilder/src/building/sim.rs | 73 ++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/crates/rbuilder/src/backtest/execute.rs b/crates/rbuilder/src/backtest/execute.rs index 2f638eb02..4621722a8 100644 --- a/crates/rbuilder/src/backtest/execute.rs +++ b/crates/rbuilder/src/backtest/execute.rs @@ -96,6 +96,27 @@ pub fn backtest_prepare_orders_from_building_context

( where P: StateProviderFactory + Clone + 'static, { + // Log ACE config status for debugging + if ace_configs.is_empty() { + tracing::debug!("ACE backtest: no ACE configs provided, ACE detection disabled"); + } else { + tracing::debug!( + ace_config_count = ace_configs.len(), + "ACE backtest: starting simulation with ACE configs" + ); + for config in &ace_configs { + tracing::debug!( + contract_address = ?config.contract_address, + from_addresses_count = config.from_addresses.len(), + to_addresses_count = config.to_addresses.len(), + detection_slots_count = config.detection_slots.len(), + unlock_signatures_count = config.unlock_signatures.len(), + force_signatures_count = config.force_signatures.len(), + "ACE backtest: loaded protocol config" + ); + } + } + let orders = available_orders .iter() .map(|order| order.order.clone()) @@ -104,8 +125,26 @@ where ctx.mempool_tx_detector.add_tx(order); } + tracing::debug!( + order_count = orders.len(), + "ACE backtest: simulating orders" + ); + let (sim_orders, sim_errors) = simulate_all_orders_with_sim_tree(provider, &ctx, &orders, false, ace_configs)?; + + // Log simulation results + let ace_orders_count = sim_orders + .iter() + .filter(|o| !o.ace_interactions.is_empty()) + .count(); + tracing::debug!( + simulated_orders = sim_orders.len(), + sim_errors = sim_errors.len(), + ace_interacting_orders = ace_orders_count, + "ACE backtest: simulation complete" + ); + Ok(BacktestBlockInput { sim_orders, sim_errors, diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index 6d25dcb07..26f82eea1 100644 --- a/crates/rbuilder/src/building/sim.rs +++ b/crates/rbuilder/src/building/sim.rs @@ -250,8 +250,22 @@ impl SimTree { let mut ace_config = HashMap::default(); let mut ace_state = HashMap::default(); + if ace_configs.is_empty() { + tracing::debug!("ACE SimTree: initialized with no ACE configs"); + } else { + tracing::debug!( + ace_config_count = ace_configs.len(), + "ACE SimTree: initializing with ACE configs" + ); + } + for config in ace_configs { let contract_address = config.contract_address; + tracing::debug!( + contract_address = ?contract_address, + detection_slots = ?config.detection_slots, + "ACE SimTree: registered protocol" + ); ace_config.insert(contract_address, config); ace_state.insert(contract_address, AceExchangeState::default()); } @@ -398,13 +412,25 @@ impl SimTree { order: Order, mut ace_state: AceSimulationState, ) { + let order_id = order.id(); let difference = ace_state.dependencies_to_handle(); // If we have handled all dependencies, this order will not be valid and thus we will // ignore it. if difference.is_empty() { + tracing::debug!( + order_id = ?order_id, + "ACE deps: order has no unhandled ACE dependencies, ignoring" + ); return; } + tracing::debug!( + order_id = ?order_id, + dependency_count = difference.len(), + dependencies = ?difference, + "ACE deps: handling ACE dependencies for failed order" + ); + let mut is_ready = true; let keys = difference @@ -413,9 +439,21 @@ impl SimTree { let dep_key = DependencyKey::AceUnlock(dep.get_contract_address()); match self.dependency_providers.get(&dep_key) { - Some(key) => Some(key), + Some(key) => { + tracing::debug!( + order_id = ?order_id, + contract_address = ?dep.get_contract_address(), + "ACE deps: found existing unlock provider for dependency" + ); + Some(key) + } None => { + tracing::debug!( + order_id = ?order_id, + contract_address = ?dep.get_contract_address(), + "ACE deps: no unlock provider found, order waiting for unlock parent" + ); is_ready = false; self.pending_dependencies .entry(dep_key) @@ -429,6 +467,10 @@ impl SimTree { // Order needs to wait for ACE unlock dependencies if !is_ready { + tracing::debug!( + order_id = ?order_id, + "ACE deps: order added to pending, waiting for unlock parents" + ); self.pending_orders .insert(order.id(), PendingOrder { order, ace_state }); return; @@ -454,6 +496,12 @@ impl SimTree { .flatten() .collect::>(); + tracing::debug!( + order_id = ?order_id, + parent_count = parents.len(), + "ACE deps: order ready with unlock parents, queued for re-simulation" + ); + // Order is ready with all unlock txs as parents self.ready_orders.push(SimulationRequest { id: rand::random(), @@ -965,6 +1013,18 @@ pub fn simulate_order_using_fork( Vec::new() }; + // Log ACE interactions detected for this order + if !ace_interactions.is_empty() { + for interaction in &ace_interactions { + tracing::debug!( + order_id = ?order.id(), + sim_success = sim_success, + ace_interaction = ?interaction, + "ACE sim: detected interaction for order" + ); + } + } + match result { Ok(res) => { let sim_value = create_sim_value(&order, &res, mempool_tx_detector); @@ -994,6 +1054,17 @@ pub fn simulate_order_using_fork( .copied() .collect(); + // Log failed orders that have ACE dependencies + if !non_unlocking.is_empty() { + tracing::debug!( + order_id = ?order.id(), + error = ?err, + non_unlocking_count = non_unlocking.len(), + non_unlocking_interactions = ?non_unlocking, + "ACE sim: order failed with non-unlocking ACE interactions (needs unlock parent)" + ); + } + Ok(OrderSimResult::Failed(SimulationFailure { error: err, ace_state: AceSimulationState { From cf74d6e95f1013154990b30e9b42d796e8ed4bb5 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Fri, 16 Jan 2026 14:48:52 -0500 Subject: [PATCH 44/51] feat: proper accounting for no fees --- crates/rbuilder/src/building/sim.rs | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index 26f82eea1..16a4a356f 100644 --- a/crates/rbuilder/src/building/sim.rs +++ b/crates/rbuilder/src/building/sim.rs @@ -1028,12 +1028,26 @@ pub fn simulate_order_using_fork( match result { Ok(res) => { let sim_value = create_sim_value(&order, &res, mempool_tx_detector); + + // Check if this is an ACE protocol unlock order (ProtocolForce or ProtocolOptional) + // These orders may have zero profit but are valuable for enabling other transactions + let is_ace_protocol_unlock = ace_interactions.iter().any(|i| i.is_protocol_tx()); + if let Err(err) = order_is_worth_executing(&sim_value) { - // Not an ACE-related failure, use default state - return Ok(OrderSimResult::Failed(SimulationFailure { - error: err, - ace_state: AceSimulationState::default(), - })); + if is_ace_protocol_unlock { + // ACE protocol unlocks bypass profit check - their value is enabling other txs + tracing::debug!( + order_id = ?order.id(), + ace_interactions = ?ace_interactions, + "ACE sim: protocol unlock order bypassing profit check" + ); + } else { + // Not an ACE protocol unlock, reject as usual + return Ok(OrderSimResult::Failed(SimulationFailure { + error: err, + ace_state: AceSimulationState::default(), + })); + } } let new_nonces = res.nonces_updated.into_iter().collect::>(); Ok(OrderSimResult::Success( From 278454df783a5075e2c7ef55fde90395ff480b90 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Fri, 16 Jan 2026 15:08:27 -0500 Subject: [PATCH 45/51] feat: proper classification of ace user orders --- crates/rbuilder-primitives/src/ace.rs | 30 +++++++++++++++++---------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/crates/rbuilder-primitives/src/ace.rs b/crates/rbuilder-primitives/src/ace.rs index e2e33c524..80e87c29a 100644 --- a/crates/rbuilder-primitives/src/ace.rs +++ b/crates/rbuilder-primitives/src/ace.rs @@ -24,14 +24,17 @@ pub struct AceConfig { } /// Classify an ACE order interaction type based on state trace, simulation success, and config. -/// Uses both state trace (address access) AND function signatures to determine interaction type. +/// +/// Classification logic: +/// - If simulation succeeds while accessing ACE slot → `Unlocking` (tx can execute without unlock parent) +/// - If simulation fails while accessing ACE slot → `NonUnlocking` (tx needs unlock parent) /// /// For `ProtocolForce` and `ProtocolOptional` classification, the transaction must: /// 1. Be a direct call to the ACE contract (`tx_to` in `config.to_addresses`) /// 2. Have the appropriate signature (`force_signatures` or `unlock_signatures`) /// 3. Be from a whitelisted address (`tx_from` in `config.from_addresses`) /// -/// All other unlocking transactions are classified as `User`. +/// All other successful unlocking transactions are classified as `User`. pub fn classify_ace_interaction( state_trace: &UsedStateTrace, sim_success: bool, @@ -73,14 +76,15 @@ pub fn classify_ace_interaction( let contract_address = config.contract_address; - if sim_success && (is_force_sig || is_unlock_sig) { + if sim_success { + // If simulation succeeded while accessing ACE slot, it's Unlocking // Protocol orders require: direct call + correct signature + whitelisted sender let source = if is_direct_protocol_call && is_force_sig && is_from_whitelisted { AceUnlockSource::ProtocolForce } else if is_direct_protocol_call && is_unlock_sig && is_from_whitelisted { AceUnlockSource::ProtocolOptional } else { - // Any unlock without all three requirements is a User unlock + // Any successful simulation that accesses ACE slot is a User unlock AceUnlockSource::User }; Some(AceInteraction::Unlocking { @@ -88,6 +92,7 @@ pub fn classify_ace_interaction( source, }) } else { + // Simulation failed while accessing ACE slot = needs unlock parent Some(AceInteraction::NonUnlocking { contract_address }) } } @@ -300,8 +305,9 @@ mod tests { } #[test] - fn test_ace_non_unlocking_interaction() { - // Transaction that accesses ACE slot but doesn't have unlock signature + fn test_ace_successful_sim_without_signature_is_user_unlock() { + // Transaction that accesses ACE slot, succeeds simulation, but doesn't have unlock signature + // Should be classified as User unlock since it can execute without unlock parent let config = real_ace_config(); let contract = config.contract_address; let detection_slot = *config.detection_slots.iter().next().unwrap(); @@ -309,7 +315,7 @@ mod tests { let trace = mock_state_trace_with_slot(contract, detection_slot); - // No unlock/force signature = NonUnlocking + // No unlock/force signature but successful = User unlock let result = classify_ace_interaction( &trace, true, @@ -321,13 +327,15 @@ mod tests { assert_eq!( result, - Some(AceInteraction::NonUnlocking { - contract_address: contract + Some(AceInteraction::Unlocking { + contract_address: contract, + source: AceUnlockSource::User }) ); - // Verify it's not an unlocking interaction - assert!(!result.unwrap().is_unlocking()); + // Verify it's unlocking but not a protocol tx + assert!(result.unwrap().is_unlocking()); + assert!(!result.unwrap().is_protocol_tx()); } #[test] From 983ff8f39ab404997fb912c2ebd18121cacc8f48 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Fri, 16 Jan 2026 15:33:45 -0500 Subject: [PATCH 46/51] test, inject on-chain landed orders --- .../build_block/backtest_build_block.rs | 3 ++ .../build_block/landed_block_from_db.rs | 40 +++++++++++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/crates/rbuilder/src/backtest/build_block/backtest_build_block.rs b/crates/rbuilder/src/backtest/build_block/backtest_build_block.rs index 20ec9abbf..12930506a 100644 --- a/crates/rbuilder/src/backtest/build_block/backtest_build_block.rs +++ b/crates/rbuilder/src/backtest/build_block/backtest_build_block.rs @@ -212,6 +212,9 @@ fn print_sim_order(sim_order: &SimulatedOrder) { ); } println!(" * gas_used {:?}", sim_value.gas_used()); + if !sim_order.ace_interactions.is_empty() { + println!(" * ace_interactions: {:?}", sim_order.ace_interactions); + } } fn print_orders_with_tx_hash( diff --git a/crates/rbuilder/src/backtest/build_block/landed_block_from_db.rs b/crates/rbuilder/src/backtest/build_block/landed_block_from_db.rs index 60e02c015..9d8bcda7f 100644 --- a/crates/rbuilder/src/backtest/build_block/landed_block_from_db.rs +++ b/crates/rbuilder/src/backtest/build_block/landed_block_from_db.rs @@ -27,11 +27,12 @@ use crate::{ live_builder::{block_list_provider::BlockList, cli::LiveBuilderConfig}, provider::StateProviderFactory, utils::{ - mevblocker::get_mevblocker_price, timestamp_as_u64, timestamp_ms_to_offset_datetime, - ProviderFactoryReopener, + extract_onchain_block_txs, mevblocker::get_mevblocker_price, timestamp_as_u64, + timestamp_ms_to_offset_datetime, ProviderFactoryReopener, }, }; use clap::Parser; +use rbuilder_primitives::{MempoolTx, Order}; use std::{path::PathBuf, str::FromStr, sync::Arc}; use super::backtest_build_block::{run_backtest_build_block, BuildBlockCfg, OrdersSource}; @@ -73,7 +74,7 @@ struct LandedBlockFromDBOrdersSource { impl LandedBlockFromDBOrdersSource { async fn new(extra_cfg: ExtraCfg, config: ConfigType) -> eyre::Result { - let block_data = read_block_data( + let mut block_data = read_block_data( &config.base_config().backtest_fetch_output_file, extra_cfg.block, extra_cfg @@ -87,6 +88,39 @@ impl LandedBlockFromDBOrdersSource { extra_cfg.show_missing, ) .await?; + + // When sim_landed_block is enabled, inject on-chain txs into available_orders + // This allows ACE detection to work on private txs that landed in the block + if extra_cfg.sim_landed_block { + let landed_txs = extract_onchain_block_txs(&block_data.onchain_block)?; + let block_timestamp_ms = timestamp_as_u64(&block_data.onchain_block) * 1000; + + // Collect existing tx hashes to avoid duplicates + let existing_tx_hashes: ahash::HashSet<_> = block_data + .available_orders + .iter() + .flat_map(|o| o.order.list_txs().into_iter().map(|(tx, _)| tx.hash())) + .collect(); + + let mut injected_count = 0; + for tx in landed_txs { + if !existing_tx_hashes.contains(&tx.hash()) { + block_data.available_orders.push(OrdersWithTimestamp { + order: Order::Tx(MempoolTx::new(tx)), + timestamp_ms: block_timestamp_ms, + }); + injected_count += 1; + } + } + + if injected_count > 0 { + println!( + "Injected {} on-chain txs into available_orders for simulation", + injected_count + ); + } + } + let blocklist = config .base_config() .blocklist_provider(CancellationToken::new()) From c7a2c65af4d99fa5bb5512ffc6dd9c48e14b07b5 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Fri, 16 Jan 2026 16:16:11 -0500 Subject: [PATCH 47/51] some better classification --- crates/rbuilder-primitives/src/ace.rs | 160 +++++++++++++++++++------- 1 file changed, 117 insertions(+), 43 deletions(-) diff --git a/crates/rbuilder-primitives/src/ace.rs b/crates/rbuilder-primitives/src/ace.rs index 80e87c29a..3f86ea84d 100644 --- a/crates/rbuilder-primitives/src/ace.rs +++ b/crates/rbuilder-primitives/src/ace.rs @@ -26,7 +26,8 @@ pub struct AceConfig { /// Classify an ACE order interaction type based on state trace, simulation success, and config. /// /// Classification logic: -/// - If simulation succeeds while accessing ACE slot → `Unlocking` (tx can execute without unlock parent) +/// - If simulation succeeds and WRITES to ACE slot → `Unlocking` (tx performs unlock) +/// - If simulation succeeds but only READS ACE slot → No ACE interaction (tx just uses unlocked state) /// - If simulation fails while accessing ACE slot → `NonUnlocking` (tx needs unlock parent) /// /// For `ProtocolForce` and `ProtocolOptional` classification, the transaction must: @@ -34,7 +35,7 @@ pub struct AceConfig { /// 2. Have the appropriate signature (`force_signatures` or `unlock_signatures`) /// 3. Be from a whitelisted address (`tx_from` in `config.from_addresses`) /// -/// All other successful unlocking transactions are classified as `User`. +/// All other successful unlocking transactions (that WRITE to detection slot) are classified as `User`. pub fn classify_ace_interaction( state_trace: &UsedStateTrace, sim_success: bool, @@ -43,7 +44,8 @@ pub fn classify_ace_interaction( tx_to: Option

, tx_from: Option
, ) -> Option { - let any_ace_slots_accessed = config + // Check if any ACE detection slots were READ + let any_ace_slots_read = config .to_addresses .iter() .flat_map(|address| { @@ -52,13 +54,21 @@ pub fn classify_ace_interaction( key: *slot, }) }) - .flat_map(|key| { - [ - state_trace.read_slot_values.contains_key(&key), - state_trace.written_slot_values.contains_key(&key), - ] + .any(|key| state_trace.read_slot_values.contains_key(&key)); + + // Check if any ACE detection slots were WRITTEN + let any_ace_slots_written = config + .to_addresses + .iter() + .flat_map(|address| { + config.detection_slots.iter().map(|slot| SlotKey { + address: *address, + key: *slot, + }) }) - .any(|read_slot_of_interest| read_slot_of_interest); + .any(|key| state_trace.written_slot_values.contains_key(&key)); + + let any_ace_slots_accessed = any_ace_slots_read || any_ace_slots_written; if !any_ace_slots_accessed { return None; @@ -77,20 +87,34 @@ pub fn classify_ace_interaction( let contract_address = config.contract_address; if sim_success { - // If simulation succeeded while accessing ACE slot, it's Unlocking - // Protocol orders require: direct call + correct signature + whitelisted sender - let source = if is_direct_protocol_call && is_force_sig && is_from_whitelisted { - AceUnlockSource::ProtocolForce - } else if is_direct_protocol_call && is_unlock_sig && is_from_whitelisted { - AceUnlockSource::ProtocolOptional + // For successful simulations, only classify as Unlocking if the tx WRITES to detection slot. + // A tx that only READS the slot is just using the unlocked state, not performing an unlock. + // Protocol orders require: direct call + correct signature + whitelisted sender + write to slot + if is_direct_protocol_call && is_force_sig && is_from_whitelisted && any_ace_slots_written { + Some(AceInteraction::Unlocking { + contract_address, + source: AceUnlockSource::ProtocolForce, + }) + } else if is_direct_protocol_call + && is_unlock_sig + && is_from_whitelisted + && any_ace_slots_written + { + Some(AceInteraction::Unlocking { + contract_address, + source: AceUnlockSource::ProtocolOptional, + }) + } else if any_ace_slots_written { + // User tx that writes to detection slot = User unlock + Some(AceInteraction::Unlocking { + contract_address, + source: AceUnlockSource::User, + }) } else { - // Any successful simulation that accesses ACE slot is a User unlock - AceUnlockSource::User - }; - Some(AceInteraction::Unlocking { - contract_address, - source, - }) + // Successful simulation that only READS detection slot = not an unlock, + // just a tx that uses the unlocked state. No ACE interaction to track. + None + } } else { // Simulation failed while accessing ACE slot = needs unlock parent Some(AceInteraction::NonUnlocking { contract_address }) @@ -186,8 +210,8 @@ mod tests { } } - /// Create a mock state trace with the detection slot accessed - fn mock_state_trace_with_slot(addr: Address, slot: B256) -> UsedStateTrace { + /// Create a mock state trace with the detection slot READ (not written) + fn mock_state_trace_with_slot_read(addr: Address, slot: B256) -> UsedStateTrace { let mut trace = UsedStateTrace::default(); trace.read_slot_values.insert( SlotKey { @@ -199,6 +223,19 @@ mod tests { trace } + /// Create a mock state trace with the detection slot WRITTEN + fn mock_state_trace_with_slot_written(addr: Address, slot: B256) -> UsedStateTrace { + let mut trace = UsedStateTrace::default(); + trace.written_slot_values.insert( + SlotKey { + address: addr, + key: slot, + }, + Default::default(), + ); + trace + } + #[test] fn test_real_ace_force_order_classification() { // Test with real force order calldata @@ -208,8 +245,8 @@ mod tests { let force_selector = *config.force_signatures.iter().next().unwrap(); let whitelisted_from = *config.from_addresses.iter().next().unwrap(); - // Mock state trace with detection slot accessed - let trace = mock_state_trace_with_slot(contract, detection_slot); + // Mock state trace with detection slot WRITTEN (unlock writes to slot) + let trace = mock_state_trace_with_slot_written(contract, detection_slot); // Direct call to ACE contract with force signature FROM WHITELISTED ADDRESS should be ProtocolForce let result = classify_ace_interaction( @@ -243,8 +280,8 @@ mod tests { let unlock_selector = *config.unlock_signatures.iter().next().unwrap(); let whitelisted_from = *config.from_addresses.iter().next().unwrap(); - // Mock state trace with detection slot accessed - let trace = mock_state_trace_with_slot(contract, detection_slot); + // Mock state trace with detection slot WRITTEN (unlock writes to slot) + let trace = mock_state_trace_with_slot_written(contract, detection_slot); // Direct call to ACE contract with unlock signature FROM WHITELISTED ADDRESS should be ProtocolOptional let result = classify_ace_interaction( @@ -272,12 +309,14 @@ mod tests { #[test] fn test_ace_user_unlock_indirect_call() { // User transaction that calls ACE contract indirectly (not tx.to = contract) + // and WRITES to the detection slot = User unlock let config = real_ace_config(); let contract = config.contract_address; let detection_slot = *config.detection_slots.iter().next().unwrap(); let unlock_selector = *config.unlock_signatures.iter().next().unwrap(); - let trace = mock_state_trace_with_slot(contract, detection_slot); + // Use write trace since only writes classify as unlock + let trace = mock_state_trace_with_slot_written(contract, detection_slot); // tx.to is NOT the ACE contract (indirect call via user tx) = User unlock // Even with whitelisted from address, indirect call is User @@ -305,17 +344,46 @@ mod tests { } #[test] - fn test_ace_successful_sim_without_signature_is_user_unlock() { - // Transaction that accesses ACE slot, succeeds simulation, but doesn't have unlock signature - // Should be classified as User unlock since it can execute without unlock parent + fn test_ace_successful_sim_read_only_returns_none() { + // Transaction that only READS ACE slot and succeeds simulation + // Should return None - tx just uses unlocked state, doesn't perform unlock + let config = real_ace_config(); + let contract = config.contract_address; + let detection_slot = *config.detection_slots.iter().next().unwrap(); + let whitelisted_from = *config.from_addresses.iter().next().unwrap(); + + // Only READ the slot, not write + let trace = mock_state_trace_with_slot_read(contract, detection_slot); + + // No write to slot = not an unlock, even if successful + let result = classify_ace_interaction( + &trace, + true, + &config, + None, + Some(contract), + Some(whitelisted_from), + ); + + assert_eq!( + result, None, + "Successful simulation that only reads detection slot should return None" + ); + } + + #[test] + fn test_ace_user_unlock_with_slot_write() { + // Transaction that WRITES to ACE slot and succeeds without unlock signature + // Should be classified as User unlock let config = real_ace_config(); let contract = config.contract_address; let detection_slot = *config.detection_slots.iter().next().unwrap(); let whitelisted_from = *config.from_addresses.iter().next().unwrap(); - let trace = mock_state_trace_with_slot(contract, detection_slot); + // WRITE to the slot + let trace = mock_state_trace_with_slot_written(contract, detection_slot); - // No unlock/force signature but successful = User unlock + // Writes to slot without unlock signature = User unlock let result = classify_ace_interaction( &trace, true, @@ -347,7 +415,8 @@ mod tests { let unlock_selector = *config.unlock_signatures.iter().next().unwrap(); let whitelisted_from = *config.from_addresses.iter().next().unwrap(); - let trace = mock_state_trace_with_slot(contract, detection_slot); + // For failed sims, even read-only access triggers NonUnlocking + let trace = mock_state_trace_with_slot_read(contract, detection_slot); // sim_success = false turns unlock into NonUnlocking let result = classify_ace_interaction( @@ -402,7 +471,7 @@ mod tests { let force_selector = *config.force_signatures.iter().next().unwrap(); let whitelisted_from = *config.from_addresses.iter().next().unwrap(); - let trace = mock_state_trace_with_slot(config.contract_address, wrong_slot); + let trace = mock_state_trace_with_slot_read(config.contract_address, wrong_slot); // Wrong slot accessed = None (even with valid signature) let result = classify_ace_interaction( @@ -555,8 +624,8 @@ mod tests { // Optional unlock signature from real transaction let unlock_selector = Selector::from_slice(&[0x18, 0x28, 0xe0, 0xe7]); - // Mock state trace showing slot 3 was accessed - let trace = mock_state_trace_with_slot(contract, slot); + // Mock state trace showing slot 3 was WRITTEN (unlocking writes to slot) + let trace = mock_state_trace_with_slot_written(contract, slot); // Test 1: Direct call to ACE contract FROM WHITELISTED ADDRESS = ProtocolOptional let result = classify_ace_interaction( @@ -576,7 +645,7 @@ mod tests { }) ); - // Test 2: Indirect call (user tx) = User unlock + // Test 2: Indirect call (user tx) that WRITES = User unlock let result_indirect = classify_ace_interaction( &trace, true, @@ -595,8 +664,10 @@ mod tests { ); // Test 3: Failed simulation with unlock signature = NonUnlocking + // (use read trace for failed sim - read access is enough to identify ACE dependency) + let read_trace = mock_state_trace_with_slot_read(contract, slot); let result_failed = classify_ace_interaction( - &trace, + &read_trace, false, &config, Some(unlock_selector), @@ -665,7 +736,8 @@ mod tests { "Address should not be in whitelist for this test" ); - let trace = mock_state_trace_with_slot(contract, detection_slot); + // Use write trace since only writes classify as unlock + let trace = mock_state_trace_with_slot_written(contract, detection_slot); // Direct call with force signature but from non-whitelisted address = User let result = classify_ace_interaction( @@ -702,7 +774,8 @@ mod tests { let non_whitelisted = address!("2222222222222222222222222222222222222222"); assert!(!config.from_addresses.contains(&non_whitelisted)); - let trace = mock_state_trace_with_slot(contract, detection_slot); + // Use write trace since only writes classify as unlock + let trace = mock_state_trace_with_slot_written(contract, detection_slot); // Direct call with unlock signature but from non-whitelisted address = User let result = classify_ace_interaction( @@ -735,7 +808,8 @@ mod tests { let detection_slot = *config.detection_slots.iter().next().unwrap(); let force_selector = *config.force_signatures.iter().next().unwrap(); - let trace = mock_state_trace_with_slot(contract, detection_slot); + // Use write trace since only writes classify as unlock + let trace = mock_state_trace_with_slot_written(contract, detection_slot); // Direct call with force signature but tx_from = None = User let result = classify_ace_interaction( From eee8b200e873d6c39bb72280189668dc794ff333 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Fri, 20 Mar 2026 17:36:33 +0000 Subject: [PATCH 48/51] fix: use ACE priority instead of profit when comparing unlock dependency providers The Entry::Occupied branch in SimTree::process_simulation_task_result compared dependency providers by coinbase_profit for all dep types, allowing a more profitable optional unlock to displace a force unlock. Now AceUnlock deps compare by interaction priority (Force > Optional > User) while Nonce deps retain profit-based comparison. Co-Authored-By: Frankenstein <263438330+frankenstien-dev@users.noreply.github.com> --- crates/rbuilder-primitives/src/lib.rs | 4 +- .../src/building/builders/ordering_builder.rs | 6 +- .../block_building_result_assembler.rs | 14 +- crates/rbuilder/src/building/order_commit.rs | 8 -- crates/rbuilder/src/building/sim.rs | 123 +++++++++++++----- .../src/building/testing/ace_tests/mod.rs | 27 ++-- .../order_input/order_replacement_manager.rs | 5 +- .../live_builder/simulation/simulation_job.rs | 6 +- 8 files changed, 137 insertions(+), 56 deletions(-) diff --git a/crates/rbuilder-primitives/src/lib.rs b/crates/rbuilder-primitives/src/lib.rs index d29f0c97a..c64a08e6b 100644 --- a/crates/rbuilder-primitives/src/lib.rs +++ b/crates/rbuilder-primitives/src/lib.rs @@ -137,7 +137,7 @@ pub struct Nonce { pub optional: bool, } -/// Information regarding a new/update replaceable Bundle/ShareBundle. +/// Information regarding a new/update replaceable Bundle. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct ReplacementData { pub key: KeyType, @@ -1046,7 +1046,7 @@ impl SimulatedOrder { } /// Unique OrderId used along the whole builder. -/// Sadly it's not perfect since we still might have some collisions (eg: ShareBundle is the tx tree hash which does not include all the other cfg). +/// Sadly it's not perfect since we still might have some collisions. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum OrderId { Tx(B256), diff --git a/crates/rbuilder/src/building/builders/ordering_builder.rs b/crates/rbuilder/src/building/builders/ordering_builder.rs index e8b3b74f6..d38532856 100644 --- a/crates/rbuilder/src/building/builders/ordering_builder.rs +++ b/crates/rbuilder/src/building/builders/ordering_builder.rs @@ -332,7 +332,11 @@ impl OrderingBuilderContext { ace_order, &|_| Ok(()), // ACE protocol orders bypass profit validation ) { - trace!(order_id = ?ace_order.id(), ?err, "Failed to pre-commit ACE protocol order"); + if ace_order.ace_interactions.iter().any(|a| a.is_force()) { + error!(order_id = ?ace_order.id(), ?err, "Failed to pre-commit ProtocolForce ACE order"); + } else { + trace!(order_id = ?ace_order.id(), ?err, "Failed to pre-commit ACE protocol order"); + } } } self.fill_orders( diff --git a/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs b/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs index ec685c643..16e432eb1 100644 --- a/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs +++ b/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs @@ -11,7 +11,7 @@ use std::{ }; use time::OffsetDateTime; use tokio_util::sync::CancellationToken; -use tracing::{info_span, trace}; +use tracing::{error, info_span, trace}; use crate::{ building::{ @@ -233,7 +233,11 @@ impl BlockBuildingResultAssembler { ace_order, &|_| Ok(()), // ACE protocol orders bypass profit validation ) { - trace!(order_id = ?ace_order.id(), ?err, "Failed to pre-commit ACE protocol order"); + if ace_order.ace_interactions.iter().any(|a| a.is_force()) { + error!(order_id = ?ace_order.id(), ?err, "Failed to pre-commit ProtocolForce ACE order"); + } else { + trace!(order_id = ?ace_order.id(), ?err, "Failed to pre-commit ACE protocol order"); + } } } @@ -353,7 +357,11 @@ impl BlockBuildingResultAssembler { ace_order, &|_| Ok(()), // ACE protocol orders bypass profit validation ) { - trace!(order_id = ?ace_order.id(), ?err, "Failed to pre-commit ACE protocol order in backtest"); + if ace_order.ace_interactions.iter().any(|a| a.is_force()) { + error!(order_id = ?ace_order.id(), ?err, "Failed to pre-commit ProtocolForce ACE order in backtest"); + } else { + trace!(order_id = ?ace_order.id(), ?err, "Failed to pre-commit ACE protocol order in backtest"); + } } } diff --git a/crates/rbuilder/src/building/order_commit.rs b/crates/rbuilder/src/building/order_commit.rs index 41dd0b676..d64653b8d 100644 --- a/crates/rbuilder/src/building/order_commit.rs +++ b/crates/rbuilder/src/building/order_commit.rs @@ -443,14 +443,6 @@ pub struct ReservedPayout { pub base_fee: U256, } -#[derive(Debug, Clone)] -pub struct ShareBundleCommitResult { - pub bundle_ok: BundleOk, - pub coinbase_diff_before_payouts: U256, - pub total_payouts_promissed: U256, - pub payouts_promissed: HashMap, -} - #[derive(thiserror::Error, Debug)] pub enum CriticalCommitOrderError { #[error("Reth error: {0}")] diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index c1c858d84..1a7b125e1 100644 --- a/crates/rbuilder/src/building/sim.rs +++ b/crates/rbuilder/src/building/sim.rs @@ -161,6 +161,8 @@ impl AceSimulationState { #[derive(Debug, Clone, PartialEq, Eq)] struct PendingOrder { order: Arc, + /// Number of nonce dependencies not yet satisfied + unsatisfied_nonce_deps: usize, /// ACE state tracking detected and accounted-for interactions ace_state: AceSimulationState, } @@ -261,6 +263,12 @@ impl SimTree { for config in ace_configs { let contract_address = config.contract_address; + if ace_config.contains_key(&contract_address) { + error!( + ?contract_address, + "ACE SimTree: duplicate contract_address in ace_protocols config, last entry wins" + ); + } tracing::debug!( contract_address = ?contract_address, detection_slots = ?config.detection_slots, @@ -307,6 +315,7 @@ impl SimTree { } OrderDependencyState::Pending(pending_deps) => { mark_order_pending_nonce(order_id); + let unsatisfied_nonce_deps = pending_deps.len(); for dep in pending_deps { self.pending_dependencies .entry(dep) @@ -317,6 +326,7 @@ impl SimTree { order.id(), PendingOrder { order, + unsatisfied_nonce_deps, ace_state: AceSimulationState::default(), }, ); @@ -467,12 +477,28 @@ impl SimTree { // Order needs to wait for ACE unlock dependencies if !is_ready { + // Account for already-found providers so that when the remaining + // providers arrive, all_dependencies_accounted() can return true. + for sim_id in &keys { + if let Some(sim) = self.sims.get(sim_id) { + ace_state.add_accounted_interactions( + sim.simulated_order.ace_interactions.iter().copied(), + ); + } + } + tracing::debug!( order_id = ?order_id, "ACE deps: order added to pending, waiting for unlock parents" ); - self.pending_orders - .insert(order.id(), PendingOrder { order, ace_state }); + self.pending_orders.insert( + order.id(), + PendingOrder { + order, + unsatisfied_nonce_deps: 0, + ace_state, + }, + ); return; } @@ -555,25 +581,39 @@ impl SimTree { for dep_key in dependencies_satisfied.iter().cloned() { match self.dependency_providers.entry(dep_key.clone()) { Entry::Occupied(mut entry) => { - // Already have a provider - check if this one is more profitable - let current_sim_profit = { + // Already have a provider - check if this one should replace it. + // For ACE unlock deps, prefer by ACE priority (Force > Optional > User) + // so a force unlock is never displaced by a more profitable optional. + // For nonce deps, prefer by coinbase profit. + let should_replace = { let sim_id = entry.get_mut(); if let Some(existing_sim) = self.sims.get(sim_id) { - existing_sim - .simulated_order - .sim_value - .full_profit_info() - .coinbase_profit() + match &dep_key { + DependencyKey::AceUnlock(_) => { + let existing_priority = max_ace_priority( + &existing_sim.simulated_order.ace_interactions, + ); + let new_priority = + max_ace_priority(&simulated_order.ace_interactions); + new_priority > existing_priority + } + DependencyKey::Nonce(_) => { + simulated_order + .sim_value + .full_profit_info() + .coinbase_profit() + > existing_sim + .simulated_order + .sim_value + .full_profit_info() + .coinbase_profit() + } + } } else { - continue; + true } }; - if simulated_order - .sim_value - .full_profit_info() - .coinbase_profit() - > current_sim_profit - { + if should_replace { entry.insert(*id); } } @@ -588,15 +628,23 @@ impl SimTree { { let pending_order = entry.get_mut(); - // Add the unlock interactions to the order's accounted_for set - if matches!(dep_key, DependencyKey::AceUnlock(_)) { - pending_order.ace_state.add_accounted_interactions( - simulated_order.ace_interactions.iter().copied(), - ); + match &dep_key { + DependencyKey::Nonce(_) => { + pending_order.unsatisfied_nonce_deps = pending_order + .unsatisfied_nonce_deps + .saturating_sub(1); + } + DependencyKey::AceUnlock(_) => { + pending_order.ace_state.add_accounted_interactions( + simulated_order.ace_interactions.iter().copied(), + ); + } } - // Check if all ACE deps are now accounted for - if pending_order.ace_state.all_dependencies_accounted() { + // Only promote when ALL dependencies (nonce + ACE) are resolved + if pending_order.unsatisfied_nonce_deps == 0 + && pending_order.ace_state.all_dependencies_accounted() + { orders_ready.push(entry.remove()); } // Otherwise order stays pending, waiting for more deps @@ -771,11 +819,16 @@ impl SimTree { let mut successful_results = Vec::with_capacity(results.len()); for result in results { - if let SimulatedResult::Success { .. } = result { - 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); + match result { + success @ SimulatedResult::Success { .. } => { + cancellations.extend(self.handle_ace_unlock(&success)?); + // All successful results need to be processed for dependency tracking + self.process_simulation_task_result(&success)?; + successful_results.push(success); + } + SimulatedResult::Failed { order, failure, .. } => { + self.handle_ace_dependencies_for_order(order, failure.ace_state); + } } } @@ -812,7 +865,7 @@ where sim_tree.push_orders(std::mem::take(&mut orders))?; } - let sim_errors = Vec::new(); + let mut sim_errors = Vec::new(); let mut state_for_sim = Arc::::from(provider.history_by_block_hash(ctx.attributes.parent)?); let mut local_ctx = ThreadBlockBuildingContext::default(); @@ -853,6 +906,7 @@ where // if we have a failure, we will handle the case were its ace by either putting // it into pending or requeing if we have the deps. Otherwise, no action is // taken and flow handles as normal. + sim_errors.push(failure.error); sim_tree.handle_ace_dependencies_for_order(sim_task.order, failure.ace_state); } OrderSimResult::Success(sim_order, nonces) => { @@ -1107,3 +1161,14 @@ fn interaction_priority(interaction: &AceInteraction) -> u8 { AceInteraction::NonUnlocking { .. } => 1, } } + +/// Returns the maximum ACE priority across all interactions for a simulated order. +/// Used to compare dependency providers for `DependencyKey::AceUnlock` so that +/// a force unlock is never displaced by a more profitable optional unlock. +fn max_ace_priority(interactions: &[AceInteraction]) -> u8 { + interactions + .iter() + .map(interaction_priority) + .max() + .unwrap_or(0) +} diff --git a/crates/rbuilder/src/building/testing/ace_tests/mod.rs b/crates/rbuilder/src/building/testing/ace_tests/mod.rs index 57daebbf8..80dd7640b 100644 --- a/crates/rbuilder/src/building/testing/ace_tests/mod.rs +++ b/crates/rbuilder/src/building/testing/ace_tests/mod.rs @@ -13,8 +13,8 @@ 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 { +fn create_test_order() -> Arc { + Arc::new(Order::Bundle(Bundle { version: BundleVersion::V1, block: None, min_timestamp: None, @@ -30,7 +30,7 @@ fn create_test_order() -> Order { metadata: Default::default(), refund: None, external_hash: None, - }) + })) } /// Create the real ACE config for testing @@ -47,10 +47,10 @@ fn test_ace_config() -> AceConfig { } } -/// Create a mock state trace with ACE detection slot accessed +/// Create a mock state trace with ACE detection slot written (unlock orders write to detection slot) fn mock_state_trace_with_ace_slot(contract: Address, slot: B256) -> UsedStateTrace { let mut trace = UsedStateTrace::default(); - trace.read_slot_values.insert( + trace.written_slot_values.insert( SlotKey { address: contract, key: slot, @@ -613,10 +613,21 @@ fn test_classify_ace_interaction_priority_force_beats_optional() { }) )); - // NonUnlocking classification (no unlock signature) + // NonUnlocking classification: failed tx that reads (but doesn't write) ACE detection slot + let read_only_trace = { + let mut trace = UsedStateTrace::default(); + trace.read_slot_values.insert( + SlotKey { + address: contract, + key: slot, + }, + Default::default(), + ); + trace + }; let non_unlocking_result = classify_ace_interaction( - &trace, - true, + &read_only_trace, + false, // tx failed &config, None, // no selector Some(contract), diff --git a/crates/rbuilder/src/live_builder/order_input/order_replacement_manager.rs b/crates/rbuilder/src/live_builder/order_input/order_replacement_manager.rs index 010594e48..dedb89ef3 100644 --- a/crates/rbuilder/src/live_builder/order_input/order_replacement_manager.rs +++ b/crates/rbuilder/src/live_builder/order_input/order_replacement_manager.rs @@ -15,8 +15,7 @@ use super::{order_sink::OrderSink, replaceable_order_sink::ReplaceableOrderSink} /// Cancels are treated as highest-priority, and after that we must always /// honor the replacement with highest `sequence_number`. /// -/// Although all the structs and fields say "bundle" we always refer to Bundle -/// or ShareBundle. +/// Although all the structs and fields say "bundle" we always refer to Bundle. /// /// For each bundle we keep the current [`BundleReplacementState`] #[derive(Debug)] @@ -84,7 +83,7 @@ struct ValidBundleState { pub order_id: OrderId, } -/// Last state we have for a replaceable ShareBundle. +/// Last state we have for a replaceable Bundle. /// It updates itself on new orders. /// On new seq: /// Valid upgrades if seq > current. diff --git a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs index 70a799430..e03ec9aea 100644 --- a/crates/rbuilder/src/live_builder/simulation/simulation_job.rs +++ b/crates/rbuilder/src/live_builder/simulation/simulation_job.rs @@ -230,14 +230,16 @@ impl SimulationJob { } SimulatedResult::Failed { failure: SimulationFailure { ref ace_state, .. }, + ref order, .. } => { - // Check if there are unhandled ACE dependencies if !ace_state.all_dependencies_accounted() { // Failed with ACE dependency - pass to sim_tree for re-queuing with unlock parent sim_tree_results.push(sim_result); + } else { + // Permanent failure - remove from in_flight tracking + self.in_flight_orders.remove(&order.id()); } - // Permanent failure without ACE dependency - nothing to do } } } From 0aded8d8572737ab71055e8403baf0e3b771af65 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Fri, 20 Mar 2026 20:32:33 +0000 Subject: [PATCH 49/51] w --- .../src/building/builders/ordering_builder.rs | 1 + .../block_building_result_assembler.rs | 2 ++ crates/rbuilder/src/building/sim.rs | 15 +++++++-------- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/crates/rbuilder/src/building/builders/ordering_builder.rs b/crates/rbuilder/src/building/builders/ordering_builder.rs index f6aa47574..3771fe364 100644 --- a/crates/rbuilder/src/building/builders/ordering_builder.rs +++ b/crates/rbuilder/src/building/builders/ordering_builder.rs @@ -330,6 +330,7 @@ impl OrderingBuilderContext { if let Err(err) = block_building_helper.commit_order( &mut self.local_ctx, ace_order, + #[allow(clippy::result_large_err)] &|_| Ok(()), // ACE protocol orders bypass profit validation ) { if ace_order.ace_interactions.iter().any(|a| a.is_force()) { diff --git a/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs b/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs index d3d57d136..271853896 100644 --- a/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs +++ b/crates/rbuilder/src/building/builders/parallel_builder/block_building_result_assembler.rs @@ -231,6 +231,7 @@ impl BlockBuildingResultAssembler { if let Err(err) = block_building_helper.commit_order( &mut self.local_ctx, ace_order, + #[allow(clippy::result_large_err)] &|_| Ok(()), // ACE protocol orders bypass profit validation ) { if ace_order.ace_interactions.iter().any(|a| a.is_force()) { @@ -358,6 +359,7 @@ impl BlockBuildingResultAssembler { if let Err(err) = block_building_helper.commit_order( &mut self.local_ctx, ace_order, + #[allow(clippy::result_large_err)] &|_| Ok(()), // ACE protocol orders bypass profit validation ) { if ace_order.ace_interactions.iter().any(|a| a.is_force()) { diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index 1a7b125e1..74c7bbb08 100644 --- a/crates/rbuilder/src/building/sim.rs +++ b/crates/rbuilder/src/building/sim.rs @@ -100,7 +100,7 @@ impl AceExchangeState { // block builder. self.force_unlock_order .as_ref() - .or_else(|| self.optional_unlock_order.as_ref()) + .or(self.optional_unlock_order.as_ref()) } } @@ -578,7 +578,7 @@ impl SimTree { let mut orders_ready: Vec = Vec::new(); // Process each dependency this simulation satisfies - for dep_key in dependencies_satisfied.iter().cloned() { + for dep_key in dependencies_satisfied { match self.dependency_providers.entry(dep_key.clone()) { Entry::Occupied(mut entry) => { // Already have a provider - check if this one should replace it. @@ -588,7 +588,7 @@ impl SimTree { let should_replace = { let sim_id = entry.get_mut(); if let Some(existing_sim) = self.sims.get(sim_id) { - match &dep_key { + match dep_key { DependencyKey::AceUnlock(_) => { let existing_priority = max_ace_priority( &existing_sim.simulated_order.ace_interactions, @@ -622,17 +622,16 @@ impl SimTree { entry.insert(*id); // Update orders waiting on this dependency - if let Some(pending_order_ids) = self.pending_dependencies.remove(&dep_key) { + if let Some(pending_order_ids) = self.pending_dependencies.remove(dep_key) { for order_id in pending_order_ids { if let Entry::Occupied(mut entry) = self.pending_orders.entry(order_id) { let pending_order = entry.get_mut(); - match &dep_key { + match dep_key { DependencyKey::Nonce(_) => { - pending_order.unsatisfied_nonce_deps = pending_order - .unsatisfied_nonce_deps - .saturating_sub(1); + pending_order.unsatisfied_nonce_deps = + pending_order.unsatisfied_nonce_deps.saturating_sub(1); } DependencyKey::AceUnlock(_) => { pending_order.ace_state.add_accounted_interactions( From 0a59358665f98d8150eb228d043dc69b1d2db710 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Wed, 1 Apr 2026 17:14:35 +0000 Subject: [PATCH 50/51] fix: drain parent traces before ACE classification in fork simulation When simulating an order with ACE unlock parents, the tracer accumulated state traces from both parent and child execution. This caused the child order to be misclassified as Unlocking { source: User } when it merely reads the unlocked detection slot, because parent writes leaked into the combined trace used for classification. Adds a has_ace_parents flag to SimulationRequest that, when set, drains the accumulated parent traces before executing the target order so ACE interaction classification reflects only the order's own slot access. --- crates/rbuilder/src/building/sim.rs | 20 +++++++++++++++++++ .../src/building/testing/ace_tests/mod.rs | 1 + .../src/live_builder/simulation/sim_worker.rs | 1 + 3 files changed, 22 insertions(+) diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index 74c7bbb08..5f11a9755 100644 --- a/crates/rbuilder/src/building/sim.rs +++ b/crates/rbuilder/src/building/sim.rs @@ -178,6 +178,11 @@ pub struct SimulationRequest { /// Used to determine if a failure is genuine (contract already unlocked) or needs retry. /// Supports multiple ACE contracts - order can progressively discover needed unlocks. pub ace_state: AceSimulationState, + /// When true, parent orders include ACE unlock providers. The tracer's accumulated + /// state trace is drained after executing parents so that ACE interaction classification + /// reflects only the target order's own slot access — preventing parent detection slot + /// writes from leaking into the child's classification. + pub has_ace_parents: bool, } #[derive(Debug)] @@ -338,6 +343,7 @@ impl SimTree { parents, // we don't have a state for it yet. ace_state: AceSimulationState::default(), + has_ace_parents: false, }); } } @@ -534,6 +540,7 @@ impl SimTree { order, parents, ace_state, + has_ace_parents: true, }); } @@ -680,6 +687,7 @@ impl SimTree { order: ready_pending_order.order, parents, ace_state, + has_ace_parents: true, }); } OrderDependencyState::Invalid => { @@ -897,6 +905,7 @@ where &mut block_state, sim_tree.ace_configs(), &sim_task.ace_state, + sim_task.has_ace_parents, )?; let (_, provider) = block_state.into_parts(); state_for_sim = provider; @@ -960,6 +969,7 @@ pub fn simulate_order( ace_configs: &HashMap, // we have parents for these ace addresses. current_ace_state: &AceSimulationState, + has_ace_parents: bool, ) -> Result { let mut tracer = AccumulatorSimulationTracer::new(); let mut fork = PartialBlockFork::new(state, ctx, local_ctx).with_tracer(&mut tracer); @@ -971,6 +981,7 @@ pub fn simulate_order( &ctx.mempool_tx_detector, ace_configs, current_ace_state, + has_ace_parents, ); fork.rollback(rollback_point); let sim_res = sim_res?; @@ -989,6 +1000,7 @@ pub fn simulate_order_using_fork( mempool_tx_detector: &MempoolTxsDetector, ace_configs: &HashMap, current_ace_state: &AceSimulationState, + has_ace_parents: bool, ) -> Result { let start = Instant::now(); @@ -1015,6 +1027,14 @@ pub fn simulate_order_using_fork( } } + // When simulating with ACE unlock parents, drain the accumulated parent traces so that + // ACE interaction classification below uses only the target order's own slot access. + // Without this, parent writes to ACE detection slots would leak into the combined trace + // and cause the child to be misclassified as Unlocking when it merely reads unlocked state. + if has_ace_parents { + let _ = fork.tracer.as_mut().and_then(|t| t.take_used_state_trace()); + } + // simulate let result = fork.commit_order(&order, space_state, true, &combined_refunds)?; let sim_time = start.elapsed(); diff --git a/crates/rbuilder/src/building/testing/ace_tests/mod.rs b/crates/rbuilder/src/building/testing/ace_tests/mod.rs index 80dd7640b..d2cd5bc45 100644 --- a/crates/rbuilder/src/building/testing/ace_tests/mod.rs +++ b/crates/rbuilder/src/building/testing/ace_tests/mod.rs @@ -352,6 +352,7 @@ fn test_simulation_request_ace_state_empty_default() { order: create_test_order(), parents: Vec::new(), ace_state: AceSimulationState::default(), + has_ace_parents: false, }; assert!(request.ace_state.all_dependencies_accounted()); diff --git a/crates/rbuilder/src/live_builder/simulation/sim_worker.rs b/crates/rbuilder/src/live_builder/simulation/sim_worker.rs index 246cee07e..9bb90d14d 100644 --- a/crates/rbuilder/src/live_builder/simulation/sim_worker.rs +++ b/crates/rbuilder/src/live_builder/simulation/sim_worker.rs @@ -73,6 +73,7 @@ pub fn run_sim_worker

( &mut block_state, ¤t_sim_context.ace_configs, &task.ace_state, + task.has_ace_parents, ); let sim_ok = match sim_result { Ok(sim_result) => { From 59f4b06d9f73eddaf06cafa2831767df3aced947 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Mon, 13 Apr 2026 17:30:58 +0000 Subject: [PATCH 51/51] fix: preserve develop's single-nonce-only provider tracking in SimTree Only register nonce dependencies as providers when a simulation updates exactly one nonce, matching develop's sims_that_update_one_nonce behavior. Multi-nonce sims (e.g. bundles with multiple senders) are stored but not tracked as providers. ACE unlock deps are always processed regardless. --- crates/rbuilder/src/building/sim.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index bf5fbc9ca..fb0a248c6 100644 --- a/crates/rbuilder/src/building/sim.rs +++ b/crates/rbuilder/src/building/sim.rs @@ -607,8 +607,20 @@ impl SimTree { // Track orders that become ready (all deps satisfied) let mut orders_ready: Vec = Vec::new(); - // Process each dependency this simulation satisfies + // Preserve develop's single-nonce-only provider tracking: only register nonce + // dependencies when the simulation updates exactly one nonce. Multi-nonce sims + // (e.g. bundles with multiple senders) are stored in `sims` but not tracked as + // providers — those orders wait until each nonce is individually satisfied. + // ACE unlock deps are always processed regardless of nonce count. + let nonce_dep_count = dependencies_satisfied + .iter() + .filter(|d| matches!(d, DependencyKey::Nonce(_))) + .count(); + for dep_key in dependencies_satisfied { + if matches!(dep_key, DependencyKey::Nonce(_)) && nonce_dep_count != 1 { + continue; + } match self.dependency_providers.entry(dep_key.clone()) { Entry::Occupied(mut entry) => { // Already have a provider - check if this one should replace it.