|
1 | 1 | use blake2::{Blake2b512, Digest}; |
2 | 2 | use kimchi::o1_utils::FieldHelpers; |
3 | | -use mina_p2p_messages::{hash::MinaHash, v2::MinaStateProtocolStateValueStableV2}; |
| 3 | +use mina_p2p_messages::{ |
| 4 | + hash::MinaHash, |
| 5 | + v2::{ |
| 6 | + ConsensusProofOfStakeDataConsensusStateValueStableV2 as MinaConsensusState, |
| 7 | + MinaStateProtocolStateValueStableV2 as MinaProtocolState, |
| 8 | + }, |
| 9 | +}; |
| 10 | +///! Consensus chain selection algorithms. The [`official specification`] was taken as a reference. |
| 11 | +/// |
| 12 | +///! [`official specification`]: https://github.com/MinaProtocol/mina/blob/develop/docs/specs/consensus/README.md |
| 13 | +use std::cmp::{max, min, Ordering}; |
| 14 | + |
| 15 | +const GRACE_PERIOD_END: u32 = 1440; |
| 16 | +const SUB_WINDOWS_PER_WINDOW: u32 = 11; |
| 17 | +const SLOTS_PER_SUB_WINDOW: u32 = 7; |
4 | 18 |
|
5 | 19 | #[derive(PartialEq)] |
6 | | -pub enum LongerChainResult { |
| 20 | +pub enum ChainResult { |
7 | 21 | Bridge, |
8 | 22 | Candidate, |
9 | 23 | } |
10 | 24 |
|
11 | | -pub fn select_longer_chain( |
12 | | - candidate: &MinaStateProtocolStateValueStableV2, |
13 | | - tip: &MinaStateProtocolStateValueStableV2, |
14 | | -) -> LongerChainResult { |
| 25 | +pub fn select_secure_chain( |
| 26 | + candidate: &MinaProtocolState, |
| 27 | + tip: &MinaProtocolState, |
| 28 | +) -> Result<ChainResult, String> { |
| 29 | + if is_short_range(candidate, tip)? { |
| 30 | + Ok(select_longer_chain(candidate, tip)) |
| 31 | + } else { |
| 32 | + let tip_density = relative_min_window_density(candidate, tip); |
| 33 | + let candidate_density = relative_min_window_density(candidate, tip); |
| 34 | + Ok(match candidate_density.cmp(&tip_density) { |
| 35 | + Ordering::Less => ChainResult::Bridge, |
| 36 | + Ordering::Equal => select_longer_chain(candidate, tip), |
| 37 | + Ordering::Greater => ChainResult::Candidate, |
| 38 | + }) |
| 39 | + } |
| 40 | +} |
| 41 | + |
| 42 | +fn select_longer_chain(candidate: &MinaProtocolState, tip: &MinaProtocolState) -> ChainResult { |
15 | 43 | let candidate_block_height = &candidate.body.consensus_state.blockchain_length.as_u32(); |
16 | 44 | let tip_block_height = &tip.body.consensus_state.blockchain_length.as_u32(); |
17 | 45 |
|
18 | 46 | if candidate_block_height > tip_block_height { |
19 | | - return LongerChainResult::Candidate; |
| 47 | + return ChainResult::Candidate; |
20 | 48 | } |
21 | 49 | // tiebreak logic |
22 | 50 | else if candidate_block_height == tip_block_height { |
23 | 51 | // compare last VRF digests lexicographically |
24 | 52 | if hash_last_vrf(candidate) > hash_last_vrf(tip) { |
25 | | - return LongerChainResult::Candidate; |
| 53 | + return ChainResult::Candidate; |
26 | 54 | } else if hash_last_vrf(candidate) == hash_last_vrf(tip) { |
27 | 55 | // compare consensus state hashes lexicographically |
28 | 56 | if hash_state(candidate) > hash_state(tip) { |
29 | | - return LongerChainResult::Candidate; |
| 57 | + return ChainResult::Candidate; |
30 | 58 | } |
31 | 59 | } |
32 | 60 | } |
33 | 61 |
|
34 | | - LongerChainResult::Bridge |
| 62 | + ChainResult::Bridge |
| 63 | +} |
| 64 | + |
| 65 | +/// Returns true if the fork is short-range, else the fork is long-range. |
| 66 | +fn is_short_range(candidate: &MinaProtocolState, tip: &MinaProtocolState) -> Result<bool, String> { |
| 67 | + // TODO(xqft): verify constants are correct |
| 68 | + if tip.body.constants != candidate.body.constants { |
| 69 | + return Err("Protocol constants on candidate and tip state are not equal".to_string()); |
| 70 | + } |
| 71 | + let slots_per_epoch = tip.body.constants.slots_per_epoch.as_u32(); |
| 72 | + |
| 73 | + let candidate = &candidate.body.consensus_state; |
| 74 | + let tip = &tip.body.consensus_state; |
| 75 | + |
| 76 | + let check = |s1: &MinaConsensusState, s2: &MinaConsensusState| { |
| 77 | + let s2_epoch_slot = s2.global_slot() % slots_per_epoch; |
| 78 | + if s1.epoch_count.as_u32() == s2.epoch_count.as_u32() + 1 |
| 79 | + && s2_epoch_slot >= slots_per_epoch * 2 / 3 |
| 80 | + { |
| 81 | + s1.staking_epoch_data.lock_checkpoint == s2.next_epoch_data.lock_checkpoint |
| 82 | + } else { |
| 83 | + false |
| 84 | + } |
| 85 | + }; |
| 86 | + |
| 87 | + Ok(if candidate.epoch_count == tip.epoch_count { |
| 88 | + candidate.staking_epoch_data.lock_checkpoint == tip.staking_epoch_data.lock_checkpoint |
| 89 | + } else { |
| 90 | + check(candidate, tip) || check(tip, candidate) |
| 91 | + }) |
| 92 | +} |
| 93 | + |
| 94 | +fn relative_min_window_density(candidate: &MinaProtocolState, tip: &MinaProtocolState) -> u32 { |
| 95 | + let candidate = &candidate.body.consensus_state; |
| 96 | + let tip = &tip.body.consensus_state; |
| 97 | + |
| 98 | + let max_slot = max(candidate.global_slot(), tip.global_slot()); |
| 99 | + |
| 100 | + if max_slot < GRACE_PERIOD_END { |
| 101 | + return candidate.min_window_density.as_u32(); |
| 102 | + } |
| 103 | + |
| 104 | + let projected_window = { |
| 105 | + let shift_count = (max_slot - candidate.global_slot() - 1).clamp(0, SUB_WINDOWS_PER_WINDOW); |
| 106 | + let mut projected_window: Vec<_> = candidate |
| 107 | + .sub_window_densities |
| 108 | + .iter() |
| 109 | + .map(|d| d.as_u32()) |
| 110 | + .collect(); |
| 111 | + |
| 112 | + let mut i = relative_sub_window(candidate); |
| 113 | + for _ in 0..shift_count { |
| 114 | + i = (i + 1) % SUB_WINDOWS_PER_WINDOW; |
| 115 | + projected_window[i as usize] = 0 |
| 116 | + } |
| 117 | + |
| 118 | + projected_window |
| 119 | + }; |
| 120 | + |
| 121 | + let projected_window_density = projected_window.iter().sum(); |
| 122 | + |
| 123 | + min( |
| 124 | + candidate.min_window_density.as_u32(), |
| 125 | + projected_window_density, |
| 126 | + ) |
| 127 | +} |
| 128 | + |
| 129 | +fn relative_sub_window(state: &MinaConsensusState) -> u32 { |
| 130 | + (state.global_slot() / SLOTS_PER_SUB_WINDOW) % SUB_WINDOWS_PER_WINDOW |
35 | 131 | } |
36 | 132 |
|
37 | | -fn hash_last_vrf(chain: &MinaStateProtocolStateValueStableV2) -> String { |
| 133 | +fn hash_last_vrf(chain: &MinaProtocolState) -> String { |
38 | 134 | let mut hasher = Blake2b512::new(); |
39 | 135 | hasher.update(chain.body.consensus_state.last_vrf_output.as_slice()); |
40 | 136 | let digest = hasher.finalize().to_vec(); |
41 | 137 |
|
42 | 138 | hex::encode(&digest) |
43 | 139 | } |
44 | 140 |
|
45 | | -fn hash_state(chain: &MinaStateProtocolStateValueStableV2) -> String { |
| 141 | +fn hash_state(chain: &MinaProtocolState) -> String { |
46 | 142 | MinaHash::hash(chain).to_hex() |
47 | 143 | } |
0 commit comments