611 lines
21 KiB
Rust
611 lines
21 KiB
Rust
//! 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<BlockId>,
|
|
/// Blocks in the merge set that are red.
|
|
pub merge_set_reds: Vec<BlockId>,
|
|
/// Blues anticone sizes (for k-cluster verification).
|
|
pub blues_anticone_sizes: HashMap<BlockId, usize>,
|
|
}
|
|
|
|
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<BlockDag>,
|
|
/// Reachability queries.
|
|
reachability: std::sync::Arc<ReachabilityStore>,
|
|
/// Cached GHOSTDAG data.
|
|
data_cache: RwLock<HashMap<BlockId, GhostdagData>>,
|
|
/// The k parameter.
|
|
k: u8,
|
|
}
|
|
|
|
impl GhostdagManager {
|
|
/// Creates a new GHOSTDAG manager.
|
|
pub fn new(
|
|
dag: std::sync::Arc<BlockDag>,
|
|
reachability: std::sync::Arc<ReachabilityStore>,
|
|
) -> Self {
|
|
Self::with_k(dag, reachability, GHOSTDAG_K)
|
|
}
|
|
|
|
/// Creates a new GHOSTDAG manager with custom k.
|
|
pub fn with_k(
|
|
dag: std::sync::Arc<BlockDag>,
|
|
reachability: std::sync::Arc<ReachabilityStore>,
|
|
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<GhostdagData, GhostdagError> {
|
|
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<BlockId, GhostdagError> {
|
|
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<Vec<BlockId>, 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<Vec<BlockId>, 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<BlockId>, Vec<BlockId>, HashMap<BlockId, usize>), GhostdagError> {
|
|
let mut blues: Vec<BlockId> = Vec::with_capacity(merge_set.len());
|
|
let mut reds: Vec<BlockId> = Vec::new();
|
|
let mut blues_anticone_sizes: HashMap<BlockId, usize> = 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<BlockId>), 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<BlockId, usize>,
|
|
) -> Result<bool, GhostdagError> {
|
|
// 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<usize, GhostdagError> {
|
|
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<bool, GhostdagError> {
|
|
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<GhostdagData, GhostdagError> {
|
|
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<BlueScore, GhostdagError> {
|
|
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<BlockId, GhostdagError> {
|
|
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<Vec<BlockId>, 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<BlockDag>,
|
|
std::sync::Arc<ReachabilityStore>,
|
|
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]);
|
|
}
|
|
}
|