From 0f738405688a1b2c1a0568ebbceb0ab937db54b6 Mon Sep 17 00:00:00 2001 From: Himess <95512809+Himess@users.noreply.github.com> Date: Sat, 13 Dec 2025 20:53:27 +0300 Subject: [PATCH 1/3] feat(tests): add BuilderTxValidation utility for validating builder transactions Adds `BuilderTxValidation` trait to test framework that provides: - `find_builder_txs()`: Returns info about builder transactions in a block - `has_builder_tx()`: Quick check if block contains builder transactions - `assert_builder_tx_count()`: Assert expected number of builder transactions Also adds `block_includes_builder_transaction` test demonstrating the utility. Closes #88 --- .../op-rbuilder/src/tests/framework/utils.rs | 56 +++++++++++++++++++ crates/op-rbuilder/src/tests/smoke.rs | 33 ++++++++++- 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/crates/op-rbuilder/src/tests/framework/utils.rs b/crates/op-rbuilder/src/tests/framework/utils.rs index 99772de1..6d79fd06 100644 --- a/crates/op-rbuilder/src/tests/framework/utils.rs +++ b/crates/op-rbuilder/src/tests/framework/utils.rs @@ -272,10 +272,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/smoke.rs b/crates/op-rbuilder/src/tests/smoke.rs index 63cfa77b..474911dd 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; @@ -296,3 +296,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(()) +} From a0bb51c8dfde5a8ea3e9c7c7e5fc0a492792222e Mon Sep 17 00:00:00 2001 From: Himess <95512809+Himess@users.noreply.github.com> Date: Wed, 17 Dec 2025 17:06:50 +0300 Subject: [PATCH 2/3] test: add BuilderTxValidation assertions to existing tests Updates smoke, flashblocks, and revert tests to use the BuilderTxValidation utility for validating builder transactions in blocks: - smoke.rs: Added builder tx count assertions to chain_produces_blocks, chain_produces_big_tx_with_gas_limit, and chain_produces_big_tx_without_gas_limit - flashblocks.rs: Added builder tx count assertions to smoke_dynamic_base, smoke_dynamic_unichain, smoke_classic_unichain, and smoke_classic_base - revert.rs: Added builder tx validation to monitor_transaction_gc, disabled, and allow_reverted_transactions_without_bundle tests --- crates/op-rbuilder/src/tests/flashblocks.rs | 21 ++++++++++-- .../op-rbuilder/src/tests/framework/utils.rs | 1 + crates/op-rbuilder/src/tests/revert.rs | 18 +++++++++-- crates/op-rbuilder/src/tests/smoke.rs | 32 +++++++++++++++++++ 4 files changed, 68 insertions(+), 4 deletions(-) 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 6d79fd06..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; 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 474911dd..06c2ddc1 100644 --- a/crates/op-rbuilder/src/tests/smoke.rs +++ b/crates/op-rbuilder/src/tests/smoke.rs @@ -53,6 +53,14 @@ async fn chain_produces_blocks(rbuilder: LocalInstance) -> eyre::Result<()> { "Empty blocks should have exactly three transactions" ); } + + // Validate builder transactions are present + if_standard! { + block.assert_builder_tx_count(1); + } + if_flashblocks! { + block.assert_builder_tx_count(2); + } } // ensure that transactions are included in blocks and each block has all the transactions @@ -103,6 +111,14 @@ async fn chain_produces_blocks(rbuilder: LocalInstance) -> eyre::Result<()> { tx_hash ); } + + // Validate builder transactions are present + if_standard! { + block.assert_builder_tx_count(1); + } + if_flashblocks! { + block.assert_builder_tx_count(2); + } } Ok(()) } @@ -249,6 +265,14 @@ async fn chain_produces_big_tx_with_gas_limit(rbuilder: LocalInstance) -> eyre:: let exclusion_result = txs.hashes().find(|hash| hash == tx_high_gas.tx_hash()); assert!(exclusion_result.is_none()); + // Validate builder transactions are present + if_standard! { + block.assert_builder_tx_count(1); + } + if_flashblocks! { + block.assert_builder_tx_count(2); + } + Ok(()) } @@ -294,6 +318,14 @@ async fn chain_produces_big_tx_without_gas_limit(rbuilder: LocalInstance) -> eyr ); } + // Validate builder transactions are present + if_standard! { + block.assert_builder_tx_count(1); + } + if_flashblocks! { + block.assert_builder_tx_count(2); + } + Ok(()) } From 68e4db1deb9cdb0a1d86447fd7de6cb6f5cdc901 Mon Sep 17 00:00:00 2001 From: Himess Date: Thu, 18 Dec 2025 21:57:22 +0300 Subject: [PATCH 3/3] fix: move builder tx validation before transactions move Fixes borrow of partially moved value error by calling assert_builder_tx_count before block.transactions is moved. --- crates/op-rbuilder/src/tests/smoke.rs | 67 ++++++++++++++------------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/crates/op-rbuilder/src/tests/smoke.rs b/crates/op-rbuilder/src/tests/smoke.rs index 06c2ddc1..c379b51e 100644 --- a/crates/op-rbuilder/src/tests/smoke.rs +++ b/crates/op-rbuilder/src/tests/smoke.rs @@ -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! { @@ -53,14 +62,6 @@ async fn chain_produces_blocks(rbuilder: LocalInstance) -> eyre::Result<()> { "Empty blocks should have exactly three transactions" ); } - - // Validate builder transactions are present - if_standard! { - block.assert_builder_tx_count(1); - } - if_flashblocks! { - block.assert_builder_tx_count(2); - } } // ensure that transactions are included in blocks and each block has all the transactions @@ -81,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! { @@ -111,14 +120,6 @@ async fn chain_produces_blocks(rbuilder: LocalInstance) -> eyre::Result<()> { tx_hash ); } - - // Validate builder transactions are present - if_standard! { - block.assert_builder_tx_count(1); - } - if_flashblocks! { - block.assert_builder_tx_count(2); - } } Ok(()) } @@ -239,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! { @@ -265,14 +275,6 @@ async fn chain_produces_big_tx_with_gas_limit(rbuilder: LocalInstance) -> eyre:: let exclusion_result = txs.hashes().find(|hash| hash == tx_high_gas.tx_hash()); assert!(exclusion_result.is_none()); - // Validate builder transactions are present - if_standard! { - block.assert_builder_tx_count(1); - } - if_flashblocks! { - block.assert_builder_tx_count(2); - } - Ok(()) } @@ -296,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 @@ -318,14 +329,6 @@ async fn chain_produces_big_tx_without_gas_limit(rbuilder: LocalInstance) -> eyr ); } - // Validate builder transactions are present - if_standard! { - block.assert_builder_tx_count(1); - } - if_flashblocks! { - block.assert_builder_tx_count(2); - } - Ok(()) }