1use alloy_consensus::{TxEnvelope, TxReceipt};
9use alloy_eips::{
10 eip1559::BaseFeeParams,
11 eip7840::BlobParams,
12 eip7928::{AccountChanges, BlockAccessList, SlotChanges},
13 BlockNumberOrTag, Typed2718,
14};
15use alloy_primitives::{Bloom, Bytes, B256};
16use alloy_provider::{network::AnyNetwork, Provider, RootProvider};
17use alloy_rpc_client::ClientBuilder;
18use alloy_rpc_types_engine::{
19 CancunPayloadFields, ExecutionData, ExecutionPayload, ExecutionPayloadSidecar,
20 PraguePayloadFields,
21};
22use clap::Parser;
23use eyre::Context;
24use reth_chainspec::EthChainSpec;
25use reth_cli::chainspec::ChainSpecParser;
26use reth_cli_runner::CliContext;
27use reth_engine_primitives::BigBlockData;
28use reth_ethereum_cli::chainspec::EthereumChainSpecParser;
29use reth_ethereum_primitives::Receipt;
30use reth_primitives_traits::proofs;
31use serde::{Deserialize, Serialize};
32use std::{collections::HashMap, future::Future};
33use tracing::{info, warn};
34
35#[derive(Debug, Clone)]
37pub struct RawTransaction {
38 pub gas_used: u64,
40 pub tx_type: u8,
42 pub raw: Bytes,
44}
45
46pub trait TransactionSource {
50 fn fetch_block_transactions(
55 &self,
56 block_number: u64,
57 ) -> impl Future<Output = eyre::Result<Option<(Vec<RawTransaction>, u64)>>> + Send;
58}
59
60#[derive(Debug)]
62pub struct RpcTransactionSource {
63 provider: RootProvider<AnyNetwork>,
64}
65
66impl RpcTransactionSource {
67 pub const fn new(provider: RootProvider<AnyNetwork>) -> Self {
69 Self { provider }
70 }
71
72 pub fn from_url(rpc_url: &str) -> eyre::Result<Self> {
74 let client = ClientBuilder::default()
75 .layer(alloy_transport::layers::RetryBackoffLayer::new(10, 800, u64::MAX))
76 .http(rpc_url.parse()?);
77 let provider = RootProvider::<AnyNetwork>::new(client);
78 Ok(Self { provider })
79 }
80}
81
82impl TransactionSource for RpcTransactionSource {
83 async fn fetch_block_transactions(
84 &self,
85 block_number: u64,
86 ) -> eyre::Result<Option<(Vec<RawTransaction>, u64)>> {
87 let (block, receipts) = tokio::try_join!(
89 self.provider.get_block_by_number(block_number.into()).full(),
90 self.provider.get_block_receipts(block_number.into())
91 )?;
92
93 let Some(block) = block else {
94 return Ok(None);
95 };
96
97 let Some(receipts) = receipts else {
98 return Err(eyre::eyre!("Receipts not found for block {}", block_number));
99 };
100
101 let block_gas_used = block.header.gas_used;
102
103 let mut prev_cumulative = 0u64;
105 let transactions: Vec<RawTransaction> = block
106 .transactions
107 .txns()
108 .zip(receipts.iter())
109 .map(|(tx, receipt)| {
110 let cumulative = receipt.inner.inner.inner.receipt.cumulative_gas_used;
111 let gas_used = cumulative - prev_cumulative;
112 prev_cumulative = cumulative;
113
114 let with_encoded = tx.inner.inner.clone().into_encoded();
115 RawTransaction {
116 gas_used,
117 tx_type: tx.inner.ty(),
118 raw: with_encoded.encoded_bytes().clone(),
119 }
120 })
121 .collect();
122
123 Ok(Some((transactions, block_gas_used)))
124 }
125}
126
127#[derive(Debug)]
129pub struct TransactionCollector<S> {
130 source: S,
131 target_gas: u64,
132}
133
134impl<S: TransactionSource> TransactionCollector<S> {
135 pub const fn new(source: S, target_gas: u64) -> Self {
137 Self { source, target_gas }
138 }
139
140 pub async fn collect(&self, start_block: u64) -> eyre::Result<CollectionResult> {
145 self.collect_gas(start_block, self.target_gas).await
146 }
147
148 pub async fn collect_gas(
152 &self,
153 start_block: u64,
154 gas_target: u64,
155 ) -> eyre::Result<CollectionResult> {
156 let mut transactions: Vec<RawTransaction> = Vec::new();
157 let mut total_gas: u64 = 0;
158 let mut current_block = start_block;
159
160 while total_gas < gas_target {
161 let Some((block_txs, _)) = self.source.fetch_block_transactions(current_block).await?
162 else {
163 tracing::warn!(target: "reth-bench", block = current_block, "Block not found, stopping");
164 break;
165 };
166
167 for tx in block_txs {
168 if tx.tx_type == 3 {
170 continue;
171 }
172
173 if total_gas + tx.gas_used <= gas_target {
174 total_gas += tx.gas_used;
175 transactions.push(tx);
176 }
177
178 if total_gas >= gas_target {
179 break;
180 }
181 }
182
183 current_block += 1;
184
185 let remaining_gas = gas_target.saturating_sub(total_gas);
187 if remaining_gas < 1_000_000 {
188 break;
189 }
190 }
191
192 info!(
193 target: "reth-bench",
194 total_txs = transactions.len(),
195 gas_sent = total_gas,
196 next_block = current_block,
197 "Finished collecting transactions"
198 );
199
200 Ok(CollectionResult { transactions, gas_sent: total_gas, next_block: current_block })
201 }
202}
203
204#[derive(Debug)]
206pub struct CollectionResult {
207 pub transactions: Vec<RawTransaction>,
209 pub gas_sent: u64,
211 pub next_block: u64,
213}
214
215#[derive(Debug, Serialize, Deserialize)]
217pub struct BigBlockPayload {
218 pub execution_data: ExecutionData,
220 #[serde(default)]
222 pub big_block_data: BigBlockData<ExecutionData>,
223 #[serde(default, skip_serializing_if = "Option::is_none")]
225 pub block_access_list: Option<BlockAccessList>,
226}
227
228#[derive(Debug, Parser)]
233pub struct Command {
234 #[arg(long, value_name = "RPC_URL")]
236 rpc_url: String,
237
238 #[arg(long, value_name = "CHAIN", default_value = "mainnet")]
240 chain: String,
241
242 #[arg(long, value_name = "FROM_BLOCK")]
244 from_block: u64,
245
246 #[arg(long, value_name = "TARGET_GAS", value_parser = super::helpers::parse_gas_limit)]
250 target_gas: u64,
251
252 #[arg(long, value_name = "NUM_BIG_BLOCKS", default_value = "1")]
258 num_big_blocks: u64,
259
260 #[arg(long, value_name = "OUTPUT_DIR")]
262 output_dir: std::path::PathBuf,
263
264 #[arg(long, default_value_t = false)]
267 bal: bool,
268}
269
270impl Command {
271 pub async fn execute(self, _ctx: CliContext) -> eyre::Result<()> {
273 if self.target_gas == 0 {
274 return Err(eyre::eyre!("--target-gas must be greater than 0"));
275 }
276 if self.num_big_blocks == 0 {
277 return Err(eyre::eyre!("--num-big-blocks must be at least 1"));
278 }
279
280 let chain_spec = EthereumChainSpecParser::parse(&self.chain)
282 .wrap_err_with(|| format!("Failed to parse chain spec: {}", self.chain))?;
283
284 info!(
285 target: "reth-bench",
286 from_block = self.from_block,
287 target_gas = self.target_gas,
288 num_big_blocks = self.num_big_blocks,
289 include_bal = self.bal,
290 chain = %chain_spec.chain(),
291 output_dir = %self.output_dir.display(),
292 "Generating big block payloads"
293 );
294
295 std::fs::create_dir_all(&self.output_dir).wrap_err_with(|| {
297 format!("Failed to create output directory: {:?}", self.output_dir)
298 })?;
299
300 let client = ClientBuilder::default()
302 .layer(alloy_transport::layers::RetryBackoffLayer::new(10, 800, u64::MAX))
303 .http(self.rpc_url.parse()?);
304 let provider = RootProvider::<AnyNetwork>::new(client);
305
306 let mut prev_big_block_hash: Option<B256> = None;
307 let mut accumulated_block_hashes: Vec<(u64, B256)> = Vec::new();
308
309 struct PrevBigBlockHeader {
312 gas_used: u64,
313 gas_limit: u64,
314 base_fee_per_gas: u64,
315 blob_gas_used: u64,
316 excess_blob_gas: u64,
317 }
318 let mut prev_big_block_header: Option<PrevBigBlockHeader> = None;
319
320 let mut next_block = self.from_block;
322
323 for big_block_idx in 0..self.num_big_blocks {
324 let range_start = next_block;
325
326 let mut blocks = Vec::new();
328 let mut block_receipts: Vec<Vec<Receipt>> = Vec::new();
329 let mut block_access_lists: Vec<Option<BlockAccessList>> = Vec::new();
330 let mut accumulated_block_gas: u64 = 0;
331
332 let mut reached_chain_tip = false;
333 while accumulated_block_gas < self.target_gas {
334 let block_number = next_block;
335 info!(target: "reth-bench", block_number, big_block = big_block_idx, "Fetching block");
336
337 let fetch_result = tokio::try_join!(
338 provider.get_block_by_number(block_number.into()).full(),
339 provider.get_block_receipts(block_number.into()),
340 );
341
342 let (rpc_block, receipts) = match fetch_result {
343 Ok((Some(block), Some(receipts))) => (block, receipts),
344 Ok((None, _) | (_, None)) => {
345 warn!(
346 target: "reth-bench",
347 block_number,
348 "Block not found — reached chain tip"
349 );
350 reached_chain_tip = true;
351 break;
352 }
353 Err(e) => return Err(e.into()),
354 };
355
356 let block_access_list = if self.bal {
357 Some(fetch_block_access_list(&provider, block_number).await.wrap_err_with(
358 || format!("Failed to fetch BAL for block {block_number}"),
359 )?)
360 } else {
361 None
362 };
363
364 let consensus_receipts: Vec<Receipt> = receipts
366 .iter()
367 .map(|r| {
368 let inner = &r.inner.inner.inner;
369 let tx_type = r.inner.inner.r#type.try_into().unwrap_or_default();
370 Receipt {
371 tx_type,
372 success: inner.receipt.status.coerce_status(),
373 cumulative_gas_used: inner.receipt.cumulative_gas_used,
374 logs: inner
375 .receipt
376 .logs
377 .iter()
378 .map(|log| alloy_primitives::Log {
379 address: log.inner.address,
380 data: log.inner.data.clone(),
381 })
382 .collect(),
383 }
384 })
385 .collect();
386
387 let block = rpc_block
389 .into_inner()
390 .map_header(|header| header.map(|h| h.into_header_with_defaults()))
391 .try_map_transactions(|tx| -> eyre::Result<TxEnvelope> {
392 tx.try_into().map_err(|_| eyre::eyre!("unsupported tx type"))
393 })?
394 .into_consensus();
395
396 let (payload, sidecar) = ExecutionPayload::from_block_slow(&block);
398 let execution_data = ExecutionData { payload, sidecar };
399
400 let block_gas = execution_data.payload.as_v1().gas_used;
401 let block_blob_gas =
402 execution_data.payload.as_v3().map(|v3| v3.blob_gas_used).unwrap_or(0);
403
404 info!(
405 target: "reth-bench",
406 block_number,
407 gas_used = block_gas,
408 blob_gas_used = block_blob_gas,
409 tx_count = execution_data.payload.transactions().len(),
410 receipts = consensus_receipts.len(),
411 "Fetched block"
412 );
413
414 accumulated_block_gas += block_gas;
415 blocks.push(execution_data);
416 block_receipts.push(consensus_receipts);
417 block_access_lists.push(block_access_list);
418 next_block += 1;
419 }
420
421 if blocks.is_empty() {
423 warn!(
424 target: "reth-bench",
425 big_block = big_block_idx,
426 requested = self.num_big_blocks,
427 "No blocks available, stopping generation early"
428 );
429 break;
430 }
431
432 let mut base = blocks.remove(0);
434 let base_receipts = block_receipts.remove(0);
435 let mut merged_block_access_list = block_access_lists.remove(0);
436 let mut env_switches = Vec::new();
437
438 let mut all_receipts: Vec<Receipt> = Vec::new();
442 let mut cumulative_gas_offset: u64 = 0;
443 {
444 let base_block_gas = base.payload.as_v1().gas_used;
446 all_receipts.extend(base_receipts.into_iter().map(|mut r| {
447 r.cumulative_gas_used += cumulative_gas_offset;
448 r
449 }));
450 cumulative_gas_offset += base_block_gas;
451 }
452
453 if !blocks.is_empty() {
454 env_switches.push((0, base.clone()));
459
460 let mut cumulative_tx_count = base.payload.transactions().len();
461
462 let last = blocks.last().unwrap();
464 let last_v1 = last.payload.as_v1();
465 let final_state_root = last_v1.state_root;
466
467 let mut total_gas_used = base.payload.as_v1().gas_used;
468 let mut total_gas_limit = base.payload.as_v1().gas_limit;
469
470 for ((block_data, receipts), block_access_list) in
472 blocks.into_iter().zip(block_receipts).zip(block_access_lists)
473 {
474 let block_v1 = block_data.payload.as_v1();
475 let block_gas = block_v1.gas_used;
476 total_gas_used += block_gas;
477 total_gas_limit += block_v1.gas_limit;
478
479 if let Some(block_access_list) = block_access_list {
480 merge_block_access_list(
481 merged_block_access_list.get_or_insert_with(Default::default),
482 block_access_list,
483 cumulative_tx_count as u64,
484 );
485 }
486
487 all_receipts.extend(receipts.into_iter().map(|mut r| {
489 r.cumulative_gas_used += cumulative_gas_offset;
490 r
491 }));
492 cumulative_gas_offset += block_gas;
493
494 env_switches.push((cumulative_tx_count, block_data.clone()));
496
497 let txs = block_data.payload.transactions().clone();
499 cumulative_tx_count += txs.len();
500 base.payload.transactions_mut().extend(txs);
501 }
502
503 let receipts_with_bloom: Vec<_> =
506 all_receipts.iter().map(|r| r.with_bloom_ref()).collect();
507 let merged_receipts_root = proofs::calculate_receipt_root(&receipts_with_bloom);
508 let merged_logs_bloom =
509 receipts_with_bloom.iter().fold(Bloom::ZERO, |bloom, r| bloom | *r.bloom_ref());
510
511 let base_v1 = base.payload.as_v1_mut();
513 base_v1.state_root = final_state_root;
514 base_v1.gas_used = total_gas_used;
515 base_v1.gas_limit = total_gas_limit;
516 base_v1.receipts_root = merged_receipts_root;
517 base_v1.logs_bloom = merged_logs_bloom;
518 }
519
520 if let Some(prev_hash) = prev_big_block_hash {
525 base.payload.as_v1_mut().parent_hash = prev_hash;
526 base.payload.as_v1_mut().block_number = self.from_block + big_block_idx;
529 }
530 if let Some(prev) = &prev_big_block_header {
531 let next_base_fee = alloy_eips::calc_next_block_base_fee(
534 prev.gas_used,
535 prev.gas_limit,
536 prev.base_fee_per_gas,
537 BaseFeeParams::ethereum(),
538 );
539 base.payload.as_v1_mut().base_fee_per_gas =
540 alloy_primitives::U256::from(next_base_fee);
541
542 let timestamp = base.payload.as_v1().timestamp;
545 let blob_params = chain_spec
546 .blob_params_at_timestamp(timestamp)
547 .unwrap_or_else(BlobParams::cancun);
548 let next_excess_blob_gas = blob_params.next_block_excess_blob_gas_osaka(
549 prev.excess_blob_gas,
550 prev.blob_gas_used,
551 prev.base_fee_per_gas,
552 );
553 if let Some(v3) = base.payload.as_v3_mut() {
554 v3.excess_blob_gas = next_excess_blob_gas;
555 }
556 }
557
558 {
562 let mut all_versioned_hashes: Vec<B256> =
563 base.sidecar.cancun().map(|c| c.versioned_hashes.clone()).unwrap_or_default();
564 let mut total_blob_gas =
565 base.payload.as_v3().map(|v3| v3.blob_gas_used).unwrap_or(0);
566 for (_, switch_data) in env_switches.iter().skip(1) {
568 if let Some(cancun) = switch_data.sidecar.cancun() {
569 all_versioned_hashes.extend_from_slice(&cancun.versioned_hashes);
570 }
571 if let Some(v3) = switch_data.payload.as_v3() {
572 total_blob_gas += v3.blob_gas_used;
573 }
574 }
575 if let Some(v3) = base.payload.as_v3_mut() {
576 v3.blob_gas_used = total_blob_gas;
577 }
578 let cancun = base.sidecar.cancun().map(|c| CancunPayloadFields {
579 versioned_hashes: all_versioned_hashes,
580 parent_beacon_block_root: c.parent_beacon_block_root,
581 });
582 let prague = base
587 .sidecar
588 .prague()
589 .map(|_| PraguePayloadFields::new(alloy_eips::eip7685::Requests::default()));
590 base.sidecar = match (cancun, prague) {
591 (Some(c), Some(p)) => ExecutionPayloadSidecar::v4(c, p),
592 (Some(c), None) => ExecutionPayloadSidecar::v3(c),
593 _ => ExecutionPayloadSidecar::none(),
594 };
595 }
596
597 let block_hash = compute_payload_block_hash(&base)?;
599 base.payload.as_v1_mut().block_hash = block_hash;
600 prev_big_block_hash = Some(block_hash);
601
602 {
605 let v1 = base.payload.as_v1();
606 prev_big_block_header = Some(PrevBigBlockHeader {
607 gas_used: v1.gas_used,
608 gas_limit: v1.gas_limit,
609 base_fee_per_gas: v1.base_fee_per_gas.to::<u64>(),
610 blob_gas_used: base.payload.as_v3().map(|v3| v3.blob_gas_used).unwrap_or(0),
611 excess_blob_gas: base.payload.as_v3().map(|v3| v3.excess_blob_gas).unwrap_or(0),
612 });
613 }
614
615 let big_block = BigBlockPayload {
616 execution_data: base,
617 big_block_data: BigBlockData {
618 env_switches,
619 prior_block_hashes: accumulated_block_hashes.clone(),
620 },
621 block_access_list: merged_block_access_list,
622 };
623
624 for (_, switch_data) in &big_block.big_block_data.env_switches {
628 let block_number = switch_data.payload.as_v1().block_number;
629 let block_hash = switch_data.payload.as_v1().block_hash;
630 accumulated_block_hashes.push((block_number, block_hash));
631 }
632 if accumulated_block_hashes.len() > 256 {
633 let excess = accumulated_block_hashes.len() - 256;
634 accumulated_block_hashes.drain(..excess);
635 }
636
637 let range_end = next_block - 1;
639 let filename = format!("big_block_{range_start}_to_{range_end}.json");
640 let filepath = self.output_dir.join(&filename);
641 let json = serde_json::to_string_pretty(&big_block)?;
642 std::fs::write(&filepath, &json)
643 .wrap_err_with(|| format!("Failed to write payload to {:?}", filepath))?;
644
645 info!(
646 target: "reth-bench",
647 path = %filepath.display(),
648 block_hash = %block_hash,
649 total_txs = big_block.execution_data.payload.transactions().len(),
650 total_gas_used = big_block.execution_data.payload.as_v1().gas_used,
651 env_switches = big_block.big_block_data.env_switches.len(),
652 prior_block_hashes = big_block.big_block_data.prior_block_hashes.len(),
653 bal_accounts = big_block.block_access_list.as_ref().map_or(0, Vec::len),
654 "Big block payload saved"
655 );
656
657 if reached_chain_tip {
658 warn!(
659 target: "reth-bench",
660 generated = big_block_idx + 1,
661 requested = self.num_big_blocks,
662 "Reached chain tip, stopping generation early"
663 );
664 break;
665 }
666 }
667
668 Ok(())
669 }
670}
671
672async fn fetch_block_access_list(
673 provider: &RootProvider<AnyNetwork>,
674 block_number: u64,
675) -> eyre::Result<BlockAccessList> {
676 provider
677 .client()
678 .request("eth_getBlockAccessListByBlockNumber", (BlockNumberOrTag::Number(block_number),))
679 .await
680 .map_err(Into::into)
681 .and_then(|block_access_list: Option<BlockAccessList>| {
682 block_access_list.ok_or_else(|| eyre::eyre!("BAL not found for block {block_number}"))
683 })
684}
685
686fn merge_block_access_list(
687 merged: &mut BlockAccessList,
688 incoming: BlockAccessList,
689 tx_index_offset: u64,
690) {
691 let mut account_positions = merged
692 .iter()
693 .enumerate()
694 .map(|(idx, account)| (account.address, idx))
695 .collect::<HashMap<_, _>>();
696
697 for mut account_changes in incoming {
698 shift_account_changes(&mut account_changes, tx_index_offset);
699
700 if let Some(&idx) = account_positions.get(&account_changes.address) {
701 merge_account_changes(&mut merged[idx], account_changes);
702 } else {
703 account_positions.insert(account_changes.address, merged.len());
704 merged.push(account_changes);
705 }
706 }
707}
708
709fn shift_account_changes(account_changes: &mut AccountChanges, tx_index_offset: u64) {
710 for slot_changes in &mut account_changes.storage_changes {
711 for change in &mut slot_changes.changes {
712 change.block_access_index += tx_index_offset;
713 }
714 }
715 for change in &mut account_changes.balance_changes {
716 change.block_access_index += tx_index_offset;
717 }
718 for change in &mut account_changes.nonce_changes {
719 change.block_access_index += tx_index_offset;
720 }
721 for change in &mut account_changes.code_changes {
722 change.block_access_index += tx_index_offset;
723 }
724}
725
726fn merge_account_changes(existing: &mut AccountChanges, incoming: AccountChanges) {
727 merge_slot_changes(&mut existing.storage_changes, incoming.storage_changes);
728 existing.storage_reads.extend(incoming.storage_reads);
729 existing.balance_changes.extend(incoming.balance_changes);
730 existing.nonce_changes.extend(incoming.nonce_changes);
731 existing.code_changes.extend(incoming.code_changes);
732}
733
734fn merge_slot_changes(existing: &mut Vec<SlotChanges>, incoming: Vec<SlotChanges>) {
735 let mut slot_positions = existing
736 .iter()
737 .enumerate()
738 .map(|(idx, slot_changes)| (slot_changes.slot, idx))
739 .collect::<HashMap<_, _>>();
740
741 for slot_changes in incoming {
742 if let Some(&idx) = slot_positions.get(&slot_changes.slot) {
743 existing[idx].changes.extend(slot_changes.changes);
744 } else {
745 slot_positions.insert(slot_changes.slot, existing.len());
746 existing.push(slot_changes);
747 }
748 }
749}
750
751pub fn compute_payload_block_hash(data: &ExecutionData) -> eyre::Result<B256> {
754 let block = data
755 .payload
756 .clone()
757 .into_block_with_sidecar_raw(&data.sidecar)
758 .wrap_err("failed to convert payload to block for hash computation")?;
759 Ok(block.header.hash_slow())
760}
761
762#[cfg(test)]
763mod tests {
764 use super::*;
765 use alloy_eips::eip7928::{BalanceChange, CodeChange, NonceChange, StorageChange};
766 use alloy_primitives::{Address, U256};
767
768 #[test]
769 fn merge_block_access_list_offsets_and_merges_accounts() {
770 let shared = Address::repeat_byte(0x11);
771 let other = Address::repeat_byte(0x22);
772
773 let mut merged = vec![AccountChanges {
774 address: shared,
775 storage_changes: vec![SlotChanges::new(
776 U256::from(1),
777 vec![StorageChange::new(0, U256::from(10))],
778 )],
779 storage_reads: vec![U256::from(3)],
780 balance_changes: vec![BalanceChange::new(1, U256::from(100))],
781 nonce_changes: vec![NonceChange::new(2, 7)],
782 code_changes: vec![],
783 }];
784
785 let incoming = vec![
786 AccountChanges {
787 address: shared,
788 storage_changes: vec![
789 SlotChanges::new(U256::from(1), vec![StorageChange::new(1, U256::from(20))]),
790 SlotChanges::new(U256::from(2), vec![StorageChange::new(2, U256::from(30))]),
791 ],
792 storage_reads: vec![U256::from(4)],
793 balance_changes: vec![BalanceChange::new(0, U256::from(150))],
794 nonce_changes: vec![NonceChange::new(2, 8)],
795 code_changes: vec![CodeChange::new(1, Bytes::from_static(&[0xaa]))],
796 },
797 AccountChanges {
798 address: other,
799 storage_changes: vec![SlotChanges::new(
800 U256::from(9),
801 vec![StorageChange::new(0, U256::from(90))],
802 )],
803 storage_reads: vec![],
804 balance_changes: vec![],
805 nonce_changes: vec![],
806 code_changes: vec![],
807 },
808 ];
809
810 merge_block_access_list(&mut merged, incoming, 3);
811
812 assert_eq!(merged.len(), 2);
813
814 let shared = &merged[0];
815 assert_eq!(shared.storage_reads, vec![U256::from(3), U256::from(4)]);
816 assert_eq!(
817 shared
818 .balance_changes
819 .iter()
820 .map(|change| change.block_access_index)
821 .collect::<Vec<_>>(),
822 vec![1, 3]
823 );
824 assert_eq!(
825 shared.nonce_changes.iter().map(|change| change.block_access_index).collect::<Vec<_>>(),
826 vec![2, 5]
827 );
828 assert_eq!(shared.code_changes[0].block_access_index, 4);
829
830 let slot_one = shared
831 .storage_changes
832 .iter()
833 .find(|slot_changes| slot_changes.slot == U256::from(1))
834 .unwrap();
835 assert_eq!(
836 slot_one.changes.iter().map(|change| change.block_access_index).collect::<Vec<_>>(),
837 vec![0, 4]
838 );
839
840 let slot_two = shared
841 .storage_changes
842 .iter()
843 .find(|slot_changes| slot_changes.slot == U256::from(2))
844 .unwrap();
845 assert_eq!(slot_two.changes[0].block_access_index, 5);
846
847 let other = &merged[1];
848 assert_eq!(other.address, Address::repeat_byte(0x22));
849 assert_eq!(other.storage_changes[0].changes[0].block_access_index, 3);
850 }
851}