From d1ddf4253aaa2113176f9d82e6905d92ec2c63b1 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Wed, 15 Oct 2025 11:24:25 -0400 Subject: [PATCH 01/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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"