diff --git a/.changelog/unreleased/improvements/1865-eth-voting-power.md b/.changelog/unreleased/improvements/1865-eth-voting-power.md new file mode 100644 index 0000000000..d9fd126c21 --- /dev/null +++ b/.changelog/unreleased/improvements/1865-eth-voting-power.md @@ -0,0 +1,2 @@ +- Rework voting on Ethereum tallies across epoch boundaries + ([\#1865](https://github.com/anoma/namada/pull/1865)) \ No newline at end of file diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 3201684e0f..0ee60b4326 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -190,9 +190,13 @@ impl Amount { denom: impl Into, ) -> Result { let denom = denom.into(); + let uint = uint.into(); + if denom == 0 { + return Ok(Self { raw: uint }); + } match Uint::from(10) .checked_pow(Uint::from(denom)) - .and_then(|scaling| scaling.checked_mul(uint.into())) + .and_then(|scaling| scaling.checked_mul(uint)) { Some(amount) => Ok(Self { raw: amount }), None => Err(AmountParseError::ConvertToDecimal), diff --git a/core/src/types/voting_power.rs b/core/src/types/voting_power.rs index 946e08b834..ab1358358c 100644 --- a/core/src/types/voting_power.rs +++ b/core/src/types/voting_power.rs @@ -12,6 +12,7 @@ use num_traits::ops::checked::CheckedAdd; use serde::de::Visitor; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use crate::types::token::Amount; use crate::types::uint::Uint; /// Namada voting power, normalized to the range `0 - 2^32`. @@ -170,6 +171,24 @@ impl Mul<&FractionalVotingPower> for FractionalVotingPower { } } +impl Mul for FractionalVotingPower { + type Output = Amount; + + fn mul(self, rhs: Amount) -> Self::Output { + self * &rhs + } +} + +impl Mul<&Amount> for FractionalVotingPower { + type Output = Amount; + + fn mul(self, &rhs: &Amount) -> Self::Output { + let whole: Uint = rhs.into(); + let fraction = (self.0 * whole).to_integer(); + Amount::from_uint(fraction, 0u8).unwrap() + } +} + impl Add for FractionalVotingPower { type Output = Self; diff --git a/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs b/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs index 8571881453..0c3ba5c34c 100644 --- a/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs +++ b/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs @@ -7,9 +7,9 @@ use namada_core::ledger::storage::{DBIter, StorageHasher, WlStorage, DB}; use namada_core::ledger::storage_api::StorageWrite; use namada_core::types::address::Address; use namada_core::types::storage::BlockHeight; +use namada_core::types::token::Amount; use namada_core::types::transaction::TxResult; use namada_core::types::vote_extensions::bridge_pool_roots::MultiSignedVext; -use namada_core::types::voting_power::FractionalVotingPower; use namada_proof_of_stake::pos_queries::PosQueries; use crate::protocol::transactions::utils::GetVoters; @@ -140,7 +140,7 @@ fn apply_update( wl_storage: &mut WlStorage, mut update: BridgePoolRoot, seen_by: Votes, - voting_powers: &HashMap<(Address, BlockHeight), FractionalVotingPower>, + voting_powers: &HashMap<(Address, BlockHeight), Amount>, ) -> Result<(ChangedKeys, bool)> where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, @@ -196,17 +196,13 @@ mod test_apply_bp_roots_to_storage { use namada_core::ledger::storage_api::StorageRead; use namada_core::proto::{SignableEthMessage, Signed}; use namada_core::types::address; - use namada_core::types::dec::Dec; use namada_core::types::ethereum_events::Uint; use namada_core::types::keccak::{keccak_hash, KeccakHash}; - use namada_core::types::key::RefTo; use namada_core::types::storage::Key; - use namada_core::types::token::Amount; use namada_core::types::vote_extensions::bridge_pool_roots; + use namada_core::types::voting_power::FractionalVotingPower; use namada_proof_of_stake::parameters::PosParams; - use namada_proof_of_stake::{ - become_validator, bond_tokens, write_pos_params, BecomeValidator, - }; + use namada_proof_of_stake::write_pos_params; use super::*; use crate::protocol::transactions::votes::{ @@ -435,7 +431,7 @@ mod test_apply_bp_roots_to_storage { .read::(&bp_root_key.voting_power()) .expect("Test failed") .expect("Test failed") - .average_voting_power(&wl_storage); + .fractional_stake(&wl_storage); assert_eq!( voting_power, FractionalVotingPower::new_u64(5, 12).unwrap() @@ -454,7 +450,7 @@ mod test_apply_bp_roots_to_storage { .read::(&bp_root_key.voting_power()) .expect("Test failed") .expect("Test failed") - .average_voting_power(&wl_storage); + .fractional_stake(&wl_storage); assert_eq!(voting_power, FractionalVotingPower::new_u64(5, 6).unwrap()); } @@ -720,32 +716,16 @@ mod test_apply_bp_roots_to_storage { pipeline_len: 1, ..Default::default() }; - write_pos_params(&mut wl_storage, params.clone()).expect("Test failed"); + write_pos_params(&mut wl_storage, params).expect("Test failed"); // insert validators 2 and 3 at epoch 1 - for (validator, stake) in [ - (&validator_2, validator_2_stake), - (&validator_3, validator_3_stake), - ] { - let keys = test_utils::TestValidatorKeys::generate(); - let consensus_key = &keys.consensus.ref_to(); - let eth_cold_key = &keys.eth_gov.ref_to(); - let eth_hot_key = &keys.eth_bridge.ref_to(); - become_validator(BecomeValidator { - storage: &mut wl_storage, - params: ¶ms, - address: validator, - consensus_key, - eth_cold_key, - eth_hot_key, - current_epoch: 0.into(), - commission_rate: Dec::new(5, 2).unwrap(), - max_commission_rate_change: Dec::new(1, 2).unwrap(), - }) - .expect("Test failed"); - bond_tokens(&mut wl_storage, None, validator, stake, 0.into()) - .expect("Test failed"); - } + test_utils::append_validators_to_storage( + &mut wl_storage, + HashMap::from([ + (validator_2.clone(), validator_2_stake), + (validator_3.clone(), validator_3_stake), + ]), + ); // query validators to make sure they were inserted correctly macro_rules! query_validators { @@ -770,6 +750,12 @@ mod test_apply_bp_roots_to_storage { epoch_0_validators, HashMap::from([(validator_1.clone(), validator_1_stake)]) ); + assert_eq!( + wl_storage + .pos_queries() + .get_total_voting_power(Some(0.into())), + validator_1_stake, + ); assert_eq!( epoch_1_validators, HashMap::from([ @@ -778,6 +764,12 @@ mod test_apply_bp_roots_to_storage { (validator_3, validator_3_stake), ]) ); + assert_eq!( + wl_storage + .pos_queries() + .get_total_voting_power(Some(1.into())), + validator_1_stake + validator_2_stake + validator_3_stake, + ); // set up the bridge pool's storage bridge_pool_vp::init_storage(&mut wl_storage); diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs index 97fa999fb4..ab2988d3d5 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs @@ -13,9 +13,9 @@ use namada_core::ledger::storage::{DBIter, WlStorage, DB}; use namada_core::types::address::Address; use namada_core::types::ethereum_events::EthereumEvent; use namada_core::types::storage::{BlockHeight, Epoch, Key}; +use namada_core::types::token::Amount; use namada_core::types::transaction::TxResult; use namada_core::types::vote_extensions::ethereum_events::MultiSignedEthEvent; -use namada_core::types::voting_power::FractionalVotingPower; use namada_proof_of_stake::pos_queries::PosQueries; use super::ChangedKeys; @@ -86,7 +86,7 @@ where pub(super) fn apply_updates( wl_storage: &mut WlStorage, updates: HashSet, - voting_powers: HashMap<(Address, BlockHeight), FractionalVotingPower>, + voting_powers: HashMap<(Address, BlockHeight), Amount>, ) -> Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, @@ -133,7 +133,7 @@ where fn apply_update( wl_storage: &mut WlStorage, update: EthMsgUpdate, - voting_powers: &HashMap<(Address, BlockHeight), FractionalVotingPower>, + voting_powers: &HashMap<(Address, BlockHeight), Amount>, ) -> Result<(ChangedKeys, bool)> where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, @@ -284,7 +284,8 @@ mod tests { use namada_core::types::ethereum_events::{ EthereumEvent, TransferToNamada, }; - use namada_core::types::token::{balance_key, minted_balance_key, Amount}; + use namada_core::types::token::{balance_key, minted_balance_key}; + use namada_core::types::voting_power::FractionalVotingPower; use super::*; use crate::protocol::transactions::utils::GetVoters; @@ -305,7 +306,7 @@ mod tests { #[test] /// Test applying a `TransfersToNamada` batch containing a single transfer fn test_apply_single_transfer() -> Result<()> { - let sole_validator = address::testing::gen_established_address(); + let (sole_validator, validator_stake) = test_utils::default_validator(); let receiver = address::testing::established_address_2(); let amount = arbitrary_amount(); @@ -326,10 +327,9 @@ mod tests { let updates = HashSet::from_iter(vec![update]); let voting_powers = HashMap::from_iter(vec![( (sole_validator.clone(), BlockHeight(100)), - FractionalVotingPower::WHOLE, + validator_stake, )]); - let mut wl_storage = TestWlStorage::default(); - test_utils::bootstrap_ethereum_bridge(&mut wl_storage); + let (mut wl_storage, _) = test_utils::setup_default_storage(); test_utils::whitelist_tokens( &mut wl_storage, [( @@ -377,7 +377,7 @@ mod tests { let voting_power = wl_storage .read::(ð_msg_keys.voting_power())? .expect("Test failed") - .average_voting_power(&wl_storage); + .fractional_stake(&wl_storage); assert_eq!(voting_power, FractionalVotingPower::WHOLE); let epoch_bytes = @@ -414,7 +414,6 @@ mod tests { test_utils::setup_storage_with_validators(HashMap::from_iter( vec![(sole_validator.clone(), Amount::native_whole(100))], )); - test_utils::bootstrap_ethereum_bridge(&mut wl_storage); test_utils::whitelist_tokens( &mut wl_storage, [( @@ -488,7 +487,6 @@ mod tests { (validator_b, Amount::native_whole(100)), ]), ); - test_utils::bootstrap_ethereum_bridge(&mut wl_storage); let receiver = address::testing::established_address_1(); let event = EthereumEvent::TransfersToNamada { @@ -590,7 +588,7 @@ mod tests { let voting_power = wl_storage .read::(ð_msg_keys.voting_power())? .expect("Test failed") - .average_voting_power(&wl_storage); + .fractional_stake(&wl_storage); assert_eq!(voting_power, FractionalVotingPower::HALF); Ok(()) @@ -664,7 +662,6 @@ mod tests { (validator_b, Amount::native_whole(100)), ]), ); - test_utils::bootstrap_ethereum_bridge(&mut wl_storage); let receiver = address::testing::established_address_1(); let event = EthereumEvent::TransfersToNamada { @@ -793,7 +790,6 @@ mod tests { (validator_b.clone(), Amount::native_whole(100)), ]), ); - test_utils::bootstrap_ethereum_bridge(&mut wl_storage); let receiver = address::testing::established_address_1(); let event = EthereumEvent::TransfersToNamada { @@ -821,7 +817,7 @@ mod tests { (KeyKind::VotingPower, Some(power)) => { let power = EpochedVotingPower::try_from_slice(&power) .expect("Test failed") - .average_voting_power(&wl_storage); + .fractional_stake(&wl_storage); assert_eq!(power, FractionalVotingPower::HALF); } (_, Some(_)) => {} @@ -851,7 +847,7 @@ mod tests { (KeyKind::VotingPower, Some(power)) => { let power = EpochedVotingPower::try_from_slice(&power) .expect("Test failed") - .average_voting_power(&wl_storage); + .fractional_stake(&wl_storage); assert_eq!(power, FractionalVotingPower::HALF); } (_, Some(_)) => {} diff --git a/ethereum_bridge/src/protocol/transactions/utils.rs b/ethereum_bridge/src/protocol/transactions/utils.rs index 1f06c46a30..d2f44c995e 100644 --- a/ethereum_bridge/src/protocol/transactions/utils.rs +++ b/ethereum_bridge/src/protocol/transactions/utils.rs @@ -6,7 +6,6 @@ use namada_core::ledger::storage::{DBIter, StorageHasher, WlStorage, DB}; use namada_core::types::address::Address; use namada_core::types::storage::BlockHeight; use namada_core::types::token; -use namada_core::types::voting_power::FractionalVotingPower; use namada_proof_of_stake::pos_queries::PosQueries; use namada_proof_of_stake::types::WeightedValidator; @@ -25,7 +24,7 @@ pub(super) trait GetVoters { pub(super) fn get_voting_powers( wl_storage: &WlStorage, proof: P, -) -> eyre::Result> +) -> eyre::Result> where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, @@ -85,15 +84,13 @@ where pub(super) fn get_voting_powers_for_selected( all_consensus: &BTreeMap>, selected: HashSet<(Address, BlockHeight)>, -) -> eyre::Result> { - let total_voting_powers = - sum_voting_powers_for_block_heights(all_consensus); +) -> eyre::Result> { let voting_powers = selected .into_iter() .map( |(addr, height)| -> eyre::Result<( (Address, BlockHeight), - FractionalVotingPower, + token::Amount, )> { let consensus_validators = all_consensus.get(&height).ok_or_else(|| { @@ -101,7 +98,7 @@ pub(super) fn get_voting_powers_for_selected( "No consensus validators found for height {height}" ) })?; - let individual_voting_power = consensus_validators + let voting_power = consensus_validators .iter() .find(|&v| v.address == addr) .ok_or_else(|| { @@ -111,21 +108,9 @@ pub(super) fn get_voting_powers_for_selected( ) })? .bonded_stake; - let total_voting_power = total_voting_powers - .get(&height) - .ok_or_else(|| { - eyre!( - "No total voting power provided for height \ - {height}" - ) - })? - .to_owned(); Ok(( (addr, height), - FractionalVotingPower::new( - individual_voting_power.into(), - total_voting_power.into(), - )?, + voting_power, )) }, ) @@ -133,24 +118,6 @@ pub(super) fn get_voting_powers_for_selected( Ok(voting_powers) } -pub(super) fn sum_voting_powers_for_block_heights( - validators: &BTreeMap>, -) -> BTreeMap { - validators - .iter() - .map(|(h, vs)| (h.to_owned(), sum_voting_powers(vs))) - .collect() -} - -pub(super) fn sum_voting_powers( - validators: &BTreeSet, -) -> token::Amount { - validators - .iter() - .map(|validator| validator.bonded_stake) - .sum::() -} - #[cfg(test)] mod tests { use std::collections::HashSet; @@ -158,6 +125,7 @@ mod tests { use assert_matches::assert_matches; use namada_core::types::address; use namada_core::types::ethereum_events::testing::arbitrary_bonded_stake; + use namada_core::types::voting_power::FractionalVotingPower; use super::*; @@ -190,7 +158,7 @@ mod tests { assert_eq!(voting_powers.len(), 1); assert_matches!( voting_powers.get(&(sole_validator, BlockHeight(100))), - Some(v) if *v == FractionalVotingPower::WHOLE + Some(v) if *v == bonded_stake ); } @@ -263,6 +231,7 @@ mod tests { weighted_validator_2, ]), )]); + let bonded_stake = bonded_stake_1 + bonded_stake_2; let result = get_voting_powers_for_selected(&consensus_validators, validators); @@ -272,56 +241,17 @@ mod tests { Err(error) => panic!("error: {:?}", error), }; assert_eq!(voting_powers.len(), 2); + let expected_stake = + FractionalVotingPower::new_u64(100, 300).unwrap() * bonded_stake; assert_matches!( voting_powers.get(&(validator_1, BlockHeight(100))), - Some(v) if *v == FractionalVotingPower::new_u64(100, 300).unwrap() + Some(v) if *v == expected_stake ); + let expected_stake = + FractionalVotingPower::new_u64(200, 300).unwrap() * bonded_stake; assert_matches!( voting_powers.get(&(validator_2, BlockHeight(100))), - Some(v) if *v == FractionalVotingPower::new_u64(200, 300).unwrap() + Some(v) if *v == expected_stake ); } - - #[test] - /// Test summing the voting powers for a set of validators containing only - /// one validator - fn test_sum_voting_powers_sole_validator() { - let sole_validator = address::testing::established_address_1(); - let bonded_stake = arbitrary_bonded_stake(); - let weighted_sole_validator = WeightedValidator { - bonded_stake, - address: sole_validator, - }; - let validators = BTreeSet::from_iter(vec![weighted_sole_validator]); - - let total = sum_voting_powers(&validators); - - assert_eq!(total, bonded_stake); - } - - #[test] - /// Test summing the voting powers for a set of validators containing two - /// validators - fn test_sum_voting_powers_two_validators() { - let validator_1 = address::testing::established_address_1(); - let validator_2 = address::testing::established_address_2(); - let bonded_stake_1 = token::Amount::from(100); - let bonded_stake_2 = token::Amount::from(200); - let weighted_validator_1 = WeightedValidator { - bonded_stake: bonded_stake_1, - address: validator_1, - }; - let weighted_validator_2 = WeightedValidator { - bonded_stake: bonded_stake_2, - address: validator_2, - }; - let validators = BTreeSet::from_iter(vec![ - weighted_validator_1, - weighted_validator_2, - ]); - - let total = sum_voting_powers(&validators); - - assert_eq!(total, token::Amount::from(300)); - } } diff --git a/ethereum_bridge/src/protocol/transactions/validator_set_update/mod.rs b/ethereum_bridge/src/protocol/transactions/validator_set_update/mod.rs index 63b6627d9c..8457b82c4f 100644 --- a/ethereum_bridge/src/protocol/transactions/validator_set_update/mod.rs +++ b/ethereum_bridge/src/protocol/transactions/validator_set_update/mod.rs @@ -6,11 +6,11 @@ use eyre::Result; use namada_core::ledger::storage::{DBIter, StorageHasher, WlStorage, DB}; use namada_core::types::address::Address; use namada_core::types::storage::{BlockHeight, Epoch}; +use namada_core::types::token::Amount; #[allow(unused_imports)] use namada_core::types::transaction::protocol::ProtocolTxType; use namada_core::types::transaction::TxResult; use namada_core::types::vote_extensions::validator_set_update; -use namada_core::types::voting_power::FractionalVotingPower; use super::ChangedKeys; use crate::protocol::transactions::utils; @@ -85,7 +85,7 @@ fn apply_update( ext: validator_set_update::VextDigest, signing_epoch: Epoch, epoch_2nd_height: BlockHeight, - voting_powers: HashMap<(Address, BlockHeight), FractionalVotingPower>, + voting_powers: HashMap<(Address, BlockHeight), Amount>, ) -> Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, @@ -199,7 +199,6 @@ where #[cfg(test)] mod test_valset_upd_state_changes { use namada_core::types::address; - use namada_core::types::token::Amount; use namada_core::types::vote_extensions::validator_set_update::VotingPowersMap; use namada_core::types::voting_power::FractionalVotingPower; use namada_proof_of_stake::pos_queries::PosQueries; diff --git a/ethereum_bridge/src/protocol/transactions/votes.rs b/ethereum_bridge/src/protocol/transactions/votes.rs index 5cf8fa4e3d..c3a82bd370 100644 --- a/ethereum_bridge/src/protocol/transactions/votes.rs +++ b/ethereum_bridge/src/protocol/transactions/votes.rs @@ -5,7 +5,6 @@ use std::collections::{BTreeMap, BTreeSet, HashMap}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use eyre::{eyre, Result}; -use namada_core::hints; use namada_core::ledger::storage::{DBIter, StorageHasher, WlStorage, DB}; use namada_core::types::address::Address; use namada_core::types::storage::{BlockHeight, Epoch}; @@ -27,31 +26,48 @@ pub(super) mod update; pub type Votes = BTreeMap; /// The voting power behind a tally aggregated over multiple epochs. -pub type EpochedVotingPower = BTreeMap; +pub type EpochedVotingPower = BTreeMap; /// Extension methods for [`EpochedVotingPower`] instances. pub trait EpochedVotingPowerExt { - /// Get the total voting power staked across all epochs - /// in this [`EpochedVotingPower`]. - fn get_epoch_voting_powers( + /// Query the stake of the most secure [`Epoch`] referenced by an + /// [`EpochedVotingPower`]. This translates to the [`Epoch`] with + /// the most staked tokens. + fn epoch_max_voting_power( &self, wl_storage: &WlStorage, - ) -> HashMap + ) -> Option where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync; - /// Get the weighted average of some tally's voting powers pertaining to all - /// epochs it was held in. - fn average_voting_power( + /// Fetch the sum of the stake tallied on an + /// [`EpochedVotingPower`]. + fn tallied_stake(&self) -> token::Amount; + + /// Fetch the sum of the stake tallied on an + /// [`EpochedVotingPower`], as a fraction over + /// the maximum stake seen in the epochs voted on. + #[inline] + fn fractional_stake( &self, wl_storage: &WlStorage, ) -> FractionalVotingPower where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync; + H: 'static + StorageHasher + Sync, + { + let Some(max_voting_power) = self.epoch_max_voting_power(wl_storage) else { + return FractionalVotingPower::NULL; + }; + FractionalVotingPower::new( + self.tallied_stake().into(), + max_voting_power.into(), + ) + .unwrap() + } - /// Check if the [`Tally`] associated with this [`EpochedVotingPower`] + /// Check if the [`Tally`] associated with an [`EpochedVotingPower`] /// can be considered `seen`. #[inline] fn has_majority_quorum(&self, wl_storage: &WlStorage) -> bool @@ -59,16 +75,29 @@ pub trait EpochedVotingPowerExt { D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { - self.average_voting_power(wl_storage) - > FractionalVotingPower::TWO_THIRDS + let Some(max_voting_power) = self.epoch_max_voting_power(wl_storage) else { + return false; + }; + // NB: Preserve the safety property of the Tendermint protocol across + // all the epochs we vote on. + // + // PROOF: We calculate the maximum amount of tokens S_max staked on + // one of the epochs the tally occurred in. At most F = 1/3 * S_max + // of the combined stake can be Byzantine, for the protocol to uphold + // its linearizability property whilst remaining "secure" against + // arbitrarily faulty nodes. Therefore, we can consider a tally secure + // if has accumulated an amount of stake greater than the threshold + // stake of S_max - F = 2/3 S_max. + let threshold = FractionalVotingPower::TWO_THIRDS * max_voting_power; + self.tallied_stake() > threshold } } impl EpochedVotingPowerExt for EpochedVotingPower { - fn get_epoch_voting_powers( + fn epoch_max_voting_power( &self, wl_storage: &WlStorage, - ) -> HashMap + ) -> Option where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, @@ -76,57 +105,13 @@ impl EpochedVotingPowerExt for EpochedVotingPower { self.keys() .copied() .map(|epoch| { - ( - epoch, - wl_storage - .pos_queries() - .get_total_voting_power(Some(epoch)), - ) + wl_storage.pos_queries().get_total_voting_power(Some(epoch)) }) - .collect() + .max() } - fn average_voting_power( - &self, - wl_storage: &WlStorage, - ) -> FractionalVotingPower - where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, - { - // if we only voted across a single epoch, we can avoid doing - // expensive I/O operations - if hints::likely(self.len() == 1) { - // TODO: switch to [`BTreeMap::first_entry`] when we start - // using Rust >= 1.66 - let Some(&power) = self.values().next() else { - hints::cold(); - unreachable!("The map has one value"); - }; - return power; - } - - let epoch_voting_powers = self.get_epoch_voting_powers(wl_storage); - let total_voting_power = epoch_voting_powers - .values() - .fold(token::Amount::from(0u64), |accum, &stake| accum + stake); - - self.iter().map(|(&epoch, &power)| (epoch, power)).fold( - FractionalVotingPower::NULL, - |average, (epoch, aggregated_voting_power)| { - let epoch_voting_power = epoch_voting_powers - .get(&epoch) - .copied() - .expect("This value should be in the map"); - debug_assert!(epoch_voting_power > 0.into()); - let weight = FractionalVotingPower::new( - epoch_voting_power.into(), - total_voting_power.into(), - ) - .unwrap(); - average + weight * aggregated_voting_power - }, - ) + fn tallied_stake(&self) -> token::Amount { + self.values().copied().sum::() } } @@ -153,7 +138,7 @@ pub struct Tally { pub fn calculate_new( wl_storage: &WlStorage, seen_by: Votes, - voting_powers: &HashMap<(Address, BlockHeight), FractionalVotingPower>, + voting_powers: &HashMap<(Address, BlockHeight), token::Amount>, ) -> Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, @@ -164,14 +149,14 @@ where match voting_powers .get(&(validator.to_owned(), block_height.to_owned())) { - Some(voting_power) => { + Some(&voting_power) => { let epoch = wl_storage .pos_queries() .get_epoch(*block_height) .expect("The queried epoch should be known"); let aggregated = seen_by_voting_power .entry(epoch) - .or_insert(FractionalVotingPower::NULL); + .or_insert_with(token::Amount::zero); *aggregated += voting_power; } None => { @@ -202,15 +187,10 @@ pub fn dedupe(signers: BTreeSet<(Address, BlockHeight)>) -> Votes { mod tests { use std::collections::BTreeSet; - use namada_core::ledger::storage::testing::TestWlStorage; - use namada_core::types::dec::Dec; - use namada_core::types::key::RefTo; use namada_core::types::storage::BlockHeight; use namada_core::types::{address, token}; use namada_proof_of_stake::parameters::PosParams; - use namada_proof_of_stake::{ - become_validator, bond_tokens, write_pos_params, BecomeValidator, - }; + use namada_proof_of_stake::write_pos_params; use super::*; use crate::test_utils; @@ -305,18 +285,21 @@ mod tests { /// fast path of the algorithm. #[test] fn test_tally_vote_single_epoch() { - let dummy_storage = TestWlStorage::default(); + let (_, dummy_validator_stake) = test_utils::default_validator(); + let (dummy_storage, _) = test_utils::setup_default_storage(); - let aggregated = - EpochedVotingPower::from([(0.into(), FractionalVotingPower::HALF)]); + let aggregated = EpochedVotingPower::from([( + 0.into(), + FractionalVotingPower::HALF * dummy_validator_stake, + )]); assert_eq!( - aggregated.average_voting_power(&dummy_storage), + aggregated.fractional_stake(&dummy_storage), FractionalVotingPower::HALF ); } /// Test that voting on a tally across epoch boundaries accounts - /// for the average voting power attained along those epochs. + /// for the maximum voting power attained along those epochs. #[test] fn test_voting_across_epoch_boundaries() { // the validators that will vote in the tally @@ -329,6 +312,9 @@ mod tests { let validator_3 = address::testing::established_address_3(); let validator_3_stake = token::Amount::native_whole(100); + let total_stake = + validator_1_stake + validator_2_stake + validator_3_stake; + // start epoch 0 with validator 1 let (mut wl_storage, _) = test_utils::setup_storage_with_validators( HashMap::from([(validator_1.clone(), validator_1_stake)]), @@ -339,32 +325,16 @@ mod tests { pipeline_len: 1, ..Default::default() }; - write_pos_params(&mut wl_storage, params.clone()).expect("Test failed"); + write_pos_params(&mut wl_storage, params).expect("Test failed"); // insert validators 2 and 3 at epoch 1 - for (validator, stake) in [ - (&validator_2, validator_2_stake), - (&validator_3, validator_3_stake), - ] { - let keys = test_utils::TestValidatorKeys::generate(); - let consensus_key = &keys.consensus.ref_to(); - let eth_cold_key = &keys.eth_gov.ref_to(); - let eth_hot_key = &keys.eth_bridge.ref_to(); - become_validator(BecomeValidator { - storage: &mut wl_storage, - params: ¶ms, - address: validator, - consensus_key, - eth_cold_key, - eth_hot_key, - current_epoch: 0.into(), - commission_rate: Dec::new(5, 2).unwrap(), - max_commission_rate_change: Dec::new(1, 2).unwrap(), - }) - .expect("Test failed"); - bond_tokens(&mut wl_storage, None, validator, stake, 0.into()) - .expect("Test failed"); - } + test_utils::append_validators_to_storage( + &mut wl_storage, + HashMap::from([ + (validator_2.clone(), validator_2_stake), + (validator_3.clone(), validator_3_stake), + ]), + ); // query validators to make sure they were inserted correctly let query_validators = |epoch: u64| { @@ -381,6 +351,12 @@ mod tests { epoch_0_validators, HashMap::from([(validator_1.clone(), validator_1_stake)]) ); + assert_eq!( + wl_storage + .pos_queries() + .get_total_voting_power(Some(0.into())), + validator_1_stake, + ); assert_eq!( epoch_1_validators, HashMap::from([ @@ -389,15 +365,21 @@ mod tests { (validator_3, validator_3_stake), ]) ); + assert_eq!( + wl_storage + .pos_queries() + .get_total_voting_power(Some(1.into())), + total_stake, + ); // check that voting works as expected let aggregated = EpochedVotingPower::from([ - (0.into(), FractionalVotingPower::ONE_THIRD), - (1.into(), FractionalVotingPower::ONE_THIRD), + (0.into(), FractionalVotingPower::ONE_THIRD * total_stake), + (1.into(), FractionalVotingPower::ONE_THIRD * total_stake), ]); assert_eq!( - aggregated.average_voting_power(&wl_storage), - FractionalVotingPower::ONE_THIRD + aggregated.fractional_stake(&wl_storage), + FractionalVotingPower::TWO_THIRDS ); } } diff --git a/ethereum_bridge/src/protocol/transactions/votes/storage.rs b/ethereum_bridge/src/protocol/transactions/votes/storage.rs index 4f6d107bb2..bd9f45dcee 100644 --- a/ethereum_bridge/src/protocol/transactions/votes/storage.rs +++ b/ethereum_bridge/src/protocol/transactions/votes/storage.rs @@ -116,16 +116,16 @@ where mod tests { use std::collections::BTreeMap; - use namada_core::ledger::storage::testing::TestWlStorage; - use namada_core::types::address; use namada_core::types::ethereum_events::EthereumEvent; - use namada_core::types::voting_power::FractionalVotingPower; use super::*; + use crate::test_utils; #[test] fn test_write_tally() { - let mut wl_storage = TestWlStorage::default(); + let (mut wl_storage, _) = test_utils::setup_default_storage(); + let (validator, validator_voting_power) = + test_utils::default_validator(); let event = EthereumEvent::TransfersToNamada { nonce: 0.into(), transfers: vec![], @@ -135,12 +135,9 @@ mod tests { let tally = Tally { voting_power: EpochedVotingPower::from([( 0.into(), - FractionalVotingPower::ONE_THIRD, - )]), - seen_by: BTreeMap::from([( - address::testing::established_address_1(), - 10.into(), + validator_voting_power, )]), + seen_by: BTreeMap::from([(validator, 10.into())]), seen: false, }; @@ -175,7 +172,9 @@ mod tests { #[test] fn test_read_tally() { - let mut wl_storage = TestWlStorage::default(); + let (mut wl_storage, _) = test_utils::setup_default_storage(); + let (validator, validator_voting_power) = + test_utils::default_validator(); let event = EthereumEvent::TransfersToNamada { nonce: 0.into(), transfers: vec![], @@ -185,12 +184,9 @@ mod tests { let tally = Tally { voting_power: EpochedVotingPower::from([( 0.into(), - FractionalVotingPower::ONE_THIRD, - )]), - seen_by: BTreeMap::from([( - address::testing::established_address_1(), - 10.into(), + validator_voting_power, )]), + seen_by: BTreeMap::from([(validator, 10.into())]), seen: false, }; wl_storage diff --git a/ethereum_bridge/src/protocol/transactions/votes/update.rs b/ethereum_bridge/src/protocol/transactions/votes/update.rs index 369290ef6b..9b20be92ea 100644 --- a/ethereum_bridge/src/protocol/transactions/votes/update.rs +++ b/ethereum_bridge/src/protocol/transactions/votes/update.rs @@ -5,7 +5,7 @@ use eyre::{eyre, Result}; use namada_core::ledger::storage::{DBIter, StorageHasher, WlStorage, DB}; use namada_core::types::address::Address; use namada_core::types::storage::BlockHeight; -use namada_core::types::voting_power::FractionalVotingPower; +use namada_core::types::token; use namada_proof_of_stake::pos_queries::PosQueries; use super::{ChangedKeys, EpochedVotingPowerExt, Tally, Votes}; @@ -14,34 +14,33 @@ use crate::storage::vote_tallies; /// Wraps all the information about new votes to be applied to some existing /// tally in storage. pub(in super::super) struct NewVotes { - inner: HashMap, + inner: HashMap, } impl NewVotes { /// Constructs a new [`NewVotes`]. /// - /// For all `votes` provided, a corresponding [`FractionalVotingPower`] must + /// For all `votes` provided, a corresponding [`token::Amount`] must /// be provided in `voting_powers` also, otherwise an error will be /// returned. pub fn new( votes: Votes, - voting_powers: &HashMap<(Address, BlockHeight), FractionalVotingPower>, + voting_powers: &HashMap<(Address, BlockHeight), token::Amount>, ) -> Result { let mut inner = HashMap::default(); for vote in votes { - let fract_voting_power = match voting_powers.get(&vote) { - Some(fract_voting_power) => fract_voting_power, + let voting_power = match voting_powers.get(&vote) { + Some(voting_power) => voting_power, None => { let (address, block_height) = vote; return Err(eyre!( - "No fractional voting power provided for vote by \ - validator {address} at block height {block_height}" + "No voting power provided for vote by validator \ + {address} at block height {block_height}" )); } }; let (address, block_height) = vote; - _ = inner - .insert(address, (block_height, fract_voting_power.to_owned())); + _ = inner.insert(address, (block_height, voting_power.to_owned())); } Ok(Self { inner }) } @@ -71,14 +70,14 @@ impl NewVotes { impl IntoIterator for NewVotes { type IntoIter = std::collections::hash_set::IntoIter; - type Item = (Address, BlockHeight, FractionalVotingPower); + type Item = (Address, BlockHeight, token::Amount); fn into_iter(self) -> Self::IntoIter { let items: HashSet<_> = self .inner .into_iter() - .map(|(address, (block_height, fract_voting_power))| { - (address, block_height, fract_voting_power) + .map(|(address, (block_height, stake))| { + (address, block_height, stake) }) .collect(); items.into_iter() @@ -174,7 +173,7 @@ where .expect("The queried epoch should be known"); let aggregated = voting_power_post .entry(epoch) - .or_insert(FractionalVotingPower::NULL); + .or_insert_with(token::Amount::zero); *aggregated += voting_power; } @@ -213,19 +212,27 @@ mod tests { use namada_core::ledger::storage::testing::TestWlStorage; use namada_core::types::address; use namada_core::types::ethereum_events::EthereumEvent; + use namada_core::types::voting_power::FractionalVotingPower; + use self::helpers::{default_event, default_total_stake, TallyParams}; use super::*; - use crate::protocol::transactions::votes::update::tests::helpers::{ - arbitrary_event, setup_tally, - }; use crate::protocol::transactions::votes::{self, EpochedVotingPower}; + use crate::test_utils; mod helpers { + use namada_proof_of_stake::total_consensus_stake_key_handle; + use super::*; + /// Default amount of staked NAM to be used in tests. + pub(super) fn default_total_stake() -> token::Amount { + // 1000 NAM + token::Amount::native_whole(1_000) + } + /// Returns an arbitrary piece of data that can have votes tallied /// against it. - pub(super) fn arbitrary_event() -> EthereumEvent { + pub(super) fn default_event() -> EthereumEvent { EthereumEvent::TransfersToNamada { nonce: 0.into(), transfers: vec![], @@ -233,22 +240,53 @@ mod tests { } } - /// Writes an initial [`Tally`] to storage, based on the passed `votes`. - pub(super) fn setup_tally( - wl_storage: &mut TestWlStorage, - event: &EthereumEvent, - keys: &vote_tallies::Keys, - votes: HashSet<(Address, BlockHeight, FractionalVotingPower)>, - ) -> Result { - let voting_power: FractionalVotingPower = - votes.iter().cloned().map(|(_, _, v)| v).sum(); - let tally = Tally { - voting_power: get_epoched_voting_power(voting_power.to_owned()), - seen_by: votes.into_iter().map(|(a, h, _)| (a, h)).collect(), - seen: voting_power > FractionalVotingPower::TWO_THIRDS, - }; - votes::storage::write(wl_storage, keys, event, &tally, false)?; - Ok(tally) + /// Parameters to construct a test [`Tally`]. + pub(super) struct TallyParams<'a> { + /// Handle to storage. + pub wl_storage: &'a mut TestWlStorage, + /// The event to be voted on. + pub event: &'a EthereumEvent, + /// Votes from the given validators at the given block height. + /// + /// The voting power of each validator is expressed as a fraction + /// of the provided `total_stake` parameter. + pub votes: HashSet<(Address, BlockHeight, token::Amount)>, + /// The [`token::Amount`] staked at epoch 0. + pub total_stake: token::Amount, + } + + impl TallyParams<'_> { + /// Write an initial [`Tally`] to storage. + pub(super) fn setup(self) -> Result { + let Self { + wl_storage, + event, + votes, + total_stake, + } = self; + let keys = vote_tallies::Keys::from(event); + let seen_voting_power: token::Amount = votes + .iter() + .map(|(_, _, voting_power)| *voting_power) + .sum(); + let tally = Tally { + voting_power: get_epoched_voting_power(seen_voting_power), + seen_by: votes + .into_iter() + .map(|(addr, height, _)| (addr, height)) + .collect(), + seen: seen_voting_power + > FractionalVotingPower::TWO_THIRDS * total_stake, + }; + votes::storage::write(wl_storage, &keys, event, &tally, false)?; + total_consensus_stake_key_handle().set( + wl_storage, + total_stake, + 0u64.into(), + 0, + )?; + Ok(tally) + } } } @@ -267,7 +305,8 @@ mod tests { fn test_vote_info_new_single_voter() -> Result<()> { let validator = address::testing::established_address_1(); let vote_height = BlockHeight(100); - let voting_power = FractionalVotingPower::ONE_THIRD; + let voting_power = + FractionalVotingPower::ONE_THIRD * default_total_stake(); let vote = (validator.clone(), vote_height); let votes = Votes::from([vote.clone()]); let voting_powers = HashMap::from([(vote, voting_power)]); @@ -278,7 +317,7 @@ mod tests { let votes: BTreeSet<_> = vote_info.into_iter().collect(); assert_eq!( votes, - BTreeSet::from([(validator, vote_height, voting_power,)]), + BTreeSet::from([(validator, vote_height, voting_power)]), ); Ok(()) } @@ -301,7 +340,8 @@ mod tests { fn test_vote_info_without_voters() -> Result<()> { let validator = address::testing::established_address_1(); let vote_height = BlockHeight(100); - let voting_power = FractionalVotingPower::ONE_THIRD; + let voting_power = + FractionalVotingPower::ONE_THIRD * default_total_stake(); let vote = (validator.clone(), vote_height); let votes = Votes::from([vote.clone()]); let voting_powers = HashMap::from([(vote, voting_power)]); @@ -319,7 +359,8 @@ mod tests { let validator = address::testing::established_address_1(); let new_validator = address::testing::established_address_2(); let vote_height = BlockHeight(100); - let voting_power = FractionalVotingPower::ONE_THIRD; + let voting_power = + FractionalVotingPower::ONE_THIRD * default_total_stake(); let vote = (validator.clone(), vote_height); let votes = Votes::from([vote.clone()]); let voting_powers = HashMap::from([(vote, voting_power)]); @@ -340,23 +381,23 @@ mod tests { let validator = address::testing::established_address_1(); let already_voted_height = BlockHeight(100); - let event = arbitrary_event(); - let keys = vote_tallies::Keys::from(&event); - let tally_pre = setup_tally( - &mut wl_storage, - &event, - &keys, - HashSet::from([( + let event = default_event(); + let tally_pre = TallyParams { + total_stake: default_total_stake(), + wl_storage: &mut wl_storage, + event: &event, + votes: HashSet::from([( validator.clone(), already_voted_height, - FractionalVotingPower::ONE_THIRD, + FractionalVotingPower::ONE_THIRD * default_total_stake(), )]), - )?; + } + .setup()?; let votes = Votes::from([(validator.clone(), BlockHeight(1000))]); let voting_powers = HashMap::from([( (validator, BlockHeight(1000)), - FractionalVotingPower::ONE_THIRD, + FractionalVotingPower::ONE_THIRD * default_total_stake(), )]); let vote_info = NewVotes::new(votes, &voting_powers)?; @@ -371,22 +412,25 @@ mod tests { #[test] fn test_calculate_already_seen() -> Result<()> { let mut wl_storage = TestWlStorage::default(); - let event = arbitrary_event(); + let event = default_event(); let keys = vote_tallies::Keys::from(&event); - let tally_pre = setup_tally( - &mut wl_storage, - &event, - &keys, - HashSet::from([( + let tally_pre = TallyParams { + total_stake: default_total_stake(), + wl_storage: &mut wl_storage, + event: &event, + votes: HashSet::from([( address::testing::established_address_1(), BlockHeight(10), - FractionalVotingPower::new_u64(3, 4)?, // this is > 2/3 + // this is > 2/3 + FractionalVotingPower::new_u64(3, 4)? * default_total_stake(), )]), - )?; + } + .setup()?; let validator = address::testing::established_address_2(); let vote_height = BlockHeight(100); - let voting_power = FractionalVotingPower::ONE_THIRD; + let voting_power = + FractionalVotingPower::new_u64(1, 4)? * default_total_stake(); let vote = (validator, vote_height); let votes = Votes::from([vote.clone()]); let voting_powers = HashMap::from([(vote, voting_power)]); @@ -403,19 +447,20 @@ mod tests { /// Tests that an unchanged tally is returned if no votes are passed. #[test] fn test_calculate_empty() -> Result<()> { - let mut wl_storage = TestWlStorage::default(); - let event = arbitrary_event(); + let (mut wl_storage, _) = test_utils::setup_default_storage(); + let event = default_event(); let keys = vote_tallies::Keys::from(&event); - let tally_pre = setup_tally( - &mut wl_storage, - &event, - &keys, - HashSet::from([( + let tally_pre = TallyParams { + total_stake: default_total_stake(), + wl_storage: &mut wl_storage, + event: &event, + votes: HashSet::from([( address::testing::established_address_1(), BlockHeight(10), - FractionalVotingPower::ONE_THIRD, + FractionalVotingPower::ONE_THIRD * default_total_stake(), )]), - )?; + } + .setup()?; let vote_info = NewVotes::new(Votes::default(), &HashMap::default())?; let (tally_post, changed_keys) = @@ -430,24 +475,26 @@ mod tests { /// not yet seen. #[test] fn test_calculate_one_vote_not_seen() -> Result<()> { - let mut wl_storage = TestWlStorage::default(); + let (mut wl_storage, _) = test_utils::setup_default_storage(); - let event = arbitrary_event(); + let event = default_event(); let keys = vote_tallies::Keys::from(&event); - let _tally_pre = setup_tally( - &mut wl_storage, - &event, - &keys, - HashSet::from([( + let _tally_pre = TallyParams { + total_stake: default_total_stake(), + wl_storage: &mut wl_storage, + event: &event, + votes: HashSet::from([( address::testing::established_address_1(), BlockHeight(10), - FractionalVotingPower::ONE_THIRD, + FractionalVotingPower::ONE_THIRD * default_total_stake(), )]), - )?; + } + .setup()?; let validator = address::testing::established_address_2(); let vote_height = BlockHeight(100); - let voting_power = FractionalVotingPower::ONE_THIRD; + let voting_power = + FractionalVotingPower::ONE_THIRD * default_total_stake(); let vote = (validator, vote_height); let votes = Votes::from([vote.clone()]); let voting_powers = HashMap::from([(vote.clone(), voting_power)]); @@ -460,7 +507,7 @@ mod tests { tally_post, Tally { voting_power: get_epoched_voting_power( - FractionalVotingPower::TWO_THIRDS, + FractionalVotingPower::TWO_THIRDS * default_total_stake(), ), seen_by: BTreeMap::from([ (address::testing::established_address_1(), 10.into()), @@ -480,27 +527,33 @@ mod tests { /// seen. #[test] fn test_calculate_one_vote_seen() -> Result<()> { - let mut wl_storage = TestWlStorage::default(); + let (mut wl_storage, _) = test_utils::setup_default_storage(); - let event = arbitrary_event(); + let first_vote_stake = + FractionalVotingPower::ONE_THIRD * default_total_stake(); + let second_vote_stake = + FractionalVotingPower::ONE_THIRD * default_total_stake(); + let total_stake = first_vote_stake + second_vote_stake; + + let event = default_event(); let keys = vote_tallies::Keys::from(&event); - let _tally_pre = setup_tally( - &mut wl_storage, - &event, - &keys, - HashSet::from([( + let _tally_pre = TallyParams { + total_stake, + wl_storage: &mut wl_storage, + event: &event, + votes: HashSet::from([( address::testing::established_address_1(), BlockHeight(10), - FractionalVotingPower::ONE_THIRD, + first_vote_stake, )]), - )?; + } + .setup()?; let validator = address::testing::established_address_2(); let vote_height = BlockHeight(100); - let voting_power = FractionalVotingPower::TWO_THIRDS; let vote = (validator, vote_height); let votes = Votes::from([vote.clone()]); - let voting_powers = HashMap::from([(vote.clone(), voting_power)]); + let voting_powers = HashMap::from([(vote.clone(), second_vote_stake)]); let vote_info = NewVotes::new(votes, &voting_powers)?; let (tally_post, changed_keys) = @@ -509,9 +562,7 @@ mod tests { assert_eq!( tally_post, Tally { - voting_power: get_epoched_voting_power( - FractionalVotingPower::WHOLE - ), + voting_power: get_epoched_voting_power(total_stake), seen_by: BTreeMap::from([ (address::testing::established_address_1(), 10.into()), vote, @@ -528,8 +579,10 @@ mod tests { #[test] fn test_keys_changed_all() -> Result<()> { - let voting_power_a = FractionalVotingPower::ONE_THIRD; - let voting_power_b = FractionalVotingPower::TWO_THIRDS; + let voting_power_a = + FractionalVotingPower::ONE_THIRD * default_total_stake(); + let voting_power_b = + FractionalVotingPower::TWO_THIRDS * default_total_stake(); let seen_a = false; let seen_b = true; @@ -543,7 +596,7 @@ mod tests { BlockHeight(20), )]); - let event = arbitrary_event(); + let event = default_event(); let keys = vote_tallies::Keys::from(&event); let pre = Tally { voting_power: get_epoched_voting_power(voting_power_a), @@ -572,11 +625,11 @@ mod tests { BlockHeight(10), )]); - let event = arbitrary_event(); + let event = default_event(); let keys = vote_tallies::Keys::from(&event); let pre = Tally { voting_power: get_epoched_voting_power( - FractionalVotingPower::ONE_THIRD, + FractionalVotingPower::ONE_THIRD * default_total_stake(), ), seen, seen_by, @@ -589,9 +642,7 @@ mod tests { Ok(()) } - fn get_epoched_voting_power( - fraction: FractionalVotingPower, - ) -> EpochedVotingPower { - EpochedVotingPower::from([(0.into(), fraction)]) + fn get_epoched_voting_power(thus_far: token::Amount) -> EpochedVotingPower { + EpochedVotingPower::from([(0.into(), thus_far)]) } } diff --git a/ethereum_bridge/src/test_utils.rs b/ethereum_bridge/src/test_utils.rs index aec6463396..3b7b80febe 100644 --- a/ethereum_bridge/src/test_utils.rs +++ b/ethereum_bridge/src/test_utils.rs @@ -17,7 +17,11 @@ use namada_core::types::key::{self, protocol_pk_key, RefTo}; use namada_core::types::storage::{BlockHeight, Key}; use namada_core::types::token; use namada_proof_of_stake::parameters::PosParams; +use namada_proof_of_stake::pos_queries::PosQueries; use namada_proof_of_stake::types::GenesisValidator; +use namada_proof_of_stake::{ + become_validator, bond_tokens, store_total_consensus_stake, BecomeValidator, +}; use crate::parameters::{ ContractVersion, Contracts, EthereumBridgeConfig, MinimumConfirmations, @@ -69,23 +73,29 @@ pub fn setup_default_storage() (wl_storage, all_keys) } -/// Set up a [`TestWlStorage`] initialized at genesis with a single -/// validator. -/// -/// The validator's address is [`address::testing::established_address_1`]. +/// Set up a [`TestWlStorage`] initialized at genesis with +/// [`default_validator`]. #[inline] pub fn init_default_storage( wl_storage: &mut TestWlStorage, ) -> HashMap { init_storage_with_validators( wl_storage, - HashMap::from_iter([( - address::testing::established_address_1(), - token::Amount::native_whole(100), - )]), + HashMap::from_iter([default_validator()]), ) } +/// Default validator used in tests. +/// +/// The validator's address is [`address::testing::established_address_1`], +/// and its voting power is proportional to the stake of 100 NAM. +#[inline] +pub fn default_validator() -> (Address, token::Amount) { + let addr = address::testing::established_address_1(); + let voting_power = token::Amount::native_whole(100); + (addr, voting_power) +} + /// Writes a dummy [`EthereumBridgeConfig`] to the given [`TestWlStorage`], and /// returns it. pub fn bootstrap_ethereum_bridge( @@ -213,23 +223,7 @@ pub fn init_storage_with_validators( 0.into(), ) .expect("Test failed"); - let config = EthereumBridgeConfig { - erc20_whitelist: vec![], - eth_start_height: Default::default(), - min_confirmations: Default::default(), - contracts: Contracts { - native_erc20: wnam(), - bridge: UpgradeableContract { - address: EthAddress([42; 20]), - version: Default::default(), - }, - governance: UpgradeableContract { - address: EthAddress([18; 20]), - version: Default::default(), - }, - }, - }; - config.init_storage(wl_storage); + bootstrap_ethereum_bridge(wl_storage); for (validator, keys) in all_keys.iter() { let protocol_key = keys.protocol.ref_to(); @@ -261,3 +255,56 @@ pub fn commit_bridge_pool_root_at_height( storage.commit_block(MockDBWriteBatch).unwrap(); storage.block.tree.delete(&get_key_from_hash(root)).unwrap(); } + +/// Append validators to storage at the current epoch +/// offset by pipeline length. +pub fn append_validators_to_storage( + wl_storage: &mut TestWlStorage, + consensus_validators: HashMap, +) -> HashMap { + let current_epoch = wl_storage.storage.get_current_epoch().0; + + let mut all_keys = HashMap::new(); + let params = wl_storage.pos_queries().get_pos_params(); + + for (validator, stake) in consensus_validators { + let keys = TestValidatorKeys::generate(); + + let consensus_key = &keys.consensus.ref_to(); + let eth_cold_key = &keys.eth_gov.ref_to(); + let eth_hot_key = &keys.eth_bridge.ref_to(); + + become_validator(BecomeValidator { + storage: wl_storage, + params: ¶ms, + address: &validator, + consensus_key, + eth_cold_key, + eth_hot_key, + current_epoch, + commission_rate: Dec::new(5, 2).unwrap(), + max_commission_rate_change: Dec::new(1, 2).unwrap(), + }) + .expect("Test failed"); + bond_tokens(wl_storage, None, &validator, stake, current_epoch) + .expect("Test failed"); + + all_keys.insert(validator, keys); + } + + store_total_consensus_stake( + wl_storage, + current_epoch + params.pipeline_len, + ) + .expect("Test failed"); + + for (validator, keys) in all_keys.iter() { + let protocol_key = keys.protocol.ref_to(); + wl_storage + .write(&protocol_pk_key(validator), protocol_key) + .expect("Test failed"); + } + wl_storage.commit_block().expect("Test failed"); + + all_keys +} diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index c18a626268..0fbbf2231b 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -4000,7 +4000,9 @@ where } } -fn get_total_consensus_stake( +/// Find the total amount of tokens staked at the given `epoch`, +/// belonging to the set of consensus validators. +pub fn get_total_consensus_stake( storage: &S, epoch: Epoch, params: &PosParams, diff --git a/proof_of_stake/src/pos_queries.rs b/proof_of_stake/src/pos_queries.rs index 351cf4902c..ce2e25cf39 100644 --- a/proof_of_stake/src/pos_queries.rs +++ b/proof_of_stake/src/pos_queries.rs @@ -18,7 +18,7 @@ use thiserror::Error; use crate::types::WeightedValidator; use crate::{ consensus_validator_set_handle, find_validator_by_raw_hash, - read_pos_params, validator_eth_cold_key_handle, + get_total_consensus_stake, read_pos_params, validator_eth_cold_key_handle, validator_eth_hot_key_handle, ConsensusValidatorSet, PosParams, }; @@ -131,10 +131,14 @@ where /// Lookup the total voting power for an epoch (defaulting to the /// epoch of the current yet-to-be-committed block). pub fn get_total_voting_power(self, epoch: Option) -> token::Amount { - self.get_consensus_validators(epoch) - .iter() - .map(|validator| validator.bonded_stake) - .sum::() + let epoch = epoch + .unwrap_or_else(|| self.wl_storage.storage.get_current_epoch().0); + let pos_params = self.get_pos_params(); + get_total_consensus_stake(self.wl_storage, epoch, &pos_params) + // NB: the only reason this call should fail is if we request + // an epoch that hasn't been reached yet. let's "fail" by + // returning a total stake of 0 NAM + .unwrap_or_default() } /// Return evidence parameters. diff --git a/shared/src/ledger/protocol/mod.rs b/shared/src/ledger/protocol/mod.rs index 2a116e8c8f..f8dab2ede4 100644 --- a/shared/src/ledger/protocol/mod.rs +++ b/shared/src/ledger/protocol/mod.rs @@ -1063,10 +1063,13 @@ mod tests { fn test_apply_protocol_tx_duplicate_eth_events_vext() -> Result<()> { let validator_a = address::testing::established_address_2(); let validator_b = address::testing::established_address_3(); + let validator_a_stake = Amount::native_whole(100); + let validator_b_stake = Amount::native_whole(100); + let total_stake = validator_a_stake + validator_b_stake; let (mut wl_storage, _) = test_utils::setup_storage_with_validators( HashMap::from_iter(vec![ - (validator_a.clone(), Amount::native_whole(100)), - (validator_b, Amount::native_whole(100)), + (validator_a.clone(), validator_a_stake), + (validator_b, validator_b_stake), ]), ); let event = EthereumEvent::TransfersToNamada { @@ -1101,8 +1104,10 @@ mod tests { // the vote should have only be applied once let voting_power: EpochedVotingPower = wl_storage.read(ð_msg_keys.voting_power())?.unwrap(); - let expected = - EpochedVotingPower::from([(0.into(), FractionalVotingPower::HALF)]); + let expected = EpochedVotingPower::from([( + 0.into(), + FractionalVotingPower::HALF * total_stake, + )]); assert_eq!(voting_power, expected); Ok(()) @@ -1115,10 +1120,13 @@ mod tests { fn test_apply_protocol_tx_duplicate_bp_roots_vext() -> Result<()> { let validator_a = address::testing::established_address_2(); let validator_b = address::testing::established_address_3(); + let validator_a_stake = Amount::native_whole(100); + let validator_b_stake = Amount::native_whole(100); + let total_stake = validator_a_stake + validator_b_stake; let (mut wl_storage, keys) = test_utils::setup_storage_with_validators( HashMap::from_iter(vec![ - (validator_a.clone(), Amount::native_whole(100)), - (validator_b, Amount::native_whole(100)), + (validator_a.clone(), validator_a_stake), + (validator_b, validator_b_stake), ]), ); bridge_pool_vp::init_storage(&mut wl_storage); @@ -1157,8 +1165,10 @@ mod tests { // the vote should have only be applied once let voting_power: EpochedVotingPower = wl_storage.read(&bp_root_keys.voting_power())?.unwrap(); - let expected = - EpochedVotingPower::from([(0.into(), FractionalVotingPower::HALF)]); + let expected = EpochedVotingPower::from([( + 0.into(), + FractionalVotingPower::HALF * total_stake, + )]); assert_eq!(voting_power, expected); Ok(()) diff --git a/shared/src/ledger/queries/shell/eth_bridge.rs b/shared/src/ledger/queries/shell/eth_bridge.rs index 2588ef33bd..237a9bf795 100644 --- a/shared/src/ledger/queries/shell/eth_bridge.rs +++ b/shared/src/ledger/queries/shell/eth_bridge.rs @@ -504,7 +504,7 @@ where "Iterating over storage should not yield keys without \ values.", ) - .average_voting_power(ctx.wl_storage); + .fractional_stake(ctx.wl_storage); for transfer in transfers { let key = get_key_from_hash(&transfer.keccak256()); let transfer = ctx @@ -1275,6 +1275,7 @@ mod test_ethbridge_router { }, }; // write validator to storage + let (_, dummy_validator_stake) = test_utils::default_validator(); test_utils::init_default_storage(&mut client.wl_storage); // write a transfer into the bridge pool @@ -1307,9 +1308,12 @@ mod test_ethbridge_router { .wl_storage .write_bytes( ð_msg_key.voting_power(), - EpochedVotingPower::from([(0.into(), voting_power)]) - .try_to_vec() - .expect("Test failed"), + EpochedVotingPower::from([( + 0.into(), + voting_power * dummy_validator_stake, + )]) + .try_to_vec() + .expect("Test failed"), ) .expect("Test failed"); client