//! GHOSTDAG blue set selection algorithm. //! //! GHOSTDAG selects a "blue set" of blocks that form the main chain, while //! still including transactions from parallel (red) blocks. The algorithm //! ensures that the blue set forms a k-cluster: any chain of blocks in the //! blue set has at most k blocks in its anticone. //! //! Key concepts: //! - **k parameter**: Controls the allowed parallelism (antichain width) //! - **Blue set**: Blocks considered "honest" and part of main chain //! - **Red set**: Parallel blocks that don't fit k-cluster property //! - **Selected parent**: The blue parent with highest blue score //! - **Blue score**: Cumulative count of blue blocks in ancestry use crate::{ dag::{BlockDag, DagError}, reachability::ReachabilityStore, BlockId, BlueScore, GHOSTDAG_K, }; use hashbrown::{HashMap, HashSet}; use parking_lot::RwLock; use thiserror::Error; /// GHOSTDAG data computed for each block. #[derive(Clone, Debug)] pub struct GhostdagData { /// Blue score (cumulative blue ancestor count). pub blue_score: BlueScore, /// The selected parent (highest blue score parent). pub selected_parent: BlockId, /// Blocks in the merge set that are blue. pub merge_set_blues: Vec, /// Blocks in the merge set that are red. pub merge_set_reds: Vec, /// Blues anticone sizes (for k-cluster verification). pub blues_anticone_sizes: HashMap, } impl GhostdagData { /// Creates data for the genesis block. pub fn genesis(genesis_id: BlockId) -> Self { GhostdagData { blue_score: 0, selected_parent: genesis_id, // Genesis is its own selected parent merge_set_blues: Vec::new(), merge_set_reds: Vec::new(), blues_anticone_sizes: HashMap::new(), } } /// Returns the total merge set size. pub fn merge_set_size(&self) -> usize { self.merge_set_blues.len() + self.merge_set_reds.len() } } /// Manager for GHOSTDAG calculations. pub struct GhostdagManager { /// The DAG structure. dag: std::sync::Arc, /// Reachability queries. reachability: std::sync::Arc, /// Cached GHOSTDAG data. data_cache: RwLock>, /// The k parameter. k: u8, } impl GhostdagManager { /// Creates a new GHOSTDAG manager. pub fn new( dag: std::sync::Arc, reachability: std::sync::Arc, ) -> Self { Self::with_k(dag, reachability, GHOSTDAG_K) } /// Creates a new GHOSTDAG manager with custom k. pub fn with_k( dag: std::sync::Arc, reachability: std::sync::Arc, k: u8, ) -> Self { let mut data_cache = HashMap::new(); // Initialize with genesis data let genesis = dag.genesis(); data_cache.insert(genesis, GhostdagData::genesis(genesis)); GhostdagManager { dag, reachability, data_cache: RwLock::new(data_cache), k, } } /// Processes a new block and computes its GHOSTDAG data. pub fn add_block( &self, block_id: BlockId, parents: &[BlockId], ) -> Result { if parents.is_empty() { return Err(GhostdagError::NoParents); } // 1. Find the selected parent (parent with highest blue score) let selected_parent = self.find_selected_parent(parents)?; // 2. Get the selected parent's GHOSTDAG data let selected_parent_data = self.get_data(&selected_parent)?; // 3. Calculate merge set (blocks not in selected parent's past) let merge_set = self.calculate_merge_set(block_id, parents, &selected_parent)?; // 4. Partition merge set into blues and reds using k-cluster let (merge_set_blues, merge_set_reds, blues_anticone_sizes) = self.partition_merge_set(&selected_parent_data, &merge_set)?; // 5. Calculate blue score let blue_score = selected_parent_data.blue_score + merge_set_blues.len() as u64 + 1; let data = GhostdagData { blue_score, selected_parent, merge_set_blues, merge_set_reds, blues_anticone_sizes, }; // Cache the result self.data_cache.write().insert(block_id, data.clone()); // Update the DAG with GHOSTDAG data self.dag.update_ghostdag_data( &block_id, data.blue_score, Some(data.selected_parent), true, // New blocks start as blue (may be re-evaluated) data.merge_set_blues.clone(), data.merge_set_reds.clone(), )?; Ok(data) } /// Finds the parent with the highest blue score. fn find_selected_parent(&self, parents: &[BlockId]) -> Result { let mut best_parent = parents[0]; let mut best_score = self.get_blue_score(&parents[0])?; for parent in parents.iter().skip(1) { let score = self.get_blue_score(parent)?; if score > best_score { best_score = score; best_parent = *parent; } } Ok(best_parent) } /// Calculates the merge set for a new block. /// /// The merge set contains all blocks that are: /// - In the past of the new block's parents /// - Not in the past of the selected parent fn calculate_merge_set( &self, _block_id: BlockId, parents: &[BlockId], selected_parent: &BlockId, ) -> Result, GhostdagError> { let mut merge_set = Vec::new(); // For each non-selected parent, add blocks not in selected parent's past for parent in parents { if parent == selected_parent { continue; } // Check if this parent is in the selected parent's past let is_in_past = self .reachability .is_ancestor(selected_parent, parent) .map_err(|e| GhostdagError::ReachabilityError(e.to_string()))?; if !is_in_past { merge_set.push(*parent); // Also add ancestors of this parent that aren't in selected parent's past // (up to merge depth limit) let parent_ancestors = self.get_ancestors_not_in_past(parent, selected_parent)?; merge_set.extend(parent_ancestors); } } // Remove duplicates merge_set.sort(); merge_set.dedup(); Ok(merge_set) } /// Gets ancestors of a block that are not in another block's past. fn get_ancestors_not_in_past( &self, block: &BlockId, reference: &BlockId, ) -> Result, GhostdagError> { let mut result = Vec::new(); let mut visited = HashSet::new(); let mut frontier = vec![*block]; // Limit traversal depth to avoid expensive computations const MAX_DEPTH: usize = 100; let mut depth = 0; while !frontier.is_empty() && depth < MAX_DEPTH { let mut next_frontier = Vec::new(); for current in frontier { if visited.contains(¤t) { continue; } visited.insert(current); // Check if current is in reference's past let is_in_past = self .reachability .is_ancestor(reference, ¤t) .unwrap_or(true); // Assume in past if error if !is_in_past && current != *block { result.push(current); // Add parents to frontier if let Some(parents) = self.dag.get_parents(¤t) { next_frontier.extend(parents.iter().copied()); } } } frontier = next_frontier; depth += 1; } Ok(result) } /// Partitions the merge set into blues and reds using k-cluster. /// /// A block is blue if adding it to the blue set maintains the k-cluster property: /// - For any blue block B in the merge set, |anticone(B) ∩ blues| ≤ k /// /// OPTIMIZATION: Uses incremental anticone tracking to avoid O(n²) complexity. /// Instead of recalculating anticone sizes, we track them incrementally as we /// add new blues and update existing sizes in O(n) per candidate. fn partition_merge_set( &self, selected_parent_data: &GhostdagData, merge_set: &[BlockId], ) -> Result<(Vec, Vec, HashMap), GhostdagError> { let mut blues: Vec = Vec::with_capacity(merge_set.len()); let mut reds: Vec = Vec::new(); let mut blues_anticone_sizes: HashMap = HashMap::with_capacity( merge_set.len() + selected_parent_data.blues_anticone_sizes.len(), ); // Initialize with selected parent's blues anticone sizes blues_anticone_sizes.extend(selected_parent_data.blues_anticone_sizes.iter()); // Sort merge set by blue score (process higher-scoring blocks first) // Pre-fetch scores to avoid repeated lookups during sort let mut sorted_merge_set: Vec<_> = merge_set .iter() .map(|id| (*id, self.get_blue_score(id).unwrap_or(0))) .collect(); sorted_merge_set.sort_by(|a, b| b.1.cmp(&a.1)); // Descending by score for (candidate, _score) in sorted_merge_set { // OPTIMIZATION: Calculate anticone relationships in a single pass // and track which existing blues are in the candidate's anticone let (anticone_size, anticone_blues) = self.calculate_blues_anticone_optimized(&candidate, &blues)?; // Check if adding this block maintains k-cluster property if anticone_size <= self.k as usize { // OPTIMIZATION: Check k-cluster incrementally // Only check blues that are in the candidate's anticone let can_add = self.check_k_cluster_incremental(&anticone_blues, &blues_anticone_sizes)?; if can_add { // Update anticone sizes for existing blues that are in candidate's anticone for blue in &anticone_blues { if let Some(size) = blues_anticone_sizes.get_mut(blue) { *size += 1; } } blues_anticone_sizes.insert(candidate, anticone_size); blues.push(candidate); } else { reds.push(candidate); } } else { reds.push(candidate); } } Ok((blues, reds, blues_anticone_sizes)) } /// Calculates anticone size and returns which blues are in the anticone. /// /// OPTIMIZATION: Returns both the count AND the list of anticone blues, /// avoiding redundant reachability queries later. fn calculate_blues_anticone_optimized( &self, candidate: &BlockId, current_blues: &[BlockId], ) -> Result<(usize, Vec), GhostdagError> { let mut anticone_blues = Vec::with_capacity(self.k as usize + 1); for blue in current_blues { let is_anticone = self .reachability .is_anticone(candidate, blue) .map_err(|e| GhostdagError::ReachabilityError(e.to_string()))?; if is_anticone { anticone_blues.push(*blue); // Early exit if we already exceed k (no point continuing) if anticone_blues.len() > self.k as usize { return Ok((anticone_blues.len(), anticone_blues)); } } } Ok((anticone_blues.len(), anticone_blues)) } /// Checks if adding a new blue would violate k-cluster for existing blues. /// /// OPTIMIZATION: Only checks blues that are in the new block's anticone, /// and uses the pre-computed anticone sizes instead of recalculating. fn check_k_cluster_incremental( &self, anticone_blues: &[BlockId], blues_anticone_sizes: &HashMap, ) -> Result { // For each blue in the candidate's anticone, check if adding 1 to its // anticone size would exceed k for blue in anticone_blues { let current_size = blues_anticone_sizes.get(blue).copied().unwrap_or(0); if current_size >= self.k as usize { return Ok(false); } } Ok(true) } /// Legacy method for compatibility - calculates blues anticone size. #[allow(dead_code)] fn calculate_blues_anticone_size( &self, candidate: &BlockId, current_blues: &[BlockId], ) -> Result { let (size, _) = self.calculate_blues_anticone_optimized(candidate, current_blues)?; Ok(size) } /// Legacy method for compatibility - checks k-cluster constraint. #[allow(dead_code)] fn check_k_cluster_with_new_blue( &self, new_blue: &BlockId, current_blues: &[BlockId], ) -> Result { let (_, anticone_blues) = self.calculate_blues_anticone_optimized(new_blue, current_blues)?; // Build a temporary anticone sizes map for the check let mut temp_sizes = HashMap::new(); for blue in current_blues { let (size, _) = self.calculate_blues_anticone_optimized(blue, current_blues)?; temp_sizes.insert(*blue, size); } self.check_k_cluster_incremental(&anticone_blues, &temp_sizes) } /// Gets the GHOSTDAG data for a block. pub fn get_data(&self, block_id: &BlockId) -> Result { self.data_cache .read() .get(block_id) .cloned() .ok_or(GhostdagError::DataNotFound(*block_id)) } /// Gets the blue score for a block. pub fn get_blue_score(&self, block_id: &BlockId) -> Result { self.data_cache .read() .get(block_id) .map(|d| d.blue_score) .ok_or(GhostdagError::DataNotFound(*block_id)) } /// Gets the selected parent of a block. pub fn get_selected_parent(&self, block_id: &BlockId) -> Result { self.data_cache .read() .get(block_id) .map(|d| d.selected_parent) .ok_or(GhostdagError::DataNotFound(*block_id)) } /// Returns the chain of selected parents from a block to genesis. pub fn get_selected_chain(&self, from: &BlockId) -> Result, GhostdagError> { let mut chain = vec![*from]; let mut current = *from; let genesis = self.dag.genesis(); while current != genesis { let parent = self.get_selected_parent(¤t)?; chain.push(parent); current = parent; } Ok(chain) } /// Checks if a block is in the blue set. pub fn is_blue(&self, block_id: &BlockId) -> bool { self.dag .get_block(block_id) .map(|meta| meta.is_blue) .unwrap_or(false) } /// Returns the k parameter. pub fn k(&self) -> u8 { self.k } } impl std::fmt::Debug for GhostdagManager { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("GhostdagManager") .field("k", &self.k) .field("cached_blocks", &self.data_cache.read().len()) .finish() } } /// Errors that can occur during GHOSTDAG calculations. #[derive(Debug, Error)] pub enum GhostdagError { #[error("No parents specified")] NoParents, #[error("GHOSTDAG data not found for block: {0}")] DataNotFound(BlockId), #[error("DAG error: {0}")] DagError(#[from] DagError), #[error("Reachability error: {0}")] ReachabilityError(String), #[error("Invalid block structure: {0}")] InvalidStructure(String), } #[cfg(test)] mod tests { use super::*; 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_dag() -> ( std::sync::Arc, std::sync::Arc, GhostdagManager, ) { 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 = GhostdagManager::with_k(dag.clone(), reachability.clone(), 3); (dag, reachability, ghostdag) } #[test] fn test_genesis_data() { let genesis = make_block_id(0); let (dag, reachability, ghostdag) = setup_test_dag(); let data = ghostdag.get_data(&genesis).unwrap(); assert_eq!(data.blue_score, 0); assert_eq!(data.selected_parent, genesis); } #[test] fn test_simple_chain() { let genesis = make_block_id(0); let (dag, reachability, ghostdag) = setup_test_dag(); // Add a simple chain: genesis -> block1 -> block2 let block1 = make_block_id(1); dag.insert_block(block1, vec![genesis], 100).unwrap(); reachability.add_block(block1, genesis, &[genesis]).unwrap(); let data1 = ghostdag.add_block(block1, &[genesis]).unwrap(); assert_eq!(data1.blue_score, 1); assert_eq!(data1.selected_parent, genesis); let block2 = make_block_id(2); dag.insert_block(block2, vec![block1], 200).unwrap(); reachability.add_block(block2, block1, &[block1]).unwrap(); let data2 = ghostdag.add_block(block2, &[block1]).unwrap(); assert_eq!(data2.blue_score, 2); assert_eq!(data2.selected_parent, block1); } #[test] fn test_selected_parent_highest_score() { let genesis = make_block_id(0); let (dag, reachability, ghostdag) = setup_test_dag(); // Create two branches from genesis let block1 = make_block_id(1); let block2 = make_block_id(2); dag.insert_block(block1, vec![genesis], 100).unwrap(); dag.insert_block(block2, vec![genesis], 100).unwrap(); reachability.add_block(block1, genesis, &[genesis]).unwrap(); reachability.add_block(block2, genesis, &[genesis]).unwrap(); ghostdag.add_block(block1, &[genesis]).unwrap(); ghostdag.add_block(block2, &[genesis]).unwrap(); // Extend block1's chain to make it have higher blue score let block3 = make_block_id(3); dag.insert_block(block3, vec![block1], 200).unwrap(); reachability.add_block(block3, block1, &[block1]).unwrap(); ghostdag.add_block(block3, &[block1]).unwrap(); // Now create a block with both branches as parents let block4 = make_block_id(4); dag.insert_block(block4, vec![block3, block2], 300).unwrap(); reachability .add_block(block4, block3, &[block3, block2]) .unwrap(); let data4 = ghostdag.add_block(block4, &[block3, block2]).unwrap(); // Block3 should be selected parent (higher chain) assert_eq!(data4.selected_parent, block3); } #[test] fn test_blue_score_increases() { let genesis = make_block_id(0); let (dag, reachability, ghostdag) = setup_test_dag(); let mut current = genesis; for i in 1..=5 { let block = make_block_id(i); dag.insert_block(block, vec![current], i as u64 * 100) .unwrap(); reachability.add_block(block, current, &[current]).unwrap(); let data = ghostdag.add_block(block, &[current]).unwrap(); assert_eq!(data.blue_score, i as u64); current = block; } } #[test] fn test_selected_chain() { let genesis = make_block_id(0); let (dag, reachability, ghostdag) = setup_test_dag(); // Build a chain let block1 = make_block_id(1); let block2 = make_block_id(2); let block3 = make_block_id(3); dag.insert_block(block1, vec![genesis], 100).unwrap(); dag.insert_block(block2, vec![block1], 200).unwrap(); dag.insert_block(block3, vec![block2], 300).unwrap(); reachability.add_block(block1, genesis, &[genesis]).unwrap(); reachability.add_block(block2, block1, &[block1]).unwrap(); reachability.add_block(block3, block2, &[block2]).unwrap(); ghostdag.add_block(block1, &[genesis]).unwrap(); ghostdag.add_block(block2, &[block1]).unwrap(); ghostdag.add_block(block3, &[block2]).unwrap(); let chain = ghostdag.get_selected_chain(&block3).unwrap(); assert_eq!(chain, vec![block3, block2, block1, genesis]); } }