-
Notifications
You must be signed in to change notification settings - Fork 26
chore: add rebuild integration test for unrelated L2 txs #1107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
RomanBrodetski
merged 13 commits into
matter-labs:main
from
romanbrodetski-ai:upstream-main-pr3-rebuild-integration-test
Mar 31, 2026
+214
−0
Merged
Changes from 5 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
639e476
chore: add rebuild integration test for unrelated L2 txs
romanbrodetski-ai b995f46
test: wait 60s after funding before sending rebuild-scenario txs
romanbrodetski-ai 2c9380a
Revert "test: wait 60s after funding before sending rebuild-scenario …
romanbrodetski-ai 9120477
test: clarify rebuild block expectations
romanbrodetski-ai cbee95f
test: rename rebuild block count constant
romanbrodetski-ai 77c5519
test: disable batcher in rebuild scenario
romanbrodetski-ai 292ca93
test: inline previous rebuild block
romanbrodetski-ai 398d901
test: use block header hashes in rebuild test
romanbrodetski-ai ca9ca64
test: derive rebuild block after setup loop
romanbrodetski-ai f5f89e7
test: mine rebuild setup blocks explicitly
romanbrodetski-ai 761f31f
test: shorten rebuild integration scenario
romanbrodetski-ai f162a68
ci: stress-test rebuild integration test 20 times
romanbrodetski-ai c3b531b
ci: revert stress-test rebuild integration test
romanbrodetski-ai File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| mod batcher; | ||
| mod external_node; | ||
| mod mempool; | ||
| mod rebuild; | ||
| mod restart; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,210 @@ | ||
| use alloy::network::{EthereumWallet, TransactionBuilder}; | ||
| use alloy::primitives::{Address, U256}; | ||
| use alloy::providers::{Provider, ProviderBuilder}; | ||
| use alloy::rpc::types::TransactionRequest; | ||
| use alloy::signers::local::LocalSigner; | ||
| use anyhow::Context; | ||
| use backon::{ConstantBuilder, Retryable}; | ||
| use std::str::FromStr; | ||
| use std::time::Duration; | ||
| use std::time::Instant; | ||
| use zksync_os_integration_tests::assert_traits::ReceiptAssert; | ||
| use zksync_os_integration_tests::{CURRENT_TO_L1, Tester, test_multisetup}; | ||
| use zksync_os_server::config::RebuildBlocksConfig; | ||
|
|
||
| const BLOCKS_TO_PRODUCE_BEFORE_REBUILD: usize = 30; | ||
| const BLOCKS_FROM_TIP_TO_EMPTY: u64 = 10; | ||
|
|
||
| #[test_multisetup([CURRENT_TO_L1])] | ||
| #[test_runtime(flavor = "multi_thread")] | ||
| async fn rebuild_after_emptying_historical_block_preserves_unrelated_l2_txs() -> anyhow::Result<()> | ||
| { | ||
| let tester = Tester::builder() | ||
| .block_time(Duration::from_millis(50)) | ||
| .build() | ||
| .await?; | ||
|
|
||
| // This test empties an older block from the main sender, which makes that sender's later | ||
| // transactions invalid because their nonces become too high. A second sender contributes the | ||
| // last historical block so we can assert rebuild still reaches the tip and preserves | ||
| // unrelated L2 transactions. | ||
| let second_wallet = EthereumWallet::new(LocalSigner::from_str( | ||
| "0xac1e09fe4f8c7b2e9e13ab632d2f6a77b8cf57fb9f3f35e6c5c7d8f1b2a3c4d5", | ||
| )?); | ||
| let second_signer = ProviderBuilder::new() | ||
| .wallet(second_wallet.clone()) | ||
| .connect(tester.l2_rpc_url()) | ||
| .await | ||
| .context("failed to connect second signer to L2")?; | ||
| let second_address = second_wallet.default_signer().address(); | ||
|
|
||
| // Fund the second wallet so its transaction can remain valid after rebuild. | ||
| tester | ||
| .l2_provider | ||
| .send_transaction( | ||
| TransactionRequest::default() | ||
| .with_to(second_address) | ||
| .with_value(U256::from(1_000_000_000_000_000u64)), | ||
| ) | ||
| .await? | ||
| .expect_successful_receipt() | ||
| .await?; | ||
|
|
||
| let mut primary_last_block = 1; | ||
| for _ in 0..BLOCKS_TO_PRODUCE_BEFORE_REBUILD { | ||
| let receipt = tester | ||
| .l2_provider | ||
| .send_transaction( | ||
| TransactionRequest::default() | ||
| .with_to(Address::random()) | ||
| .with_value(U256::from(1u64)), | ||
| ) | ||
| .await? | ||
| .expect_successful_receipt() | ||
| .await?; | ||
| primary_last_block = receipt | ||
| .block_number | ||
| .expect("transfer receipt should have a block number"); | ||
| } | ||
| // Put the second sender into the last historical block so rebuild must preserve at least one | ||
| // unrelated transaction after emptying an older block from the primary sender. | ||
| let second_sender_receipt = second_signer | ||
| .send_transaction( | ||
| TransactionRequest::default() | ||
| .with_to(Address::random()) | ||
| .with_value(U256::from(1u64)), | ||
| ) | ||
| .await? | ||
| .expect_successful_receipt() | ||
| .await?; | ||
| let last_rebuilt_block = second_sender_receipt | ||
| .block_number | ||
| .expect("second sender receipt should have a block number"); | ||
| let block_to_empty = primary_last_block - BLOCKS_FROM_TIP_TO_EMPTY; | ||
| let block_before_empty = block_to_empty - 1; | ||
RomanBrodetski marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| let last_rebuilt_tx_hash = second_sender_receipt.transaction_hash; | ||
RomanBrodetski marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| let original_previous_block_hash = tester | ||
| .l2_provider | ||
| .get_block_by_number(block_before_empty.into()) | ||
| .await? | ||
| .context("previous block should exist")? | ||
| .header | ||
| .hash_slow(); | ||
|
|
||
| let original_emptied_block_hash = tester | ||
| .l2_provider | ||
| .get_block_by_number(block_to_empty.into()) | ||
| .await? | ||
| .context("original block should exist")? | ||
| .header | ||
| .hash_slow(); | ||
|
|
||
| let original_last_block_hash = tester | ||
| .l2_provider | ||
| .get_block_by_number(last_rebuilt_block.into()) | ||
| .await? | ||
| .context("last block should exist")? | ||
| .header | ||
| .hash_slow(); | ||
|
|
||
| let restarted = tester | ||
| .restart_with_overrides(|config| { | ||
RomanBrodetski marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| config.sequencer_config.block_rebuild = Some(RebuildBlocksConfig { | ||
| from_block: block_to_empty, | ||
| blocks_to_empty: vec![block_to_empty], | ||
| }); | ||
| }) | ||
| .await?; | ||
| let rebuild_started_at = Instant::now(); | ||
|
|
||
| let rebuilt_last_block = (|| async { | ||
| let rebuilt_last_block = restarted | ||
| .l2_provider | ||
| .get_block_by_number(last_rebuilt_block.into()) | ||
| .await? | ||
| .context("rebuilt last block should exist")?; | ||
| let rebuilt_last_block_hash = rebuilt_last_block.header.hash_slow(); | ||
|
|
||
| if rebuilt_last_block_hash != original_last_block_hash { | ||
| Ok(rebuilt_last_block) | ||
| } else { | ||
RomanBrodetski marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| anyhow::bail!( | ||
| "rebuild not finished yet: last_block={} hash={} original_hash={}", | ||
| last_rebuilt_block, | ||
| rebuilt_last_block_hash, | ||
| original_last_block_hash, | ||
| ); | ||
| } | ||
| }) | ||
| .retry( | ||
| ConstantBuilder::default() | ||
| .with_delay(Duration::from_millis(200)) | ||
| .with_max_times(100), | ||
| ) | ||
| .await?; | ||
|
|
||
| let rebuilt_emptied_block = restarted | ||
| .l2_provider | ||
| .get_block_by_number(block_to_empty.into()) | ||
| .await? | ||
| .context("rebuilt emptied block should exist")?; | ||
| let rebuilt_previous_block_hash = restarted | ||
| .l2_provider | ||
| .get_block_by_number(block_before_empty.into()) | ||
| .await? | ||
| .context("rebuilt previous block should exist")? | ||
| .header | ||
| .hash_slow(); | ||
| let rebuilt_emptied_block_tx_count = restarted | ||
| .l2_provider | ||
| .get_block_transaction_count_by_number(block_to_empty.into()) | ||
| .await? | ||
| .context("rebuilt emptied block tx count should exist")?; | ||
| let rebuilt_last_tx = restarted | ||
| .l2_provider | ||
| .get_transaction_by_hash(last_rebuilt_tx_hash) | ||
| .await? | ||
| .context("rebuilt last transaction should exist")?; | ||
| let rebuilt_emptied_block_hash = rebuilt_emptied_block.header.hash_slow(); | ||
| let rebuilt_last_block_hash = rebuilt_last_block.header.hash_slow(); | ||
| let rebuild_elapsed = rebuild_started_at.elapsed(); | ||
|
|
||
| assert_ne!( | ||
| rebuilt_emptied_block_hash, original_emptied_block_hash, | ||
| "emptied block should be rebuilt with a different hash" | ||
| ); | ||
| assert_eq!( | ||
| rebuilt_emptied_block_tx_count, 0, | ||
| "emptied block should be rebuilt without transactions" | ||
| ); | ||
| assert_eq!( | ||
| rebuilt_previous_block_hash, original_previous_block_hash, | ||
| "block before the emptied block should remain unchanged" | ||
| ); | ||
| assert_ne!( | ||
| rebuilt_last_block_hash, original_last_block_hash, | ||
| "last rebuilt block should have a different hash after rebuild" | ||
| ); | ||
| assert_eq!( | ||
| rebuilt_last_tx.block_number, | ||
| Some(last_rebuilt_block), | ||
| "unrelated transaction should remain in the rebuilt last block" | ||
| ); | ||
|
|
||
| tracing::info!( | ||
| block_number = last_rebuilt_block, | ||
| "Rebuild finished in {:?}: emptied block {} hash changed {} -> {} and now has {} txs; last rebuilt block {} hash changed {} -> {}; unrelated tx {} ended up in block {:?}", | ||
| rebuild_elapsed, // ~10s at the time of writing this test | ||
| block_to_empty, | ||
| original_emptied_block_hash, | ||
| rebuilt_emptied_block_hash, | ||
| rebuilt_emptied_block_tx_count, | ||
| last_rebuilt_block, | ||
| original_last_block_hash, | ||
| rebuilt_last_block_hash, | ||
| last_rebuilt_tx_hash, | ||
| rebuilt_last_tx.block_number, | ||
| ); | ||
| Ok(()) | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.