A complete blockchain implementation featuring: - synord: Full node with GHOSTDAG consensus - explorer-web: Modern React blockchain explorer with 3D DAG visualization - CLI wallet and tools - Smart contract SDK and example contracts (DEX, NFT, token) - WASM crypto library for browser/mobile
435 lines
12 KiB
Rust
435 lines
12 KiB
Rust
//! 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<Option<BlockId>>,
|
|
/// Blocks that have been pruned (headers kept, body removed).
|
|
pruned_blocks: RwLock<HashSet<BlockId>>,
|
|
}
|
|
|
|
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<BlockId> {
|
|
*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<Option<BlockId>, 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<Vec<BlockId>, 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<PruningResult, PruningError> {
|
|
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<BlockId>,
|
|
/// New pruning point.
|
|
pub new_pruning_point: Option<BlockId>,
|
|
/// Number of blocks pruned.
|
|
pub blocks_pruned: usize,
|
|
/// List of pruned block IDs.
|
|
pub blocks_to_prune: Vec<BlockId>,
|
|
}
|
|
|
|
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<BlockId>,
|
|
/// Blue scores for verification.
|
|
pub blue_scores: Vec<BlueScore>,
|
|
}
|
|
|
|
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<BlockDag>,
|
|
std::sync::Arc<ReachabilityStore>,
|
|
std::sync::Arc<GhostdagManager>,
|
|
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());
|
|
}
|
|
}
|