kaspa_consensus/processes/
pruning.rs

1use std::{collections::VecDeque, sync::Arc};
2
3use super::reachability::ReachabilityResultExtensions;
4use crate::model::{
5    services::reachability::{MTReachabilityService, ReachabilityService},
6    stores::{
7        ghostdag::{CompactGhostdagData, GhostdagStoreReader},
8        headers::HeaderStoreReader,
9        headers_selected_tip::HeadersSelectedTipStoreReader,
10        past_pruning_points::PastPruningPointsStoreReader,
11        pruning::PruningPointInfo,
12        reachability::ReachabilityStoreReader,
13    },
14};
15use kaspa_hashes::Hash;
16use kaspa_utils::option::OptionExtensions;
17use parking_lot::RwLock;
18
19#[derive(Clone)]
20pub struct PruningPointManager<
21    S: GhostdagStoreReader,
22    T: ReachabilityStoreReader,
23    U: HeaderStoreReader,
24    V: PastPruningPointsStoreReader,
25    W: HeadersSelectedTipStoreReader,
26> {
27    pruning_depth: u64,
28    finality_depth: u64,
29    genesis_hash: Hash,
30
31    reachability_service: MTReachabilityService<T>,
32    ghostdag_store: Arc<S>,
33    headers_store: Arc<U>,
34    past_pruning_points_store: Arc<V>,
35    header_selected_tip_store: Arc<RwLock<W>>,
36}
37
38impl<
39        S: GhostdagStoreReader,
40        T: ReachabilityStoreReader,
41        U: HeaderStoreReader,
42        V: PastPruningPointsStoreReader,
43        W: HeadersSelectedTipStoreReader,
44    > PruningPointManager<S, T, U, V, W>
45{
46    pub fn new(
47        pruning_depth: u64,
48        finality_depth: u64,
49        genesis_hash: Hash,
50        reachability_service: MTReachabilityService<T>,
51        ghostdag_store: Arc<S>,
52        headers_store: Arc<U>,
53        past_pruning_points_store: Arc<V>,
54        header_selected_tip_store: Arc<RwLock<W>>,
55    ) -> Self {
56        Self {
57            pruning_depth,
58            finality_depth,
59            genesis_hash,
60            reachability_service,
61            ghostdag_store,
62            headers_store,
63            past_pruning_points_store,
64            header_selected_tip_store,
65        }
66    }
67
68    pub fn next_pruning_points_and_candidate_by_ghostdag_data(
69        &self,
70        ghostdag_data: CompactGhostdagData,
71        suggested_low_hash: Option<Hash>,
72        current_candidate: Hash,
73        current_pruning_point: Hash,
74    ) -> (Vec<Hash>, Hash) {
75        let low_hash = match suggested_low_hash {
76            Some(suggested) => {
77                if !self.reachability_service.is_chain_ancestor_of(suggested, current_candidate) {
78                    assert!(self.reachability_service.is_chain_ancestor_of(current_candidate, suggested));
79                    suggested
80                } else {
81                    current_candidate
82                }
83            }
84            None => current_candidate,
85        };
86
87        // If the pruning point is more out of date than that, an IBD with headers proof is needed anyway.
88        let mut new_pruning_points = Vec::with_capacity((self.pruning_depth / self.finality_depth) as usize);
89        let mut latest_pruning_point_bs = self.ghostdag_store.get_blue_score(current_pruning_point).unwrap();
90
91        if latest_pruning_point_bs + self.pruning_depth > ghostdag_data.blue_score {
92            // The pruning point is not in depth of self.pruning_depth, so there's
93            // no point in checking if it is required to update it. This can happen
94            // because the virtual is not updated after IBD, so the pruning point
95            // might be in depth less than self.pruning_depth.
96            return (vec![], current_candidate);
97        }
98
99        let mut new_candidate = current_candidate;
100
101        for selected_child in self.reachability_service.forward_chain_iterator(low_hash, ghostdag_data.selected_parent, true) {
102            let selected_child_bs = self.ghostdag_store.get_blue_score(selected_child).unwrap();
103
104            if ghostdag_data.blue_score - selected_child_bs < self.pruning_depth {
105                break;
106            }
107
108            new_candidate = selected_child;
109            let new_candidate_bs = selected_child_bs;
110
111            if self.finality_score(new_candidate_bs) > self.finality_score(latest_pruning_point_bs) {
112                new_pruning_points.push(new_candidate);
113                latest_pruning_point_bs = new_candidate_bs;
114            }
115        }
116
117        (new_pruning_points, new_candidate)
118    }
119
120    // finality_score is the number of finality intervals passed since
121    // the given block.
122    fn finality_score(&self, blue_score: u64) -> u64 {
123        blue_score / self.finality_depth
124    }
125
126    pub fn expected_header_pruning_point(&self, ghostdag_data: CompactGhostdagData, pruning_info: PruningPointInfo) -> Hash {
127        if ghostdag_data.selected_parent == self.genesis_hash {
128            return self.genesis_hash;
129        }
130
131        let (current_pruning_point, current_candidate, current_pruning_point_index) = pruning_info.decompose();
132
133        let sp_header_pp = self.headers_store.get_header(ghostdag_data.selected_parent).unwrap().pruning_point;
134        let sp_header_pp_blue_score = self.headers_store.get_blue_score(sp_header_pp).unwrap();
135
136        // If the block doesn't have the pruning in its selected chain we know for sure that it can't trigger a pruning point
137        // change (we check the selected parent to take care of the case where the block is the virtual which doesn't have reachability data).
138        let has_pruning_point_in_its_selected_chain =
139            self.reachability_service.is_chain_ancestor_of(current_pruning_point, ghostdag_data.selected_parent);
140
141        // Note: the pruning point from the POV of the current block is the first block in its chain that is in depth of self.pruning_depth and
142        // its finality score is greater than the previous pruning point. This is why if the diff between finality_score(selected_parent.blue_score + 1) * finality_interval
143        // and the current block blue score is less than self.pruning_depth we can know for sure that this block didn't trigger a pruning point change.
144        let min_required_blue_score_for_next_pruning_point = (self.finality_score(sp_header_pp_blue_score) + 1) * self.finality_depth;
145        let next_or_current_pp = if has_pruning_point_in_its_selected_chain
146            && min_required_blue_score_for_next_pruning_point + self.pruning_depth <= ghostdag_data.blue_score
147        {
148            // If the selected parent pruning point is in the future of current global pruning point, then provide it as a suggestion
149            let suggested_low_hash = self
150                .reachability_service
151                .is_dag_ancestor_of_result(current_pruning_point, sp_header_pp)
152                .unwrap_option()
153                .and_then(|b| if b { Some(sp_header_pp) } else { None });
154            let (new_pruning_points, _) = self.next_pruning_points_and_candidate_by_ghostdag_data(
155                ghostdag_data,
156                suggested_low_hash,
157                current_candidate,
158                current_pruning_point,
159            );
160
161            new_pruning_points.last().copied().unwrap_or(current_pruning_point)
162        } else {
163            sp_header_pp
164        };
165
166        if self.is_pruning_point_in_pruning_depth(ghostdag_data.blue_score, next_or_current_pp) {
167            return next_or_current_pp;
168        }
169
170        for i in (0..=current_pruning_point_index).rev() {
171            let past_pp = self.past_pruning_points_store.get(i).unwrap();
172            if self.is_pruning_point_in_pruning_depth(ghostdag_data.blue_score, past_pp) {
173                return past_pp;
174            }
175        }
176
177        self.genesis_hash
178    }
179
180    fn is_pruning_point_in_pruning_depth(&self, pov_blue_score: u64, pruning_point: Hash) -> bool {
181        let pp_bs = self.headers_store.get_blue_score(pruning_point).unwrap();
182        pov_blue_score >= pp_bs + self.pruning_depth
183    }
184
185    pub fn is_valid_pruning_point(&self, pp_candidate: Hash, hst: Hash) -> bool {
186        if pp_candidate == self.genesis_hash {
187            return true;
188        }
189        if !self.reachability_service.is_chain_ancestor_of(pp_candidate, hst) {
190            return false;
191        }
192
193        let hst_bs = self.ghostdag_store.get_blue_score(hst).unwrap();
194        self.is_pruning_point_in_pruning_depth(hst_bs, pp_candidate)
195    }
196
197    pub fn are_pruning_points_in_valid_chain(&self, pruning_info: PruningPointInfo, hst: Hash) -> bool {
198        // We want to validate that the past pruning points form a chain to genesis. Since
199        // each pruning point's header doesn't point to the previous pruning point, but to
200        // the pruning point from its POV, we can't just traverse from one pruning point to
201        // the next one by merely relying on the current pruning point header, but instead
202        // we rely on the fact that each pruning point is pointed by another known block or
203        // pruning point.
204        // So in the first stage we go over the selected chain and add to the queue of expected
205        // pruning points all the pruning points from the POV of some chain block. In the second
206        // stage we go over the past pruning points from recent to older, check that it's the head
207        // of the queue (by popping the queue), and add its header pruning point to the queue since
208        // we expect to see it later on the list.
209        // The first stage is important because the most recent pruning point is pointing to a few
210        // pruning points before, so the first few pruning points on the list won't be pointed by
211        // any other pruning point in the list, so we are compelled to check if it's referenced by
212        // the selected chain.
213        let mut expected_pps_queue = VecDeque::new();
214        for current in self.reachability_service.backward_chain_iterator(hst, pruning_info.pruning_point, false) {
215            let current_header = self.headers_store.get_header(current).unwrap();
216            if expected_pps_queue.back().is_none_or_ex(|&&h| h != current_header.pruning_point) {
217                expected_pps_queue.push_back(current_header.pruning_point);
218            }
219        }
220
221        for idx in (0..=pruning_info.index).rev() {
222            let pp = self.past_pruning_points_store.get(idx).unwrap();
223            let pp_header = self.headers_store.get_header(pp).unwrap();
224            let Some(expected_pp) = expected_pps_queue.pop_front() else {
225                // If we have less than expected pruning points.
226                return false;
227            };
228
229            if expected_pp != pp {
230                return false;
231            }
232
233            if idx == 0 {
234                // The 0th pruning point should always be genesis, and no
235                // more pruning points should be expected below it.
236                if !expected_pps_queue.is_empty() || pp != self.genesis_hash {
237                    return false;
238                }
239                break;
240            }
241
242            // Add the pruning point from the POV of the current one if it's
243            // not already added.
244            match expected_pps_queue.back() {
245                Some(last_added_pp) => {
246                    if *last_added_pp != pp_header.pruning_point {
247                        expected_pps_queue.push_back(pp_header.pruning_point);
248                    }
249                }
250                None => {
251                    // expected_pps_queue should always have one block in the queue
252                    // until we reach genesis.
253                    return false;
254                }
255            }
256        }
257
258        true
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    // TODO: add unit-tests for next_pruning_point_and_candidate_by_block_hash and expected_header_pruning_point
265}