|
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 | +use std::cmp::{max, min, Ordering}; |
4 | 11 |
|
5 | | -#[derive(PartialEq)] |
6 | | -pub enum LongerChainResult { |
| 12 | +const GRACE_PERIOD_END: u32 = 1440; |
| 13 | +const SUB_WINDOWS_PER_WINDOW: u32 = 11; |
| 14 | +const SLOTS_PER_SUB_WINDOW: u32 = 7; |
| 15 | + |
| 16 | +#[derive(Debug, PartialEq)] |
| 17 | +pub enum ChainResult { |
7 | 18 | Bridge, |
8 | 19 | Candidate, |
9 | 20 | } |
10 | 21 |
|
11 | | -pub fn select_longer_chain( |
12 | | - candidate: &MinaStateProtocolStateValueStableV2, |
13 | | - tip: &MinaStateProtocolStateValueStableV2, |
14 | | -) -> LongerChainResult { |
| 22 | +pub fn select_secure_chain( |
| 23 | + candidate: &MinaProtocolState, |
| 24 | + tip: &MinaProtocolState, |
| 25 | +) -> Result<ChainResult, String> { |
| 26 | + if is_short_range(candidate, tip)? { |
| 27 | + Ok(select_longer_chain(candidate, tip)) |
| 28 | + } else { |
| 29 | + let tip_density = relative_min_window_density(candidate, tip); |
| 30 | + let candidate_density = relative_min_window_density(candidate, tip); |
| 31 | + Ok(match candidate_density.cmp(&tip_density) { |
| 32 | + Ordering::Less => ChainResult::Bridge, |
| 33 | + Ordering::Equal => select_longer_chain(candidate, tip), |
| 34 | + Ordering::Greater => ChainResult::Candidate, |
| 35 | + }) |
| 36 | + } |
| 37 | +} |
| 38 | + |
| 39 | +fn select_longer_chain(candidate: &MinaProtocolState, tip: &MinaProtocolState) -> ChainResult { |
15 | 40 | let candidate_block_height = &candidate.body.consensus_state.blockchain_length.as_u32(); |
16 | 41 | let tip_block_height = &tip.body.consensus_state.blockchain_length.as_u32(); |
17 | 42 |
|
18 | 43 | if candidate_block_height > tip_block_height { |
19 | | - return LongerChainResult::Candidate; |
| 44 | + return ChainResult::Candidate; |
20 | 45 | } |
21 | 46 | // tiebreak logic |
22 | 47 | else if candidate_block_height == tip_block_height { |
23 | 48 | // compare last VRF digests lexicographically |
24 | 49 | if hash_last_vrf(candidate) > hash_last_vrf(tip) { |
25 | | - return LongerChainResult::Candidate; |
| 50 | + return ChainResult::Candidate; |
26 | 51 | } else if hash_last_vrf(candidate) == hash_last_vrf(tip) { |
27 | 52 | // compare consensus state hashes lexicographically |
28 | 53 | if hash_state(candidate) > hash_state(tip) { |
29 | | - return LongerChainResult::Candidate; |
| 54 | + return ChainResult::Candidate; |
30 | 55 | } |
31 | 56 | } |
32 | 57 | } |
33 | 58 |
|
34 | | - LongerChainResult::Bridge |
| 59 | + ChainResult::Bridge |
| 60 | +} |
| 61 | + |
| 62 | +/// Returns true if the fork is short-range, else the fork is long-range. |
| 63 | +fn is_short_range(candidate: &MinaProtocolState, tip: &MinaProtocolState) -> Result<bool, String> { |
| 64 | + // TODO(xqft): verify constants are correct |
| 65 | + if tip.body.constants != candidate.body.constants { |
| 66 | + return Err("Protocol constants on candidate and tip state are not equal".to_string()); |
| 67 | + } |
| 68 | + let slots_per_epoch = tip.body.constants.slots_per_epoch.as_u32(); |
| 69 | + |
| 70 | + let candidate = &candidate.body.consensus_state; |
| 71 | + let tip = &tip.body.consensus_state; |
| 72 | + |
| 73 | + let check = |s1: &MinaConsensusState, s2: &MinaConsensusState| { |
| 74 | + let s2_epoch_slot = s2.global_slot() % slots_per_epoch; |
| 75 | + if s1.epoch_count.as_u32() == s2.epoch_count.as_u32() + 1 |
| 76 | + && s2_epoch_slot >= slots_per_epoch * 2 / 3 |
| 77 | + { |
| 78 | + s1.staking_epoch_data.lock_checkpoint == s2.next_epoch_data.lock_checkpoint |
| 79 | + } else { |
| 80 | + false |
| 81 | + } |
| 82 | + }; |
| 83 | + |
| 84 | + Ok(if candidate.epoch_count == tip.epoch_count { |
| 85 | + candidate.staking_epoch_data.lock_checkpoint == tip.staking_epoch_data.lock_checkpoint |
| 86 | + } else { |
| 87 | + check(candidate, tip) || check(tip, candidate) |
| 88 | + }) |
| 89 | +} |
| 90 | + |
| 91 | +fn relative_min_window_density(candidate: &MinaProtocolState, tip: &MinaProtocolState) -> u32 { |
| 92 | + let candidate = &candidate.body.consensus_state; |
| 93 | + let tip = &tip.body.consensus_state; |
| 94 | + |
| 95 | + let max_slot = max(candidate.global_slot(), tip.global_slot()); |
| 96 | + |
| 97 | + if max_slot < GRACE_PERIOD_END { |
| 98 | + return candidate.min_window_density.as_u32(); |
| 99 | + } |
| 100 | + |
| 101 | + let projected_window = { |
| 102 | + let shift_count = (max_slot - candidate.global_slot() - 1).clamp(0, SUB_WINDOWS_PER_WINDOW); |
| 103 | + let mut projected_window: Vec<_> = candidate |
| 104 | + .sub_window_densities |
| 105 | + .iter() |
| 106 | + .map(|d| d.as_u32()) |
| 107 | + .collect(); |
| 108 | + |
| 109 | + let mut i = relative_sub_window(candidate); |
| 110 | + for _ in 0..shift_count { |
| 111 | + i = (i + 1) % SUB_WINDOWS_PER_WINDOW; |
| 112 | + projected_window[i as usize] = 0 |
| 113 | + } |
| 114 | + |
| 115 | + projected_window |
| 116 | + }; |
| 117 | + |
| 118 | + let projected_window_density = projected_window.iter().sum(); |
| 119 | + |
| 120 | + min( |
| 121 | + candidate.min_window_density.as_u32(), |
| 122 | + projected_window_density, |
| 123 | + ) |
35 | 124 | } |
36 | 125 |
|
37 | | -fn hash_last_vrf(chain: &MinaStateProtocolStateValueStableV2) -> String { |
| 126 | +fn relative_sub_window(state: &MinaConsensusState) -> u32 { |
| 127 | + (state.global_slot() / SLOTS_PER_SUB_WINDOW) % SUB_WINDOWS_PER_WINDOW |
| 128 | +} |
| 129 | + |
| 130 | +fn hash_last_vrf(chain: &MinaProtocolState) -> String { |
38 | 131 | let mut hasher = Blake2b512::new(); |
39 | 132 | hasher.update(chain.body.consensus_state.last_vrf_output.as_slice()); |
40 | 133 | let digest = hasher.finalize().to_vec(); |
41 | 134 |
|
42 | | - hex::encode(&digest) |
| 135 | + hex::encode(digest) |
43 | 136 | } |
44 | 137 |
|
45 | | -fn hash_state(chain: &MinaStateProtocolStateValueStableV2) -> String { |
| 138 | +fn hash_state(chain: &MinaProtocolState) -> String { |
46 | 139 | MinaHash::hash(chain).to_hex() |
47 | 140 | } |
| 141 | + |
| 142 | +#[cfg(test)] |
| 143 | +mod test { |
| 144 | + use mina_bridge_core::proof::state_proof::MinaStateProof; |
| 145 | + |
| 146 | + use super::*; |
| 147 | + |
| 148 | + const PROOF_BYTES: &[u8] = |
| 149 | + include_bytes!("../../../../scripts/test_files/mina/mina_state.proof"); |
| 150 | + |
| 151 | + #[test] |
| 152 | + fn new_mina_state_passes_consensus_checks() { |
| 153 | + let valid_proof: MinaStateProof = bincode::deserialize(PROOF_BYTES).unwrap(); |
| 154 | + let old_tip = valid_proof.bridge_tip_state; |
| 155 | + let new_tip = valid_proof.candidate_chain_states.last().unwrap(); |
| 156 | + |
| 157 | + assert_eq!( |
| 158 | + select_secure_chain(new_tip, &old_tip).unwrap(), |
| 159 | + ChainResult::Candidate |
| 160 | + ); |
| 161 | + } |
| 162 | + |
| 163 | + #[test] |
| 164 | + fn old_mina_state_fails_consensus_checks() { |
| 165 | + let valid_proof: MinaStateProof = bincode::deserialize(PROOF_BYTES).unwrap(); |
| 166 | + let old_tip = valid_proof.bridge_tip_state; |
| 167 | + let new_tip = valid_proof.candidate_chain_states.last().unwrap(); |
| 168 | + |
| 169 | + assert_eq!( |
| 170 | + select_secure_chain(&old_tip, new_tip).unwrap(), |
| 171 | + ChainResult::Bridge |
| 172 | + ); |
| 173 | + } |
| 174 | +} |
0 commit comments