1use crate::download::{
2 manifest::{ComponentManifest, ComponentSelection, SnapshotComponentType, SnapshotManifest},
3 SelectionPreset,
4};
5use reth_chainspec::{EthereumHardfork, EthereumHardforks};
6use reth_config::config::{BlocksPerFileConfig, Config, PruneConfig, StaticFilesConfig};
7use reth_db::tables;
8use reth_db_api::transaction::{DbTx, DbTxMut};
9use reth_node_core::args::DefaultPruningValues;
10use reth_prune_types::{PruneCheckpoint, PruneMode, PruneSegment};
11use reth_stages_types::StageCheckpoint;
12use std::{collections::BTreeMap, path::Path};
13use tracing::info;
14
15const MINIMUM_RECEIPTS_DISTANCE: u64 = 64;
17
18const MINIMUM_HISTORY_DISTANCE: u64 = 10064;
21
22pub fn write_config(config: &Config, data_dir: &Path) -> eyre::Result<bool> {
26 let config_path = data_dir.join("reth.toml");
27
28 if config_path.exists() {
29 info!(target: "reth::cli",
30 path = ?config_path,
31 "reth.toml already exists, skipping config generation"
32 );
33 return Ok(false);
34 }
35
36 let toml_str = toml::to_string_pretty(config)?;
37 reth_fs_util::write(&config_path, toml_str)?;
38
39 info!(target: "reth::cli",
40 path = ?config_path,
41 "Generated reth.toml based on downloaded components"
42 );
43
44 Ok(true)
45}
46
47pub(crate) fn write_prune_checkpoints_tx<Tx>(
49 tx: &Tx,
50 config: &Config,
51 snapshot_block: u64,
52) -> eyre::Result<()>
53where
54 Tx: DbTx + DbTxMut,
55{
56 let segments = &config.prune.segments;
57
58 let checkpoints: Vec<(PruneSegment, PruneMode)> = [
60 (PruneSegment::SenderRecovery, segments.sender_recovery),
61 (PruneSegment::TransactionLookup, segments.transaction_lookup),
62 (PruneSegment::Receipts, segments.receipts),
63 (PruneSegment::AccountHistory, segments.account_history),
64 (PruneSegment::StorageHistory, segments.storage_history),
65 (PruneSegment::Bodies, segments.bodies_history),
66 ]
67 .into_iter()
68 .filter_map(|(segment, mode)| mode.map(|m| (segment, m)))
69 .collect();
70
71 if checkpoints.is_empty() {
72 return Ok(());
73 }
74
75 let tx_number =
77 tx.get::<tables::BlockBodyIndices>(snapshot_block)?.map(|indices| indices.last_tx_num());
78
79 for (segment, prune_mode) in &checkpoints {
80 let checkpoint = PruneCheckpoint {
81 block_number: Some(snapshot_block),
82 tx_number,
83 prune_mode: *prune_mode,
84 };
85
86 tx.put::<tables::PruneCheckpoints>(*segment, checkpoint)?;
87
88 info!(target: "reth::cli",
89 segment = %segment,
90 block = snapshot_block,
91 tx = ?tx_number,
92 mode = ?prune_mode,
93 "Set prune checkpoint"
94 );
95 }
96
97 Ok(())
98}
99
100const INDEX_STAGE_IDS: [&str; 3] =
103 ["TransactionLookup", "IndexAccountHistory", "IndexStorageHistory"];
104
105const INDEX_PRUNE_SEGMENTS: [PruneSegment; 3] =
107 [PruneSegment::TransactionLookup, PruneSegment::AccountHistory, PruneSegment::StorageHistory];
108
109pub(crate) fn reset_index_stage_checkpoints_tx<Tx>(tx: &Tx) -> eyre::Result<()>
122where
123 Tx: DbTx + DbTxMut,
124{
125 for stage_id in INDEX_STAGE_IDS {
126 tx.put::<tables::StageCheckpoints>(stage_id.to_string(), StageCheckpoint::default())?;
127
128 tx.delete::<tables::StageCheckpointProgresses>(stage_id.to_string(), None)?;
130
131 info!(target: "reth::cli", stage = stage_id, "Reset stage checkpoint to block 0");
132 }
133
134 for segment in INDEX_PRUNE_SEGMENTS {
137 tx.delete::<tables::PruneCheckpoints>(segment, None)?;
138 }
139
140 Ok(())
141}
142
143pub(crate) fn config_for_selections(
148 selections: &BTreeMap<SnapshotComponentType, ComponentSelection>,
149 manifest: &SnapshotManifest,
150 preset: Option<SelectionPreset>,
151 chain_spec: Option<&impl EthereumHardforks>,
152) -> Config {
153 let selection_for = |ty| selections.get(&ty).copied().unwrap_or(ComponentSelection::None);
154
155 let tx_sel = selection_for(SnapshotComponentType::Transactions);
156 let senders_sel = selection_for(SnapshotComponentType::TransactionSenders);
157 let receipt_sel = selection_for(SnapshotComponentType::Receipts);
158 let account_cs_sel = selection_for(SnapshotComponentType::AccountChangesets);
159 let storage_cs_sel = selection_for(SnapshotComponentType::StorageChangesets);
160
161 let is_archive = [tx_sel, senders_sel, receipt_sel, account_cs_sel, storage_cs_sel]
163 .iter()
164 .all(|s| *s == ComponentSelection::All);
165
166 let blocks_per_file = |ty: SnapshotComponentType| -> Option<u64> {
168 match manifest.component(ty)? {
169 ComponentManifest::Chunked(c) => Some(c.blocks_per_file),
170 ComponentManifest::Single(_) => None,
171 }
172 };
173 let static_files = StaticFilesConfig {
174 blocks_per_file: BlocksPerFileConfig {
175 headers: blocks_per_file(SnapshotComponentType::Headers),
176 transactions: blocks_per_file(SnapshotComponentType::Transactions),
177 receipts: blocks_per_file(SnapshotComponentType::Receipts),
178 transaction_senders: blocks_per_file(SnapshotComponentType::TransactionSenders),
179 account_change_sets: blocks_per_file(SnapshotComponentType::AccountChangesets),
180 storage_change_sets: blocks_per_file(SnapshotComponentType::StorageChangesets),
181 },
182 };
183
184 if is_archive || matches!(preset, Some(SelectionPreset::Archive)) {
185 return Config { static_files, ..Default::default() };
186 }
187
188 if matches!(preset, Some(SelectionPreset::Full)) {
189 let defaults = DefaultPruningValues::get_global();
190 let mut segments = defaults.full_prune_modes.clone();
191
192 if defaults.full_bodies_history_use_pre_merge {
193 segments.bodies_history = chain_spec.and_then(|chain_spec| {
194 chain_spec
195 .ethereum_fork_activation(EthereumHardfork::Paris)
196 .block_number()
197 .map(PruneMode::Before)
198 });
199 }
200
201 return Config {
202 prune: PruneConfig { segments, ..Default::default() },
203 static_files,
204 ..Default::default()
205 };
206 }
207
208 let mut config = Config::default();
209 let mut prune = PruneConfig::default();
210
211 if senders_sel != ComponentSelection::All {
212 prune.segments.sender_recovery = Some(PruneMode::Full);
213 }
214 prune.segments.transaction_lookup = Some(PruneMode::Full);
215
216 if let Some(mode) = selection_to_prune_mode(tx_sel, Some(MINIMUM_HISTORY_DISTANCE)) {
217 prune.segments.bodies_history = Some(mode);
218 }
219
220 if let Some(mode) = selection_to_prune_mode(receipt_sel, Some(MINIMUM_RECEIPTS_DISTANCE)) {
221 prune.segments.receipts = Some(mode);
222 }
223
224 if let Some(mode) = selection_to_prune_mode(account_cs_sel, Some(MINIMUM_HISTORY_DISTANCE)) {
225 prune.segments.account_history = Some(mode);
226 }
227
228 if let Some(mode) = selection_to_prune_mode(storage_cs_sel, Some(MINIMUM_HISTORY_DISTANCE)) {
229 prune.segments.storage_history = Some(mode);
230 }
231
232 config.prune = prune;
233 config.static_files = static_files;
234 config
235}
236
237fn selection_to_prune_mode(
243 sel: ComponentSelection,
244 min_distance: Option<u64>,
245) -> Option<PruneMode> {
246 match sel {
247 ComponentSelection::All => None,
248 ComponentSelection::Distance(d) => {
249 Some(PruneMode::Distance(min_distance.map_or(d, |min| d.max(min))))
250 }
251 ComponentSelection::Since(block) => Some(PruneMode::Before(block)),
252 ComponentSelection::None => Some(min_distance.map_or(PruneMode::Full, PruneMode::Distance)),
253 }
254}
255
256pub(crate) fn describe_prune_config(config: &Config) -> Vec<String> {
258 let segments = &config.prune.segments;
259
260 [
261 ("sender_recovery", segments.sender_recovery),
262 ("transaction_lookup", segments.transaction_lookup),
263 ("bodies_history", segments.bodies_history),
264 ("receipts", segments.receipts),
265 ("account_history", segments.account_history),
266 ("storage_history", segments.storage_history),
267 ]
268 .into_iter()
269 .filter_map(|(name, mode)| mode.map(|m| format!("{name}={}", format_mode(&m))))
270 .collect()
271}
272
273fn format_mode(mode: &PruneMode) -> String {
275 match mode {
276 PruneMode::Full => "\"full\"".to_string(),
277 PruneMode::Distance(d) => format!("{{ distance = {d} }}"),
278 PruneMode::Before(b) => format!("{{ before = {b} }}"),
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use reth_db::Database;
286
287 fn empty_manifest() -> SnapshotManifest {
289 SnapshotManifest {
290 block: 0,
291 chain_id: 1,
292 storage_version: 2,
293 timestamp: 0,
294 base_url: None,
295 reth_version: None,
296 components: BTreeMap::new(),
297 extensions: Default::default(),
298 }
299 }
300
301 #[test]
302 fn write_prune_checkpoints_sets_all_segments() {
303 let dir = tempfile::tempdir().unwrap();
304 let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
305
306 let mut selections = BTreeMap::new();
307 selections.insert(SnapshotComponentType::State, ComponentSelection::All);
308 selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
309 let config = config_for_selections(
310 &selections,
311 &empty_manifest(),
312 None,
313 None::<&reth_chainspec::ChainSpec>,
314 );
315 let snapshot_block = 21_000_000;
316
317 {
318 let tx = db.tx_mut().unwrap();
319 write_prune_checkpoints_tx(&tx, &config, snapshot_block).unwrap();
320 tx.commit().unwrap();
321 }
322
323 let tx = db.tx().unwrap();
325 for segment in [
326 PruneSegment::SenderRecovery,
327 PruneSegment::TransactionLookup,
328 PruneSegment::Receipts,
329 PruneSegment::AccountHistory,
330 PruneSegment::StorageHistory,
331 PruneSegment::Bodies,
332 ] {
333 let checkpoint = tx
334 .get::<tables::PruneCheckpoints>(segment)
335 .unwrap()
336 .unwrap_or_else(|| panic!("expected checkpoint for {segment}"));
337 assert_eq!(checkpoint.block_number, Some(snapshot_block));
338 assert_eq!(checkpoint.tx_number, None);
340 }
341 }
342
343 #[test]
344 fn write_prune_checkpoints_archive_no_checkpoints() {
345 let dir = tempfile::tempdir().unwrap();
346 let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
347
348 let mut selections = BTreeMap::new();
350 for ty in SnapshotComponentType::ALL {
351 selections.insert(ty, ComponentSelection::All);
352 }
353 let config = config_for_selections(
354 &selections,
355 &empty_manifest(),
356 None,
357 None::<&reth_chainspec::ChainSpec>,
358 );
359
360 {
361 let tx = db.tx_mut().unwrap();
362 write_prune_checkpoints_tx(&tx, &config, 21_000_000).unwrap();
363 tx.commit().unwrap();
364 }
365
366 let tx = db.tx().unwrap();
367 for segment in [PruneSegment::SenderRecovery, PruneSegment::TransactionLookup] {
368 assert!(
369 tx.get::<tables::PruneCheckpoints>(segment).unwrap().is_none(),
370 "expected no checkpoint for {segment} on archive node"
371 );
372 }
373 }
374
375 #[test]
376 fn selections_all_no_pruning() {
377 let mut selections = BTreeMap::new();
378 for ty in SnapshotComponentType::ALL {
379 selections.insert(ty, ComponentSelection::All);
380 }
381 let config = config_for_selections(
382 &selections,
383 &empty_manifest(),
384 None,
385 None::<&reth_chainspec::ChainSpec>,
386 );
387 assert_eq!(config.prune.segments.transaction_lookup, None);
389 assert_eq!(config.prune.segments.sender_recovery, None);
390 assert_eq!(config.prune.segments.bodies_history, None);
391 assert_eq!(config.prune.segments.receipts, None);
392 assert_eq!(config.prune.segments.account_history, None);
393 assert_eq!(config.prune.segments.storage_history, None);
394 }
395
396 #[test]
397 fn selections_none_clamps_to_minimum_distance() {
398 let mut selections = BTreeMap::new();
399 selections.insert(SnapshotComponentType::State, ComponentSelection::All);
400 selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
401 let config = config_for_selections(
402 &selections,
403 &empty_manifest(),
404 None,
405 None::<&reth_chainspec::ChainSpec>,
406 );
407 assert_eq!(config.prune.segments.transaction_lookup, Some(PruneMode::Full));
408 assert_eq!(config.prune.segments.sender_recovery, Some(PruneMode::Full));
409 assert_eq!(
411 config.prune.segments.bodies_history,
412 Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
413 );
414 assert_eq!(
415 config.prune.segments.receipts,
416 Some(PruneMode::Distance(MINIMUM_RECEIPTS_DISTANCE))
417 );
418 assert_eq!(
419 config.prune.segments.account_history,
420 Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
421 );
422 assert_eq!(
423 config.prune.segments.storage_history,
424 Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
425 );
426 }
427
428 #[test]
429 fn selections_distance_maps_bodies_history() {
430 let mut selections = BTreeMap::new();
431 selections.insert(SnapshotComponentType::State, ComponentSelection::All);
432 selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
433 selections
434 .insert(SnapshotComponentType::Transactions, ComponentSelection::Distance(10_064));
435 selections.insert(SnapshotComponentType::Receipts, ComponentSelection::None);
436 selections
437 .insert(SnapshotComponentType::AccountChangesets, ComponentSelection::Distance(10_064));
438 selections
439 .insert(SnapshotComponentType::StorageChangesets, ComponentSelection::Distance(10_064));
440 let config = config_for_selections(
441 &selections,
442 &empty_manifest(),
443 None,
444 None::<&reth_chainspec::ChainSpec>,
445 );
446
447 assert_eq!(config.prune.segments.transaction_lookup, Some(PruneMode::Full));
448 assert_eq!(config.prune.segments.sender_recovery, Some(PruneMode::Full));
449 assert_eq!(config.prune.segments.bodies_history, Some(PruneMode::Distance(10_064)));
451 assert_eq!(
452 config.prune.segments.receipts,
453 Some(PruneMode::Distance(MINIMUM_RECEIPTS_DISTANCE))
454 );
455 assert_eq!(config.prune.segments.account_history, Some(PruneMode::Distance(10_064)));
456 assert_eq!(config.prune.segments.storage_history, Some(PruneMode::Distance(10_064)));
457 }
458
459 #[test]
460 fn selections_since_maps_to_before_prune_mode() {
461 let mut selections = BTreeMap::new();
462 selections.insert(SnapshotComponentType::State, ComponentSelection::All);
463 selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
464 selections
465 .insert(SnapshotComponentType::Transactions, ComponentSelection::Since(15_537_394));
466 selections.insert(SnapshotComponentType::Receipts, ComponentSelection::Since(15_537_394));
467 selections.insert(
468 SnapshotComponentType::AccountChangesets,
469 ComponentSelection::Since(15_537_394),
470 );
471 selections.insert(
472 SnapshotComponentType::StorageChangesets,
473 ComponentSelection::Since(15_537_394),
474 );
475
476 let config = config_for_selections(
477 &selections,
478 &empty_manifest(),
479 None,
480 None::<&reth_chainspec::ChainSpec>,
481 );
482
483 assert_eq!(config.prune.segments.bodies_history, Some(PruneMode::Before(15_537_394)));
484 assert_eq!(config.prune.segments.receipts, Some(PruneMode::Before(15_537_394)));
485 assert_eq!(config.prune.segments.account_history, Some(PruneMode::Before(15_537_394)));
486 assert_eq!(config.prune.segments.storage_history, Some(PruneMode::Before(15_537_394)));
487 }
488
489 #[test]
490 fn full_preset_matches_default_full_prune_config() {
491 let mut selections = BTreeMap::new();
492 selections.insert(SnapshotComponentType::State, ComponentSelection::All);
493 selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
494 selections
495 .insert(SnapshotComponentType::Transactions, ComponentSelection::Distance(500_000));
496 selections.insert(SnapshotComponentType::Receipts, ComponentSelection::Distance(10_064));
497
498 let chain_spec = reth_chainspec::MAINNET.clone();
499 let config = config_for_selections(
500 &selections,
501 &empty_manifest(),
502 Some(SelectionPreset::Full),
503 Some(chain_spec.as_ref()),
504 );
505
506 assert_eq!(config.prune.segments.sender_recovery, Some(PruneMode::Full));
507 assert_eq!(config.prune.segments.transaction_lookup, None);
508 assert_eq!(
509 config.prune.segments.receipts,
510 Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
511 );
512 assert_eq!(
513 config.prune.segments.account_history,
514 Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
515 );
516 assert_eq!(
517 config.prune.segments.storage_history,
518 Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
519 );
520
521 let paris_block = chain_spec
522 .ethereum_fork_activation(EthereumHardfork::Paris)
523 .block_number()
524 .expect("mainnet Paris block should be known");
525 assert_eq!(config.prune.segments.bodies_history, Some(PruneMode::Before(paris_block)));
526 }
527
528 #[test]
529 fn describe_selections_all_no_pruning() {
530 let mut selections = BTreeMap::new();
531 for ty in SnapshotComponentType::ALL {
532 selections.insert(ty, ComponentSelection::All);
533 }
534 let config = config_for_selections(
535 &selections,
536 &empty_manifest(),
537 None,
538 None::<&reth_chainspec::ChainSpec>,
539 );
540 let desc = describe_prune_config(&config);
541 assert!(desc.is_empty());
543 }
544
545 #[test]
546 fn describe_selections_with_distances() {
547 let mut selections = BTreeMap::new();
548 selections.insert(SnapshotComponentType::State, ComponentSelection::All);
549 selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
550 selections
551 .insert(SnapshotComponentType::Transactions, ComponentSelection::Distance(10_064));
552 selections.insert(SnapshotComponentType::Receipts, ComponentSelection::None);
553 let config = config_for_selections(
554 &selections,
555 &empty_manifest(),
556 None,
557 None::<&reth_chainspec::ChainSpec>,
558 );
559 let desc = describe_prune_config(&config);
560 assert!(desc.contains(&"sender_recovery=\"full\"".to_string()));
561 assert!(desc.contains(&"bodies_history={ distance = 10064 }".to_string()));
563 assert!(desc.contains(&"receipts={ distance = 64 }".to_string()));
564 }
565
566 #[test]
567 fn reset_index_stage_checkpoints_clears_only_rocksdb_index_stages() {
568 let dir = tempfile::tempdir().unwrap();
569 let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
570
571 let tip_checkpoint = StageCheckpoint::new(24_500_000);
573 {
574 let tx = db.tx_mut().unwrap();
575 for stage_id in INDEX_STAGE_IDS {
576 tx.put::<tables::StageCheckpoints>(stage_id.to_string(), tip_checkpoint).unwrap();
577 }
578 for segment in INDEX_PRUNE_SEGMENTS {
579 tx.put::<tables::PruneCheckpoints>(
580 segment,
581 PruneCheckpoint {
582 block_number: Some(24_500_000),
583 tx_number: None,
584 prune_mode: PruneMode::Full,
585 },
586 )
587 .unwrap();
588 }
589
590 tx.put::<tables::StageCheckpoints>("SenderRecovery".to_string(), tip_checkpoint)
592 .unwrap();
593 tx.put::<tables::PruneCheckpoints>(
594 PruneSegment::SenderRecovery,
595 PruneCheckpoint {
596 block_number: Some(24_500_000),
597 tx_number: None,
598 prune_mode: PruneMode::Full,
599 },
600 )
601 .unwrap();
602 tx.commit().unwrap();
603 }
604
605 {
607 let tx = db.tx_mut().unwrap();
608 reset_index_stage_checkpoints_tx(&tx).unwrap();
609 tx.commit().unwrap();
610 }
611
612 let tx = db.tx().unwrap();
614 for stage_id in INDEX_STAGE_IDS {
615 let checkpoint = tx
616 .get::<tables::StageCheckpoints>(stage_id.to_string())
617 .unwrap()
618 .expect("checkpoint should exist");
619 assert_eq!(checkpoint.block_number, 0, "stage {stage_id} should be reset to block 0");
620 }
621
622 for segment in INDEX_PRUNE_SEGMENTS {
624 assert!(
625 tx.get::<tables::PruneCheckpoints>(segment).unwrap().is_none(),
626 "prune checkpoint for {segment} should be deleted"
627 );
628 }
629
630 let sender_stage_checkpoint = tx
632 .get::<tables::StageCheckpoints>("SenderRecovery".to_string())
633 .unwrap()
634 .expect("sender checkpoint should exist");
635 assert_eq!(sender_stage_checkpoint.block_number, tip_checkpoint.block_number);
636
637 let sender_prune_checkpoint = tx
638 .get::<tables::PruneCheckpoints>(PruneSegment::SenderRecovery)
639 .unwrap()
640 .expect("sender prune checkpoint should exist");
641 assert_eq!(sender_prune_checkpoint.block_number, Some(tip_checkpoint.block_number));
642 }
643}