diff --git a/crates/op-rbuilder/src/tests/flashblocks.rs b/crates/op-rbuilder/src/tests/flashblocks.rs index c60e9d5a..2c0c2833 100644 --- a/crates/op-rbuilder/src/tests/flashblocks.rs +++ b/crates/op-rbuilder/src/tests/flashblocks.rs @@ -9,8 +9,9 @@ use std::time::Duration; use crate::{ args::{FlashblocksArgs, OpRbuilderArgs}, tests::{ - BlockTransactionsExt, BundleOpts, ChainDriver, FLASHBLOCKS_NUMBER_ADDRESS, LocalInstance, - TransactionBuilderExt, flashblocks_number_contract::FlashblocksNumber, + BlockTransactionsExt, BuilderTxValidation, BundleOpts, ChainDriver, + FLASHBLOCKS_NUMBER_ADDRESS, LocalInstance, TransactionBuilderExt, + flashblocks_number_contract::FlashblocksNumber, }, }; @@ -43,6 +44,10 @@ async fn smoke_dynamic_base(rbuilder: LocalInstance) -> eyre::Result<()> { } let block = driver.build_new_block_with_current_timestamp(None).await?; assert_eq!(block.transactions.len(), 8, "Got: {:?}", block.transactions); // 5 normal txn + deposit + 2 builder txn + + // Validate builder transactions using BuilderTxValidation + block.assert_builder_tx_count(2); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; } @@ -81,6 +86,10 @@ async fn smoke_dynamic_unichain(rbuilder: LocalInstance) -> eyre::Result<()> { } let block = driver.build_new_block_with_current_timestamp(None).await?; assert_eq!(block.transactions.len(), 8, "Got: {:?}", block.transactions); // 5 normal txn + deposit + 2 builder txn + + // Validate builder transactions using BuilderTxValidation + block.assert_builder_tx_count(2); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; } @@ -119,6 +128,10 @@ async fn smoke_classic_unichain(rbuilder: LocalInstance) -> eyre::Result<()> { } let block = driver.build_new_block().await?; assert_eq!(block.transactions.len(), 8, "Got: {:?}", block.transactions); // 5 normal txn + deposit + 2 builder txn + + // Validate builder transactions using BuilderTxValidation + block.assert_builder_tx_count(2); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; } @@ -157,6 +170,10 @@ async fn smoke_classic_base(rbuilder: LocalInstance) -> eyre::Result<()> { } let block = driver.build_new_block().await?; assert_eq!(block.transactions.len(), 8, "Got: {:?}", block.transactions); // 5 normal txn + deposit + 2 builder txn + + // Validate builder transactions using BuilderTxValidation + block.assert_builder_tx_count(2); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; } diff --git a/crates/op-rbuilder/src/tests/framework/utils.rs b/crates/op-rbuilder/src/tests/framework/utils.rs index 99772de1..1a1658c8 100644 --- a/crates/op-rbuilder/src/tests/framework/utils.rs +++ b/crates/op-rbuilder/src/tests/framework/utils.rs @@ -9,6 +9,7 @@ use crate::{ tx_signer::Signer, }; use alloy_eips::Encodable2718; +use alloy_network::TransactionResponse; use alloy_primitives::{Address, B256, BlockHash, TxHash, TxKind, U256, hex}; use alloy_rpc_types_eth::{Block, BlockTransactionHashes}; use alloy_sol_types::SolCall; @@ -272,10 +273,66 @@ impl ChainDriverExt for ChainDriver

{ } } +/// Result of builder transaction validation in a block. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BuilderTxInfo { + /// Number of builder transactions found in the block. + pub count: usize, + /// Indices of builder transactions within the block. + pub indices: Vec, +} + +impl BuilderTxInfo { + /// Returns true if the block contains at least one builder transaction. + pub fn has_builder_tx(&self) -> bool { + self.count > 0 + } +} + pub trait BlockTransactionsExt { fn includes(&self, txs: &impl AsTxs) -> bool; } +/// Extension trait for validating builder transactions in blocks. +pub trait BuilderTxValidation { + /// Checks if the block contains builder transactions from the configured builder address. + /// Returns information about builder transactions found in the block. + fn find_builder_txs(&self) -> BuilderTxInfo; + + /// Returns true if the block contains at least one builder transaction. + fn has_builder_tx(&self) -> bool { + self.find_builder_txs().has_builder_tx() + } + + /// Asserts that the block contains exactly the expected number of builder transactions. + fn assert_builder_tx_count(&self, expected: usize) { + let info = self.find_builder_txs(); + assert_eq!( + info.count, expected, + "Expected {} builder transaction(s), found {} at indices {:?}", + expected, info.count, info.indices + ); + } +} + +impl BuilderTxValidation for Block { + fn find_builder_txs(&self) -> BuilderTxInfo { + let builder_address = builder_signer().address; + let mut indices = Vec::new(); + + for (idx, tx) in self.transactions.txns().enumerate() { + if tx.from() == builder_address { + indices.push(idx); + } + } + + BuilderTxInfo { + count: indices.len(), + indices, + } + } +} + impl BlockTransactionsExt for Block { fn includes(&self, txs: &impl AsTxs) -> bool { txs.as_txs() diff --git a/crates/op-rbuilder/src/tests/revert.rs b/crates/op-rbuilder/src/tests/revert.rs index 76e1c5b9..8ed792c9 100644 --- a/crates/op-rbuilder/src/tests/revert.rs +++ b/crates/op-rbuilder/src/tests/revert.rs @@ -6,8 +6,8 @@ use crate::{ args::OpRbuilderArgs, primitives::bundle::MAX_BLOCK_RANGE_BLOCKS, tests::{ - BlockTransactionsExt, BundleOpts, ChainDriver, ChainDriverExt, LocalInstance, ONE_ETH, - OpRbuilderArgsTestExt, TransactionBuilderExt, + BlockTransactionsExt, BuilderTxValidation, BundleOpts, ChainDriver, ChainDriverExt, + LocalInstance, ONE_ETH, OpRbuilderArgsTestExt, TransactionBuilderExt, }, }; @@ -53,6 +53,14 @@ async fn monitor_transaction_gc(rbuilder: LocalInstance) -> eyre::Result<()> { assert_eq!(generated_block.transactions.len(), 3); } + // Validate builder transactions using BuilderTxValidation + if_standard! { + generated_block.assert_builder_tx_count(1); + } + if_flashblocks! { + generated_block.assert_builder_tx_count(2); + } + // since we created the 10 transactions with increasing block ranges, as we generate blocks // one transaction will be gc on each block. // transactions from [0, i] should be dropped, transactions from [i+1, 10] should be queued @@ -88,6 +96,9 @@ async fn disabled(rbuilder: LocalInstance) -> eyre::Result<()> { assert!(block.includes(valid_tx.tx_hash())); assert!(block.includes(reverting_tx.tx_hash())); + + // Validate builder transactions are present + assert!(block.has_builder_tx()); } Ok(()) @@ -396,6 +407,9 @@ async fn allow_reverted_transactions_without_bundle(rbuilder: LocalInstance) -> assert!(block.includes(valid_tx.tx_hash())); assert!(block.includes(reverting_tx.tx_hash())); + + // Validate builder transactions are present + assert!(block.has_builder_tx()); } Ok(()) diff --git a/crates/op-rbuilder/src/tests/smoke.rs b/crates/op-rbuilder/src/tests/smoke.rs index 63cfa77b..c379b51e 100644 --- a/crates/op-rbuilder/src/tests/smoke.rs +++ b/crates/op-rbuilder/src/tests/smoke.rs @@ -1,6 +1,6 @@ use crate::{ args::OpRbuilderArgs, - tests::{LocalInstance, TransactionBuilderExt}, + tests::{BuilderTxValidation, LocalInstance, TransactionBuilderExt}, }; use alloy_primitives::TxHash; @@ -34,6 +34,15 @@ async fn chain_produces_blocks(rbuilder: LocalInstance) -> eyre::Result<()> { // the deposit transaction and the block generator's transaction for _ in 0..SAMPLE_SIZE { let block = driver.build_new_block_with_current_timestamp(None).await?; + + // Validate builder transactions are present (must be done before moving transactions) + if_standard! { + block.assert_builder_tx_count(1); + } + if_flashblocks! { + block.assert_builder_tx_count(2); + } + let transactions = block.transactions; if_standard! { @@ -73,6 +82,14 @@ async fn chain_produces_blocks(rbuilder: LocalInstance) -> eyre::Result<()> { let block = driver.build_new_block_with_current_timestamp(None).await?; + // Validate builder transactions are present (must be done before moving transactions) + if_standard! { + block.assert_builder_tx_count(1); + } + if_flashblocks! { + block.assert_builder_tx_count(2); + } + let txs = block.transactions; if_standard! { @@ -223,6 +240,15 @@ async fn chain_produces_big_tx_with_gas_limit(rbuilder: LocalInstance) -> eyre:: .expect("Failed to send transaction"); let block = driver.build_new_block_with_current_timestamp(None).await?; + + // Validate builder transactions are present (must be done before moving transactions) + if_standard! { + block.assert_builder_tx_count(1); + } + if_flashblocks! { + block.assert_builder_tx_count(2); + } + let txs = block.transactions; if_standard! { @@ -272,6 +298,15 @@ async fn chain_produces_big_tx_without_gas_limit(rbuilder: LocalInstance) -> eyr .expect("Failed to send transaction"); let block = driver.build_new_block_with_current_timestamp(None).await?; + + // Validate builder transactions are present (must be done before moving transactions) + if_standard! { + block.assert_builder_tx_count(1); + } + if_flashblocks! { + block.assert_builder_tx_count(2); + } + let txs = block.transactions; // assert we included the tx @@ -296,3 +331,34 @@ async fn chain_produces_big_tx_without_gas_limit(rbuilder: LocalInstance) -> eyr Ok(()) } + +/// Validates that each block contains builder transactions using the +/// BuilderTxValidation utility. +#[rb_test] +async fn block_includes_builder_transaction(rbuilder: LocalInstance) -> eyre::Result<()> { + let driver = rbuilder.driver().await?; + + const SAMPLE_SIZE: usize = 5; + + for _ in 0..SAMPLE_SIZE { + let block = driver.build_new_block_with_current_timestamp(None).await?; + + // Validate that the block contains builder transactions + assert!( + block.has_builder_tx(), + "Block should contain at least one builder transaction" + ); + + // Standard builder: 1 builder tx + // Flashblocks builder: 2 builder txs (fallback + flashblock number) + if_standard! { + block.assert_builder_tx_count(1); + } + + if_flashblocks! { + block.assert_builder_tx_count(2); + } + } + + Ok(()) +}