From 8925ced43775bb005da6d5013e903422f337778c Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 31 Aug 2023 14:26:57 +0100 Subject: [PATCH 1/9] Make `get_total_consensus_stake` public --- proof_of_stake/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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, From 17cb4906e7b6e430ca562c703a6ec53226bfbec5 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 31 Aug 2023 14:27:29 +0100 Subject: [PATCH 2/9] Write `PosQueries::get_total_voting_power` based on `get_total_consensus_stake` --- proof_of_stake/src/pos_queries.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) 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. From 32bac23d4a15e1190afbe43cd581b6078c67f87a Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 31 Aug 2023 15:36:50 +0100 Subject: [PATCH 3/9] Add eth testing util to append validators to storage --- ethereum_bridge/src/test_utils.rs | 57 +++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/ethereum_bridge/src/test_utils.rs b/ethereum_bridge/src/test_utils.rs index aec6463396..1347d29021 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, @@ -261,3 +265,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 +} From b5f4caf3faead2710eafd7225ffb39723ae0fc54 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 31 Aug 2023 15:37:59 +0100 Subject: [PATCH 4/9] Append validators to storage using new testing util --- .../transactions/bridge_pool_roots.rs | 50 ++++++++----------- .../src/protocol/transactions/votes.rs | 50 ++++++++----------- 2 files changed, 42 insertions(+), 58 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs b/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs index 8571881453..4e6b2b9727 100644 --- a/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs +++ b/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs @@ -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_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::{ @@ -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/votes.rs b/ethereum_bridge/src/protocol/transactions/votes.rs index 5cf8fa4e3d..eabc0cf1f1 100644 --- a/ethereum_bridge/src/protocol/transactions/votes.rs +++ b/ethereum_bridge/src/protocol/transactions/votes.rs @@ -203,14 +203,10 @@ 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; @@ -339,32 +335,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 +361,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,6 +375,12 @@ mod tests { (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, + ); // check that voting works as expected let aggregated = EpochedVotingPower::from([ From f42f6dde1fa2402aee5e3584ab5d3d8da1194e41 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 1 Sep 2023 10:08:45 +0100 Subject: [PATCH 5/9] Convert Uint to Amount with 0 denom --- core/src/types/token.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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), From 62184e70b266aa724c62d3c0d2cced6ab34878f6 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 1 Sep 2023 10:09:04 +0100 Subject: [PATCH 6/9] Multiply Amount with FractionalVotingPower --- core/src/types/voting_power.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/core/src/types/voting_power.rs b/core/src/types/voting_power.rs index 946e08b834..844df5811b 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,27 @@ 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(); + match Amount::from_uint(fraction, 0u8) { + Ok(amount) => amount, + _ => unreachable!(), + } + } +} + impl Add for FractionalVotingPower { type Output = Self; From 561c49aeac855112c38e5733d25ad5709977461d Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 1 Sep 2023 15:24:27 +0100 Subject: [PATCH 7/9] Rework voting across epochs on Ethereum tallies Instead of accounting for votes on Ethereum tallies based on the average voting power available across all epochs the tally took place in, we account for the maximum voting power found across all these epochs. --- .../transactions/bridge_pool_roots.rs | 10 +- .../transactions/ethereum_events/mod.rs | 28 +- .../src/protocol/transactions/utils.rs | 98 +------ .../transactions/validator_set_update/mod.rs | 5 +- .../src/protocol/transactions/votes.rs | 144 +++++----- .../protocol/transactions/votes/storage.rs | 26 +- .../src/protocol/transactions/votes/update.rs | 251 +++++++++++------- ethereum_bridge/src/test_utils.rs | 40 ++- shared/src/ledger/protocol/mod.rs | 26 +- shared/src/ledger/queries/shell/eth_bridge.rs | 12 +- 10 files changed, 303 insertions(+), 337 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs b/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs index 4e6b2b9727..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, @@ -199,8 +199,8 @@ mod test_apply_bp_roots_to_storage { use namada_core::types::ethereum_events::Uint; use namada_core::types::keccak::{keccak_hash, KeccakHash}; 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::write_pos_params; @@ -431,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() @@ -450,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()); } 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 eabc0cf1f1..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,7 +187,6 @@ 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::storage::BlockHeight; use namada_core::types::{address, token}; use namada_proof_of_stake::parameters::PosParams; @@ -301,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 @@ -325,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)]), @@ -379,17 +369,17 @@ mod tests { wl_storage .pos_queries() .get_total_voting_power(Some(1.into())), - validator_1_stake + validator_2_stake + validator_3_stake, + 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 1347d29021..3b7b80febe 100644 --- a/ethereum_bridge/src/test_utils.rs +++ b/ethereum_bridge/src/test_utils.rs @@ -73,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( @@ -217,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(); diff --git a/shared/src/ledger/protocol/mod.rs b/shared/src/ledger/protocol/mod.rs index c6ed0814b1..7b8910e968 100644 --- a/shared/src/ledger/protocol/mod.rs +++ b/shared/src/ledger/protocol/mod.rs @@ -1128,10 +1128,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 { @@ -1166,8 +1169,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(()) @@ -1180,10 +1185,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); @@ -1222,8 +1230,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 From c7c6b9312be4b0a2699036abb5257889dd6367a1 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Mon, 18 Sep 2023 13:07:25 +0100 Subject: [PATCH 8/9] Remove explicit unreachable match arm Co-authored-by: Tomas Zemanovic --- core/src/types/voting_power.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/core/src/types/voting_power.rs b/core/src/types/voting_power.rs index 844df5811b..ab1358358c 100644 --- a/core/src/types/voting_power.rs +++ b/core/src/types/voting_power.rs @@ -185,10 +185,7 @@ impl Mul<&Amount> for FractionalVotingPower { fn mul(self, &rhs: &Amount) -> Self::Output { let whole: Uint = rhs.into(); let fraction = (self.0 * whole).to_integer(); - match Amount::from_uint(fraction, 0u8) { - Ok(amount) => amount, - _ => unreachable!(), - } + Amount::from_uint(fraction, 0u8).unwrap() } } From 8049d74eeff622f25b9bafe6263232a665a677bc Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 5 Sep 2023 13:09:48 +0000 Subject: [PATCH 9/9] Changelog for #1865 --- .changelog/unreleased/improvements/1865-eth-voting-power.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/improvements/1865-eth-voting-power.md 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