Skip to main content

reth_cli_commands/init_state/
without_evm.rs

1use alloy_consensus::BlockHeader;
2use alloy_primitives::{BlockNumber, B256};
3use alloy_rlp::Decodable;
4use reth_codecs::Compact;
5use reth_node_builder::NodePrimitives;
6use reth_primitives_traits::{SealedBlock, SealedHeader, SealedHeaderFor};
7use reth_provider::{
8    providers::StaticFileProvider, BlockWriter, ProviderResult, StageCheckpointWriter,
9    StaticFileProviderFactory, StaticFileWriter,
10};
11use reth_stages::{StageCheckpoint, StageId};
12use reth_static_file_types::StaticFileSegment;
13use std::path::Path;
14use tracing::info;
15
16/// Reads the header RLP from a file and returns the Header.
17///
18/// This supports both raw rlp bytes and rlp hex string.
19pub(crate) fn read_header_from_file<H>(path: &Path) -> Result<H, eyre::Error>
20where
21    H: Decodable,
22{
23    let buf = if let Ok(content) = reth_fs_util::read_to_string(path) {
24        alloy_primitives::hex::decode(content.trim())?
25    } else {
26        // If UTF-8 decoding fails, read as raw bytes
27        reth_fs_util::read(path)?
28    };
29
30    let header = H::decode(&mut &buf[..])?;
31    Ok(header)
32}
33
34/// Creates a dummy chain (with no transactions) up to the last EVM block and appends the
35/// first valid block.
36pub fn setup_without_evm<Provider, F>(
37    provider_rw: &Provider,
38    header: SealedHeader<<Provider::Primitives as NodePrimitives>::BlockHeader>,
39    header_factory: F,
40) -> ProviderResult<()>
41where
42    Provider: StaticFileProviderFactory
43        + StageCheckpointWriter
44        + BlockWriter<Block = <Provider::Primitives as NodePrimitives>::Block>,
45    F: Fn(BlockNumber) -> <Provider::Primitives as NodePrimitives>::BlockHeader
46        + Send
47        + Sync
48        + 'static,
49{
50    info!(target: "reth::cli", new_tip = ?header.num_hash(), "Setting up dummy EVM chain before importing state.");
51
52    let static_file_provider = provider_rw.static_file_provider();
53    // Write EVM dummy data up to `header - 1` block. Skip when the supplied
54    // header is at block 0: `header.number() - 1` would underflow in u64 to
55    // `u64::MAX`, sending `append_dummy_chain` into a 1..=u64::MAX loop that
56    // exhausts memory before failing.
57    if header.number() > 0 {
58        append_dummy_chain(&static_file_provider, header.number() - 1, header_factory)?;
59    }
60
61    info!(target: "reth::cli", "Appending first valid block.");
62
63    append_first_block(provider_rw, &header)?;
64
65    for stage in StageId::ALL {
66        provider_rw.save_stage_checkpoint(stage, StageCheckpoint::new(header.number()))?;
67    }
68
69    info!(target: "reth::cli", "Set up finished.");
70
71    Ok(())
72}
73
74/// Appends the first block.
75///
76/// By appending it, static file writer also verifies that all segments are at the same
77/// height.
78fn append_first_block<Provider>(
79    provider_rw: &Provider,
80    header: &SealedHeaderFor<Provider::Primitives>,
81) -> ProviderResult<()>
82where
83    Provider: BlockWriter<Block = <Provider::Primitives as NodePrimitives>::Block>
84        + StaticFileProviderFactory<Primitives: NodePrimitives<BlockHeader: Compact>>,
85{
86    provider_rw.insert_block(
87        &SealedBlock::<<Provider::Primitives as NodePrimitives>::Block>::from_sealed_parts(
88            header.clone(),
89            Default::default(),
90        )
91        .try_recover()
92        .expect("no senders or txes"),
93    )?;
94
95    let sf_provider = provider_rw.static_file_provider();
96
97    sf_provider.latest_writer(StaticFileSegment::Receipts)?.increment_block(header.number())?;
98
99    Ok(())
100}
101
102/// Creates a dummy chain with no transactions/receipts up to `target_height` block inclusive.
103///
104/// * Headers: It will push an empty block.
105/// * Transactions: It will not push any tx, only increments the end block range.
106/// * Receipts: It will not push any receipt, only increments the end block range.
107/// * TransactionSenders: If the segment exists, increments the end block range.
108fn append_dummy_chain<N, F>(
109    sf_provider: &StaticFileProvider<N>,
110    target_height: BlockNumber,
111    header_factory: F,
112) -> ProviderResult<()>
113where
114    N: NodePrimitives,
115    F: Fn(BlockNumber) -> N::BlockHeader + Send + Sync + 'static,
116{
117    let (tx, rx) = std::sync::mpsc::channel();
118
119    // Spawn jobs for incrementing the block end range of transactions, receipts, and senders.
120    for segment in [
121        StaticFileSegment::Transactions,
122        StaticFileSegment::Receipts,
123        StaticFileSegment::TransactionSenders,
124    ] {
125        if sf_provider.get_highest_static_file_block(segment).is_none() {
126            continue
127        }
128        let tx_clone = tx.clone();
129        let provider = sf_provider.clone();
130        let thread_name = match segment {
131            StaticFileSegment::Transactions => "init-state-txs",
132            StaticFileSegment::Receipts => "init-state-receipts",
133            StaticFileSegment::TransactionSenders => "init-state-senders",
134            _ => "init-state-segment",
135        };
136        reth_tasks::spawn_os_thread(thread_name, move || {
137            let result = provider.latest_writer(segment).and_then(|mut writer| {
138                for block_num in 1..=target_height {
139                    writer.increment_block(block_num)?;
140                }
141                Ok(())
142            });
143
144            tx_clone.send(result).unwrap();
145        });
146    }
147
148    // Spawn job for appending empty headers
149    let provider = sf_provider.clone();
150    reth_tasks::spawn_os_thread("init-state-headers", move || {
151        let result = provider.latest_writer(StaticFileSegment::Headers).and_then(|mut writer| {
152            for block_num in 1..=target_height {
153                // TODO: should we fill with real parent_hash?
154                let header = header_factory(block_num);
155                writer.append_header(&header, &B256::ZERO)?;
156            }
157            Ok(())
158        });
159
160        tx.send(result).unwrap();
161    });
162
163    // Catches any StaticFileWriter error.
164    while let Ok(append_result) = rx.recv() {
165        if let Err(err) = append_result {
166            tracing::error!(target: "reth::cli", "Error appending dummy chain: {err}");
167            return Err(err)
168        }
169    }
170
171    // If, for any reason, rayon crashes this verifies if all segments are at the same
172    // target_height.
173    for segment in [
174        StaticFileSegment::Headers,
175        StaticFileSegment::Receipts,
176        StaticFileSegment::Transactions,
177        StaticFileSegment::TransactionSenders,
178    ] {
179        if sf_provider.get_highest_static_file_block(segment).is_none() {
180            continue
181        }
182        assert_eq!(
183            sf_provider.latest_writer(segment)?.user_header().block_end(),
184            Some(target_height),
185            "Static file segment {segment} was unsuccessful advancing its block height."
186        );
187    }
188
189    Ok(())
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use alloy_consensus::Header;
196    use alloy_primitives::{address, b256};
197    use reth_db_common::init::init_genesis;
198    use reth_provider::{test_utils::create_test_provider_factory, DatabaseProviderFactory};
199    use std::{
200        io::Write,
201        sync::{
202            atomic::{AtomicU64, Ordering},
203            Arc,
204        },
205    };
206    use tempfile::NamedTempFile;
207
208    #[test]
209    fn test_read_header_from_file_hex_string() {
210        let header_rlp = "0xf90212a00d84d79f59fc384a1f6402609a5b7253b4bfe7a4ae12608ed107273e5422b6dda01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d493479471562b71999873db5b286df957af199ec94617f7a0f496f3d199c51a1aaee67dac95f24d92ac13c60d25181e1eecd6eca5ddf32ac0a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000808206a4840365908a808468e975f09ad983011003846765746888676f312e32352e308664617277696ea06f485a167165ec12e0ab3e6ab59a7b88560b90306ac98a26eb294abf95a8c59b88000000000000000007";
211
212        let mut temp_file = NamedTempFile::new().unwrap();
213        temp_file.write_all(header_rlp.as_bytes()).unwrap();
214        temp_file.flush().unwrap();
215
216        let header: Header = read_header_from_file(temp_file.path()).unwrap();
217
218        assert_eq!(header.number, 1700);
219        assert_eq!(
220            header.parent_hash,
221            b256!("0d84d79f59fc384a1f6402609a5b7253b4bfe7a4ae12608ed107273e5422b6dd")
222        );
223        assert_eq!(header.beneficiary, address!("71562b71999873db5b286df957af199ec94617f7"));
224    }
225
226    #[test]
227    fn test_read_header_from_file_raw_bytes() {
228        let header_rlp = "0xf90212a00d84d79f59fc384a1f6402609a5b7253b4bfe7a4ae12608ed107273e5422b6dda01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d493479471562b71999873db5b286df957af199ec94617f7a0f496f3d199c51a1aaee67dac95f24d92ac13c60d25181e1eecd6eca5ddf32ac0a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000808206a4840365908a808468e975f09ad983011003846765746888676f312e32352e308664617277696ea06f485a167165ec12e0ab3e6ab59a7b88560b90306ac98a26eb294abf95a8c59b88000000000000000007";
229        let header_bytes =
230            alloy_primitives::hex::decode(header_rlp.trim_start_matches("0x")).unwrap();
231
232        let mut temp_file = NamedTempFile::new().unwrap();
233        temp_file.write_all(&header_bytes).unwrap();
234        temp_file.flush().unwrap();
235
236        let header: Header = read_header_from_file(temp_file.path()).unwrap();
237
238        assert_eq!(header.number, 1700);
239        assert_eq!(
240            header.parent_hash,
241            b256!("0d84d79f59fc384a1f6402609a5b7253b4bfe7a4ae12608ed107273e5422b6dd")
242        );
243        assert_eq!(header.beneficiary, address!("71562b71999873db5b286df957af199ec94617f7"));
244    }
245
246    #[test]
247    fn test_setup_without_evm_succeeds() {
248        let header_rlp = "0xf90212a00d84d79f59fc384a1f6402609a5b7253b4bfe7a4ae12608ed107273e5422b6dda01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d493479471562b71999873db5b286df957af199ec94617f7a0f496f3d199c51a1aaee67dac95f24d92ac13c60d25181e1eecd6eca5ddf32ac0a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000808206a4840365908a808468e975f09ad983011003846765746888676f312e32352e308664617277696ea06f485a167165ec12e0ab3e6ab59a7b88560b90306ac98a26eb294abf95a8c59b88000000000000000007";
249        let header_bytes =
250            alloy_primitives::hex::decode(header_rlp.trim_start_matches("0x")).unwrap();
251
252        let mut temp_file = NamedTempFile::new().unwrap();
253        temp_file.write_all(&header_bytes).unwrap();
254        temp_file.flush().unwrap();
255
256        let header: Header = read_header_from_file(temp_file.path()).unwrap();
257        let header_hash = b256!("4f05e4392969fc82e41f6d6a8cea379323b0b2d3ddf7def1a33eec03883e3a33");
258
259        let provider_factory = create_test_provider_factory();
260
261        init_genesis(&provider_factory).unwrap();
262
263        let provider_rw = provider_factory.database_provider_rw().unwrap();
264
265        setup_without_evm(&provider_rw, SealedHeader::new(header, header_hash), |number| Header {
266            number,
267            ..Default::default()
268        })
269        .unwrap();
270
271        let static_files = provider_factory.static_file_provider();
272        let writer = static_files.latest_writer(StaticFileSegment::Headers).unwrap();
273        let actual_next_height = writer.next_block_number();
274        let expected_next_height = 1701;
275
276        assert_eq!(actual_next_height, expected_next_height);
277    }
278
279    /// Regression: a header at block 0 used to send `append_dummy_chain` into
280    /// a `1..=u64::MAX` loop because `header.number() - 1` underflowed in
281    /// u64. The guard `if header.number() > 0` skips the dummy-chain step
282    /// when there is no pre-genesis range to backfill, so `header_factory`
283    /// is never invoked.
284    #[test]
285    fn test_setup_without_evm_skips_dummy_chain_for_genesis_header() {
286        let header = Header { number: 0, ..Default::default() };
287        let header_hash = header.hash_slow();
288
289        let provider_factory = create_test_provider_factory();
290        init_genesis(&provider_factory).unwrap();
291        let provider_rw = provider_factory.database_provider_rw().unwrap();
292
293        let factory_calls = Arc::new(AtomicU64::new(0));
294        let factory_calls_inner = Arc::clone(&factory_calls);
295
296        // The Result of `setup_without_evm` itself is not asserted: with
297        // `number == 0` plus a genesis already written by `init_genesis`,
298        // the subsequent `append_first_block` may legitimately fail. The
299        // bug under test is the OOM in the dummy-chain loop, observable
300        // through the factory-call counter below.
301        let _ = setup_without_evm(
302            &provider_rw,
303            SealedHeader::new(header, header_hash),
304            move |number| {
305                // Bound calls so a regression cannot exhaust the test
306                // runner's memory; the only correct value here is 0.
307                let n = factory_calls_inner.fetch_add(1, Ordering::Relaxed);
308                assert!(n < 8, "header_factory must not be invoked for a genesis-block header");
309                Header { number, ..Default::default() }
310            },
311        );
312
313        assert_eq!(
314            factory_calls.load(Ordering::Relaxed),
315            0,
316            "append_dummy_chain must be skipped when header.number() == 0"
317        );
318    }
319}