reth_e2e_test_utils/
test_rlp_utils.rs

1//! Utilities for creating and writing RLP test data
2
3use alloy_consensus::{constants::EMPTY_WITHDRAWALS, BlockHeader, Header};
4use alloy_eips::eip4895::Withdrawals;
5use alloy_primitives::{Address, B256, B64, U256};
6use alloy_rlp::Encodable;
7use reth_chainspec::{ChainSpec, EthereumHardforks};
8use reth_ethereum_primitives::{Block, BlockBody};
9use reth_primitives::SealedBlock;
10use reth_primitives_traits::Block as BlockTrait;
11use std::{io::Write, path::Path};
12use tracing::debug;
13
14/// Generate test blocks for a given chain spec
15pub fn generate_test_blocks(chain_spec: &ChainSpec, count: u64) -> Vec<SealedBlock> {
16    let mut blocks: Vec<SealedBlock> = Vec::new();
17    let genesis_header = chain_spec.sealed_genesis_header();
18    let mut parent_hash = genesis_header.hash();
19    let mut parent_number = genesis_header.number();
20    let mut parent_base_fee = genesis_header.base_fee_per_gas;
21    let mut parent_gas_limit = genesis_header.gas_limit;
22
23    debug!(target: "e2e::import",
24        "Genesis header base fee: {:?}, gas limit: {}, state root: {:?}",
25        parent_base_fee,
26        parent_gas_limit,
27        genesis_header.state_root()
28    );
29
30    for i in 1..=count {
31        // Create a simple header
32        let mut header = Header {
33            parent_hash,
34            number: parent_number + 1,
35            gas_limit: parent_gas_limit, // Use parent's gas limit
36            gas_used: 0,                 // Empty blocks use no gas
37            timestamp: genesis_header.timestamp() + i * 12, // 12 second blocks
38            beneficiary: Address::ZERO,
39            receipts_root: alloy_consensus::constants::EMPTY_RECEIPTS,
40            logs_bloom: Default::default(),
41            difficulty: U256::from(1), // Will be overridden for post-merge
42            // Use the same state root as parent for now (empty state changes)
43            state_root: if i == 1 {
44                genesis_header.state_root()
45            } else {
46                blocks.last().unwrap().state_root
47            },
48            transactions_root: alloy_consensus::constants::EMPTY_TRANSACTIONS,
49            ommers_hash: alloy_consensus::constants::EMPTY_OMMER_ROOT_HASH,
50            mix_hash: B256::ZERO,
51            nonce: B64::from(0u64),
52            extra_data: Default::default(),
53            base_fee_per_gas: None,
54            withdrawals_root: None,
55            blob_gas_used: None,
56            excess_blob_gas: None,
57            parent_beacon_block_root: None,
58            requests_hash: None,
59        };
60
61        // Set required fields based on chain spec
62        if chain_spec.is_london_active_at_block(header.number) {
63            // Calculate base fee based on parent block
64            if let Some(parent_fee) = parent_base_fee {
65                // For the first block, we need to use the exact expected base fee
66                // The consensus rules expect it to be calculated from the genesis
67                let (parent_gas_used, parent_gas_limit) = if i == 1 {
68                    // Genesis block parameters
69                    (genesis_header.gas_used, genesis_header.gas_limit)
70                } else {
71                    let last_block = blocks.last().unwrap();
72                    (last_block.gas_used, last_block.gas_limit)
73                };
74                header.base_fee_per_gas = Some(alloy_eips::calc_next_block_base_fee(
75                    parent_gas_used,
76                    parent_gas_limit,
77                    parent_fee,
78                    chain_spec.base_fee_params_at_timestamp(header.timestamp),
79                ));
80                debug!(target: "e2e::import", "Block {} calculated base fee: {:?} (parent gas used: {}, parent gas limit: {}, parent base fee: {})",
81                    i, header.base_fee_per_gas, parent_gas_used, parent_gas_limit, parent_fee);
82                parent_base_fee = header.base_fee_per_gas;
83            }
84        }
85
86        // For post-merge blocks
87        if chain_spec.is_paris_active_at_block(header.number) {
88            header.difficulty = U256::ZERO;
89            header.nonce = B64::ZERO;
90        }
91
92        // For post-shanghai blocks
93        if chain_spec.is_shanghai_active_at_timestamp(header.timestamp) {
94            header.withdrawals_root = Some(EMPTY_WITHDRAWALS);
95        }
96
97        // For post-cancun blocks
98        if chain_spec.is_cancun_active_at_timestamp(header.timestamp) {
99            header.blob_gas_used = Some(0);
100            header.excess_blob_gas = Some(0);
101            header.parent_beacon_block_root = Some(B256::ZERO);
102        }
103
104        // Create an empty block body
105        let body = BlockBody {
106            transactions: vec![],
107            ommers: vec![],
108            withdrawals: header.withdrawals_root.is_some().then(Withdrawals::default),
109        };
110
111        // Create the block
112        let block = Block { header: header.clone(), body: body.clone() };
113        let sealed_block = BlockTrait::seal_slow(block);
114
115        debug!(target: "e2e::import",
116            "Generated block {} with hash {:?}",
117            sealed_block.number(),
118            sealed_block.hash()
119        );
120        debug!(target: "e2e::import",
121            "  Body has {} transactions, {} ommers, withdrawals: {}",
122            body.transactions.len(),
123            body.ommers.len(),
124            body.withdrawals.is_some()
125        );
126
127        // Update parent for next iteration
128        parent_hash = sealed_block.hash();
129        parent_number = sealed_block.number();
130        parent_gas_limit = sealed_block.gas_limit;
131        if header.base_fee_per_gas.is_some() {
132            parent_base_fee = header.base_fee_per_gas;
133        }
134
135        blocks.push(sealed_block);
136    }
137
138    blocks
139}
140
141/// Write blocks to RLP file
142pub fn write_blocks_to_rlp(blocks: &[SealedBlock], path: &Path) -> std::io::Result<()> {
143    let mut file = std::fs::File::create(path)?;
144    let mut total_bytes = 0;
145
146    for (i, block) in blocks.iter().enumerate() {
147        // Convert SealedBlock to Block before encoding
148        let block_for_encoding = block.clone().unseal();
149
150        let mut buf = Vec::new();
151        block_for_encoding.encode(&mut buf);
152        debug!(target: "e2e::import",
153            "Block {} has {} transactions, encoded to {} bytes",
154            i,
155            block.body().transactions.len(),
156            buf.len()
157        );
158
159        // Debug: check what's in the encoded data
160        debug!(target: "e2e::import", "Block {} encoded to {} bytes", i, buf.len());
161        if buf.len() < 20 {
162            debug!(target: "e2e::import", "  Raw bytes: {:?}", &buf);
163        } else {
164            debug!(target: "e2e::import", "  First 20 bytes: {:?}", &buf[..20]);
165        }
166
167        total_bytes += buf.len();
168        file.write_all(&buf)?;
169    }
170
171    file.flush()?;
172    debug!(target: "e2e::import", "Total RLP bytes written: {total_bytes}");
173    Ok(())
174}
175
176/// Create FCU JSON for the tip of the chain
177pub fn create_fcu_json(tip: &SealedBlock) -> serde_json::Value {
178    serde_json::json!({
179        "params": [{
180            "headBlockHash": format!("0x{:x}", tip.hash()),
181            "safeBlockHash": format!("0x{:x}", tip.hash()),
182            "finalizedBlockHash": format!("0x{:x}", tip.hash()),
183        }]
184    })
185}