cfxcore/consensus/consensus_inner/
confirmation_meter.rs

1// Copyright 2019 Conflux Foundation. All rights reserved.
2// Conflux is free software and distributed under GNU General Public License.
3// See http://www.gnu.org/licenses/
4
5use crate::consensus::{
6    consensus_inner::{NULL, NULLU64},
7    ConsensusGraphInner,
8};
9use cfx_parameters::{
10    consensus::DEFERRED_STATE_EPOCH_COUNT, consensus_internal::*,
11};
12use cfx_types::H256;
13use parking_lot::RwLock;
14use std::{cmp::max, collections::VecDeque, convert::TryFrom};
15
16pub struct TotalWeightInPastMovingDelta {
17    pub old: i128,
18    pub cur: i128,
19    pub delta: i128,
20}
21
22pub struct FinalityManager {
23    pub lowest_epoch_num: u64,
24    pub risks_less_than: VecDeque<f64>,
25}
26
27struct ConfirmationMeterInner {
28    total_weight_in_past_2d: TotalWeightInPastMovingDelta,
29    finality_manager: FinalityManager,
30}
31
32impl ConfirmationMeterInner {
33    pub fn new() -> Self {
34        Self {
35            total_weight_in_past_2d: TotalWeightInPastMovingDelta {
36                old: 0,
37                cur: 0,
38                delta: 0,
39            },
40            finality_manager: FinalityManager {
41                lowest_epoch_num: 0,
42                risks_less_than: VecDeque::new(),
43            },
44        }
45    }
46}
47
48/// `ConfirmationMeter` computes an approximate *local view* confirmation risk
49/// given the current blockchain state. Local view means that the meter assumes
50/// a potential block propagation delay and assumes a worst case scenario of
51/// what this delay could do.
52///
53/// The meter serves two purposes. First, it allows the underlying storage layer
54/// to determine whether it is *relatively safe* to discard previous snapshots.
55/// Snapshot consumes a lot of disk space and it is ideal to discard old ones.
56/// Second, it enables the consensus layer to provide an interface to query the
57/// confirmation status of a block/transaction.
58pub struct ConfirmationMeter {
59    inner: RwLock<ConfirmationMeterInner>,
60}
61
62impl ConfirmationMeter {
63    pub fn new() -> Self {
64        Self {
65            inner: RwLock::new(ConfirmationMeterInner::new()),
66        }
67    }
68
69    pub fn clear(&self) {
70        let mut inner = self.inner.write();
71        *inner = ConfirmationMeterInner::new();
72    }
73
74    /// This is the function that should be invoked every 2 *
75    /// BLOCK_PROPAGATION_DELAY by the synchronization layer to measure the
76    /// weight of generated blocks in 2d
77    pub fn update_total_weight_delta_heartbeat(&self) {
78        let mut inner = self.inner.write();
79        let total_weight = &mut inner.total_weight_in_past_2d;
80        total_weight.delta = total_weight.cur - total_weight.old;
81        total_weight.old = total_weight.cur;
82    }
83
84    /// The `ConsensusGraph` calls this function for every inserted and
85    /// activated block to accumulate the total weight value
86    pub fn aggregate_total_weight_in_past(&self, weight: i128) {
87        let mut inner = self.inner.write();
88        let total_weight = &mut inner.total_weight_in_past_2d;
89        total_weight.cur += weight;
90    }
91
92    /// The `ConsensusGraph` invokes this function when making a checkpoint. The
93    /// confirmation meter needs to aware of the genesis change and make
94    /// adjustment accordingly.
95    pub fn reset_for_checkpoint(&self, total_weight: i128, stable_height: u64) {
96        let mut inner = self.inner.write();
97        let change = inner.total_weight_in_past_2d.cur - total_weight;
98        inner.total_weight_in_past_2d.cur = total_weight;
99        inner.total_weight_in_past_2d.old -= change;
100
101        if stable_height > inner.finality_manager.lowest_epoch_num {
102            let gap = stable_height - inner.finality_manager.lowest_epoch_num;
103            for _i in 0..gap {
104                inner.finality_manager.risks_less_than.pop_front();
105            }
106            inner.finality_manager.lowest_epoch_num = stable_height;
107        }
108    }
109
110    pub fn get_confirmed_epoch_num(&self) -> u64 {
111        let x = self.inner.read().finality_manager.lowest_epoch_num;
112        if x > 0 {
113            x - 1
114        } else {
115            0
116        }
117    }
118
119    /// Query the confirmation hash of a specific block.
120    pub fn confirmation_risk_by_hash(
121        &self, g_inner: &ConsensusGraphInner, hash: H256,
122    ) -> Option<f64> {
123        if hash == g_inner.data_man.true_genesis.hash() {
124            return Some(CONFIRMATION_METER_MIN_MAINTAINED_RISK);
125        }
126        let index = match g_inner.hash_to_arena_indices.get(&hash) {
127            Some(i) => *i,
128            None => {
129                // The block is not in memory, check if it's confirmed before.
130                return match g_inner
131                    .data_man
132                    .block_execution_result_by_hash_from_db(&hash)
133                {
134                    // It's garbage collected because of checkpoint, but it
135                    // is executed before checkpoint, so
136                    // is definitely confirmed.
137                    Some(_) => Some(CONFIRMATION_METER_MIN_MAINTAINED_RISK),
138                    // The block has not entered consensus or it's skipped
139                    // in execution, either not-in-same-era
140                    // or not in the epoch set bound.
141                    // FIXME: Skipped blocks' order are actually confirmed.
142                    None => None,
143                };
144            }
145        };
146        let epoch_num = g_inner.arena[index].data.epoch_number;
147        if epoch_num == NULLU64 {
148            // The block is in the anticone of cur era genesis or its not
149            // included in any epoch on the pivot chain yet.
150            // FIXME: Its order is confirmed if it's in cur_era_genesis
151            // anticone.
152            return None;
153        }
154
155        if epoch_num == 0 {
156            return Some(0.0);
157        }
158
159        let finality = &self.inner.read().finality_manager;
160
161        if epoch_num < finality.lowest_epoch_num {
162            return Some(CONFIRMATION_METER_MIN_MAINTAINED_RISK);
163        }
164
165        let idx = (epoch_num - finality.lowest_epoch_num) as usize;
166        if idx < finality.risks_less_than.len() {
167            let mut max_risk = 0.0;
168            for i in 0..idx + 1 {
169                let risk = *finality.risks_less_than.get(i).unwrap();
170                if max_risk < risk {
171                    max_risk = risk;
172                }
173            }
174            Some(max_risk)
175        } else {
176            Some(0.9)
177        }
178    }
179
180    fn confirmation_risk_from_m_n(m: i128, n: i128) -> f64 {
181        let m_n_diff = m as f64 - n as f64;
182        let mut risk = 0.9;
183        let threshold_1 = if 0.75 * m as f64 - 22.0 < 2250.0 {
184            0.75 * m as f64 - 22.0
185        } else {
186            2250.0
187        };
188        if m_n_diff >= threshold_1 {
189            return risk;
190        }
191        risk = 0.0001;
192        let threshold_2 = if 0.70 * m as f64 - 22.0 < 1500.0 {
193            0.70 * m as f64 - 22.0
194        } else {
195            1500.0
196        };
197        if m_n_diff >= threshold_2 {
198            return risk;
199        }
200        risk = 0.000001;
201        let threshold_3 = if 0.65 * m as f64 - 22.0 < 750.0 {
202            0.65 * m as f64 - 22.0
203        } else {
204            750.0
205        };
206        if m_n_diff >= threshold_3 {
207            return risk;
208        }
209        risk = 0.00000001;
210        risk
211    }
212
213    fn confirmation_risk(
214        &self, g_inner: &ConsensusGraphInner, w_0: i128, w_4: i128,
215        epoch_num: u64,
216    ) -> f64 {
217        // Compute w_1
218        let idx = g_inner.get_pivot_block_arena_index(epoch_num);
219        let pivot_idx = g_inner.height_to_pivot_index(epoch_num);
220        let w_1 = g_inner.weight_tree.get(idx);
221
222        // Compute w_2
223        let parent = g_inner.arena[idx].parent;
224        assert!(parent != NULL);
225        let mut max_weight = 0;
226        for child in g_inner.arena[parent].children.iter() {
227            if *child == idx {
228                continue;
229            }
230
231            let child_weight = g_inner.weight_tree.get(*child);
232            if child_weight > max_weight {
233                max_weight = child_weight;
234            }
235        }
236        let w_2 = max_weight;
237
238        // Compute w_3
239        let w_3 = g_inner.pivot_chain_metadata[pivot_idx].past_weight;
240
241        // Compute d
242        let d = i128::try_from(g_inner.current_difficulty.low_u128()).unwrap();
243
244        // Compute n
245        let w_2_4 = w_2 + w_4;
246        let n = if w_1 >= w_2_4 { w_1 - w_2_4 } else { 0 };
247
248        let n = (n / d) + 1;
249
250        // Compute m
251        let m = if w_0 >= w_3 { w_0 - w_3 } else { 0 };
252
253        let m = m / d;
254
255        // debug!("Confirmation Risk: m {} n {} w_0 {}, w_1 {}, w_2 {}, w_3 {},
256        // w_4 {}, epoch_num {} genesis {}", m, n, w_0, w_1, w_2, w_3, w_4,
257        // epoch_num, g_inner.cur_era_genesis_block_arena_index);
258
259        Self::confirmation_risk_from_m_n(m, n)
260    }
261
262    /// `ConsensusGraphInner` invokes this function to recompute confirmation
263    /// risk of all epochs periodically
264    pub fn update_confirmation_risks(&self, g_inner: &ConsensusGraphInner) {
265        if g_inner.pivot_chain.len() > DEFERRED_STATE_EPOCH_COUNT as usize {
266            let w_0 = g_inner
267                .weight_tree
268                .get(g_inner.cur_era_genesis_block_arena_index);
269            let mut risks = VecDeque::new();
270            let mut epoch_num = g_inner
271                .pivot_index_to_height(g_inner.pivot_chain.len())
272                - DEFERRED_STATE_EPOCH_COUNT;
273            let mut count = 0;
274            while epoch_num > g_inner.cur_era_genesis_height
275                && count < CONFIRMATION_METER_MAX_NUM_MAINTAINED_RISK
276            {
277                let w_4 = self.inner.read().total_weight_in_past_2d.delta;
278                let risk = self.confirmation_risk(g_inner, w_0, w_4, epoch_num);
279                risks.push_front(risk);
280                epoch_num -= 1;
281                count += 1;
282                if risk <= CONFIRMATION_METER_MIN_MAINTAINED_RISK {
283                    break;
284                }
285            }
286
287            if risks.is_empty() {
288                epoch_num = g_inner.cur_era_genesis_height;
289            } else {
290                epoch_num += 1;
291            }
292
293            let finality = &mut self.inner.write().finality_manager;
294            debug!("Confirmation Risk: {:?}", risks);
295            finality.lowest_epoch_num = epoch_num;
296            finality.risks_less_than = risks;
297        }
298    }
299
300    /// This is an expensive function to check whether the current tree graph
301    /// will generate adaptive block under `me` in future. This function is
302    /// used by Conflux to determine when we will remove old snapshots. If
303    /// this is true, we will avoid remove snapshots from the storage layer.
304    pub fn is_adaptive_possible(
305        &self, g_inner: &ConsensusGraphInner, me: usize,
306    ) -> bool {
307        let psi = CONFIRMATION_METER_PSI;
308        // Find the first pivot chain block whose timer diff is less than 140
309        let mut cur_height = g_inner.cur_era_stable_height;
310        let mut cur_arena_index =
311            g_inner.get_pivot_block_arena_index(cur_height);
312        while g_inner.arena[cur_arena_index]
313            .data
314            .ledger_view_timer_chain_height
315            + CONFIRMATION_METER_ADAPTIVE_TEST_TIMER_DIFF
316            <= g_inner.arena[me].data.ledger_view_timer_chain_height
317            && cur_height < g_inner.best_epoch_number()
318        {
319            cur_height += 1;
320            cur_arena_index = g_inner.get_pivot_block_arena_index(cur_height);
321        }
322
323        if cur_height == g_inner.cur_era_stable_height {
324            return false;
325        }
326
327        let mut end_checking_height =
328            (cur_height - g_inner.cur_era_stable_height + psi - 1) / psi * psi
329                + g_inner.cur_era_stable_height;
330        // corner case, should be extremely rare
331        if end_checking_height > g_inner.best_epoch_number() {
332            end_checking_height -= psi;
333        }
334        let n = (end_checking_height - g_inner.cur_era_stable_height) / psi;
335        let total_weight = g_inner
336            .weight_tree
337            .get(g_inner.cur_era_genesis_block_arena_index);
338        let me_index =
339            g_inner.height_to_pivot_index(g_inner.arena[me].data.epoch_number);
340        let x_3 =
341            total_weight - g_inner.pivot_chain_metadata[me_index].past_weight;
342
343        let mut adaptive_risk = 0f64;
344        let d = i128::try_from(g_inner.current_difficulty.low_u128()).unwrap();
345        for i in 0..n {
346            let a_pivot_index = g_inner.height_to_pivot_index(
347                g_inner.cur_era_stable_height + i * psi as u64,
348            );
349            let b_pivot_index = g_inner.height_to_pivot_index(
350                g_inner.cur_era_stable_height + (i + 1) * psi as u64,
351            );
352            let b = g_inner.pivot_chain[b_pivot_index];
353            let y = g_inner.weight_tree.get(b);
354            let mut x_1 = 0;
355            for v in a_pivot_index..b_pivot_index {
356                let pivot = g_inner.pivot_chain[v];
357                let next_pivot = g_inner.pivot_chain[v + 1];
358                for child in &g_inner.arena[pivot].children {
359                    if *child != next_pivot {
360                        let child_subtree_weight =
361                            g_inner.weight_tree.get(*child);
362                        x_1 = max(x_1, child_subtree_weight);
363                    }
364                }
365            }
366            let n_j = (y
367                - x_1
368                - x_3
369                - self.inner.read().total_weight_in_past_2d.delta)
370                / d;
371            let m_j = (total_weight
372                - g_inner.pivot_chain_metadata[a_pivot_index].past_weight)
373                / d;
374
375            let i_risk =
376                10f64.powf((m_j as f64 / 3.0 - n_j as f64) / 700.0 + 5.3);
377            adaptive_risk += i_risk;
378        }
379
380        adaptive_risk > CONFIRMATION_METER_MAXIMUM_ADAPTIVE_RISK
381    }
382}
383
384#[cfg(test)]
385mod tests {
386    use super::ConfirmationMeter;
387
388    #[test]
389    fn confirmation_risk_uses_offset_in_lowest_threshold() {
390        assert_eq!(
391            ConfirmationMeter::confirmation_risk_from_m_n(500, 190),
392            0.000001,
393        );
394        assert_eq!(
395            ConfirmationMeter::confirmation_risk_from_m_n(500, 198),
396            0.00000001,
397        );
398    }
399}