Skip to main content

reth_bench/bench/
generate_big_block.rs

1//! Command for generating large blocks by merging transactions from consecutive real blocks.
2//!
3//! This command fetches consecutive blocks from an RPC until a target gas usage is reached,
4//! takes block 0 as the "base" payload, concatenates transactions from subsequent blocks,
5//! and saves the result to disk as a [`BigBlockPayload`] JSON file containing the merged
6//! [`ExecutionData`] and environment switches at each block boundary.
7
8use 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/// A single transaction with its gas used and raw encoded bytes.
36#[derive(Debug, Clone)]
37pub struct RawTransaction {
38    /// The actual gas used by the transaction (from receipt).
39    pub gas_used: u64,
40    /// The transaction type (e.g., 3 for EIP-4844 blob txs).
41    pub tx_type: u8,
42    /// The raw RLP-encoded transaction bytes.
43    pub raw: Bytes,
44}
45
46/// Abstraction over sources of transactions for big block generation.
47///
48/// Implementors provide transactions from different sources (RPC, database, files, etc.)
49pub trait TransactionSource {
50    /// Fetch transactions from a specific block number.
51    ///
52    /// Returns `Ok(None)` if the block doesn't exist.
53    /// Returns `Ok(Some((transactions, gas_used)))` with the block's transactions and total gas.
54    fn fetch_block_transactions(
55        &self,
56        block_number: u64,
57    ) -> impl Future<Output = eyre::Result<Option<(Vec<RawTransaction>, u64)>>> + Send;
58}
59
60/// RPC-based transaction source that fetches from a remote node.
61#[derive(Debug)]
62pub struct RpcTransactionSource {
63    provider: RootProvider<AnyNetwork>,
64}
65
66impl RpcTransactionSource {
67    /// Create a new RPC transaction source.
68    pub const fn new(provider: RootProvider<AnyNetwork>) -> Self {
69        Self { provider }
70    }
71
72    /// Create from an RPC URL with retry backoff.
73    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        // Fetch block and receipts in parallel
88        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        // Convert cumulative gas from receipts to per-tx gas_used
104        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/// Collects transactions from a source up to a target gas usage.
128#[derive(Debug)]
129pub struct TransactionCollector<S> {
130    source: S,
131    target_gas: u64,
132}
133
134impl<S: TransactionSource> TransactionCollector<S> {
135    /// Create a new transaction collector.
136    pub const fn new(source: S, target_gas: u64) -> Self {
137        Self { source, target_gas }
138    }
139
140    /// Collect transactions starting from the given block number.
141    ///
142    /// Skips blob transactions (type 3) and collects until target gas is reached.
143    /// Returns a `CollectionResult` with transactions, gas info, and next block.
144    pub async fn collect(&self, start_block: u64) -> eyre::Result<CollectionResult> {
145        self.collect_gas(start_block, self.target_gas).await
146    }
147
148    /// Collect transactions up to a specific gas target.
149    ///
150    /// This is used both for initial collection and for retry top-ups.
151    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                // Skip blob transactions (EIP-4844, type 3)
169                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            // Stop early if remaining gas is under 1M (close enough to target)
186            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/// Result of collecting transactions from blocks.
205#[derive(Debug)]
206pub struct CollectionResult {
207    /// Collected transactions with their gas info.
208    pub transactions: Vec<RawTransaction>,
209    /// Total gas sent (sum of historical `gas_used` for all collected txs).
210    pub gas_sent: u64,
211    /// Next block number to continue collecting from.
212    pub next_block: u64,
213}
214
215/// A merged big block payload with environment switches at block boundaries.
216#[derive(Debug, Serialize, Deserialize)]
217pub struct BigBlockPayload {
218    /// The primary execution data with all concatenated transactions.
219    pub execution_data: ExecutionData,
220    /// Big block data containing environment switches and prior block hashes.
221    #[serde(default)]
222    pub big_block_data: BigBlockData<ExecutionData>,
223    /// Flattened BAL across all constituent blocks, if requested during generation.
224    #[serde(default, skip_serializing_if = "Option::is_none")]
225    pub block_access_list: Option<BlockAccessList>,
226}
227
228/// `reth bench generate-big-block` command
229///
230/// Generates a large block by fetching consecutive blocks from an RPC, merging their
231/// transactions into a single payload, and saving the result to disk.
232#[derive(Debug, Parser)]
233pub struct Command {
234    /// The RPC URL to use for fetching blocks.
235    #[arg(long, value_name = "RPC_URL")]
236    rpc_url: String,
237
238    /// The chain name or path to a chain spec JSON file.
239    #[arg(long, value_name = "CHAIN", default_value = "mainnet")]
240    chain: String,
241
242    /// Block number to start from.
243    #[arg(long, value_name = "FROM_BLOCK")]
244    from_block: u64,
245
246    /// Target gas usage per big block. Consecutive real blocks are merged until
247    /// this gas target is reached (or exceeded by the last included block).
248    /// Accepts optional suffixes: K (thousand), M (million), G (billion).
249    #[arg(long, value_name = "TARGET_GAS", value_parser = super::helpers::parse_gas_limit)]
250    target_gas: u64,
251
252    /// Number of sequential big blocks to generate.
253    ///
254    /// Each big block merges real blocks until `--target-gas` is reached.
255    /// Sequential big blocks are chained: block N+1's `parent_hash` is set to
256    /// block N's computed hash.
257    #[arg(long, value_name = "NUM_BIG_BLOCKS", default_value = "1")]
258    num_big_blocks: u64,
259
260    /// Output directory for generated payloads.
261    #[arg(long, value_name = "OUTPUT_DIR")]
262    output_dir: std::path::PathBuf,
263
264    /// Query `eth_getBlockAccessListByBlockNumber` for each fetched block and persist
265    /// the flattened BAL on the stored payload.
266    #[arg(long, default_value_t = false)]
267    bal: bool,
268}
269
270impl Command {
271    /// Execute the `generate-big-block` command.
272    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        // Resolve chain spec for blob params lookup
281        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        // Create output directory
296        std::fs::create_dir_all(&self.output_dir).wrap_err_with(|| {
297            format!("Failed to create output directory: {:?}", self.output_dir)
298        })?;
299
300        // Set up RPC provider
301        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        // Track previous big block's merged header fields for deriving basefee and
310        // excess_blob_gas on subsequent big blocks.
311        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        // Track the next block to fetch across big blocks so they don't overlap.
321        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            // Fetch consecutive blocks until the gas target is reached.
327            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                // Convert RPC receipts to consensus receipts
365                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                // Convert to consensus block
388                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                // Convert to ExecutionData
397                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 we hit the chain tip without fetching any blocks, stop generating.
422            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            // Block 0 is the base
433            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            // Accumulate all receipts with corrected cumulative_gas_used.
439            // Each block's receipts have cumulative gas relative to that block;
440            // we add the prior blocks' total gas to make them globally correct.
441            let mut all_receipts: Vec<Receipt> = Vec::new();
442            let mut cumulative_gas_offset: u64 = 0;
443            {
444                // Base block receipts (block 0) — no offset needed
445                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                // Store the original unmutated base block as env_switch at index 0.
455                // This preserves the real gas_limit, basefee, etc. for segment 0's
456                // EVM environment, which would otherwise be lost when we mutate the
457                // base payload header below.
458                env_switches.push((0, base.clone()));
459
460                let mut cumulative_tx_count = base.payload.transactions().len();
461
462                // Collect state from the last block for header fields
463                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                // Concatenate transactions from subsequent blocks and build env_switches
471                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                    // Accumulate receipts with corrected cumulative_gas_used
488                    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                    // Record environment switch at this block boundary
495                    env_switches.push((cumulative_tx_count, block_data.clone()));
496
497                    // Append this block's transactions to the base payload
498                    let txs = block_data.payload.transactions().clone();
499                    cumulative_tx_count += txs.len();
500                    base.payload.transactions_mut().extend(txs);
501                }
502
503                // Compute merged receipts_root and logs_bloom from all accumulated
504                // receipts (with globally-correct cumulative_gas_used).
505                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                // Mutate the base payload header
512                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            // Chain sequential big blocks: set parent_hash, block_number, basefee,
521            // and excess_blob_gas for sequential continuity. The engine validates
522            // each big block against its parent, so these fields must be
523            // derivable from the previous big block's merged header.
524            if let Some(prev_hash) = prev_big_block_hash {
525                base.payload.as_v1_mut().parent_hash = prev_hash;
526                // First big block keeps its original block number (from_block).
527                // Subsequent big blocks increment from there.
528                base.payload.as_v1_mut().block_number = self.from_block + big_block_idx;
529            }
530            if let Some(prev) = &prev_big_block_header {
531                // Derive basefee from the previous big block's merged header using
532                // the standard EIP-1559 formula so validate_against_parent_eip1559_base_fee passes.
533                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                // Derive excess_blob_gas from the previous big block's merged header
543                // so validate_against_parent_4844 passes.
544                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            // Merge blob data from all constituent blocks: sum blob_gas_used
559            // and concatenate versioned hashes so the sidecar matches the blob
560            // transactions in the merged payload body.
561            {
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                // Skip env_switch[0] (base block clone) to avoid double-counting
567                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                // For merged blocks, set an empty requests hash in the Prague sidecar.
583                // The correct requests_hash cannot be computed from RPC data alone
584                // (raw execution layer requests are not exposed via eth_getBlockByNumber).
585                // Use --testing.skip-requests-hash-check when validating big block payloads.
586                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            // Compute the real block hash from the mutated payload
598            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            // Record this big block's merged header fields so the next big block
603            // can derive its basefee and excess_blob_gas correctly.
604            {
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            // Accumulate real block hashes from this big block's env_switches for
625            // subsequent big blocks' BLOCKHASH lookups. Cap at 256 entries since the
626            // BLOCKHASH opcode only looks back 256 blocks.
627            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            // Save to disk
638            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
751/// Computes the block hash for an [`ExecutionData`] by converting it to a raw block
752/// and hashing the header.
753pub 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}