Skip to content

Commit 82e32bb

Browse files
committed
Add long range fork rule
1 parent 0afb987 commit 82e32bb

2 files changed

Lines changed: 121 additions & 23 deletions

File tree

Lines changed: 108 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,143 @@
11
use blake2::{Blake2b512, Digest};
22
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;
418

519
#[derive(PartialEq)]
6-
pub enum LongerChainResult {
20+
pub enum ChainResult {
721
Bridge,
822
Candidate,
923
}
1024

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 {
1543
let candidate_block_height = &candidate.body.consensus_state.blockchain_length.as_u32();
1644
let tip_block_height = &tip.body.consensus_state.blockchain_length.as_u32();
1745

1846
if candidate_block_height > tip_block_height {
19-
return LongerChainResult::Candidate;
47+
return ChainResult::Candidate;
2048
}
2149
// tiebreak logic
2250
else if candidate_block_height == tip_block_height {
2351
// compare last VRF digests lexicographically
2452
if hash_last_vrf(candidate) > hash_last_vrf(tip) {
25-
return LongerChainResult::Candidate;
53+
return ChainResult::Candidate;
2654
} else if hash_last_vrf(candidate) == hash_last_vrf(tip) {
2755
// compare consensus state hashes lexicographically
2856
if hash_state(candidate) > hash_state(tip) {
29-
return LongerChainResult::Candidate;
57+
return ChainResult::Candidate;
3058
}
3159
}
3260
}
3361

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
35131
}
36132

37-
fn hash_last_vrf(chain: &MinaStateProtocolStateValueStableV2) -> String {
133+
fn hash_last_vrf(chain: &MinaProtocolState) -> String {
38134
let mut hasher = Blake2b512::new();
39135
hasher.update(chain.body.consensus_state.last_vrf_output.as_slice());
40136
let digest = hasher.finalize().to_vec();
41137

42138
hex::encode(&digest)
43139
}
44140

45-
fn hash_state(chain: &MinaStateProtocolStateValueStableV2) -> String {
141+
fn hash_state(chain: &MinaProtocolState) -> String {
46142
MinaHash::hash(chain).to_hex()
47143
}

operator/mina/lib/src/lib.rs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ mod consensus_state;
33
use mina_bridge_core::proof::state_proof::{MinaStateProof, MinaStatePubInputs};
44

55
use ark_ec::short_weierstrass_jacobian::GroupAffine;
6-
use consensus_state::{select_longer_chain, LongerChainResult};
6+
use consensus_state::{select_secure_chain, ChainResult};
77
use kimchi::mina_curves::pasta::{Fp, PallasParameters};
88
use kimchi::verifier_index::VerifierIndex;
99
use lazy_static::lazy_static;
@@ -62,10 +62,16 @@ pub extern "C" fn verify_mina_state_ffi(
6262
let srs = get_srs::<Fp>();
6363
let srs = srs.lock().unwrap();
6464

65-
// Consensus check: Short fork rule
66-
let longer_chain = select_longer_chain(&candidate_tip_state, &bridge_tip_state);
67-
if longer_chain == LongerChainResult::Bridge {
68-
eprintln!("Failed consensus checks for candidate tip state against bridge's tip");
65+
// Consensus checks
66+
let secure_chain = match select_secure_chain(&candidate_tip_state, &bridge_tip_state) {
67+
Ok(res) => res,
68+
Err(err) => {
69+
eprintln!("Failed consensus checks: {err}");
70+
return false;
71+
}
72+
};
73+
if secure_chain == ChainResult::Bridge {
74+
eprintln!("Failed consensus checks for candidate tip state (bridge's tip is more secure)");
6975
return false;
7076
}
7177

@@ -242,12 +248,8 @@ mod test {
242248
assert!(pub_input_size <= pub_input_buffer.len());
243249
pub_input_buffer[..pub_input_size].clone_from_slice(PROTOCOL_STATE_BAD_CONSENSUS_PUB_BYTES);
244250

245-
let result = verify_protocol_state_proof_ffi(
246-
&proof_buffer,
247-
proof_size,
248-
&pub_input_buffer,
249-
pub_input_size,
250-
);
251+
let result =
252+
verify_mina_state_ffi(&proof_buffer, proof_size, &pub_input_buffer, pub_input_size);
251253
assert!(!result);
252254
}
253255
}

0 commit comments

Comments
 (0)