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 { block_interval: PruneConfig::default().block_interval, segments },
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::None => Some(min_distance.map_or(PruneMode::Full, PruneMode::Distance)),
252 }
253}
254
255pub(crate) fn describe_prune_config(config: &Config) -> Vec<String> {
257 let segments = &config.prune.segments;
258
259 [
260 ("sender_recovery", segments.sender_recovery),
261 ("transaction_lookup", segments.transaction_lookup),
262 ("bodies_history", segments.bodies_history),
263 ("receipts", segments.receipts),
264 ("account_history", segments.account_history),
265 ("storage_history", segments.storage_history),
266 ]
267 .into_iter()
268 .filter_map(|(name, mode)| mode.map(|m| format!("{name}={}", format_mode(&m))))
269 .collect()
270}
271
272fn format_mode(mode: &PruneMode) -> String {
273 match mode {
274 PruneMode::Full => "\"full\"".to_string(),
275 PruneMode::Distance(d) => format!("{{ distance = {d} }}"),
276 PruneMode::Before(b) => format!("{{ before = {b} }}"),
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use reth_db::Database;
284
285 fn empty_manifest() -> SnapshotManifest {
287 SnapshotManifest {
288 block: 0,
289 chain_id: 1,
290 storage_version: 2,
291 timestamp: 0,
292 base_url: None,
293 components: BTreeMap::new(),
294 }
295 }
296
297 #[test]
298 fn write_prune_checkpoints_sets_all_segments() {
299 let dir = tempfile::tempdir().unwrap();
300 let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
301
302 let mut selections = BTreeMap::new();
303 selections.insert(SnapshotComponentType::State, ComponentSelection::All);
304 selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
305 let config = config_for_selections(
306 &selections,
307 &empty_manifest(),
308 None,
309 None::<&reth_chainspec::ChainSpec>,
310 );
311 let snapshot_block = 21_000_000;
312
313 {
314 let tx = db.tx_mut().unwrap();
315 write_prune_checkpoints_tx(&tx, &config, snapshot_block).unwrap();
316 tx.commit().unwrap();
317 }
318
319 let tx = db.tx().unwrap();
321 for segment in [
322 PruneSegment::SenderRecovery,
323 PruneSegment::TransactionLookup,
324 PruneSegment::Receipts,
325 PruneSegment::AccountHistory,
326 PruneSegment::StorageHistory,
327 PruneSegment::Bodies,
328 ] {
329 let checkpoint = tx
330 .get::<tables::PruneCheckpoints>(segment)
331 .unwrap()
332 .unwrap_or_else(|| panic!("expected checkpoint for {segment}"));
333 assert_eq!(checkpoint.block_number, Some(snapshot_block));
334 assert_eq!(checkpoint.tx_number, None);
336 }
337 }
338
339 #[test]
340 fn write_prune_checkpoints_archive_no_checkpoints() {
341 let dir = tempfile::tempdir().unwrap();
342 let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
343
344 let mut selections = BTreeMap::new();
346 for ty in SnapshotComponentType::ALL {
347 selections.insert(ty, ComponentSelection::All);
348 }
349 let config = config_for_selections(
350 &selections,
351 &empty_manifest(),
352 None,
353 None::<&reth_chainspec::ChainSpec>,
354 );
355
356 {
357 let tx = db.tx_mut().unwrap();
358 write_prune_checkpoints_tx(&tx, &config, 21_000_000).unwrap();
359 tx.commit().unwrap();
360 }
361
362 let tx = db.tx().unwrap();
363 for segment in [PruneSegment::SenderRecovery, PruneSegment::TransactionLookup] {
364 assert!(
365 tx.get::<tables::PruneCheckpoints>(segment).unwrap().is_none(),
366 "expected no checkpoint for {segment} on archive node"
367 );
368 }
369 }
370
371 #[test]
372 fn selections_all_no_pruning() {
373 let mut selections = BTreeMap::new();
374 for ty in SnapshotComponentType::ALL {
375 selections.insert(ty, ComponentSelection::All);
376 }
377 let config = config_for_selections(
378 &selections,
379 &empty_manifest(),
380 None,
381 None::<&reth_chainspec::ChainSpec>,
382 );
383 assert_eq!(config.prune.segments.transaction_lookup, None);
385 assert_eq!(config.prune.segments.sender_recovery, None);
386 assert_eq!(config.prune.segments.bodies_history, None);
387 assert_eq!(config.prune.segments.receipts, None);
388 assert_eq!(config.prune.segments.account_history, None);
389 assert_eq!(config.prune.segments.storage_history, None);
390 }
391
392 #[test]
393 fn selections_none_clamps_to_minimum_distance() {
394 let mut selections = BTreeMap::new();
395 selections.insert(SnapshotComponentType::State, ComponentSelection::All);
396 selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
397 let config = config_for_selections(
398 &selections,
399 &empty_manifest(),
400 None,
401 None::<&reth_chainspec::ChainSpec>,
402 );
403 assert_eq!(config.prune.segments.transaction_lookup, Some(PruneMode::Full));
404 assert_eq!(config.prune.segments.sender_recovery, Some(PruneMode::Full));
405 assert_eq!(
407 config.prune.segments.bodies_history,
408 Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
409 );
410 assert_eq!(
411 config.prune.segments.receipts,
412 Some(PruneMode::Distance(MINIMUM_RECEIPTS_DISTANCE))
413 );
414 assert_eq!(
415 config.prune.segments.account_history,
416 Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
417 );
418 assert_eq!(
419 config.prune.segments.storage_history,
420 Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
421 );
422 }
423
424 #[test]
425 fn selections_distance_maps_bodies_history() {
426 let mut selections = BTreeMap::new();
427 selections.insert(SnapshotComponentType::State, ComponentSelection::All);
428 selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
429 selections
430 .insert(SnapshotComponentType::Transactions, ComponentSelection::Distance(10_064));
431 selections.insert(SnapshotComponentType::Receipts, ComponentSelection::None);
432 selections
433 .insert(SnapshotComponentType::AccountChangesets, ComponentSelection::Distance(10_064));
434 selections
435 .insert(SnapshotComponentType::StorageChangesets, ComponentSelection::Distance(10_064));
436 let config = config_for_selections(
437 &selections,
438 &empty_manifest(),
439 None,
440 None::<&reth_chainspec::ChainSpec>,
441 );
442
443 assert_eq!(config.prune.segments.transaction_lookup, Some(PruneMode::Full));
444 assert_eq!(config.prune.segments.sender_recovery, Some(PruneMode::Full));
445 assert_eq!(config.prune.segments.bodies_history, Some(PruneMode::Distance(10_064)));
447 assert_eq!(
448 config.prune.segments.receipts,
449 Some(PruneMode::Distance(MINIMUM_RECEIPTS_DISTANCE))
450 );
451 assert_eq!(config.prune.segments.account_history, Some(PruneMode::Distance(10_064)));
452 assert_eq!(config.prune.segments.storage_history, Some(PruneMode::Distance(10_064)));
453 }
454
455 #[test]
456 fn full_preset_matches_default_full_prune_config() {
457 let mut selections = BTreeMap::new();
458 selections.insert(SnapshotComponentType::State, ComponentSelection::All);
459 selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
460 selections
461 .insert(SnapshotComponentType::Transactions, ComponentSelection::Distance(500_000));
462 selections.insert(SnapshotComponentType::Receipts, ComponentSelection::Distance(10_064));
463
464 let chain_spec = reth_chainspec::MAINNET.clone();
465 let config = config_for_selections(
466 &selections,
467 &empty_manifest(),
468 Some(SelectionPreset::Full),
469 Some(chain_spec.as_ref()),
470 );
471
472 assert_eq!(config.prune.segments.sender_recovery, Some(PruneMode::Full));
473 assert_eq!(config.prune.segments.transaction_lookup, None);
474 assert_eq!(
475 config.prune.segments.receipts,
476 Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
477 );
478 assert_eq!(
479 config.prune.segments.account_history,
480 Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
481 );
482 assert_eq!(
483 config.prune.segments.storage_history,
484 Some(PruneMode::Distance(MINIMUM_HISTORY_DISTANCE))
485 );
486
487 let paris_block = chain_spec
488 .ethereum_fork_activation(EthereumHardfork::Paris)
489 .block_number()
490 .expect("mainnet Paris block should be known");
491 assert_eq!(config.prune.segments.bodies_history, Some(PruneMode::Before(paris_block)));
492 }
493
494 #[test]
495 fn describe_selections_all_no_pruning() {
496 let mut selections = BTreeMap::new();
497 for ty in SnapshotComponentType::ALL {
498 selections.insert(ty, ComponentSelection::All);
499 }
500 let config = config_for_selections(
501 &selections,
502 &empty_manifest(),
503 None,
504 None::<&reth_chainspec::ChainSpec>,
505 );
506 let desc = describe_prune_config(&config);
507 assert!(desc.is_empty());
509 }
510
511 #[test]
512 fn describe_selections_with_distances() {
513 let mut selections = BTreeMap::new();
514 selections.insert(SnapshotComponentType::State, ComponentSelection::All);
515 selections.insert(SnapshotComponentType::Headers, ComponentSelection::All);
516 selections
517 .insert(SnapshotComponentType::Transactions, ComponentSelection::Distance(10_064));
518 selections.insert(SnapshotComponentType::Receipts, ComponentSelection::None);
519 let config = config_for_selections(
520 &selections,
521 &empty_manifest(),
522 None,
523 None::<&reth_chainspec::ChainSpec>,
524 );
525 let desc = describe_prune_config(&config);
526 assert!(desc.contains(&"sender_recovery=\"full\"".to_string()));
527 assert!(desc.contains(&"bodies_history={ distance = 10064 }".to_string()));
529 assert!(desc.contains(&"receipts={ distance = 64 }".to_string()));
530 }
531
532 #[test]
533 fn reset_index_stage_checkpoints_clears_only_rocksdb_index_stages() {
534 let dir = tempfile::tempdir().unwrap();
535 let db = reth_db::init_db(dir.path(), reth_db::mdbx::DatabaseArguments::default()).unwrap();
536
537 let tip_checkpoint = StageCheckpoint::new(24_500_000);
539 {
540 let tx = db.tx_mut().unwrap();
541 for stage_id in INDEX_STAGE_IDS {
542 tx.put::<tables::StageCheckpoints>(stage_id.to_string(), tip_checkpoint).unwrap();
543 }
544 for segment in INDEX_PRUNE_SEGMENTS {
545 tx.put::<tables::PruneCheckpoints>(
546 segment,
547 PruneCheckpoint {
548 block_number: Some(24_500_000),
549 tx_number: None,
550 prune_mode: PruneMode::Full,
551 },
552 )
553 .unwrap();
554 }
555
556 tx.put::<tables::StageCheckpoints>("SenderRecovery".to_string(), tip_checkpoint)
558 .unwrap();
559 tx.put::<tables::PruneCheckpoints>(
560 PruneSegment::SenderRecovery,
561 PruneCheckpoint {
562 block_number: Some(24_500_000),
563 tx_number: None,
564 prune_mode: PruneMode::Full,
565 },
566 )
567 .unwrap();
568 tx.commit().unwrap();
569 }
570
571 {
573 let tx = db.tx_mut().unwrap();
574 reset_index_stage_checkpoints_tx(&tx).unwrap();
575 tx.commit().unwrap();
576 }
577
578 let tx = db.tx().unwrap();
580 for stage_id in INDEX_STAGE_IDS {
581 let checkpoint = tx
582 .get::<tables::StageCheckpoints>(stage_id.to_string())
583 .unwrap()
584 .expect("checkpoint should exist");
585 assert_eq!(checkpoint.block_number, 0, "stage {stage_id} should be reset to block 0");
586 }
587
588 for segment in INDEX_PRUNE_SEGMENTS {
590 assert!(
591 tx.get::<tables::PruneCheckpoints>(segment).unwrap().is_none(),
592 "prune checkpoint for {segment} should be deleted"
593 );
594 }
595
596 let sender_stage_checkpoint = tx
598 .get::<tables::StageCheckpoints>("SenderRecovery".to_string())
599 .unwrap()
600 .expect("sender checkpoint should exist");
601 assert_eq!(sender_stage_checkpoint.block_number, tip_checkpoint.block_number);
602
603 let sender_prune_checkpoint = tx
604 .get::<tables::PruneCheckpoints>(PruneSegment::SenderRecovery)
605 .unwrap()
606 .expect("sender prune checkpoint should exist");
607 assert_eq!(sender_prune_checkpoint.block_number, Some(tip_checkpoint.block_number));
608 }
609}