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 }
298 }
299
300 #[test]
301 fn write_prune_checkpoints_sets_all_segments() {
302 let dir = tempfile::tempdir().unwrap();
303 let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
304
305 let mut selections = BTreeMap::new();
306 selections.insert(SnapshotComponentType::State, ComponentSelection::All);
307 selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
308 let config = config_for_selections(
309 &selections,
310 &empty_manifest(),
311 None,
312 None::<&reth_chainspec::ChainSpec>,
313 );
314 let snapshot_block = 21_000_000;
315
316 {
317 let tx = db.tx_mut().unwrap();
318 write_prune_checkpoints_tx(&tx, &config, snapshot_block).unwrap();
319 tx.commit().unwrap();
320 }
321
322 let tx = db.tx().unwrap();
324 for segment in [
325 PruneSegment::SenderRecovery,
326 PruneSegment::TransactionLookup,
327 PruneSegment::Receipts,
328 PruneSegment::AccountHistory,
329 PruneSegment::StorageHistory,
330 PruneSegment::Bodies,
331 ] {
332 let checkpoint = tx
333 .get::<tables::PruneCheckpoints>(segment)
334 .unwrap()
335 .unwrap_or_else(|| panic!("expected checkpoint for {segment}"));
336 assert_eq!(checkpoint.block_number, Some(snapshot_block));
337 assert_eq!(checkpoint.tx_number, None);
339 }
340 }
341
342 #[test]
343 fn write_prune_checkpoints_archive_no_checkpoints() {
344 let dir = tempfile::tempdir().unwrap();
345 let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
346
347 let mut selections = BTreeMap::new();
349 for ty in SnapshotComponentType::ALL {
350 selections.insert(ty, ComponentSelection::All);
351 }
352 let config = config_for_selections(
353 &selections,
354 &empty_manifest(),
355 None,
356 None::<&reth_chainspec::ChainSpec>,
357 );
358
359 {
360 let tx = db.tx_mut().unwrap();
361 write_prune_checkpoints_tx(&tx, &config, 21_000_000).unwrap();
362 tx.commit().unwrap();
363 }
364
365 let tx = db.tx().unwrap();
366 for segment in [PruneSegment::SenderRecovery, PruneSegment::TransactionLookup] {
367 assert!(
368 tx.get::<tables::PruneCheckpoints>(segment).unwrap().is_none(),
369 "expected no checkpoint for {segment} on archive node"
370 );
371 }
372 }
373
374 #[test]
375 fn selections_all_no_pruning() {
376 let mut selections = BTreeMap::new();
377 for ty in SnapshotComponentType::ALL {
378 selections.insert(ty, ComponentSelection::All);
379 }
380 let config = config_for_selections(
381 &selections,
382 &empty_manifest(),
383 None,
384 None::<&reth_chainspec::ChainSpec>,
385 );
386 assert_eq!(config.prune.segments.transaction_lookup, None);
388 assert_eq!(config.prune.segments.sender_recovery, None);
389 assert_eq!(config.prune.segments.bodies_history, None);
390 assert_eq!(config.prune.segments.receipts, None);
391 assert_eq!(config.prune.segments.account_history, None);
392 assert_eq!(config.prune.segments.storage_history, None);
393 }
394
395 #[test]
396 fn selections_none_clamps_to_minimum_distance() {
397 let mut selections = BTreeMap::new();
398 selections.insert(SnapshotComponentType::State, ComponentSelection::All);
399 selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
400 let config = config_for_selections(
401 &selections,
402 &empty_manifest(),
403 None,
404 None::<&reth_chainspec::ChainSpec>,
405 );
406 assert_eq!(config.prune.segments.transaction_lookup, Some(PruneMode::Full));
407 assert_eq!(config.prune.segments.sender_recovery, Some(PruneMode::Full));
408 assert_eq!(
410 config.prune.segments.bodies_history,
411 Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
412 );
413 assert_eq!(
414 config.prune.segments.receipts,
415 Some(PruneMode::Distance(MINIMUM_RECEIPTS_DISTANCE))
416 );
417 assert_eq!(
418 config.prune.segments.account_history,
419 Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
420 );
421 assert_eq!(
422 config.prune.segments.storage_history,
423 Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
424 );
425 }
426
427 #[test]
428 fn selections_distance_maps_bodies_history() {
429 let mut selections = BTreeMap::new();
430 selections.insert(SnapshotComponentType::State, ComponentSelection::All);
431 selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
432 selections
433 .insert(SnapshotComponentType::Transactions, ComponentSelection::Distance(10_064));
434 selections.insert(SnapshotComponentType::Receipts, ComponentSelection::None);
435 selections
436 .insert(SnapshotComponentType::AccountChangesets, ComponentSelection::Distance(10_064));
437 selections
438 .insert(SnapshotComponentType::StorageChangesets, ComponentSelection::Distance(10_064));
439 let config = config_for_selections(
440 &selections,
441 &empty_manifest(),
442 None,
443 None::<&reth_chainspec::ChainSpec>,
444 );
445
446 assert_eq!(config.prune.segments.transaction_lookup, Some(PruneMode::Full));
447 assert_eq!(config.prune.segments.sender_recovery, Some(PruneMode::Full));
448 assert_eq!(config.prune.segments.bodies_history, Some(PruneMode::Distance(10_064)));
450 assert_eq!(
451 config.prune.segments.receipts,
452 Some(PruneMode::Distance(MINIMUM_RECEIPTS_DISTANCE))
453 );
454 assert_eq!(config.prune.segments.account_history, Some(PruneMode::Distance(10_064)));
455 assert_eq!(config.prune.segments.storage_history, Some(PruneMode::Distance(10_064)));
456 }
457
458 #[test]
459 fn selections_since_maps_to_before_prune_mode() {
460 let mut selections = BTreeMap::new();
461 selections.insert(SnapshotComponentType::State, ComponentSelection::All);
462 selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
463 selections
464 .insert(SnapshotComponentType::Transactions, ComponentSelection::Since(15_537_394));
465 selections.insert(SnapshotComponentType::Receipts, ComponentSelection::Since(15_537_394));
466 selections.insert(
467 SnapshotComponentType::AccountChangesets,
468 ComponentSelection::Since(15_537_394),
469 );
470 selections.insert(
471 SnapshotComponentType::StorageChangesets,
472 ComponentSelection::Since(15_537_394),
473 );
474
475 let config = config_for_selections(
476 &selections,
477 &empty_manifest(),
478 None,
479 None::<&reth_chainspec::ChainSpec>,
480 );
481
482 assert_eq!(config.prune.segments.bodies_history, Some(PruneMode::Before(15_537_394)));
483 assert_eq!(config.prune.segments.receipts, Some(PruneMode::Before(15_537_394)));
484 assert_eq!(config.prune.segments.account_history, Some(PruneMode::Before(15_537_394)));
485 assert_eq!(config.prune.segments.storage_history, Some(PruneMode::Before(15_537_394)));
486 }
487
488 #[test]
489 fn full_preset_matches_default_full_prune_config() {
490 let mut selections = BTreeMap::new();
491 selections.insert(SnapshotComponentType::State, ComponentSelection::All);
492 selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
493 selections
494 .insert(SnapshotComponentType::Transactions, ComponentSelection::Distance(500_000));
495 selections.insert(SnapshotComponentType::Receipts, ComponentSelection::Distance(10_064));
496
497 let chain_spec = reth_chainspec::MAINNET.clone();
498 let config = config_for_selections(
499 &selections,
500 &empty_manifest(),
501 Some(SelectionPreset::Full),
502 Some(chain_spec.as_ref()),
503 );
504
505 assert_eq!(config.prune.segments.sender_recovery, Some(PruneMode::Full));
506 assert_eq!(config.prune.segments.transaction_lookup, None);
507 assert_eq!(
508 config.prune.segments.receipts,
509 Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
510 );
511 assert_eq!(
512 config.prune.segments.account_history,
513 Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
514 );
515 assert_eq!(
516 config.prune.segments.storage_history,
517 Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
518 );
519
520 let paris_block = chain_spec
521 .ethereum_fork_activation(EthereumHardfork::Paris)
522 .block_number()
523 .expect("mainnet Paris block should be known");
524 assert_eq!(config.prune.segments.bodies_history, Some(PruneMode::Before(paris_block)));
525 }
526
527 #[test]
528 fn describe_selections_all_no_pruning() {
529 let mut selections = BTreeMap::new();
530 for ty in SnapshotComponentType::ALL {
531 selections.insert(ty, ComponentSelection::All);
532 }
533 let config = config_for_selections(
534 &selections,
535 &empty_manifest(),
536 None,
537 None::<&reth_chainspec::ChainSpec>,
538 );
539 let desc = describe_prune_config(&config);
540 assert!(desc.is_empty());
542 }
543
544 #[test]
545 fn describe_selections_with_distances() {
546 let mut selections = BTreeMap::new();
547 selections.insert(SnapshotComponentType::State, ComponentSelection::All);
548 selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
549 selections
550 .insert(SnapshotComponentType::Transactions, ComponentSelection::Distance(10_064));
551 selections.insert(SnapshotComponentType::Receipts, ComponentSelection::None);
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.contains(&"sender_recovery=\"full\"".to_string()));
560 assert!(desc.contains(&"bodies_history={ distance = 10064 }".to_string()));
562 assert!(desc.contains(&"receipts={ distance = 64 }".to_string()));
563 }
564
565 #[test]
566 fn reset_index_stage_checkpoints_clears_only_rocksdb_index_stages() {
567 let dir = tempfile::tempdir().unwrap();
568 let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
569
570 let tip_checkpoint = StageCheckpoint::new(24_500_000);
572 {
573 let tx = db.tx_mut().unwrap();
574 for stage_id in INDEX_STAGE_IDS {
575 tx.put::<tables::StageCheckpoints>(stage_id.to_string(), tip_checkpoint).unwrap();
576 }
577 for segment in INDEX_PRUNE_SEGMENTS {
578 tx.put::<tables::PruneCheckpoints>(
579 segment,
580 PruneCheckpoint {
581 block_number: Some(24_500_000),
582 tx_number: None,
583 prune_mode: PruneMode::Full,
584 },
585 )
586 .unwrap();
587 }
588
589 tx.put::<tables::StageCheckpoints>("SenderRecovery".to_string(), tip_checkpoint)
591 .unwrap();
592 tx.put::<tables::PruneCheckpoints>(
593 PruneSegment::SenderRecovery,
594 PruneCheckpoint {
595 block_number: Some(24_500_000),
596 tx_number: None,
597 prune_mode: PruneMode::Full,
598 },
599 )
600 .unwrap();
601 tx.commit().unwrap();
602 }
603
604 {
606 let tx = db.tx_mut().unwrap();
607 reset_index_stage_checkpoints_tx(&tx).unwrap();
608 tx.commit().unwrap();
609 }
610
611 let tx = db.tx().unwrap();
613 for stage_id in INDEX_STAGE_IDS {
614 let checkpoint = tx
615 .get::<tables::StageCheckpoints>(stage_id.to_string())
616 .unwrap()
617 .expect("checkpoint should exist");
618 assert_eq!(checkpoint.block_number, 0, "stage {stage_id} should be reset to block 0");
619 }
620
621 for segment in INDEX_PRUNE_SEGMENTS {
623 assert!(
624 tx.get::<tables::PruneCheckpoints>(segment).unwrap().is_none(),
625 "prune checkpoint for {segment} should be deleted"
626 );
627 }
628
629 let sender_stage_checkpoint = tx
631 .get::<tables::StageCheckpoints>("SenderRecovery".to_string())
632 .unwrap()
633 .expect("sender checkpoint should exist");
634 assert_eq!(sender_stage_checkpoint.block_number, tip_checkpoint.block_number);
635
636 let sender_prune_checkpoint = tx
637 .get::<tables::PruneCheckpoints>(PruneSegment::SenderRecovery)
638 .unwrap()
639 .expect("sender prune checkpoint should exist");
640 assert_eq!(sender_prune_checkpoint.block_number, Some(tip_checkpoint.block_number));
641 }
642}