Skip to content
Open
2,200 changes: 1,136 additions & 1,064 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ reth-provider = { git = "https://github.com/scroll-tech/reth.git", default-featu
reth-rpc-api = { git = "https://github.com/scroll-tech/reth.git", default-features = false }
reth-rpc-eth-api = { git = "https://github.com/scroll-tech/reth.git", default-features = false }
reth-rpc-eth-types = { git = "https://github.com/scroll-tech/reth.git", default-features = false }
reth-rpc-layer = { git = "https://github.com/scroll-tech/reth.git", default-features = false }
reth-rpc-server-types = { git = "https://github.com/scroll-tech/reth.git", default-features = false }
reth-storage-api = { git = "https://github.com/scroll-tech/reth.git", default-features = false }
reth-tasks = { git = "https://github.com/scroll-tech/reth.git", default-features = false }
Expand Down
11 changes: 11 additions & 0 deletions crates/node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,16 @@ aws-config = "1.8.0"
aws-sdk-kms = "1.76.0"

# test-utils
alloy-eips = { workspace = true, optional = true }
alloy-rpc-types-eth = { workspace = true, optional = true }
alloy-rpc-types-engine = { workspace = true, optional = true }
reth-e2e-test-utils = { workspace = true, optional = true }
reth-engine-local = { workspace = true, optional = true }
reth-provider = { workspace = true, optional = true }
reth-rpc-layer = { workspace = true, optional = true }
reth-rpc-server-types = { workspace = true, optional = true }
reth-storage-api = { workspace = true, optional = true }
reth-tokio-util = { workspace = true, optional = true }
scroll-alloy-rpc-types-engine = { workspace = true, optional = true }
scroll-alloy-rpc-types.workspace = true

Expand Down Expand Up @@ -108,6 +113,7 @@ reth-e2e-test-utils.workspace = true
reth-node-core.workspace = true
reth-provider.workspace = true
reth-primitives-traits.workspace = true
reth-rpc-layer.workspace = true
reth-rpc-server-types.workspace = true
reth-scroll-node = { workspace = true, features = ["test-utils"] }
reth-storage-api.workspace = true
Expand Down Expand Up @@ -140,10 +146,15 @@ test-utils = [
"rollup-node/test-utils",
"reth-e2e-test-utils",
"reth-rpc-server-types",
"reth-rpc-layer",
"reth-tokio-util",
"scroll-alloy-rpc-types-engine",
"alloy-rpc-types-engine",
"reth-primitives-traits/test-utils",
"reth-network-p2p/test-utils",
"rollup-node-chain-orchestrator/test-utils",
"scroll-network/test-utils",
"alloy-eips",
"reth-storage-api",
"alloy-rpc-types-eth",
]
171 changes: 171 additions & 0 deletions crates/node/src/test_utils/block_builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
//! Block building helpers for test fixtures.

use super::fixture::TestFixture;
use crate::test_utils::EventAssertions;

use alloy_primitives::B256;
use reth_primitives_traits::transaction::TxHashRef;
use reth_scroll_primitives::ScrollBlock;
use scroll_alloy_consensus::ScrollTransaction;

/// Builder for constructing and validating blocks in tests.
#[derive(Debug)]
pub struct BlockBuilder<'a> {
fixture: &'a mut TestFixture,
expected_tx_hashes: Vec<B256>,
expected_tx_count: Option<usize>,
Comment on lines +15 to +16
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these be mutually exclusive? I.e, maybe represented by an enum:

pub enum TransactionAssertion {
    Count(usize),
    Hashes(Vec<256>),
    None
}

I'm just wondering if we can construct an inconsistent state, i.e. BlockBuilder::new(..).expect_tx_count(3).expect_tx(tx_hash_1).expect_tx(tx_hash_2);

Edit: I suppose there is the case where we may only want to assert the hashes of a subset of the total number of transactions?

expected_base_fee: Option<u64>,
expected_block_number: Option<u64>,
expect_l1_message: bool,
expected_l1_message_count: Option<usize>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should expect_l1_message and expect_l1_message_count be mutually exclusive?

}

impl<'a> BlockBuilder<'a> {
/// Create a new block builder.
pub(crate) fn new(fixture: &'a mut TestFixture) -> Self {
Self {
fixture,
expected_tx_hashes: Vec::new(),
expected_tx_count: None,
expected_block_number: None,
expected_base_fee: None,
expect_l1_message: false,
expected_l1_message_count: None,
}
}

/// Expect a specific transaction to be included in the block.
pub fn expect_tx(mut self, tx_hash: B256) -> Self {
self.expected_tx_hashes.push(tx_hash);
self
}

/// Expect a specific number of transactions in the block.
pub const fn expect_tx_count(mut self, count: usize) -> Self {
self.expected_tx_count = Some(count);
self
}

/// Expect a specific block number.
pub const fn expect_block_number(mut self, number: u64) -> Self {
self.expected_block_number = Some(number);
self
}

/// Expect at least one L1 message in the block.
pub const fn expect_l1_message(mut self) -> Self {
self.expect_l1_message = true;
self
}

/// Expect a specific number of L1 messages in the block.
pub const fn expect_l1_message_count(mut self, count: usize) -> Self {
self.expected_l1_message_count = Some(count);
self
}

/// Build the block and validate against expectations.
pub async fn await_block(self) -> eyre::Result<ScrollBlock> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe build_and_await_block would be a more descriptive and accurate name for this function.

let sequencer_node = &self.fixture.nodes[0];

// Get the sequencer from the rollup manager handle
let handle = &sequencer_node.rollup_manager_handle;

// Trigger block building
handle.build_block();

// If extract the block number.
let expect = self.fixture.expect_event();
let block =
if let Some(b) = self.expected_block_number {
expect.block_sequenced(b).await?
} else {
expect.extract(|e| {
if let rollup_node_chain_orchestrator::ChainOrchestratorEvent::BlockSequenced(
block,
) = e
{
Some(block.clone())
} else {
None
}
}).await?
};

// Finally validate the block.
self.validate_block(&block)
}

/// Validate the block against expectations.
fn validate_block(self, block: &ScrollBlock) -> eyre::Result<ScrollBlock> {
// Check transaction count
if let Some(expected_count) = self.expected_tx_count {
if block.body.transactions.len() != expected_count {
return Err(eyre::eyre!(
"Expected {} transactions, but block has {}",
expected_count,
block.body.transactions.len()
));
}
}

// Check block number
if let Some(expected_number) = self.expected_block_number {
if block.header.number != expected_number {
return Err(eyre::eyre!(
"Expected {} number, but block has {}",
expected_number,
block.header.number
));
}
}

// Check specific transaction hashes
for expected_hash in &self.expected_tx_hashes {
if !block.body.transactions.iter().any(|tx| tx.tx_hash() == expected_hash) {
return Err(eyre::eyre!(
"Expected transaction {:?} not found in block",
expected_hash
));
}
}

// Check base fee
if let Some(expected_base_fee) = self.expected_base_fee {
let actual_base_fee = block
.header
.base_fee_per_gas
.ok_or_else(|| eyre::eyre!("Block has no base fee"))?;
if actual_base_fee != expected_base_fee {
return Err(eyre::eyre!(
"Expected base fee {}, but block has {}",
expected_base_fee,
actual_base_fee
));
}
}

// Check L1 messages
if self.expect_l1_message {
let l1_message_count =
block.body.transactions.iter().filter(|tx| tx.queue_index().is_some()).count();
if l1_message_count == 0 {
return Err(eyre::eyre!("Expected at least one L1 message, but block has none"));
}
}

if let Some(expected_count) = self.expected_l1_message_count {
let l1_message_count =
block.body.transactions.iter().filter(|tx| tx.queue_index().is_some()).count();
if l1_message_count != expected_count {
return Err(eyre::eyre!(
"Expected {} L1 messages, but block has {}",
expected_count,
l1_message_count
));
}
}

Ok(block.clone())
}
}
Loading