Skip to content
Merged
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
168 changes: 166 additions & 2 deletions crates/op-rbuilder/src/primitives/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,54 @@ use alloy_rpc_types_eth::erc4337::TransactionConditional;
use reth_rpc_eth_types::EthApiError;
use serde::{Deserialize, Serialize};

/// Maximum number of blocks allowed in the block range for bundle execution.
///
/// This constant limits how far into the future a bundle can be scheduled to
/// prevent excessive resource usage and ensure timely execution. When no
/// maximum block number is specified, this value is added to the current block
/// number to set the default upper bound.
pub const MAX_BLOCK_RANGE_BLOCKS: u64 = 10;

/// A bundle represents a collection of transactions that should be executed
/// together with specific conditional constraints.
///
/// Bundles allow for sophisticated transaction ordering and conditional
/// execution based on block numbers, flashblock numbers, and timestamps. They
/// are a key primitive in MEV (Maximal Extractable Value) strategies and block
/// building.
///
/// # Validation
///
/// The following validations are performed before adding the transaction to the
/// mempool:
/// - Block number ranges are valid (min ≀ max)
/// - Maximum block numbers are not in the past
/// - Block ranges don't exceed `MAX_BLOCK_RANGE_BLOCKS` (currently 10)
/// - There's only one transaction in the bundle
/// - Flashblock number ranges are valid (min ≀ max)
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct Bundle {
/// List of raw transaction data to be included in the bundle.
///
/// Each transaction is represented as raw bytes that will be decoded and
/// executed in the specified order when the bundle conditions are met.
#[serde(rename = "txs")]
pub transactions: Vec<Bytes>,

/// Optional list of transaction hashes that are allowed to revert.
///
/// By default, if any transaction in a bundle reverts, the entire bundle is
/// considered invalid. This field allows specific transactions to revert
/// without invalidating the bundle, enabling more sophisticated MEV
/// strategies.
#[serde(rename = "revertingTxHashes")]
pub reverting_hashes: Option<Vec<B256>>,

/// Minimum block number at which this bundle can be included.
///
/// If specified, the bundle will only be considered for inclusion in blocks
/// at or after this block number. This allows for scheduling bundles for
/// future execution.
#[serde(
default,
rename = "minBlockNumber",
Expand All @@ -21,6 +59,11 @@ pub struct Bundle {
)]
pub block_number_min: Option<u64>,

/// Maximum block number at which this bundle can be included.
///
/// If specified, the bundle will be considered invalid for inclusion in
/// blocks after this block number. If not specified, defaults to the
/// current block number plus `MAX_BLOCK_RANGE_BLOCKS`.
#[serde(
default,
rename = "maxBlockNumber",
Expand All @@ -29,6 +72,11 @@ pub struct Bundle {
)]
pub block_number_max: Option<u64>,

/// Minimum flashblock number at which this bundle can be included.
///
/// Flashblocks are preconfirmations that are built incrementally. This
/// field along with `maxFlashblockNumber` allows bundles to be scheduled
/// for more precise execution.
#[serde(
default,
rename = "minFlashblockNumber",
Expand All @@ -37,6 +85,10 @@ pub struct Bundle {
)]
pub flashblock_number_min: Option<u64>,

/// Maximum flashblock number at which this bundle can be included.
///
/// Similar to `minFlashblockNumber`, this sets an upper bound on which
/// flashblocks can include this bundle.
#[serde(
default,
rename = "maxFlashblockNumber",
Expand All @@ -45,15 +97,25 @@ pub struct Bundle {
)]
pub flashblock_number_max: Option<u64>,

// Not recommended because this is subject to the builder node clock
/// Minimum timestamp (Unix epoch seconds) for bundle inclusion.
///
/// **Warning**: Not recommended for production use as it depends on the
/// builder node's clock, which may not be perfectly synchronized with
/// network time. Block number constraints are preferred for deterministic
/// behavior.
#[serde(
default,
rename = "minTimestamp",
skip_serializing_if = "Option::is_none"
)]
pub min_timestamp: Option<u64>,

// Not recommended because this is subject to the builder node clock
/// Maximum timestamp (Unix epoch seconds) for bundle inclusion.
///
/// **Warning**: Not recommended for production use as it depends on the
/// builder node's clock, which may not be perfectly synchronized with
/// network time. Block number constraints are preferred for deterministic
/// behavior.
#[serde(
default,
rename = "maxTimestamp",
Expand All @@ -74,6 +136,9 @@ pub enum BundleConditionalError {
MinGreaterThanMax { min: u64, max: u64 },
#[error("block_number_max ({max}) is a past block (current: {current})")]
MaxBlockInPast { max: u64, current: u64 },
/// To prevent resource exhaustion and ensure timely execution, bundles
/// cannot be scheduled more than `MAX_BLOCK_RANGE_BLOCKS` blocks into the
/// future.
#[error(
"block_number_max ({max}) is too high (current: {current}, max allowed: {max_allowed})"
)]
Expand All @@ -82,10 +147,15 @@ pub enum BundleConditionalError {
current: u64,
max_allowed: u64,
},
/// When no explicit maximum block number is provided, the system uses
/// `current_block + MAX_BLOCK_RANGE_BLOCKS` as the default maximum. This
/// error occurs when the specified minimum exceeds this default maximum.
#[error(
"block_number_min ({min}) is too high with default max range (max allowed: {max_allowed})"
)]
MinTooHighForDefaultRange { min: u64, max_allowed: u64 },
#[error("flashblock_number_min ({min}) is greater than flashblock_number_max ({max})")]
FlashblockMinGreaterThanMax { min: u64, max: u64 },
}

pub struct BundleConditional {
Expand Down Expand Up @@ -144,6 +214,15 @@ impl Bundle {
}
}

// Validate flashblock number range
if let Some(min) = self.flashblock_number_min {
if let Some(max) = self.flashblock_number_max {
if min > max {
return Err(BundleConditionalError::FlashblockMinGreaterThanMax { min, max });
}
}
}

Ok(BundleConditional {
transaction_conditional: TransactionConditional {
block_number_min,
Expand All @@ -158,8 +237,18 @@ impl Bundle {
}
}

/// Result returned after successfully submitting a bundle for inclusion.
///
/// This struct contains the unique identifier for the submitted bundle, which
/// can be used to track the bundle's status and inclusion in future blocks.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct BundleResult {
/// Transaction hash of the single transaction in the bundle.
///
/// This hash can be used to:
/// - Track bundle inclusion in blocks
/// - Query bundle status
/// - Reference the bundle in subsequent operations
#[serde(rename = "bundleHash")]
pub bundle_hash: B256,
}
Expand Down Expand Up @@ -334,4 +423,79 @@ mod tests {
assert_eq!(result.block_number_min, Some(999));
assert_eq!(result.block_number_max, Some(1010));
}

#[test]
fn test_bundle_conditional_flashblock_min_greater_than_max() {
let bundle = Bundle {
flashblock_number_min: Some(105),
flashblock_number_max: Some(100),
..Default::default()
};

let last_block = 1000;
let result = bundle.conditional(last_block);

assert!(matches!(
result,
Err(BundleConditionalError::FlashblockMinGreaterThanMax { min: 105, max: 100 })
));
}

#[test]
fn test_bundle_conditional_with_valid_flashblock_range() {
let bundle = Bundle {
flashblock_number_min: Some(100),
flashblock_number_max: Some(105),
..Default::default()
};

let last_block = 1000;
let result = bundle.conditional(last_block).unwrap();

assert_eq!(result.flashblock_number_min, Some(100));
assert_eq!(result.flashblock_number_max, Some(105));
}

#[test]
fn test_bundle_conditional_with_only_flashblock_min() {
let bundle = Bundle {
flashblock_number_min: Some(100),
..Default::default()
};

let last_block = 1000;
let result = bundle.conditional(last_block).unwrap();

assert_eq!(result.flashblock_number_min, Some(100));
assert_eq!(result.flashblock_number_max, None);
}

#[test]
fn test_bundle_conditional_with_only_flashblock_max() {
let bundle = Bundle {
flashblock_number_max: Some(105),
..Default::default()
};

let last_block = 1000;
let result = bundle.conditional(last_block).unwrap();

assert_eq!(result.flashblock_number_min, None);
assert_eq!(result.flashblock_number_max, Some(105));
}

#[test]
fn test_bundle_conditional_flashblock_equal_values() {
let bundle = Bundle {
flashblock_number_min: Some(100),
flashblock_number_max: Some(100),
..Default::default()
};

let last_block = 1000;
let result = bundle.conditional(last_block).unwrap();

assert_eq!(result.flashblock_number_min, Some(100));
assert_eq!(result.flashblock_number_max, Some(100));
}
}
Loading
Loading