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
14const 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
43pub 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.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 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(a, b) || check(b, a)
71 }
72}
73
74pub 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 let shift_count = max_slot
89 .saturating_sub(global_slot(b1) + 1)
90 .min(SUB_WINDOWS_PER_WINDOW);
91
92 let mut projected_window = b1
94 .sub_window_densities
95 .iter()
96 .map(|d| d.as_u32())
97 .collect::<Vec<_>>();
98
99 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#[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 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 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}