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