From 49e4a02a3f3e009bd078745a782c7ac10aa03861 Mon Sep 17 00:00:00 2001 From: Giacomo Pasini Date: Tue, 2 Aug 2022 10:33:17 +0200 Subject: [PATCH] Additional threshold (#142) * add additional per-challenge filter * update Cargo.lock * address review comments --- Cargo.lock | 135 +++++++++-- catalyst-toolbox/Cargo.toml | 1 + .../src/bin/cli/rewards/voters.rs | 88 ++++++- catalyst-toolbox/src/rewards/voters.rs | 214 ++++++++++++++++-- 4 files changed, 392 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1659480d..97e205c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,6 +78,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -436,7 +445,7 @@ dependencies = [ [[package]] name = "cardano-legacy-address" version = "0.1.1" -source = "git+https://github.com/input-output-hk/chain-libs.git?branch=master#a8ac1f74eb596e27f9383f3a13e4da3fc16d8d43" +source = "git+https://github.com/input-output-hk/chain-libs.git?branch=master#135e49a572c7fd1460138c511bdd875c92100680" dependencies = [ "cbor_event", "cryptoxide 0.4.2", @@ -511,6 +520,7 @@ dependencies = [ "url", "versionisator", "vit-servicing-station-lib", + "vit-servicing-station-tests", "voting-hir 0.1.0", "wallet", ] @@ -569,7 +579,7 @@ dependencies = [ [[package]] name = "chain-addr" version = "0.1.0" -source = "git+https://github.com/input-output-hk/chain-libs.git?branch=master#a8ac1f74eb596e27f9383f3a13e4da3fc16d8d43" +source = "git+https://github.com/input-output-hk/chain-libs.git?branch=master#135e49a572c7fd1460138c511bdd875c92100680" dependencies = [ "bech32 0.8.1", "chain-core", @@ -583,7 +593,7 @@ dependencies = [ [[package]] name = "chain-core" version = "0.1.0" -source = "git+https://github.com/input-output-hk/chain-libs.git?branch=master#a8ac1f74eb596e27f9383f3a13e4da3fc16d8d43" +source = "git+https://github.com/input-output-hk/chain-libs.git?branch=master#135e49a572c7fd1460138c511bdd875c92100680" dependencies = [ "chain-ser", ] @@ -591,7 +601,7 @@ dependencies = [ [[package]] name = "chain-crypto" version = "0.1.0" -source = "git+https://github.com/input-output-hk/chain-libs.git?branch=master#a8ac1f74eb596e27f9383f3a13e4da3fc16d8d43" +source = "git+https://github.com/input-output-hk/chain-libs.git?branch=master#135e49a572c7fd1460138c511bdd875c92100680" dependencies = [ "bech32 0.8.1", "cryptoxide 0.4.2", @@ -613,7 +623,7 @@ dependencies = [ [[package]] name = "chain-evm" version = "0.1.0" -source = "git+https://github.com/input-output-hk/chain-libs.git?branch=master#a8ac1f74eb596e27f9383f3a13e4da3fc16d8d43" +source = "git+https://github.com/input-output-hk/chain-libs.git?branch=master#135e49a572c7fd1460138c511bdd875c92100680" dependencies = [ "aurora-bn", "base64 0.13.0", @@ -642,7 +652,7 @@ dependencies = [ [[package]] name = "chain-impl-mockchain" version = "0.1.0" -source = "git+https://github.com/input-output-hk/chain-libs.git?branch=master#a8ac1f74eb596e27f9383f3a13e4da3fc16d8d43" +source = "git+https://github.com/input-output-hk/chain-libs.git?branch=master#135e49a572c7fd1460138c511bdd875c92100680" dependencies = [ "cardano-legacy-address", "chain-addr", @@ -682,7 +692,7 @@ dependencies = [ [[package]] name = "chain-ser" version = "0.1.0" -source = "git+https://github.com/input-output-hk/chain-libs.git?branch=master#a8ac1f74eb596e27f9383f3a13e4da3fc16d8d43" +source = "git+https://github.com/input-output-hk/chain-libs.git?branch=master#135e49a572c7fd1460138c511bdd875c92100680" dependencies = [ "thiserror", ] @@ -703,7 +713,7 @@ dependencies = [ [[package]] name = "chain-time" version = "0.1.0" -source = "git+https://github.com/input-output-hk/chain-libs.git?branch=master#a8ac1f74eb596e27f9383f3a13e4da3fc16d8d43" +source = "git+https://github.com/input-output-hk/chain-libs.git?branch=master#135e49a572c7fd1460138c511bdd875c92100680" dependencies = [ "chain-core", "chain-ser", @@ -715,9 +725,9 @@ dependencies = [ [[package]] name = "chain-vote" version = "0.1.0" -source = "git+https://github.com/input-output-hk/chain-libs.git?branch=master#a8ac1f74eb596e27f9383f3a13e4da3fc16d8d43" +source = "git+https://github.com/input-output-hk/chain-libs.git?branch=master#135e49a572c7fd1460138c511bdd875c92100680" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", "chain-core", "chain-crypto", "const_format", @@ -773,7 +783,7 @@ version = "2.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ - "ansi_term", + "ansi_term 0.12.1", "atty", "bitflags", "strsim 0.8.0", @@ -1093,6 +1103,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "ctor" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "ctr" version = "0.8.0" @@ -1338,6 +1358,12 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "dyn-clone" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d07a982d1fb29db01e5a59b1918e03da4df7297eaeee7686ac45542fd4e59c8" + [[package]] name = "ed25519" version = "1.5.2" @@ -1581,6 +1607,16 @@ dependencies = [ "synstructure", ] +[[package]] +name = "fake" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d68f517805463f3a896a9d29c1d6ff09d3579ded64a7201b4069f8f9c0d52fd" +dependencies = [ + "http", + "rand 0.8.5", +] + [[package]] name = "fastrand" version = "1.7.0" @@ -2391,7 +2427,7 @@ dependencies = [ [[package]] name = "imhamt" version = "0.1.0" -source = "git+https://github.com/input-output-hk/chain-libs.git?branch=master#a8ac1f74eb596e27f9383f3a13e4da3fc16d8d43" +source = "git+https://github.com/input-output-hk/chain-libs.git?branch=master#135e49a572c7fd1460138c511bdd875c92100680" dependencies = [ "proptest 1.0.0 (git+https://github.com/input-output-hk/proptest.git)", "rustc_version", @@ -2701,7 +2737,7 @@ dependencies = [ [[package]] name = "jormungandr-lib" version = "0.13.0" -source = "git+https://github.com/input-output-hk/jormungandr.git?branch=master#c27977964284c09505ef4e6fbc5403b311dbc224" +source = "git+https://github.com/input-output-hk/jormungandr.git?branch=master#1b1704f95ba84acca3a57a6c97260e6255978163" dependencies = [ "base64 0.13.0", "bech32 0.8.1", @@ -3516,6 +3552,15 @@ version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" +[[package]] +name = "output_vt100" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" +dependencies = [ + "winapi", +] + [[package]] name = "owo-colors" version = "3.4.0" @@ -3832,6 +3877,18 @@ dependencies = [ "termtree", ] +[[package]] +name = "pretty_assertions" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427" +dependencies = [ + "ansi_term 0.11.0", + "ctor", + "difference", + "output_vt100", +] + [[package]] name = "primitive-types" version = "0.11.1" @@ -4827,7 +4884,7 @@ checksum = "cc88c725d61fc6c3132893370cac4a0200e3fedf5da8331c570664b1987f5ca2" [[package]] name = "snapshot-service" version = "0.1.0" -source = "git+https://github.com/input-output-hk/vit-servicing-station.git?branch=master#1448284748bed304b7c0e12643fcf4473ebbbfb4" +source = "git+https://github.com/input-output-hk/vit-servicing-station.git?branch=master#8d431648d84fc1164c228f6638eaee02cff2d2e4" dependencies = [ "jormungandr-lib", "notify", @@ -4854,7 +4911,7 @@ dependencies = [ [[package]] name = "sparse-array" version = "0.1.0" -source = "git+https://github.com/input-output-hk/chain-libs.git?branch=master#a8ac1f74eb596e27f9383f3a13e4da3fc16d8d43" +source = "git+https://github.com/input-output-hk/chain-libs.git?branch=master#135e49a572c7fd1460138c511bdd875c92100680" [[package]] name = "spin" @@ -5589,7 +5646,7 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bc28f93baff38037f64e6f43d34cfa1605f27a49c34e8a04c5e78b0babf2596" dependencies = [ - "ansi_term", + "ansi_term 0.12.1", "serde", "serde_json", "sharded-slab", @@ -5647,7 +5704,7 @@ dependencies = [ [[package]] name = "typed-bytes" version = "0.1.0" -source = "git+https://github.com/input-output-hk/chain-libs.git?branch=master#a8ac1f74eb596e27f9383f3a13e4da3fc16d8d43" +source = "git+https://github.com/input-output-hk/chain-libs.git?branch=master#135e49a572c7fd1460138c511bdd875c92100680" [[package]] name = "typenum" @@ -5806,7 +5863,7 @@ dependencies = [ [[package]] name = "vit-servicing-station-lib" version = "0.3.4-dev" -source = "git+https://github.com/input-output-hk/vit-servicing-station.git?branch=master#1448284748bed304b7c0e12643fcf4473ebbbfb4" +source = "git+https://github.com/input-output-hk/vit-servicing-station.git?branch=master#8d431648d84fc1164c228f6638eaee02cff2d2e4" dependencies = [ "async-trait", "base64 0.12.3", @@ -5835,6 +5892,46 @@ dependencies = [ "warp", ] +[[package]] +name = "vit-servicing-station-tests" +version = "0.3.4-dev" +source = "git+https://github.com/input-output-hk/vit-servicing-station.git?branch=master#8d431648d84fc1164c228f6638eaee02cff2d2e4" +dependencies = [ + "assert_cmd 2.0.4", + "assert_fs", + "base64 0.12.3", + "cfg-if 0.1.10", + "chain-addr", + "chain-crypto", + "chain-impl-mockchain", + "diesel", + "diesel_migrations", + "dyn-clone", + "fake", + "hyper", + "itertools 0.10.3", + "jortestkit", + "lazy_static", + "libsqlite3-sys", + "predicates 2.1.1", + "pretty_assertions", + "quickcheck", + "quickcheck_macros", + "rand 0.7.3", + "rand_core 0.5.1", + "reqwest", + "serde", + "serde_json", + "structopt", + "tempfile", + "thiserror", + "time 0.3.11", + "tokio", + "url", + "vit-servicing-station-lib", + "voting-hir 0.1.0 (git+https://github.com/input-output-hk/catalyst-toolbox?branch=main)", +] + [[package]] name = "void" version = "1.0.2" @@ -5854,7 +5951,7 @@ dependencies = [ [[package]] name = "voting-hir" version = "0.1.0" -source = "git+https://github.com/input-output-hk/catalyst-toolbox?branch=main#d260af9d865fafa386f6fedf4bf643ffc5cdca02" +source = "git+https://github.com/input-output-hk/catalyst-toolbox?branch=main#ea16440d54b13ef34690507f2c5d54ff86f2b013" dependencies = [ "jormungandr-lib", "serde", diff --git a/catalyst-toolbox/Cargo.toml b/catalyst-toolbox/Cargo.toml index 3faa7d63..5eb99ac4 100644 --- a/catalyst-toolbox/Cargo.toml +++ b/catalyst-toolbox/Cargo.toml @@ -75,6 +75,7 @@ proptest = { git = "https://github.com/input-output-hk/proptest", branch = "mast test-strategy = "0.2" serde_test = "1" voting-hir = { path = "../voting-hir", features = ["serde", "proptest"] } +vit-servicing-station-tests = { git = "https://github.com/input-output-hk/vit-servicing-station.git", branch = "master" } [build-dependencies] versionisator = "1.0.3" diff --git a/catalyst-toolbox/src/bin/cli/rewards/voters.rs b/catalyst-toolbox/src/bin/cli/rewards/voters.rs index 1894746c..fd9df6cd 100644 --- a/catalyst-toolbox/src/bin/cli/rewards/voters.rs +++ b/catalyst-toolbox/src/bin/cli/rewards/voters.rs @@ -1,12 +1,17 @@ -use catalyst_toolbox::rewards::voters::{calc_voter_rewards, Rewards, VoteCount}; +use catalyst_toolbox::rewards::voters::{calc_voter_rewards, Rewards, Threshold, VoteCount}; use catalyst_toolbox::snapshot::{registration::MainnetRewardAddress, SnapshotInfo}; use catalyst_toolbox::utils::assert_are_close; +use jormungandr_lib::{ + crypto::{account::Identifier, hash::Hash}, + interfaces::AccountVotes, +}; +use vit_servicing_station_lib::db::models::proposals::FullProposalInfo; -use color_eyre::Report; +use color_eyre::{eyre::eyre, Report}; use jcli_lib::jcli_lib::block::Common; use structopt::StructOpt; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; use std::path::PathBuf; #[derive(StructOpt)] @@ -22,12 +27,26 @@ pub struct VotersRewards { #[structopt(long)] snapshot_info_path: PathBuf, + /// Path to a json-encoded list of VotePlanStatusFull to consider for voters + /// participation in the election. + /// This can be retrived from the v1/vote/active/plans/full endpoint exposed + /// by a Jormungandr node. #[structopt(long)] votes_count_path: PathBuf, - /// Number of votes required to be able to receive voter rewards + /// Number of global votes required to be able to receive voter rewards #[structopt(long, default_value)] - vote_threshold: u64, + vote_threshold: usize, + + /// Path to a json-encoded map from challenge id to an optional required threshold + /// per-challenge in order to receive rewards. + #[structopt(long)] + per_challenge_threshold: Option, + + /// Path to the list of proposals active in this election. + /// Can be obtained from /api/v0/proposals. + #[structopt(long)] + proposals: PathBuf, } fn write_rewards_results( @@ -55,20 +74,71 @@ impl VotersRewards { snapshot_info_path, votes_count_path, vote_threshold, + per_challenge_threshold, + proposals, } = self; - let vote_count: VoteCount = serde_json::from_reader(jcli_lib::utils::io::open_file_read( - &Some(votes_count_path), - )?)?; + let proposals_per_voteplan = serde_json::from_reader::<_, Vec>( + jcli_lib::utils::io::open_file_read(&Some(proposals))?, + )? + .into_iter() + .fold(>>::new(), |mut acc, prop| { + let entry = acc + .entry(prop.voteplan.chain_voteplan_id.clone()) + .or_default(); + entry.push(prop); + entry.sort_by_key(|p| p.voteplan.chain_proposal_index); + acc + }); + + let vote_count = serde_json::from_reader::<_, HashMap>>( + jcli_lib::utils::io::open_file_read(&Some(votes_count_path))?, + )? + .into_iter() + .try_fold(VoteCount::new(), |mut acc, (account, votes)| { + for vote in &votes { + let voteplan = vote.vote_plan_id; + let props = proposals_per_voteplan + .get(&voteplan.to_string()) + .iter() + .flat_map(|p| p.iter()) + .enumerate() + .filter(|(i, _p)| vote.votes.contains(&(*i as u8))) + .map(|(_, p)| { + Ok::<_, Report>(Hash::from( + <[u8; 32]>::try_from(p.proposal.chain_proposal_id.clone()).map_err( + |v| eyre!("Invalid proposal hash length {}, expected 32", v.len()), + )?, + )) + }) + .collect::, _>>()?; + acc.entry(account.clone()).or_default().extend(props); + } + Ok::<_, Report>(acc) + })?; let snapshot: Vec = serde_json::from_reader( jcli_lib::utils::io::open_file_read(&Some(snapshot_info_path))?, )?; + let additional_thresholds: HashMap = if let Some(file) = per_challenge_threshold + { + serde_json::from_reader(jcli_lib::utils::io::open_file_read(&Some(file))?)? + } else { + HashMap::new() + }; + let results = calc_voter_rewards( vote_count, - vote_threshold, snapshot, + Threshold::new( + vote_threshold, + additional_thresholds, + proposals_per_voteplan + .into_iter() + .flat_map(|(_k, v)| v.into_iter()) + .collect(), + )?, Rewards::from(total_rewards), )?; diff --git a/catalyst-toolbox/src/rewards/voters.rs b/catalyst-toolbox/src/rewards/voters.rs index a9e24e6d..1b9567b2 100644 --- a/catalyst-toolbox/src/rewards/voters.rs +++ b/catalyst-toolbox/src/rewards/voters.rs @@ -1,12 +1,66 @@ use crate::snapshot::{registration::MainnetRewardAddress, SnapshotInfo}; -use jormungandr_lib::crypto::account::Identifier; +use jormungandr_lib::crypto::{account::Identifier, hash::Hash}; use rust_decimal::Decimal; use std::collections::{BTreeMap, HashMap, HashSet}; use thiserror::Error; +use vit_servicing_station_lib::db::models::proposals::FullProposalInfo; pub const ADA_TO_LOVELACE_FACTOR: u64 = 1_000_000; pub type Rewards = Decimal; +pub struct Threshold { + total: usize, + per_challenge: HashMap, + proposals_per_challenge: HashMap>, +} + +impl Threshold { + pub fn new( + total_threshold: usize, + per_challenge: HashMap, + proposals: Vec, + ) -> Result { + let proposals = proposals + .into_iter() + .map(|p| { + <[u8; 32]>::try_from(p.proposal.chain_proposal_id) + .map_err(Error::InvalidHash) + .map(|hash| (p.proposal.challenge_id, Hash::from(hash))) + }) + .collect::, Error>>()?; + Ok(Self { + total: total_threshold, + per_challenge, + proposals_per_challenge: proposals.into_iter().fold( + HashMap::new(), + |mut acc, (challenge_id, hash)| { + acc.entry(challenge_id).or_default().insert(hash); + acc + }, + ), + }) + } + + fn filter(&self, votes: &HashSet) -> bool { + if votes.len() < self.total { + return false; + } + + for (challenge, threshold) in &self.per_challenge { + let votes_in_challengs = self + .proposals_per_challenge + .get(challenge) + .map(|props| votes.intersection(props).count()) + .unwrap_or_default(); + if votes_in_challengs < *threshold { + return false; + } + } + + true + } +} + #[derive(Debug, Error)] pub enum Error { #[error("Value overflowed its maximum value")] @@ -15,6 +69,8 @@ pub enum Error { MultipleEntries, #[error("Unknown voter group {0}")] UnknownVoterGroup(String), + #[error("Invalid blake2b256 hash")] + InvalidHash(Vec), } fn calculate_reward( @@ -33,18 +89,21 @@ fn calculate_reward( .collect() } -pub type VoteCount = HashMap; +pub type VoteCount = HashMap>; fn filter_active_addresses( vote_count: VoteCount, - threshold: u64, snapshot_info: Vec, + threshold: Threshold, ) -> Vec { snapshot_info .into_iter() .filter(|v| { - let addr = v.hir.voting_key.to_hex(); - vote_count.get(&addr).copied().unwrap_or_default() >= threshold + if let Some(votes) = vote_count.get(&v.hir.voting_key) { + threshold.filter(votes) + } else { + threshold.filter(&HashSet::new()) + } }) .collect() } @@ -79,8 +138,8 @@ fn rewards_to_mainnet_addresses( pub fn calc_voter_rewards( vote_count: VoteCount, - vote_threshold: u64, voters: Vec, + vote_threshold: Threshold, total_rewards: Rewards, ) -> Result, Error> { let unique_voters = voters @@ -90,7 +149,7 @@ pub fn calc_voter_rewards( if unique_voters.len() != voters.len() { return Err(Error::MultipleEntries); } - let active_addresses = filter_active_addresses(vote_count, vote_threshold, voters); + let active_addresses = filter_active_addresses(vote_count, voters, vote_threshold); let mut total_active_stake = 0u64; let mut stake_per_voter = HashMap::new(); @@ -120,11 +179,17 @@ mod tests { let votes_count = snapshot .voting_keys() .into_iter() - .map(|key| (key.to_hex(), 1)) + .map(|key| (key.clone(), HashSet::from([Hash::from([0u8; 32])]))) .collect::(); let n_voters = votes_count.len(); let voters = snapshot.to_full_snapshot_info(); - let rewards = calc_voter_rewards(votes_count, 1, voters, Rewards::ONE).unwrap(); + let rewards = calc_voter_rewards( + votes_count, + voters, + Threshold::new(1, HashMap::new(), Vec::new()).unwrap(), + Rewards::ONE, + ) + .unwrap(); if n_voters > 0 { assert_are_close(rewards.values().sum::(), Rewards::ONE) } else { @@ -136,7 +201,13 @@ mod tests { fn test_all_inactive(snapshot: Snapshot) { let votes_count = VoteCount::new(); let voters = snapshot.to_full_snapshot_info(); - let rewards = calc_voter_rewards(votes_count, 1, voters, Rewards::ONE).unwrap(); + let rewards = calc_voter_rewards( + votes_count, + voters, + Threshold::new(1, HashMap::new(), Vec::new()).unwrap(), + Rewards::ONE, + ) + .unwrap(); assert_eq!(rewards.len(), 0); } @@ -147,9 +218,21 @@ mod tests { let votes_count = voting_keys .iter() .enumerate() - .map(|(i, key)| (key.to_hex(), (i % 2 == 0) as u64)) + .map(|(i, &key)| { + ( + key.to_owned(), + if i % 2 == 0 { + HashSet::from([Hash::from([0u8; 32])]) + } else { + HashSet::new() + }, + ) + }) .collect::(); - let n_voters = votes_count.iter().filter(|(_, votes)| **votes > 0).count(); + let n_voters = votes_count + .iter() + .filter(|(_, votes)| !votes.is_empty()) + .count(); let voters = snapshot.to_full_snapshot_info(); let voters_active = voters .clone() @@ -159,9 +242,20 @@ mod tests { .map(|(_, utxo)| utxo) .collect::>(); - let mut rewards = calc_voter_rewards(votes_count.clone(), 1, voters, Rewards::ONE).unwrap(); - let rewards_no_inactive = - calc_voter_rewards(votes_count, 1, voters_active, Rewards::ONE).unwrap(); + let mut rewards = calc_voter_rewards( + votes_count.clone(), + voters, + Threshold::new(1, HashMap::new(), Vec::new()).unwrap(), + Rewards::ONE, + ) + .unwrap(); + let rewards_no_inactive = calc_voter_rewards( + votes_count, + voters_active, + Threshold::new(1, HashMap::new(), Vec::new()).unwrap(), + Rewards::ONE, + ) + .unwrap(); // Rewards should ignore inactive voters assert_eq!(rewards, rewards_no_inactive); if n_voters > 0 { @@ -228,7 +322,13 @@ mod tests { let voters = snapshot.to_full_snapshot_info(); - let rewards = calc_voter_rewards(VoteCount::new(), 0, voters, Rewards::ONE).unwrap(); + let rewards = calc_voter_rewards( + VoteCount::new(), + voters, + Threshold::new(0, HashMap::new(), Vec::new()).unwrap(), + Rewards::ONE, + ) + .unwrap(); assert_eq!(rewards.values().sum::(), Rewards::ONE); for (addr, reward) in rewards { assert_eq!( @@ -266,10 +366,88 @@ mod tests { let voters = snapshot.to_full_snapshot_info(); - let rewards = calc_voter_rewards(VoteCount::new(), 0, voters, Rewards::ONE).unwrap(); + let rewards = calc_voter_rewards( + VoteCount::new(), + voters, + Threshold::new(0, HashMap::new(), Vec::new()).unwrap(), + Rewards::ONE, + ) + .unwrap(); assert_are_close(rewards.values().sum::(), Rewards::ONE); for (_, reward) in rewards { - assert_eq!(reward, Rewards::ONE / Rewards::from(9)); + assert_eq!(reward, Rewards::ONE / Rewards::from(9u8)); + } + } + + #[proptest] + fn test_per_category_threshold(snapshot: Snapshot) { + use vit_servicing_station_tests::common::data::ArbitrarySnapshotGenerator; + + let voters = snapshot.to_full_snapshot_info(); + + let snapshot = ArbitrarySnapshotGenerator::default().snapshot(); + let mut proposals = snapshot.proposals(); + // for some reasone they are base64 encoded and truncatin is just easier + for proposal in &mut proposals { + proposal.proposal.chain_proposal_id.truncate(32); + } + let proposals_by_challenge = + proposals + .iter() + .fold(>>::new(), |mut acc, prop| { + acc.entry(prop.proposal.challenge_id) + .or_default() + .push(Hash::from( + <[u8; 32]>::try_from(prop.proposal.chain_proposal_id.clone()).unwrap(), + )); + acc + }); + let per_challenge_threshold = proposals_by_challenge + .iter() + .map(|(challenge, p)| (*challenge, p.len())) + .collect::>(); + + let mut votes_count = voters + .iter() + .map(|v| { + ( + v.hir.voting_key.clone(), + proposals_by_challenge + .values() + .flat_map(|p| p.iter()) + .cloned() + .collect::>(), + ) + }) + .collect::>(); + let (_, inactive) = votes_count.split_at_mut(voters.len() / 2); + for v in inactive { + v.1.remove(&v.1.iter().next().unwrap().clone()); } + + let only_active = votes_count + .clone() + .into_iter() + .take(voters.len() / 2) + .collect::>(); + let votes_count = votes_count.into_iter().collect::>(); + + let rewards = calc_voter_rewards( + votes_count, + voters.clone(), + Threshold::new(1, per_challenge_threshold.clone(), proposals.clone()).unwrap(), + Rewards::ONE, + ) + .unwrap(); + + let rewards_only_active = calc_voter_rewards( + only_active, + voters, + Threshold::new(1, per_challenge_threshold, proposals).unwrap(), + Rewards::ONE, + ) + .unwrap(); + + assert_eq!(rewards_only_active, rewards); } }