openmina_core/
consensus.rs

1use mina_p2p_messages::v2::{
2    self, BlockTimeTimeStableV1,
3    ConsensusProofOfStakeDataConsensusStateValueStableV2 as MinaConsensusState, StateHash,
4};
5use redux::Timestamp;
6use serde::{Deserialize, Serialize};
7use time::{macros::format_description, OffsetDateTime};
8
9use crate::constants::constraint_constants;
10pub use crate::constants::{
11    checkpoint_window_size_in_slots, slots_per_window, CHECKPOINTS_PER_YEAR,
12};
13
14// TODO get constants from elsewhere
15const GRACE_PERIOD_END: u32 = 1440;
16const SUB_WINDOWS_PER_WINDOW: u32 = 11;
17const SLOTS_PER_SUB_WINDOW: u32 = 7;
18
19#[derive(Serialize, Deserialize, Debug, Clone)]
20pub enum ConsensusShortRangeForkDecisionReason {
21    ChainLength,
22    Vrf,
23    StateHash,
24}
25
26#[derive(Serialize, Deserialize, Debug, Clone)]
27pub enum ConsensusLongRangeForkDecisionReason {
28    SubWindowDensity,
29    ChainLength,
30    Vrf,
31    StateHash,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ConsensusTime {
36    pub start_time: Timestamp,
37    pub end_time: Timestamp,
38    pub epoch: u32,
39    pub global_slot: u32,
40    pub slot: u32,
41}
42
43// TODO(binier): do we need to verify constants? Probably they are verified
44// using block proof verification, but check just to be sure.
45pub fn is_short_range_fork(a: &MinaConsensusState, b: &MinaConsensusState) -> bool {
46    let check = |s1: &MinaConsensusState, s2: &MinaConsensusState| {
47        let slots_per_epoch = s2.curr_global_slot_since_hard_fork.slots_per_epoch.as_u32();
48        let s2_epoch_slot = s2.global_slot() % slots_per_epoch;
49        if s1.epoch_count.as_u32() == s2.epoch_count.as_u32() + 1
50            && s2_epoch_slot >= slots_per_epoch * 2 / 3
51        {
52            crate::log::trace!(crate::log::system_time(); kind = "is_short_range_fork", msg = format!("s2 is 1 epoch behind and not in seed update range: {} vs {}", s1.staking_epoch_data.lock_checkpoint, s2.next_epoch_data.lock_checkpoint));
53            // S1 is one epoch ahead of S2 and S2 is not in the seed update range
54            s1.staking_epoch_data.lock_checkpoint == s2.next_epoch_data.lock_checkpoint
55        } else {
56            crate::log::trace!(crate::log::system_time(); kind = "is_short_range_fork", msg = format!("chains are from different epochs"));
57            false
58        }
59    };
60
61    crate::log::trace!(crate::log::system_time(); kind = "is_short_range_fork", msg = format!("epoch count: {} vs {}", a.epoch_count.as_u32(), b.epoch_count.as_u32()));
62    if a.epoch_count == b.epoch_count {
63        let a_prev_lock_checkpoint = &a.staking_epoch_data.lock_checkpoint;
64        let b_prev_lock_checkpoint = &b.staking_epoch_data.lock_checkpoint;
65        // Simple case: blocks have same previous epoch, so compare previous epochs' lock_checkpoints
66        crate::log::trace!(crate::log::system_time(); kind = "is_short_range_fork", msg = format!("checkpoints: {} vs {}", a_prev_lock_checkpoint, b_prev_lock_checkpoint));
67        a_prev_lock_checkpoint == b_prev_lock_checkpoint
68    } else {
69        // Check for previous epoch case using both orientations
70        check(a, b) || check(b, a)
71    }
72}
73
74/// Relative minimum window density.
75///
76/// See [specification](https://github.com/MinaProtocol/mina/tree/develop/docs/specs/consensus#5412-relative-minimum-window-density)
77pub fn relative_min_window_density(b1: &MinaConsensusState, b2: &MinaConsensusState) -> u32 {
78    use std::cmp::{max, min};
79
80    let max_slot = max(global_slot(b1), global_slot(b2));
81
82    if max_slot < GRACE_PERIOD_END {
83        return b1.min_window_density.as_u32();
84    }
85
86    let projected_window = {
87        // Compute shift count
88        let shift_count = max_slot
89            .saturating_sub(global_slot(b1) + 1)
90            .min(SUB_WINDOWS_PER_WINDOW);
91
92        // Initialize projected window
93        let mut projected_window = b1
94            .sub_window_densities
95            .iter()
96            .map(|d| d.as_u32())
97            .collect::<Vec<_>>();
98
99        // Ring-shift
100        let mut i = relative_sub_window_from_global_slot(global_slot(b1));
101        for _ in 0..=shift_count {
102            i = (i + 1) % SUB_WINDOWS_PER_WINDOW;
103            projected_window[i as usize] = 0;
104        }
105
106        projected_window
107    };
108
109    let projected_window_density = density(projected_window);
110
111    min(b1.min_window_density.as_u32(), projected_window_density)
112}
113
114fn density(projected_window: Vec<u32>) -> u32 {
115    projected_window.iter().sum()
116}
117
118fn relative_sub_window_from_global_slot(global_slot: u32) -> u32 {
119    (global_slot / SLOTS_PER_SUB_WINDOW) % SUB_WINDOWS_PER_WINDOW
120}
121
122fn global_slot(b: &MinaConsensusState) -> u32 {
123    b.curr_global_slot_since_hard_fork.slot_number.as_u32()
124}
125
126pub fn short_range_fork_take(
127    tip_cs: &MinaConsensusState,
128    candidate_cs: &MinaConsensusState,
129    tip_hash: &StateHash,
130    candidate_hash: &StateHash,
131) -> (bool, ConsensusShortRangeForkDecisionReason) {
132    use std::cmp::Ordering::*;
133    use ConsensusShortRangeForkDecisionReason::*;
134
135    let tip_height = &tip_cs.blockchain_length;
136    let candidate_height = &candidate_cs.blockchain_length;
137    match candidate_height.cmp(tip_height) {
138        Greater => return (true, ChainLength),
139        Less => return (false, ChainLength),
140        Equal => {}
141    }
142
143    let tip_vrf = tip_cs.last_vrf_output.blake2b();
144    let candidate_vrf = candidate_cs.last_vrf_output.blake2b();
145    match candidate_vrf.cmp(&tip_vrf) {
146        Greater => return (true, Vrf),
147        Less => return (false, Vrf),
148        Equal => {}
149    }
150
151    (candidate_hash > tip_hash, StateHash)
152}
153
154pub fn long_range_fork_take(
155    tip_cs: &MinaConsensusState,
156    candidate_cs: &MinaConsensusState,
157    tip_hash: &StateHash,
158    candidate_hash: &StateHash,
159) -> (bool, ConsensusLongRangeForkDecisionReason) {
160    use std::cmp::Ordering::*;
161    use ConsensusLongRangeForkDecisionReason::*;
162
163    let tip_density = relative_min_window_density(tip_cs, candidate_cs);
164    let candidate_density = relative_min_window_density(candidate_cs, tip_cs);
165    match candidate_density.cmp(&tip_density) {
166        Greater => return (true, SubWindowDensity),
167        Less => return (false, SubWindowDensity),
168        Equal => {}
169    }
170
171    let tip_height = &tip_cs.blockchain_length;
172    let candidate_height = &candidate_cs.blockchain_length;
173    match candidate_height.cmp(tip_height) {
174        Greater => return (true, ChainLength),
175        Less => return (false, ChainLength),
176        Equal => {}
177    }
178
179    let tip_vrf = tip_cs.last_vrf_output.blake2b();
180    let candidate_vrf = candidate_cs.last_vrf_output.blake2b();
181    match candidate_vrf.cmp(&tip_vrf) {
182        Greater => return (true, Vrf),
183        Less => return (false, Vrf),
184        Equal => {}
185    }
186
187    (candidate_hash > tip_hash, StateHash)
188}
189
190pub fn consensus_take(
191    tip_cs: &MinaConsensusState,
192    candidate_cs: &MinaConsensusState,
193    tip_hash: &StateHash,
194    candidate_hash: &StateHash,
195) -> bool {
196    if is_short_range_fork(tip_cs, candidate_cs) {
197        short_range_fork_take(tip_cs, candidate_cs, tip_hash, candidate_hash).0
198    } else {
199        long_range_fork_take(tip_cs, candidate_cs, tip_hash, candidate_hash).0
200    }
201}
202
203pub fn in_seed_update_range(
204    slot: u32,
205    constants: &v2::MinaBaseProtocolConstantsCheckedValueStableV1,
206) -> bool {
207    let third_epoch = constants.slots_per_epoch.as_u32() / 3;
208    assert_eq!(constants.slots_per_epoch.as_u32(), third_epoch * 3);
209    slot < third_epoch * 2
210}
211
212pub fn in_same_checkpoint_window(
213    slot1: &v2::ConsensusGlobalSlotStableV1,
214    slot2: &v2::ConsensusGlobalSlotStableV1,
215) -> bool {
216    checkpoint_window(slot1) == checkpoint_window(slot2)
217}
218
219pub fn checkpoint_window(slot: &v2::ConsensusGlobalSlotStableV1) -> u32 {
220    slot.slot_number.as_u32() / checkpoint_window_size_in_slots()
221}
222
223pub fn global_sub_window(
224    slot: &v2::ConsensusGlobalSlotStableV1,
225    constants: &v2::MinaBaseProtocolConstantsCheckedValueStableV1,
226) -> u32 {
227    slot.slot_number.as_u32() / constants.slots_per_sub_window.as_u32()
228}
229
230pub fn relative_sub_window(global_sub_window: u32) -> u32 {
231    global_sub_window % constraint_constants().sub_windows_per_window as u32
232}
233
234// TODO: Move ledger/src/scan_state/currency.rs types to core and replace
235// primmitive types here with thoise numeric types.
236#[derive(Clone, Debug, Serialize, Deserialize)]
237pub struct ConsensusConstants {
238    pub k: u32,
239    pub delta: u32,
240    pub block_window_duration_ms: u64,
241    pub slots_per_sub_window: u32,
242    pub slots_per_window: u32,
243    pub sub_windows_per_window: u32,
244    pub slots_per_epoch: u32,
245    pub grace_period_slots: u32,
246    pub grace_period_end: u32,
247    pub slot_duration_ms: u64,
248    pub epoch_duration: u64,
249    pub checkpoint_window_slots_per_year: u32,
250    pub checkpoint_window_size_in_slots: u32,
251    pub delta_duration: u64,
252    pub genesis_state_timestamp: BlockTimeTimeStableV1,
253}
254
255impl ConsensusConstants {
256    // We mimick the code layout of the OCaml node's here. `create_primed` could easily
257    // be inlined in `create`, but OCaml code keeps them separate ans so do we for now.
258    fn create_primed(
259        constraint_constants: &crate::constants::ConstraintConstants,
260        protocol_constants: &v2::MinaBaseProtocolConstantsCheckedValueStableV1,
261    ) -> Self {
262        let delta = protocol_constants.delta.as_u32();
263        let slots_per_epoch = protocol_constants.slots_per_epoch.as_u32();
264        let slots_per_window = protocol_constants.slots_per_sub_window.as_u32()
265            * constraint_constants.sub_windows_per_window as u32;
266        let grace_period_end = protocol_constants.grace_period_slots.as_u32() + slots_per_window;
267        let epoch_duration =
268            (slots_per_epoch as u64) * constraint_constants.block_window_duration_ms;
269        let delta_duration = constraint_constants.block_window_duration_ms * (delta + 1) as u64;
270        Self {
271            k: protocol_constants.k.as_u32(),
272            delta,
273            block_window_duration_ms: constraint_constants.block_window_duration_ms,
274            slots_per_sub_window: protocol_constants.slots_per_sub_window.as_u32(),
275            slots_per_window,
276            sub_windows_per_window: constraint_constants.sub_windows_per_window as u32,
277            slots_per_epoch,
278            grace_period_slots: protocol_constants.grace_period_slots.as_u32(),
279            grace_period_end,
280            slot_duration_ms: constraint_constants.block_window_duration_ms,
281            epoch_duration,
282            checkpoint_window_slots_per_year: 0,
283            checkpoint_window_size_in_slots: 0,
284            delta_duration,
285            genesis_state_timestamp: protocol_constants.genesis_state_timestamp,
286        }
287    }
288
289    pub fn assert_invariants(&self) {
290        let grace_period_effective_end = self.grace_period_end - self.slots_per_window;
291        assert!(grace_period_effective_end < (self.slots_per_epoch / 3));
292        // Because of how these values are computed (see below), this
293        // fails if and only if block_window_duration is a multiple of
294        // 27 or 512, or any of these multiplied by a power of 3 or 2
295        // respectively.
296        // 365 * 24 * 60 * 60 * 1000 = 2^10 * 3^3 * 5^6 * 73
297        // Therefore, if divided by 2^9 or 3^3, the whole value will not be
298        // divisible by 12 (2^2 * 3) anymore.
299        assert_eq!(
300            self.checkpoint_window_slots_per_year as u64,
301            self.checkpoint_window_size_in_slots as u64 * CHECKPOINTS_PER_YEAR
302        )
303    }
304
305    pub fn create(
306        constraint_constants: &crate::constants::ConstraintConstants,
307        protocol_constants: &v2::MinaBaseProtocolConstantsCheckedValueStableV1,
308    ) -> Self {
309        let mut constants = Self::create_primed(constraint_constants, protocol_constants);
310        const MILLISECS_PER_YEAR: u64 = 365 * 24 * 60 * 60 * 1000;
311        let slots_per_year = MILLISECS_PER_YEAR / constants.block_window_duration_ms;
312        constants.checkpoint_window_slots_per_year = slots_per_year as u32;
313        constants.checkpoint_window_size_in_slots = (slots_per_year / CHECKPOINTS_PER_YEAR) as u32;
314        constants.assert_invariants();
315        constants
316    }
317
318    pub fn human_readable_genesis_timestamp(&self) -> Result<String, String> {
319        let format = format_description!("[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:6][offset_hour sign:mandatory]:[offset_minute]");
320        OffsetDateTime::from_unix_timestamp((self.genesis_state_timestamp.as_u64() / 1000) as i64)
321            .map_err(|e| e.to_string())
322            .and_then(|dt| dt.format(&format).map_err(|e| e.to_string()))
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use super::{long_range_fork_take, short_range_fork_take};
329    use mina_p2p_messages::v2::{MinaStateProtocolStateValueStableV2, StateHash};
330    macro_rules! fork_file {
331        ($prefix:expr, $tip:expr, $cnd:expr, $suffix:expr) => {
332            concat!(
333                "../../tests/files/forks/",
334                $prefix,
335                "-",
336                $tip,
337                "-",
338                $cnd,
339                "-",
340                $suffix,
341                ".json"
342            )
343        };
344    }
345    macro_rules! fork_test {
346        ($prefix:expr, $tip:expr, $cnd:expr, $func:ident, $decision:expr) => {
347            let tip_str = include_str!(fork_file!($prefix, $tip, $cnd, "tip"));
348            let cnd_str = include_str!(fork_file!($prefix, $tip, $cnd, "cnd"));
349            let tip_hash = $tip.parse::<StateHash>().unwrap();
350            let cnd_hash = $cnd.parse::<StateHash>().unwrap();
351            let tip = serde_json::from_str::<MinaStateProtocolStateValueStableV2>(tip_str).unwrap();
352            let cnd = serde_json::from_str::<MinaStateProtocolStateValueStableV2>(cnd_str).unwrap();
353
354            let (take, _) = $func(
355                &tip.body.consensus_state,
356                &cnd.body.consensus_state,
357                &tip_hash,
358                &cnd_hash,
359            );
360            assert_eq!(take, $decision);
361        };
362
363        (long take $prefix:expr, $tip:expr, $cnd:expr) => {
364            fork_test!(
365                concat!("long-take-", $prefix),
366                $tip,
367                $cnd,
368                long_range_fork_take,
369                true
370            );
371        };
372
373        (long keep $prefix:expr, $tip:expr, $cnd:expr) => {
374            fork_test!(
375                concat!("long-keep-", $prefix),
376                $tip,
377                $cnd,
378                long_range_fork_take,
379                false
380            );
381        };
382
383        (short take $prefix:expr, $tip:expr, $cnd:expr) => {
384            fork_test!(
385                concat!("short-take-", $prefix),
386                $tip,
387                $cnd,
388                short_range_fork_take,
389                true
390            );
391        };
392
393        (short keep $prefix:expr, $tip:expr, $cnd:expr) => {
394            fork_test!(
395                concat!("short-keep-", $prefix),
396                $tip,
397                $cnd,
398                short_range_fork_take,
399                false
400            );
401        };
402    }
403
404    #[test]
405    fn long_range_fork() {
406        fork_test!(
407            long take
408                "density-92-97",
409            "3NLESd9gzU52bDWSXL5uUAYbCojHXSVdeBX4sCMF3V8Ns9D1Sriy",
410            "3NLQfKJ4kBagLgmiwyiVw9zbi53tiNy8TNu2ua1jmCyEecgbBJoN"
411        );
412        fork_test!(
413            long keep
414                "density-161-166",
415            "3NKY1kxHMRfjBbjfAA5fsasUCWFF9B7YqYFfNH4JFku6ZCUUXyLG",
416            "3NLFoBQ6y3nku79LQqPgKBmuo5Ngnpr7rfZygzdRrcPtz2gewRFC"
417        );
418    }
419
420    #[test]
421    fn short_range_fork() {
422        fork_test!(
423            short take
424                "length-60-61",
425            "3NLQEb5mXqXCL34rueHrMkUVyWSQ7aYjvi6K98ZdpEnTozef69uR",
426            "3NKuw8mvieV9RLpdRmHb4kxg7NWR83TfwzNkVmJCeHUmVWFdUQCp"
427        );
428        fork_test!(
429            short take
430                "vrf-99-99",
431                "3NL4kAA33FRs9K66GvVNupNT94L4shALtYLHJRfmxhdZV8iPg2pi",
432                "3NKC9F6mgtvRiHgYxiPBt1P5QDYaPVpD3YWyJhjmJZkNnT7RYitm"
433        );
434        fork_test!(
435            short keep
436                "vrf-117-117",
437                "3NLWvDBFYJ2NXZ1EKMZXHB52zcbVtosHPArn4cGj8pDKkYsTHNnC",
438                "3NKLEnUBTAhC95XEdJpLvJPqAUuvkC176tFKyLDcXUcofXXgQUvY"
439        );
440    }
441}