From d6b35a7668b2b374206719ec19518c56da1e48f3 Mon Sep 17 00:00:00 2001 From: manza Date: Mon, 16 Dec 2024 08:48:00 -0700 Subject: [PATCH] Consideration min votes threshold update + Added unit test for vote (#39) --- server/Cargo.lock | 2 +- server/Cargo.toml | 2 +- server/src/archive.rs | 71 +++++++++++++++++ server/src/config.rs | 15 ++++ server/src/ocv.rs | 37 +++++++-- server/src/vote.rs | 174 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 293 insertions(+), 8 deletions(-) diff --git a/server/Cargo.lock b/server/Cargo.lock index 4f773381..8ffb7175 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -1653,7 +1653,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mina-ocv" -version = "0.12.0" +version = "0.12.3" dependencies = [ "anyhow", "aws-sdk-s3", diff --git a/server/Cargo.toml b/server/Cargo.toml index fa845e8e..95633b15 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mina-ocv" -version = "0.12.2" +version = "0.12.3" edition = "2021" [dependencies] diff --git a/server/src/archive.rs b/server/src/archive.rs index b64941b8..3385caa3 100644 --- a/server/src/archive.rs +++ b/server/src/archive.rs @@ -85,3 +85,74 @@ pub struct FetchTransactionResult { #[diesel(sql_type = BigInt)] pub nonce: i64, } + +pub trait ArchiveInterface { + fn fetch_chain_tip(&self) -> Result; + fn fetch_latest_slot(&self) -> Result; + fn fetch_transactions(&self, start_time: i64, end_time: i64) -> Result>; +} + +impl ArchiveInterface for Archive { + fn fetch_chain_tip(&self) -> Result { + self.fetch_chain_tip() + } + + fn fetch_latest_slot(&self) -> Result { + self.fetch_latest_slot() + } + + fn fetch_transactions(&self, start_time: i64, end_time: i64) -> Result> { + self.fetch_transactions(start_time, end_time) + } +} + +pub struct MockArchive; + +impl ArchiveInterface for MockArchive { + fn fetch_chain_tip(&self) -> Result { + Ok(100) // Return a mock value for the chain tip + } + + fn fetch_latest_slot(&self) -> Result { + Ok(200) // Return a mock value for the latest slot + } + + fn fetch_transactions(&self, start_time: i64, _end_time: i64) -> Result> { + Ok(vec![FetchTransactionResult { + account: "mock_account".to_string(), + hash: "mock_hash".to_string(), + memo: "mock_memo".to_string(), + height: 1, + status: BlockStatus::Pending, // Use a mock value + timestamp: start_time + 1000, + nonce: 42, + }]) // Return a mock list of transactions + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_fetch_chain_tip() { + let archive = MockArchive; + let chain_tip = archive.fetch_chain_tip().unwrap(); + assert_eq!(chain_tip, 100); + } + + #[test] + fn test_fetch_latest_slot() { + let archive = MockArchive; + let latest_slot = archive.fetch_latest_slot().unwrap(); + assert_eq!(latest_slot, 200); + } + + #[test] + fn test_fetch_transactions() { + let archive = MockArchive; + let transactions = archive.fetch_transactions(1733371364000, 1733803364000).unwrap(); + assert_eq!(transactions.len(), 1); + assert_eq!(transactions[0].account, "mock_account"); + } +} diff --git a/server/src/config.rs b/server/src/config.rs index 8d58b3bc..164b441c 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -13,6 +13,9 @@ pub struct OcvConfig { /// The mina network to connect to. #[clap(long, env)] pub network: Network, + /// The environment stage. + #[clap(long, env = "RELEASE_STAGE")] + pub release_stage: ReleaseStage, /// The URL from which the `proposals.json` should be fetched. #[clap(long, env = "PROPOSALS_URL")] pub maybe_proposals_url: Option, @@ -34,6 +37,7 @@ impl OcvConfig { caches: Caches::build(), archive: Archive::new(&self.archive_database_url), network: self.network, + release_stage: self.release_stage, ledger_storage_path: PathBuf::from_str(&self.ledger_storage_path)?, bucket_name: self.bucket_name.clone(), proposals: self.load_proposals().await?, @@ -68,3 +72,14 @@ pub enum Network { #[display("berkeley")] Berkeley, } + +#[derive(Clone, Copy, Parser, ValueEnum, Debug, Display, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum ReleaseStage { + #[display("development")] + Development, + #[display("staging")] + Staging, + #[display("production")] + Production, +} diff --git a/server/src/ocv.rs b/server/src/ocv.rs index 2e0f3ca7..15066116 100644 --- a/server/src/ocv.rs +++ b/server/src/ocv.rs @@ -4,13 +4,14 @@ use anyhow::{Result, anyhow}; use rust_decimal::Decimal; use serde::Serialize; -use crate::{Archive, Ledger, Network, Proposal, Vote, VoteWithWeight, Wrapper, util::Caches}; +use crate::{Archive, Ledger, Network, Proposal, ReleaseStage, Vote, VoteWithWeight, Wrapper, util::Caches}; #[derive(Clone)] pub struct Ocv { pub caches: Caches, pub archive: Archive, pub network: Network, + pub release_stage: ReleaseStage, pub ledger_storage_path: PathBuf, pub bucket_name: String, pub proposals: Vec, @@ -45,6 +46,34 @@ impl Ocv { Ok(ProposalResponse { proposal, votes }) } + /// Checks whether the positive community vote threshold has been met based + /// on the release stage. + /// + /// # Arguments + /// + /// * `total_positive_community_votes` - Total number of positive votes from the community. + /// * `total_negative_community_votes` - Total number of negative votes from the community. + /// + /// # Returns + /// + /// Returns `true` if the positive votes meet the threshold for the current release stage, + /// otherwise `false`. + /// + /// # Description + /// + /// - For the `Production` release stage, the minimum threshold for positive votes is 10. + /// - For other release stages, the threshold is 2. + pub fn has_met_vote_threshold( + &self, + total_positive_community_votes: usize, + _total_negative_community_votes: usize, + ) -> bool { + let min_positive_votes = if self.release_stage == ReleaseStage::Production { 10 } else { 2 }; + tracing::info!("min_positive_votes {}", min_positive_votes); + tracing::info!("release_stage {}", self.release_stage); + total_positive_community_votes >= min_positive_votes + } + pub async fn proposal_consideration( &self, id: usize, @@ -69,7 +98,6 @@ impl Ocv { tracing::info!("votes {}", votes.len()); votes }; - // weighted votes let mut positive_stake_weight = Decimal::from(0); let mut negative_stake_weight = Decimal::from(0); @@ -86,7 +114,7 @@ impl Ocv { } } // Check if enough positive votes - if total_positive_community_votes < 10 { + if !self.has_met_vote_threshold(total_positive_community_votes, total_negative_community_votes) { return Ok(GetMinaProposalConsiderationResponse { proposal_id: id, total_community_votes: votes.len(), @@ -95,7 +123,6 @@ impl Ocv { total_stake_weight: Decimal::ZERO, positive_stake_weight: Decimal::ZERO, negative_stake_weight: Decimal::ZERO, - votes, elegible: false, vote_status: "Insufficient voters".to_string(), }); @@ -153,7 +180,6 @@ impl Ocv { total_stake_weight, positive_stake_weight, negative_stake_weight, - votes, elegible: true, vote_status: "Proposal selected for the next phase".to_string(), }) @@ -258,7 +284,6 @@ pub struct GetMinaProposalConsiderationResponse { total_stake_weight: Decimal, positive_stake_weight: Decimal, negative_stake_weight: Decimal, - votes: Vec, vote_status: String, elegible: bool, } diff --git a/server/src/vote.rs b/server/src/vote.rs index 6c4ef245..f819dc28 100644 --- a/server/src/vote.rs +++ b/server/src/vote.rs @@ -302,4 +302,178 @@ mod tests { Vote::new("2", "4", "E4YiC7vB4DC9JoQvaj83nBWwHC3gJh4G9EBef7xh4ti4idBAgZai7", 120, BlockStatus::Pending, 120, 2), ] } + + #[test] + fn test_decode_mep_memo() { + let mut vote = Vote::new("1", "1", "", 100, BlockStatus::Pending, 100, 1); + + vote.update_memo("E4Yd67s51QN9DZVDy8JKPEoNGykMsYQ5KRiKpZHiLZTjA8dB9SnFT"); + assert_eq!(vote.decode_memo().unwrap(), "BeepBoop"); + + vote.update_memo("E4YXRxe1SLybDSwNoEKENNyoaj9ro9WDbiDPtJrX9Fmj5nUtVeq6x"); + assert_eq!(vote.decode_memo().unwrap(), "YES 1"); + + vote.update_memo("E4YVQPTeHZGie6hAZbLSCbuj5UuhfWsCxnLbUgBPqGJgdi8XX9CNP"); + assert_eq!(vote.decode_memo().unwrap(), "NO 1"); + } + + #[test] + fn test_match_decode_mep_memo() { + let key = "1"; + let mut votes = get_test_mep_votes(); + + let v0_decoded = votes[0].match_decoded_mef_memo(key).unwrap(); + let v1_decoded = votes[1].match_decoded_mef_memo(key).unwrap(); + let v2_decoded = votes[2].match_decoded_mef_memo(key).unwrap(); + let v3_decoded = votes[3].match_decoded_mef_memo(key).unwrap(); + let v4_decoded = votes[4].match_decoded_mef_memo(key).unwrap(); + let v5_decoded = votes[5].match_decoded_mef_memo(key).unwrap(); + let v6_decoded = votes[6].match_decoded_mef_memo(key).unwrap(); + let v7_decoded = votes[7].match_decoded_mef_memo(key).unwrap(); + let v8_decoded = votes[8].match_decoded_mef_memo(key).unwrap(); + let v9_decoded = votes[9].match_decoded_mef_memo(key).unwrap(); + let v10_decoded = votes[10].match_decoded_mef_memo(key).unwrap(); + + assert_eq!(v0_decoded, "YES 1"); + assert_eq!(v1_decoded, "YES 1"); + assert_eq!(v2_decoded, "YES 1"); + assert_eq!(v3_decoded, "YES 1"); + assert_eq!(v4_decoded, "YES 1"); + assert_eq!(v5_decoded, "YES 1"); + assert_eq!(v6_decoded, "YES 1"); + assert_eq!(v7_decoded, "YES 1"); + assert_eq!(v8_decoded, "YES 1"); + assert_eq!(v9_decoded, "YES 1"); + assert_eq!(v10_decoded, "NO 1"); + } + + #[test] + fn test_process_mep_votes() { + let votes = get_test_mep_votes(); + let binding = Wrapper(votes).process_mep(1, 130); + let processed = binding.to_vec().0; + + assert_eq!(processed.len(), 11); + + let a1 = processed.iter().find(|s| s.account == "1").unwrap(); + let a2 = processed.iter().find(|s| s.account == "2").unwrap(); + + assert_eq!(a1.account, "1"); + assert_eq!(a1.hash, "1"); + assert_eq!(a1.memo, "YES 1"); + assert_eq!(a1.height, 331718); + assert_eq!(a1.status, BlockStatus::Canonical); + assert_eq!(a1.nonce, 1); + + assert_eq!(a2.account, "2"); + assert_eq!(a2.hash, "2"); + assert_eq!(a2.memo, "YES 1"); + assert_eq!(a2.height, 341719); + assert_eq!(a2.status, BlockStatus::Pending); + assert_eq!(a2.nonce, 2); + } + + fn get_test_mep_votes() -> Vec { + vec![ + Vote::new( + "1", + "1", + "E4YXRxe1SLybDSwNoEKENNyoaj9ro9WDbiDPtJrX9Fmj5nUtVeq6x", + 331718, + BlockStatus::Canonical, + 1730897878000, + 1, + ), + Vote::new( + "2", + "2", + "E4YXRxe1SLybDSwNoEKENNyoaj9ro9WDbiDPtJrX9Fmj5nUtVeq6x", + 341719, + BlockStatus::Pending, + 1730897878000, + 2, + ), + Vote::new( + "3", + "3", + "E4YXRxe1SLybDSwNoEKENNyoaj9ro9WDbiDPtJrX9Fmj5nUtVeq6x", + 351320, + BlockStatus::Pending, + 1730897878000, + 3, + ), + Vote::new( + "4", + "4", + "E4YXRxe1SLybDSwNoEKENNyoaj9ro9WDbiDPtJrX9Fmj5nUtVeq6x", + 352721, + BlockStatus::Pending, + 1730897878000, + 4, + ), + Vote::new( + "5", + "5", + "E4YXRxe1SLybDSwNoEKENNyoaj9ro9WDbiDPtJrX9Fmj5nUtVeq6x", + 353722, + BlockStatus::Pending, + 1730897878000, + 5, + ), + Vote::new( + "6", + "6", + "E4YXRxe1SLybDSwNoEKENNyoaj9ro9WDbiDPtJrX9Fmj5nUtVeq6x", + 354723, + BlockStatus::Pending, + 1730897878000, + 6, + ), + Vote::new( + "7", + "7", + "E4YXRxe1SLybDSwNoEKENNyoaj9ro9WDbiDPtJrX9Fmj5nUtVeq6x", + 355724, + BlockStatus::Pending, + 1730897878000, + 7, + ), + Vote::new( + "8", + "8", + "E4YXRxe1SLybDSwNoEKENNyoaj9ro9WDbiDPtJrX9Fmj5nUtVeq6x", + 356725, + BlockStatus::Pending, + 1730897878000, + 8, + ), + Vote::new( + "9", + "9", + "E4YXRxe1SLybDSwNoEKENNyoaj9ro9WDbiDPtJrX9Fmj5nUtVeq6x", + 357726, + BlockStatus::Pending, + 1730897878000, + 9, + ), + Vote::new( + "10", + "10", + "E4YXRxe1SLybDSwNoEKENNyoaj9ro9WDbiDPtJrX9Fmj5nUtVeq6x", + 358727, + BlockStatus::Pending, + 1730897878000, + 10, + ), + Vote::new( + "11", + "11", + "E4YVQPTeHZGie6hAZbLSCbuj5UuhfWsCxnLbUgBPqGJgdi8XX9CNP", + 358728, + BlockStatus::Pending, + 1730897878000, + 11, + ), + ] + } }