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
104 changes: 104 additions & 0 deletions crates/rbuilder-primitives/src/ace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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<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,
}
1 change: 1 addition & 0 deletions crates/rbuilder-primitives/src/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub fn write_order<Buffer: Write>(
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)
Expand Down
118 changes: 110 additions & 8 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,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)]
Expand Down Expand Up @@ -1055,12 +1059,98 @@ 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<u64> {
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<Nonce> {
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 {
Bundle(Bundle),
Tx(MempoolTx),
ShareBundle(ShareBundle),
AceTx(AceTx),
}

/// Uniquely identifies a replaceable sbundle
Expand Down Expand Up @@ -1107,6 +1197,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.can_execute_with_block_base_fee(block_base_fee),
}
}

Expand All @@ -1116,8 +1207,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() {
Expand All @@ -1139,6 +1229,7 @@ impl Order {
address: tx.tx_with_blobs.tx.signer(),
optional: false,
}],
Order::AceTx(tx) => tx.nonces(),
Order::ShareBundle(bundle) => bundle.nonces(),
}
}
Expand All @@ -1147,6 +1238,7 @@ 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),
}
}
Expand All @@ -1167,6 +1259,7 @@ 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(),
}
}
Expand All @@ -1177,6 +1270,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) => tx.list_txs_revert(),
Order::ShareBundle(bundle) => bundle.list_txs_revert(),
}
}
Expand All @@ -1185,7 +1279,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(),
}
}
Expand All @@ -1203,7 +1297,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),
Expand All @@ -1220,7 +1314,7 @@ impl Order {
pub fn target_block(&self) -> Option<u64> {
match self {
Order::Bundle(bundle) => bundle.block,
Order::Tx(_) => None,
Order::AceTx(_) | Order::Tx(_) => None,
Order::ShareBundle(bundle) => Some(bundle.block),
}
}
Expand All @@ -1230,14 +1324,15 @@ impl Order {
match self {
Order::Bundle(bundle) => bundle.signer,
Order::ShareBundle(bundle) => bundle.signer,
Order::Tx(_) => None,
Order::AceTx(_) | Order::Tx(_) => None,
}
}

pub fn metadata(&self) -> &Metadata {
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,
}
}
Expand Down Expand Up @@ -1362,6 +1457,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<UsedStateTrace>,
pub ace_interaction: Option<AceInteraction>,
}

impl SimulatedOrder {
Expand All @@ -1381,12 +1477,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());
Expand Down Expand Up @@ -1417,6 +1514,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"))
}
Expand All @@ -1430,6 +1530,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:?}"),
}
}
}
Expand All @@ -1444,6 +1545,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,
Expand Down
Loading