//! DAG pruning for memory management. //! //! As the DAG grows, we need to prune old blocks to maintain reasonable //! memory usage. Pruning removes blocks that are deep enough to be //! considered finalized while maintaining the ability to verify new blocks. //! //! Key concepts: //! - **Pruning point**: The oldest block we keep full data for //! - **Virtual genesis**: Acts as the new genesis for pruned DAG //! - **Finality depth**: How deep a block must be to be considered final use crate::{ dag::{BlockDag, DagError}, ghostdag::GhostdagManager, BlockId, BlueScore, FINALITY_DEPTH, PRUNING_DEPTH, }; use hashbrown::HashSet; use parking_lot::RwLock; use thiserror::Error; /// Configuration for pruning behavior. #[derive(Clone, Debug)] pub struct PruningConfig { /// Depth at which blocks are considered final. pub finality_depth: u64, /// Depth at which blocks can be pruned. pub pruning_depth: u64, /// Minimum blocks to keep in DAG. pub min_blocks: usize, /// Maximum blocks before triggering pruning. pub max_blocks: usize, } impl Default for PruningConfig { fn default() -> Self { PruningConfig { finality_depth: FINALITY_DEPTH, pruning_depth: PRUNING_DEPTH, min_blocks: 10_000, max_blocks: 1_000_000, } } } impl PruningConfig { /// Creates a config suitable for testing. pub fn for_testing() -> Self { PruningConfig { finality_depth: 100, pruning_depth: 200, min_blocks: 10, max_blocks: 1000, } } } /// Manages pruning of the DAG. pub struct PruningManager { /// Configuration. config: PruningConfig, /// Current pruning point. pruning_point: RwLock>, /// Blocks that have been pruned (headers kept, body removed). pruned_blocks: RwLock>, } impl PruningManager { /// Creates a new pruning manager. pub fn new(config: PruningConfig) -> Self { PruningManager { config, pruning_point: RwLock::new(None), pruned_blocks: RwLock::new(HashSet::new()), } } /// Creates a pruning manager with default config. pub fn with_defaults() -> Self { Self::new(PruningConfig::default()) } /// Returns the current pruning point. pub fn pruning_point(&self) -> Option { *self.pruning_point.read() } /// Checks if a block has been pruned. pub fn is_pruned(&self, block_id: &BlockId) -> bool { self.pruned_blocks.read().contains(block_id) } /// Calculates the new pruning point based on the current tips. /// /// The pruning point is the block at `pruning_depth` on the selected chain. pub fn calculate_pruning_point( &self, ghostdag: &GhostdagManager, tips: &[BlockId], ) -> Result, PruningError> { if tips.is_empty() { return Ok(None); } // Find the tip with highest blue score let mut best_tip = tips[0]; let mut best_score = ghostdag.get_blue_score(&tips[0]).unwrap_or(0); for tip in tips.iter().skip(1) { let score = ghostdag.get_blue_score(tip).unwrap_or(0); if score > best_score { best_score = score; best_tip = *tip; } } // Get the selected chain from best tip let selected_chain = ghostdag.get_selected_chain(&best_tip)?; // Find block at pruning depth if selected_chain.len() > self.config.pruning_depth as usize { let pruning_index = self.config.pruning_depth as usize; Ok(Some(selected_chain[pruning_index])) } else { Ok(None) // Not deep enough to prune } } /// Calculates which blocks can be pruned. /// /// Returns blocks that are: /// - Below the pruning point on the selected chain /// - Not needed for verification of recent blocks pub fn calculate_blocks_to_prune( &self, dag: &BlockDag, ghostdag: &GhostdagManager, new_pruning_point: &BlockId, ) -> Result, PruningError> { let _current_pruning_point = self.pruning_point(); let mut blocks_to_prune = Vec::new(); // Get all blocks below the new pruning point let ancestors = dag.get_ancestors(new_pruning_point, usize::MAX); for ancestor in ancestors { // Skip if already pruned if self.is_pruned(&ancestor) { continue; } // Skip genesis if ancestor == dag.genesis() { continue; } // Check if block is below finality depth let ancestor_score = ghostdag.get_blue_score(&ancestor).unwrap_or(0); let pp_score = ghostdag.get_blue_score(new_pruning_point).unwrap_or(0); if pp_score - ancestor_score > self.config.finality_depth { blocks_to_prune.push(ancestor); } } Ok(blocks_to_prune) } /// Updates the pruning point and marks blocks as pruned. pub fn update_pruning_point( &self, dag: &BlockDag, ghostdag: &GhostdagManager, tips: &[BlockId], ) -> Result { let new_pp = self.calculate_pruning_point(ghostdag, tips)?; let Some(new_pruning_point) = new_pp else { return Ok(PruningResult::no_change()); }; let current_pp = self.pruning_point(); // Check if pruning point actually changed if current_pp == Some(new_pruning_point) { return Ok(PruningResult::no_change()); } // Calculate blocks to prune let blocks_to_prune = self.calculate_blocks_to_prune(dag, ghostdag, &new_pruning_point)?; // Mark blocks as pruned let mut pruned = self.pruned_blocks.write(); for block in &blocks_to_prune { pruned.insert(*block); } // Update pruning point *self.pruning_point.write() = Some(new_pruning_point); Ok(PruningResult { old_pruning_point: current_pp, new_pruning_point: Some(new_pruning_point), blocks_pruned: blocks_to_prune.len(), blocks_to_prune, }) } /// Checks if pruning should be triggered. pub fn should_prune(&self, dag: &BlockDag) -> bool { dag.len() > self.config.max_blocks } /// Returns the number of pruned blocks. pub fn pruned_count(&self) -> usize { self.pruned_blocks.read().len() } /// Returns the configuration. pub fn config(&self) -> &PruningConfig { &self.config } /// Validates that a block is not in the pruned set. pub fn validate_not_pruned(&self, block_id: &BlockId) -> Result<(), PruningError> { if self.is_pruned(block_id) { Err(PruningError::BlockPruned(*block_id)) } else { Ok(()) } } } impl std::fmt::Debug for PruningManager { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("PruningManager") .field("pruning_point", &self.pruning_point()) .field("pruned_count", &self.pruned_count()) .field("config", &self.config) .finish() } } /// Result of a pruning operation. #[derive(Clone, Debug)] pub struct PruningResult { /// Previous pruning point. pub old_pruning_point: Option, /// New pruning point. pub new_pruning_point: Option, /// Number of blocks pruned. pub blocks_pruned: usize, /// List of pruned block IDs. pub blocks_to_prune: Vec, } impl PruningResult { /// Creates a result indicating no change. pub fn no_change() -> Self { PruningResult { old_pruning_point: None, new_pruning_point: None, blocks_pruned: 0, blocks_to_prune: Vec::new(), } } /// Returns true if pruning occurred. pub fn did_prune(&self) -> bool { self.blocks_pruned > 0 } /// Returns true if the pruning point changed. pub fn pruning_point_changed(&self) -> bool { self.old_pruning_point != self.new_pruning_point } } /// Errors related to pruning. #[derive(Debug, Error)] pub enum PruningError { #[error("Block has been pruned: {0}")] BlockPruned(BlockId), #[error("Invalid pruning point: {0}")] InvalidPruningPoint(BlockId), #[error("GHOSTDAG error: {0}")] GhostdagError(#[from] crate::ghostdag::GhostdagError), #[error("DAG error: {0}")] DagError(#[from] DagError), } /// Proof that a block existed before pruning. #[derive(Clone, Debug)] pub struct PruningProof { /// The pruning point this proof is relative to. pub pruning_point: BlockId, /// Headers of blocks in the proof path. pub headers: Vec, /// Blue scores for verification. pub blue_scores: Vec, } impl PruningProof { /// Creates a new pruning proof. pub fn new(pruning_point: BlockId) -> Self { PruningProof { pruning_point, headers: Vec::new(), blue_scores: Vec::new(), } } /// Adds a block to the proof. pub fn add_block(&mut self, block_id: BlockId, blue_score: BlueScore) { self.headers.push(block_id); self.blue_scores.push(blue_score); } /// Returns the number of blocks in the proof. pub fn len(&self) -> usize { self.headers.len() } /// Returns true if the proof is empty. pub fn is_empty(&self) -> bool { self.headers.is_empty() } } #[cfg(test)] mod tests { use super::*; use crate::{dag::BlockDag, reachability::ReachabilityStore}; use synor_types::Hash256; fn make_block_id(n: u8) -> BlockId { let mut bytes = [0u8; 32]; bytes[0] = n; Hash256::from_bytes(bytes) } fn setup_test() -> ( std::sync::Arc, std::sync::Arc, std::sync::Arc, PruningManager, ) { let genesis = make_block_id(0); let dag = std::sync::Arc::new(BlockDag::new(genesis, 0)); let reachability = std::sync::Arc::new(ReachabilityStore::new(genesis)); let ghostdag = std::sync::Arc::new(GhostdagManager::with_k( dag.clone(), reachability.clone(), 3, )); let pruning = PruningManager::new(PruningConfig::for_testing()); (dag, reachability, ghostdag, pruning) } #[test] fn test_initial_state() { let (_, _, _, pruning) = setup_test(); assert!(pruning.pruning_point().is_none()); assert_eq!(pruning.pruned_count(), 0); } #[test] fn test_is_pruned() { let (_, _, _, pruning) = setup_test(); let block = make_block_id(1); assert!(!pruning.is_pruned(&block)); // Manually add to pruned set for testing pruning.pruned_blocks.write().insert(block); assert!(pruning.is_pruned(&block)); } #[test] fn test_should_prune() { let genesis = make_block_id(0); let dag = BlockDag::new(genesis, 0); let config = PruningConfig { max_blocks: 5, ..PruningConfig::for_testing() }; let pruning = PruningManager::new(config); // Initially shouldn't prune assert!(!pruning.should_prune(&dag)); } #[test] fn test_pruning_result() { let result = PruningResult::no_change(); assert!(!result.did_prune()); assert!(!result.pruning_point_changed()); let result = PruningResult { old_pruning_point: Some(make_block_id(0)), new_pruning_point: Some(make_block_id(1)), blocks_pruned: 5, blocks_to_prune: vec![make_block_id(2)], }; assert!(result.did_prune()); assert!(result.pruning_point_changed()); } #[test] fn test_validate_not_pruned() { let (_, _, _, pruning) = setup_test(); let block = make_block_id(1); assert!(pruning.validate_not_pruned(&block).is_ok()); pruning.pruned_blocks.write().insert(block); assert!(pruning.validate_not_pruned(&block).is_err()); } #[test] fn test_pruning_proof() { let mut proof = PruningProof::new(make_block_id(0)); assert!(proof.is_empty()); proof.add_block(make_block_id(1), 100); proof.add_block(make_block_id(2), 200); assert_eq!(proof.len(), 2); assert!(!proof.is_empty()); } }