Skip to content
Closed
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
11 changes: 11 additions & 0 deletions crates/op-rbuilder/src/args/op.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,17 @@ pub struct FlashblocksArgs {
env = "FLASHBLOCK_NUMBER_CONTRACT_ADDRESS"
)]
pub flashblocks_number_contract_address: Option<Address>,

/// Maximum number of flashblocks per block
///
/// This caps the maximum number of flashblocks that can be produced per block,
/// regardless of the calculated number based on timing.
#[arg(
long = "flashblocks.max-per-block",
env = "FLASHBLOCKS_MAX_PER_BLOCK",
default_value = "10"
)]
pub max_flashblocks_per_block: u64,
}

impl Default for FlashblocksArgs {
Expand Down
13 changes: 12 additions & 1 deletion crates/op-rbuilder/src/builders/flashblocks/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ pub struct FlashblocksConfig {
///
/// If set a builder tx will be added to the start of every flashblock instead of the regular builder tx.
pub flashblocks_number_contract_address: Option<Address>,

/// Maximum number of flashblocks per block.
///
/// This caps the maximum number of flashblocks that can be produced per block,
/// regardless of the calculated number based on timing.
pub max_flashblocks_per_block: u64,
}

impl Default for FlashblocksConfig {
Expand All @@ -49,6 +55,7 @@ impl Default for FlashblocksConfig {
fixed: false,
calculate_state_root: true,
flashblocks_number_contract_address: None,
max_flashblocks_per_block: 10,
}
}
}
Expand All @@ -73,13 +80,16 @@ impl TryFrom<OpRbuilderArgs> for FlashblocksConfig {
let flashblocks_number_contract_address =
args.flashblocks.flashblocks_number_contract_address;

let max_flashblocks_per_block = args.flashblocks.max_flashblocks_per_block;

Ok(Self {
ws_addr,
interval,
leeway_time,
fixed,
calculate_state_root,
flashblocks_number_contract_address,
max_flashblocks_per_block,
})
}
}
Expand All @@ -93,6 +103,7 @@ impl FlashBlocksConfigExt for BuilderConfig<FlashblocksConfig> {
if self.block_time.as_millis() == 0 {
return 0;
}
(self.block_time.as_millis() / self.specific.interval.as_millis()) as u64
let calculated = (self.block_time.as_millis() / self.specific.interval.as_millis()) as u64;
calculated.min(self.specific.max_flashblocks_per_block)
}
}
10 changes: 8 additions & 2 deletions crates/op-rbuilder/src/builders/flashblocks/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -853,7 +853,7 @@ where
let interval = self.config.specific.interval.as_millis() as u64;
let time_drift = time_drift.as_millis() as u64;
let first_flashblock_offset = time_drift.rem(interval);
if first_flashblock_offset == 0 {
let (calculated_flashblocks, first_offset) = if first_flashblock_offset == 0 {
// We have perfect division, so we use interval as first fb offset
(time_drift.div(interval), Duration::from_millis(interval))
} else {
Expand All @@ -862,7 +862,13 @@ where
time_drift.div(interval) + 1,
Duration::from_millis(first_flashblock_offset),
)
}
};

// Apply the maximum flashblocks per block cap
(
calculated_flashblocks.min(self.config.specific.max_flashblocks_per_block),
first_offset,
)
}
}

Expand Down
124 changes: 124 additions & 0 deletions crates/op-rbuilder/src/tests/flashblocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use crate::{
flashblocks_fixed: false,
flashblocks_calculate_state_root: true,
flashblocks_number_contract_address: None,
max_flashblocks_per_block: 10,
},
..Default::default()
})]
Expand Down Expand Up @@ -57,6 +58,7 @@ async fn smoke_dynamic_base(rbuilder: LocalInstance) -> eyre::Result<()> {
flashblocks_fixed: false,
flashblocks_calculate_state_root: true,
flashblocks_number_contract_address: None,
max_flashblocks_per_block: 10,
},
..Default::default()
})]
Expand Down Expand Up @@ -96,6 +98,7 @@ async fn smoke_dynamic_unichain(rbuilder: LocalInstance) -> eyre::Result<()> {
flashblocks_fixed: true,
flashblocks_calculate_state_root: true,
flashblocks_number_contract_address: None,
max_flashblocks_per_block: 10,
},
..Default::default()
})]
Expand Down Expand Up @@ -135,6 +138,7 @@ async fn smoke_classic_unichain(rbuilder: LocalInstance) -> eyre::Result<()> {
flashblocks_fixed: true,
flashblocks_calculate_state_root: true,
flashblocks_number_contract_address: None,
max_flashblocks_per_block: 10,
},
..Default::default()
})]
Expand Down Expand Up @@ -174,6 +178,7 @@ async fn smoke_classic_base(rbuilder: LocalInstance) -> eyre::Result<()> {
flashblocks_fixed: false,
flashblocks_calculate_state_root: true,
flashblocks_number_contract_address: None,
max_flashblocks_per_block: 10,
},
..Default::default()
})]
Expand Down Expand Up @@ -220,6 +225,7 @@ async fn unichain_dynamic_with_lag(rbuilder: LocalInstance) -> eyre::Result<()>
flashblocks_fixed: false,
flashblocks_calculate_state_root: true,
flashblocks_number_contract_address: None,
max_flashblocks_per_block: 10,
},
..Default::default()
})]
Expand Down Expand Up @@ -259,6 +265,7 @@ async fn dynamic_with_full_block_lag(rbuilder: LocalInstance) -> eyre::Result<()
flashblocks_fixed: false,
flashblocks_calculate_state_root: true,
flashblocks_number_contract_address: None,
max_flashblocks_per_block: 10,
},
..Default::default()
})]
Expand Down Expand Up @@ -320,6 +327,7 @@ async fn test_flashblock_min_filtering(rbuilder: LocalInstance) -> eyre::Result<
flashblocks_fixed: false,
flashblocks_calculate_state_root: true,
flashblocks_number_contract_address: None,
max_flashblocks_per_block: 10,
},
..Default::default()
})]
Expand Down Expand Up @@ -377,6 +385,7 @@ async fn test_flashblock_max_filtering(rbuilder: LocalInstance) -> eyre::Result<
flashblocks_fixed: false,
flashblocks_calculate_state_root: true,
flashblocks_number_contract_address: None,
max_flashblocks_per_block: 10,
},
..Default::default()
})]
Expand Down Expand Up @@ -423,6 +432,7 @@ async fn test_flashblock_min_max_filtering(rbuilder: LocalInstance) -> eyre::Res
flashblocks_fixed: false,
flashblocks_calculate_state_root: false,
flashblocks_number_contract_address: None,
max_flashblocks_per_block: 10,
},
..Default::default()
})]
Expand Down Expand Up @@ -456,3 +466,117 @@ async fn test_flashblocks_no_state_root_calculation(rbuilder: LocalInstance) ->

Ok(())
}

#[rb_test(flashblocks, args = OpRbuilderArgs {
chain_block_time: 1000,
flashblocks: FlashblocksArgs {
enabled: true,
flashblocks_port: 1239,
flashblocks_addr: "127.0.0.1".into(),
flashblocks_block_time: 200,
flashblocks_leeway_time: 100,
flashblocks_fixed: true,
flashblocks_calculate_state_root: true,
flashblocks_number_contract_address: None,
max_flashblocks_per_block: 3, // Cap at 3 flashblocks instead of default 5
},
..Default::default()
})]
async fn test_max_flashblocks_per_block_cap(rbuilder: LocalInstance) -> eyre::Result<()> {
let driver = rbuilder.driver().await?;
let flashblocks_listener = rbuilder.spawn_flashblocks_listener();

// Send transactions to ensure blocks have activity
for _ in 0..5 {
let _ = driver
.create_transaction()
.random_valid_transfer()
.send()
.await?;
}

// Build a block - normally would produce 5 flashblocks (1000ms / 200ms = 5)
// But with max_flashblocks_per_block: 3, it should cap at 3
let block = driver.build_new_block().await?;
assert_eq!(
block.transactions.len(),
8,
"Block should contain all transactions"
); // 5 normal txn + deposit + 2 builder txn

let flashblocks = flashblocks_listener.get_flashblocks();
// Should produce only 3 flashblocks + 1 base flashblock = 4 total
assert_eq!(
4,
flashblocks.len(),
"Should be capped at 3 flashblocks + 1 base = 4 total"
);

// Verify flashblock indices are within expected range
for fb in &flashblocks {
assert!(
fb.index <= 3,
"Flashblock index should not exceed 3, got index {}",
fb.index
);
}

flashblocks_listener.stop().await
}

#[rb_test(flashblocks, args = OpRbuilderArgs {
chain_block_time: 2000,
flashblocks: FlashblocksArgs {
enabled: true,
flashblocks_port: 1239,
flashblocks_addr: "127.0.0.1".into(),
flashblocks_block_time: 100, // Short interval would normally create 20 flashblocks
flashblocks_leeway_time: 50,
flashblocks_fixed: false,
flashblocks_calculate_state_root: true,
flashblocks_number_contract_address: None,
max_flashblocks_per_block: 5, // Cap at 5 flashblocks
},
..Default::default()
})]
async fn test_max_flashblocks_dynamic_timing(rbuilder: LocalInstance) -> eyre::Result<()> {
let driver = rbuilder.driver().await?;
let flashblocks_listener = rbuilder.spawn_flashblocks_listener();

// Send transactions
for _ in 0..3 {
let _ = driver
.create_transaction()
.random_valid_transfer()
.send()
.await?;
}

// Build block with current timestamp (dynamic timing)
// Would normally create ~20 flashblocks (2000ms / 100ms = 20)
// But should be capped at 5
let block = driver.build_new_block_with_current_timestamp(None).await?;
assert!(
block.transactions.len() >= 5,
"Block should contain transactions"
);

let flashblocks = flashblocks_listener.get_flashblocks();
// Should be capped at 5 flashblocks + 1 base = 6 total
assert!(
flashblocks.len() <= 6,
"Should be capped at 5 flashblocks + 1 base = 6 total, got {}",
flashblocks.len()
);

// Verify no flashblock index exceeds the cap
for fb in &flashblocks {
assert!(
fb.index <= 5,
"Flashblock index should not exceed 5, got index {}",
fb.index
);
}

flashblocks_listener.stop().await
}
Loading