Skip to content

Commit

Permalink
Merge pull request #23 from lambdaclass/long_range_consensus
Browse files Browse the repository at this point in the history
Add long-range fork consensus rule
  • Loading branch information
gabrielbosio authored Sep 6, 2024
2 parents 281967f + 6554ca0 commit 79973eb
Show file tree
Hide file tree
Showing 15 changed files with 171 additions and 60 deletions.
Binary file removed batcher/aligned/test_files/mina/protocol_state.proof
Binary file not shown.
Binary file removed batcher/aligned/test_files/mina/protocol_state.pub
Binary file not shown.
Binary file not shown.
Binary file not shown.
4 changes: 2 additions & 2 deletions operator/mina/lib/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion operator/mina/lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ bs58 = "0.5.1"
lazy_static = "1.5.0"
blake2 = "0.10.6"
once_cell = "1.19.0"
mina_bridge_core = { git = "https://github.com/lambdaclass/mina_bridge", branch = "relative_finalization" }
mina_bridge_core = { git = "https://github.com/lambdaclass/mina_bridge", branch = "new_account_proof" }
bincode = "1.3.3"

[patch.crates-io]
Expand Down
155 changes: 141 additions & 14 deletions operator/mina/lib/src/consensus_state.rs
Original file line number Diff line number Diff line change
@@ -1,47 +1,174 @@
use blake2::{Blake2b512, Digest};
use kimchi::o1_utils::FieldHelpers;
use mina_p2p_messages::{hash::MinaHash, v2::MinaStateProtocolStateValueStableV2};
use mina_p2p_messages::{
hash::MinaHash,
v2::{
ConsensusProofOfStakeDataConsensusStateValueStableV2 as MinaConsensusState,
MinaStateProtocolStateValueStableV2 as MinaProtocolState,
},
};
use std::cmp::{max, min, Ordering};

#[derive(PartialEq)]
pub enum LongerChainResult {
const GRACE_PERIOD_END: u32 = 1440;
const SUB_WINDOWS_PER_WINDOW: u32 = 11;
const SLOTS_PER_SUB_WINDOW: u32 = 7;

#[derive(Debug, PartialEq)]
pub enum ChainResult {
Bridge,
Candidate,
}

pub fn select_longer_chain(
candidate: &MinaStateProtocolStateValueStableV2,
tip: &MinaStateProtocolStateValueStableV2,
) -> LongerChainResult {
pub fn select_secure_chain(
candidate: &MinaProtocolState,
tip: &MinaProtocolState,
) -> Result<ChainResult, String> {
if is_short_range(candidate, tip)? {
Ok(select_longer_chain(candidate, tip))
} else {
let tip_density = relative_min_window_density(candidate, tip);
let candidate_density = relative_min_window_density(candidate, tip);
Ok(match candidate_density.cmp(&tip_density) {
Ordering::Less => ChainResult::Bridge,
Ordering::Equal => select_longer_chain(candidate, tip),
Ordering::Greater => ChainResult::Candidate,
})
}
}

fn select_longer_chain(candidate: &MinaProtocolState, tip: &MinaProtocolState) -> ChainResult {
let candidate_block_height = &candidate.body.consensus_state.blockchain_length.as_u32();
let tip_block_height = &tip.body.consensus_state.blockchain_length.as_u32();

if candidate_block_height > tip_block_height {
return LongerChainResult::Candidate;
return ChainResult::Candidate;
}
// tiebreak logic
else if candidate_block_height == tip_block_height {
// compare last VRF digests lexicographically
if hash_last_vrf(candidate) > hash_last_vrf(tip) {
return LongerChainResult::Candidate;
return ChainResult::Candidate;
} else if hash_last_vrf(candidate) == hash_last_vrf(tip) {
// compare consensus state hashes lexicographically
if hash_state(candidate) > hash_state(tip) {
return LongerChainResult::Candidate;
return ChainResult::Candidate;
}
}
}

LongerChainResult::Bridge
ChainResult::Bridge
}

/// Returns true if the fork is short-range, else the fork is long-range.
fn is_short_range(candidate: &MinaProtocolState, tip: &MinaProtocolState) -> Result<bool, String> {
// TODO(xqft): verify constants are correct
if tip.body.constants != candidate.body.constants {
return Err("Protocol constants on candidate and tip state are not equal".to_string());
}
let slots_per_epoch = tip.body.constants.slots_per_epoch.as_u32();

let candidate = &candidate.body.consensus_state;
let tip = &tip.body.consensus_state;

let check = |s1: &MinaConsensusState, s2: &MinaConsensusState| {
let s2_epoch_slot = s2.global_slot() % slots_per_epoch;
if s1.epoch_count.as_u32() == s2.epoch_count.as_u32() + 1
&& s2_epoch_slot >= slots_per_epoch * 2 / 3
{
s1.staking_epoch_data.lock_checkpoint == s2.next_epoch_data.lock_checkpoint
} else {
false
}
};

Ok(if candidate.epoch_count == tip.epoch_count {
candidate.staking_epoch_data.lock_checkpoint == tip.staking_epoch_data.lock_checkpoint
} else {
check(candidate, tip) || check(tip, candidate)
})
}

fn relative_min_window_density(candidate: &MinaProtocolState, tip: &MinaProtocolState) -> u32 {
let candidate = &candidate.body.consensus_state;
let tip = &tip.body.consensus_state;

let max_slot = max(candidate.global_slot(), tip.global_slot());

if max_slot < GRACE_PERIOD_END {
return candidate.min_window_density.as_u32();
}

let projected_window = {
let shift_count = (max_slot - candidate.global_slot() - 1).clamp(0, SUB_WINDOWS_PER_WINDOW);
let mut projected_window: Vec<_> = candidate
.sub_window_densities
.iter()
.map(|d| d.as_u32())
.collect();

let mut i = relative_sub_window(candidate);
for _ in 0..shift_count {
i = (i + 1) % SUB_WINDOWS_PER_WINDOW;
projected_window[i as usize] = 0
}

projected_window
};

let projected_window_density = projected_window.iter().sum();

min(
candidate.min_window_density.as_u32(),
projected_window_density,
)
}

fn hash_last_vrf(chain: &MinaStateProtocolStateValueStableV2) -> String {
fn relative_sub_window(state: &MinaConsensusState) -> u32 {
(state.global_slot() / SLOTS_PER_SUB_WINDOW) % SUB_WINDOWS_PER_WINDOW
}

fn hash_last_vrf(chain: &MinaProtocolState) -> String {
let mut hasher = Blake2b512::new();
hasher.update(chain.body.consensus_state.last_vrf_output.as_slice());
let digest = hasher.finalize().to_vec();

hex::encode(&digest)
hex::encode(digest)
}

fn hash_state(chain: &MinaStateProtocolStateValueStableV2) -> String {
fn hash_state(chain: &MinaProtocolState) -> String {
MinaHash::hash(chain).to_hex()
}

#[cfg(test)]
mod test {
use mina_bridge_core::proof::state_proof::MinaStateProof;

use super::*;

const PROOF_BYTES: &[u8] =
include_bytes!("../../../../scripts/test_files/mina/mina_state.proof");

#[test]
fn new_mina_state_passes_consensus_checks() {
let valid_proof: MinaStateProof = bincode::deserialize(PROOF_BYTES).unwrap();
let old_tip = valid_proof.bridge_tip_state;
let new_tip = valid_proof.candidate_chain_states.last().unwrap();

assert_eq!(
select_secure_chain(new_tip, &old_tip).unwrap(),
ChainResult::Candidate
);
}

#[test]
fn old_mina_state_fails_consensus_checks() {
let valid_proof: MinaStateProof = bincode::deserialize(PROOF_BYTES).unwrap();
let old_tip = valid_proof.bridge_tip_state;
let new_tip = valid_proof.candidate_chain_states.last().unwrap();

assert_eq!(
select_secure_chain(&old_tip, new_tip).unwrap(),
ChainResult::Bridge
);
}
}
62 changes: 23 additions & 39 deletions operator/mina/lib/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
/// Consensus chain selection algorithms. The [`official specification`] was taken as a reference.
///
/// [`official specification`]: https://github.com/MinaProtocol/mina/blob/develop/docs/specs/consensus/README.md
mod consensus_state;
mod verifier_index;

use mina_bridge_core::proof::state_proof::{MinaStateProof, MinaStatePubInputs};

use ark_ec::short_weierstrass_jacobian::GroupAffine;
use consensus_state::{select_longer_chain, LongerChainResult};
use consensus_state::{select_secure_chain, ChainResult};
use kimchi::mina_curves::pasta::{Fp, PallasParameters};
use kimchi::verifier_index::VerifierIndex;
use lazy_static::lazy_static;
Expand All @@ -13,8 +17,6 @@ use mina_tree::proofs::verification::verify_block;
use mina_tree::verifier::get_srs;
use verifier_index::deserialize_blockchain_vk;

mod verifier_index;

lazy_static! {
static ref VERIFIER_INDEX: VerifierIndex<GroupAffine<PallasParameters>> =
deserialize_blockchain_vk().unwrap();
Expand Down Expand Up @@ -62,10 +64,16 @@ pub extern "C" fn verify_mina_state_ffi(
let srs = get_srs::<Fp>();
let srs = srs.lock().unwrap();

// Consensus check: Short fork rule
let longer_chain = select_longer_chain(&candidate_tip_state, &bridge_tip_state);
if longer_chain == LongerChainResult::Bridge {
eprintln!("Failed consensus checks for candidate tip state against bridge's tip");
// Consensus checks
let secure_chain = match select_secure_chain(&candidate_tip_state, &bridge_tip_state) {
Ok(res) => res,
Err(err) => {
eprintln!("Failed consensus checks for candidate tip: {err}");
return false;
}
};
if secure_chain == ChainResult::Bridge {
eprintln!("Failed consensus checks for candidate tip: bridge's tip is more secure");
return false;
}

Expand Down Expand Up @@ -187,17 +195,14 @@ mod test {
use super::*;

const PROOF_BYTES: &[u8] =
include_bytes!("../../../../batcher/aligned/test_files/mina/protocol_state.proof");
include_bytes!("../../../../scripts/test_files/mina/mina_state.proof");
const PUB_INPUT_BYTES: &[u8] =
include_bytes!("../../../../batcher/aligned/test_files/mina/protocol_state.pub");
const PROTOCOL_STATE_BAD_HASH_PUB_BYTES: &[u8] =
include_bytes!("../../../../batcher/aligned/test_files/mina/protocol_state_bad_hash.pub");
const PROTOCOL_STATE_BAD_CONSENSUS_PUB_BYTES: &[u8] = include_bytes!(
"../../../../batcher/aligned/test_files/mina/protocol_state_bad_consensus.pub"
);
include_bytes!("../../../../scripts/test_files/mina/mina_state.pub");
const BAD_HASH_PUB_INPUT_BYTES: &[u8] =
include_bytes!("../../../../scripts/test_files/mina/mina_state_bad_hash.pub");

#[test]
fn protocol_state_proof_verifies() {
fn valid_mina_state_proof_verifies() {
let mut proof_buffer = [0u8; super::MAX_PROOF_SIZE];
let proof_size = PROOF_BYTES.len();
assert!(proof_size <= proof_buffer.len());
Expand All @@ -214,40 +219,19 @@ mod test {
}

#[test]
fn proof_of_protocol_state_with_bad_hash_does_not_verify() {
fn mina_state_proof_with_bad_bridge_tip_hash_does_not_verify() {
let mut proof_buffer = [0u8; super::MAX_PROOF_SIZE];
let proof_size = PROOF_BYTES.len();
assert!(proof_size <= proof_buffer.len());
proof_buffer[..proof_size].clone_from_slice(PROOF_BYTES);

let mut pub_input_buffer = [0u8; super::MAX_PUB_INPUT_SIZE];
let pub_input_size = PROTOCOL_STATE_BAD_HASH_PUB_BYTES.len();
let pub_input_size = BAD_HASH_PUB_INPUT_BYTES.len();
assert!(pub_input_size <= pub_input_buffer.len());
pub_input_buffer[..pub_input_size].clone_from_slice(PROTOCOL_STATE_BAD_HASH_PUB_BYTES);
pub_input_buffer[..pub_input_size].clone_from_slice(BAD_HASH_PUB_INPUT_BYTES);

let result =
verify_mina_state_ffi(&proof_buffer, proof_size, &pub_input_buffer, pub_input_size);
assert!(!result);
}

#[test]
fn proof_of_protocol_state_with_bad_consensus_does_not_verify() {
let mut proof_buffer = [0u8; super::MAX_PROOF_SIZE];
let proof_size = PROOF_BYTES.len();
assert!(proof_size <= proof_buffer.len());
proof_buffer[..proof_size].clone_from_slice(PROOF_BYTES);

let mut pub_input_buffer = [0u8; super::MAX_PUB_INPUT_SIZE];
let pub_input_size = PROTOCOL_STATE_BAD_CONSENSUS_PUB_BYTES.len();
assert!(pub_input_size <= pub_input_buffer.len());
pub_input_buffer[..pub_input_size].clone_from_slice(PROTOCOL_STATE_BAD_CONSENSUS_PUB_BYTES);

let result = verify_protocol_state_proof_ffi(
&proof_buffer,
proof_size,
&pub_input_buffer,
pub_input_size,
);
assert!(!result);
}
}
4 changes: 2 additions & 2 deletions operator/mina/mina_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

func TestMinaStateProofVerifies(t *testing.T) {
fmt.Println(os.Getwd())
proofFile, err := os.Open("../../batcher/aligned/test_files/mina/protocol_state.proof")
proofFile, err := os.Open("../../scripts/test_files/mina/mina_state.proof")
if err != nil {
t.Errorf("could not open mina state proof file")
}
Expand All @@ -21,7 +21,7 @@ func TestMinaStateProofVerifies(t *testing.T) {
t.Errorf("could not read bytes from mina state proof file")
}

pubInputFile, err := os.Open("../../batcher/aligned/test_files/mina/protocol_state.pub")
pubInputFile, err := os.Open("../../scripts/test_files/mina/mina_state.pub")
if err != nil {
t.Errorf("could not open mina state hash file")
}
Expand Down
4 changes: 2 additions & 2 deletions operator/mina_account/mina_account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

func TestMinaStateProofVerifies(t *testing.T) {
fmt.Println(os.Getwd())
proofFile, err := os.Open("../../batcher/aligned/test_files/mina/account_B62qrQKS9ghd91shs73TCmBJRW9GzvTJK443DPx2YbqcyoLc56g1ny9.proof")
proofFile, err := os.Open("../../scripts/test_files/mina_account/account_B62qrQKS9ghd91shs73TCmBJRW9GzvTJK443DPx2YbqcyoLc56g1ny9.proof")
if err != nil {
t.Errorf("could not open mina account proof file")
}
Expand All @@ -21,7 +21,7 @@ func TestMinaStateProofVerifies(t *testing.T) {
t.Errorf("could not read bytes from mina account proof file")
}

pubInputFile, err := os.Open("../../batcher/aligned/test_files/mina/account_B62qrQKS9ghd91shs73TCmBJRW9GzvTJK443DPx2YbqcyoLc56g1ny9.pub")
pubInputFile, err := os.Open("../../scripts/test_files/mina_account/account_B62qrQKS9ghd91shs73TCmBJRW9GzvTJK443DPx2YbqcyoLc56g1ny9.pub")
if err != nil {
t.Errorf("could not open mina account pub inputs file")
}
Expand Down
Binary file added scripts/test_files/mina/mina_state.proof
Binary file not shown.
Binary file added scripts/test_files/mina/mina_state.pub
Binary file not shown.
Binary file added scripts/test_files/mina/mina_state_bad_hash.pub
Binary file not shown.

0 comments on commit 79973eb

Please sign in to comment.