Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/rbuilder-primitives/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
94 changes: 94 additions & 0 deletions crates/rbuilder-primitives/src/ace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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 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
pub fn classify_ace_interaction(
&self,
state_trace: &UsedStateTrace,
sim_success: bool,
) -> Option<AceInteraction> {
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<AceInteraction> {
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,
}
8 changes: 5 additions & 3 deletions crates/rbuilder-primitives/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -40,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)]
Expand Down Expand Up @@ -1116,8 +1117,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() {
Expand Down Expand Up @@ -1362,6 +1362,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<UsedStateTrace>,
pub is_ace: bool,
pub ace_interaction: Option<AceInteraction>,
}

impl SimulatedOrder {
Expand Down
2 changes: 2 additions & 0 deletions crates/rbuilder-primitives/src/serialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,8 @@ pub enum RawOrderConvertError {
FailedToDecodeShareBundle(RawShareBundleConvertError),
#[error("Blobs not supported by RawOrder")]
BlobsNotSupported,
#[error("{0}")]
UnsupportedOrderType(String),
}

impl RawOrder {
Expand Down
1 change: 1 addition & 0 deletions crates/rbuilder/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions crates/rbuilder/src/bin/run-bundle-on-prefix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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
Expand Down
208 changes: 208 additions & 0 deletions crates/rbuilder/src/building/ace_collector.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
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<AceExchange, AceExchangeData>,
ace_tx_lookup: ahash::HashMap<AceExchange, AceConfig>,
}

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<AceOrderEntry>,
/// Optional ACE protocol tx - conditionally included
pub optional_ace_tx: Option<AceOrderEntry>,
/// 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<AceOrderEntry>,
}

#[derive(Debug, Clone)]
pub struct AceOrderEntry {
pub simulated: Arc<SimulatedOrder>,
/// Profit after bundle simulation
pub bundle_profit: U256,
}

impl AceExchangeData {
/// Add an ACE protocol transaction
fn add_ace_protocol_tx(
&mut self,
simulated: Arc<SimulatedOrder>,
unlock_type: AceUnlockType,
) -> Vec<SimulationRequest> {
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<SimulationRequest> {
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<SimulatedOrderCommand> {
// 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<SimulatedOrder>) -> Option<SimulationRequest> {
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<AceConfig>) -> 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<SimulatedOrder>,
unlock_type: AceUnlockType,
exchange: AceExchange,
) -> Vec<SimulationRequest> {
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<SimulatedOrderCommand> {
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<SimulatedOrder>,
interaction: AceInteraction,
) -> Option<SimulationRequest> {
self.exchanges
.entry(interaction.get_exchange())
.or_default()
.add_mempool_tx(simulated)
}

/// Get all configured exchanges
pub fn get_exchanges(&self) -> Vec<AceExchange> {
self.exchanges.keys().cloned().collect()
}

/// Clear all orders
pub fn clear(&mut self) {
self.exchanges.clear();
}
}
Loading