From fde5484726c4eb09371a424e4b7376f005008c80 Mon Sep 17 00:00:00 2001 From: Giacomo Pasini Date: Tue, 17 May 2022 10:04:02 +0200 Subject: [PATCH 1/7] Process snapshot to output VoterHIR Use VoterHIR as the output format for the snapshot tool --- catalyst-toolbox/src/bin/cli/snapshot/mod.rs | 5 +++-- catalyst-toolbox/src/snapshot/mod.rs | 8 -------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/catalyst-toolbox/src/bin/cli/snapshot/mod.rs b/catalyst-toolbox/src/bin/cli/snapshot/mod.rs index 5523561f..c4870a94 100644 --- a/catalyst-toolbox/src/bin/cli/snapshot/mod.rs +++ b/catalyst-toolbox/src/bin/cli/snapshot/mod.rs @@ -3,6 +3,7 @@ use color_eyre::Report; use fraction::Fraction; use jcli_lib::utils::{output_file::OutputFile, output_format::OutputFormat}; use jormungandr_lib::interfaces::Value; +use rust_decimal::Decimal; use std::fs::File; use std::io::Write; use std::path::PathBuf; @@ -20,7 +21,7 @@ pub struct SnapshotCmd { snapshot: PathBuf, /// Registrations voting power threshold for eligibility #[structopt(short, long)] - threshold: Value, + min_stake_threshold: Value, /// Voter group to assign direct voters to. /// If empty, defaults to "voter" @@ -59,7 +60,7 @@ impl SnapshotCmd { let assigner = RepsVotersAssigner::new(direct_voter, representative, self.reps_db_api_url)?; let initials = Snapshot::from_raw_snapshot( raw_snapshot, - self.threshold, + self.min_stake_threshold, self.voting_power_cap, &assigner, )? diff --git a/catalyst-toolbox/src/snapshot/mod.rs b/catalyst-toolbox/src/snapshot/mod.rs index 3c90e493..75be52c8 100644 --- a/catalyst-toolbox/src/snapshot/mod.rs +++ b/catalyst-toolbox/src/snapshot/mod.rs @@ -176,14 +176,6 @@ mod tests { use proptest::prelude::*; use test_strategy::proptest; - struct DummyAssigner; - - impl VotingGroupAssigner for DummyAssigner { - fn assign(&self, _vk: &Identifier) -> String { - String::new() - } - } - impl Snapshot { pub fn to_block0_initials(&self, discrimination: Discrimination) -> Vec { self.inner From 1e84995452e0e778b1f74d6e979cf47f788ad5e5 Mon Sep 17 00:00:00 2001 From: Giacomo Pasini Date: Wed, 18 May 2022 10:16:41 +0200 Subject: [PATCH 2/7] Add cap for voting influence in snapshot Limit overall influence of actors in an election by capping their maximum voting power at arbitrary levels. Since a lot of configurations are going in the snapshot process, I'm thinking of having it output two different artifacts: the list of VoterHIRs, as already proposed, and another one which is a light wrapper around VoterHIR which contains contributions from snapshot, to be used for the reward process. I'm proposing to keep the separate because the first one is more general, while the second one is tied to our current deployment and more likely to change. --- catalyst-toolbox/Cargo.toml | 2 +- catalyst-toolbox/src/bin/cli/snapshot/mod.rs | 1 - catalyst-toolbox/src/snapshot/mod.rs | 8 ++++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/catalyst-toolbox/Cargo.toml b/catalyst-toolbox/Cargo.toml index c2da8d67..a2a0599f 100644 --- a/catalyst-toolbox/Cargo.toml +++ b/catalyst-toolbox/Cargo.toml @@ -56,7 +56,7 @@ image = "0.23.12" qrcode = "0.12" quircs = "0.10.0" symmetric-cipher = { git = "https://github.com/input-output-hk/chain-wallet-libs.git", branch = "master" } -graphql_client = "0.10" +graphql_client = { version = "0.10" } gag = "1" vit-servicing-station-lib = { git = "https://github.com/input-output-hk/vit-servicing-station.git", branch = "master" } env_logger = "0.9" diff --git a/catalyst-toolbox/src/bin/cli/snapshot/mod.rs b/catalyst-toolbox/src/bin/cli/snapshot/mod.rs index c4870a94..dad41200 100644 --- a/catalyst-toolbox/src/bin/cli/snapshot/mod.rs +++ b/catalyst-toolbox/src/bin/cli/snapshot/mod.rs @@ -3,7 +3,6 @@ use color_eyre::Report; use fraction::Fraction; use jcli_lib::utils::{output_file::OutputFile, output_format::OutputFormat}; use jormungandr_lib::interfaces::Value; -use rust_decimal::Decimal; use std::fs::File; use std::io::Write; use std::path::PathBuf; diff --git a/catalyst-toolbox/src/snapshot/mod.rs b/catalyst-toolbox/src/snapshot/mod.rs index 75be52c8..3c90e493 100644 --- a/catalyst-toolbox/src/snapshot/mod.rs +++ b/catalyst-toolbox/src/snapshot/mod.rs @@ -176,6 +176,14 @@ mod tests { use proptest::prelude::*; use test_strategy::proptest; + struct DummyAssigner; + + impl VotingGroupAssigner for DummyAssigner { + fn assign(&self, _vk: &Identifier) -> String { + String::new() + } + } + impl Snapshot { pub fn to_block0_initials(&self, discrimination: Discrimination) -> Vec { self.inner From 4466fa0b1afc6a895538331de32c4f5650f0d67f Mon Sep 17 00:00:00 2001 From: Giacomo Pasini Date: Tue, 24 May 2022 18:26:44 +0200 Subject: [PATCH 3/7] update rewards --- .../src/bin/cli/rewards/voters.rs | 50 +-- catalyst-toolbox/src/rewards/voters.rs | 332 +++++++++--------- .../src/snapshot/influence_cap.rs | 26 +- catalyst-toolbox/src/snapshot/mod.rs | 29 +- 4 files changed, 211 insertions(+), 226 deletions(-) diff --git a/catalyst-toolbox/src/bin/cli/rewards/voters.rs b/catalyst-toolbox/src/bin/cli/rewards/voters.rs index 4c2423c0..a6aaa5ad 100644 --- a/catalyst-toolbox/src/bin/cli/rewards/voters.rs +++ b/catalyst-toolbox/src/bin/cli/rewards/voters.rs @@ -1,13 +1,10 @@ use catalyst_toolbox::rewards::voters::{calc_voter_rewards, Rewards, VoteCount}; -use catalyst_toolbox::snapshot::{ - registration::MainnetRewardAddress, voting_group::VotingGroupAssigner, Snapshot, -}; +use catalyst_toolbox::snapshot::{registration::MainnetRewardAddress, SnapshotInfo}; use catalyst_toolbox::utils::assert_are_close; +use voting_hir::VotingGroup; use color_eyre::Report; -use fraction::Fraction; use jcli_lib::jcli_lib::block::Common; -use jormungandr_lib::{crypto::account::Identifier, interfaces::Block0Configuration}; use structopt::StructOpt; use std::collections::BTreeMap; @@ -22,19 +19,9 @@ pub struct VotersRewards { #[structopt(long)] total_rewards: u64, - /// Path to raw snapshot + /// Path to a json encoded list of `SnapshotInfo` #[structopt(long)] - snapshot_path: PathBuf, - - /// Stake threshold to be able to participate in a Catalyst sidechain - /// Registrations with less than the threshold associated to the stake address - /// will be ignored - #[structopt(long)] - registration_threshold: u64, - - /// Voting power cap for each account - #[structopt(short, long)] - voting_power_cap: Fraction, + snapshot_info_path: PathBuf, #[structopt(long)] votes_count_path: PathBuf, @@ -42,6 +29,10 @@ pub struct VotersRewards { /// Number of votes required to be able to receive voter rewards #[structopt(long, default_value)] vote_threshold: u64, + + direct_voter_group: VotingGroup, + + representative_group: VotingGroup, } fn write_rewards_results( @@ -66,32 +57,28 @@ impl VotersRewards { let VotersRewards { common, total_rewards, - snapshot_path, - registration_threshold, + snapshot_info_path, votes_count_path, vote_threshold, - voting_power_cap, + direct_voter_group, + representative_group, } = self; - let block = common.input.load_block()?; - let block0 = Block0Configuration::from_block(&block)?; let vote_count: VoteCount = serde_json::from_reader(jcli_lib::utils::io::open_file_read( &Some(votes_count_path), )?)?; - let snapshot = Snapshot::from_raw_snapshot( - serde_json::from_reader(jcli_lib::utils::io::open_file_read(&Some(snapshot_path))?)?, - registration_threshold.into(), - voting_power_cap, - &DummyAssigner, + let snapshot: Vec = serde_json::from_reader( + jcli_lib::utils::io::open_file_read(&Some(snapshot_info_path))?, )?; let results = calc_voter_rewards( vote_count, vote_threshold, - &block0, snapshot, Rewards::from(total_rewards), + representative_group, + direct_voter_group, )?; let actual_rewards = results.values().sum::(); @@ -101,10 +88,3 @@ impl VotersRewards { Ok(()) } } - -struct DummyAssigner; -impl VotingGroupAssigner for DummyAssigner { - fn assign(&self, _vk: &Identifier) -> String { - unimplemented!() - } -} diff --git a/catalyst-toolbox/src/rewards/voters.rs b/catalyst-toolbox/src/rewards/voters.rs index d5daae7f..04eb5757 100644 --- a/catalyst-toolbox/src/rewards/voters.rs +++ b/catalyst-toolbox/src/rewards/voters.rs @@ -1,13 +1,9 @@ -use crate::snapshot::{registration::MainnetRewardAddress, Snapshot}; -use chain_addr::{Discrimination, Kind}; -use chain_impl_mockchain::transaction::UnspecifiedAccountIdentifier; -use chain_impl_mockchain::vote::CommitteeId; +use crate::snapshot::{registration::MainnetRewardAddress, SnapshotInfo}; use jormungandr_lib::crypto::account::Identifier; use rust_decimal::Decimal; use std::collections::{BTreeMap, HashMap, HashSet}; use thiserror::Error; - -use jormungandr_lib::interfaces::{Address, Block0Configuration, Initial}; +use voting_hir::VotingGroup; pub const ADA_TO_LOVELACE_FACTOR: u64 = 1_000_000; pub type Rewards = Decimal; @@ -16,121 +12,94 @@ pub type Rewards = Decimal; pub enum Error { #[error("Value overflowed its maximum value")] Overflow, + #[error("Multiple snapshot entries per voter are not supported")] + MultipleEntries, + #[error("Unknown voter group {0}")] + UnknownVoterGroup(String), } -fn calculate_active_stake<'address>( - committee_keys: &HashSet
, - block0: &'address Block0Configuration, - active_addresses: &ActiveAddresses, -) -> Result<(u64, HashMap<&'address Address, u64>), Error> { - let mut total_stake: u64 = 0; - let mut stake_per_voter: HashMap<&'address Address, u64> = HashMap::new(); - - for fund in &block0.initial { - match fund { - Initial::Fund(_) => {} - Initial::Cert(_) => {} - Initial::LegacyFund(_) => {} - Initial::Token(token) => { - for destination in &token.to { - // Exclude committee addresses (managed by IOG) and - // non active voters from total active stake for the purpose of calculating - // voter rewards - if !committee_keys.contains(&destination.address) - && active_addresses.contains(&destination.address) - { - let value: u64 = destination.value.into(); - total_stake = total_stake.checked_add(value).ok_or(Error::Overflow)?; - let entry = stake_per_voter.entry(&destination.address).or_default(); - *entry += value; - } - } - } +// Get the voting power used in rewards calculation: +// * actual voting power for representatives +// * voting power without cap for direct voters +fn get_rewards_voting_power( + account: &SnapshotInfo, + representative_group: &str, + direct_voter_group: &str, +) -> Result { + match &account.hir.voting_group { + group if representative_group == group => Ok(account.hir.voting_power.into()), + group if direct_voter_group == group => { + Ok(account.contributions.iter().map(|c| c.value).sum::()) } + other => Err(Error::UnknownVoterGroup(other.into())), + } +} + +fn calculate_active_stake( + snapshot_info: &[SnapshotInfo], + representative_group: VotingGroup, + direct_voter_group: VotingGroup, +) -> Result<(u64, HashMap), Error> { + let mut total_stake: u64 = 0; + let mut stake_per_voter: HashMap = HashMap::new(); + + for voter in snapshot_info { + let vp = get_rewards_voting_power(voter, &representative_group, &direct_voter_group)?; + total_stake = total_stake.checked_add(vp).ok_or(Error::Overflow)?; + let entry = stake_per_voter + .entry(voter.hir.voting_key.clone()) + .or_default(); + *entry += vp; } Ok((total_stake, stake_per_voter)) } -fn calculate_reward<'address>( +fn calculate_reward( total_stake: u64, - stake_per_voter: &HashMap<&'address Address, u64>, - active_addresses: &ActiveAddresses, + stake_per_voter: HashMap, total_rewards: Rewards, -) -> HashMap<&'address Address, Rewards> { +) -> HashMap { stake_per_voter - .iter() + .into_iter() .map(|(k, v)| { - let reward = if active_addresses.contains(k) { - Rewards::from(*v) / Rewards::from(total_stake) * total_rewards - } else { - Rewards::ZERO - }; - (*k, reward) + ( + k, + (Rewards::from(v) / Rewards::from(total_stake) * total_rewards), + ) }) .collect() } pub type VoteCount = HashMap; -pub type ActiveAddresses = HashSet
; -fn active_addresses( +fn filter_active_addresses( vote_count: VoteCount, - block0: &Block0Configuration, threshold: u64, - snapshot: &Snapshot, -) -> ActiveAddresses { - let discrimination = block0.blockchain_configuration.discrimination; - snapshot - .voting_keys() - // Add all keys from snapshot so that they are accounted for - // even if they didn't vote and the threshold is 0. - // Active accounts are overwritten with the correct count. - .map(|key| (key.to_hex(), 0)) - .chain(vote_count.into_iter()) - .filter_map(|(account_hex, count)| { - if count >= threshold { - Some( - account_hex_to_address(account_hex, discrimination) - .expect("Valid hex encoded UnspecifiedAccountIdentifier"), - ) - } else { - None - } + snapshot_info: Vec, +) -> Vec { + snapshot_info + .into_iter() + .filter(|v| { + let addr = v.hir.voting_key.to_hex(); + vote_count.get(&addr).copied().unwrap_or_default() >= threshold }) .collect() } -fn account_hex_to_address( - account_hex: String, - discrimination: Discrimination, -) -> Result { - let mut buffer = [0u8; 32]; - hex::decode_to_slice(account_hex, &mut buffer)?; - let identifier: UnspecifiedAccountIdentifier = UnspecifiedAccountIdentifier::from(buffer); - Ok(Address::from(chain_addr::Address( - discrimination, - Kind::Account( - identifier - .to_single_account() - .expect("Only single accounts are supported") - .into(), - ), - ))) -} - fn rewards_to_mainnet_addresses( - rewards: HashMap<&'_ Address, Rewards>, - snapshot: Snapshot, + rewards: HashMap, + voters: Vec, ) -> BTreeMap { let mut res = BTreeMap::new(); + let snapshot_info_by_key = voters + .into_iter() + .map(|v| (v.hir.voting_key.clone(), v)) + .collect::>(); for (addr, reward) in rewards { - let contributions = snapshot.contributions_for_voting_key::( - addr.1 - .public_key() - .expect("non account address") - .clone() - .into(), - ); + let contributions = snapshot_info_by_key + .get(&addr) + .map(|v| v.contributions.clone()) + .unwrap_or_default(); let total_value = contributions .iter() .map(|c| Rewards::from(c.value)) @@ -148,33 +117,24 @@ fn rewards_to_mainnet_addresses( pub fn calc_voter_rewards( vote_count: VoteCount, vote_threshold: u64, - block0: &Block0Configuration, - snapshot: Snapshot, + voters: Vec, total_rewards: Rewards, + representative_group: VotingGroup, + direct_voter_group: VotingGroup, ) -> Result, Error> { - let active_addresses = active_addresses(vote_count, block0, vote_threshold, &snapshot); - - let committee_keys: HashSet
= block0 - .blockchain_configuration - .committees + let unique_voters = voters .iter() - .cloned() - .map(|id| { - let id = CommitteeId::from(id); - let pk = id.public_key(); + .map(|s| s.hir.voting_key.clone()) + .collect::>(); + if unique_voters.len() != voters.len() { + return Err(Error::MultipleEntries); + } + let active_addresses = filter_active_addresses(vote_count, vote_threshold, voters); - chain_addr::Address(Discrimination::Production, Kind::Account(pk)).into() - }) - .collect(); let (total_active_stake, stake_per_voter) = - calculate_active_stake(&committee_keys, block0, &active_addresses)?; - let rewards = calculate_reward( - total_active_stake, - &stake_per_voter, - &active_addresses, - total_rewards, - ); - Ok(rewards_to_mainnet_addresses(rewards, snapshot)) + calculate_active_stake(&active_addresses, representative_group, direct_voter_group)?; + let rewards = calculate_reward(total_active_stake, stake_per_voter, total_rewards); + Ok(rewards_to_mainnet_addresses(rewards, active_addresses)) } #[cfg(test)] @@ -183,42 +143,10 @@ mod tests { use crate::snapshot::registration::*; use crate::snapshot::*; use crate::utils::assert_are_close; - use chain_impl_mockchain::chaintypes::ConsensusVersion; - use chain_impl_mockchain::fee::LinearFee; - use chain_impl_mockchain::tokens::{identifier::TokenIdentifier, name::TokenName}; use fraction::Fraction; use jormungandr_lib::crypto::account::Identifier; - use jormungandr_lib::interfaces::BlockchainConfiguration; - use jormungandr_lib::interfaces::{Destination, Initial, InitialToken, InitialUTxO}; - use std::convert::TryFrom; use test_strategy::proptest; - fn blockchain_configuration(initial_funds: Vec) -> Block0Configuration { - Block0Configuration { - blockchain_configuration: BlockchainConfiguration::new( - Discrimination::Test, - ConsensusVersion::Bft, - LinearFee::new(1, 1, 1), - ), - // Temporarily create dummy until we update the snapshot - initial: vec![Initial::Token(InitialToken { - token_id: TokenIdentifier { - policy_hash: [0; 28].into(), - token_name: TokenName::try_from(Vec::new()).unwrap(), - } - .into(), - policy: Default::default(), - to: initial_funds - .into_iter() - .map(|utxo| Destination { - address: utxo.address, - value: utxo.value, - }) - .collect(), - })], - } - } - #[proptest] fn test_all_active(snapshot: Snapshot) { let votes_count = snapshot @@ -227,9 +155,16 @@ mod tests { .map(|key| (key.to_hex(), 1)) .collect::(); let n_voters = votes_count.len(); - let initial = snapshot.to_block0_initials(Discrimination::Test); - let block0 = blockchain_configuration(initial); - let rewards = calc_voter_rewards(votes_count, 1, &block0, snapshot, Rewards::ONE).unwrap(); + let voters = snapshot.to_full_snapshot_info(); + let rewards = calc_voter_rewards( + votes_count, + 1, + voters, + Rewards::ONE, + String::new(), + String::new(), + ) + .unwrap(); if n_voters > 0 { assert_are_close(rewards.values().sum::(), Rewards::ONE) } else { @@ -240,9 +175,16 @@ mod tests { #[proptest] fn test_all_inactive(snapshot: Snapshot) { let votes_count = VoteCount::new(); - let initial = snapshot.to_block0_initials(Discrimination::Test); - let block0 = blockchain_configuration(initial); - let rewards = calc_voter_rewards(votes_count, 1, &block0, snapshot, Rewards::ONE).unwrap(); + let voters = snapshot.to_full_snapshot_info(); + let rewards = calc_voter_rewards( + votes_count, + 1, + voters, + Rewards::ONE, + String::new(), + String::new(), + ) + .unwrap(); assert_eq!(rewards.len(), 0); } @@ -256,31 +198,31 @@ mod tests { .map(|(i, key)| (key.to_hex(), (i % 2 == 0) as u64)) .collect::(); let n_voters = votes_count.iter().filter(|(_, votes)| **votes > 0).count(); - let initial = snapshot.to_block0_initials(Discrimination::Test); - let initial_active = initial - .iter() - .cloned() + let voters = snapshot.to_full_snapshot_info(); + let voters_active = voters + .clone() + .into_iter() .enumerate() .filter(|(i, _utxo)| i % 2 == 0) .map(|(_, utxo)| utxo) .collect::>(); - let block0 = blockchain_configuration(initial); - let block0_active = blockchain_configuration(initial_active); let mut rewards = calc_voter_rewards( votes_count.clone(), 1, - &block0, - snapshot.clone(), + voters, Rewards::ONE, + String::new(), + String::new(), ) .unwrap(); let rewards_no_inactive = calc_voter_rewards( votes_count, 1, - &block0_active, - snapshot.clone(), + voters_active, Rewards::ONE, + String::new(), + String::new(), ) .unwrap(); // Rewards should ignore inactive voters @@ -347,11 +289,65 @@ mod tests { ) .unwrap(); - let initial = snapshot.to_block0_initials(Discrimination::Test); - let block0 = blockchain_configuration(initial); + let voters = snapshot.to_full_snapshot_info(); + + let rewards = calc_voter_rewards( + VoteCount::new(), + 0, + voters, + Rewards::ONE, + String::from("rep"), + String::new(), + ) + .unwrap(); + assert_eq!(rewards.values().sum::(), Rewards::ONE); + for (addr, reward) in rewards { + assert_eq!( + reward, + addr.parse::().unwrap() / Rewards::from(total_stake) + ); + } + } + + #[test] + fn test_direct_voters_cap_ignored() { + let mut raw_snapshot = Vec::new(); + + let mut total_stake = 0u64; + for i in 1..10u64 { + let voting_pub_key = Identifier::from_hex(&hex::encode([i as u8; 32])).unwrap(); + let stake_public_key = i.to_string(); + let reward_address = i.to_string(); + let delegations = Delegations::New(vec![(voting_pub_key.clone(), 1)]); + raw_snapshot.push(VotingRegistration { + stake_public_key, + voting_power: i.into(), + reward_address, + delegations, + voting_purpose: 0, + }); + total_stake += i; + } + + let snapshot = Snapshot::from_raw_snapshot( + raw_snapshot.into(), + 0.into(), + Fraction::new(1u64, 9u64), + &|_vk: &Identifier| String::new(), + ) + .unwrap(); + + let voters = snapshot.to_full_snapshot_info(); - let rewards = - calc_voter_rewards(VoteCount::new(), 0, &block0, snapshot, Rewards::ONE).unwrap(); + let rewards = calc_voter_rewards( + VoteCount::new(), + 0, + voters, + Rewards::ONE, + String::from("rep"), + String::new(), + ) + .unwrap(); assert_eq!(rewards.values().sum::(), Rewards::ONE); for (addr, reward) in rewards { assert_eq!( diff --git a/catalyst-toolbox/src/snapshot/influence_cap.rs b/catalyst-toolbox/src/snapshot/influence_cap.rs index 3782e2fa..c4dc4fcc 100644 --- a/catalyst-toolbox/src/snapshot/influence_cap.rs +++ b/catalyst-toolbox/src/snapshot/influence_cap.rs @@ -1,4 +1,4 @@ -use super::{Error, VoterEntry}; +use super::{Error, SnapshotInfo}; use fraction::{BigFraction, Fraction}; use rust_decimal::prelude::ToPrimitive; @@ -46,9 +46,9 @@ fn calc_vp_to_remove(x: u64, tot: u64, threshold: Fraction) -> u64 { /// /// Complexity: O(NlogN + min(ceil(1/T), N)) pub fn cap_voting_influence( - mut voters: Vec, + mut voters: Vec, threshold: Fraction, -) -> Result, Error> { +) -> Result, Error> { voters = voters .into_iter() .filter(|v| v.hir.voting_power > 0.into()) @@ -120,7 +120,7 @@ mod tests { #[proptest] fn test_insufficient_voters( - #[strategy(vec(any::(), 1..100))] voters: Vec, + #[strategy(vec(any::(), 1..100))] voters: Vec, ) { let cap = Fraction::new(1u64, voters.len() as u64 + 1); assert!(cap_voting_influence(voters, cap).is_err()); @@ -128,8 +128,8 @@ mod tests { #[proptest] fn test_exact_voters( - #[strategy(vec(any_with::((Default::default(), DEFAULT_VP_STRATEGY)), 1..100))] - voters: Vec, + #[strategy(vec(any_with::((Default::default(), DEFAULT_VP_STRATEGY)), 1..100))] + voters: Vec, ) { let cap = Fraction::new(1u64, voters.len() as u64); let min = voters.iter().map(|v| v.hir.voting_power).min().unwrap(); @@ -142,8 +142,8 @@ mod tests { #[proptest] fn test_exact_voters_fixed_at_threshold( - #[strategy(vec(any_with::((Default::default(), DEFAULT_VP_STRATEGY)), 101..=101))] - voters: Vec, + #[strategy(vec(any_with::((Default::default(), DEFAULT_VP_STRATEGY)), 101..=101))] + voters: Vec, ) { let cap = Fraction::new(999u64, 100000u64); let res = cap_voting_influence(voters, cap).unwrap(); @@ -159,8 +159,8 @@ mod tests { #[proptest] fn test_exact_voters_fixed_below_threshold( - #[strategy(vec(any_with::((Default::default(), DEFAULT_VP_STRATEGY)), 100..=100))] - voters: Vec, + #[strategy(vec(any_with::((Default::default(), DEFAULT_VP_STRATEGY)), 100..=100))] + voters: Vec, ) { let cap = Fraction::new(9u64, 1000u64); assert!(cap_voting_influence(voters, cap).is_err()); @@ -168,8 +168,8 @@ mod tests { #[proptest] fn test_below_threshold( - #[strategy(vec(any_with::((Default::default(), DEFAULT_VP_STRATEGY)), 100..300))] - voters: Vec, + #[strategy(vec(any_with::((Default::default(), DEFAULT_VP_STRATEGY)), 100..300))] + voters: Vec, ) { let cap = Fraction::new(1u64, 100u64); let res = cap_voting_influence(voters, cap).unwrap(); @@ -183,7 +183,7 @@ mod tests { } } - impl Arbitrary for VoterEntry { + impl Arbitrary for SnapshotInfo { type Parameters = (String, VpRange); type Strategy = BoxedStrategy; diff --git a/catalyst-toolbox/src/snapshot/mod.rs b/catalyst-toolbox/src/snapshot/mod.rs index 3c90e493..33c646f5 100644 --- a/catalyst-toolbox/src/snapshot/mod.rs +++ b/catalyst-toolbox/src/snapshot/mod.rs @@ -7,7 +7,7 @@ use voting_group::VotingGroupAssigner; use fraction::Fraction; use jormungandr_lib::{crypto::account::Identifier, interfaces::Value}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::{borrow::Borrow, collections::BTreeMap, iter::Iterator, num::NonZeroU64}; use thiserror::Error; use voting_hir::VoterHIR; @@ -32,23 +32,27 @@ pub enum Error { } /// Contribution to a voting key for some registration -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct KeyContribution { pub reward_address: MainnetRewardAddress, pub value: u64, } -#[derive(Clone, Debug, PartialEq)] -pub struct VoterEntry { - contributions: Vec, - hir: VoterHIR, +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct SnapshotInfo { + /// The values in the contributions are the original values in the registration transactions and + /// thus retain the original proportions. + /// However, it's possible that the sum of those values is greater than the voting power assigned in the + /// VoterHIR, due to voting power caps or additional transformations. + pub contributions: Vec, + pub hir: VoterHIR, } #[derive(Clone, Debug, PartialEq)] pub struct Snapshot { // a raw public key is preferred so that we don't have to worry about discrimination when deserializing from // a CIP-36 compatible encoding - inner: BTreeMap, + inner: BTreeMap, stake_threshold: Value, } @@ -114,7 +118,7 @@ impl Snapshot { }); let entries = raw_contribs .into_iter() - .map(|(k, contributions)| VoterEntry { + .map(|(k, contributions)| SnapshotInfo { hir: VoterHIR { voting_group: voting_group_assigner.assign(&k), voting_key: k, @@ -123,6 +127,7 @@ impl Snapshot { contributions, }) .collect(); + dbg!(&entries); Ok(Self { inner: Self::apply_voting_power_cap(entries, cap)? .into_iter() @@ -133,9 +138,9 @@ impl Snapshot { } fn apply_voting_power_cap( - voters: Vec, + voters: Vec, cap: Fraction, - ) -> Result, Error> { + ) -> Result, Error> { Ok(influence_cap::cap_voting_influence(voters, cap)? .into_iter() .collect()) @@ -152,6 +157,10 @@ impl Snapshot { .collect::>() } + pub fn to_full_snapshot_info(&self) -> Vec { + self.inner.values().cloned().collect() + } + pub fn voting_keys(&self) -> impl Iterator { self.inner.keys() } From c54afaba6a95d71d5a21ad941fd521055e724dd4 Mon Sep 17 00:00:00 2001 From: Giacomo Pasini Date: Wed, 25 May 2022 10:36:56 +0200 Subject: [PATCH 4/7] add test for representatives --- catalyst-toolbox/src/rewards/voters.rs | 43 ++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/catalyst-toolbox/src/rewards/voters.rs b/catalyst-toolbox/src/rewards/voters.rs index 04eb5757..c0b6a62b 100644 --- a/catalyst-toolbox/src/rewards/voters.rs +++ b/catalyst-toolbox/src/rewards/voters.rs @@ -356,4 +356,47 @@ mod tests { ); } } + + #[test] + fn test_representatives_rewards_cap() { + let mut raw_snapshot = Vec::new(); + + for i in 1..10u64 { + let voting_pub_key = Identifier::from_hex(&hex::encode([i as u8; 32])).unwrap(); + let stake_public_key = i.to_string(); + let reward_address = i.to_string(); + let delegations = Delegations::New(vec![(voting_pub_key.clone(), 1)]); + raw_snapshot.push(VotingRegistration { + stake_public_key, + voting_power: i.into(), + reward_address, + delegations, + voting_purpose: 0, + }); + } + + let snapshot = Snapshot::from_raw_snapshot( + raw_snapshot.into(), + 0.into(), + Fraction::new(1u64, 9u64), + &|_vk: &Identifier| String::new(), + ) + .unwrap(); + + let voters = snapshot.to_full_snapshot_info(); + + let rewards = calc_voter_rewards( + VoteCount::new(), + 0, + voters, + Rewards::ONE, + String::new(), + String::from("direct"), + ) + .unwrap(); + assert_are_close(rewards.values().sum::(), Rewards::ONE); + for (_, reward) in rewards { + assert_eq!(reward, Rewards::ONE / Rewards::from(9)); + } + } } From 4ec9c608c66270f282073380ac3570ba1e47f7f6 Mon Sep 17 00:00:00 2001 From: Giacomo Pasini Date: Wed, 25 May 2022 10:43:43 +0200 Subject: [PATCH 5/7] add missing cli options --- catalyst-toolbox/src/bin/cli/rewards/voters.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/catalyst-toolbox/src/bin/cli/rewards/voters.rs b/catalyst-toolbox/src/bin/cli/rewards/voters.rs index a6aaa5ad..fc9073d6 100644 --- a/catalyst-toolbox/src/bin/cli/rewards/voters.rs +++ b/catalyst-toolbox/src/bin/cli/rewards/voters.rs @@ -30,8 +30,12 @@ pub struct VotersRewards { #[structopt(long, default_value)] vote_threshold: u64, + /// Voting group used for direct voters + #[structopt(long)] direct_voter_group: VotingGroup, + /// Voting group used for representatives + #[structopt(long)] representative_group: VotingGroup, } From 2754ab4cde951c83e4a45234f7b4f2cb4e08844b Mon Sep 17 00:00:00 2001 From: Giacomo Pasini Date: Wed, 25 May 2022 17:38:45 +0200 Subject: [PATCH 6/7] simplify direct voter rewards --- .../src/bin/cli/rewards/voters.rs | 12 -- catalyst-toolbox/src/rewards/voters.rs | 161 ++---------------- 2 files changed, 17 insertions(+), 156 deletions(-) diff --git a/catalyst-toolbox/src/bin/cli/rewards/voters.rs b/catalyst-toolbox/src/bin/cli/rewards/voters.rs index fc9073d6..2ac9de48 100644 --- a/catalyst-toolbox/src/bin/cli/rewards/voters.rs +++ b/catalyst-toolbox/src/bin/cli/rewards/voters.rs @@ -29,14 +29,6 @@ pub struct VotersRewards { /// Number of votes required to be able to receive voter rewards #[structopt(long, default_value)] vote_threshold: u64, - - /// Voting group used for direct voters - #[structopt(long)] - direct_voter_group: VotingGroup, - - /// Voting group used for representatives - #[structopt(long)] - representative_group: VotingGroup, } fn write_rewards_results( @@ -64,8 +56,6 @@ impl VotersRewards { snapshot_info_path, votes_count_path, vote_threshold, - direct_voter_group, - representative_group, } = self; let vote_count: VoteCount = serde_json::from_reader(jcli_lib::utils::io::open_file_read( @@ -81,8 +71,6 @@ impl VotersRewards { vote_threshold, snapshot, Rewards::from(total_rewards), - representative_group, - direct_voter_group, )?; let actual_rewards = results.values().sum::(); diff --git a/catalyst-toolbox/src/rewards/voters.rs b/catalyst-toolbox/src/rewards/voters.rs index c0b6a62b..a9e24e6d 100644 --- a/catalyst-toolbox/src/rewards/voters.rs +++ b/catalyst-toolbox/src/rewards/voters.rs @@ -3,7 +3,6 @@ use jormungandr_lib::crypto::account::Identifier; use rust_decimal::Decimal; use std::collections::{BTreeMap, HashMap, HashSet}; use thiserror::Error; -use voting_hir::VotingGroup; pub const ADA_TO_LOVELACE_FACTOR: u64 = 1_000_000; pub type Rewards = Decimal; @@ -18,42 +17,6 @@ pub enum Error { UnknownVoterGroup(String), } -// Get the voting power used in rewards calculation: -// * actual voting power for representatives -// * voting power without cap for direct voters -fn get_rewards_voting_power( - account: &SnapshotInfo, - representative_group: &str, - direct_voter_group: &str, -) -> Result { - match &account.hir.voting_group { - group if representative_group == group => Ok(account.hir.voting_power.into()), - group if direct_voter_group == group => { - Ok(account.contributions.iter().map(|c| c.value).sum::()) - } - other => Err(Error::UnknownVoterGroup(other.into())), - } -} - -fn calculate_active_stake( - snapshot_info: &[SnapshotInfo], - representative_group: VotingGroup, - direct_voter_group: VotingGroup, -) -> Result<(u64, HashMap), Error> { - let mut total_stake: u64 = 0; - let mut stake_per_voter: HashMap = HashMap::new(); - - for voter in snapshot_info { - let vp = get_rewards_voting_power(voter, &representative_group, &direct_voter_group)?; - total_stake = total_stake.checked_add(vp).ok_or(Error::Overflow)?; - let entry = stake_per_voter - .entry(voter.hir.voting_key.clone()) - .or_default(); - *entry += vp; - } - Ok((total_stake, stake_per_voter)) -} - fn calculate_reward( total_stake: u64, stake_per_voter: HashMap, @@ -119,8 +82,6 @@ pub fn calc_voter_rewards( vote_threshold: u64, voters: Vec, total_rewards: Rewards, - representative_group: VotingGroup, - direct_voter_group: VotingGroup, ) -> Result, Error> { let unique_voters = voters .iter() @@ -131,8 +92,15 @@ pub fn calc_voter_rewards( } let active_addresses = filter_active_addresses(vote_count, vote_threshold, voters); - let (total_active_stake, stake_per_voter) = - calculate_active_stake(&active_addresses, representative_group, direct_voter_group)?; + let mut total_active_stake = 0u64; + let mut stake_per_voter = HashMap::new(); + // iterative as Iterator::sum() panic on overflows + for voter in &active_addresses { + total_active_stake = total_active_stake + .checked_add(voter.hir.voting_power.into()) + .ok_or(Error::Overflow)?; + stake_per_voter.insert(voter.hir.voting_key.clone(), voter.hir.voting_power.into()); + } let rewards = calculate_reward(total_active_stake, stake_per_voter, total_rewards); Ok(rewards_to_mainnet_addresses(rewards, active_addresses)) } @@ -156,15 +124,7 @@ mod tests { .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, - String::new(), - String::new(), - ) - .unwrap(); + let rewards = calc_voter_rewards(votes_count, 1, voters, Rewards::ONE).unwrap(); if n_voters > 0 { assert_are_close(rewards.values().sum::(), Rewards::ONE) } else { @@ -176,15 +136,7 @@ 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, - String::new(), - String::new(), - ) - .unwrap(); + let rewards = calc_voter_rewards(votes_count, 1, voters, Rewards::ONE).unwrap(); assert_eq!(rewards.len(), 0); } @@ -207,24 +159,9 @@ mod tests { .map(|(_, utxo)| utxo) .collect::>(); - let mut rewards = calc_voter_rewards( - votes_count.clone(), - 1, - voters, - Rewards::ONE, - String::new(), - String::new(), - ) - .unwrap(); - let rewards_no_inactive = calc_voter_rewards( - votes_count, - 1, - voters_active, - Rewards::ONE, - String::new(), - String::new(), - ) - .unwrap(); + 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(); // Rewards should ignore inactive voters assert_eq!(rewards, rewards_no_inactive); if n_voters > 0 { @@ -291,15 +228,7 @@ mod tests { let voters = snapshot.to_full_snapshot_info(); - let rewards = calc_voter_rewards( - VoteCount::new(), - 0, - voters, - Rewards::ONE, - String::from("rep"), - String::new(), - ) - .unwrap(); + let rewards = calc_voter_rewards(VoteCount::new(), 0, voters, Rewards::ONE).unwrap(); assert_eq!(rewards.values().sum::(), Rewards::ONE); for (addr, reward) in rewards { assert_eq!( @@ -310,10 +239,9 @@ mod tests { } #[test] - fn test_direct_voters_cap_ignored() { + fn test_rewards_cap() { let mut raw_snapshot = Vec::new(); - let mut total_stake = 0u64; for i in 1..10u64 { let voting_pub_key = Identifier::from_hex(&hex::encode([i as u8; 32])).unwrap(); let stake_public_key = i.to_string(); @@ -326,7 +254,6 @@ mod tests { delegations, voting_purpose: 0, }); - total_stake += i; } let snapshot = Snapshot::from_raw_snapshot( @@ -339,61 +266,7 @@ mod tests { let voters = snapshot.to_full_snapshot_info(); - let rewards = calc_voter_rewards( - VoteCount::new(), - 0, - voters, - Rewards::ONE, - String::from("rep"), - String::new(), - ) - .unwrap(); - assert_eq!(rewards.values().sum::(), Rewards::ONE); - for (addr, reward) in rewards { - assert_eq!( - reward, - addr.parse::().unwrap() / Rewards::from(total_stake) - ); - } - } - - #[test] - fn test_representatives_rewards_cap() { - let mut raw_snapshot = Vec::new(); - - for i in 1..10u64 { - let voting_pub_key = Identifier::from_hex(&hex::encode([i as u8; 32])).unwrap(); - let stake_public_key = i.to_string(); - let reward_address = i.to_string(); - let delegations = Delegations::New(vec![(voting_pub_key.clone(), 1)]); - raw_snapshot.push(VotingRegistration { - stake_public_key, - voting_power: i.into(), - reward_address, - delegations, - voting_purpose: 0, - }); - } - - let snapshot = Snapshot::from_raw_snapshot( - raw_snapshot.into(), - 0.into(), - Fraction::new(1u64, 9u64), - &|_vk: &Identifier| String::new(), - ) - .unwrap(); - - let voters = snapshot.to_full_snapshot_info(); - - let rewards = calc_voter_rewards( - VoteCount::new(), - 0, - voters, - Rewards::ONE, - String::new(), - String::from("direct"), - ) - .unwrap(); + let rewards = calc_voter_rewards(VoteCount::new(), 0, voters, Rewards::ONE).unwrap(); assert_are_close(rewards.values().sum::(), Rewards::ONE); for (_, reward) in rewards { assert_eq!(reward, Rewards::ONE / Rewards::from(9)); From 1a3b8fa31e19e45834cc1f60e9208d0965bfdb93 Mon Sep 17 00:00:00 2001 From: Giacomo Pasini Date: Wed, 1 Jun 2022 12:21:44 +0200 Subject: [PATCH 7/7] remove unused import --- catalyst-toolbox/src/bin/cli/rewards/voters.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/catalyst-toolbox/src/bin/cli/rewards/voters.rs b/catalyst-toolbox/src/bin/cli/rewards/voters.rs index 2ac9de48..1894746c 100644 --- a/catalyst-toolbox/src/bin/cli/rewards/voters.rs +++ b/catalyst-toolbox/src/bin/cli/rewards/voters.rs @@ -1,7 +1,6 @@ use catalyst_toolbox::rewards::voters::{calc_voter_rewards, Rewards, VoteCount}; use catalyst_toolbox::snapshot::{registration::MainnetRewardAddress, SnapshotInfo}; use catalyst_toolbox::utils::assert_are_close; -use voting_hir::VotingGroup; use color_eyre::Report; use jcli_lib::jcli_lib::block::Common;