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