diff --git a/Cargo.lock b/Cargo.lock index 14c65d9f4..4f375eea6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9367,6 +9367,7 @@ dependencies = [ "sha2 0.10.9", "shellexpand", "sqlx", + "strum 0.27.2", "sysperf", "tempfile", "test_utils", @@ -9489,6 +9490,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-primitives/Cargo.toml b/crates/rbuilder-primitives/Cargo.toml index 207a3d434..0c3e3077d 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] alloy-primitives = { workspace = true, features = ["arbitrary"] } diff --git a/crates/rbuilder-primitives/src/ace.rs b/crates/rbuilder-primitives/src/ace.rs new file mode 100644 index 000000000..9c23ec304 --- /dev/null +++ b/crates/rbuilder-primitives/src/ace.rs @@ -0,0 +1,106 @@ +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, Deserialize, FromStr)] +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_some({ + 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 }, +} + +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 { + /// 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 d403c7c0c..c5ac852e7 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; @@ -40,7 +41,10 @@ pub use test_data_generator::TestDataGenerator; use thiserror::Error; use uuid::Uuid; -use crate::serialize::TxEncoding; +use crate::{ + ace::{AceExchange, AceInteraction, AceUnlockType}, + serialize::TxEncoding, +}; /// Extra metadata for an order. #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -1055,6 +1059,91 @@ 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 { @@ -1116,8 +1205,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(_) => vec![self], Order::ShareBundle(sb) => { let res = sb.original_orders(); if res.is_empty() { @@ -1362,6 +1450,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, + pub ace_interaction: Option, } impl SimulatedOrder { diff --git a/crates/rbuilder-primitives/src/serialize.rs b/crates/rbuilder-primitives/src/serialize.rs index 474e2a5bc..1a68a8a36 100644 --- a/crates/rbuilder-primitives/src/serialize.rs +++ b/crates/rbuilder-primitives/src/serialize.rs @@ -843,6 +843,8 @@ pub enum RawOrderConvertError { FailedToDecodeShareBundle(RawShareBundleConvertError), #[error("Blobs not supported by RawOrder")] BlobsNotSupported, + #[error("{0}")] + UnsupportedOrderType(String), } impl RawOrder { diff --git a/crates/rbuilder/Cargo.toml b/crates/rbuilder/Cargo.toml index 843c3d282..2602f6b4b 100644 --- a/crates/rbuilder/Cargo.toml +++ b/crates/rbuilder/Cargo.toml @@ -133,6 +133,7 @@ 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" diff --git a/crates/rbuilder/src/bin/run-bundle-on-prefix.rs b/crates/rbuilder/src/bin/run-bundle-on-prefix.rs index 01eb99b31..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,8 @@ 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(()))?; println!("{:?} {:?}", tx.hash(), res.is_ok()); @@ -315,6 +317,8 @@ fn execute_orders_on_tob( order: order_ts.order.clone(), 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 new file mode 100644 index 000000000..8e5dd117a --- /dev/null +++ b/crates/rbuilder/src/building/ace_collector.rs @@ -0,0 +1,224 @@ +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(&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 +#[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 + pub 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() + } + + pub 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. + 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 + .take() + .map(|order| SimulatedOrderCommand::Cancellation(order.simulated.order.id())) + } + + 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. + + 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(&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, + 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 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 2c37e5e9c..58887b4a2 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,8 @@ 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, + 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 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/block_orders/test_data_generator.rs b/crates/rbuilder/src/building/block_orders/test_data_generator.rs index c792a439d..281c8ff55 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,8 @@ impl TestDataGenerator { order, 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 da5d5bbd0..ab91929ec 100644 --- a/crates/rbuilder/src/building/builders/ordering_builder.rs +++ b/crates/rbuilder/src/building/builders/ordering_builder.rs @@ -282,6 +282,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 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()); + } + } + let mut block_building_helper = BlockBuildingHelperFromProvider::new_with_execution_tracer( built_block_id, self.state.clone(), @@ -294,6 +306,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..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 @@ -186,6 +186,25 @@ 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 order.is_ace { + 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, _)| !group.orders[*order_idx].is_ace); + } + let mut block_building_helper = BlockBuildingHelperFromProvider::new( self.built_block_id_source.get_new_id(), self.state.clone(), @@ -199,6 +218,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 +292,28 @@ 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 order.is_ace { + 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, _)| !group.orders[*order_idx].is_ace); + } + let mut block_building_helper = BlockBuildingHelperFromProvider::new( self.built_block_id_source.get_new_id(), self.state.clone(), @@ -275,8 +328,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/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/mod.rs b/crates/rbuilder/src/building/mod.rs index ba74d5a45..fe513d798 100644 --- a/crates/rbuilder/src/building/mod.rs +++ b/crates/rbuilder/src/building/mod.rs @@ -74,6 +74,7 @@ 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/order_commit.rs b/crates/rbuilder/src/building/order_commit.rs index c45ab67a5..7bb4ac814 100644 --- a/crates/rbuilder/src/building/order_commit.rs +++ b/crates/rbuilder/src/building/order_commit.rs @@ -15,6 +15,7 @@ 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, @@ -280,6 +281,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}")] diff --git a/crates/rbuilder/src/building/sim.rs b/crates/rbuilder/src/building/sim.rs index 2e70fd412..5e7447850 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)] @@ -105,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(()); @@ -422,10 +431,67 @@ 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 has_parents = !parent_orders.is_empty(); + 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 { + // 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); + } + } + } + Ok(OrderSimResultWithGas { result: sim_res, gas_used: tracer.used_gas, @@ -472,6 +538,8 @@ pub fn simulate_order_using_fork( order, sim_value, used_state_trace: res.used_state_trace, + ace_interaction: None, + is_ace: false, }), new_nonces, )) 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/base_config.rs b/crates/rbuilder/src/live_builder/base_config.rs index 733121e74..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 { @@ -270,6 +273,7 @@ impl BaseConfig { simulation_use_random_coinbase: self.simulation_use_random_coinbase, faster_finalize: self.faster_finalize, order_flow_tracer_manager, + ace_config: self.ace_protocols.clone(), }) } @@ -487,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/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..4c6d9d01b 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 { 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 b1bff3c57..41cf93d0f 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_collector::AceCollector; use std::{fmt, sync::Arc}; use crate::{ @@ -38,6 +39,7 @@ 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, @@ -76,8 +78,10 @@ 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, @@ -183,6 +187,43 @@ 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(&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 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 @@ -198,10 +239,21 @@ 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; } + + 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 .in_flight_orders @@ -223,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); } } } @@ -320,7 +372,7 @@ impl fmt::Debug for OrderCounter { self.total(), self.mempool_txs, self.bundles, - self.share_bundles + self.share_bundles, ) } } diff --git a/crates/rbuilder/src/mev_boost/mod.rs b/crates/rbuilder/src/mev_boost/mod.rs index 0b618ba6d..507be6ad7 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}; @@ -769,6 +771,29 @@ impl RelayClient { if let Some(top_competitor_bid) = metadata.value.top_competitor_bid { builder = builder.header(TOP_BID_HEADER, top_competitor_bid.to_string()); } + if !metadata.order_ids.is_empty() { + const MAX_BUNDLE_IDS: usize = 150; + let bundle_ids: Vec<_> = metadata + .order_ids + .iter() + .filter_map(|order| match order { + rbuilder_primitives::OrderId::Tx(_fixed_bytes) => None, + rbuilder_primitives::OrderId::Bundle(uuid) => Some(uuid), + rbuilder_primitives::OrderId::ShareBundle(_fixed_bytes) => None, + }) + .collect(); + let total_bundles = bundle_ids.len(); + let mut bundle_ids = bundle_ids + .iter() + .take(MAX_BUNDLE_IDS) + .map(|uuid| format!("{uuid:?}")); + let bundle_ids = if total_bundles > MAX_BUNDLE_IDS { + bundle_ids.join(",") + ",CAPPED" + } else { + bundle_ids.join(",") + }; + builder = builder.header(BUNDLE_HASHES_HEADER, bundle_ids); + } const MAX_BUNDLE_HASHES: usize = 150; if !metadata.bundle_hashes.is_empty() { diff --git a/examples/config/rbuilder/config-live-example.toml b/examples/config/rbuilder/config-live-example.toml index 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" -