Skip to main content

reth_stages/stages/
utils.rs

1//! Utils for `stages`.
2use alloy_primitives::{
3    map::{AddressMap, HashMap},
4    Address, BlockNumber, TxNumber, B256,
5};
6use reth_config::config::EtlConfig;
7use reth_db_api::{
8    cursor::{DbCursorRO, DbCursorRW},
9    models::{
10        sharded_key::NUM_OF_INDICES_IN_SHARD, storage_sharded_key::StorageShardedKey,
11        AccountBeforeTx, AddressStorageKey, BlockNumberAddress, ShardedKey,
12    },
13    table::{Decode, Decompress, Table},
14    transaction::DbTx,
15    BlockNumberList,
16};
17use reth_etl::Collector;
18use reth_primitives_traits::NodePrimitives;
19use reth_provider::{
20    providers::StaticFileProvider, to_range, BlockReader, DBProvider, EitherWriter, ProviderError,
21    StaticFileProviderFactory,
22};
23use reth_stages_api::StageError;
24use reth_static_file_types::StaticFileSegment;
25use reth_storage_api::{ChangeSetReader, StorageChangeSetReader};
26use std::{hash::Hash, ops::RangeBounds};
27use tracing::info;
28
29/// Number of blocks before pushing indices from cache to [`Collector`]
30const DEFAULT_CACHE_THRESHOLD: u64 = 100_000;
31
32/// Collects all history (`H`) indices for a range of changesets (`CS`) and stores them in a
33/// [`Collector`].
34///
35/// ## Process
36/// The function utilizes a `HashMap` cache with a structure of `PartialKey` (`P`) (Address or
37/// Address.StorageKey) to `BlockNumberList`. When the cache exceeds its capacity, its contents are
38/// moved to a [`Collector`]. Here, each entry's key is a concatenation of `PartialKey` and the
39/// highest block number in its list.
40///
41/// ## Example
42/// 1. Initial Cache State: `{ Address1: [1,2,3], ... }`
43/// 2. Cache is flushed to the `Collector`.
44/// 3. Updated Cache State: `{ Address1: [100,300], ... }`
45/// 4. Cache is flushed again.
46///
47/// As a result, the `Collector` will contain entries such as `(Address1.3, [1,2,3])` and
48/// `(Address1.300, [100,300])`. The entries may be stored across one or more files.
49pub(crate) fn collect_history_indices<Provider, CS, H, P>(
50    provider: &Provider,
51    range: impl RangeBounds<CS::Key>,
52    sharded_key_factory: impl Fn(P, BlockNumber) -> H::Key,
53    partial_key_factory: impl Fn((CS::Key, CS::Value)) -> (u64, P),
54    etl_config: &EtlConfig,
55) -> Result<Collector<H::Key, H::Value>, StageError>
56where
57    Provider: DBProvider,
58    CS: Table,
59    H: Table<Value = BlockNumberList>,
60    P: Copy + Eq + Hash,
61{
62    let mut changeset_cursor = provider.tx_ref().cursor_read::<CS>()?;
63
64    let mut collector = Collector::new(etl_config.file_size, etl_config.dir.clone());
65    let mut cache: HashMap<P, Vec<u64>> = HashMap::default();
66
67    let mut collect = |cache: &mut HashMap<P, Vec<u64>>| {
68        for (key, indices) in cache.drain() {
69            let last = *indices.last().expect("qed");
70            collector
71                .insert(sharded_key_factory(key, last), BlockNumberList::new_pre_sorted(indices))?;
72        }
73        Ok::<(), StageError>(())
74    };
75
76    // observability
77    let total_changesets = provider.tx_ref().entries::<CS>()?;
78    let interval = (total_changesets / 1000).max(1);
79
80    let mut flush_counter = 0;
81    let mut current_block_number = u64::MAX;
82    for (idx, entry) in changeset_cursor.walk_range(range)?.enumerate() {
83        let (block_number, key) = partial_key_factory(entry?);
84        cache.entry(key).or_default().push(block_number);
85
86        if idx > 0 && idx.is_multiple_of(interval) && total_changesets > 1000 {
87            info!(target: "sync::stages::index_history", progress = %format_args!("{:.4}%", (idx as f64 / total_changesets as f64) * 100.0), "Collecting indices");
88        }
89
90        // Make sure we only flush the cache every DEFAULT_CACHE_THRESHOLD blocks.
91        if current_block_number != block_number {
92            current_block_number = block_number;
93            flush_counter += 1;
94            if flush_counter > DEFAULT_CACHE_THRESHOLD {
95                collect(&mut cache)?;
96                flush_counter = 0;
97            }
98        }
99    }
100    collect(&mut cache)?;
101
102    Ok(collector)
103}
104
105/// Allows collecting indices from a cache with a custom insert fn
106fn collect_indices<K, F>(
107    cache: impl Iterator<Item = (K, Vec<u64>)>,
108    mut insert_fn: F,
109) -> Result<(), StageError>
110where
111    F: FnMut(K, Vec<u64>) -> Result<(), StageError>,
112{
113    for (key, indices) in cache {
114        insert_fn(key, indices)?
115    }
116    Ok(())
117}
118
119/// Collects account history indices using a provider that implements `ChangeSetReader`.
120pub(crate) fn collect_account_history_indices<Provider>(
121    provider: &Provider,
122    range: impl RangeBounds<BlockNumber>,
123    etl_config: &EtlConfig,
124) -> Result<Collector<ShardedKey<Address>, BlockNumberList>, StageError>
125where
126    Provider: DBProvider + ChangeSetReader + StaticFileProviderFactory,
127{
128    let mut collector = Collector::new(etl_config.file_size, etl_config.dir.clone());
129    let mut cache: AddressMap<Vec<u64>> = AddressMap::default();
130
131    let mut insert_fn = |address: Address, indices: Vec<u64>| {
132        let last = indices.last().expect("indices is non-empty");
133        collector
134            .insert(ShardedKey::new(address, *last), BlockNumberList::new_pre_sorted(indices))?;
135        Ok(())
136    };
137
138    // Convert range bounds to concrete range
139    let range = to_range(range);
140    let start_block = range.start;
141
142    // Use the new walker for lazy iteration over static file changesets
143    let static_file_provider = provider.static_file_provider();
144
145    let walker = static_file_provider.walk_account_changeset_range(range);
146
147    let mut flush_counter = 0;
148    let mut current_block_number = u64::MAX;
149
150    for changeset_result in walker {
151        let (block_number, AccountBeforeTx { address, .. }) = changeset_result?;
152        cache.entry(address).or_default().push(block_number);
153
154        if block_number != current_block_number {
155            current_block_number = block_number;
156            flush_counter += 1;
157        }
158
159        if flush_counter > DEFAULT_CACHE_THRESHOLD {
160            info!(
161                target: "sync::stages::index_history",
162                processed_blocks = current_block_number.saturating_sub(start_block) + 1,
163                current_block = current_block_number,
164                "Collecting indices"
165            );
166            collect_indices(cache.drain(), &mut insert_fn)?;
167            flush_counter = 0;
168        }
169    }
170    collect_indices(cache.into_iter(), insert_fn)?;
171
172    Ok(collector)
173}
174
175/// Collects storage history indices using a provider that implements `StorageChangeSetReader`.
176pub(crate) fn collect_storage_history_indices<Provider>(
177    provider: &Provider,
178    range: impl RangeBounds<BlockNumber>,
179    etl_config: &EtlConfig,
180) -> Result<Collector<StorageShardedKey, BlockNumberList>, StageError>
181where
182    Provider: DBProvider + StorageChangeSetReader + StaticFileProviderFactory,
183{
184    let mut collector = Collector::new(etl_config.file_size, etl_config.dir.clone());
185    let mut cache: HashMap<AddressStorageKey, Vec<u64>> = HashMap::default();
186
187    let mut insert_fn = |key: AddressStorageKey, indices: Vec<u64>| {
188        let last = indices.last().expect("qed");
189        collector.insert(
190            StorageShardedKey::new(key.0 .0, key.0 .1, *last),
191            BlockNumberList::new_pre_sorted(indices),
192        )?;
193        Ok::<(), StageError>(())
194    };
195
196    let range = to_range(range);
197    let start_block = range.start;
198    let static_file_provider = provider.static_file_provider();
199
200    let walker = static_file_provider.walk_storage_changeset_range(range);
201
202    let mut flush_counter = 0;
203    let mut current_block_number = u64::MAX;
204
205    for changeset_result in walker {
206        let (BlockNumberAddress((block_number, address)), storage) = changeset_result?;
207        cache.entry(AddressStorageKey((address, storage.key))).or_default().push(block_number);
208
209        if block_number != current_block_number {
210            current_block_number = block_number;
211            flush_counter += 1;
212        }
213
214        if flush_counter > DEFAULT_CACHE_THRESHOLD {
215            info!(
216                target: "sync::stages::index_history",
217                processed_blocks = current_block_number.saturating_sub(start_block) + 1,
218                current_block = current_block_number,
219                "Collecting indices"
220            );
221            collect_indices(cache.drain(), &mut insert_fn)?;
222            flush_counter = 0;
223        }
224    }
225
226    collect_indices(cache.into_iter(), insert_fn)?;
227
228    Ok(collector)
229}
230
231/// Loads account history indices into the database via `EitherWriter`.
232///
233/// Works with [`EitherWriter`] to support both MDBX and `RocksDB` backends.
234///
235/// ## Process
236/// Iterates over elements, grouping indices by their address. It flushes indices to disk
237/// when reaching a shard's max length (`NUM_OF_INDICES_IN_SHARD`) or when the address changes,
238/// ensuring the last previous address shard is stored.
239///
240/// Uses `Option<Address>` instead of `Address::default()` as the sentinel to avoid
241/// incorrectly treating `Address::ZERO` as "no previous address".
242pub(crate) fn load_account_history<N, CURSOR>(
243    mut collector: Collector<ShardedKey<Address>, BlockNumberList>,
244    append_only: bool,
245    writer: &mut EitherWriter<'_, CURSOR, N>,
246) -> Result<(), StageError>
247where
248    N: NodePrimitives,
249    CURSOR: DbCursorRW<reth_db_api::tables::AccountsHistory>
250        + DbCursorRO<reth_db_api::tables::AccountsHistory>,
251{
252    let mut current_address: Option<Address> = None;
253    // Accumulator for block numbers where the current address changed.
254    let mut current_list = Vec::<u64>::new();
255
256    let total_entries = collector.len();
257    let interval = (total_entries / 10).max(1);
258
259    for (index, element) in collector.iter()?.enumerate() {
260        let (k, v) = element?;
261        let sharded_key = ShardedKey::<Address>::decode_owned(k)?;
262        let new_list = BlockNumberList::decompress_owned(v)?;
263
264        if index > 0 && index.is_multiple_of(interval) && total_entries > 10 {
265            info!(target: "sync::stages::index_history", progress = %format_args!("{:.2}%", (index as f64 / total_entries as f64) * 100.0), "Writing indices");
266        }
267
268        let address = sharded_key.key;
269
270        // When address changes, flush the previous address's shards and start fresh.
271        if current_address != Some(address) {
272            // Flush all remaining shards for the previous address (uses u64::MAX for last shard).
273            if let Some(prev_addr) = current_address {
274                flush_account_history_shards(prev_addr, &mut current_list, append_only, writer)?;
275            }
276
277            current_address = Some(address);
278            current_list.clear();
279
280            // On incremental sync, merge with the existing last shard from the database.
281            // The last shard is stored with key (address, u64::MAX) so we can find it.
282            if !append_only &&
283                let Some(last_shard) = writer.get_last_account_history_shard(address)?
284            {
285                current_list.extend(last_shard.iter());
286            }
287        }
288
289        // Append new block numbers to the accumulator.
290        current_list.extend(new_list.iter());
291
292        // Flush complete shards, keeping the last (partial) shard buffered.
293        flush_account_history_shards_partial(address, &mut current_list, append_only, writer)?;
294    }
295
296    // Flush the final address's remaining shard.
297    if let Some(addr) = current_address {
298        flush_account_history_shards(addr, &mut current_list, append_only, writer)?;
299    }
300
301    Ok(())
302}
303
304/// Flushes complete shards for account history, keeping the trailing partial shard buffered.
305///
306/// Only flushes when we have more than one shard's worth of data, keeping the last
307/// (possibly partial) shard for continued accumulation. This avoids writing a shard
308/// that may need to be updated when more indices arrive.
309fn flush_account_history_shards_partial<N, CURSOR>(
310    address: Address,
311    list: &mut Vec<u64>,
312    append_only: bool,
313    writer: &mut EitherWriter<'_, CURSOR, N>,
314) -> Result<(), StageError>
315where
316    N: NodePrimitives,
317    CURSOR: DbCursorRW<reth_db_api::tables::AccountsHistory>
318        + DbCursorRO<reth_db_api::tables::AccountsHistory>,
319{
320    // Nothing to flush if we haven't filled a complete shard yet.
321    if list.len() <= NUM_OF_INDICES_IN_SHARD {
322        return Ok(());
323    }
324
325    let num_full_shards = list.len() / NUM_OF_INDICES_IN_SHARD;
326
327    // Always keep at least one shard buffered for continued accumulation.
328    // If len is exact multiple of shard size, keep the last full shard.
329    let shards_to_flush = if list.len().is_multiple_of(NUM_OF_INDICES_IN_SHARD) {
330        num_full_shards - 1
331    } else {
332        num_full_shards
333    };
334
335    if shards_to_flush == 0 {
336        return Ok(());
337    }
338
339    // Split: flush the first N shards, keep the remainder buffered.
340    let flush_len = shards_to_flush * NUM_OF_INDICES_IN_SHARD;
341    let remainder = list.split_off(flush_len);
342
343    // Write each complete shard with its highest block number as the key.
344    for chunk in list.chunks(NUM_OF_INDICES_IN_SHARD) {
345        let highest = *chunk.last().expect("chunk is non-empty");
346        let key = ShardedKey::new(address, highest);
347        let value = BlockNumberList::new_pre_sorted(chunk.iter().copied());
348
349        if append_only {
350            writer.append_account_history(key, &value)?;
351        } else {
352            writer.upsert_account_history(key, &value)?;
353        }
354    }
355
356    // Keep the remaining indices for the next iteration.
357    *list = remainder;
358    Ok(())
359}
360
361/// Flushes all remaining shards for account history, using `u64::MAX` for the last shard.
362///
363/// The `u64::MAX` key for the final shard is an invariant that allows `seek_exact(address,
364/// u64::MAX)` to find the last shard during incremental sync for merging with new indices.
365fn flush_account_history_shards<N, CURSOR>(
366    address: Address,
367    list: &mut Vec<u64>,
368    append_only: bool,
369    writer: &mut EitherWriter<'_, CURSOR, N>,
370) -> Result<(), StageError>
371where
372    N: NodePrimitives,
373    CURSOR: DbCursorRW<reth_db_api::tables::AccountsHistory>
374        + DbCursorRO<reth_db_api::tables::AccountsHistory>,
375{
376    if list.is_empty() {
377        return Ok(());
378    }
379
380    let num_chunks = list.len().div_ceil(NUM_OF_INDICES_IN_SHARD);
381
382    for (i, chunk) in list.chunks(NUM_OF_INDICES_IN_SHARD).enumerate() {
383        let is_last = i == num_chunks - 1;
384
385        // Use u64::MAX for the final shard's key. This invariant allows incremental sync
386        // to find the last shard via seek_exact(address, u64::MAX) for merging.
387        let highest = if is_last { u64::MAX } else { *chunk.last().expect("chunk is non-empty") };
388
389        let key = ShardedKey::new(address, highest);
390        let value = BlockNumberList::new_pre_sorted(chunk.iter().copied());
391
392        if append_only {
393            writer.append_account_history(key, &value)?;
394        } else {
395            writer.upsert_account_history(key, &value)?;
396        }
397    }
398
399    list.clear();
400    Ok(())
401}
402
403/// Called when database is ahead of static files. Attempts to find the first block we are missing
404/// transactions for.
405pub(crate) fn missing_static_data_error<Provider>(
406    last_tx_num: TxNumber,
407    static_file_provider: &StaticFileProvider<Provider::Primitives>,
408    provider: &Provider,
409    segment: StaticFileSegment,
410) -> Result<StageError, ProviderError>
411where
412    Provider: BlockReader + StaticFileProviderFactory,
413{
414    let mut last_block =
415        static_file_provider.get_highest_static_file_block(segment).unwrap_or_default();
416
417    // To be extra safe, we make sure that the last tx num matches the last block from its indices.
418    // If not, get it.
419    loop {
420        if let Some(indices) = provider.block_body_indices(last_block)? &&
421            indices.last_tx_num() <= last_tx_num
422        {
423            break
424        }
425        if last_block == 0 {
426            break
427        }
428        last_block -= 1;
429    }
430
431    let missing_block = Box::new(provider.sealed_header(last_block + 1)?.unwrap_or_default());
432
433    Ok(StageError::MissingStaticFileData {
434        block: Box::new(missing_block.block_with_parent()),
435        segment,
436    })
437}
438
439/// Loads storage history indices into the database via `EitherWriter`.
440///
441/// Works with [`EitherWriter`] to support both MDBX and `RocksDB` backends.
442///
443/// ## Process
444/// Iterates over elements, grouping indices by their (address, `storage_key`) pairs. It flushes
445/// indices to disk when reaching a shard's max length (`NUM_OF_INDICES_IN_SHARD`) or when the
446/// (address, `storage_key`) pair changes, ensuring the last previous shard is stored.
447///
448/// Uses `Option<(Address, B256)>` instead of default values as the sentinel to avoid
449/// incorrectly treating `(Address::ZERO, B256::ZERO)` as "no previous key".
450pub(crate) fn load_storage_history<N, CURSOR>(
451    mut collector: Collector<StorageShardedKey, BlockNumberList>,
452    append_only: bool,
453    writer: &mut EitherWriter<'_, CURSOR, N>,
454) -> Result<(), StageError>
455where
456    N: NodePrimitives,
457    CURSOR: DbCursorRW<reth_db_api::tables::StoragesHistory>
458        + DbCursorRO<reth_db_api::tables::StoragesHistory>,
459{
460    let mut current_key: Option<(Address, B256)> = None;
461    // Accumulator for block numbers where the current (address, storage_key) changed.
462    let mut current_list = Vec::<u64>::new();
463
464    let total_entries = collector.len();
465    let interval = (total_entries / 10).max(1);
466
467    for (index, element) in collector.iter()?.enumerate() {
468        let (k, v) = element?;
469        let sharded_key = StorageShardedKey::decode_owned(k)?;
470        let new_list = BlockNumberList::decompress_owned(v)?;
471
472        if index > 0 && index.is_multiple_of(interval) && total_entries > 10 {
473            info!(target: "sync::stages::index_history", progress = %format_args!("{:.2}%", (index as f64 / total_entries as f64) * 100.0), "Writing indices");
474        }
475
476        let partial_key = (sharded_key.address, sharded_key.sharded_key.key);
477
478        // When (address, storage_key) changes, flush the previous key's shards and start fresh.
479        if current_key != Some(partial_key) {
480            // Flush all remaining shards for the previous key (uses u64::MAX for last shard).
481            if let Some((prev_addr, prev_storage_key)) = current_key {
482                flush_storage_history_shards(
483                    prev_addr,
484                    prev_storage_key,
485                    &mut current_list,
486                    append_only,
487                    writer,
488                )?;
489            }
490
491            current_key = Some(partial_key);
492            current_list.clear();
493
494            // On incremental sync, merge with the existing last shard from the database.
495            // The last shard is stored with key (address, storage_key, u64::MAX) so we can find it.
496            if !append_only &&
497                let Some(last_shard) =
498                    writer.get_last_storage_history_shard(partial_key.0, partial_key.1)?
499            {
500                current_list.extend(last_shard.iter());
501            }
502        }
503
504        // Append new block numbers to the accumulator.
505        current_list.extend(new_list.iter());
506
507        // Flush complete shards, keeping the last (partial) shard buffered.
508        flush_storage_history_shards_partial(
509            partial_key.0,
510            partial_key.1,
511            &mut current_list,
512            append_only,
513            writer,
514        )?;
515    }
516
517    // Flush the final key's remaining shard.
518    if let Some((addr, storage_key)) = current_key {
519        flush_storage_history_shards(addr, storage_key, &mut current_list, append_only, writer)?;
520    }
521
522    Ok(())
523}
524
525/// Flushes complete shards for storage history, keeping the trailing partial shard buffered.
526///
527/// Only flushes when we have more than one shard's worth of data, keeping the last
528/// (possibly partial) shard for continued accumulation. This avoids writing a shard
529/// that may need to be updated when more indices arrive.
530fn flush_storage_history_shards_partial<N, CURSOR>(
531    address: Address,
532    storage_key: B256,
533    list: &mut Vec<u64>,
534    append_only: bool,
535    writer: &mut EitherWriter<'_, CURSOR, N>,
536) -> Result<(), StageError>
537where
538    N: NodePrimitives,
539    CURSOR: DbCursorRW<reth_db_api::tables::StoragesHistory>
540        + DbCursorRO<reth_db_api::tables::StoragesHistory>,
541{
542    // Nothing to flush if we haven't filled a complete shard yet.
543    if list.len() <= NUM_OF_INDICES_IN_SHARD {
544        return Ok(());
545    }
546
547    let num_full_shards = list.len() / NUM_OF_INDICES_IN_SHARD;
548
549    // Always keep at least one shard buffered for continued accumulation.
550    // If len is exact multiple of shard size, keep the last full shard.
551    let shards_to_flush = if list.len().is_multiple_of(NUM_OF_INDICES_IN_SHARD) {
552        num_full_shards - 1
553    } else {
554        num_full_shards
555    };
556
557    if shards_to_flush == 0 {
558        return Ok(());
559    }
560
561    // Split: flush the first N shards, keep the remainder buffered.
562    let flush_len = shards_to_flush * NUM_OF_INDICES_IN_SHARD;
563    let remainder = list.split_off(flush_len);
564
565    // Write each complete shard with its highest block number as the key.
566    for chunk in list.chunks(NUM_OF_INDICES_IN_SHARD) {
567        let highest = *chunk.last().expect("chunk is non-empty");
568        let key = StorageShardedKey::new(address, storage_key, highest);
569        let value = BlockNumberList::new_pre_sorted(chunk.iter().copied());
570
571        if append_only {
572            writer.append_storage_history(key, &value)?;
573        } else {
574            writer.upsert_storage_history(key, &value)?;
575        }
576    }
577
578    // Keep the remaining indices for the next iteration.
579    *list = remainder;
580    Ok(())
581}
582
583/// Flushes all remaining shards for storage history, using `u64::MAX` for the last shard.
584///
585/// The `u64::MAX` key for the final shard is an invariant that allows
586/// `seek_exact(address, storage_key, u64::MAX)` to find the last shard during incremental
587/// sync for merging with new indices.
588fn flush_storage_history_shards<N, CURSOR>(
589    address: Address,
590    storage_key: B256,
591    list: &mut Vec<u64>,
592    append_only: bool,
593    writer: &mut EitherWriter<'_, CURSOR, N>,
594) -> Result<(), StageError>
595where
596    N: NodePrimitives,
597    CURSOR: DbCursorRW<reth_db_api::tables::StoragesHistory>
598        + DbCursorRO<reth_db_api::tables::StoragesHistory>,
599{
600    if list.is_empty() {
601        return Ok(());
602    }
603
604    let num_chunks = list.len().div_ceil(NUM_OF_INDICES_IN_SHARD);
605
606    for (i, chunk) in list.chunks(NUM_OF_INDICES_IN_SHARD).enumerate() {
607        let is_last = i == num_chunks - 1;
608
609        // Use u64::MAX for the final shard's key. This invariant allows incremental sync
610        // to find the last shard via seek_exact(address, storage_key, u64::MAX) for merging.
611        let highest = if is_last { u64::MAX } else { *chunk.last().expect("chunk is non-empty") };
612
613        let key = StorageShardedKey::new(address, storage_key, highest);
614        let value = BlockNumberList::new_pre_sorted(chunk.iter().copied());
615
616        if append_only {
617            writer.append_storage_history(key, &value)?;
618        } else {
619            writer.upsert_storage_history(key, &value)?;
620        }
621    }
622
623    list.clear();
624    Ok(())
625}