diff --git a/crates/op-rbuilder/src/args/op.rs b/crates/op-rbuilder/src/args/op.rs index bd860f155..f1f040606 100644 --- a/crates/op-rbuilder/src/args/op.rs +++ b/crates/op-rbuilder/src/args/op.rs @@ -166,6 +166,17 @@ pub struct FlashblocksArgs { env = "FLASHBLOCK_NUMBER_CONTRACT_ADDRESS" )] pub flashblocks_number_contract_address: Option
, + + /// 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 { diff --git a/crates/op-rbuilder/src/builders/flashblocks/config.rs b/crates/op-rbuilder/src/builders/flashblocks/config.rs index f2dca7759..cd587233c 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/config.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/config.rs @@ -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
, + + /// 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 { @@ -49,6 +55,7 @@ impl Default for FlashblocksConfig { fixed: false, calculate_state_root: true, flashblocks_number_contract_address: None, + max_flashblocks_per_block: 10, } } } @@ -73,6 +80,8 @@ impl TryFrom 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, @@ -80,6 +89,7 @@ impl TryFrom for FlashblocksConfig { fixed, calculate_state_root, flashblocks_number_contract_address, + max_flashblocks_per_block, }) } } @@ -93,6 +103,7 @@ impl FlashBlocksConfigExt for BuilderConfig { 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) } } diff --git a/crates/op-rbuilder/src/builders/flashblocks/payload.rs b/crates/op-rbuilder/src/builders/flashblocks/payload.rs index ae11a6da6..2a92ac1c8 100644 --- a/crates/op-rbuilder/src/builders/flashblocks/payload.rs +++ b/crates/op-rbuilder/src/builders/flashblocks/payload.rs @@ -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 { @@ -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, + ) } } diff --git a/crates/op-rbuilder/src/tests/flashblocks.rs b/crates/op-rbuilder/src/tests/flashblocks.rs index 8204af75f..f508e8936 100644 --- a/crates/op-rbuilder/src/tests/flashblocks.rs +++ b/crates/op-rbuilder/src/tests/flashblocks.rs @@ -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() })] @@ -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() })] @@ -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() })] @@ -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() })] @@ -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() })] @@ -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() })] @@ -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() })] @@ -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() })] @@ -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() })] @@ -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() })] @@ -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 +}