diff --git a/batcher/aligned/test_files/mina/protocol_state.proof b/batcher/aligned/test_files/mina/protocol_state.proof deleted file mode 100644 index 12d53de93..000000000 Binary files a/batcher/aligned/test_files/mina/protocol_state.proof and /dev/null differ diff --git a/batcher/aligned/test_files/mina/protocol_state.pub b/batcher/aligned/test_files/mina/protocol_state.pub deleted file mode 100644 index b46087ca5..000000000 Binary files a/batcher/aligned/test_files/mina/protocol_state.pub and /dev/null differ diff --git a/batcher/aligned/test_files/mina/protocol_state_bad_consensus.pub b/batcher/aligned/test_files/mina/protocol_state_bad_consensus.pub deleted file mode 100644 index b264a40bb..000000000 Binary files a/batcher/aligned/test_files/mina/protocol_state_bad_consensus.pub and /dev/null differ diff --git a/batcher/aligned/test_files/mina/protocol_state_bad_hash.pub b/batcher/aligned/test_files/mina/protocol_state_bad_hash.pub deleted file mode 100644 index 068de7f96..000000000 Binary files a/batcher/aligned/test_files/mina/protocol_state_bad_hash.pub and /dev/null differ diff --git a/operator/mina/lib/Cargo.lock b/operator/mina/lib/Cargo.lock index 1d34207fa..cfe7219e6 100644 --- a/operator/mina/lib/Cargo.lock +++ b/operator/mina/lib/Cargo.lock @@ -3586,7 +3586,7 @@ dependencies = [ [[package]] name = "mina_bridge_core" version = "0.1.0" -source = "git+https://github.com/lambdaclass/mina_bridge?branch=relative_finalization#fd359911b8da3039972276c7872e457d45e26646" +source = "git+https://github.com/lambdaclass/mina_bridge?branch=new_account_proof#fea0e6a52fa950bdc60f2afad71ad21c03296a24" dependencies = [ "aligned-sdk", "alloy", @@ -3617,7 +3617,7 @@ dependencies = [ "rpassword", "serde", "serde_json", - "serde_with 1.14.0", + "serde_with 3.9.0", "sha3", "tokio", ] diff --git a/operator/mina/lib/Cargo.toml b/operator/mina/lib/Cargo.toml index fc916dc53..3e0f70d29 100644 --- a/operator/mina/lib/Cargo.toml +++ b/operator/mina/lib/Cargo.toml @@ -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] diff --git a/operator/mina/lib/src/consensus_state.rs b/operator/mina/lib/src/consensus_state.rs index b6d49e141..67f296e7a 100644 --- a/operator/mina/lib/src/consensus_state.rs +++ b/operator/mina/lib/src/consensus_state.rs @@ -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 { + 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 { + // 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 + ); + } +} diff --git a/operator/mina/lib/src/lib.rs b/operator/mina/lib/src/lib.rs index 957bbc278..ad5dae273 100644 --- a/operator/mina/lib/src/lib.rs +++ b/operator/mina/lib/src/lib.rs @@ -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; @@ -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> = deserialize_blockchain_vk().unwrap(); @@ -62,10 +64,16 @@ pub extern "C" fn verify_mina_state_ffi( let srs = get_srs::(); 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; } @@ -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()); @@ -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); - } } diff --git a/operator/mina/mina_test.go b/operator/mina/mina_test.go index 89317a34a..4fba72c24 100644 --- a/operator/mina/mina_test.go +++ b/operator/mina/mina_test.go @@ -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") } @@ -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") } diff --git a/operator/mina_account/mina_account_test.go b/operator/mina_account/mina_account_test.go index cec1d42a3..2390df8d8 100644 --- a/operator/mina_account/mina_account_test.go +++ b/operator/mina_account/mina_account_test.go @@ -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") } @@ -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") } diff --git a/scripts/test_files/mina/mina_state.proof b/scripts/test_files/mina/mina_state.proof new file mode 100644 index 000000000..c13ba0c52 Binary files /dev/null and b/scripts/test_files/mina/mina_state.proof differ diff --git a/scripts/test_files/mina/mina_state.pub b/scripts/test_files/mina/mina_state.pub new file mode 100644 index 000000000..51ac31aa0 Binary files /dev/null and b/scripts/test_files/mina/mina_state.pub differ diff --git a/scripts/test_files/mina/mina_state_bad_hash.pub b/scripts/test_files/mina/mina_state_bad_hash.pub new file mode 100644 index 000000000..2c8692a2d Binary files /dev/null and b/scripts/test_files/mina/mina_state_bad_hash.pub differ diff --git a/batcher/aligned/test_files/mina/account_B62qrQKS9ghd91shs73TCmBJRW9GzvTJK443DPx2YbqcyoLc56g1ny9.proof b/scripts/test_files/mina_account/account_B62qrQKS9ghd91shs73TCmBJRW9GzvTJK443DPx2YbqcyoLc56g1ny9.proof similarity index 100% rename from batcher/aligned/test_files/mina/account_B62qrQKS9ghd91shs73TCmBJRW9GzvTJK443DPx2YbqcyoLc56g1ny9.proof rename to scripts/test_files/mina_account/account_B62qrQKS9ghd91shs73TCmBJRW9GzvTJK443DPx2YbqcyoLc56g1ny9.proof diff --git a/batcher/aligned/test_files/mina/account_B62qrQKS9ghd91shs73TCmBJRW9GzvTJK443DPx2YbqcyoLc56g1ny9.pub b/scripts/test_files/mina_account/account_B62qrQKS9ghd91shs73TCmBJRW9GzvTJK443DPx2YbqcyoLc56g1ny9.pub similarity index 100% rename from batcher/aligned/test_files/mina/account_B62qrQKS9ghd91shs73TCmBJRW9GzvTJK443DPx2YbqcyoLc56g1ny9.pub rename to scripts/test_files/mina_account/account_B62qrQKS9ghd91shs73TCmBJRW9GzvTJK443DPx2YbqcyoLc56g1ny9.pub