Skip to main content

reth_db_common/
init.rs

1//! Reth genesis initialization utility functions.
2
3use alloy_consensus::BlockHeader;
4use alloy_genesis::GenesisAccount;
5use alloy_primitives::{
6    keccak256,
7    map::{AddressMap, B256Map, B256Set, HashMap},
8    Address, B256, U256,
9};
10use reth_chainspec::EthChainSpec;
11use reth_codecs::Compact;
12use reth_config::config::EtlConfig;
13use reth_db_api::{
14    cursor::{DbCursorRW, DbDupCursorRO, DbDupCursorRW},
15    models::{
16        integer_list::IntegerListError, storage_sharded_key::StorageShardedKey, AccountBeforeTx,
17        BlockNumberAddress, IntegerList, ShardedKey,
18    },
19    table::DupSort,
20    tables,
21    transaction::{DbTx, DbTxMut},
22    DatabaseError,
23};
24use reth_etl::Collector;
25use reth_execution_errors::StateRootError;
26use reth_primitives_traits::{
27    Account, Bytecode, GotExpected, NodePrimitives, SealedHeader, StorageEntry,
28};
29use reth_provider::{
30    errors::provider::ProviderResult, providers::StaticFileWriter, BlockHashReader, BlockNumReader,
31    BundleStateInit, ChainSpecProvider, DBProvider, DatabaseProviderFactory, ExecutionOutcome,
32    HashingWriter, HeaderProvider, HistoryWriter, MetadataProvider, MetadataWriter,
33    OriginalValuesKnown, ProviderError, RevertsInit, RocksDBProviderFactory, StageCheckpointReader,
34    StageCheckpointWriter, StateWriteConfig, StateWriter, StaticFileProviderFactory,
35    StorageSettings, StorageSettingsCache, TrieWriter,
36};
37use reth_stages_types::{StageCheckpoint, StageId};
38use reth_static_file_types::StaticFileSegment;
39use reth_trie::{
40    prefix_set::TriePrefixSets, IntermediateStateRootState, StateRoot as StateRootComputer,
41    StateRootProgress,
42};
43use reth_trie_db::DatabaseStateRoot;
44
45type DbStateRoot<'a, TX, A> = StateRootComputer<
46    reth_trie_db::DatabaseTrieCursorFactory<&'a TX, A>,
47    reth_trie_db::DatabaseHashedCursorFactory<&'a TX>,
48>;
49
50use serde::{Deserialize, Serialize};
51use std::io::BufRead;
52use tracing::{debug, error, info, trace, warn};
53
54pub use reth_provider::init::{
55    insert_account_history, insert_genesis_account_history, insert_genesis_history,
56    insert_genesis_storage_history, insert_history, insert_storage_history,
57};
58
59/// Default soft limit for number of bytes to read from state dump file, before inserting into
60/// database.
61///
62/// Default is 1 GB.
63pub const DEFAULT_SOFT_LIMIT_BYTE_LEN_ACCOUNTS_CHUNK: usize = 1_000_000_000;
64
65/// Soft limit for the number of flushed updates after which to log progress summary.
66const SOFT_LIMIT_COUNT_FLUSHED_UPDATES: usize = 1_000_000;
67
68/// Max number of storage "units" (1 per account + 1 per storage slot) before committing
69/// the current MDBX transaction and opening a new one. This bounds dirty page accumulation
70/// and prevents OOM on large state imports.
71const STORAGE_COMMIT_THRESHOLD: usize = 100_000;
72
73/// Max number of trie updates retained before init-state state root computation commits progress.
74const STATE_ROOT_COMMIT_THRESHOLD: u64 = 25_000;
75
76/// Storage initialization error type.
77#[derive(Debug, thiserror::Error, Clone)]
78pub enum InitStorageError {
79    /// Genesis header found on static files but the database is empty.
80    #[error(
81        "static files found, but the database is uninitialized. If attempting to re-syncing, delete both."
82    )]
83    UninitializedDatabase,
84    /// An existing genesis block was found in the database, and its hash did not match the hash of
85    /// the chainspec.
86    #[error(
87        "genesis hash in the storage does not match the specified chainspec: chainspec is {chainspec_hash}, database is {storage_hash}"
88    )]
89    GenesisHashMismatch {
90        /// Expected genesis hash.
91        chainspec_hash: B256,
92        /// Actual genesis hash.
93        storage_hash: B256,
94    },
95    /// Provider error.
96    #[error(transparent)]
97    Provider(#[from] ProviderError),
98    /// State root error while computing the state root
99    #[error(transparent)]
100    StateRootError(#[from] StateRootError),
101    /// State root doesn't match the expected one.
102    #[error("state root mismatch: {_0}")]
103    StateRootMismatch(GotExpected<B256>),
104}
105
106impl From<DatabaseError> for InitStorageError {
107    fn from(error: DatabaseError) -> Self {
108        Self::Provider(ProviderError::Database(error))
109    }
110}
111
112/// Write the genesis block if it has not already been written
113pub fn init_genesis<PF>(factory: &PF) -> Result<B256, InitStorageError>
114where
115    PF: DatabaseProviderFactory
116        + StaticFileProviderFactory<Primitives: NodePrimitives<BlockHeader: Compact>>
117        + ChainSpecProvider
118        + StageCheckpointReader
119        + BlockNumReader
120        + MetadataProvider
121        + StorageSettingsCache,
122    PF::ProviderRW: StaticFileProviderFactory<Primitives = PF::Primitives>
123        + StageCheckpointWriter
124        + HistoryWriter
125        + HeaderProvider
126        + HashingWriter
127        + StateWriter
128        + TrieWriter
129        + MetadataWriter
130        + ChainSpecProvider
131        + StorageSettingsCache
132        + RocksDBProviderFactory
133        + AsRef<PF::ProviderRW>,
134    PF::ChainSpec: EthChainSpec<Header = <PF::Primitives as NodePrimitives>::BlockHeader>,
135{
136    init_genesis_with_settings(factory, StorageSettings::base())
137}
138
139/// Write the genesis block if it has not already been written with [`StorageSettings`].
140pub fn init_genesis_with_settings<PF>(
141    factory: &PF,
142    genesis_storage_settings: StorageSettings,
143) -> Result<B256, InitStorageError>
144where
145    PF: DatabaseProviderFactory
146        + StaticFileProviderFactory<Primitives: NodePrimitives<BlockHeader: Compact>>
147        + ChainSpecProvider
148        + StageCheckpointReader
149        + BlockNumReader
150        + MetadataProvider
151        + StorageSettingsCache,
152    PF::ProviderRW: StaticFileProviderFactory<Primitives = PF::Primitives>
153        + StageCheckpointWriter
154        + HistoryWriter
155        + HeaderProvider
156        + HashingWriter
157        + StateWriter
158        + TrieWriter
159        + MetadataWriter
160        + ChainSpecProvider
161        + StorageSettingsCache
162        + RocksDBProviderFactory
163        + AsRef<PF::ProviderRW>,
164    PF::ChainSpec: EthChainSpec<Header = <PF::Primitives as NodePrimitives>::BlockHeader>,
165{
166    init_genesis_with_settings_and_validate(factory, genesis_storage_settings, true)
167}
168
169/// Write the genesis block if it has not already been written with [`StorageSettings`],
170/// optionally validating the DB-resident genesis hash against the chainspec hash.
171///
172/// Retries interrupted genesis initialization when the Headers checkpoint is absent and the
173/// static files contain only the matching genesis block.
174pub fn init_genesis_with_settings_and_validate<PF>(
175    factory: &PF,
176    genesis_storage_settings: StorageSettings,
177    validate_genesis_hash: bool,
178) -> Result<B256, InitStorageError>
179where
180    PF: DatabaseProviderFactory
181        + StaticFileProviderFactory<Primitives: NodePrimitives<BlockHeader: Compact>>
182        + ChainSpecProvider
183        + StageCheckpointReader
184        + BlockNumReader
185        + MetadataProvider
186        + StorageSettingsCache,
187    PF::ProviderRW: StaticFileProviderFactory<Primitives = PF::Primitives>
188        + StageCheckpointWriter
189        + HistoryWriter
190        + HeaderProvider
191        + HashingWriter
192        + StateWriter
193        + TrieWriter
194        + MetadataWriter
195        + ChainSpecProvider
196        + StorageSettingsCache
197        + RocksDBProviderFactory
198        + AsRef<PF::ProviderRW>,
199    PF::ChainSpec: EthChainSpec<Header = <PF::Primitives as NodePrimitives>::BlockHeader>,
200{
201    let chain = factory.chain_spec();
202
203    let genesis = chain.genesis();
204    let hash = chain.genesis_hash();
205
206    // Get the genesis block number from the chain spec
207    let genesis_block_number = chain.genesis_header().number();
208
209    // Check if we already have the genesis header or if we have the wrong one.
210    match factory.block_hash(genesis_block_number) {
211        Ok(None) | Err(ProviderError::MissingStaticFileBlock(StaticFileSegment::Headers, _)) => {}
212        Ok(Some(block_hash)) => {
213            if block_hash == hash {
214                if factory.get_stage_checkpoint(StageId::Headers)?.is_some() {
215                    let stored = factory.storage_settings()?.unwrap_or_else(StorageSettings::v1);
216                    if stored != genesis_storage_settings {
217                        warn!(
218                            target: "reth::storage",
219                            ?stored,
220                            requested = ?genesis_storage_settings,
221                            "Storage settings mismatch detected. Using the stored settings from the existing database."
222                        );
223                    }
224
225                    debug!("Genesis already written, skipping.");
226                    return Ok(hash)
227                }
228
229                // A static-first genesis commit can be interrupted before its MDBX transaction
230                // commits. The genesis MDBX writes are atomic, so an absent Headers checkpoint
231                // permits replay when static files contain only genesis. Reject static files
232                // from an already-synced node whose MDBX database was deleted.
233                let static_files = factory.static_file_provider();
234                let has_non_genesis_static_files = StaticFileSegment::iter().any(|segment| {
235                    static_files
236                        .get_highest_static_file_block(segment)
237                        .is_some_and(|block| block != genesis_block_number) ||
238                        static_files
239                            .get_lowest_range_start(segment)
240                            .is_some_and(|block| block != genesis_block_number) ||
241                        (segment.is_tx_based() &&
242                            static_files.get_highest_static_file_tx(segment).is_some())
243                });
244                if has_non_genesis_static_files {
245                    error!(target: "reth::storage", "Genesis header found on static files, but database is uninitialized.");
246                    return Err(InitStorageError::UninitializedDatabase)
247                }
248
249                // Genesis changesets may already have been published. Remove them before replay
250                // so storage v2 does not append the same genesis block twice after a restart.
251                for segment in
252                    [StaticFileSegment::AccountChangeSets, StaticFileSegment::StorageChangeSets]
253                {
254                    static_files.delete_segment(segment)?;
255                }
256                warn!(target: "reth::storage", "Resuming interrupted genesis initialization.");
257            } else if !validate_genesis_hash {
258                warn!(
259                    target: "reth::storage",
260                    chainspec_hash = %hash,
261                    storage_hash = %block_hash,
262                    "Genesis hash mismatch with chainspec; trusting DB per --debug.skip-genesis-validation"
263                );
264                return Ok(block_hash)
265            } else {
266                return Err(InitStorageError::GenesisHashMismatch {
267                    chainspec_hash: hash,
268                    storage_hash: block_hash,
269                })
270            }
271        }
272        Err(e) => {
273            debug!(?e);
274            return Err(e.into());
275        }
276    }
277
278    debug!("Writing genesis block.");
279
280    // Make sure to set storage settings before anything writes
281    factory.set_storage_settings_cache(genesis_storage_settings);
282
283    let alloc = &genesis.alloc;
284
285    // use transaction to insert genesis header
286    let provider_rw = factory.database_provider_rw()?;
287
288    // Behaviour reserved only for new nodes should be set in the storage settings.
289    provider_rw.write_storage_settings(genesis_storage_settings)?;
290
291    // For non-zero genesis blocks, set expected_block_start BEFORE insert_genesis_state.
292    // When block_range is None, next_block_number() uses expected_block_start. By default,
293    // expected_block_start comes from find_fixed_range which returns the file range start (0),
294    // not the genesis block number. This would cause increment_block(N) to fail.
295    let static_file_provider = provider_rw.static_file_provider();
296    if genesis_block_number > 0 {
297        if genesis_storage_settings.storage_v2 {
298            static_file_provider
299                .get_writer(genesis_block_number, StaticFileSegment::AccountChangeSets)?
300                .user_header_mut()
301                .set_expected_block_start(genesis_block_number);
302        }
303        if genesis_storage_settings.storage_v2 {
304            static_file_provider
305                .get_writer(genesis_block_number, StaticFileSegment::StorageChangeSets)?
306                .user_header_mut()
307                .set_expected_block_start(genesis_block_number);
308        }
309    }
310
311    insert_genesis_hashes(&provider_rw, alloc.iter())?;
312    insert_genesis_history(&provider_rw, alloc.iter())?;
313
314    // Insert header
315    insert_genesis_header(&provider_rw, &chain)?;
316
317    insert_genesis_state(&provider_rw, alloc.iter())?;
318
319    // compute state root to populate trie tables
320    compute_state_root(&provider_rw, None)?;
321
322    // set stage checkpoint to genesis block number for all stages
323    let checkpoint = StageCheckpoint::new(genesis_block_number);
324    for stage in StageId::ALL {
325        provider_rw.save_stage_checkpoint(stage, checkpoint)?;
326    }
327
328    // Static file segments start empty, so we need to initialize the block range.
329    // For genesis blocks with non-zero block numbers, we use get_writer() instead of
330    // latest_writer() and set_block_range() to ensure static files start at the correct block.
331    let static_file_provider = provider_rw.static_file_provider();
332
333    static_file_provider
334        .get_writer(genesis_block_number, StaticFileSegment::Receipts)?
335        .user_header_mut()
336        .set_block_range(genesis_block_number, genesis_block_number);
337    static_file_provider
338        .get_writer(genesis_block_number, StaticFileSegment::Transactions)?
339        .user_header_mut()
340        .set_block_range(genesis_block_number, genesis_block_number);
341
342    if genesis_storage_settings.storage_v2 {
343        static_file_provider
344            .get_writer(genesis_block_number, StaticFileSegment::TransactionSenders)?
345            .user_header_mut()
346            .set_block_range(genesis_block_number, genesis_block_number);
347    }
348
349    provider_rw.commit()?;
350
351    Ok(hash)
352}
353
354/// Inserts the genesis state into the database.
355pub fn insert_genesis_state<'a, 'b, Provider>(
356    provider: &Provider,
357    alloc: impl Iterator<Item = (&'a Address, &'b GenesisAccount)>,
358) -> ProviderResult<()>
359where
360    Provider: StaticFileProviderFactory
361        + DBProvider<Tx: DbTxMut>
362        + HeaderProvider
363        + StateWriter
364        + ChainSpecProvider
365        + AsRef<Provider>,
366{
367    let genesis_block_number = provider.chain_spec().genesis_header().number();
368    insert_state(provider, alloc, genesis_block_number)
369}
370
371/// Inserts state at given block into database.
372pub fn insert_state<'a, 'b, Provider>(
373    provider: &Provider,
374    alloc: impl Iterator<Item = (&'a Address, &'b GenesisAccount)>,
375    block: u64,
376) -> ProviderResult<()>
377where
378    Provider: StaticFileProviderFactory
379        + DBProvider<Tx: DbTxMut>
380        + HeaderProvider
381        + StateWriter
382        + AsRef<Provider>,
383{
384    let capacity = alloc.size_hint().1.unwrap_or(0);
385    let mut state_init: BundleStateInit =
386        AddressMap::with_capacity_and_hasher(capacity, Default::default());
387    let mut reverts_init: AddressMap<_> =
388        AddressMap::with_capacity_and_hasher(capacity, Default::default());
389    let mut contracts: B256Map<Bytecode> =
390        B256Map::with_capacity_and_hasher(capacity, Default::default());
391
392    for (address, account) in alloc {
393        let bytecode_hash = if let Some(code) = &account.code {
394            match Bytecode::new_raw_checked(code.clone()) {
395                Ok(bytecode) => {
396                    let hash = bytecode.hash_slow();
397                    contracts.insert(hash, bytecode);
398                    Some(hash)
399                }
400                Err(err) => {
401                    error!(%address, %err, "Failed to decode genesis bytecode.");
402                    return Err(DatabaseError::Other(err.to_string()).into());
403                }
404            }
405        } else {
406            None
407        };
408
409        // get state
410        let storage = account
411            .storage
412            .as_ref()
413            .map(|m| {
414                m.iter()
415                    .map(|(key, value)| {
416                        let value = U256::from_be_bytes(value.0);
417                        (*key, (U256::ZERO, value))
418                    })
419                    .collect::<B256Map<_>>()
420            })
421            .unwrap_or_default();
422
423        reverts_init.insert(
424            *address,
425            (Some(None), storage.keys().map(|k| StorageEntry::new(*k, U256::ZERO)).collect()),
426        );
427
428        state_init.insert(
429            *address,
430            (None, Some(Account { bytecode_hash, ..Account::from(account) }), storage),
431        );
432    }
433    let all_reverts_init: RevertsInit = HashMap::from_iter([(block, reverts_init)]);
434
435    let execution_outcome = ExecutionOutcome::new_init(
436        state_init,
437        all_reverts_init,
438        contracts,
439        Vec::default(),
440        block,
441        Vec::new(),
442    );
443
444    provider.write_state(
445        &execution_outcome,
446        OriginalValuesKnown::Yes,
447        StateWriteConfig::default(),
448    )?;
449
450    trace!(target: "reth::cli", "Inserted state");
451
452    Ok(())
453}
454
455/// Inserts hashes for the genesis state.
456pub fn insert_genesis_hashes<'a, 'b, Provider>(
457    provider: &Provider,
458    alloc: impl Iterator<Item = (&'a Address, &'b GenesisAccount)> + Clone,
459) -> ProviderResult<()>
460where
461    Provider: DBProvider<Tx: DbTxMut> + HashingWriter,
462{
463    // insert and hash accounts to hashing table
464    let alloc_accounts = alloc.clone().map(|(addr, account)| (*addr, Some(Account::from(account))));
465    provider.insert_account_for_hashing(alloc_accounts)?;
466
467    trace!(target: "reth::cli", "Inserted account hashes");
468
469    let alloc_storage = alloc.filter_map(|(addr, account)| {
470        // only return Some if there is storage
471        account.storage.as_ref().map(|storage| {
472            (*addr, storage.iter().map(|(&key, &value)| StorageEntry { key, value: value.into() }))
473        })
474    });
475    provider.insert_storage_for_hashing(alloc_storage)?;
476
477    trace!(target: "reth::cli", "Inserted storage hashes");
478
479    Ok(())
480}
481
482/// Inserts header for the genesis state.
483pub fn insert_genesis_header<Provider, Spec>(
484    provider: &Provider,
485    chain: &Spec,
486) -> ProviderResult<()>
487where
488    Provider: StaticFileProviderFactory<Primitives: NodePrimitives<BlockHeader: Compact>>
489        + DBProvider<Tx: DbTxMut>,
490    Spec: EthChainSpec<Header = <Provider::Primitives as NodePrimitives>::BlockHeader>,
491{
492    let (header, block_hash) = (chain.genesis_header(), chain.genesis_hash());
493    let static_file_provider = provider.static_file_provider();
494
495    // Get the actual genesis block number from the header
496    let genesis_block_number = header.number();
497
498    match static_file_provider.block_hash(genesis_block_number) {
499        Ok(None) | Err(ProviderError::MissingStaticFileBlock(StaticFileSegment::Headers, _)) => {
500            let difficulty = header.difficulty();
501
502            // For genesis blocks with non-zero block numbers, we need to ensure they are stored
503            // in the correct static file range. We use get_writer() with the genesis block number
504            // to ensure the genesis block is stored in the correct static file range.
505            let mut writer = static_file_provider
506                .get_writer(genesis_block_number, StaticFileSegment::Headers)?;
507
508            // For non-zero genesis blocks, we need to set block range to genesis_block_number and
509            // append header without increment block
510            if genesis_block_number > 0 {
511                writer
512                    .user_header_mut()
513                    .set_block_range(genesis_block_number, genesis_block_number);
514                writer.append_header_direct(header, difficulty, &block_hash)?;
515            } else {
516                // For zero genesis blocks, use normal append_header
517                writer.append_header(header, &block_hash)?;
518            }
519        }
520        Ok(Some(_)) => {}
521        Err(e) => return Err(e),
522    }
523
524    provider.tx_ref().put::<tables::HeaderNumbers>(block_hash, genesis_block_number)?;
525    provider.tx_ref().put::<tables::BlockBodyIndices>(genesis_block_number, Default::default())?;
526
527    Ok(())
528}
529
530/// Reads account state from a [`BufRead`] reader and initializes it at the highest block that can
531/// be found on database.
532///
533/// It's similar to [`init_genesis`] but supports importing state too big to fit in memory, and can
534/// be set to the highest block present. One practical usecase is to import OP mainnet state at
535/// bedrock transition block.
536pub fn init_from_state_dump<PF>(
537    mut reader: impl BufRead,
538    provider_factory: &PF,
539    etl_config: EtlConfig,
540) -> eyre::Result<B256>
541where
542    PF: DatabaseProviderFactory<
543        ProviderRW: StaticFileProviderFactory
544                        + DBProvider<Tx: DbTxMut>
545                        + BlockNumReader
546                        + ChainSpecProvider
547                        + StageCheckpointWriter
548                        + HistoryWriter
549                        + HeaderProvider
550                        + HashingWriter
551                        + TrieWriter
552                        + StateWriter
553                        + StorageSettingsCache
554                        + RocksDBProviderFactory
555                        + AsRef<PF::ProviderRW>,
556    >,
557{
558    if etl_config.file_size == 0 {
559        return Err(eyre::eyre!("ETL file size cannot be zero"))
560    }
561
562    let (block, hash, expected_state_root) = {
563        let provider_rw = provider_factory.database_provider_rw()?;
564        let block = provider_rw.last_block_number()?;
565        let hash = provider_rw
566            .block_hash(block)?
567            .ok_or_else(|| eyre::eyre!("Block hash not found for block {}", block))?;
568        let header = provider_rw
569            .header_by_number(block)?
570            .map(|h| SealedHeader::new(h, hash))
571            .ok_or_else(|| ProviderError::HeaderNotFound(block.into()))?;
572        let state_root = header.state_root();
573
574        debug!(target: "reth::cli",
575            block,
576            chain=%provider_rw.chain_spec().chain(),
577            "Initializing state at block"
578        );
579
580        (block, hash, state_root)
581    };
582
583    // first line can be state root
584    let dump_state_root = parse_state_root(&mut reader)?;
585    if expected_state_root != dump_state_root {
586        error!(target: "reth::cli",
587            ?dump_state_root,
588            ?expected_state_root,
589            "State root from state dump does not match state root in current header."
590        );
591        return Err(InitStorageError::StateRootMismatch(GotExpected {
592            got: dump_state_root,
593            expected: expected_state_root,
594        })
595        .into())
596    }
597
598    // remaining lines are accounts
599    let collector = parse_accounts(&mut reader, etl_config)?;
600
601    // write state to db with chunked commits to avoid OOM
602    dump_state(collector, provider_factory, block)?;
603
604    info!(target: "reth::cli", "All accounts written to database, starting state root computation (may take some time)");
605
606    // clear trie tables so state root is computed from scratch
607    {
608        let provider_rw = provider_factory.database_provider_rw()?;
609        provider_rw.tx_ref().clear::<tables::AccountsTrie>()?;
610        provider_rw.tx_ref().clear::<tables::StoragesTrie>()?;
611        provider_rw.commit()?;
612    }
613
614    // compute and compare state root
615    let computed_state_root = compute_state_root_chunked(provider_factory)?;
616    if computed_state_root == expected_state_root {
617        info!(target: "reth::cli",
618            ?computed_state_root,
619            "Computed state root matches state root in state dump"
620        );
621    } else {
622        error!(target: "reth::cli",
623            ?computed_state_root,
624            ?expected_state_root,
625            "Computed state root does not match state root in state dump"
626        );
627
628        return Err(InitStorageError::StateRootMismatch(GotExpected {
629            got: computed_state_root,
630            expected: expected_state_root,
631        })
632        .into())
633    }
634
635    // insert sync stages for stages that require state
636    {
637        let provider_rw = provider_factory.database_provider_rw()?;
638        for stage in StageId::STATE_REQUIRED {
639            provider_rw.save_stage_checkpoint(stage, StageCheckpoint::new(block))?;
640        }
641        provider_rw.commit()?;
642    }
643
644    Ok(hash)
645}
646
647/// Parses and returns expected state root.
648fn parse_state_root(reader: &mut impl BufRead) -> eyre::Result<B256> {
649    let mut line = String::new();
650    reader.read_line(&mut line)?;
651
652    let expected_state_root = serde_json::from_str::<StateRoot>(&line)?.root;
653    trace!(target: "reth::cli",
654        root=%expected_state_root,
655        "Read state root from file"
656    );
657    Ok(expected_state_root)
658}
659
660/// Parses accounts and pushes them to a [`Collector`].
661fn parse_accounts(
662    reader: impl BufRead,
663    etl_config: EtlConfig,
664) -> Result<Collector<Address, GenesisAccount>, eyre::Error> {
665    let mut collector = Collector::new(etl_config.file_size, etl_config.dir);
666    let mut parsed_accounts = 0usize;
667
668    let stream =
669        serde_json::Deserializer::from_reader(reader).into_iter::<GenesisAccountWithAddress>();
670    for account in stream {
671        let GenesisAccountWithAddress { genesis_account, address } = account?;
672        collector.insert(address, genesis_account)?;
673
674        parsed_accounts += 1;
675        if parsed_accounts.is_multiple_of(100_000) {
676            info!(target: "reth::cli", parsed_accounts, "Parsed accounts");
677        }
678    }
679
680    Ok(collector)
681}
682
683/// Takes a [`Collector`] and writes all accounts directly to database tables.
684///
685/// This bypasses the higher-level `insert_state`/`insert_genesis_hashes`/`insert_history`
686/// functions which build intermediate structures (`BundleStateInit`, `RevertsInit`,
687/// `ExecutionOutcome`) that duplicate all storage data 2-3x in memory. For accounts with
688/// millions of storage entries this causes OOM.
689///
690/// Instead, each account is written directly to all required tables using cursor operations,
691/// using `append`/`append_dup` for sorted tables where possible (MDBX fast path that skips
692/// B-tree traversal). Commits happen every [`STORAGE_COMMIT_THRESHOLD`] storage units to
693/// bound MDBX dirty page accumulation.
694///
695/// The dump is written over the state already in the database (e.g. the genesis alloc written by
696/// `init-state --without-evm`): each dump slot replaces the same existing slot, while existing
697/// accounts and slots the dump omits are left untouched. Replaced accounts and slots keep their
698/// history: the change sets record their previous values and `block` is appended to their existing
699/// history shards.
700///
701/// NOTE: This function is not idempotent. If the process crashes mid-import, the database
702/// must be wiped before retrying.
703fn dump_state<PF>(
704    mut collector: Collector<Address, GenesisAccount>,
705    provider_factory: &PF,
706    block: u64,
707) -> Result<(), eyre::Error>
708where
709    PF: DatabaseProviderFactory<ProviderRW: DBProvider<Tx: DbTxMut>>,
710    PF::ProviderRW: StaticFileProviderFactory + StorageSettingsCache + RocksDBProviderFactory,
711{
712    let storage_settings = provider_factory.database_provider_rw()?.cached_storage_settings();
713    if storage_settings.storage_v2 {
714        return dump_state_v2(collector, provider_factory, block)
715    }
716
717    let accounts_len = collector.len();
718    let mut total_accounts: usize = 0;
719    let mut storage_units: usize = 0;
720
721    // pre-allocate the history list once — every entry uses the same single-block bitmap
722    let history_list = IntegerList::new([block])?;
723
724    // track seen bytecode hashes to avoid re-hashing and re-writing duplicates
725    let mut seen_bytecodes: B256Set = B256Set::default();
726
727    let mut provider_rw = provider_factory.database_provider_rw()?;
728
729    for entry in collector.iter()? {
730        let (address_raw, account_raw) = entry?;
731        let (address, _) = Address::from_compact(address_raw.as_slice(), address_raw.len());
732        let (account, _) = GenesisAccount::from_compact(account_raw.as_slice(), account_raw.len());
733
734        let account_storage_len = account.storage.as_ref().map_or(0, |s| s.len());
735        let account_units = 1 + account_storage_len;
736
737        // commit before this account would push us over the threshold
738        if storage_units > 0 && storage_units + account_units > STORAGE_COMMIT_THRESHOLD {
739            provider_rw.commit()?;
740            provider_rw = provider_factory.database_provider_rw()?;
741            info!(target: "reth::cli",
742                total_accounts,
743                accounts_len,
744                storage_units,
745                "Committed chunk"
746            );
747            storage_units = 0;
748            seen_bytecodes = B256Set::default();
749        }
750
751        write_account_to_db(
752            provider_rw.tx_ref(),
753            &address,
754            &account,
755            block,
756            &history_list,
757            &mut seen_bytecodes,
758        )?;
759
760        total_accounts += 1;
761        storage_units += account_units;
762
763        if total_accounts.is_multiple_of(100_000) {
764            info!(target: "reth::cli", total_accounts, accounts_len, "Writing accounts...");
765        }
766    }
767
768    // commit final batch
769    provider_rw.commit()?;
770
771    info!(target: "reth::cli", total_accounts, "All accounts written to database");
772
773    Ok(())
774}
775
776fn dump_state_v2<PF>(
777    mut collector: Collector<Address, GenesisAccount>,
778    provider_factory: &PF,
779    block: u64,
780) -> Result<(), eyre::Error>
781where
782    PF: DatabaseProviderFactory<
783        ProviderRW: StaticFileProviderFactory
784                        + DBProvider<Tx: DbTxMut>
785                        + StorageSettingsCache
786                        + RocksDBProviderFactory,
787    >,
788{
789    let accounts_len = collector.len();
790    let mut total_accounts: usize = 0;
791    let mut storage_units: usize = 0;
792
793    // pre-allocate the history list once — every entry uses the same single-block bitmap
794    let history_list = IntegerList::new([block])?;
795
796    // track seen bytecode hashes to avoid re-hashing and re-writing duplicates
797    let mut seen_bytecodes: B256Set = B256Set::default();
798
799    let mut provider_rw = provider_factory.database_provider_rw()?;
800    let static_file_provider = provider_rw.static_file_provider();
801    let rocksdb_provider = provider_rw.rocksdb_provider();
802    let mut history_batch = rocksdb_provider.batch_with_auto_commit();
803    if snapshot_state_tables_empty(provider_rw.tx_ref())? {
804        reset_pre_snapshot_changeset_segment(
805            &static_file_provider,
806            StaticFileSegment::AccountChangeSets,
807            block,
808        )?;
809        reset_pre_snapshot_changeset_segment(
810            &static_file_provider,
811            StaticFileSegment::StorageChangeSets,
812            block,
813        )?;
814    }
815
816    {
817        let mut account_changeset_writer =
818            static_file_provider.get_writer(block, StaticFileSegment::AccountChangeSets)?;
819        let mut storage_changeset_writer =
820            static_file_provider.get_writer(block, StaticFileSegment::StorageChangeSets)?;
821        prepare_account_changeset_writer(&mut account_changeset_writer, block)?;
822        prepare_storage_changeset_writer(&mut storage_changeset_writer, block)?;
823
824        for entry in collector.iter()? {
825            let (address_raw, account_raw) = entry?;
826            let (address, _) = Address::from_compact(address_raw.as_slice(), address_raw.len());
827            let (account, _) =
828                GenesisAccount::from_compact(account_raw.as_slice(), account_raw.len());
829
830            let account_storage_len = account.storage.as_ref().map_or(0, |s| s.len());
831            let account_units = 1 + account_storage_len;
832
833            // commit before this account would push us over the threshold
834            if storage_units > 0 && storage_units + account_units > STORAGE_COMMIT_THRESHOLD {
835                history_batch.commit()?;
836                commit_mdbx_only(provider_rw)?;
837                provider_rw = provider_factory.database_provider_rw()?;
838                history_batch = rocksdb_provider.batch_with_auto_commit();
839                info!(target: "reth::cli",
840                    total_accounts,
841                    accounts_len,
842                    storage_units,
843                    "Committed chunk"
844                );
845                storage_units = 0;
846                seen_bytecodes = B256Set::default();
847            }
848
849            write_account_to_db_v2(
850                provider_rw.tx_ref(),
851                (&mut account_changeset_writer, &mut storage_changeset_writer),
852                &mut history_batch,
853                &address,
854                &account,
855                block,
856                &history_list,
857                &mut seen_bytecodes,
858            )?;
859
860            total_accounts += 1;
861            storage_units += account_units;
862
863            if total_accounts.is_multiple_of(100_000) {
864                info!(target: "reth::cli", total_accounts, accounts_len, "Writing accounts...");
865            }
866        }
867    }
868
869    history_batch.commit()?;
870    commit_mdbx_only(provider_rw)?;
871    static_file_provider.finalize()?;
872
873    info!(target: "reth::cli", total_accounts, "All accounts written to database");
874
875    Ok(())
876}
877
878fn prepare_account_changeset_writer<N: NodePrimitives>(
879    writer: &mut reth_provider::providers::StaticFileProviderRWRefMut<'_, N>,
880    block: u64,
881) -> ProviderResult<()> {
882    let next_block = writer.next_block_number();
883    if next_block < block {
884        info!(
885            target: "reth::cli",
886            from_block = next_block,
887            to_block = block - 1,
888            "Padding empty account changesets before state import"
889        );
890        for empty_block in next_block..block {
891            writer.append_account_changeset(Vec::new(), empty_block)?;
892            if empty_block > next_block && empty_block.is_multiple_of(1_000_000) {
893                info!(
894                    target: "reth::cli",
895                    padded_to_block = empty_block,
896                    "Padded empty account changesets"
897                );
898            }
899        }
900    }
901
902    writer.begin_account_changeset(block)
903}
904
905fn prepare_storage_changeset_writer<N: NodePrimitives>(
906    writer: &mut reth_provider::providers::StaticFileProviderRWRefMut<'_, N>,
907    block: u64,
908) -> ProviderResult<()> {
909    let next_block = writer.next_block_number();
910    if next_block < block {
911        info!(
912            target: "reth::cli",
913            from_block = next_block,
914            to_block = block - 1,
915            "Padding empty storage changesets before state import"
916        );
917        for empty_block in next_block..block {
918            writer.append_storage_changeset(Vec::new(), empty_block)?;
919            if empty_block > next_block && empty_block.is_multiple_of(1_000_000) {
920                info!(
921                    target: "reth::cli",
922                    padded_to_block = empty_block,
923                    "Padded empty storage changesets"
924                );
925            }
926        }
927    }
928
929    writer.begin_storage_changeset(block)
930}
931
932fn snapshot_state_tables_empty<TX: reth_db_api::transaction::DbTx>(
933    tx: &TX,
934) -> ProviderResult<bool> {
935    Ok(tx.entries::<tables::PlainAccountState>()? == 0 &&
936        tx.entries::<tables::PlainStorageState>()? == 0 &&
937        tx.entries::<tables::HashedAccounts>()? == 0 &&
938        tx.entries::<tables::HashedStorages>()? == 0 &&
939        tx.entries::<tables::AccountChangeSets>()? == 0 &&
940        tx.entries::<tables::StorageChangeSets>()? == 0 &&
941        tx.entries::<tables::Bytecodes>()? == 0)
942}
943
944fn reset_pre_snapshot_changeset_segment<N: NodePrimitives>(
945    static_file_provider: &reth_provider::providers::StaticFileProvider<N>,
946    segment: StaticFileSegment,
947    block: u64,
948) -> ProviderResult<()> {
949    if block == 0 {
950        return Ok(())
951    }
952
953    let Some(highest_block) = static_file_provider.get_highest_static_file_block(segment) else {
954        return Ok(())
955    };
956
957    if highest_block >= block {
958        return Ok(())
959    }
960
961    let file_start = static_file_provider.find_fixed_range(segment, block).start();
962    info!(
963        target: "reth::cli",
964        ?segment,
965        highest_block,
966        import_block = block,
967        file_start,
968        "Resetting pre-snapshot changeset static files before state import"
969    );
970    static_file_provider.delete_segment(segment)?;
971
972    Ok(())
973}
974
975fn commit_mdbx_only<Provider>(provider: Provider) -> ProviderResult<()>
976where
977    Provider: DBProvider<Tx: DbTxMut>,
978{
979    reth_db_api::transaction::DbTx::commit(provider.into_tx()).map_err(ProviderError::from)
980}
981
982/// Writes one storage slot into a `DupSort` storage table, replacing any existing entry for the
983/// same slot, and returns the replaced value.
984///
985/// On `DupSort` tables `upsert`/`append_dup` add a new duplicate instead of replacing an entry with
986/// the same subkey, so a slot already written from the genesis alloc has to be deleted first. Only
987/// that slot is touched: a state dump may omit an account's slots that are unchanged since genesis,
988/// so the account's other existing slots must survive. A zero value deletes the slot.
989fn replace_storage_slot<T, C>(
990    cursor: &mut C,
991    key: T::Key,
992    entry: StorageEntry,
993) -> Result<Option<U256>, DatabaseError>
994where
995    T: DupSort<SubKey = B256, Value = StorageEntry>,
996    T::Key: Clone,
997    C: DbDupCursorRO<T> + DbCursorRW<T>,
998{
999    let previous = cursor
1000        .seek_by_key_subkey(key.clone(), entry.key)?
1001        .filter(|existing| existing.key == entry.key)
1002        .map(|existing| existing.value);
1003    if previous.is_some() {
1004        cursor.delete_current()?;
1005    }
1006    if !entry.value.is_zero() {
1007        cursor.upsert(key, &entry)?;
1008    }
1009    Ok(previous)
1010}
1011
1012/// Returns the last history shard of an account or slot whose existing state the dump replaces,
1013/// with `block` appended.
1014///
1015/// Existing state comes from the genesis alloc, so the shard holds at most the genesis block and
1016/// never needs rechunking. `block` is not appended again when the dump is imported at the genesis
1017/// block itself.
1018fn history_shard_with_block(
1019    shard: Option<IntegerList>,
1020    block: u64,
1021) -> Result<IntegerList, IntegerListError> {
1022    let mut shard = shard.unwrap_or_else(IntegerList::empty);
1023    if shard.max().is_none_or(|last| last < block) {
1024        shard.push(block)?;
1025    }
1026    Ok(shard)
1027}
1028
1029/// Writes a single account and all its storage to every required DB table directly,
1030/// without building intermediary structures.
1031///
1032/// Uses `append_dup` for `DupSort` tables where insertion order matches key order (the ETL
1033/// collector sorts by address, so `AccountChangeSets` and `StorageChangeSets` receive data in
1034/// sorted order within each account). State tables use `put`/`upsert` to replace existing
1035/// genesis entries, with storage slots deleted before replacement.
1036#[allow(clippy::clone_on_copy)]
1037fn write_account_to_db<TX: DbTx + DbTxMut>(
1038    tx: &TX,
1039    address: &Address,
1040    genesis_account: &GenesisAccount,
1041    block: u64,
1042    history_list: &IntegerList,
1043    seen_bytecodes: &mut B256Set,
1044) -> Result<(), eyre::Error> {
1045    let bytecode_hash = if let Some(code) = &genesis_account.code {
1046        let bytecode = Bytecode::new_raw_checked(code.clone())
1047            .map_err(|e| eyre::eyre!("Invalid bytecode for {address}: {e}"))?;
1048        let hash = bytecode.hash_slow();
1049        if seen_bytecodes.insert(hash) {
1050            tx.put::<tables::Bytecodes>(hash, bytecode)?;
1051        }
1052        Some(hash)
1053    } else {
1054        None
1055    };
1056
1057    let account = Account { bytecode_hash, ..Account::from(genesis_account) };
1058
1059    let hashed_address = keccak256(address);
1060    let previous_account = tx.get::<tables::PlainAccountState>(*address)?;
1061    let replaces_account = previous_account.is_some();
1062
1063    // plain state — sorted by address (ETL order), use append
1064    tx.put::<tables::PlainAccountState>(*address, account.clone())?;
1065
1066    // hashed state — unsorted (keccak scrambles order), must use put
1067    tx.put::<tables::HashedAccounts>(hashed_address, account)?;
1068
1069    // account changeset — DupSort keyed by block, subkey sorted by address (ETL order)
1070    let mut acct_cs_cursor = tx.cursor_dup_write::<tables::AccountChangeSets>()?;
1071    acct_cs_cursor
1072        .append_dup(block, AccountBeforeTx { address: *address, info: previous_account })?;
1073
1074    // account history
1075    let history_key = ShardedKey::new(*address, u64::MAX);
1076    let account_history = if replaces_account {
1077        history_shard_with_block(tx.get::<tables::AccountsHistory>(history_key.clone())?, block)?
1078    } else {
1079        history_list.clone()
1080    };
1081    tx.put::<tables::AccountsHistory>(history_key, account_history)?;
1082
1083    // storage entries: each dump slot replaces the same slot from the genesis alloc, other existing
1084    // slots of the account stay (see `replace_storage_slot`).
1085    if let Some(storage) = &genesis_account.storage {
1086        let mut hashed_storage_cursor = tx.cursor_dup_write::<tables::HashedStorages>()?;
1087        let mut plain_storage_cursor = tx.cursor_dup_write::<tables::PlainStorageState>()?;
1088        let mut storage_cs_cursor = tx.cursor_dup_write::<tables::StorageChangeSets>()?;
1089
1090        for (&key, &value) in storage {
1091            let value_u256 = U256::from_be_bytes(value.0);
1092
1093            let previous_value = replace_storage_slot(
1094                &mut plain_storage_cursor,
1095                *address,
1096                StorageEntry { key, value: value_u256 },
1097            )?;
1098            replace_storage_slot(
1099                &mut hashed_storage_cursor,
1100                hashed_address,
1101                StorageEntry { key: keccak256(key), value: value_u256 },
1102            )?;
1103
1104            // storage changeset — sorted by (block, address), then by key via append_dup
1105            storage_cs_cursor.append_dup(
1106                BlockNumberAddress((block, *address)),
1107                StorageEntry { key, value: previous_value.unwrap_or_default() },
1108            )?;
1109
1110            // storage history
1111            let history_key = StorageShardedKey::new(*address, key, u64::MAX);
1112            let storage_history = if previous_value.is_some() {
1113                history_shard_with_block(
1114                    tx.get::<tables::StoragesHistory>(history_key.clone())?,
1115                    block,
1116                )?
1117            } else {
1118                history_list.clone()
1119            };
1120            tx.put::<tables::StoragesHistory>(history_key, storage_history)?;
1121        }
1122    }
1123
1124    Ok(())
1125}
1126
1127/// Writes a single account to the v2 storage destinations.
1128///
1129/// Storage v2 uses hashed state as the canonical state, static-file change sets, and `RocksDB`
1130/// history indices. The ETL collector yields accounts sorted by address and genesis storage is a
1131/// `BTreeMap`, so the streaming static-file writes preserve the required order.
1132#[expect(clippy::too_many_arguments)]
1133fn write_account_to_db_v2<TX, N>(
1134    tx: &TX,
1135    changeset_writers: (
1136        &mut reth_provider::providers::StaticFileProviderRWRefMut<'_, N>,
1137        &mut reth_provider::providers::StaticFileProviderRWRefMut<'_, N>,
1138    ),
1139    history_batch: &mut reth_provider::providers::RocksDBBatch<'_>,
1140    address: &Address,
1141    genesis_account: &GenesisAccount,
1142    block: u64,
1143    history_list: &IntegerList,
1144    seen_bytecodes: &mut B256Set,
1145) -> Result<(), eyre::Error>
1146where
1147    TX: DbTx + DbTxMut,
1148    N: NodePrimitives,
1149{
1150    let bytecode_hash = if let Some(code) = &genesis_account.code {
1151        let bytecode = Bytecode::new_raw_checked(code.clone())
1152            .map_err(|e| eyre::eyre!("Invalid bytecode for {address}: {e}"))?;
1153        let hash = bytecode.hash_slow();
1154        if seen_bytecodes.insert(hash) {
1155            tx.put::<tables::Bytecodes>(hash, bytecode)?;
1156        }
1157        Some(hash)
1158    } else {
1159        None
1160    };
1161
1162    let account = Account { bytecode_hash, ..Account::from(genesis_account) };
1163
1164    let hashed_address = keccak256(address);
1165    let (account_changeset_writer, storage_changeset_writer) = changeset_writers;
1166
1167    let previous_account = tx.get::<tables::HashedAccounts>(hashed_address)?;
1168    let replaces_account = previous_account.is_some();
1169    tx.put::<tables::HashedAccounts>(hashed_address, account)?;
1170    account_changeset_writer.append_account_changeset_entry(AccountBeforeTx {
1171        address: *address,
1172        info: previous_account,
1173    })?;
1174    let history_key = ShardedKey::new(*address, u64::MAX);
1175    if replaces_account {
1176        let account_history = history_shard_with_block(
1177            history_batch.get::<tables::AccountsHistory>(history_key.clone())?,
1178            block,
1179        )?;
1180        history_batch.put::<tables::AccountsHistory>(history_key, &account_history)?;
1181    } else {
1182        history_batch.put::<tables::AccountsHistory>(history_key, history_list)?;
1183    }
1184
1185    if let Some(storage) = &genesis_account.storage {
1186        // Each dump slot replaces the same slot from the genesis alloc; see `replace_storage_slot`.
1187        let mut hashed_storage_cursor = tx.cursor_dup_write::<tables::HashedStorages>()?;
1188
1189        for (&key, &value) in storage {
1190            let value_u256 = U256::from_be_bytes(value.0);
1191
1192            let previous_value = replace_storage_slot(
1193                &mut hashed_storage_cursor,
1194                hashed_address,
1195                StorageEntry { key: keccak256(key), value: value_u256 },
1196            )?;
1197
1198            storage_changeset_writer.append_storage_changeset_entry(
1199                reth_db_api::models::StorageBeforeTx {
1200                    address: *address,
1201                    key,
1202                    value: previous_value.unwrap_or_default(),
1203                },
1204            )?;
1205
1206            let history_key = StorageShardedKey::new(*address, key, u64::MAX);
1207            if previous_value.is_some() {
1208                let storage_history = history_shard_with_block(
1209                    history_batch.get::<tables::StoragesHistory>(history_key.clone())?,
1210                    block,
1211                )?;
1212                history_batch.put::<tables::StoragesHistory>(history_key, &storage_history)?;
1213            } else {
1214                history_batch.put::<tables::StoragesHistory>(history_key, history_list)?;
1215            }
1216        }
1217    }
1218
1219    Ok(())
1220}
1221
1222/// Computes the state root (from scratch) based on the accounts and storages present in the
1223/// database.
1224fn compute_state_root<Provider>(
1225    provider: &Provider,
1226    prefix_sets: Option<TriePrefixSets>,
1227) -> Result<B256, InitStorageError>
1228where
1229    Provider: DBProvider<Tx: DbTxMut> + TrieWriter + StorageSettingsCache,
1230{
1231    reth_trie_db::with_adapter!(provider, |A| {
1232        compute_state_root_inner::<_, A>(provider, prefix_sets)
1233    })
1234}
1235
1236fn compute_state_root_inner<Provider, A>(
1237    provider: &Provider,
1238    prefix_sets: Option<TriePrefixSets>,
1239) -> Result<B256, InitStorageError>
1240where
1241    Provider: DBProvider<Tx: DbTxMut> + TrieWriter + StorageSettingsCache,
1242    A: reth_trie_db::TrieTableAdapter,
1243{
1244    trace!(target: "reth::cli", "Computing state root");
1245
1246    let tx = provider.tx_ref();
1247    let mut intermediate_state: Option<IntermediateStateRootState> = None;
1248    let mut total_flushed_updates = 0;
1249
1250    loop {
1251        let mut state_root =
1252            DbStateRoot::<_, A>::from_tx(tx).with_intermediate_state(intermediate_state);
1253
1254        if let Some(sets) = prefix_sets.clone() {
1255            state_root = state_root.with_prefix_sets(sets);
1256        }
1257
1258        match state_root.root_with_progress()? {
1259            StateRootProgress::Progress(state, _, updates) => {
1260                let updated_len = provider.write_trie_updates(updates)?;
1261                total_flushed_updates += updated_len;
1262
1263                trace!(target: "reth::cli",
1264                    last_account_key = %state.account_root_state.last_hashed_key,
1265                    updated_len,
1266                    total_flushed_updates,
1267                    "Flushing trie updates"
1268                );
1269
1270                intermediate_state = Some(*state);
1271
1272                if total_flushed_updates.is_multiple_of(SOFT_LIMIT_COUNT_FLUSHED_UPDATES) {
1273                    info!(target: "reth::cli",
1274                        total_flushed_updates,
1275                        "Flushing trie updates"
1276                    );
1277                }
1278            }
1279            StateRootProgress::Complete(root, _, updates) => {
1280                let updated_len = provider.write_trie_updates(updates)?;
1281                total_flushed_updates += updated_len;
1282
1283                trace!(target: "reth::cli",
1284                    %root,
1285                    updated_len,
1286                    total_flushed_updates,
1287                    "State root has been computed"
1288                );
1289
1290                return Ok(root)
1291            }
1292        }
1293    }
1294}
1295
1296/// Computes the state root (from scratch) with periodic commits to free MDBX dirty pages.
1297///
1298/// Opens a fresh transaction each iteration to release dirty pages, preventing OOM on large
1299/// states where trie updates accumulate gigabytes of MDBX dirty pages.
1300fn compute_state_root_chunked<PF>(provider_factory: &PF) -> Result<B256, InitStorageError>
1301where
1302    PF: DatabaseProviderFactory<
1303        ProviderRW: DBProvider<Tx: DbTxMut> + TrieWriter + StorageSettingsCache,
1304    >,
1305{
1306    let provider_rw = provider_factory.database_provider_rw().map_err(provider_db_err)?;
1307
1308    reth_trie_db::with_adapter!(&provider_rw, |A| {
1309        drop(provider_rw);
1310        compute_state_root_chunked_inner::<PF, A>(provider_factory)
1311    })
1312}
1313
1314fn compute_state_root_chunked_inner<PF, A>(provider_factory: &PF) -> Result<B256, InitStorageError>
1315where
1316    PF: DatabaseProviderFactory<
1317        ProviderRW: DBProvider<Tx: DbTxMut> + TrieWriter + StorageSettingsCache,
1318    >,
1319    A: reth_trie_db::TrieTableAdapter,
1320{
1321    trace!(target: "reth::cli", "Computing state root");
1322
1323    let mut intermediate_state: Option<IntermediateStateRootState> = None;
1324    let mut total_flushed_updates = 0;
1325
1326    loop {
1327        let provider_rw = provider_factory.database_provider_rw().map_err(provider_db_err)?;
1328        let tx = provider_rw.tx_ref();
1329
1330        let state_root = DbStateRoot::<_, A>::from_tx(tx)
1331            .with_intermediate_state(intermediate_state.take())
1332            .with_threshold(STATE_ROOT_COMMIT_THRESHOLD);
1333
1334        match state_root.root_with_progress()? {
1335            StateRootProgress::Progress(state, _, updates) => {
1336                let updated_len = provider_rw.write_trie_updates(updates)?;
1337                total_flushed_updates += updated_len;
1338
1339                info!(target: "reth::cli",
1340                    last_account_key = %state.account_root_state.last_hashed_key,
1341                    updated_len,
1342                    total_flushed_updates,
1343                    "Flushing trie updates (committing to free memory)"
1344                );
1345
1346                intermediate_state = Some(*state);
1347                provider_rw.commit().map_err(provider_db_err)?;
1348            }
1349            StateRootProgress::Complete(root, _, updates) => {
1350                let updated_len = provider_rw.write_trie_updates(updates)?;
1351                total_flushed_updates += updated_len;
1352
1353                info!(target: "reth::cli",
1354                    %root,
1355                    updated_len,
1356                    total_flushed_updates,
1357                    "State root computation complete"
1358                );
1359
1360                provider_rw.commit().map_err(provider_db_err)?;
1361                return Ok(root)
1362            }
1363        }
1364    }
1365}
1366
1367/// Converts a provider error into an [`InitStorageError`].
1368fn provider_db_err(e: impl std::fmt::Display) -> InitStorageError {
1369    InitStorageError::from(StateRootError::Database(DatabaseError::Other(e.to_string())))
1370}
1371
1372/// Type to deserialize state root from state dump file.
1373#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
1374struct StateRoot {
1375    root: B256,
1376}
1377
1378/// An account as in the state dump file. This contains a [`GenesisAccount`] and the account's
1379/// address.
1380#[derive(Debug, Serialize, Deserialize)]
1381struct GenesisAccountWithAddress {
1382    /// The account's balance, nonce, code, and storage.
1383    #[serde(flatten)]
1384    genesis_account: GenesisAccount,
1385    /// The account's address.
1386    address: Address,
1387}
1388
1389#[cfg(test)]
1390mod tests {
1391    use super::*;
1392    use alloy_consensus::constants::{
1393        HOLESKY_GENESIS_HASH, MAINNET_GENESIS_HASH, SEPOLIA_GENESIS_HASH,
1394    };
1395    use alloy_genesis::Genesis;
1396    use reth_chainspec::{Chain, ChainSpec, HOLESKY, MAINNET, SEPOLIA};
1397    use reth_db::DatabaseEnv;
1398    use reth_db_api::{
1399        cursor::DbCursorRO,
1400        models::{storage_sharded_key::StorageShardedKey, IntegerList, ShardedKey},
1401        table::{Table, TableRow},
1402        transaction::DbTx,
1403        Database,
1404    };
1405    use reth_provider::{
1406        test_utils::{create_test_provider_factory_with_chain_spec, MockNodeTypesWithDB},
1407        ProviderFactory, RocksDBProviderFactory,
1408    };
1409    use std::{collections::BTreeMap, sync::Arc};
1410
1411    fn collect_table_entries<DB, T>(
1412        tx: &<DB as Database>::TX,
1413    ) -> Result<Vec<TableRow<T>>, InitStorageError>
1414    where
1415        DB: Database,
1416        T: Table,
1417    {
1418        Ok(tx.cursor_read::<T>()?.walk_range(..)?.collect::<Result<Vec<_>, _>>()?)
1419    }
1420
1421    #[test]
1422    fn parse_accounts_streams_jsonl_accounts() {
1423        let input = br#"{"address":"0x0000000000000000000000000000000000000002","balance":"0x2"}
1424{"address":"0x0000000000000000000000000000000000000001","balance":"0x1"}
1425"#;
1426
1427        let mut collector =
1428            parse_accounts(&input[..], EtlConfig::new(None, 128)).expect("parse succeeds");
1429
1430        let accounts = collector
1431            .iter()
1432            .unwrap()
1433            .map(|entry| {
1434                let (address_raw, account_raw) = entry.unwrap();
1435                let (address, _) = Address::from_compact(address_raw.as_slice(), address_raw.len());
1436                let (account, _) =
1437                    GenesisAccount::from_compact(account_raw.as_slice(), account_raw.len());
1438                (address, account.balance)
1439            })
1440            .collect::<Vec<_>>();
1441
1442        assert_eq!(
1443            accounts,
1444            vec![
1445                (Address::with_last_byte(1), U256::ONE),
1446                (Address::with_last_byte(2), U256::from(2))
1447            ]
1448        );
1449    }
1450
1451    #[test]
1452    fn dump_state_uses_storage_v2_destinations() {
1453        let storage_key = B256::with_last_byte(3);
1454        let input = br#"{"address":"0x0000000000000000000000000000000000000001","balance":"0x1"}
1455{"address":"0x0000000000000000000000000000000000000002","balance":"0x0","storage":{"0x0000000000000000000000000000000000000000000000000000000000000003":"0x0000000000000000000000000000000000000000000000000000000000000004"}}
1456"#;
1457
1458        let collector = parse_accounts(&input[..], EtlConfig::new(None, 128)).unwrap();
1459        let factory = create_test_provider_factory_with_chain_spec(MAINNET.clone());
1460        factory.set_storage_settings_cache(StorageSettings::v2());
1461        let block = 10;
1462
1463        dump_state(collector, &factory, block).unwrap();
1464
1465        let provider = factory.provider().unwrap();
1466        let tx = provider.tx_ref();
1467        assert_eq!(tx.entries::<tables::PlainAccountState>().unwrap(), 0);
1468        assert_eq!(tx.entries::<tables::PlainStorageState>().unwrap(), 0);
1469        assert_eq!(tx.entries::<tables::AccountChangeSets>().unwrap(), 0);
1470        assert_eq!(tx.entries::<tables::StorageChangeSets>().unwrap(), 0);
1471        assert_eq!(tx.entries::<tables::HashedAccounts>().unwrap(), 2);
1472        assert_eq!(tx.entries::<tables::HashedStorages>().unwrap(), 1);
1473
1474        let address_with_balance = Address::with_last_byte(1);
1475        let address_with_storage = Address::with_last_byte(2);
1476        assert_eq!(
1477            reth_provider::ChangeSetReader::account_block_changeset(&provider, block).unwrap(),
1478            vec![
1479                AccountBeforeTx { address: address_with_balance, info: None },
1480                AccountBeforeTx { address: address_with_storage, info: None }
1481            ]
1482        );
1483        assert_eq!(
1484            reth_provider::StorageChangeSetReader::storage_changeset(&provider, block).unwrap(),
1485            vec![(
1486                BlockNumberAddress((block, address_with_storage)),
1487                StorageEntry { key: storage_key, value: U256::ZERO }
1488            )]
1489        );
1490
1491        let rocksdb = factory.rocksdb_provider();
1492        let accounts = rocksdb
1493            .iter::<tables::AccountsHistory>()
1494            .unwrap()
1495            .collect::<Result<Vec<_>, _>>()
1496            .unwrap();
1497        let storages = rocksdb
1498            .iter::<tables::StoragesHistory>()
1499            .unwrap()
1500            .collect::<Result<Vec<_>, _>>()
1501            .unwrap();
1502
1503        assert_eq!(
1504            accounts,
1505            vec![
1506                (
1507                    ShardedKey::new(address_with_balance, u64::MAX),
1508                    IntegerList::new([block]).unwrap()
1509                ),
1510                (
1511                    ShardedKey::new(address_with_storage, u64::MAX),
1512                    IntegerList::new([block]).unwrap()
1513                )
1514            ]
1515        );
1516        assert_eq!(
1517            storages,
1518            vec![(
1519                StorageShardedKey::new(address_with_storage, storage_key, u64::MAX),
1520                IntegerList::new([block]).unwrap()
1521            )]
1522        );
1523    }
1524
1525    #[test]
1526    fn dump_state_v2_resets_presnapshot_changeset_static_files() {
1527        let storage_key = B256::with_last_byte(3);
1528        let input = br#"{"address":"0x0000000000000000000000000000000000000002","balance":"0x0","storage":{"0x0000000000000000000000000000000000000000000000000000000000000003":"0x0000000000000000000000000000000000000000000000000000000000000004"}}
1529"#;
1530
1531        let collector = parse_accounts(&input[..], EtlConfig::new(None, 128)).unwrap();
1532        let factory = create_test_provider_factory_with_chain_spec(MAINNET.clone());
1533        factory.set_storage_settings_cache(StorageSettings::v2());
1534        let static_files = factory.static_file_provider();
1535
1536        {
1537            let mut writer =
1538                static_files.get_writer(0, StaticFileSegment::AccountChangeSets).unwrap();
1539            writer.append_account_changeset(Vec::new(), 0).unwrap();
1540        }
1541        {
1542            let mut writer =
1543                static_files.get_writer(0, StaticFileSegment::StorageChangeSets).unwrap();
1544            writer.append_storage_changeset(Vec::new(), 0).unwrap();
1545        }
1546        static_files.commit().unwrap();
1547
1548        let block = 500_010;
1549        dump_state(collector, &factory, block).unwrap();
1550        static_files.initialize_index().unwrap();
1551
1552        let provider = factory.provider().unwrap();
1553        let address = Address::with_last_byte(2);
1554        assert!(reth_provider::ChangeSetReader::account_block_changeset(&provider, 5)
1555            .unwrap()
1556            .is_empty());
1557        assert!(reth_provider::StorageChangeSetReader::storage_changeset(&provider, 5)
1558            .unwrap()
1559            .is_empty());
1560
1561        let account_file_start =
1562            static_files.find_fixed_range(StaticFileSegment::AccountChangeSets, block).start();
1563        let storage_file_start =
1564            static_files.find_fixed_range(StaticFileSegment::StorageChangeSets, block).start();
1565        assert_eq!(account_file_start, 500_000);
1566        assert_eq!(storage_file_start, 500_000);
1567        assert!(reth_provider::ChangeSetReader::account_block_changeset(
1568            &provider,
1569            account_file_start
1570        )
1571        .unwrap()
1572        .is_empty());
1573        assert!(reth_provider::StorageChangeSetReader::storage_changeset(
1574            &provider,
1575            storage_file_start
1576        )
1577        .unwrap()
1578        .is_empty());
1579
1580        assert_eq!(
1581            reth_provider::ChangeSetReader::account_block_changeset(&provider, block).unwrap(),
1582            vec![AccountBeforeTx { address, info: None }]
1583        );
1584        assert_eq!(
1585            reth_provider::StorageChangeSetReader::storage_changeset(&provider, block).unwrap(),
1586            vec![(
1587                BlockNumberAddress((block, address)),
1588                StorageEntry { key: storage_key, value: U256::ZERO }
1589            )]
1590        );
1591
1592        let account_offsets = static_files
1593            .get_segment_provider_for_block(StaticFileSegment::AccountChangeSets, block, None)
1594            .unwrap()
1595            .read_changeset_offsets()
1596            .unwrap()
1597            .unwrap();
1598        let storage_offsets = static_files
1599            .get_segment_provider_for_block(StaticFileSegment::StorageChangeSets, block, None)
1600            .unwrap()
1601            .read_changeset_offsets()
1602            .unwrap()
1603            .unwrap();
1604        assert_eq!(account_offsets.len() as u64, block - account_file_start + 1);
1605        assert_eq!(storage_offsets.len() as u64, block - storage_file_start + 1);
1606    }
1607
1608    /// Imports a state dump over a database whose genesis alloc already holds state, as
1609    /// `init-state --without-evm` does, and checks the result is the genesis state overlaid by the
1610    /// dump: dump accounts replace their account fields, each dump slot replaces the same genesis
1611    /// slot (a zero value deletes it), and genesis accounts and slots the dump omits survive.
1612    /// Replaced genesis accounts and slots keep their history.
1613    fn assert_dump_state_overlays_genesis_state(settings: StorageSettings) {
1614        let dump_only = Address::with_last_byte(1);
1615        let overlapping = Address::with_last_byte(2);
1616        let no_dump_storage = Address::with_last_byte(3);
1617        let genesis_only = Address::with_last_byte(4);
1618        let slot = B256::with_last_byte;
1619        let val = B256::with_last_byte;
1620
1621        let genesis_alloc = BTreeMap::from([
1622            (
1623                overlapping,
1624                GenesisAccount {
1625                    balance: U256::from(1),
1626                    storage: Some(BTreeMap::from([
1627                        // Overwritten by the dump.
1628                        (slot(1), val(0x11)),
1629                        // Omitted by the dump (unchanged since genesis): must survive.
1630                        (slot(2), val(0x12)),
1631                        // Present in the dump with the same value.
1632                        (slot(4), val(0x14)),
1633                        // Zeroed by the dump: must be deleted.
1634                        (slot(6), val(0x16)),
1635                    ])),
1636                    ..Default::default()
1637                },
1638            ),
1639            (
1640                no_dump_storage,
1641                GenesisAccount {
1642                    storage: Some(BTreeMap::from([(slot(7), val(0x17))])),
1643                    ..Default::default()
1644                },
1645            ),
1646            (
1647                genesis_only,
1648                GenesisAccount {
1649                    balance: U256::from(9),
1650                    storage: Some(BTreeMap::from([(slot(8), val(0x18))])),
1651                    ..Default::default()
1652                },
1653            ),
1654        ]);
1655        let dump_alloc = BTreeMap::from([
1656            (
1657                dump_only,
1658                GenesisAccount {
1659                    balance: U256::from(5),
1660                    storage: Some(BTreeMap::from([(slot(5), val(0x25))])),
1661                    ..Default::default()
1662                },
1663            ),
1664            (
1665                overlapping,
1666                GenesisAccount {
1667                    balance: U256::from(2),
1668                    storage: Some(BTreeMap::from([
1669                        (slot(1), val(0x21)),
1670                        (slot(3), val(0x23)),
1671                        (slot(4), val(0x14)),
1672                        (slot(6), B256::ZERO),
1673                    ])),
1674                    ..Default::default()
1675                },
1676            ),
1677            // In the dump without storage: its genesis slot is not listed, so it stays.
1678            (no_dump_storage, GenesisAccount { balance: U256::from(3), ..Default::default() }),
1679        ]);
1680
1681        // Genesis overlaid by the dump.
1682        let mut expected_alloc = genesis_alloc.clone();
1683        for (address, account) in &dump_alloc {
1684            let mut storage =
1685                genesis_alloc.get(address).and_then(|a| a.storage.clone()).unwrap_or_default();
1686            for (key, value) in account.storage.iter().flatten() {
1687                if value.is_zero() {
1688                    storage.remove(key);
1689                } else {
1690                    storage.insert(*key, *value);
1691                }
1692            }
1693            expected_alloc.insert(
1694                *address,
1695                GenesisAccount {
1696                    storage: (!storage.is_empty()).then_some(storage),
1697                    ..account.clone()
1698                },
1699            );
1700        }
1701        let expected_state_root = reth_trie::root::state_root_ref_unhashed(&expected_alloc);
1702
1703        let chain_spec = Arc::new(ChainSpec {
1704            chain: Chain::from_id(1),
1705            genesis: Genesis { alloc: genesis_alloc, ..Default::default() },
1706            hardforks: Default::default(),
1707            paris_block_and_final_difficulty: None,
1708            deposit_contract: None,
1709            ..Default::default()
1710        });
1711        let factory = create_test_provider_factory_with_chain_spec(chain_spec);
1712        init_genesis_with_settings(&factory, settings).unwrap();
1713        factory.set_storage_settings_cache(settings);
1714
1715        let input = dump_alloc
1716            .iter()
1717            .map(|(address, account)| {
1718                serde_json::to_string(&GenesisAccountWithAddress {
1719                    genesis_account: account.clone(),
1720                    address: *address,
1721                })
1722                .unwrap()
1723            })
1724            .collect::<Vec<_>>()
1725            .join("\n");
1726        let collector = parse_accounts(input.as_bytes(), EtlConfig::new(None, 128)).unwrap();
1727        dump_state(collector, &factory, 10).unwrap();
1728
1729        let storage_of = |address: &Address| -> Vec<(B256, U256)> {
1730            expected_alloc[address]
1731                .storage
1732                .iter()
1733                .flatten()
1734                .map(|(key, value)| (*key, U256::from_be_bytes(value.0)))
1735                .collect()
1736        };
1737
1738        {
1739            let provider = factory.provider().unwrap();
1740            let tx = provider.tx_ref();
1741
1742            let hashed_storage =
1743                collect_table_entries::<DatabaseEnv, tables::HashedStorages>(tx).unwrap();
1744            for address in expected_alloc.keys() {
1745                let mut expected = storage_of(address)
1746                    .into_iter()
1747                    .map(|(key, value)| {
1748                        (keccak256(address), StorageEntry { key: keccak256(key), value })
1749                    })
1750                    .collect::<Vec<_>>();
1751                expected.sort_by_key(|(_, entry)| entry.key);
1752                let actual = hashed_storage
1753                    .iter()
1754                    .filter(|(hashed_address, _)| *hashed_address == keccak256(address))
1755                    .copied()
1756                    .collect::<Vec<_>>();
1757                assert_eq!(actual, expected, "hashed storage of {address}");
1758            }
1759            let expected_slots = expected_alloc.keys().map(|a| storage_of(a).len()).sum::<usize>();
1760            assert_eq!(hashed_storage.len(), expected_slots);
1761
1762            let hashed_accounts =
1763                collect_table_entries::<DatabaseEnv, tables::HashedAccounts>(tx).unwrap();
1764            assert_eq!(hashed_accounts.len(), expected_alloc.len());
1765
1766            let plain_storage =
1767                collect_table_entries::<DatabaseEnv, tables::PlainStorageState>(tx).unwrap();
1768            if settings.storage_v2 {
1769                assert!(plain_storage.is_empty());
1770            } else {
1771                let expected = expected_alloc
1772                    .keys()
1773                    .flat_map(|address| {
1774                        storage_of(address)
1775                            .into_iter()
1776                            .map(|(key, value)| (*address, StorageEntry { key, value }))
1777                    })
1778                    .collect::<Vec<_>>();
1779                assert_eq!(plain_storage, expected);
1780            }
1781        }
1782
1783        // Replaced accounts and slots keep their genesis values as change set pre-images and their
1784        // genesis block in history.
1785        {
1786            let provider = factory.provider().unwrap();
1787            let genesis_account =
1788                |balance| Some(Account { balance: U256::from(balance), ..Default::default() });
1789            assert_eq!(
1790                reth_provider::ChangeSetReader::account_block_changeset(&provider, 10).unwrap(),
1791                vec![
1792                    AccountBeforeTx { address: dump_only, info: None },
1793                    AccountBeforeTx { address: overlapping, info: genesis_account(1) },
1794                    AccountBeforeTx { address: no_dump_storage, info: genesis_account(0) },
1795                ]
1796            );
1797            let pre_image = |address, key, value: B256| {
1798                (
1799                    BlockNumberAddress((10, address)),
1800                    StorageEntry { key: slot(key), value: U256::from_be_bytes(value.0) },
1801                )
1802            };
1803            assert_eq!(
1804                reth_provider::StorageChangeSetReader::storage_changeset(&provider, 10).unwrap(),
1805                vec![
1806                    pre_image(dump_only, 5, B256::ZERO),
1807                    pre_image(overlapping, 1, val(0x11)),
1808                    pre_image(overlapping, 3, B256::ZERO),
1809                    pre_image(overlapping, 4, val(0x14)),
1810                    pre_image(overlapping, 6, val(0x16)),
1811                ]
1812            );
1813
1814            let (accounts_history, storages_history) = if settings.storage_v2 {
1815                let rocksdb = factory.rocksdb_provider();
1816                (
1817                    rocksdb
1818                        .iter::<tables::AccountsHistory>()
1819                        .unwrap()
1820                        .collect::<Result<Vec<_>, _>>()
1821                        .unwrap(),
1822                    rocksdb
1823                        .iter::<tables::StoragesHistory>()
1824                        .unwrap()
1825                        .collect::<Result<Vec<_>, _>>()
1826                        .unwrap(),
1827                )
1828            } else {
1829                let tx = provider.tx_ref();
1830                (
1831                    collect_table_entries::<DatabaseEnv, tables::AccountsHistory>(tx).unwrap(),
1832                    collect_table_entries::<DatabaseEnv, tables::StoragesHistory>(tx).unwrap(),
1833                )
1834            };
1835            let blocks = |blocks: &[u64]| IntegerList::new(blocks.iter().copied()).unwrap();
1836            assert_eq!(
1837                accounts_history,
1838                vec![
1839                    (ShardedKey::new(dump_only, u64::MAX), blocks(&[10])),
1840                    (ShardedKey::new(overlapping, u64::MAX), blocks(&[0, 10])),
1841                    (ShardedKey::new(no_dump_storage, u64::MAX), blocks(&[0, 10])),
1842                    (ShardedKey::new(genesis_only, u64::MAX), blocks(&[0])),
1843                ]
1844            );
1845            let slot_history = |address, key, list: &[u64]| {
1846                (StorageShardedKey::new(address, slot(key), u64::MAX), blocks(list))
1847            };
1848            assert_eq!(
1849                storages_history,
1850                vec![
1851                    slot_history(dump_only, 5, &[10]),
1852                    slot_history(overlapping, 1, &[0, 10]),
1853                    slot_history(overlapping, 2, &[0]),
1854                    slot_history(overlapping, 3, &[10]),
1855                    slot_history(overlapping, 4, &[0, 10]),
1856                    slot_history(overlapping, 6, &[0, 10]),
1857                    slot_history(no_dump_storage, 7, &[0]),
1858                    slot_history(genesis_only, 8, &[0]),
1859                ]
1860            );
1861        }
1862
1863        {
1864            let provider_rw = factory.database_provider_rw().unwrap();
1865            provider_rw.tx_ref().clear::<tables::AccountsTrie>().unwrap();
1866            provider_rw.tx_ref().clear::<tables::StoragesTrie>().unwrap();
1867            provider_rw.commit().unwrap();
1868        }
1869        assert_eq!(compute_state_root_chunked(&factory).unwrap(), expected_state_root);
1870    }
1871
1872    #[test]
1873    fn dump_state_overlays_genesis_state_v1() {
1874        assert_dump_state_overlays_genesis_state(StorageSettings::v1());
1875    }
1876
1877    #[test]
1878    fn dump_state_overlays_genesis_state_v2() {
1879        assert_dump_state_overlays_genesis_state(StorageSettings::v2());
1880    }
1881
1882    #[test]
1883    fn success_init_genesis_mainnet() {
1884        let genesis_hash =
1885            init_genesis(&create_test_provider_factory_with_chain_spec(MAINNET.clone())).unwrap();
1886
1887        // actual, expected
1888        assert_eq!(genesis_hash, MAINNET_GENESIS_HASH);
1889    }
1890
1891    #[test]
1892    fn success_init_genesis_sepolia() {
1893        let genesis_hash =
1894            init_genesis(&create_test_provider_factory_with_chain_spec(SEPOLIA.clone())).unwrap();
1895
1896        // actual, expected
1897        assert_eq!(genesis_hash, SEPOLIA_GENESIS_HASH);
1898    }
1899
1900    #[test]
1901    fn success_init_genesis_holesky() {
1902        let genesis_hash =
1903            init_genesis(&create_test_provider_factory_with_chain_spec(HOLESKY.clone())).unwrap();
1904
1905        // actual, expected
1906        assert_eq!(genesis_hash, HOLESKY_GENESIS_HASH);
1907    }
1908
1909    #[test]
1910    fn fail_init_inconsistent_db() {
1911        let factory = create_test_provider_factory_with_chain_spec(SEPOLIA.clone());
1912        let static_file_provider = factory.static_file_provider();
1913        let rocksdb_provider = factory.rocksdb_provider();
1914        init_genesis(&factory).unwrap();
1915
1916        // Try to init db with a different genesis block
1917        let genesis_hash = init_genesis(
1918            &ProviderFactory::<MockNodeTypesWithDB>::new(
1919                factory.into_db(),
1920                MAINNET.clone(),
1921                static_file_provider,
1922                rocksdb_provider,
1923                reth_tasks::Runtime::test(),
1924            )
1925            .unwrap(),
1926        );
1927
1928        assert!(matches!(
1929            genesis_hash.unwrap_err(),
1930            InitStorageError::GenesisHashMismatch {
1931                chainspec_hash: MAINNET_GENESIS_HASH,
1932                storage_hash: SEPOLIA_GENESIS_HASH
1933            }
1934        ))
1935    }
1936
1937    #[test]
1938    fn skip_genesis_hash_validation_accepts_mismatched_db() {
1939        let factory = create_test_provider_factory_with_chain_spec(SEPOLIA.clone());
1940        let static_file_provider = factory.static_file_provider();
1941        let rocksdb_provider = factory.rocksdb_provider();
1942        init_genesis(&factory).unwrap();
1943
1944        let result = init_genesis_with_settings_and_validate(
1945            &ProviderFactory::<MockNodeTypesWithDB>::new(
1946                factory.into_db(),
1947                MAINNET.clone(),
1948                static_file_provider,
1949                rocksdb_provider,
1950                reth_tasks::Runtime::test(),
1951            )
1952            .unwrap(),
1953            StorageSettings::base(),
1954            false,
1955        );
1956
1957        let returned = result.expect("skip_genesis_validation should suppress mismatch error");
1958        assert_eq!(
1959            returned, SEPOLIA_GENESIS_HASH,
1960            "bypass returns the DB-resident hash, not the chainspec hash",
1961        );
1962    }
1963
1964    #[test]
1965    fn init_genesis_history() {
1966        let address_with_balance = Address::with_last_byte(1);
1967        let address_with_storage = Address::with_last_byte(2);
1968        let storage_key = B256::with_last_byte(1);
1969        let chain_spec = Arc::new(ChainSpec {
1970            chain: Chain::from_id(1),
1971            genesis: Genesis {
1972                alloc: BTreeMap::from([
1973                    (
1974                        address_with_balance,
1975                        GenesisAccount { balance: U256::ONE, ..Default::default() },
1976                    ),
1977                    (
1978                        address_with_storage,
1979                        GenesisAccount {
1980                            storage: Some(BTreeMap::from([(storage_key, B256::random())])),
1981                            ..Default::default()
1982                        },
1983                    ),
1984                ]),
1985                ..Default::default()
1986            },
1987            hardforks: Default::default(),
1988            paris_block_and_final_difficulty: None,
1989            deposit_contract: None,
1990            ..Default::default()
1991        });
1992
1993        let factory = create_test_provider_factory_with_chain_spec(chain_spec);
1994        init_genesis(&factory).unwrap();
1995
1996        let expected_accounts = vec![
1997            (ShardedKey::new(address_with_balance, u64::MAX), IntegerList::new([0]).unwrap()),
1998            (ShardedKey::new(address_with_storage, u64::MAX), IntegerList::new([0]).unwrap()),
1999        ];
2000        let expected_storages = vec![(
2001            StorageShardedKey::new(address_with_storage, storage_key, u64::MAX),
2002            IntegerList::new([0]).unwrap(),
2003        )];
2004
2005        let collect_from_mdbx = |factory: &ProviderFactory<MockNodeTypesWithDB>| {
2006            let provider = factory.provider().unwrap();
2007            let tx = provider.tx_ref();
2008            (
2009                collect_table_entries::<DatabaseEnv, tables::AccountsHistory>(tx).unwrap(),
2010                collect_table_entries::<DatabaseEnv, tables::StoragesHistory>(tx).unwrap(),
2011            )
2012        };
2013
2014        {
2015            let settings = factory.cached_storage_settings();
2016            let rocksdb = factory.rocksdb_provider();
2017
2018            let collect_rocksdb = |rocksdb: &reth_provider::providers::RocksDBProvider| {
2019                (
2020                    rocksdb
2021                        .iter::<tables::AccountsHistory>()
2022                        .unwrap()
2023                        .collect::<Result<Vec<_>, _>>()
2024                        .unwrap(),
2025                    rocksdb
2026                        .iter::<tables::StoragesHistory>()
2027                        .unwrap()
2028                        .collect::<Result<Vec<_>, _>>()
2029                        .unwrap(),
2030                )
2031            };
2032
2033            let (accounts, storages) = if settings.storage_v2 {
2034                collect_rocksdb(&rocksdb)
2035            } else {
2036                collect_from_mdbx(&factory)
2037            };
2038            assert_eq!(accounts, expected_accounts);
2039            assert_eq!(storages, expected_storages);
2040        }
2041    }
2042
2043    #[test]
2044    fn warn_storage_settings_mismatch() {
2045        let factory = create_test_provider_factory_with_chain_spec(MAINNET.clone());
2046        init_genesis_with_settings(&factory, StorageSettings::v1()).unwrap();
2047
2048        // Request different settings - should warn but succeed
2049        let result = init_genesis_with_settings(&factory, StorageSettings::v2());
2050
2051        // Should succeed (warning is logged, not an error)
2052        assert!(result.is_ok());
2053    }
2054
2055    #[test]
2056    fn allow_same_storage_settings() {
2057        let factory = create_test_provider_factory_with_chain_spec(MAINNET.clone());
2058        let settings = StorageSettings::v2();
2059        init_genesis_with_settings(&factory, settings).unwrap();
2060
2061        let result = init_genesis_with_settings(&factory, settings);
2062
2063        assert!(result.is_ok());
2064    }
2065}