diff --git a/.changelog/unreleased/improvements/2253-pos-crate-refactor.md b/.changelog/unreleased/improvements/2253-pos-crate-refactor.md new file mode 100644 index 0000000000..716e2166a5 --- /dev/null +++ b/.changelog/unreleased/improvements/2253-pos-crate-refactor.md @@ -0,0 +1,2 @@ +- Refactor the PoS crate by breaking up the lib and tests code into smaller + files. ([\#2253](https://github.com/anoma/namada/pull/2253)) \ No newline at end of file diff --git a/apps/src/lib/bench_utils.rs b/apps/src/lib/bench_utils.rs index 00d065c6c0..ddfae1a4cc 100644 --- a/apps/src/lib/bench_utils.rs +++ b/apps/src/lib/bench_utils.rs @@ -217,7 +217,8 @@ impl Default for BenchShell { source: Some(defaults::albert_address()), }; let params = - proof_of_stake::read_pos_params(&shell.wl_storage).unwrap(); + proof_of_stake::storage::read_pos_params(&shell.wl_storage) + .unwrap(); let mut bench_shell = BenchShell { inner: shell, tempdir, @@ -398,13 +399,14 @@ impl BenchShell { pub fn advance_epoch(&mut self) { let params = - proof_of_stake::read_pos_params(&self.inner.wl_storage).unwrap(); + proof_of_stake::storage::read_pos_params(&self.inner.wl_storage) + .unwrap(); self.wl_storage.storage.block.epoch = self.wl_storage.storage.block.epoch.next(); let current_epoch = self.wl_storage.storage.block.epoch; - proof_of_stake::copy_validator_sets_and_positions( + proof_of_stake::validator_set_update::copy_validator_sets_and_positions( &mut self.wl_storage, ¶ms, current_epoch, diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 3c6eb3127a..3c35458eeb 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -18,7 +18,7 @@ use namada::ledger::storage::write_log::StorageModification; use namada::ledger::storage::EPOCH_SWITCH_BLOCKS_DELAY; use namada::ledger::storage_api::token::credit_tokens; use namada::ledger::storage_api::{pgf, ResultExt, StorageRead, StorageWrite}; -use namada::proof_of_stake::{ +use namada::proof_of_stake::storage::{ find_validator_by_raw_hash, read_last_block_proposer_address, read_pos_params, read_total_stake, write_last_block_proposer_address, }; @@ -99,7 +99,7 @@ where } let pos_params = - namada_proof_of_stake::read_pos_params(&self.wl_storage)?; + namada_proof_of_stake::storage::read_pos_params(&self.wl_storage)?; if new_epoch { update_allowed_conversions(&mut self.wl_storage)?; @@ -108,7 +108,7 @@ where // Copy the new_epoch + pipeline_len - 1 validator set into // new_epoch + pipeline_len - namada_proof_of_stake::copy_validator_sets_and_positions( + namada_proof_of_stake::validator_set_update::copy_validator_sets_and_positions( &mut self.wl_storage, &pos_params, current_epoch, @@ -729,7 +729,7 @@ where let inflation = token::Amount::from_uint(inflation, 0) .expect("Should not fail Uint -> Amount conversion"); - namada_proof_of_stake::update_rewards_products_and_mint_inflation( + namada_proof_of_stake::rewards::update_rewards_products_and_mint_inflation( &mut self.wl_storage, ¶ms, last_epoch, @@ -854,7 +854,7 @@ where tracing::debug!( "Found last block proposer: {proposer_address}" ); - namada_proof_of_stake::log_block_rewards( + namada_proof_of_stake::rewards::log_block_rewards( &mut self.wl_storage, if new_epoch { current_epoch.prev() @@ -987,19 +987,19 @@ mod test_finalize_block { use namada::ledger::storage_api; use namada::ledger::storage_api::StorageWrite; use namada::proof_of_stake::storage::{ - is_validator_slashes_key, slashes_prefix, - }; - use namada::proof_of_stake::types::{ - BondId, SlashType, ValidatorState, WeightedValidator, - }; - use namada::proof_of_stake::{ enqueued_slashes_handle, get_num_consensus_validators, read_consensus_validator_set_addresses_with_stake, - read_validator_stake, rewards_accumulator_handle, unjail_validator, + read_validator_stake, rewards_accumulator_handle, validator_consensus_key_handle, validator_rewards_products_handle, validator_slashes_handle, validator_state_handle, write_pos_params, - ADDRESS as pos_address, }; + use namada::proof_of_stake::storage_key::{ + is_validator_slashes_key, slashes_prefix, + }; + use namada::proof_of_stake::types::{ + BondId, SlashType, ValidatorState, WeightedValidator, + }; + use namada::proof_of_stake::{unjail_validator, ADDRESS as pos_address}; use namada::proto::{Code, Data, Section, Signature}; use namada::types::dec::POS_DECIMAL_PRECISION; use namada::types::ethereum_events::{EthAddress, Uint as ethUint}; @@ -1020,7 +1020,7 @@ mod test_finalize_block { use namada::types::uint::Uint; use namada::types::vote_extensions::ethereum_events; use namada_sdk::eth_bridge::MinimumConfirmations; - use namada_sdk::proof_of_stake::{ + use namada_sdk::proof_of_stake::storage::{ liveness_missed_votes_handle, liveness_sum_missed_votes_handle, read_consensus_validator_set_addresses, }; @@ -1824,12 +1824,15 @@ mod test_finalize_block { // Keep applying finalize block let validator = shell.mode.get_validator_address().unwrap(); let pos_params = - namada_proof_of_stake::read_pos_params(&shell.wl_storage).unwrap(); - let consensus_key = - namada_proof_of_stake::validator_consensus_key_handle(validator) - .get(&shell.wl_storage, Epoch::default(), &pos_params) - .unwrap() + namada_proof_of_stake::storage::read_pos_params(&shell.wl_storage) .unwrap(); + let consensus_key = + namada_proof_of_stake::storage::validator_consensus_key_handle( + validator, + ) + .get(&shell.wl_storage, Epoch::default(), &pos_params) + .unwrap() + .unwrap(); let proposer_address = HEXUPPER .decode(consensus_key.tm_raw_hash().as_bytes()) .unwrap(); @@ -2370,7 +2373,7 @@ mod test_finalize_block { // Check the bond amounts for rewards up thru the withdrawable epoch let withdraw_epoch = current_epoch + params.withdrawable_epoch_offset(); let last_claim_epoch = - namada_proof_of_stake::get_last_reward_claim_epoch( + namada_proof_of_stake::storage::get_last_reward_claim_epoch( &shell.wl_storage, &validator.address, &validator.address, @@ -2488,7 +2491,7 @@ mod test_finalize_block { let validator = validator_set.pop_first().unwrap(); let commission_rate = - namada_proof_of_stake::validator_commission_rate_handle( + namada_proof_of_stake::storage::validator_commission_rate_handle( &validator.address, ) .get(&shell.wl_storage, Epoch(0), ¶ms) @@ -2702,8 +2705,10 @@ mod test_finalize_block { // Check that there's 3 unique consensus keys let consensus_keys = - namada_proof_of_stake::get_consensus_key_set(&shell.wl_storage) - .unwrap(); + namada_proof_of_stake::storage::get_consensus_key_set( + &shell.wl_storage, + ) + .unwrap(); assert_eq!(consensus_keys.len(), 3); // let ck1 = validator_consensus_key_handle(&validator) // .get(&storage, current_epoch, ¶ms) @@ -2757,8 +2762,10 @@ mod test_finalize_block { // Check that there's 5 unique consensus keys let consensus_keys = - namada_proof_of_stake::get_consensus_key_set(&shell.wl_storage) - .unwrap(); + namada_proof_of_stake::storage::get_consensus_key_set( + &shell.wl_storage, + ) + .unwrap(); assert_eq!(consensus_keys.len(), 5); // Advance to pipeline epoch @@ -2837,8 +2844,10 @@ mod test_finalize_block { // Check that there's 7 unique consensus keys let consensus_keys = - namada_proof_of_stake::get_consensus_key_set(&shell.wl_storage) - .unwrap(); + namada_proof_of_stake::storage::get_consensus_key_set( + &shell.wl_storage, + ) + .unwrap(); assert_eq!(consensus_keys.len(), 7); // Advance to pipeline epoch @@ -2900,8 +2909,10 @@ mod test_finalize_block { // Check that there's 8 unique consensus keys let consensus_keys = - namada_proof_of_stake::get_consensus_key_set(&shell.wl_storage) - .unwrap(); + namada_proof_of_stake::storage::get_consensus_key_set( + &shell.wl_storage, + ) + .unwrap(); assert_eq!(consensus_keys.len(), 8); // Advance to pipeline epoch @@ -3473,12 +3484,15 @@ mod test_finalize_block { let validator = shell.mode.get_validator_address().unwrap().to_owned(); let pos_params = - namada_proof_of_stake::read_pos_params(&shell.wl_storage).unwrap(); - let consensus_key = - namada_proof_of_stake::validator_consensus_key_handle(&validator) - .get(&shell.wl_storage, Epoch::default(), &pos_params) - .unwrap() + namada_proof_of_stake::storage::read_pos_params(&shell.wl_storage) .unwrap(); + let consensus_key = + namada_proof_of_stake::storage::validator_consensus_key_handle( + &validator, + ) + .get(&shell.wl_storage, Epoch::default(), &pos_params) + .unwrap() + .unwrap(); let proposer_address = HEXUPPER .decode(consensus_key.tm_raw_hash().as_bytes()) .unwrap(); @@ -4060,7 +4074,7 @@ mod test_finalize_block { ) .unwrap(); - let val_stake = namada_proof_of_stake::read_validator_stake( + let val_stake = namada_proof_of_stake::storage::read_validator_stake( &shell.wl_storage, ¶ms, &val1.address, @@ -4068,7 +4082,7 @@ mod test_finalize_block { ) .unwrap(); - let total_stake = namada_proof_of_stake::read_total_stake( + let total_stake = namada_proof_of_stake::storage::read_total_stake( &shell.wl_storage, ¶ms, current_epoch + params.pipeline_len, @@ -4103,14 +4117,14 @@ mod test_finalize_block { ) .unwrap(); - let val_stake = namada_proof_of_stake::read_validator_stake( + let val_stake = namada_proof_of_stake::storage::read_validator_stake( &shell.wl_storage, ¶ms, &val1.address, current_epoch + params.pipeline_len, ) .unwrap(); - let total_stake = namada_proof_of_stake::read_total_stake( + let total_stake = namada_proof_of_stake::storage::read_total_stake( &shell.wl_storage, ¶ms, current_epoch + params.pipeline_len, @@ -4248,16 +4262,18 @@ mod test_finalize_block { assert_eq!(enqueued_slash.r#type, SlashType::DuplicateVote); assert_eq!(enqueued_slash.rate, Dec::zero()); let last_slash = - namada_proof_of_stake::read_validator_last_slash_epoch( + namada_proof_of_stake::storage::read_validator_last_slash_epoch( &shell.wl_storage, &val1.address, ) .unwrap(); assert_eq!(last_slash, Some(misbehavior_epoch)); assert!( - namada_proof_of_stake::validator_slashes_handle(&val1.address) - .is_empty(&shell.wl_storage) - .unwrap() + namada_proof_of_stake::storage::validator_slashes_handle( + &val1.address + ) + .is_empty(&shell.wl_storage) + .unwrap() ); tracing::debug!("Advancing to epoch 7"); @@ -4316,7 +4332,7 @@ mod test_finalize_block { assert_eq!(enqueued_slashes_8.len(&shell.wl_storage).unwrap(), 2_u64); assert_eq!(enqueued_slashes_9.len(&shell.wl_storage).unwrap(), 1_u64); let last_slash = - namada_proof_of_stake::read_validator_last_slash_epoch( + namada_proof_of_stake::storage::read_validator_last_slash_epoch( &shell.wl_storage, &val1.address, ) @@ -4332,18 +4348,21 @@ mod test_finalize_block { .unwrap() ); assert!( - namada_proof_of_stake::validator_slashes_handle(&val1.address) - .is_empty(&shell.wl_storage) - .unwrap() + namada_proof_of_stake::storage::validator_slashes_handle( + &val1.address + ) + .is_empty(&shell.wl_storage) + .unwrap() ); - let pre_stake_10 = namada_proof_of_stake::read_validator_stake( - &shell.wl_storage, - ¶ms, - &val1.address, - Epoch(10), - ) - .unwrap(); + let pre_stake_10 = + namada_proof_of_stake::storage::read_validator_stake( + &shell.wl_storage, + ¶ms, + &val1.address, + Epoch(10), + ) + .unwrap(); assert_eq!( pre_stake_10, initial_stake + del_1_amount @@ -4370,14 +4389,14 @@ mod test_finalize_block { let (current_epoch, _) = advance_epoch(&mut shell, &pkh1, &votes, None); assert_eq!(current_epoch.0, 9_u64); - let val_stake_3 = namada_proof_of_stake::read_validator_stake( + let val_stake_3 = namada_proof_of_stake::storage::read_validator_stake( &shell.wl_storage, ¶ms, &val1.address, Epoch(3), ) .unwrap(); - let val_stake_4 = namada_proof_of_stake::read_validator_stake( + let val_stake_4 = namada_proof_of_stake::storage::read_validator_stake( &shell.wl_storage, ¶ms, &val1.address, @@ -4385,13 +4404,13 @@ mod test_finalize_block { ) .unwrap(); - let tot_stake_3 = namada_proof_of_stake::read_total_stake( + let tot_stake_3 = namada_proof_of_stake::storage::read_total_stake( &shell.wl_storage, ¶ms, Epoch(3), ) .unwrap(); - let tot_stake_4 = namada_proof_of_stake::read_total_stake( + let tot_stake_4 = namada_proof_of_stake::storage::read_total_stake( &shell.wl_storage, ¶ms, Epoch(4), @@ -4415,7 +4434,9 @@ mod test_finalize_block { // There should be 2 slashes processed for the validator, each with rate // equal to the cubic slashing rate let val_slashes = - namada_proof_of_stake::validator_slashes_handle(&val1.address); + namada_proof_of_stake::storage::validator_slashes_handle( + &val1.address, + ); assert_eq!(val_slashes.len(&shell.wl_storage).unwrap(), 2u64); let is_rate_good = val_slashes .iter(&shell.wl_storage) @@ -4592,7 +4613,7 @@ mod test_finalize_block { assert_eq!(current_epoch.0, 12_u64); tracing::debug!("\nCHECK BOND AND UNBOND DETAILS"); - let details = namada_proof_of_stake::bonds_and_unbonds( + let details = namada_proof_of_stake::queries::bonds_and_unbonds( &shell.wl_storage, None, None, @@ -4811,13 +4832,14 @@ mod test_finalize_block { Epoch::default(), ); - let validator_stake = namada_proof_of_stake::read_validator_stake( - &shell.wl_storage, - ¶ms, - &val2, - Epoch::default(), - ) - .unwrap(); + let validator_stake = + namada_proof_of_stake::storage::read_validator_stake( + &shell.wl_storage, + ¶ms, + &val2, + Epoch::default(), + ) + .unwrap(); let val3 = initial_consensus_set[2].clone(); let val4 = initial_consensus_set[3].clone(); diff --git a/apps/src/lib/node/ledger/shell/governance.rs b/apps/src/lib/node/ledger/shell/governance.rs index 0bbdac54ce..4991310d09 100644 --- a/apps/src/lib/node/ledger/shell/governance.rs +++ b/apps/src/lib/node/ledger/shell/governance.rs @@ -19,8 +19,9 @@ use namada::ledger::protocol; use namada::ledger::storage::types::encode; use namada::ledger::storage::{DBIter, StorageHasher, DB}; use namada::ledger::storage_api::{pgf, token, StorageWrite}; +use namada::proof_of_stake::bond_amount; use namada::proof_of_stake::parameters::PosParams; -use namada::proof_of_stake::{bond_amount, read_total_stake}; +use namada::proof_of_stake::storage::read_total_stake; use namada::proto::{Code, Data}; use namada::types::address::Address; use namada::types::storage::Epoch; diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index 93241e0e26..7272d4cb6c 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -51,7 +51,9 @@ use namada::ledger::storage::{ use namada::ledger::storage_api::tx::validate_tx_bytes; use namada::ledger::storage_api::{self, StorageRead}; use namada::ledger::{parameters, pos, protocol}; -use namada::proof_of_stake::{self, process_slashes, read_pos_params, slash}; +use namada::proof_of_stake::slashing::{process_slashes, slash}; +use namada::proof_of_stake::storage::read_pos_params; +use namada::proof_of_stake::{self}; use namada::proto::{self, Section, Tx}; use namada::types::address::Address; use namada::types::chain::ChainId; @@ -749,7 +751,7 @@ where let validator_raw_hash = tm_raw_hash_to_string(evidence.validator.address); let validator = - match proof_of_stake::find_validator_by_raw_hash( + match proof_of_stake::storage::find_validator_by_raw_hash( &self.wl_storage, &validator_raw_hash, ) @@ -1556,13 +1558,13 @@ where let (current_epoch, _gas) = self.wl_storage.storage.get_current_epoch(); let pos_params = - namada_proof_of_stake::read_pos_params(&self.wl_storage) + namada_proof_of_stake::storage::read_pos_params(&self.wl_storage) .expect("Could not find the PoS parameters"); let validator_set_update_fn = if is_genesis { namada_proof_of_stake::genesis_validator_set_tendermint } else { - namada_proof_of_stake::validator_set_update_tendermint + namada_proof_of_stake::validator_set_update::validator_set_update_tendermint }; validator_set_update_fn( @@ -1609,7 +1611,7 @@ mod test_utils { use namada::ledger::storage::{LastBlock, Sha256Hasher}; use namada::ledger::storage_api::StorageWrite; use namada::proof_of_stake::parameters::PosParams; - use namada::proof_of_stake::validator_consensus_key_handle; + use namada::proof_of_stake::storage::validator_consensus_key_handle; use namada::proto::{Code, Data}; use namada::tendermint::abci::types::VoteInfo; use namada::types::address; diff --git a/apps/src/lib/node/ledger/shell/prepare_proposal.rs b/apps/src/lib/node/ledger/shell/prepare_proposal.rs index d1c6a657b1..1161401180 100644 --- a/apps/src/lib/node/ledger/shell/prepare_proposal.rs +++ b/apps/src/lib/node/ledger/shell/prepare_proposal.rs @@ -5,7 +5,7 @@ use namada::core::ledger::gas::TxGasMeter; use namada::ledger::pos::PosQueries; use namada::ledger::protocol::get_fee_unshielding_transaction; use namada::ledger::storage::{DBIter, StorageHasher, TempWlStorage, DB}; -use namada::proof_of_stake::find_validator_by_raw_hash; +use namada::proof_of_stake::storage::find_validator_by_raw_hash; use namada::proto::Tx; use namada::types::address::Address; use namada::types::internal::TxInQueue; @@ -383,11 +383,12 @@ mod test_prepare_proposal { use namada::ledger::gas::Gas; use namada::ledger::pos::PosQueries; use namada::ledger::replay_protection; - use namada::proof_of_stake::types::WeightedValidator; - use namada::proof_of_stake::{ + use namada::proof_of_stake::storage::{ consensus_validator_set_handle, - read_consensus_validator_set_addresses_with_stake, Epoch, + read_consensus_validator_set_addresses_with_stake, }; + use namada::proof_of_stake::types::WeightedValidator; + use namada::proof_of_stake::Epoch; use namada::proto::{Code, Data, Header, Section, Signature, Signed}; use namada::types::address::{self, Address}; use namada::types::ethereum_events::EthereumEvent; diff --git a/apps/src/lib/node/ledger/shell/process_proposal.rs b/apps/src/lib/node/ledger/shell/process_proposal.rs index 9ced7a0885..24d1be2b9f 100644 --- a/apps/src/lib/node/ledger/shell/process_proposal.rs +++ b/apps/src/lib/node/ledger/shell/process_proposal.rs @@ -8,7 +8,7 @@ use namada::ledger::pos::PosQueries; use namada::ledger::protocol::get_fee_unshielding_transaction; use namada::ledger::storage::TempWlStorage; use namada::ledger::storage_api::tx::validate_tx_bytes; -use namada::proof_of_stake::find_validator_by_raw_hash; +use namada::proof_of_stake::storage::find_validator_by_raw_hash; use namada::types::internal::TxInQueue; use namada::types::transaction::protocol::{ ethereum_tx_data_variants, ProtocolTxType, diff --git a/apps/src/lib/node/ledger/shell/queries.rs b/apps/src/lib/node/ledger/shell/queries.rs index 84c494faca..6adb53969d 100644 --- a/apps/src/lib/node/ledger/shell/queries.rs +++ b/apps/src/lib/node/ledger/shell/queries.rs @@ -68,7 +68,7 @@ where mod test_queries { use namada::core::ledger::storage::EPOCH_SWITCH_BLOCKS_DELAY; use namada::ledger::pos::PosQueries; - use namada::proof_of_stake::read_consensus_validator_set_addresses_with_stake; + use namada::proof_of_stake::storage::read_consensus_validator_set_addresses_with_stake; use namada::proof_of_stake::types::WeightedValidator; use namada::tendermint::abci::types::VoteInfo; use namada::types::storage::Epoch; diff --git a/apps/src/lib/node/ledger/shell/testing/node.rs b/apps/src/lib/node/ledger/shell/testing/node.rs index 4f8fa13342..6f27f92d9b 100644 --- a/apps/src/lib/node/ledger/shell/testing/node.rs +++ b/apps/src/lib/node/ledger/shell/testing/node.rs @@ -20,11 +20,11 @@ use namada::ledger::storage::{ LastBlock, Sha256Hasher, EPOCH_SWITCH_BLOCKS_DELAY, }; use namada::proof_of_stake::pos_queries::PosQueries; -use namada::proof_of_stake::types::WeightedValidator; -use namada::proof_of_stake::{ +use namada::proof_of_stake::storage::{ read_consensus_validator_set_addresses_with_stake, validator_consensus_key_handle, }; +use namada::proof_of_stake::types::WeightedValidator; use namada::tendermint::abci::response::Info; use namada::tendermint::abci::types::VoteInfo; use namada::tendermint_rpc::SimpleRequest; diff --git a/apps/src/lib/node/ledger/shell/vote_extensions/bridge_pool_vext.rs b/apps/src/lib/node/ledger/shell/vote_extensions/bridge_pool_vext.rs index 3aded3035a..629cce4e8d 100644 --- a/apps/src/lib/node/ledger/shell/vote_extensions/bridge_pool_vext.rs +++ b/apps/src/lib/node/ledger/shell/vote_extensions/bridge_pool_vext.rs @@ -203,14 +203,14 @@ mod test_bp_vote_extensions { use namada::core::ledger::eth_bridge::storage::bridge_pool::get_key_from_hash; use namada::ledger::pos::PosQueries; use namada::ledger::storage_api::StorageWrite; + use namada::proof_of_stake::storage::{ + consensus_validator_set_handle, + read_consensus_validator_set_addresses_with_stake, + }; use namada::proof_of_stake::types::{ Position as ValidatorPosition, WeightedValidator, }; - use namada::proof_of_stake::{ - become_validator, consensus_validator_set_handle, - read_consensus_validator_set_addresses_with_stake, BecomeValidator, - Epoch, - }; + use namada::proof_of_stake::{become_validator, BecomeValidator, Epoch}; use namada::proto::{SignableEthMessage, Signed}; use namada::tendermint::abci::types::VoteInfo; use namada::types::ethereum_events::Uint; diff --git a/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs b/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs index 127c4c4d0e..23c3013458 100644 --- a/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs +++ b/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs @@ -299,11 +299,11 @@ mod test_vote_extensions { use namada::eth_bridge::storage::bridge_pool; use namada::ledger::eth_bridge::EthBridgeQueries; use namada::ledger::pos::PosQueries; - use namada::proof_of_stake::types::WeightedValidator; - use namada::proof_of_stake::{ + use namada::proof_of_stake::storage::{ consensus_validator_set_handle, read_consensus_validator_set_addresses_with_stake, }; + use namada::proof_of_stake::types::WeightedValidator; use namada::tendermint::abci::types::VoteInfo; use namada::types::address::testing::gen_established_address; use namada::types::ethereum_events::{ diff --git a/apps/src/lib/node/ledger/shell/vote_extensions/val_set_update.rs b/apps/src/lib/node/ledger/shell/vote_extensions/val_set_update.rs index b4b5595a0e..91ecadbdc5 100644 --- a/apps/src/lib/node/ledger/shell/vote_extensions/val_set_update.rs +++ b/apps/src/lib/node/ledger/shell/vote_extensions/val_set_update.rs @@ -258,11 +258,12 @@ mod test_vote_extensions { NestedSubKey, SubKey, }; use namada::ledger::pos::PosQueries; - use namada::proof_of_stake::types::WeightedValidator; - use namada::proof_of_stake::{ + use namada::proof_of_stake::storage::{ consensus_validator_set_handle, - read_consensus_validator_set_addresses_with_stake, Epoch, + read_consensus_validator_set_addresses_with_stake, }; + use namada::proof_of_stake::types::WeightedValidator; + use namada::proof_of_stake::Epoch; use namada::tendermint::abci::types::VoteInfo; use namada::types::key::RefTo; use namada::types::vote_extensions::validator_set_update; diff --git a/apps/src/lib/node/ledger/shims/abcipp_shim.rs b/apps/src/lib/node/ledger/shims/abcipp_shim.rs index 563d88ac59..a8140ce56a 100644 --- a/apps/src/lib/node/ledger/shims/abcipp_shim.rs +++ b/apps/src/lib/node/ledger/shims/abcipp_shim.rs @@ -5,7 +5,7 @@ use std::pin::Pin; use std::task::{Context, Poll}; use futures::future::FutureExt; -use namada::proof_of_stake::find_validator_by_raw_hash; +use namada::proof_of_stake::storage::find_validator_by_raw_hash; use namada::proto::Tx; use namada::types::hash::Hash; use namada::types::key::tm_raw_hash_to_string; diff --git a/benches/native_vps.rs b/benches/native_vps.rs index b84cd43930..9a30e9e56e 100644 --- a/benches/native_vps.rs +++ b/benches/native_vps.rs @@ -116,7 +116,8 @@ fn governance(c: &mut Criterion) { let content_section = Section::ExtraData(Code::new(vec![], None)); let params = - proof_of_stake::read_pos_params(&shell.wl_storage).unwrap(); + proof_of_stake::storage::read_pos_params(&shell.wl_storage) + .unwrap(); let voting_start_epoch = Epoch(2 + params.pipeline_len + params.unbonding_len); // Must start after current epoch @@ -167,7 +168,8 @@ fn governance(c: &mut Criterion) { )); let params = - proof_of_stake::read_pos_params(&shell.wl_storage).unwrap(); + proof_of_stake::storage::read_pos_params(&shell.wl_storage) + .unwrap(); let voting_start_epoch = Epoch(2 + params.pipeline_len + params.unbonding_len); // Must start after current epoch diff --git a/benches/txs.rs b/benches/txs.rs index e19c79de4d..523cd48489 100644 --- a/benches/txs.rs +++ b/benches/txs.rs @@ -25,8 +25,9 @@ use namada::ibc::core::host::types::identifiers::{ }; use namada::ledger::eth_bridge::read_native_erc20_address; use namada::ledger::storage_api::{StorageRead, StorageWrite}; +use namada::proof_of_stake::storage::read_pos_params; use namada::proof_of_stake::types::SlashType; -use namada::proof_of_stake::{self, read_pos_params, KeySeg}; +use namada::proof_of_stake::{self, KeySeg}; use namada::proto::{Code, Section}; use namada::types::address::{self, Address}; use namada::types::eth_bridge_pool::{GasFee, PendingTransfer}; @@ -285,9 +286,10 @@ fn withdraw(c: &mut Criterion) { shell.wl_storage.commit_tx(); // Advance Epoch for pipeline and unbonding length - let params = - proof_of_stake::read_pos_params(&shell.wl_storage) - .unwrap(); + let params = proof_of_stake::storage::read_pos_params( + &shell.wl_storage, + ) + .unwrap(); let advance_epochs = params.pipeline_len + params.unbonding_len; @@ -330,7 +332,7 @@ fn redelegate(c: &mut Criterion) { let shell = BenchShell::default(); // Find the other genesis validator let current_epoch = shell.wl_storage.get_block_epoch().unwrap(); - let validators = namada::proof_of_stake::read_consensus_validator_set_addresses(&shell.inner.wl_storage, current_epoch).unwrap(); + let validators = namada::proof_of_stake::storage::read_consensus_validator_set_addresses(&shell.inner.wl_storage, current_epoch).unwrap(); let validator_2 = validators.into_iter().find(|addr| addr != &defaults::validator_address()).expect("There must be another validator to redelegate to"); // Prepare the redelegation tx (shell, redelegation(validator_2)) @@ -835,7 +837,7 @@ fn unjail_validator(c: &mut Criterion) { let pos_params = read_pos_params(&shell.wl_storage).unwrap(); let current_epoch = shell.wl_storage.storage.block.epoch; let evidence_epoch = current_epoch.prev(); - proof_of_stake::slash( + proof_of_stake::slashing::slash( &mut shell.wl_storage, &pos_params, current_epoch, @@ -1055,9 +1057,10 @@ fn claim_rewards(c: &mut Criterion) { let mut shell = BenchShell::default(); // Advance Epoch for pipeline and unbonding length - let params = - proof_of_stake::read_pos_params(&shell.wl_storage) - .unwrap(); + let params = proof_of_stake::storage::read_pos_params( + &shell.wl_storage, + ) + .unwrap(); let advance_epochs = params.pipeline_len + params.unbonding_len; diff --git a/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs b/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs index 42041d9666..6fcf8fa93d 100644 --- a/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs +++ b/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs @@ -235,7 +235,7 @@ mod test_apply_bp_roots_to_storage { use namada_core::types::vote_extensions::bridge_pool_roots; use namada_core::types::voting_power::FractionalVotingPower; use namada_proof_of_stake::parameters::OwnedPosParams; - use namada_proof_of_stake::write_pos_params; + use namada_proof_of_stake::storage::write_pos_params; use super::*; use crate::protocol::transactions::votes::{ diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs index e9ff768fa0..d15c71eaa8 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs @@ -708,10 +708,11 @@ mod tests { // commit then update the epoch wl_storage.storage.commit_block(MockDBWriteBatch).unwrap(); - let unbonding_len = namada_proof_of_stake::read_pos_params(&wl_storage) - .expect("Test failed") - .unbonding_len - + 1; + let unbonding_len = + namada_proof_of_stake::storage::read_pos_params(&wl_storage) + .expect("Test failed") + .unbonding_len + + 1; wl_storage.storage.last_epoch = wl_storage.storage.last_epoch + unbonding_len; wl_storage.storage.block.epoch = wl_storage.storage.last_epoch + 1_u64; @@ -844,10 +845,11 @@ mod tests { // commit then update the epoch wl_storage.storage.commit_block(MockDBWriteBatch).unwrap(); - let unbonding_len = namada_proof_of_stake::read_pos_params(&wl_storage) - .expect("Test failed") - .unbonding_len - + 1; + let unbonding_len = + namada_proof_of_stake::storage::read_pos_params(&wl_storage) + .expect("Test failed") + .unbonding_len + + 1; wl_storage.storage.last_epoch = wl_storage.storage.last_epoch + unbonding_len; wl_storage.storage.block.epoch = wl_storage.storage.last_epoch + 1_u64; diff --git a/ethereum_bridge/src/protocol/transactions/votes.rs b/ethereum_bridge/src/protocol/transactions/votes.rs index cc029e28f5..cdf7461047 100644 --- a/ethereum_bridge/src/protocol/transactions/votes.rs +++ b/ethereum_bridge/src/protocol/transactions/votes.rs @@ -190,7 +190,7 @@ mod tests { use namada_core::types::storage::BlockHeight; use namada_core::types::{address, token}; use namada_proof_of_stake::parameters::OwnedPosParams; - use namada_proof_of_stake::write_pos_params; + use namada_proof_of_stake::storage::write_pos_params; use super::*; use crate::test_utils; diff --git a/ethereum_bridge/src/protocol/transactions/votes/update.rs b/ethereum_bridge/src/protocol/transactions/votes/update.rs index a98be1859d..b6ac1d102d 100644 --- a/ethereum_bridge/src/protocol/transactions/votes/update.rs +++ b/ethereum_bridge/src/protocol/transactions/votes/update.rs @@ -220,7 +220,7 @@ mod tests { use crate::test_utils; mod helpers { - use namada_proof_of_stake::total_consensus_stake_key_handle; + use namada_proof_of_stake::storage::total_consensus_stake_handle; use super::*; @@ -279,7 +279,7 @@ mod tests { > FractionalVotingPower::TWO_THIRDS * total_stake, }; votes::storage::write(wl_storage, &keys, event, &tally, false)?; - total_consensus_stake_key_handle().set( + total_consensus_stake_handle().set( wl_storage, total_stake, 0u64.into(), diff --git a/ethereum_bridge/src/storage/eth_bridge_queries.rs b/ethereum_bridge/src/storage/eth_bridge_queries.rs index 06cb1ed024..5e132592de 100644 --- a/ethereum_bridge/src/storage/eth_bridge_queries.rs +++ b/ethereum_bridge/src/storage/eth_bridge_queries.rs @@ -22,7 +22,7 @@ use namada_core::types::voting_power::{ EthBridgeVotingPower, FractionalVotingPower, }; use namada_proof_of_stake::pos_queries::{ConsensusValidators, PosQueries}; -use namada_proof_of_stake::{ +use namada_proof_of_stake::storage::{ validator_eth_cold_key_handle, validator_eth_hot_key_handle, }; diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 531ea5e31a..a390d6ec00 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -9,9 +9,13 @@ pub mod epoched; pub mod parameters; pub mod pos_queries; +pub mod queries; pub mod rewards; +pub mod slashing; pub mod storage; +pub mod storage_key; pub mod types; +pub mod validator_set_update; // pub mod validation; mod error; @@ -19,55 +23,68 @@ mod error; mod tests; use core::fmt::Debug; -use std::cmp::{self, Reverse}; -use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::cmp::{self}; +use std::collections::{BTreeMap, BTreeSet, HashSet}; -use borsh::BorshDeserialize; pub use error::*; use namada_core::ledger::storage_api::collections::lazy_map::{ - Collectable, LazyMap, NestedMap, NestedSubKey, SubKey, + Collectable, LazyMap, NestedSubKey, SubKey, }; -use namada_core::ledger::storage_api::collections::{LazyCollection, LazySet}; use namada_core::ledger::storage_api::{ - self, governance, token, ResultExt, StorageRead, StorageWrite, + self, token, StorageRead, StorageWrite, }; -use namada_core::types::address::{self, Address, InternalAddress}; +use namada_core::types::address::{Address, InternalAddress}; use namada_core::types::dec::Dec; -use namada_core::types::key::{ - common, protocol_pk_key, tm_consensus_key_raw_hash, PublicKeyTmRawHash, -}; +use namada_core::types::key::common; use namada_core::types::storage::BlockHeight; pub use namada_core::types::storage::{Epoch, Key, KeySeg}; -use once_cell::unsync::Lazy; pub use parameters::{OwnedPosParams, PosParams}; -use rewards::PosRewardsCalculator; -use storage::{ - bonds_for_source_prefix, bonds_prefix, consensus_keys_key, - get_validator_address_from_bond, is_bond_key, is_unbond_key, - is_validator_slashes_key, last_block_proposer_key, - last_pos_reward_claim_epoch_key, params_key, rewards_counter_key, - slashes_prefix, unbonds_for_source_prefix, unbonds_prefix, - validator_address_raw_hash_key, validator_description_key, - validator_discord_key, validator_email_key, validator_last_slash_key, - validator_max_commission_rate_change_key, validator_website_key, + +use crate::queries::{find_bonds, has_bonds}; +use crate::rewards::{ + add_rewards_to_counter, compute_current_rewards_from_bonds, + read_rewards_counter, take_rewards_from_counter, +}; +use crate::slashing::{ + apply_list_slashes, compute_amount_after_slashing_unbond, + compute_amount_after_slashing_withdraw, find_validator_slashes, +}; +use crate::storage::{ + below_capacity_validator_set_handle, bond_handle, + consensus_validator_set_handle, delegator_redelegated_bonds_handle, + delegator_redelegated_unbonds_handle, get_last_reward_claim_epoch, + liveness_missed_votes_handle, liveness_sum_missed_votes_handle, + read_consensus_validator_set_addresses, read_non_pos_owned_params, + read_pos_params, read_validator_last_slash_epoch, + read_validator_max_commission_rate_change, read_validator_stake, + total_bonded_handle, total_consensus_stake_handle, total_unbonded_handle, + try_insert_consensus_key, unbond_handle, update_total_deltas, + update_validator_deltas, validator_addresses_handle, + validator_commission_rate_handle, validator_consensus_key_handle, + validator_deltas_handle, validator_eth_cold_key_handle, + validator_eth_hot_key_handle, validator_incoming_redelegations_handle, + validator_outgoing_redelegations_handle, validator_protocol_key_handle, + validator_rewards_products_handle, validator_set_positions_handle, + validator_slashes_handle, validator_state_handle, + validator_total_redelegated_bonded_handle, + validator_total_redelegated_unbonded_handle, write_last_reward_claim_epoch, + write_pos_params, write_validator_address_raw_hash, + write_validator_description, write_validator_discord_handle, + write_validator_email, write_validator_max_commission_rate_change, + write_validator_metadata, write_validator_website, }; -use types::{ - into_tm_voting_power, BelowCapacityValidatorSet, - BelowCapacityValidatorSets, BondDetails, BondId, Bonds, - BondsAndUnbondsDetail, BondsAndUnbondsDetails, CommissionRates, - ConsensusValidator, ConsensusValidatorSet, ConsensusValidatorSets, - DelegatorRedelegatedBonded, DelegatorRedelegatedUnbonded, - EagerRedelegatedBondsMap, EpochedSlashes, IncomingRedelegations, - LivenessMissedVotes, LivenessSumMissedVotes, OutgoingRedelegations, - Position, RedelegatedBondsOrUnbonds, RedelegatedTokens, - ReverseOrdTokenAmount, RewardsAccumulator, RewardsProducts, Slash, - SlashType, SlashedAmount, Slashes, TotalConsensusStakes, TotalDeltas, - TotalRedelegatedBonded, TotalRedelegatedUnbonded, UnbondDetails, Unbonds, - ValidatorAddresses, ValidatorConsensusKeys, ValidatorDeltas, - ValidatorEthColdKeys, ValidatorEthHotKeys, ValidatorMetaData, - ValidatorPositionAddresses, ValidatorProtocolKeys, ValidatorSetPositions, - ValidatorSetUpdate, ValidatorState, ValidatorStates, - ValidatorTotalUnbonded, VoteInfo, WeightedValidator, +use crate::storage_key::{bonds_for_source_prefix, is_bond_key}; +use crate::types::{ + BondId, ConsensusValidator, ConsensusValidatorSet, + EagerRedelegatedBondsMap, RedelegatedBondsOrUnbonds, RedelegatedTokens, + ResultSlashing, Slash, Unbonds, ValidatorMetaData, ValidatorSetUpdate, + ValidatorState, VoteInfo, +}; +use crate::validator_set_update::{ + copy_validator_sets_and_positions, insert_validator_into_validator_set, + promote_next_below_capacity_validator_to_consensus, + remove_below_capacity_validator, remove_consensus_validator, + update_validator_set, }; /// Address of the PoS account implemented as a native VP @@ -84,217 +101,6 @@ pub fn staking_token_address(storage: &impl StorageRead) -> Address { .expect("Must be able to read native token address") } -/// Get the storage handle to the epoched consensus validator set -pub fn consensus_validator_set_handle() -> ConsensusValidatorSets { - let key = storage::consensus_validator_set_key(); - ConsensusValidatorSets::open(key) -} - -/// Get the storage handle to the epoched below-capacity validator set -pub fn below_capacity_validator_set_handle() -> BelowCapacityValidatorSets { - let key = storage::below_capacity_validator_set_key(); - BelowCapacityValidatorSets::open(key) -} - -/// Get the storage handle to a PoS validator's consensus key (used for -/// signing block votes). -pub fn validator_consensus_key_handle( - validator: &Address, -) -> ValidatorConsensusKeys { - let key = storage::validator_consensus_key_key(validator); - ValidatorConsensusKeys::open(key) -} - -/// Get the storage handle to a PoS validator's protocol key key. -pub fn validator_protocol_key_handle( - validator: &Address, -) -> ValidatorProtocolKeys { - let key = protocol_pk_key(validator); - ValidatorProtocolKeys::open(key) -} - -/// Get the storage handle to a PoS validator's eth hot key. -pub fn validator_eth_hot_key_handle( - validator: &Address, -) -> ValidatorEthHotKeys { - let key = storage::validator_eth_hot_key_key(validator); - ValidatorEthHotKeys::open(key) -} - -/// Get the storage handle to a PoS validator's eth cold key. -pub fn validator_eth_cold_key_handle( - validator: &Address, -) -> ValidatorEthColdKeys { - let key = storage::validator_eth_cold_key_key(validator); - ValidatorEthColdKeys::open(key) -} - -/// Get the storage handle to the total consensus validator stake -pub fn total_consensus_stake_key_handle() -> TotalConsensusStakes { - let key = storage::total_consensus_stake_key(); - TotalConsensusStakes::open(key) -} - -/// Get the storage handle to a PoS validator's state -pub fn validator_state_handle(validator: &Address) -> ValidatorStates { - let key = storage::validator_state_key(validator); - ValidatorStates::open(key) -} - -/// Get the storage handle to a PoS validator's deltas -pub fn validator_deltas_handle(validator: &Address) -> ValidatorDeltas { - let key = storage::validator_deltas_key(validator); - ValidatorDeltas::open(key) -} - -/// Get the storage handle to the total deltas -pub fn total_deltas_handle() -> TotalDeltas { - let key = storage::total_deltas_key(); - TotalDeltas::open(key) -} - -/// Get the storage handle to the set of all validators -pub fn validator_addresses_handle() -> ValidatorAddresses { - let key = storage::validator_addresses_key(); - ValidatorAddresses::open(key) -} - -/// Get the storage handle to a PoS validator's commission rate -pub fn validator_commission_rate_handle( - validator: &Address, -) -> CommissionRates { - let key = storage::validator_commission_rate_key(validator); - CommissionRates::open(key) -} - -/// Get the storage handle to a bond, which is dynamically updated with when -/// unbonding -pub fn bond_handle(source: &Address, validator: &Address) -> Bonds { - let bond_id = BondId { - source: source.clone(), - validator: validator.clone(), - }; - let key = storage::bond_key(&bond_id); - Bonds::open(key) -} - -/// Get the storage handle to a validator's total bonds, which are not updated -/// due to unbonding -pub fn total_bonded_handle(validator: &Address) -> Bonds { - let key = storage::validator_total_bonded_key(validator); - Bonds::open(key) -} - -/// Get the storage handle to an unbond -pub fn unbond_handle(source: &Address, validator: &Address) -> Unbonds { - let bond_id = BondId { - source: source.clone(), - validator: validator.clone(), - }; - let key = storage::unbond_key(&bond_id); - Unbonds::open(key) -} - -/// Get the storage handle to a validator's total-unbonded map -pub fn total_unbonded_handle(validator: &Address) -> ValidatorTotalUnbonded { - let key = storage::validator_total_unbonded_key(validator); - ValidatorTotalUnbonded::open(key) -} - -/// Get the storage handle to a PoS validator's deltas -pub fn validator_set_positions_handle() -> ValidatorSetPositions { - let key = storage::validator_set_positions_key(); - ValidatorSetPositions::open(key) -} - -/// Get the storage handle to a PoS validator's slashes -pub fn validator_slashes_handle(validator: &Address) -> Slashes { - let key = storage::validator_slashes_key(validator); - Slashes::open(key) -} - -/// Get the storage handle to list of all slashes to be processed and ultimately -/// placed in the `validator_slashes_handle` -pub fn enqueued_slashes_handle() -> EpochedSlashes { - let key = storage::enqueued_slashes_key(); - EpochedSlashes::open(key) -} - -/// Get the storage handle to the rewards accumulator for the consensus -/// validators in a given epoch -pub fn rewards_accumulator_handle() -> RewardsAccumulator { - let key = storage::consensus_validator_rewards_accumulator_key(); - RewardsAccumulator::open(key) -} - -/// Get the storage handle to a validator's rewards products -pub fn validator_rewards_products_handle( - validator: &Address, -) -> RewardsProducts { - let key = storage::validator_rewards_product_key(validator); - RewardsProducts::open(key) -} - -/// Get the storage handle to a validator's incoming redelegations -pub fn validator_incoming_redelegations_handle( - validator: &Address, -) -> IncomingRedelegations { - let key = storage::validator_incoming_redelegations_key(validator); - IncomingRedelegations::open(key) -} - -/// Get the storage handle to a validator's outgoing redelegations -pub fn validator_outgoing_redelegations_handle( - validator: &Address, -) -> OutgoingRedelegations { - let key: Key = storage::validator_outgoing_redelegations_key(validator); - OutgoingRedelegations::open(key) -} - -/// Get the storage handle to a validator's total redelegated bonds -pub fn validator_total_redelegated_bonded_handle( - validator: &Address, -) -> TotalRedelegatedBonded { - let key: Key = storage::validator_total_redelegated_bonded_key(validator); - TotalRedelegatedBonded::open(key) -} - -/// Get the storage handle to a validator's outgoing redelegations -pub fn validator_total_redelegated_unbonded_handle( - validator: &Address, -) -> TotalRedelegatedUnbonded { - let key: Key = storage::validator_total_redelegated_unbonded_key(validator); - TotalRedelegatedUnbonded::open(key) -} - -/// Get the storage handle to a delegator's redelegated bonds information -pub fn delegator_redelegated_bonds_handle( - delegator: &Address, -) -> DelegatorRedelegatedBonded { - let key: Key = storage::delegator_redelegated_bonds_key(delegator); - DelegatorRedelegatedBonded::open(key) -} - -/// Get the storage handle to a delegator's redelegated unbonds information -pub fn delegator_redelegated_unbonds_handle( - delegator: &Address, -) -> DelegatorRedelegatedUnbonded { - let key: Key = storage::delegator_redelegated_unbonds_key(delegator); - DelegatorRedelegatedUnbonded::open(key) -} - -/// Get the storage handle to the missed votes for liveness tracking -pub fn liveness_missed_votes_handle() -> LivenessMissedVotes { - let key = storage::liveness_missed_votes_key(); - LivenessMissedVotes::open(key) -} - -/// Get the storage handle to the sum of missed votes for liveness tracking -pub fn liveness_sum_missed_votes_handle() -> LivenessSumMissedVotes { - let key = storage::liveness_sum_missed_votes_key(); - LivenessSumMissedVotes::open(key) -} - /// Init genesis. Requires that the governance parameters are initialized. pub fn init_genesis( storage: &mut S, @@ -339,4810 +145,1686 @@ where Ok(()) } -/// Read PoS parameters -pub fn read_pos_params(storage: &S) -> storage_api::Result -where - S: StorageRead, -{ - let params = storage - .read(¶ms_key()) - .transpose() - .expect("PosParams should always exist in storage after genesis")?; - read_non_pos_owned_params(storage, params) -} - -/// Read non-PoS-owned parameters to add them to `OwnedPosParams` to construct -/// `PosParams`. -pub fn read_non_pos_owned_params( +/// Check if the provided address is a validator address +pub fn is_validator( storage: &S, - owned: OwnedPosParams, -) -> storage_api::Result + address: &Address, +) -> storage_api::Result where S: StorageRead, { - let max_proposal_period = governance::get_max_proposal_period(storage)?; - Ok(PosParams { - owned, - max_proposal_period, - }) -} - -/// Write PoS parameters -pub fn write_pos_params( - storage: &mut S, - params: &OwnedPosParams, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - let key = params_key(); - storage.write(&key, params) + // TODO: should this check be made different? I suppose it does work but + // feels weird... + let rate = read_validator_max_commission_rate_change(storage, address)?; + Ok(rate.is_some()) } -/// Get the validator address given the raw hash of the Tendermint consensus key -pub fn find_validator_by_raw_hash( +/// Check if the provided address is a delegator address, optionally at a +/// particular epoch +pub fn is_delegator( storage: &S, - raw_hash: impl AsRef, -) -> storage_api::Result> + address: &Address, + epoch: Option, +) -> storage_api::Result where S: StorageRead, { - let key = validator_address_raw_hash_key(raw_hash); - storage.read(&key) + let prefix = bonds_for_source_prefix(address); + match epoch { + Some(epoch) => { + let iter = storage_api::iter_prefix_bytes(storage, &prefix)?; + for res in iter { + let (key, _) = res?; + if let Some((bond_id, bond_epoch)) = is_bond_key(&key) { + if bond_id.source != bond_id.validator + && bond_epoch <= epoch + { + return Ok(true); + } + } + } + Ok(false) + } + None => { + let iter = storage_api::iter_prefix_bytes(storage, &prefix)?; + for res in iter { + let (key, _) = res?; + if let Some((bond_id, _epoch)) = is_bond_key(&key) { + if bond_id.source != bond_id.validator { + return Ok(true); + } + } + } + Ok(false) + } + } } -/// Write PoS validator's address raw hash. -pub fn write_validator_address_raw_hash( +/// Self-bond tokens to a validator when `source` is `None` or equal to +/// the `validator` address, or delegate tokens from the `source` to the +/// `validator`. +pub fn bond_tokens( storage: &mut S, + source: Option<&Address>, validator: &Address, - consensus_key: &common::PublicKey, + amount: token::Amount, + current_epoch: Epoch, + offset_opt: Option, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, { - let raw_hash = tm_consensus_key_raw_hash(consensus_key); - storage.write(&validator_address_raw_hash_key(raw_hash), validator) + tracing::debug!( + "Bonding token amount {} at epoch {current_epoch}", + amount.to_string_native() + ); + if amount.is_zero() { + return Ok(()); + } + + // Transfer the bonded tokens from the source to PoS + if let Some(source) = source { + if source != validator && is_validator(storage, source)? { + return Err( + BondError::SourceMustNotBeAValidator(source.clone()).into() + ); + } + } + let source = source.unwrap_or(validator); + tracing::debug!("Source {source} --> Validator {validator}"); + + let staking_token = staking_token_address(storage); + token::transfer(storage, &staking_token, source, &ADDRESS, amount)?; + + let params = read_pos_params(storage)?; + let offset = offset_opt.unwrap_or(params.pipeline_len); + let offset_epoch = current_epoch + offset; + + // Check that the validator is actually a validator + let validator_state_handle = validator_state_handle(validator); + let state = validator_state_handle.get(storage, offset_epoch, ¶ms)?; + if state.is_none() { + return Err(BondError::NotAValidator(validator.clone()).into()); + } + + let bond_handle = bond_handle(source, validator); + let total_bonded_handle = total_bonded_handle(validator); + + if tracing::level_enabled!(tracing::Level::DEBUG) { + let bonds = find_bonds(storage, source, validator)?; + tracing::debug!("\nBonds before incrementing: {bonds:#?}"); + } + + // Initialize or update the bond at the pipeline offset + bond_handle.add(storage, amount, current_epoch, offset)?; + total_bonded_handle.add(storage, amount, current_epoch, offset)?; + + if tracing::level_enabled!(tracing::Level::DEBUG) { + let bonds = find_bonds(storage, source, validator)?; + tracing::debug!("\nBonds after incrementing: {bonds:#?}"); + } + + // Update the validator set + // Allow bonding even if the validator is jailed. However, if jailed, there + // must be no changes to the validator set. Check at the pipeline epoch. + let is_jailed_or_inactive_at_pipeline = matches!( + validator_state_handle.get(storage, offset_epoch, ¶ms)?, + Some(ValidatorState::Jailed) | Some(ValidatorState::Inactive) + ); + if !is_jailed_or_inactive_at_pipeline { + update_validator_set( + storage, + ¶ms, + validator, + amount.change(), + current_epoch, + offset_opt, + )?; + } + + // Update the validator and total deltas + update_validator_deltas( + storage, + ¶ms, + validator, + amount.change(), + current_epoch, + offset_opt, + )?; + + update_total_deltas( + storage, + ¶ms, + amount.change(), + current_epoch, + offset_opt, + )?; + + Ok(()) } -/// Read PoS validator's max commission rate change. -pub fn read_validator_max_commission_rate_change( +/// Compute total validator stake for the current epoch +fn compute_total_consensus_stake( storage: &S, - validator: &Address, -) -> storage_api::Result> + epoch: Epoch, +) -> storage_api::Result where S: StorageRead, { - let key = validator_max_commission_rate_change_key(validator); - storage.read(&key) + consensus_validator_set_handle() + .at(&epoch) + .iter(storage)? + .fold(Ok(token::Amount::zero()), |acc, entry| { + let acc = acc?; + let ( + NestedSubKey::Data { + key: amount, + nested_sub_key: _, + }, + _validator, + ) = entry?; + Ok(acc.checked_add(amount).expect( + "Total consensus stake computation should not overflow.", + )) + }) } -/// Write PoS validator's max commission rate change. -pub fn write_validator_max_commission_rate_change( +/// Compute and then store the total consensus stake +pub fn compute_and_store_total_consensus_stake( storage: &mut S, - validator: &Address, - change: Dec, + epoch: Epoch, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, { - let key = validator_max_commission_rate_change_key(validator); - storage.write(&key, change) -} + let total = compute_total_consensus_stake(storage, epoch)?; + tracing::debug!( + "Total consensus stake for epoch {}: {}", + epoch, + total.to_string_native() + ); + total_consensus_stake_handle().set(storage, total, epoch, 0) +} -/// Read the most recent slash epoch for the given epoch -pub fn read_validator_last_slash_epoch( - storage: &S, - validator: &Address, -) -> storage_api::Result> -where - S: StorageRead, -{ - let key = validator_last_slash_key(validator); - storage.read(&key) +/// Used below in `fn unbond_tokens` to update the bond and unbond amounts +#[derive(Eq, Hash, PartialEq)] +struct BondAndUnbondUpdates { + bond_start: Epoch, + new_bond_value: token::Change, + unbond_value: token::Change, } -/// Write the most recent slash epoch for the given epoch -pub fn write_validator_last_slash_epoch( +/// Unbond tokens that are bonded between a validator and a source (self or +/// delegator). +/// +/// This fn is also called during redelegation for a source validator, in +/// which case the `is_redelegation` param must be true. +pub fn unbond_tokens( storage: &mut S, + source: Option<&Address>, validator: &Address, - epoch: Epoch, -) -> storage_api::Result<()> + amount: token::Amount, + current_epoch: Epoch, + is_redelegation: bool, +) -> storage_api::Result where S: StorageRead + StorageWrite, { - let key = validator_last_slash_key(validator); - storage.write(&key, epoch) -} + if amount.is_zero() { + return Ok(ResultSlashing::default()); + } -/// Read last block proposer address. -pub fn read_last_block_proposer_address( - storage: &S, -) -> storage_api::Result> -where - S: StorageRead, -{ - let key = last_block_proposer_key(); - storage.read(&key) -} + let params = read_pos_params(storage)?; + let pipeline_epoch = current_epoch + params.pipeline_len; + let withdrawable_epoch = current_epoch + params.withdrawable_epoch_offset(); + tracing::debug!( + "Unbonding token amount {} at epoch {}, withdrawable at epoch {}", + amount.to_string_native(), + current_epoch, + withdrawable_epoch + ); -/// Write last block proposer address. -pub fn write_last_block_proposer_address( - storage: &mut S, - address: Address, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - let key = last_block_proposer_key(); - storage.write(&key, address) -} + // Make sure source is not some other validator + if let Some(source) = source { + if source != validator && is_validator(storage, source)? { + return Err( + BondError::SourceMustNotBeAValidator(source.clone()).into() + ); + } + } + // Make sure the target is actually a validator + if !is_validator(storage, validator)? { + return Err(BondError::NotAValidator(validator.clone()).into()); + } + // Make sure the validator is not currently frozen + if is_validator_frozen(storage, validator, current_epoch, ¶ms)? { + return Err(UnbondError::ValidatorIsFrozen(validator.clone()).into()); + } -/// Read PoS validator's delta value. -pub fn read_validator_deltas_value( - storage: &S, - validator: &Address, - epoch: &namada_core::types::storage::Epoch, -) -> storage_api::Result> -where - S: StorageRead, -{ - let handle = validator_deltas_handle(validator); - handle.get_delta_val(storage, *epoch) -} + let source = source.unwrap_or(validator); + let bonds_handle = bond_handle(source, validator); -/// Read PoS validator's stake (sum of deltas). -/// For non-validators and validators with `0` stake, this returns the default - -/// `token::Amount::zero()`. -pub fn read_validator_stake( - storage: &S, - params: &PosParams, - validator: &Address, - epoch: namada_core::types::storage::Epoch, -) -> storage_api::Result -where - S: StorageRead, -{ - let handle = validator_deltas_handle(validator); - let amount = handle - .get_sum(storage, epoch, params)? - .map(|change| { - debug_assert!(change.non_negative()); - token::Amount::from_change(change) - }) + // Make sure there are enough tokens left in the bond at the pipeline offset + let remaining_at_pipeline = bonds_handle + .get_sum(storage, pipeline_epoch, ¶ms)? .unwrap_or_default(); - Ok(amount) -} + if amount > remaining_at_pipeline { + return Err(UnbondError::UnbondAmountGreaterThanBond( + amount.to_string_native(), + remaining_at_pipeline.to_string_native(), + ) + .into()); + } -/// Add or remove PoS validator's stake delta value -pub fn update_validator_deltas( - storage: &mut S, - params: &OwnedPosParams, - validator: &Address, - delta: token::Change, - current_epoch: namada_core::types::storage::Epoch, - offset_opt: Option, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - let handle = validator_deltas_handle(validator); - let offset = offset_opt.unwrap_or(params.pipeline_len); - let val = handle - .get_delta_val(storage, current_epoch + offset)? - .unwrap_or_default(); - handle.set( - storage, - val.checked_add(&delta) - .expect("Validator deltas updated amount should not overflow"), - current_epoch, - offset, - ) -} + if tracing::level_enabled!(tracing::Level::DEBUG) { + let bonds = find_bonds(storage, source, validator)?; + tracing::debug!("\nBonds before decrementing: {bonds:#?}"); + } -/// Read PoS total stake (sum of deltas). -pub fn read_total_stake( - storage: &S, - params: &PosParams, - epoch: namada_core::types::storage::Epoch, -) -> storage_api::Result -where - S: StorageRead, -{ - let handle = total_deltas_handle(); - let amnt = handle - .get_sum(storage, epoch, params)? - .map(|change| { - debug_assert!(change.non_negative()); - token::Amount::from_change(change) - }) - .unwrap_or_default(); - Ok(amnt) -} + let unbonds = unbond_handle(source, validator); -/// Read all addresses from consensus validator set. -pub fn read_consensus_validator_set_addresses( - storage: &S, - epoch: namada_core::types::storage::Epoch, -) -> storage_api::Result> -where - S: StorageRead, -{ - consensus_validator_set_handle() - .at(&epoch) - .iter(storage)? - .map(|res| res.map(|(_sub_key, address)| address)) - .collect() -} + let redelegated_bonds = + delegator_redelegated_bonds_handle(source).at(validator); -/// Read all addresses from below-capacity validator set. -pub fn read_below_capacity_validator_set_addresses( - storage: &S, - epoch: namada_core::types::storage::Epoch, -) -> storage_api::Result> -where - S: StorageRead, -{ - below_capacity_validator_set_handle() - .at(&epoch) - .iter(storage)? - .map(|res| res.map(|(_sub_key, address)| address)) - .collect() -} + #[cfg(debug_assertions)] + let redel_bonds_pre = redelegated_bonds.collect_map(storage)?; -/// Read all addresses from the below-threshold set -pub fn read_below_threshold_validator_set_addresses( - storage: &S, - epoch: namada_core::types::storage::Epoch, -) -> storage_api::Result> -where - S: StorageRead, -{ - let params = read_pos_params(storage)?; - Ok(validator_addresses_handle() - .at(&epoch) - .iter(storage)? - .map(Result::unwrap) - .filter(|address| { - matches!( - validator_state_handle(address).get(storage, epoch, ¶ms), - Ok(Some(ValidatorState::BelowThreshold)) - ) - }) - .collect()) -} + // `resultUnbonding` + // Find the bonds to fully unbond (remove) and one to partially unbond, if + // necessary + let bonds_to_unbond = find_bonds_to_remove( + storage, + &bonds_handle.get_data_handler(), + amount, + )?; -/// Read all addresses from consensus validator set with their stake. -pub fn read_consensus_validator_set_addresses_with_stake( - storage: &S, - epoch: namada_core::types::storage::Epoch, -) -> storage_api::Result> -where - S: StorageRead, -{ - consensus_validator_set_handle() - .at(&epoch) - .iter(storage)? - .map(|res| { - res.map( - |( - NestedSubKey::Data { - key: bonded_stake, - nested_sub_key: _, - }, - address, - )| { - WeightedValidator { - address, - bonded_stake, - } - }, - ) - }) - .collect() -} + // `modifiedRedelegation` + // A bond may have both redelegated and non-redelegated tokens in it. If + // this is the case, compute the modified state of the redelegation. + let modified_redelegation = match bonds_to_unbond.new_entry { + Some((bond_epoch, new_bond_amount)) => { + if redelegated_bonds.contains(storage, &bond_epoch)? { + let cur_bond_amount = bonds_handle + .get_delta_val(storage, bond_epoch)? + .unwrap_or_default(); + compute_modified_redelegation( + storage, + &redelegated_bonds.at(&bond_epoch), + bond_epoch, + cur_bond_amount - new_bond_amount, + )? + } else { + ModifiedRedelegation::default() + } + } + None => ModifiedRedelegation::default(), + }; -/// Count the number of consensus validators -pub fn get_num_consensus_validators( - storage: &S, - epoch: namada_core::types::storage::Epoch, -) -> storage_api::Result -where - S: StorageRead, -{ - Ok(consensus_validator_set_handle() - .at(&epoch) - .iter(storage)? - .count() as u64) -} + // Compute the new unbonds eagerly + // `keysUnbonds` + // Get a set of epochs from which we're unbonding (fully and partially). + let bond_epochs_to_unbond = + if let Some((start_epoch, _)) = bonds_to_unbond.new_entry { + let mut to_remove = bonds_to_unbond.epochs.clone(); + to_remove.insert(start_epoch); + to_remove + } else { + bonds_to_unbond.epochs.clone() + }; -/// Read all addresses from below-capacity validator set with their stake. -pub fn read_below_capacity_validator_set_addresses_with_stake( - storage: &S, - epoch: namada_core::types::storage::Epoch, -) -> storage_api::Result> -where - S: StorageRead, -{ - below_capacity_validator_set_handle() - .at(&epoch) - .iter(storage)? - .map(|res| { - res.map( - |( - NestedSubKey::Data { - key: ReverseOrdTokenAmount(bonded_stake), - nested_sub_key: _, - }, - address, - )| { - WeightedValidator { - address, - bonded_stake, - } - }, - ) + // `newUnbonds` + // For each epoch we're unbonding, find the amount that's being unbonded. + // For full unbonds, this is the current bond value. For partial unbonds + // it is a difference between the current and new bond amount. + let new_unbonds_map = bond_epochs_to_unbond + .into_iter() + .map(|epoch| { + let cur_bond_value = bonds_handle + .get_delta_val(storage, epoch) + .unwrap() + .unwrap_or_default(); + let value = if let Some((start_epoch, new_bond_amount)) = + bonds_to_unbond.new_entry + { + if start_epoch == epoch { + cur_bond_value - new_bond_amount + } else { + cur_bond_value + } + } else { + cur_bond_value + }; + (epoch, value) }) - .collect() -} + .collect::>(); -/// Read all validator addresses. -pub fn read_all_validator_addresses( - storage: &S, - epoch: namada_core::types::storage::Epoch, -) -> storage_api::Result> -where - S: StorageRead, -{ - validator_addresses_handle() - .at(&epoch) - .iter(storage)? - .collect() -} - -/// Update PoS total deltas. -/// Note: for EpochedDelta, write the value to change storage by -pub fn update_total_deltas( - storage: &mut S, - params: &OwnedPosParams, - delta: token::Change, - current_epoch: namada_core::types::storage::Epoch, - offset_opt: Option, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - let handle = total_deltas_handle(); - let offset = offset_opt.unwrap_or(params.pipeline_len); - let val = handle - .get_delta_val(storage, current_epoch + offset)? - .unwrap_or_default(); - handle.set( - storage, - val.checked_add(&delta) - .expect("Total deltas updated amount should not overflow"), - current_epoch, - offset, - ) -} - -/// Check if the provided address is a validator address -pub fn is_validator( - storage: &S, - address: &Address, -) -> storage_api::Result -where - S: StorageRead, -{ - // TODO: should this check be made different? I suppose it does work but - // feels weird... - let rate = read_validator_max_commission_rate_change(storage, address)?; - Ok(rate.is_some()) -} + // `updatedBonded` + // Remove bonds for all the full unbonds. + for epoch in &bonds_to_unbond.epochs { + bonds_handle.get_data_handler().remove(storage, epoch)?; + } + // Replace bond amount for partial unbond, if any. + if let Some((bond_epoch, new_bond_amount)) = bonds_to_unbond.new_entry { + bonds_handle.set(storage, new_bond_amount, bond_epoch, 0)?; + } -/// Check if the provided address is a delegator address, optionally at a -/// particular epoch -pub fn is_delegator( - storage: &S, - address: &Address, - epoch: Option, -) -> storage_api::Result -where - S: StorageRead, -{ - let prefix = bonds_for_source_prefix(address); - match epoch { - Some(epoch) => { - let iter = storage_api::iter_prefix_bytes(storage, &prefix)?; - for res in iter { - let (key, _) = res?; - if let Some((bond_id, bond_epoch)) = is_bond_key(&key) { - if bond_id.source != bond_id.validator - && bond_epoch <= epoch - { - return Ok(true); - } - } - } - Ok(false) - } - None => { - let iter = storage_api::iter_prefix_bytes(storage, &prefix)?; - for res in iter { - let (key, _) = res?; - if let Some((bond_id, _epoch)) = is_bond_key(&key) { - if bond_id.source != bond_id.validator { - return Ok(true); - } - } - } - Ok(false) + // `updatedUnbonded` + // Update the unbonds in storage using the eager map computed above + if !is_redelegation { + for (start_epoch, &unbond_amount) in new_unbonds_map.iter() { + unbonds.at(start_epoch).update( + storage, + withdrawable_epoch, + |cur_val| cur_val.unwrap_or_default() + unbond_amount, + )?; } } -} -/// Self-bond tokens to a validator when `source` is `None` or equal to -/// the `validator` address, or delegate tokens from the `source` to the -/// `validator`. -pub fn bond_tokens( - storage: &mut S, - source: Option<&Address>, - validator: &Address, - amount: token::Amount, - current_epoch: Epoch, - offset_opt: Option, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - tracing::debug!( - "Bonding token amount {} at epoch {current_epoch}", - amount.to_string_native() - ); - if amount.is_zero() { - return Ok(()); - } + // `newRedelegatedUnbonds` + // This is what the delegator's redelegated unbonds would look like if this + // was the only unbond in the PoS system. We need to add these redelegated + // unbonds to the existing redelegated unbonds + let new_redelegated_unbonds = compute_new_redelegated_unbonds( + storage, + &redelegated_bonds, + &bonds_to_unbond.epochs, + &modified_redelegation, + )?; - // Transfer the bonded tokens from the source to PoS - if let Some(source) = source { - if source != validator && is_validator(storage, source)? { - return Err( - BondError::SourceMustNotBeAValidator(source.clone()).into() - ); + // `updatedRedelegatedBonded` + // NOTE: for now put this here after redelegated unbonds calc bc that one + // uses the pre-modified redelegated bonds from storage! + // First remove redelegation entries in epochs with full unbonds. + for epoch_to_remove in &bonds_to_unbond.epochs { + redelegated_bonds.remove_all(storage, epoch_to_remove)?; + } + if let Some(epoch) = modified_redelegation.epoch { + tracing::debug!("\nIs modified redelegation"); + if modified_redelegation.validators_to_remove.is_empty() { + redelegated_bonds.remove_all(storage, &epoch)?; + } else { + // Then update the redelegated bonds at this epoch + let rbonds = redelegated_bonds.at(&epoch); + update_redelegated_bonds(storage, &rbonds, &modified_redelegation)?; } } - let source = source.unwrap_or(validator); - tracing::debug!("Source {source} --> Validator {validator}"); - let staking_token = staking_token_address(storage); - token::transfer(storage, &staking_token, source, &ADDRESS, amount)?; + if !is_redelegation { + // `val updatedRedelegatedUnbonded` with updates applied below + // Delegator's redelegated unbonds to this validator. + let delegator_redelegated_unbonded = + delegator_redelegated_unbonds_handle(source).at(validator); - let params = read_pos_params(storage)?; - let offset = offset_opt.unwrap_or(params.pipeline_len); - let offset_epoch = current_epoch + offset; + // Quint `def updateRedelegatedUnbonded` with `val + // updatedRedelegatedUnbonded` together with last statement + // in `updatedDelegator.with("redelegatedUnbonded", ...` updated + // directly in storage + for (start, unbonds) in &new_redelegated_unbonds { + let this_redelegated_unbonded = delegator_redelegated_unbonded + .at(start) + .at(&withdrawable_epoch); - // Check that the validator is actually a validator - let validator_state_handle = validator_state_handle(validator); - let state = validator_state_handle.get(storage, offset_epoch, ¶ms)?; - if state.is_none() { - return Err(BondError::NotAValidator(validator.clone()).into()); + // Update the delegator's redelegated unbonds with the change + for (src_validator, redelegated_unbonds) in unbonds { + let redelegated_unbonded = + this_redelegated_unbonded.at(src_validator); + for (&redelegation_epoch, &change) in redelegated_unbonds { + redelegated_unbonded.update( + storage, + redelegation_epoch, + |current| current.unwrap_or_default() + change, + )?; + } + } + } } + // all `val updatedDelegator` changes are applied at this point - let bond_handle = bond_handle(source, validator); - let total_bonded_handle = total_bonded_handle(validator); - - if tracing::level_enabled!(tracing::Level::DEBUG) { - let bonds = find_bonds(storage, source, validator)?; - tracing::debug!("\nBonds before incrementing: {bonds:#?}"); + // `val updatedTotalBonded` and `val updatedTotalUnbonded` with updates + // Update the validator's total bonded and unbonded amounts + let total_bonded = total_bonded_handle(validator).get_data_handler(); + let total_unbonded = total_unbonded_handle(validator).at(&pipeline_epoch); + for (&start_epoch, &amount) in &new_unbonds_map { + total_bonded.update(storage, start_epoch, |current| { + current.unwrap_or_default() - amount + })?; + total_unbonded.update(storage, start_epoch, |current| { + current.unwrap_or_default() + amount + })?; } - // Initialize or update the bond at the pipeline offset - bond_handle.add(storage, amount, current_epoch, offset)?; - total_bonded_handle.add(storage, amount, current_epoch, offset)?; - - if tracing::level_enabled!(tracing::Level::DEBUG) { - let bonds = find_bonds(storage, source, validator)?; - tracing::debug!("\nBonds after incrementing: {bonds:#?}"); - } + let total_redelegated_bonded = + validator_total_redelegated_bonded_handle(validator); + let total_redelegated_unbonded = + validator_total_redelegated_unbonded_handle(validator); + for (redelegation_start_epoch, unbonds) in &new_redelegated_unbonds { + for (src_validator, changes) in unbonds { + for (bond_start_epoch, change) in changes { + // total redelegated bonded + let bonded_sub_map = total_redelegated_bonded + .at(redelegation_start_epoch) + .at(src_validator); + bonded_sub_map.update( + storage, + *bond_start_epoch, + |current| current.unwrap_or_default() - *change, + )?; - // Update the validator set - // Allow bonding even if the validator is jailed. However, if jailed, there - // must be no changes to the validator set. Check at the pipeline epoch. - let is_jailed_or_inactive_at_pipeline = matches!( - validator_state_handle.get(storage, offset_epoch, ¶ms)?, - Some(ValidatorState::Jailed) | Some(ValidatorState::Inactive) - ); - if !is_jailed_or_inactive_at_pipeline { - update_validator_set( - storage, - ¶ms, - validator, - amount.change(), - current_epoch, - offset_opt, - )?; + // total redelegated unbonded + let unbonded_sub_map = total_redelegated_unbonded + .at(&pipeline_epoch) + .at(redelegation_start_epoch) + .at(src_validator); + unbonded_sub_map.update( + storage, + *bond_start_epoch, + |current| current.unwrap_or_default() + *change, + )?; + } + } } - // Update the validator and total deltas - update_validator_deltas( + let slashes = find_validator_slashes(storage, validator)?; + // `val resultSlashing` + let result_slashing = compute_amount_after_slashing_unbond( storage, ¶ms, - validator, - amount.change(), - current_epoch, - offset_opt, - )?; - - update_total_deltas( - storage, - ¶ms, - amount.change(), - current_epoch, - offset_opt, + &new_unbonds_map, + &new_redelegated_unbonds, + slashes, )?; + #[cfg(debug_assertions)] + let redel_bonds_post = redelegated_bonds.collect_map(storage)?; + debug_assert!( + result_slashing.sum <= amount, + "Amount after slashing ({}) must be <= requested amount to unbond \ + ({}).", + result_slashing.sum.to_string_native(), + amount.to_string_native(), + ); - Ok(()) -} - -/// Insert the new validator into the right validator set (depending on its -/// stake) -fn insert_validator_into_validator_set( - storage: &mut S, - params: &PosParams, - address: &Address, - stake: token::Amount, - current_epoch: Epoch, - offset: u64, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - let target_epoch = current_epoch + offset; - let consensus_set = consensus_validator_set_handle().at(&target_epoch); - let below_cap_set = below_capacity_validator_set_handle().at(&target_epoch); - - let num_consensus_validators = - get_num_consensus_validators(storage, target_epoch)?; - - if stake < params.validator_stake_threshold { - validator_state_handle(address).set( - storage, - ValidatorState::BelowThreshold, - current_epoch, - offset, - )?; - } else if num_consensus_validators < params.max_validator_slots { - insert_validator_into_set( - &consensus_set.at(&stake), + let change_after_slashing = -result_slashing.sum.change(); + // Update the validator set at the pipeline offset. Since unbonding from a + // jailed validator who is no longer frozen is allowed, only update the + // validator set if the validator is not jailed + let is_jailed_or_inactive_at_pipeline = matches!( + validator_state_handle(validator).get( storage, - &target_epoch, - address, - )?; - validator_state_handle(address).set( + pipeline_epoch, + ¶ms + )?, + Some(ValidatorState::Jailed) | Some(ValidatorState::Inactive) + ); + if !is_jailed_or_inactive_at_pipeline { + update_validator_set( storage, - ValidatorState::Consensus, + ¶ms, + validator, + change_after_slashing, current_epoch, - offset, + None, )?; - } else { - // Check to see if the current genesis validator should replace one - // already in the consensus set - let min_consensus_amount = - get_min_consensus_validator_amount(&consensus_set, storage)?; - if stake > min_consensus_amount { - // Swap this genesis validator in and demote the last min consensus - // validator - let min_consensus_handle = consensus_set.at(&min_consensus_amount); - // Remove last min consensus validator - let last_min_consensus_position = - find_last_position(&min_consensus_handle, storage)?.expect( - "There must be always be at least 1 consensus validator", - ); - let removed = min_consensus_handle - .remove(storage, &last_min_consensus_position)? - .expect( - "There must be always be at least 1 consensus validator", - ); - // Insert last min consensus validator into the below-capacity set - insert_validator_into_set( - &below_cap_set.at(&min_consensus_amount.into()), - storage, - &target_epoch, - &removed, - )?; - validator_state_handle(&removed).set( - storage, - ValidatorState::BelowCapacity, - current_epoch, - offset, - )?; - // Insert the current genesis validator into the consensus set - insert_validator_into_set( - &consensus_set.at(&stake), - storage, - &target_epoch, - address, - )?; - // Update and set the validator states - validator_state_handle(address).set( - storage, - ValidatorState::Consensus, - current_epoch, - offset, - )?; - } else { - // Insert the current genesis validator into the below-capacity set - insert_validator_into_set( - &below_cap_set.at(&stake.into()), - storage, - &target_epoch, - address, - )?; - validator_state_handle(address).set( - storage, - ValidatorState::BelowCapacity, - current_epoch, - offset, - )?; - } - } - Ok(()) -} - -/// Update validator set at the pipeline epoch when a validator receives a new -/// bond and when its bond is unbonded (self-bond or delegation). -fn update_validator_set( - storage: &mut S, - params: &PosParams, - validator: &Address, - token_change: token::Change, - current_epoch: Epoch, - offset: Option, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - if token_change.is_zero() { - return Ok(()); - } - let offset = offset.unwrap_or(params.pipeline_len); - let epoch = current_epoch + offset; - tracing::debug!( - "Update epoch for validator set: {epoch}, validator: {validator}" - ); - let consensus_validator_set = consensus_validator_set_handle(); - let below_capacity_validator_set = below_capacity_validator_set_handle(); - - // Validator sets at the pipeline offset - let consensus_val_handle = consensus_validator_set.at(&epoch); - let below_capacity_val_handle = below_capacity_validator_set.at(&epoch); - - let tokens_pre = read_validator_stake(storage, params, validator, epoch)?; - - let tokens_post = tokens_pre - .change() - .checked_add(&token_change) - .expect("Post-validator set update token amount has overflowed"); - debug_assert!(tokens_post.non_negative()); - let tokens_post = token::Amount::from_change(tokens_post); - - // If token amounts both before and after the action are below the threshold - // stake, do nothing - if tokens_pre < params.validator_stake_threshold - && tokens_post < params.validator_stake_threshold - { - return Ok(()); - } - - // The position is only set when the validator is in consensus or - // below_capacity set (not in below_threshold set) - let position = - read_validator_set_position(storage, validator, epoch, params)?; - if let Some(position) = position { - let consensus_vals_pre = consensus_val_handle.at(&tokens_pre); - - let in_consensus = if consensus_vals_pre.contains(storage, &position)? { - let val_address = consensus_vals_pre.get(storage, &position)?; - debug_assert!(val_address.is_some()); - val_address == Some(validator.clone()) - } else { - false - }; - - if in_consensus { - // It's initially consensus - tracing::debug!("Target validator is consensus"); - - // First remove the consensus validator - consensus_vals_pre.remove(storage, &position)?; - - let max_below_capacity_validator_amount = - get_max_below_capacity_validator_amount( - &below_capacity_val_handle, - storage, - )? - .unwrap_or_default(); - - if tokens_post < params.validator_stake_threshold { - tracing::debug!( - "Demoting this validator to the below-threshold set" - ); - // Set the validator state as below-threshold - validator_state_handle(validator).set( - storage, - ValidatorState::BelowThreshold, - current_epoch, - offset, - )?; - - // Remove the validator's position from storage - validator_set_positions_handle() - .at(&epoch) - .remove(storage, validator)?; - - // Promote the next below-cap validator if there is one - if let Some(max_bc_amount) = - get_max_below_capacity_validator_amount( - &below_capacity_val_handle, - storage, - )? - { - // Remove the max below-capacity validator first - let below_capacity_vals_max = - below_capacity_val_handle.at(&max_bc_amount.into()); - let lowest_position = - find_first_position(&below_capacity_vals_max, storage)? - .unwrap(); - let removed_max_below_capacity = below_capacity_vals_max - .remove(storage, &lowest_position)? - .expect("Must have been removed"); - - // Insert the previous max below-capacity validator into the - // consensus set - insert_validator_into_set( - &consensus_val_handle.at(&max_bc_amount), - storage, - &epoch, - &removed_max_below_capacity, - )?; - validator_state_handle(&removed_max_below_capacity).set( - storage, - ValidatorState::Consensus, - current_epoch, - offset, - )?; - } - } else if tokens_post < max_below_capacity_validator_amount { - tracing::debug!( - "Demoting this validator to the below-capacity set and \ - promoting another to the consensus set" - ); - // Place the validator into the below-capacity set and promote - // the lowest position max below-capacity - // validator. - - // Remove the max below-capacity validator first - let below_capacity_vals_max = below_capacity_val_handle - .at(&max_below_capacity_validator_amount.into()); - let lowest_position = - find_first_position(&below_capacity_vals_max, storage)? - .unwrap(); - let removed_max_below_capacity = below_capacity_vals_max - .remove(storage, &lowest_position)? - .expect("Must have been removed"); - - // Insert the previous max below-capacity validator into the - // consensus set - insert_validator_into_set( - &consensus_val_handle - .at(&max_below_capacity_validator_amount), - storage, - &epoch, - &removed_max_below_capacity, - )?; - validator_state_handle(&removed_max_below_capacity).set( - storage, - ValidatorState::Consensus, - current_epoch, - offset, - )?; - - // Insert the current validator into the below-capacity set - insert_validator_into_set( - &below_capacity_val_handle.at(&tokens_post.into()), - storage, - &epoch, - validator, - )?; - validator_state_handle(validator).set( - storage, - ValidatorState::BelowCapacity, - current_epoch, - offset, - )?; - } else { - tracing::debug!("Validator remains in consensus set"); - // The current validator should remain in the consensus set - - // place it into a new position - insert_validator_into_set( - &consensus_val_handle.at(&tokens_post), - storage, - &epoch, - validator, - )?; - } - } else { - // It's initially below-capacity - tracing::debug!("Target validator is below-capacity"); - - let below_capacity_vals_pre = - below_capacity_val_handle.at(&tokens_pre.into()); - let removed = below_capacity_vals_pre.remove(storage, &position)?; - debug_assert!(removed.is_some()); - debug_assert_eq!(&removed.unwrap(), validator); - - let min_consensus_validator_amount = - get_min_consensus_validator_amount( - &consensus_val_handle, - storage, - )?; - - if tokens_post > min_consensus_validator_amount { - // Place the validator into the consensus set and demote the - // last position min consensus validator to the - // below-capacity set - tracing::debug!( - "Inserting validator into the consensus set and demoting \ - a consensus validator to the below-capacity set" - ); - - insert_into_consensus_and_demote_to_below_cap( - storage, - validator, - tokens_post, - min_consensus_validator_amount, - current_epoch, - offset, - &consensus_val_handle, - &below_capacity_val_handle, - )?; - } else if tokens_post >= params.validator_stake_threshold { - tracing::debug!("Validator remains in below-capacity set"); - // The current validator should remain in the below-capacity set - insert_validator_into_set( - &below_capacity_val_handle.at(&tokens_post.into()), - storage, - &epoch, - validator, - )?; - validator_state_handle(validator).set( - storage, - ValidatorState::BelowCapacity, - current_epoch, - offset, - )?; - } else { - // The current validator is demoted to the below-threshold set - tracing::debug!( - "Demoting this validator to the below-threshold set" - ); - - validator_state_handle(validator).set( - storage, - ValidatorState::BelowThreshold, - current_epoch, - offset, - )?; - - // Remove the validator's position from storage - validator_set_positions_handle() - .at(&epoch) - .remove(storage, validator)?; - } - } - } else { - // At non-zero offset (0 is genesis only) - if offset > 0 { - // If there is no position at pipeline offset, then the validator - // must be in the below-threshold set - debug_assert!(tokens_pre < params.validator_stake_threshold); - } - tracing::debug!("Target validator is below-threshold"); - - // Move the validator into the appropriate set - let num_consensus_validators = - get_num_consensus_validators(storage, epoch)?; - if num_consensus_validators < params.max_validator_slots { - // Just insert into the consensus set - tracing::debug!("Inserting validator into the consensus set"); - - insert_validator_into_set( - &consensus_val_handle.at(&tokens_post), - storage, - &epoch, - validator, - )?; - validator_state_handle(validator).set( - storage, - ValidatorState::Consensus, - current_epoch, - offset, - )?; - } else { - let min_consensus_validator_amount = - get_min_consensus_validator_amount( - &consensus_val_handle, - storage, - )?; - if tokens_post > min_consensus_validator_amount { - // Insert this validator into consensus and demote one into the - // below-capacity - tracing::debug!( - "Inserting validator into the consensus set and demoting \ - a consensus validator to the below-capacity set" - ); - - insert_into_consensus_and_demote_to_below_cap( - storage, - validator, - tokens_post, - min_consensus_validator_amount, - current_epoch, - offset, - &consensus_val_handle, - &below_capacity_val_handle, - )?; - } else { - // Insert this validator into below-capacity - tracing::debug!( - "Inserting validator into the below-capacity set" - ); - - insert_validator_into_set( - &below_capacity_val_handle.at(&tokens_post.into()), - storage, - &epoch, - validator, - )?; - validator_state_handle(validator).set( - storage, - ValidatorState::BelowCapacity, - current_epoch, - offset, - )?; - } - } } - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -fn insert_into_consensus_and_demote_to_below_cap( - storage: &mut S, - validator: &Address, - tokens_post: token::Amount, - min_consensus_amount: token::Amount, - current_epoch: Epoch, - offset: u64, - consensus_set: &ConsensusValidatorSet, - below_capacity_set: &BelowCapacityValidatorSet, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - // First, remove the last position min consensus validator - let consensus_vals_min = consensus_set.at(&min_consensus_amount); - let last_position_of_min_consensus_vals = - find_last_position(&consensus_vals_min, storage)? - .expect("There must be always be at least 1 consensus validator"); - let removed_min_consensus = consensus_vals_min - .remove(storage, &last_position_of_min_consensus_vals)? - .expect("There must be always be at least 1 consensus validator"); - - let offset_epoch = current_epoch + offset; - - // Insert the min consensus validator into the below-capacity - // set - insert_validator_into_set( - &below_capacity_set.at(&min_consensus_amount.into()), - storage, - &offset_epoch, - &removed_min_consensus, - )?; - validator_state_handle(&removed_min_consensus).set( - storage, - ValidatorState::BelowCapacity, - current_epoch, - offset, - )?; - - // Insert the current validator into the consensus set - insert_validator_into_set( - &consensus_set.at(&tokens_post), + // Update the validator and total deltas at the pipeline offset + update_validator_deltas( storage, - &offset_epoch, + ¶ms, validator, + change_after_slashing, + current_epoch, + None, )?; - validator_state_handle(validator).set( + update_total_deltas( storage, - ValidatorState::Consensus, + ¶ms, + change_after_slashing, current_epoch, - offset, + None, )?; - Ok(()) -} - -/// Copy the consensus and below-capacity validator sets and positions into a -/// future epoch. Also copies the epoched set of all known validators in the -/// network. -pub fn copy_validator_sets_and_positions( - storage: &mut S, - params: &PosParams, - current_epoch: Epoch, - target_epoch: Epoch, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - let prev_epoch = target_epoch.prev(); - - let consensus_validator_set = consensus_validator_set_handle(); - let below_capacity_validator_set = below_capacity_validator_set_handle(); - - let (consensus, below_capacity) = ( - consensus_validator_set.at(&prev_epoch), - below_capacity_validator_set.at(&prev_epoch), - ); - debug_assert!(!consensus.is_empty(storage)?); - - // Need to copy into memory here to avoid borrowing a ref - // simultaneously as immutable and mutable - let mut consensus_in_mem: HashMap<(token::Amount, Position), Address> = - HashMap::new(); - let mut below_cap_in_mem: HashMap< - (ReverseOrdTokenAmount, Position), - Address, - > = HashMap::new(); - - for val in consensus.iter(storage)? { - let ( - NestedSubKey::Data { - key: stake, - nested_sub_key: SubKey::Data(position), - }, - address, - ) = val?; - consensus_in_mem.insert((stake, position), address); - } - for val in below_capacity.iter(storage)? { - let ( - NestedSubKey::Data { - key: stake, - nested_sub_key: SubKey::Data(position), - }, - address, - ) = val?; - below_cap_in_mem.insert((stake, position), address); - } - for ((val_stake, val_position), val_address) in consensus_in_mem.into_iter() - { - consensus_validator_set - .at(&target_epoch) - .at(&val_stake) - .insert(storage, val_position, val_address)?; + if tracing::level_enabled!(tracing::Level::DEBUG) { + let bonds = find_bonds(storage, source, validator)?; + tracing::debug!("\nBonds after decrementing: {bonds:#?}"); } - for ((val_stake, val_position), val_address) in below_cap_in_mem.into_iter() + // Invariant: in the affected epochs, the delta of bonds must be >= delta of + // redelegated bonds deltas sum + #[cfg(debug_assertions)] { - below_capacity_validator_set - .at(&target_epoch) - .at(&val_stake) - .insert(storage, val_position, val_address)?; - } - // Purge consensus and below-capacity validator sets - consensus_validator_set.update_data(storage, params, current_epoch)?; - below_capacity_validator_set.update_data(storage, params, current_epoch)?; - - // Copy validator positions - let mut positions = HashMap::::default(); - let validator_set_positions_handle = validator_set_positions_handle(); - let positions_handle = validator_set_positions_handle.at(&prev_epoch); - - for result in positions_handle.iter(storage)? { - let (validator, position) = result?; - positions.insert(validator, position); - } - - let new_positions_handle = validator_set_positions_handle.at(&target_epoch); - for (validator, position) in positions { - let prev = new_positions_handle.insert(storage, validator, position)?; - debug_assert!(prev.is_none()); - } - validator_set_positions_handle.set_last_update(storage, current_epoch)?; - - // Purge old epochs of validator positions - validator_set_positions_handle.update_data( - storage, - params, - current_epoch, - )?; - - // Copy set of all validator addresses - let mut all_validators = HashSet::
::default(); - let validator_addresses_handle = validator_addresses_handle(); - let all_validators_handle = validator_addresses_handle.at(&prev_epoch); - for result in all_validators_handle.iter(storage)? { - let validator = result?; - all_validators.insert(validator); - } - let new_all_validators_handle = - validator_addresses_handle.at(&target_epoch); - for validator in all_validators { - let was_in = new_all_validators_handle.insert(storage, validator)?; - debug_assert!(!was_in); - } - - // Purge old epochs of all validator addresses - validator_addresses_handle.update_data(storage, params, current_epoch)?; - - Ok(()) -} - -/// Compute total validator stake for the current epoch -fn compute_total_consensus_stake( - storage: &S, - epoch: Epoch, -) -> storage_api::Result -where - S: StorageRead, -{ - consensus_validator_set_handle() - .at(&epoch) - .iter(storage)? - .fold(Ok(token::Amount::zero()), |acc, entry| { - let acc = acc?; - let ( - NestedSubKey::Data { - key: amount, - nested_sub_key: _, - }, - _validator, - ) = entry?; - Ok(acc.checked_add(amount).expect( - "Total consensus stake computation should not overflow.", - )) - }) -} - -/// Compute and then store the total consensus stake -pub fn compute_and_store_total_consensus_stake( - storage: &mut S, - epoch: Epoch, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - let total = compute_total_consensus_stake(storage, epoch)?; - tracing::debug!( - "Total consensus stake for epoch {}: {}", - epoch, - total.to_string_native() - ); - total_consensus_stake_key_handle().set(storage, total, epoch, 0) -} - -/// Read the position of the validator in the subset of validators that have the -/// same bonded stake. This information is held in its own epoched structure in -/// addition to being inside the validator sets. -fn read_validator_set_position( - storage: &S, - validator: &Address, - epoch: Epoch, - _params: &PosParams, -) -> storage_api::Result> -where - S: StorageRead, -{ - let handle = validator_set_positions_handle(); - handle.get_data_handler().at(&epoch).get(storage, validator) -} - -/// Find the first (lowest) position in a validator set if it is not empty -fn find_first_position( - handle: &ValidatorPositionAddresses, - storage: &S, -) -> storage_api::Result> -where - S: StorageRead, -{ - let lowest_position = handle - .iter(storage)? - .next() - .transpose()? - .map(|(position, _addr)| position); - Ok(lowest_position) -} - -/// Find the last (greatest) position in a validator set if it is not empty -fn find_last_position( - handle: &ValidatorPositionAddresses, - storage: &S, -) -> storage_api::Result> -where - S: StorageRead, -{ - let position = handle - .iter(storage)? - .last() - .transpose()? - .map(|(position, _addr)| position); - Ok(position) -} - -/// Find next position in a validator set or 0 if empty -fn find_next_position( - handle: &ValidatorPositionAddresses, - storage: &S, -) -> storage_api::Result -where - S: StorageRead, -{ - let position_iter = handle.iter(storage)?; - let next = position_iter - .last() - .transpose()? - .map(|(position, _address)| position.next()) - .unwrap_or_default(); - Ok(next) -} - -fn get_min_consensus_validator_amount( - handle: &ConsensusValidatorSet, - storage: &S, -) -> storage_api::Result -where - S: StorageRead, -{ - Ok(handle - .iter(storage)? - .next() - .transpose()? - .map(|(subkey, _address)| match subkey { - NestedSubKey::Data { - key, - nested_sub_key: _, - } => key, - }) - .unwrap_or_default()) -} - -/// Returns `Ok(None)` when the below capacity set is empty. -fn get_max_below_capacity_validator_amount( - handle: &BelowCapacityValidatorSet, - storage: &S, -) -> storage_api::Result> -where - S: StorageRead, -{ - Ok(handle - .iter(storage)? - .next() - .transpose()? - .map(|(subkey, _address)| match subkey { - NestedSubKey::Data { - key, - nested_sub_key: _, - } => token::Amount::from(key), - })) -} - -fn insert_validator_into_set( - handle: &ValidatorPositionAddresses, - storage: &mut S, - epoch: &Epoch, - address: &Address, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - let next_position = find_next_position(handle, storage)?; - tracing::debug!( - "Inserting validator {} into position {:?} at epoch {}", - address.clone(), - next_position.clone(), - epoch.clone() - ); - handle.insert(storage, next_position, address.clone())?; - validator_set_positions_handle().at(epoch).insert( - storage, - address.clone(), - next_position, - )?; - Ok(()) -} - -/// Used below in `fn unbond_tokens` to update the bond and unbond amounts -#[derive(Eq, Hash, PartialEq)] -struct BondAndUnbondUpdates { - bond_start: Epoch, - new_bond_value: token::Change, - unbond_value: token::Change, -} - -/// Temp: In quint this is from `ResultUnbondTx` field `resultSlashing: {sum: -/// int, epochMap: Epoch -> int}` -#[derive(Debug, Default)] -pub struct ResultSlashing { - /// The token amount unbonded from the validator stake after accounting for - /// slashes - pub sum: token::Amount, - /// Map from bond start epoch to token amount after slashing - pub epoch_map: BTreeMap, -} - -/// Unbond tokens that are bonded between a validator and a source (self or -/// delegator). -/// -/// This fn is also called during redelegation for a source validator, in -/// which case the `is_redelegation` param must be true. -pub fn unbond_tokens( - storage: &mut S, - source: Option<&Address>, - validator: &Address, - amount: token::Amount, - current_epoch: Epoch, - is_redelegation: bool, -) -> storage_api::Result -where - S: StorageRead + StorageWrite, -{ - if amount.is_zero() { - return Ok(ResultSlashing::default()); - } - - let params = read_pos_params(storage)?; - let pipeline_epoch = current_epoch + params.pipeline_len; - let withdrawable_epoch = current_epoch + params.withdrawable_epoch_offset(); - tracing::debug!( - "Unbonding token amount {} at epoch {}, withdrawable at epoch {}", - amount.to_string_native(), - current_epoch, - withdrawable_epoch - ); - - // Make sure source is not some other validator - if let Some(source) = source { - if source != validator && is_validator(storage, source)? { - return Err( - BondError::SourceMustNotBeAValidator(source.clone()).into() - ); - } - } - // Make sure the target is actually a validator - if !is_validator(storage, validator)? { - return Err(BondError::NotAValidator(validator.clone()).into()); - } - // Make sure the validator is not currently frozen - if is_validator_frozen(storage, validator, current_epoch, ¶ms)? { - return Err(UnbondError::ValidatorIsFrozen(validator.clone()).into()); - } - - let source = source.unwrap_or(validator); - let bonds_handle = bond_handle(source, validator); - - // Make sure there are enough tokens left in the bond at the pipeline offset - let remaining_at_pipeline = bonds_handle - .get_sum(storage, pipeline_epoch, ¶ms)? - .unwrap_or_default(); - if amount > remaining_at_pipeline { - return Err(UnbondError::UnbondAmountGreaterThanBond( - amount.to_string_native(), - remaining_at_pipeline.to_string_native(), - ) - .into()); - } - - if tracing::level_enabled!(tracing::Level::DEBUG) { - let bonds = find_bonds(storage, source, validator)?; - tracing::debug!("\nBonds before decrementing: {bonds:#?}"); - } - - let unbonds = unbond_handle(source, validator); - - let redelegated_bonds = - delegator_redelegated_bonds_handle(source).at(validator); - - #[cfg(debug_assertions)] - let redel_bonds_pre = redelegated_bonds.collect_map(storage)?; - - // `resultUnbonding` - // Find the bonds to fully unbond (remove) and one to partially unbond, if - // necessary - let bonds_to_unbond = find_bonds_to_remove( - storage, - &bonds_handle.get_data_handler(), - amount, - )?; - - // `modifiedRedelegation` - // A bond may have both redelegated and non-redelegated tokens in it. If - // this is the case, compute the modified state of the redelegation. - let modified_redelegation = match bonds_to_unbond.new_entry { - Some((bond_epoch, new_bond_amount)) => { - if redelegated_bonds.contains(storage, &bond_epoch)? { - let cur_bond_amount = bonds_handle - .get_delta_val(storage, bond_epoch)? - .unwrap_or_default(); - compute_modified_redelegation( - storage, - &redelegated_bonds.at(&bond_epoch), - bond_epoch, - cur_bond_amount - new_bond_amount, - )? - } else { - ModifiedRedelegation::default() - } - } - None => ModifiedRedelegation::default(), - }; - - // Compute the new unbonds eagerly - // `keysUnbonds` - // Get a set of epochs from which we're unbonding (fully and partially). - let bond_epochs_to_unbond = - if let Some((start_epoch, _)) = bonds_to_unbond.new_entry { - let mut to_remove = bonds_to_unbond.epochs.clone(); - to_remove.insert(start_epoch); - to_remove - } else { - bonds_to_unbond.epochs.clone() - }; - - // `newUnbonds` - // For each epoch we're unbonding, find the amount that's being unbonded. - // For full unbonds, this is the current bond value. For partial unbonds - // it is a difference between the current and new bond amount. - let new_unbonds_map = bond_epochs_to_unbond - .into_iter() - .map(|epoch| { - let cur_bond_value = bonds_handle - .get_delta_val(storage, epoch) - .unwrap() - .unwrap_or_default(); - let value = if let Some((start_epoch, new_bond_amount)) = - bonds_to_unbond.new_entry - { - if start_epoch == epoch { - cur_bond_value - new_bond_amount - } else { - cur_bond_value - } - } else { - cur_bond_value - }; - (epoch, value) - }) - .collect::>(); - - // `updatedBonded` - // Remove bonds for all the full unbonds. - for epoch in &bonds_to_unbond.epochs { - bonds_handle.get_data_handler().remove(storage, epoch)?; - } - // Replace bond amount for partial unbond, if any. - if let Some((bond_epoch, new_bond_amount)) = bonds_to_unbond.new_entry { - bonds_handle.set(storage, new_bond_amount, bond_epoch, 0)?; - } - - // `updatedUnbonded` - // Update the unbonds in storage using the eager map computed above - if !is_redelegation { - for (start_epoch, &unbond_amount) in new_unbonds_map.iter() { - unbonds.at(start_epoch).update( - storage, - withdrawable_epoch, - |cur_val| cur_val.unwrap_or_default() + unbond_amount, - )?; - } - } - - // `newRedelegatedUnbonds` - // This is what the delegator's redelegated unbonds would look like if this - // was the only unbond in the PoS system. We need to add these redelegated - // unbonds to the existing redelegated unbonds - let new_redelegated_unbonds = compute_new_redelegated_unbonds( - storage, - &redelegated_bonds, - &bonds_to_unbond.epochs, - &modified_redelegation, - )?; - - // `updatedRedelegatedBonded` - // NOTE: for now put this here after redelegated unbonds calc bc that one - // uses the pre-modified redelegated bonds from storage! - // First remove redelegation entries in epochs with full unbonds. - for epoch_to_remove in &bonds_to_unbond.epochs { - redelegated_bonds.remove_all(storage, epoch_to_remove)?; - } - if let Some(epoch) = modified_redelegation.epoch { - tracing::debug!("\nIs modified redelegation"); - if modified_redelegation.validators_to_remove.is_empty() { - redelegated_bonds.remove_all(storage, &epoch)?; - } else { - // Then update the redelegated bonds at this epoch - let rbonds = redelegated_bonds.at(&epoch); - update_redelegated_bonds(storage, &rbonds, &modified_redelegation)?; - } - } - - if !is_redelegation { - // `val updatedRedelegatedUnbonded` with updates applied below - // Delegator's redelegated unbonds to this validator. - let delegator_redelegated_unbonded = - delegator_redelegated_unbonds_handle(source).at(validator); - - // Quint `def updateRedelegatedUnbonded` with `val - // updatedRedelegatedUnbonded` together with last statement - // in `updatedDelegator.with("redelegatedUnbonded", ...` updated - // directly in storage - for (start, unbonds) in &new_redelegated_unbonds { - let this_redelegated_unbonded = delegator_redelegated_unbonded - .at(start) - .at(&withdrawable_epoch); - - // Update the delegator's redelegated unbonds with the change - for (src_validator, redelegated_unbonds) in unbonds { - let redelegated_unbonded = - this_redelegated_unbonded.at(src_validator); - for (&redelegation_epoch, &change) in redelegated_unbonds { - redelegated_unbonded.update( - storage, - redelegation_epoch, - |current| current.unwrap_or_default() + change, - )?; - } - } - } - } - // all `val updatedDelegator` changes are applied at this point - - // `val updatedTotalBonded` and `val updatedTotalUnbonded` with updates - // Update the validator's total bonded and unbonded amounts - let total_bonded = total_bonded_handle(validator).get_data_handler(); - let total_unbonded = total_unbonded_handle(validator).at(&pipeline_epoch); - for (&start_epoch, &amount) in &new_unbonds_map { - total_bonded.update(storage, start_epoch, |current| { - current.unwrap_or_default() - amount - })?; - total_unbonded.update(storage, start_epoch, |current| { - current.unwrap_or_default() + amount - })?; - } - - let total_redelegated_bonded = - validator_total_redelegated_bonded_handle(validator); - let total_redelegated_unbonded = - validator_total_redelegated_unbonded_handle(validator); - for (redelegation_start_epoch, unbonds) in &new_redelegated_unbonds { - for (src_validator, changes) in unbonds { - for (bond_start_epoch, change) in changes { - // total redelegated bonded - let bonded_sub_map = total_redelegated_bonded - .at(redelegation_start_epoch) - .at(src_validator); - bonded_sub_map.update( - storage, - *bond_start_epoch, - |current| current.unwrap_or_default() - *change, - )?; - - // total redelegated unbonded - let unbonded_sub_map = total_redelegated_unbonded - .at(&pipeline_epoch) - .at(redelegation_start_epoch) - .at(src_validator); - unbonded_sub_map.update( - storage, - *bond_start_epoch, - |current| current.unwrap_or_default() + *change, - )?; - } - } - } - - let slashes = find_validator_slashes(storage, validator)?; - // `val resultSlashing` - let result_slashing = compute_amount_after_slashing_unbond( - storage, - ¶ms, - &new_unbonds_map, - &new_redelegated_unbonds, - slashes, - )?; - #[cfg(debug_assertions)] - let redel_bonds_post = redelegated_bonds.collect_map(storage)?; - debug_assert!( - result_slashing.sum <= amount, - "Amount after slashing ({}) must be <= requested amount to unbond \ - ({}).", - result_slashing.sum.to_string_native(), - amount.to_string_native(), - ); - - let change_after_slashing = -result_slashing.sum.change(); - // Update the validator set at the pipeline offset. Since unbonding from a - // jailed validator who is no longer frozen is allowed, only update the - // validator set if the validator is not jailed - let is_jailed_or_inactive_at_pipeline = matches!( - validator_state_handle(validator).get( - storage, - pipeline_epoch, - ¶ms - )?, - Some(ValidatorState::Jailed) | Some(ValidatorState::Inactive) - ); - if !is_jailed_or_inactive_at_pipeline { - update_validator_set( - storage, - ¶ms, - validator, - change_after_slashing, - current_epoch, - None, - )?; - } - - // Update the validator and total deltas at the pipeline offset - update_validator_deltas( - storage, - ¶ms, - validator, - change_after_slashing, - current_epoch, - None, - )?; - update_total_deltas( - storage, - ¶ms, - change_after_slashing, - current_epoch, - None, - )?; - - if tracing::level_enabled!(tracing::Level::DEBUG) { - let bonds = find_bonds(storage, source, validator)?; - tracing::debug!("\nBonds after decrementing: {bonds:#?}"); - } - - // Invariant: in the affected epochs, the delta of bonds must be >= delta of - // redelegated bonds deltas sum - #[cfg(debug_assertions)] - { - let mut epochs = bonds_to_unbond.epochs.clone(); - if let Some((epoch, _)) = bonds_to_unbond.new_entry { - epochs.insert(epoch); - } - for epoch in epochs { - let cur_bond = bonds_handle - .get_delta_val(storage, epoch)? - .unwrap_or_default(); - let redelegated_deltas = redelegated_bonds - .at(&epoch) - // Sum of redelegations from any src validator - .collect_map(storage)? - .into_values() - .map(|redeleg| redeleg.into_values().sum()) - .sum(); - debug_assert!( - cur_bond >= redelegated_deltas, - "After unbonding, in epoch {epoch} the bond amount {} must be \ - >= redelegated deltas at pipeline {}.\n\nredelegated_bonds \ - pre: {redel_bonds_pre:#?}\nredelegated_bonds post: \ - {redel_bonds_post:#?},\nmodified_redelegation: \ - {modified_redelegation:#?},\nbonds_to_unbond: \ - {bonds_to_unbond:#?}", - cur_bond.to_string_native(), - redelegated_deltas.to_string_native() - ); - } - } - - // Tally rewards (only call if this is not the first epoch) - if current_epoch > Epoch::default() { - let mut rewards = token::Amount::zero(); - - let last_claim_epoch = - get_last_reward_claim_epoch(storage, source, validator)? - .unwrap_or_default(); - let rewards_products = validator_rewards_products_handle(validator); - - for (start_epoch, slashed_amount) in &result_slashing.epoch_map { - // Stop collecting rewards at the moment the unbond is initiated - // (right now) - for ep in - Epoch::iter_bounds_inclusive(*start_epoch, current_epoch.prev()) - { - // Consider the last epoch when rewards were claimed - if ep < last_claim_epoch { - continue; - } - let rp = - rewards_products.get(storage, &ep)?.unwrap_or_default(); - rewards += rp * (*slashed_amount); - } - } - - // Update the rewards from the current unbonds first - add_rewards_to_counter(storage, source, validator, rewards)?; - } - - Ok(result_slashing) -} - -#[derive(Debug, Default, Eq, PartialEq)] -struct FoldRedelegatedBondsResult { - total_redelegated: token::Amount, - total_after_slashing: token::Amount, -} - -/// Iterates over a `redelegated_unbonds` and computes the both the sum of all -/// redelegated tokens and how much is left after applying all relevant slashes. -// `def foldAndSlashRedelegatedBondsMap` -fn fold_and_slash_redelegated_bonds( - storage: &S, - params: &OwnedPosParams, - redelegated_unbonds: &EagerRedelegatedBondsMap, - start_epoch: Epoch, - list_slashes: &[Slash], - slash_epoch_filter: impl Fn(Epoch) -> bool, -) -> FoldRedelegatedBondsResult -where - S: StorageRead, -{ - let mut result = FoldRedelegatedBondsResult::default(); - for (src_validator, bonds_map) in redelegated_unbonds { - for (bond_start, &change) in bonds_map { - // Merge the two lists of slashes - let mut merged: Vec = - // Look-up slashes for this validator ... - validator_slashes_handle(src_validator) - .iter(storage) - .unwrap() - .map(Result::unwrap) - .filter(|slash| { - params.in_redelegation_slashing_window( - slash.epoch, - params.redelegation_start_epoch_from_end( - start_epoch, - ), - start_epoch, - ) && *bond_start <= slash.epoch - && slash_epoch_filter(slash.epoch) - }) - // ... and add `list_slashes` - .chain(list_slashes.iter().cloned()) - .collect(); - - // Sort slashes by epoch - merged.sort_by(|s1, s2| s1.epoch.partial_cmp(&s2.epoch).unwrap()); - - result.total_redelegated += change; - result.total_after_slashing += - apply_list_slashes(params, &merged, change); - } - } - result -} - -/// Computes how much remains from an amount of tokens after applying a list of -/// slashes. -/// -/// - `slashes` - a list of slashes ordered by misbehaving epoch. -/// - `amount` - the amount of slashable tokens. -// `def applyListSlashes` -fn apply_list_slashes( - params: &OwnedPosParams, - slashes: &[Slash], - amount: token::Amount, -) -> token::Amount { - let mut final_amount = amount; - let mut computed_slashes = BTreeMap::::new(); - for slash in slashes { - let slashed_amount = - compute_slashable_amount(params, slash, amount, &computed_slashes); - final_amount = - final_amount.checked_sub(slashed_amount).unwrap_or_default(); - computed_slashes.insert(slash.epoch, slashed_amount); - } - final_amount -} - -/// Computes how much is left from a bond or unbond after applying a slash given -/// that a set of slashes may have been previously applied. -// `def computeSlashableAmount` -fn compute_slashable_amount( - params: &OwnedPosParams, - slash: &Slash, - amount: token::Amount, - computed_slashes: &BTreeMap, -) -> token::Amount { - let updated_amount = computed_slashes - .iter() - .filter(|(&epoch, _)| { - // Keep slashes that have been applied and processed before the - // current slash occurred. We use `<=` because slashes processed at - // `slash.epoch` (at the start of the epoch) are also processed - // before this slash occurred. - epoch + params.slash_processing_epoch_offset() <= slash.epoch - }) - .fold(amount, |acc, (_, &amnt)| { - acc.checked_sub(amnt).unwrap_or_default() - }); - updated_amount.mul_ceil(slash.rate) -} - -/// Epochs for full and partial unbonds. -#[derive(Debug, Default)] -struct BondsForRemovalRes { - /// Full unbond epochs - pub epochs: BTreeSet, - /// Partial unbond epoch associated with the new bond amount - pub new_entry: Option<(Epoch, token::Amount)>, -} - -/// In decreasing epoch order, decrement the non-zero bond amount entries until -/// the full `amount` has been removed. Returns a `BondsForRemovalRes` object -/// that contains the epochs for which the full bond amount is removed and -/// additionally information for the one epoch whose bond amount is partially -/// removed, if any. -fn find_bonds_to_remove( - storage: &S, - bonds_handle: &LazyMap, - amount: token::Amount, -) -> storage_api::Result -where - S: StorageRead, -{ - #[allow(clippy::needless_collect)] - let bonds: Vec> = bonds_handle.iter(storage)?.collect(); - - let mut bonds_for_removal = BondsForRemovalRes::default(); - let mut remaining = amount; - - for bond in bonds.into_iter().rev() { - let (bond_epoch, bond_amount) = bond?; - let to_unbond = cmp::min(bond_amount, remaining); - if to_unbond == bond_amount { - bonds_for_removal.epochs.insert(bond_epoch); - } else { - bonds_for_removal.new_entry = - Some((bond_epoch, bond_amount - to_unbond)); - } - remaining -= to_unbond; - if remaining.is_zero() { - break; - } - } - Ok(bonds_for_removal) -} - -#[derive(Debug, Default, PartialEq, Eq)] -struct ModifiedRedelegation { - epoch: Option, - validators_to_remove: BTreeSet
, - validator_to_modify: Option
, - epochs_to_remove: BTreeSet, - epoch_to_modify: Option, - new_amount: Option, -} - -/// Used in `fn unbond_tokens` to compute the modified state of a redelegation -/// if redelegated tokens are being unbonded. -fn compute_modified_redelegation( - storage: &S, - redelegated_bonds: &RedelegatedTokens, - start_epoch: Epoch, - amount_to_unbond: token::Amount, -) -> storage_api::Result -where - S: StorageRead, -{ - let mut modified_redelegation = ModifiedRedelegation::default(); - - let mut src_validators = BTreeSet::
::new(); - let mut total_redelegated = token::Amount::zero(); - for rb in redelegated_bonds.iter(storage)? { - let ( - NestedSubKey::Data { - key: src_validator, - nested_sub_key: _, - }, - amount, - ) = rb?; - total_redelegated += amount; - src_validators.insert(src_validator); - } - - modified_redelegation.epoch = Some(start_epoch); - - // If the total amount of redelegated bonds is less than the target amount, - // then all redelegated bonds must be unbonded. - if total_redelegated <= amount_to_unbond { - return Ok(modified_redelegation); - } - - let mut remaining = amount_to_unbond; - for src_validator in src_validators.into_iter() { - if remaining.is_zero() { - break; - } - let rbonds = redelegated_bonds.at(&src_validator); - let total_src_val_amount = rbonds - .iter(storage)? - .map(|res| { - let (_, amount) = res?; - Ok(amount) - }) - .sum::>()?; - - // TODO: move this into the `if total_redelegated <= remaining` branch - // below, then we don't have to remove it in `fn - // update_redelegated_bonds` when `validator_to_modify` is Some (and - // avoid `modified_redelegation.validators_to_remove.clone()`). - // It affects assumption 2. in `fn compute_new_redelegated_unbonds`, but - // that looks trivial to change. - // NOTE: not sure if this TODO is still relevant... - modified_redelegation - .validators_to_remove - .insert(src_validator.clone()); - if total_src_val_amount <= remaining { - remaining -= total_src_val_amount; - } else { - let bonds_to_remove = - find_bonds_to_remove(storage, &rbonds, remaining)?; - - remaining = token::Amount::zero(); - - // NOTE: When there are multiple `src_validators` from which we're - // unbonding, `validator_to_modify` cannot get overridden, because - // only one of them can be a partial unbond (`new_entry` - // is partial unbond) - if let Some((bond_epoch, new_bond_amount)) = - bonds_to_remove.new_entry - { - modified_redelegation.validator_to_modify = Some(src_validator); - modified_redelegation.epochs_to_remove = { - let mut epochs = bonds_to_remove.epochs; - // TODO: remove this insertion then we don't have to remove - // it again in `fn update_redelegated_bonds` - // when `epoch_to_modify` is Some (and avoid - // `modified_redelegation.epochs_to_remove.clone`) - // It affects assumption 3. in `fn - // compute_new_redelegated_unbonds`, but that also looks - // trivial to change. - epochs.insert(bond_epoch); - epochs - }; - modified_redelegation.epoch_to_modify = Some(bond_epoch); - modified_redelegation.new_amount = Some(new_bond_amount); - } else { - modified_redelegation.validator_to_modify = Some(src_validator); - modified_redelegation.epochs_to_remove = bonds_to_remove.epochs; - } - } - } - Ok(modified_redelegation) -} - -fn update_redelegated_bonds( - storage: &mut S, - redelegated_bonds: &RedelegatedTokens, - modified_redelegation: &ModifiedRedelegation, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - if let Some(val_to_modify) = &modified_redelegation.validator_to_modify { - let mut updated_vals_to_remove = - modified_redelegation.validators_to_remove.clone(); - updated_vals_to_remove.remove(val_to_modify); - - // Remove the updated_vals_to_remove keys from the - // redelegated_bonds map - for val in &updated_vals_to_remove { - redelegated_bonds.remove_all(storage, val)?; - } - - if let Some(epoch_to_modify) = modified_redelegation.epoch_to_modify { - let mut updated_epochs_to_remove = - modified_redelegation.epochs_to_remove.clone(); - updated_epochs_to_remove.remove(&epoch_to_modify); - let val_bonds_to_modify = redelegated_bonds.at(val_to_modify); - for epoch in updated_epochs_to_remove { - val_bonds_to_modify.remove(storage, &epoch)?; - } - val_bonds_to_modify.insert( - storage, - epoch_to_modify, - modified_redelegation.new_amount.unwrap(), - )?; - } else { - // Then remove to epochs_to_remove from the redelegated bonds of the - // val_to_modify - let val_bonds_to_modify = redelegated_bonds.at(val_to_modify); - for epoch in &modified_redelegation.epochs_to_remove { - val_bonds_to_modify.remove(storage, epoch)?; - } - } - } else { - // Remove all validators in modified_redelegation.validators_to_remove - // from redelegated_bonds - for val in &modified_redelegation.validators_to_remove { - redelegated_bonds.remove_all(storage, val)?; - } - } - Ok(()) -} - -/// Temp helper type to match quint model. -/// Result of `compute_new_redelegated_unbonds` that contains a map of -/// redelegated unbonds. -/// The map keys from outside in are: -/// -/// - redelegation end epoch where redeleg stops contributing to src validator -/// - src validator address -/// - src bond start epoch where it started contributing to src validator -type EagerRedelegatedUnbonds = BTreeMap; - -/// Computes a map of redelegated unbonds from a set of redelegated bonds. -/// -/// - `redelegated_bonds` - a map of redelegated bonds from epoch to -/// `RedelegatedTokens`. -/// - `epochs_to_remove` - a set of epochs that indicate the set of epochs -/// unbonded. -/// - `modified` record that represents a redelegated bond that it is only -/// partially unbonded. -/// -/// The function assumes that: -/// -/// 1. `modified.epoch` is not in the `epochs_to_remove` set. -/// 2. `modified.validator_to_modify` is in `modified.vals_to_remove`. -/// 3. `modified.epoch_to_modify` is in in `modified.epochs_to_remove`. -// `def computeNewRedelegatedUnbonds` from Quint -fn compute_new_redelegated_unbonds( - storage: &S, - redelegated_bonds: &RedelegatedBondsOrUnbonds, - epochs_to_remove: &BTreeSet, - modified: &ModifiedRedelegation, -) -> storage_api::Result -where - S: StorageRead + StorageWrite, -{ - let unbonded_epochs = if let Some(epoch) = modified.epoch { - debug_assert!( - !epochs_to_remove.contains(&epoch), - "1. assumption in `fn compute_new_redelegated_unbonds` doesn't \ - hold" - ); - let mut epochs = epochs_to_remove.clone(); - epochs.insert(epoch); - epochs - .iter() - .cloned() - .filter(|e| redelegated_bonds.contains(storage, e).unwrap()) - .collect::>() - } else { - epochs_to_remove - .iter() - .cloned() - .filter(|e| redelegated_bonds.contains(storage, e).unwrap()) - .collect::>() - }; - debug_assert!( - modified - .validator_to_modify - .as_ref() - .map(|validator| modified.validators_to_remove.contains(validator)) - .unwrap_or(true), - "2. assumption in `fn compute_new_redelegated_unbonds` doesn't hold" - ); - debug_assert!( - modified - .epoch_to_modify - .as_ref() - .map(|epoch| modified.epochs_to_remove.contains(epoch)) - .unwrap_or(true), - "3. assumption in `fn compute_new_redelegated_unbonds` doesn't hold" - ); - - // quint `newRedelegatedUnbonds` returned from - // `computeNewRedelegatedUnbonds` - let new_redelegated_unbonds: EagerRedelegatedUnbonds = unbonded_epochs - .into_iter() - .map(|start| { - let mut rbonds = EagerRedelegatedBondsMap::default(); - if modified - .epoch - .map(|redelegation_epoch| start != redelegation_epoch) - .unwrap_or(true) - || modified.validators_to_remove.is_empty() - { - for res in redelegated_bonds.at(&start).iter(storage).unwrap() { - let ( - NestedSubKey::Data { - key: validator, - nested_sub_key: SubKey::Data(epoch), - }, - amount, - ) = res.unwrap(); - rbonds - .entry(validator.clone()) - .or_default() - .insert(epoch, amount); - } - (start, rbonds) - } else { - for src_validator in &modified.validators_to_remove { - if modified - .validator_to_modify - .as_ref() - .map(|validator| src_validator != validator) - .unwrap_or(true) - { - let raw_bonds = - redelegated_bonds.at(&start).at(src_validator); - for res in raw_bonds.iter(storage).unwrap() { - let (bond_epoch, bond_amount) = res.unwrap(); - rbonds - .entry(src_validator.clone()) - .or_default() - .insert(bond_epoch, bond_amount); - } - } else { - for bond_start in &modified.epochs_to_remove { - let cur_redel_bond_amount = redelegated_bonds - .at(&start) - .at(src_validator) - .get(storage, bond_start) - .unwrap() - .unwrap_or_default(); - let raw_bonds = rbonds - .entry(src_validator.clone()) - .or_default(); - if modified - .epoch_to_modify - .as_ref() - .map(|epoch| bond_start != epoch) - .unwrap_or(true) - { - raw_bonds - .insert(*bond_start, cur_redel_bond_amount); - } else { - raw_bonds.insert( - *bond_start, - cur_redel_bond_amount - - modified - .new_amount - // Safe unwrap - it shouldn't - // get to - // this if it's None - .unwrap(), - ); - } - } - } - } - (start, rbonds) - } - }) - .collect(); - - Ok(new_redelegated_unbonds) -} - -/// Compute a token amount after slashing, given the initial amount and a set of -/// slashes. It is assumed that the input `slashes` are those committed while -/// the `amount` was contributing to voting power. -fn get_slashed_amount( - params: &PosParams, - amount: token::Amount, - slashes: &BTreeMap, -) -> storage_api::Result { - let mut updated_amount = amount; - let mut computed_amounts = Vec::::new(); - - for (&infraction_epoch, &slash_rate) in slashes { - let mut computed_to_remove = BTreeSet::>::new(); - for (ix, slashed_amount) in computed_amounts.iter().enumerate() { - // Update amount with slashes that happened more than unbonding_len - // epochs before this current slash - if slashed_amount.epoch + params.slash_processing_epoch_offset() - <= infraction_epoch - { - updated_amount = updated_amount - .checked_sub(slashed_amount.amount) - .unwrap_or_default(); - computed_to_remove.insert(Reverse(ix)); - } - } - // Invariant: `computed_to_remove` must be in reverse ord to avoid - // left-shift of the `computed_amounts` after call to `remove` - // invalidating the rest of the indices. - for item in computed_to_remove { - computed_amounts.remove(item.0); - } - computed_amounts.push(SlashedAmount { - amount: updated_amount.mul_ceil(slash_rate), - epoch: infraction_epoch, - }); - } - - let total_computed_amounts = computed_amounts - .into_iter() - .map(|slashed| slashed.amount) - .sum(); - - let final_amount = updated_amount - .checked_sub(total_computed_amounts) - .unwrap_or_default(); - - Ok(final_amount) -} - -// `def computeAmountAfterSlashingUnbond` -fn compute_amount_after_slashing_unbond( - storage: &S, - params: &OwnedPosParams, - unbonds: &BTreeMap, - redelegated_unbonds: &EagerRedelegatedUnbonds, - slashes: Vec, -) -> storage_api::Result -where - S: StorageRead, -{ - let mut result_slashing = ResultSlashing::default(); - for (&start_epoch, amount) in unbonds { - // `val listSlashes` - let list_slashes: Vec = slashes - .iter() - .filter(|slash| slash.epoch >= start_epoch) - .cloned() - .collect(); - // `val resultFold` - let result_fold = if let Some(redelegated_unbonds) = - redelegated_unbonds.get(&start_epoch) - { - fold_and_slash_redelegated_bonds( - storage, - params, - redelegated_unbonds, - start_epoch, - &list_slashes, - |_| true, - ) - } else { - FoldRedelegatedBondsResult::default() - }; - // `val totalNoRedelegated` - let total_not_redelegated = amount - .checked_sub(result_fold.total_redelegated) - .unwrap_or_default(); - // `val afterNoRedelegated` - let after_not_redelegated = - apply_list_slashes(params, &list_slashes, total_not_redelegated); - // `val amountAfterSlashing` - let amount_after_slashing = - after_not_redelegated + result_fold.total_after_slashing; - // Accumulation step - result_slashing.sum += amount_after_slashing; - result_slashing - .epoch_map - .insert(start_epoch, amount_after_slashing); - } - Ok(result_slashing) -} - -/// Compute from a set of unbonds (both redelegated and not) how much is left -/// after applying all relevant slashes. -// `def computeAmountAfterSlashingWithdraw` -fn compute_amount_after_slashing_withdraw( - storage: &S, - params: &OwnedPosParams, - unbonds_and_redelegated_unbonds: &BTreeMap< - (Epoch, Epoch), - (token::Amount, EagerRedelegatedBondsMap), - >, - slashes: Vec, -) -> storage_api::Result -where - S: StorageRead, -{ - let mut result_slashing = ResultSlashing::default(); - - for ((start_epoch, withdraw_epoch), (amount, redelegated_unbonds)) in - unbonds_and_redelegated_unbonds.iter() - { - // TODO: check if slashes in the same epoch can be - // folded into one effective slash - let end_epoch = *withdraw_epoch - - params.unbonding_len - - params.cubic_slashing_window_length; - // Find slashes that apply to `start_epoch..end_epoch` - let list_slashes = slashes - .iter() - .filter(|slash| { - // Started before the slash occurred - start_epoch <= &slash.epoch - // Ends after the slash - && end_epoch > slash.epoch - }) - .cloned() - .collect::>(); - - // Find the sum and the sum after slashing of the redelegated unbonds - let result_fold = fold_and_slash_redelegated_bonds( - storage, - params, - redelegated_unbonds, - *start_epoch, - &list_slashes, - |_| true, - ); - - // Unbond amount that didn't come from a redelegation - let total_not_redelegated = *amount - result_fold.total_redelegated; - // Find how much remains after slashing non-redelegated amount - let after_not_redelegated = - apply_list_slashes(params, &list_slashes, total_not_redelegated); - - // Add back the unbond and redelegated unbond amount after slashing - let amount_after_slashing = - after_not_redelegated + result_fold.total_after_slashing; - - result_slashing.sum += amount_after_slashing; - result_slashing - .epoch_map - .insert(*start_epoch, amount_after_slashing); - } - - Ok(result_slashing) -} - -/// Arguments to [`become_validator`]. -pub struct BecomeValidator<'a> { - /// Proof-of-stake parameters. - pub params: &'a PosParams, - /// The validator's address. - pub address: &'a Address, - /// The validator's consensus key, used by Tendermint. - pub consensus_key: &'a common::PublicKey, - /// The validator's protocol key. - pub protocol_key: &'a common::PublicKey, - /// The validator's Ethereum bridge cold key. - pub eth_cold_key: &'a common::PublicKey, - /// The validator's Ethereum bridge hot key. - pub eth_hot_key: &'a common::PublicKey, - /// The numeric value of the current epoch. - pub current_epoch: Epoch, - /// Commission rate. - pub commission_rate: Dec, - /// Max commission rate change. - pub max_commission_rate_change: Dec, - /// Validator metadata - pub metadata: ValidatorMetaData, - /// Optional offset to use instead of pipeline offset - pub offset_opt: Option, -} - -/// Initialize data for a new validator. -pub fn become_validator( - storage: &mut S, - args: BecomeValidator<'_>, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - let BecomeValidator { - params, - address, - consensus_key, - protocol_key, - eth_cold_key, - eth_hot_key, - current_epoch, - commission_rate, - max_commission_rate_change, - metadata, - offset_opt, - } = args; - let offset = offset_opt.unwrap_or(params.pipeline_len); - - if !address.is_established() { - return Err(storage_api::Error::new_const( - "The given address {address} is not established. Only an \ - established address can become a validator.", - )); - } - - if is_validator(storage, address)? { - return Err(storage_api::Error::new_const( - "The given address is already a validator", - )); - } - - // If the address is not yet a validator, it cannot have self-bonds, but it - // may have delegations. - if has_bonds(storage, address)? { - return Err(storage_api::Error::new_const( - "The given address has delegations and therefore cannot become a \ - validator. Unbond first.", - )); - } - - // This will fail if the key is already being used - try_insert_consensus_key(storage, consensus_key)?; - - let pipeline_epoch = current_epoch + offset; - validator_addresses_handle() - .at(&pipeline_epoch) - .insert(storage, address.clone())?; - - // Non-epoched validator data - write_validator_address_raw_hash(storage, address, consensus_key)?; - write_validator_max_commission_rate_change( - storage, - address, - max_commission_rate_change, - )?; - write_validator_metadata(storage, address, &metadata)?; - - // Epoched validator data - validator_consensus_key_handle(address).set( - storage, - consensus_key.clone(), - current_epoch, - offset, - )?; - validator_protocol_key_handle(address).set( - storage, - protocol_key.clone(), - current_epoch, - offset, - )?; - validator_eth_hot_key_handle(address).set( - storage, - eth_hot_key.clone(), - current_epoch, - offset, - )?; - validator_eth_cold_key_handle(address).set( - storage, - eth_cold_key.clone(), - current_epoch, - offset, - )?; - validator_commission_rate_handle(address).set( - storage, - commission_rate, - current_epoch, - offset, - )?; - validator_deltas_handle(address).set( - storage, - token::Change::zero(), - current_epoch, - offset, - )?; - - // The validator's stake at initialization is 0, so its state is immediately - // below-threshold - validator_state_handle(address).set( - storage, - ValidatorState::BelowThreshold, - current_epoch, - offset, - )?; - - insert_validator_into_validator_set( - storage, - params, - address, - token::Amount::zero(), - current_epoch, - offset, - )?; - - Ok(()) -} - -/// Consensus key change for a validator -pub fn change_consensus_key( - storage: &mut S, - validator: &Address, - consensus_key: &common::PublicKey, - current_epoch: Epoch, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - tracing::debug!("Changing consensus key for validator {}", validator); - - // Require that the new consensus key is an Ed25519 key - match consensus_key { - common::PublicKey::Ed25519(_) => {} - common::PublicKey::Secp256k1(_) => { - return Err(ConsensusKeyChangeError::MustBeEd25519.into()); - } - } - - // Check for uniqueness of the consensus key - try_insert_consensus_key(storage, consensus_key)?; - - // Set the new consensus key at the pipeline epoch - let params = read_pos_params(storage)?; - validator_consensus_key_handle(validator).set( - storage, - consensus_key.clone(), - current_epoch, - params.pipeline_len, - )?; - - // Write validator's new raw hash - write_validator_address_raw_hash(storage, validator, consensus_key)?; - - Ok(()) -} - -/// Withdraw tokens from those that have been unbonded from proof-of-stake -pub fn withdraw_tokens( - storage: &mut S, - source: Option<&Address>, - validator: &Address, - current_epoch: Epoch, -) -> storage_api::Result -where - S: StorageRead + StorageWrite, -{ - let params = read_pos_params(storage)?; - let source = source.unwrap_or(validator); - - tracing::debug!("Withdrawing tokens in epoch {current_epoch}"); - tracing::debug!("Source {} --> Validator {}", source, validator); - - let unbond_handle: Unbonds = unbond_handle(source, validator); - let redelegated_unbonds = - delegator_redelegated_unbonds_handle(source).at(validator); - - // Check that there are unbonded tokens available for withdrawal - if unbond_handle.is_empty(storage)? { - return Err(WithdrawError::NoUnbondFound(BondId { - source: source.clone(), - validator: validator.clone(), - }) - .into()); - } - - let mut unbonds_and_redelegated_unbonds: BTreeMap< - (Epoch, Epoch), - (token::Amount, EagerRedelegatedBondsMap), - > = BTreeMap::new(); - - for unbond in unbond_handle.iter(storage)? { - let ( - NestedSubKey::Data { - key: start_epoch, - nested_sub_key: SubKey::Data(withdraw_epoch), - }, - amount, - ) = unbond?; - - // Logging - tracing::debug!( - "Unbond delta ({start_epoch}..{withdraw_epoch}), amount {}", - amount.to_string_native() - ); - // Consider only unbonds that are eligible to be withdrawn - if withdraw_epoch > current_epoch { - tracing::debug!( - "Not yet withdrawable until epoch {withdraw_epoch}" - ); - continue; - } - - let mut eager_redelegated_unbonds = EagerRedelegatedBondsMap::default(); - let matching_redelegated_unbonds = - redelegated_unbonds.at(&start_epoch).at(&withdraw_epoch); - for ub in matching_redelegated_unbonds.iter(storage)? { - let ( - NestedSubKey::Data { - key: address, - nested_sub_key: SubKey::Data(epoch), - }, - amount, - ) = ub?; - eager_redelegated_unbonds - .entry(address) - .or_default() - .entry(epoch) - .or_insert(amount); - } - - unbonds_and_redelegated_unbonds.insert( - (start_epoch, withdraw_epoch), - (amount, eager_redelegated_unbonds), - ); - } - - let slashes = find_validator_slashes(storage, validator)?; - - // `val resultSlashing` - let result_slashing = compute_amount_after_slashing_withdraw( - storage, - ¶ms, - &unbonds_and_redelegated_unbonds, - slashes, - )?; - - let withdrawable_amount = result_slashing.sum; - tracing::debug!( - "Withdrawing total {}", - withdrawable_amount.to_string_native() - ); - - // `updateDelegator` with `unbonded` and `redelegeatedUnbonded` - for ((start_epoch, withdraw_epoch), _unbond_and_redelegations) in - unbonds_and_redelegated_unbonds - { - tracing::debug!("Remove ({start_epoch}..{withdraw_epoch}) from unbond"); - unbond_handle - .at(&start_epoch) - .remove(storage, &withdraw_epoch)?; - redelegated_unbonds - .at(&start_epoch) - .remove_all(storage, &withdraw_epoch)?; - - if unbond_handle.at(&start_epoch).is_empty(storage)? { - unbond_handle.remove_all(storage, &start_epoch)?; - } - if redelegated_unbonds.at(&start_epoch).is_empty(storage)? { - redelegated_unbonds.remove_all(storage, &start_epoch)?; - } - } - - // Transfer the withdrawable tokens from the PoS address back to the source - let staking_token = staking_token_address(storage); - token::transfer( - storage, - &staking_token, - &ADDRESS, - source, - withdrawable_amount, - )?; - - // TODO: Transfer the slashed tokens from the PoS address to the Slash Pool - // address - // token::transfer( - // storage, - // &staking_token, - // &ADDRESS, - // &SLASH_POOL_ADDRESS, - // total_slashed, - // )?; - - Ok(withdrawable_amount) -} - -/// Change the commission rate of a validator -pub fn change_validator_commission_rate( - storage: &mut S, - validator: &Address, - new_rate: Dec, - current_epoch: Epoch, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - if new_rate.is_negative() { - return Err(CommissionRateChangeError::NegativeRate( - new_rate, - validator.clone(), - ) - .into()); - } - - if new_rate > Dec::one() { - return Err(CommissionRateChangeError::LargerThanOne( - new_rate, - validator.clone(), - ) - .into()); - } - - let max_change = - read_validator_max_commission_rate_change(storage, validator)?; - if max_change.is_none() { - return Err(CommissionRateChangeError::NoMaxSetInStorage( - validator.clone(), - ) - .into()); - } - - let params = read_pos_params(storage)?; - let commission_handle = validator_commission_rate_handle(validator); - let pipeline_epoch = current_epoch + params.pipeline_len; - - let rate_at_pipeline = commission_handle - .get(storage, pipeline_epoch, ¶ms)? - .expect("Could not find a rate in given epoch"); - if new_rate == rate_at_pipeline { - return Ok(()); - } - let rate_before_pipeline = commission_handle - .get(storage, pipeline_epoch.prev(), ¶ms)? - .expect("Could not find a rate in given epoch"); - - let change_from_prev = new_rate.abs_diff(&rate_before_pipeline); - if change_from_prev > max_change.unwrap() { - return Err(CommissionRateChangeError::RateChangeTooLarge( - change_from_prev, - validator.clone(), - ) - .into()); - } - - commission_handle.set(storage, new_rate, current_epoch, params.pipeline_len) -} - -/// Check if the given consensus key is already being used to ensure uniqueness. -/// -/// If it's not being used, it will be inserted into the set that's being used -/// for this. If it's already used, this will return an Error. -pub fn try_insert_consensus_key( - storage: &mut S, - consensus_key: &common::PublicKey, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - let key = consensus_keys_key(); - LazySet::open(key).try_insert(storage, consensus_key.clone()) -} - -/// Get the unique set of consensus keys in storage -pub fn get_consensus_key_set( - storage: &S, -) -> storage_api::Result> -where - S: StorageRead, -{ - let key = consensus_keys_key(); - let lazy_set = LazySet::::open(key); - Ok(lazy_set.iter(storage)?.map(Result::unwrap).collect()) -} - -/// Check if the given consensus key is already being used to ensure uniqueness. -pub fn is_consensus_key_used( - storage: &S, - consensus_key: &common::PublicKey, -) -> storage_api::Result -where - S: StorageRead, -{ - let key = consensus_keys_key(); - let handle = LazySet::open(key); - handle.contains(storage, consensus_key) -} - -/// Get the total bond amount, including slashes, for a given bond ID and epoch. -/// Returns the bond amount after slashing. For future epochs the value is -/// subject to change. -pub fn bond_amount( - storage: &S, - bond_id: &BondId, - epoch: Epoch, -) -> storage_api::Result -where - S: StorageRead, -{ - let params = read_pos_params(storage)?; - // Outer key is the start epoch used to calculate slashes. The inner - // keys are discarded after applying slashes. - let mut amounts: BTreeMap = BTreeMap::default(); - - // Bonds - let bonds = - bond_handle(&bond_id.source, &bond_id.validator).get_data_handler(); - for next in bonds.iter(storage)? { - let (start, delta) = next?; - if start <= epoch { - let amount = amounts.entry(start).or_default(); - *amount += delta; - } - } - - // Add unbonds that are still contributing to stake - let unbonds = unbond_handle(&bond_id.source, &bond_id.validator); - for next in unbonds.iter(storage)? { - let ( - NestedSubKey::Data { - key: start, - nested_sub_key: SubKey::Data(withdrawable_epoch), - }, - delta, - ) = next?; - // This is the first epoch in which the unbond stops contributing to - // voting power - let end = withdrawable_epoch - params.withdrawable_epoch_offset() - + params.pipeline_len; - - if start <= epoch && end > epoch { - let amount = amounts.entry(start).or_default(); - *amount += delta; - } - } - - if bond_id.validator != bond_id.source { - // Add outgoing redelegations that are still contributing to the source - // validator's stake - let redelegated_bonds = - delegator_redelegated_bonds_handle(&bond_id.source); - for res in redelegated_bonds.iter(storage)? { - let ( - NestedSubKey::Data { - key: _dest_validator, - nested_sub_key: - NestedSubKey::Data { - key: end, - nested_sub_key: - NestedSubKey::Data { - key: src_validator, - nested_sub_key: SubKey::Data(start), - }, - }, - }, - delta, - ) = res?; - if src_validator == bond_id.validator - && start <= epoch - && end > epoch - { - let amount = amounts.entry(start).or_default(); - *amount += delta; - } - } - - // Add outgoing redelegation unbonds that are still contributing to - // the source validator's stake - let redelegated_unbonds = - delegator_redelegated_unbonds_handle(&bond_id.source); - for res in redelegated_unbonds.iter(storage)? { - let ( - NestedSubKey::Data { - key: _dest_validator, - nested_sub_key: - NestedSubKey::Data { - key: redelegation_epoch, - nested_sub_key: - NestedSubKey::Data { - key: _withdraw_epoch, - nested_sub_key: - NestedSubKey::Data { - key: src_validator, - nested_sub_key: SubKey::Data(start), - }, - }, - }, - }, - delta, - ) = res?; - if src_validator == bond_id.validator - // If the unbonded bond was redelegated after this epoch ... - && redelegation_epoch > epoch - // ... the start was before or at this epoch - && start <= epoch - { - let amount = amounts.entry(start).or_default(); - *amount += delta; - } - } - } - - if !amounts.is_empty() { - let slashes = find_validator_slashes(storage, &bond_id.validator)?; - - // Apply slashes - for (&start, amount) in amounts.iter_mut() { - let list_slashes = slashes - .iter() - .filter(|slash| { - let processing_epoch = - slash.epoch + params.slash_processing_epoch_offset(); - // Only use slashes that were processed before or at the - // epoch associated with the bond amount. This assumes - // that slashes are applied before inflation. - processing_epoch <= epoch && start <= slash.epoch - }) - .cloned() - .collect::>(); - - *amount = apply_list_slashes(¶ms, &list_slashes, *amount); - } - } - - Ok(amounts.values().cloned().sum()) -} - -/// Get bond amounts within the `claim_start..=claim_end` epoch range for -/// claiming rewards for a given bond ID. Returns a map of bond amounts -/// associated with every epoch within the given epoch range (accumulative) in -/// which an amount contributed to the validator's stake. -/// This function will only consider slashes that were processed before or at -/// the epoch in which we're calculating the bond amount to correspond to the -/// validator stake that was used to calculate reward products (slashes do *not* -/// retrospectively affect the rewards calculated before slash processing). -pub fn bond_amounts_for_rewards( - storage: &S, - bond_id: &BondId, - claim_start: Epoch, - claim_end: Epoch, -) -> storage_api::Result> -where - S: StorageRead, -{ - let params = read_pos_params(storage)?; - // Outer key is every epoch in which the a bond amount contributed to stake - // and the inner key is the start epoch used to calculate slashes. The inner - // keys are discarded after applying slashes. - let mut amounts: BTreeMap> = - BTreeMap::default(); - - // Only need to do bonds since rewwards are accumulated during - // `unbond_tokens` - let bonds = - bond_handle(&bond_id.source, &bond_id.validator).get_data_handler(); - for next in bonds.iter(storage)? { - let (start, delta) = next?; - - for ep in Epoch::iter_bounds_inclusive(claim_start, claim_end) { - // A bond that wasn't unbonded is added to all epochs up to - // `claim_end` - if start <= ep { - let amount = - amounts.entry(ep).or_default().entry(start).or_default(); - *amount += delta; - } - } - } - - if !amounts.is_empty() { - let slashes = find_validator_slashes(storage, &bond_id.validator)?; - let redelegated_bonded = - delegator_redelegated_bonds_handle(&bond_id.source) - .at(&bond_id.validator); - - // Apply slashes - for (&ep, amounts) in amounts.iter_mut() { - for (&start, amount) in amounts.iter_mut() { - let list_slashes = slashes - .iter() - .filter(|slash| { - let processing_epoch = slash.epoch - + params.slash_processing_epoch_offset(); - // Only use slashes that were processed before or at the - // epoch associated with the bond amount. This assumes - // that slashes are applied before inflation. - processing_epoch <= ep && start <= slash.epoch - }) - .cloned() - .collect::>(); - - let slash_epoch_filter = - |e: Epoch| e + params.slash_processing_epoch_offset() <= ep; - - let redelegated_bonds = - redelegated_bonded.at(&start).collect_map(storage)?; - - let result_fold = fold_and_slash_redelegated_bonds( - storage, - ¶ms, - &redelegated_bonds, - start, - &list_slashes, - slash_epoch_filter, - ); - - let total_not_redelegated = - *amount - result_fold.total_redelegated; - - let after_not_redelegated = apply_list_slashes( - ¶ms, - &list_slashes, - total_not_redelegated, - ); - - *amount = - after_not_redelegated + result_fold.total_after_slashing; - } + let mut epochs = bonds_to_unbond.epochs.clone(); + if let Some((epoch, _)) = bonds_to_unbond.new_entry { + epochs.insert(epoch); } - } - - Ok(amounts - .into_iter() - // Flatten the inner maps to discard bond start epochs - .map(|(ep, amounts)| (ep, amounts.values().cloned().sum())) - .collect()) -} - -/// Get the genesis consensus validators stake and consensus key for Tendermint, -/// converted from [`ValidatorSetUpdate`]s using the given function. -pub fn genesis_validator_set_tendermint( - storage: &S, - params: &PosParams, - current_epoch: Epoch, - mut f: impl FnMut(ValidatorSetUpdate) -> T, -) -> storage_api::Result> -where - S: StorageRead, -{ - let consensus_validator_handle = - consensus_validator_set_handle().at(¤t_epoch); - let iter = consensus_validator_handle.iter(storage)?; - - iter.map(|validator| { - let ( - NestedSubKey::Data { - key: new_stake, - nested_sub_key: _, - }, - address, - ) = validator?; - let consensus_key = validator_consensus_key_handle(&address) - .get(storage, current_epoch, params)? - .unwrap(); - let converted = f(ValidatorSetUpdate::Consensus(ConsensusValidator { - consensus_key, - bonded_stake: new_stake, - })); - Ok(converted) - }) - .collect() -} - -/// Communicate imminent validator set updates to Tendermint. This function is -/// called two blocks before the start of a new epoch because Tendermint -/// validator updates become active two blocks after the updates are submitted. -pub fn validator_set_update_tendermint( - storage: &S, - params: &PosParams, - current_epoch: Epoch, - f: impl FnMut(ValidatorSetUpdate) -> T, -) -> storage_api::Result> -where - S: StorageRead, -{ - tracing::debug!("Communicating validator set updates to Tendermint."); - // Because this is called 2 blocks before a start on an epoch, we're gonna - // give Tendermint updates for the next epoch - let next_epoch = current_epoch.next(); - - let new_consensus_validator_handle = - consensus_validator_set_handle().at(&next_epoch); - let prev_consensus_validator_handle = - consensus_validator_set_handle().at(¤t_epoch); - - let new_consensus_validators = new_consensus_validator_handle - .iter(storage)? - .map(|validator| { - let ( - NestedSubKey::Data { - key: new_stake, - nested_sub_key: _, - }, - address, - ) = validator.unwrap(); - - tracing::debug!( - "Consensus validator address {address}, stake {}", - new_stake.to_string_native() - ); - - let new_consensus_key = validator_consensus_key_handle(&address) - .get(storage, next_epoch, params) - .unwrap() - .unwrap(); - - let old_consensus_key = validator_consensus_key_handle(&address) - .get(storage, current_epoch, params) - .unwrap(); - - // Check if the validator was consensus in the previous epoch with - // the same stake. If so, no updated is needed. - // Look up previous state and prev and current voting powers - if !prev_consensus_validator_handle.is_empty(storage).unwrap() { - let prev_state = validator_state_handle(&address) - .get(storage, current_epoch, params) - .unwrap(); - let prev_tm_voting_power = Lazy::new(|| { - let prev_validator_stake = read_validator_stake( - storage, - params, - &address, - current_epoch, - ) - .unwrap(); - into_tm_voting_power( - params.tm_votes_per_token, - prev_validator_stake, - ) - }); - let new_tm_voting_power = Lazy::new(|| { - into_tm_voting_power(params.tm_votes_per_token, new_stake) - }); - - // If it was in `Consensus` before and voting power has not - // changed, skip the update - if matches!(prev_state, Some(ValidatorState::Consensus)) - && *prev_tm_voting_power == *new_tm_voting_power - { - if old_consensus_key.as_ref().unwrap() == &new_consensus_key - { - tracing::debug!( - "skipping validator update, {address} is in \ - consensus set but voting power hasn't changed" - ); - return vec![]; - } else { - return vec![ - ValidatorSetUpdate::Consensus(ConsensusValidator { - consensus_key: new_consensus_key, - bonded_stake: new_stake, - }), - ValidatorSetUpdate::Deactivated( - old_consensus_key.unwrap(), - ), - ]; - } - } - // If both previous and current voting powers are 0, and the - // validator_stake_threshold is 0, skip update - if params.validator_stake_threshold.is_zero() - && *prev_tm_voting_power == 0 - && *new_tm_voting_power == 0 - { - tracing::info!( - "skipping validator update, {address} is in consensus \ - set but without voting power" - ); - return vec![]; - } - } - - tracing::debug!( - "{address} consensus key {}", - new_consensus_key.tm_raw_hash() + for epoch in epochs { + let cur_bond = bonds_handle + .get_delta_val(storage, epoch)? + .unwrap_or_default(); + let redelegated_deltas = redelegated_bonds + .at(&epoch) + // Sum of redelegations from any src validator + .collect_map(storage)? + .into_values() + .map(|redeleg| redeleg.into_values().sum()) + .sum(); + debug_assert!( + cur_bond >= redelegated_deltas, + "After unbonding, in epoch {epoch} the bond amount {} must be \ + >= redelegated deltas at pipeline {}.\n\nredelegated_bonds \ + pre: {redel_bonds_pre:#?}\nredelegated_bonds post: \ + {redel_bonds_post:#?},\nmodified_redelegation: \ + {modified_redelegation:#?},\nbonds_to_unbond: \ + {bonds_to_unbond:#?}", + cur_bond.to_string_native(), + redelegated_deltas.to_string_native() ); + } + } - if old_consensus_key.as_ref() == Some(&new_consensus_key) - || old_consensus_key.is_none() - { - vec![ValidatorSetUpdate::Consensus(ConsensusValidator { - consensus_key: new_consensus_key, - bonded_stake: new_stake, - })] - } else { - vec![ - ValidatorSetUpdate::Consensus(ConsensusValidator { - consensus_key: new_consensus_key, - bonded_stake: new_stake, - }), - ValidatorSetUpdate::Deactivated(old_consensus_key.unwrap()), - ] - } - }); - - let prev_consensus_validators = prev_consensus_validator_handle - .iter(storage)? - .map(|validator| { - let ( - NestedSubKey::Data { - key: _prev_stake, - nested_sub_key: _, - }, - address, - ) = validator.unwrap(); - - let new_state = validator_state_handle(&address) - .get(storage, next_epoch, params) - .unwrap(); + // Tally rewards (only call if this is not the first epoch) + if current_epoch > Epoch::default() { + let mut rewards = token::Amount::zero(); - let prev_tm_voting_power = Lazy::new(|| { - let prev_validator_stake = read_validator_stake( - storage, - params, - &address, - current_epoch, - ) - .unwrap(); - into_tm_voting_power( - params.tm_votes_per_token, - prev_validator_stake, - ) - }); + let last_claim_epoch = + get_last_reward_claim_epoch(storage, source, validator)? + .unwrap_or_default(); + let rewards_products = validator_rewards_products_handle(validator); - let old_consensus_key = validator_consensus_key_handle(&address) - .get(storage, current_epoch, params) - .unwrap() - .unwrap(); - - // If the validator is still in the Consensus set, we accounted for - // it in the `new_consensus_validators` iterator above - if matches!(new_state, Some(ValidatorState::Consensus)) { - return vec![]; - } else if params.validator_stake_threshold.is_zero() - && *prev_tm_voting_power == 0 + for (start_epoch, slashed_amount) in &result_slashing.epoch_map { + // Stop collecting rewards at the moment the unbond is initiated + // (right now) + for ep in + Epoch::iter_bounds_inclusive(*start_epoch, current_epoch.prev()) { - // If the new state is not Consensus but its prev voting power - // was 0 and the stake threshold is 0, we can also skip the - // update - tracing::info!( - "skipping validator update, {address} is in consensus set \ - but without voting power" - ); - return vec![]; + // Consider the last epoch when rewards were claimed + if ep < last_claim_epoch { + continue; + } + let rp = + rewards_products.get(storage, &ep)?.unwrap_or_default(); + rewards += rp * (*slashed_amount); } + } - // The remaining validators were previously Consensus but no longer - // are, so they must be deactivated - let consensus_key = validator_consensus_key_handle(&address) - .get(storage, next_epoch, params) - .unwrap() - .unwrap(); - tracing::debug!( - "{address} consensus key {}", - consensus_key.tm_raw_hash() - ); - vec![ValidatorSetUpdate::Deactivated(old_consensus_key)] - }); + // Update the rewards from the current unbonds first + add_rewards_to_counter(storage, source, validator, rewards)?; + } - Ok(new_consensus_validators - .chain(prev_consensus_validators) - .flatten() - .map(f) - .collect()) + Ok(result_slashing) } -/// Find all validators to which a given bond `owner` (or source) has a -/// delegation -pub fn find_delegation_validators( - storage: &S, - owner: &Address, -) -> storage_api::Result> -where - S: StorageRead, -{ - let bonds_prefix = bonds_for_source_prefix(owner); - let mut delegations: HashSet
= HashSet::new(); - - for iter_result in storage_api::iter_prefix_bytes(storage, &bonds_prefix)? { - let (key, _bond_bytes) = iter_result?; - let validator_address = get_validator_address_from_bond(&key) - .ok_or_else(|| { - storage_api::Error::new_const( - "Delegation key should contain validator address.", - ) - })?; - delegations.insert(validator_address); - } - Ok(delegations) +#[derive(Debug, Default, Eq, PartialEq)] +struct FoldRedelegatedBondsResult { + total_redelegated: token::Amount, + total_after_slashing: token::Amount, } -/// Find all validators to which a given bond `owner` (or source) has a -/// delegation with the amount -pub fn find_delegations( +/// Iterates over a `redelegated_unbonds` and computes the both the sum of all +/// redelegated tokens and how much is left after applying all relevant slashes. +// `def foldAndSlashRedelegatedBondsMap` +fn fold_and_slash_redelegated_bonds( storage: &S, - owner: &Address, - epoch: &Epoch, -) -> storage_api::Result> + params: &OwnedPosParams, + redelegated_unbonds: &EagerRedelegatedBondsMap, + start_epoch: Epoch, + list_slashes: &[Slash], + slash_epoch_filter: impl Fn(Epoch) -> bool, +) -> FoldRedelegatedBondsResult where S: StorageRead, { - let bonds_prefix = bonds_for_source_prefix(owner); - let params = read_pos_params(storage)?; - let mut delegations: HashMap = HashMap::new(); - - for iter_result in storage_api::iter_prefix_bytes(storage, &bonds_prefix)? { - let (key, _bond_bytes) = iter_result?; - let validator_address = get_validator_address_from_bond(&key) - .ok_or_else(|| { - storage_api::Error::new_const( - "Delegation key should contain validator address.", - ) - })?; - let deltas_sum = bond_handle(owner, &validator_address) - .get_sum(storage, *epoch, ¶ms)? - .unwrap_or_default(); - delegations.insert(validator_address, deltas_sum); + let mut result = FoldRedelegatedBondsResult::default(); + for (src_validator, bonds_map) in redelegated_unbonds { + for (bond_start, &change) in bonds_map { + // Merge the two lists of slashes + let mut merged: Vec = + // Look-up slashes for this validator ... + validator_slashes_handle(src_validator) + .iter(storage) + .unwrap() + .map(Result::unwrap) + .filter(|slash| { + params.in_redelegation_slashing_window( + slash.epoch, + params.redelegation_start_epoch_from_end( + start_epoch, + ), + start_epoch, + ) && *bond_start <= slash.epoch + && slash_epoch_filter(slash.epoch) + }) + // ... and add `list_slashes` + .chain(list_slashes.iter().cloned()) + .collect(); + + // Sort slashes by epoch + merged.sort_by(|s1, s2| s1.epoch.partial_cmp(&s2.epoch).unwrap()); + + result.total_redelegated += change; + result.total_after_slashing += + apply_list_slashes(params, &merged, change); + } } - Ok(delegations) + result } -/// Find if the given source address has any bonds. -pub fn has_bonds(storage: &S, source: &Address) -> storage_api::Result -where - S: StorageRead, -{ - let max_epoch = Epoch(u64::MAX); - let delegations = find_delegations(storage, source, &max_epoch)?; - Ok(!delegations - .values() - .cloned() - .sum::() - .is_zero()) +/// Epochs for full and partial unbonds. +#[derive(Debug, Default)] +struct BondsForRemovalRes { + /// Full unbond epochs + pub epochs: BTreeSet, + /// Partial unbond epoch associated with the new bond amount + pub new_entry: Option<(Epoch, token::Amount)>, } -/// Find PoS slashes applied to a validator, if any -pub fn find_validator_slashes( +/// In decreasing epoch order, decrement the non-zero bond amount entries until +/// the full `amount` has been removed. Returns a `BondsForRemovalRes` object +/// that contains the epochs for which the full bond amount is removed and +/// additionally information for the one epoch whose bond amount is partially +/// removed, if any. +fn find_bonds_to_remove( storage: &S, - validator: &Address, -) -> storage_api::Result> + bonds_handle: &LazyMap, + amount: token::Amount, +) -> storage_api::Result where S: StorageRead, { - validator_slashes_handle(validator).iter(storage)?.collect() -} + #[allow(clippy::needless_collect)] + let bonds: Vec> = bonds_handle.iter(storage)?.collect(); -/// Find raw bond deltas for the given source and validator address. -pub fn find_bonds( - storage: &S, - source: &Address, - validator: &Address, -) -> storage_api::Result> -where - S: StorageRead, -{ - bond_handle(source, validator) - .get_data_handler() - .iter(storage)? - .collect() + let mut bonds_for_removal = BondsForRemovalRes::default(); + let mut remaining = amount; + + for bond in bonds.into_iter().rev() { + let (bond_epoch, bond_amount) = bond?; + let to_unbond = cmp::min(bond_amount, remaining); + if to_unbond == bond_amount { + bonds_for_removal.epochs.insert(bond_epoch); + } else { + bonds_for_removal.new_entry = + Some((bond_epoch, bond_amount - to_unbond)); + } + remaining -= to_unbond; + if remaining.is_zero() { + break; + } + } + Ok(bonds_for_removal) } -/// Find raw unbond deltas for the given source and validator address. -pub fn find_unbonds( - storage: &S, - source: &Address, - validator: &Address, -) -> storage_api::Result> -where - S: StorageRead, -{ - unbond_handle(source, validator) - .iter(storage)? - .map(|next_result| { - let ( - NestedSubKey::Data { - key: start_epoch, - nested_sub_key: SubKey::Data(withdraw_epoch), - }, - amount, - ) = next_result?; - Ok(((start_epoch, withdraw_epoch), amount)) - }) - .collect() +#[derive(Debug, Default, PartialEq, Eq)] +struct ModifiedRedelegation { + epoch: Option, + validators_to_remove: BTreeSet
, + validator_to_modify: Option
, + epochs_to_remove: BTreeSet, + epoch_to_modify: Option, + new_amount: Option, } -/// Collect the details of all bonds and unbonds that match the source and -/// validator arguments. If either source or validator is `None`, then grab the -/// information for all sources or validators, respectively. -pub fn bonds_and_unbonds( +/// Used in `fn unbond_tokens` to compute the modified state of a redelegation +/// if redelegated tokens are being unbonded. +fn compute_modified_redelegation( storage: &S, - source: Option
, - validator: Option
, -) -> storage_api::Result + redelegated_bonds: &RedelegatedTokens, + start_epoch: Epoch, + amount_to_unbond: token::Amount, +) -> storage_api::Result where S: StorageRead, { - let params = read_pos_params(storage)?; + let mut modified_redelegation = ModifiedRedelegation::default(); + + let mut src_validators = BTreeSet::
::new(); + let mut total_redelegated = token::Amount::zero(); + for rb in redelegated_bonds.iter(storage)? { + let ( + NestedSubKey::Data { + key: src_validator, + nested_sub_key: _, + }, + amount, + ) = rb?; + total_redelegated += amount; + src_validators.insert(src_validator); + } + + modified_redelegation.epoch = Some(start_epoch); + + // If the total amount of redelegated bonds is less than the target amount, + // then all redelegated bonds must be unbonded. + if total_redelegated <= amount_to_unbond { + return Ok(modified_redelegation); + } - match (source.clone(), validator.clone()) { - (Some(source), Some(validator)) => { - find_bonds_and_unbonds_details(storage, ¶ms, source, validator) + let mut remaining = amount_to_unbond; + for src_validator in src_validators.into_iter() { + if remaining.is_zero() { + break; } - _ => { - get_multiple_bonds_and_unbonds(storage, ¶ms, source, validator) + let rbonds = redelegated_bonds.at(&src_validator); + let total_src_val_amount = rbonds + .iter(storage)? + .map(|res| { + let (_, amount) = res?; + Ok(amount) + }) + .sum::>()?; + + // TODO: move this into the `if total_redelegated <= remaining` branch + // below, then we don't have to remove it in `fn + // update_redelegated_bonds` when `validator_to_modify` is Some (and + // avoid `modified_redelegation.validators_to_remove.clone()`). + // It affects assumption 2. in `fn compute_new_redelegated_unbonds`, but + // that looks trivial to change. + // NOTE: not sure if this TODO is still relevant... + modified_redelegation + .validators_to_remove + .insert(src_validator.clone()); + if total_src_val_amount <= remaining { + remaining -= total_src_val_amount; + } else { + let bonds_to_remove = + find_bonds_to_remove(storage, &rbonds, remaining)?; + + remaining = token::Amount::zero(); + + // NOTE: When there are multiple `src_validators` from which we're + // unbonding, `validator_to_modify` cannot get overriden, because + // only one of them can be a partial unbond (`new_entry` + // is partial unbond) + if let Some((bond_epoch, new_bond_amount)) = + bonds_to_remove.new_entry + { + modified_redelegation.validator_to_modify = Some(src_validator); + modified_redelegation.epochs_to_remove = { + let mut epochs = bonds_to_remove.epochs; + // TODO: remove this insertion then we don't have to remove + // it again in `fn update_redelegated_bonds` + // when `epoch_to_modify` is Some (and avoid + // `modified_redelegation.epochs_to_remove.clone`) + // It affects assumption 3. in `fn + // compute_new_redelegated_unbonds`, but that also looks + // trivial to change. + epochs.insert(bond_epoch); + epochs + }; + modified_redelegation.epoch_to_modify = Some(bond_epoch); + modified_redelegation.new_amount = Some(new_bond_amount); + } else { + modified_redelegation.validator_to_modify = Some(src_validator); + modified_redelegation.epochs_to_remove = bonds_to_remove.epochs; + } } } + Ok(modified_redelegation) } -/// Collect the details of all of the enqueued slashes to be processed in future -/// epochs into a nested map -pub fn find_all_enqueued_slashes( - storage: &S, - epoch: Epoch, -) -> storage_api::Result>>> +fn update_redelegated_bonds( + storage: &mut S, + redelegated_bonds: &RedelegatedTokens, + modified_redelegation: &ModifiedRedelegation, +) -> storage_api::Result<()> where - S: StorageRead, + S: StorageRead + StorageWrite, { - let mut enqueued = HashMap::>>::new(); - for res in enqueued_slashes_handle().get_data_handler().iter(storage)? { - let ( - NestedSubKey::Data { - key: processing_epoch, - nested_sub_key: - NestedSubKey::Data { - key: address, - nested_sub_key: _, - }, - }, - slash, - ) = res?; - if processing_epoch <= epoch { - continue; - } + if let Some(val_to_modify) = &modified_redelegation.validator_to_modify { + let mut updated_vals_to_remove = + modified_redelegation.validators_to_remove.clone(); + updated_vals_to_remove.remove(val_to_modify); - let slashes = enqueued - .entry(address) - .or_default() - .entry(processing_epoch) - .or_default(); - slashes.push(slash); - } - Ok(enqueued) -} + // Remove the updated_vals_to_remove keys from the + // redelegated_bonds map + for val in &updated_vals_to_remove { + redelegated_bonds.remove_all(storage, val)?; + } -/// Find all slashes and the associated validators in the PoS system -pub fn find_all_slashes( - storage: &S, -) -> storage_api::Result>> -where - S: StorageRead, -{ - let mut slashes: HashMap> = HashMap::new(); - let slashes_iter = storage_api::iter_prefix_bytes( - storage, - &slashes_prefix(), - )? - .filter_map(|result| { - if let Ok((key, val_bytes)) = result { - if let Some(validator) = is_validator_slashes_key(&key) { - let slash: Slash = - BorshDeserialize::try_from_slice(&val_bytes).ok()?; - return Some((validator, slash)); + if let Some(epoch_to_modify) = modified_redelegation.epoch_to_modify { + let mut updated_epochs_to_remove = + modified_redelegation.epochs_to_remove.clone(); + updated_epochs_to_remove.remove(&epoch_to_modify); + let val_bonds_to_modify = redelegated_bonds.at(val_to_modify); + for epoch in updated_epochs_to_remove { + val_bonds_to_modify.remove(storage, &epoch)?; + } + val_bonds_to_modify.insert( + storage, + epoch_to_modify, + modified_redelegation.new_amount.unwrap(), + )?; + } else { + // Then remove to epochs_to_remove from the redelegated bonds of the + // val_to_modify + let val_bonds_to_modify = redelegated_bonds.at(val_to_modify); + for epoch in &modified_redelegation.epochs_to_remove { + val_bonds_to_modify.remove(storage, epoch)?; } } - None - }); - - slashes_iter.for_each(|(address, slash)| match slashes.get(&address) { - Some(vec) => { - let mut vec = vec.clone(); - vec.push(slash); - slashes.insert(address, vec); - } - None => { - slashes.insert(address, vec![slash]); + } else { + // Remove all validators in modified_redelegation.validators_to_remove + // from redelegated_bonds + for val in &modified_redelegation.validators_to_remove { + redelegated_bonds.remove_all(storage, val)?; } - }); - Ok(slashes) + } + Ok(()) } -fn get_multiple_bonds_and_unbonds( +/// Temp helper type to match quint model. +/// Result of `compute_new_redelegated_unbonds` that contains a map of +/// redelegated unbonds. +/// The map keys from outside in are: +/// +/// - redelegation end epoch where redeleg stops contributing to src validator +/// - src validator address +/// - src bond start epoch where it started contributing to src validator +type EagerRedelegatedUnbonds = BTreeMap; + +/// Computes a map of redelegated unbonds from a set of redelegated bonds. +/// +/// - `redelegated_bonds` - a map of redelegated bonds from epoch to +/// `RedelegatedTokens`. +/// - `epochs_to_remove` - a set of epochs that indicate the set of epochs +/// unbonded. +/// - `modified` record that represents a redelegated bond that it is only +/// partially unbonded. +/// +/// The function assumes that: +/// +/// 1. `modified.epoch` is not in the `epochs_to_remove` set. +/// 2. `modified.validator_to_modify` is in `modified.vals_to_remove`. +/// 3. `modified.epoch_to_modify` is in in `modified.epochs_to_remove`. +// `def computeNewRedelegatedUnbonds` from Quint +fn compute_new_redelegated_unbonds( storage: &S, - params: &PosParams, - source: Option
, - validator: Option
, -) -> storage_api::Result + redelegated_bonds: &RedelegatedBondsOrUnbonds, + epochs_to_remove: &BTreeSet, + modified: &ModifiedRedelegation, +) -> storage_api::Result where - S: StorageRead, + S: StorageRead + StorageWrite, { + let unbonded_epochs = if let Some(epoch) = modified.epoch { + debug_assert!( + !epochs_to_remove.contains(&epoch), + "1. assumption in `fn compute_new_redelegated_unbonds` doesn't \ + hold" + ); + let mut epochs = epochs_to_remove.clone(); + epochs.insert(epoch); + epochs + .iter() + .cloned() + .filter(|e| redelegated_bonds.contains(storage, e).unwrap()) + .collect::>() + } else { + epochs_to_remove + .iter() + .cloned() + .filter(|e| redelegated_bonds.contains(storage, e).unwrap()) + .collect::>() + }; + debug_assert!( + modified + .validator_to_modify + .as_ref() + .map(|validator| modified.validators_to_remove.contains(validator)) + .unwrap_or(true), + "2. assumption in `fn compute_new_redelegated_unbonds` doesn't hold" + ); debug_assert!( - source.is_none() || validator.is_none(), - "Use `find_bonds_and_unbonds_details` when full bond ID is known" + modified + .epoch_to_modify + .as_ref() + .map(|epoch| modified.epochs_to_remove.contains(epoch)) + .unwrap_or(true), + "3. assumption in `fn compute_new_redelegated_unbonds` doesn't hold" ); - let mut slashes_cache = HashMap::>::new(); - // Applied slashes grouped by validator address - let mut applied_slashes = HashMap::>::new(); - - // TODO: if validator is `Some`, look-up all its bond owners (including - // self-bond, if any) first - let prefix = match source.as_ref() { - Some(source) => bonds_for_source_prefix(source), - None => bonds_prefix(), - }; - // We have to iterate raw bytes, cause the epoched data `last_update` field - // gets matched here too - let mut raw_bonds = storage_api::iter_prefix_bytes(storage, &prefix)? - .filter_map(|result| { - if let Ok((key, val_bytes)) = result { - if let Some((bond_id, start)) = is_bond_key(&key) { - if source.is_some() - && source.as_ref().unwrap() != &bond_id.source - { - return None; - } - if validator.is_some() - && validator.as_ref().unwrap() != &bond_id.validator - { - return None; - } - let change: token::Amount = - BorshDeserialize::try_from_slice(&val_bytes).ok()?; - if change.is_zero() { - return None; - } - return Some((bond_id, start, change)); + // quint `newRedelegatedUnbonds` returned from + // `computeNewRedelegatedUnbonds` + let new_redelegated_unbonds: EagerRedelegatedUnbonds = unbonded_epochs + .into_iter() + .map(|start| { + let mut rbonds = EagerRedelegatedBondsMap::default(); + if modified + .epoch + .map(|redelegation_epoch| start != redelegation_epoch) + .unwrap_or(true) + || modified.validators_to_remove.is_empty() + { + for res in redelegated_bonds.at(&start).iter(storage).unwrap() { + let ( + NestedSubKey::Data { + key: validator, + nested_sub_key: SubKey::Data(epoch), + }, + amount, + ) = res.unwrap(); + rbonds + .entry(validator.clone()) + .or_default() + .insert(epoch, amount); } - } - None - }); - - let prefix = match source.as_ref() { - Some(source) => unbonds_for_source_prefix(source), - None => unbonds_prefix(), - }; - let mut raw_unbonds = storage_api::iter_prefix_bytes(storage, &prefix)? - .filter_map(|result| { - if let Ok((key, val_bytes)) = result { - if let Some((bond_id, start, withdraw)) = is_unbond_key(&key) { - if source.is_some() - && source.as_ref().unwrap() != &bond_id.source - { - return None; - } - if validator.is_some() - && validator.as_ref().unwrap() != &bond_id.validator + (start, rbonds) + } else { + for src_validator in &modified.validators_to_remove { + if modified + .validator_to_modify + .as_ref() + .map(|validator| src_validator != validator) + .unwrap_or(true) { - return None; - } - match (source.clone(), validator.clone()) { - (None, Some(validator)) => { - if bond_id.validator != validator { - return None; - } - } - (Some(owner), None) => { - if owner != bond_id.source { - return None; - } + let raw_bonds = + redelegated_bonds.at(&start).at(src_validator); + for res in raw_bonds.iter(storage).unwrap() { + let (bond_epoch, bond_amount) = res.unwrap(); + rbonds + .entry(src_validator.clone()) + .or_default() + .insert(bond_epoch, bond_amount); } - _ => {} - } - let amount: token::Amount = - BorshDeserialize::try_from_slice(&val_bytes).ok()?; - return Some((bond_id, start, withdraw, amount)); - } - } - None - }); - - let mut bonds_and_unbonds = - HashMap::, Vec)>::new(); - - raw_bonds.try_for_each(|(bond_id, start, change)| { - if !slashes_cache.contains_key(&bond_id.validator) { - let slashes = find_validator_slashes(storage, &bond_id.validator)?; - slashes_cache.insert(bond_id.validator.clone(), slashes); - } - let slashes = slashes_cache - .get(&bond_id.validator) - .expect("We must have inserted it if it's not cached already"); - let validator = bond_id.validator.clone(); - let (bonds, _unbonds) = bonds_and_unbonds.entry(bond_id).or_default(); - bonds.push(make_bond_details( - params, - &validator, - change, - start, - slashes, - &mut applied_slashes, - )); - Ok::<_, storage_api::Error>(()) - })?; - - raw_unbonds.try_for_each(|(bond_id, start, withdraw, amount)| { - if !slashes_cache.contains_key(&bond_id.validator) { - let slashes = find_validator_slashes(storage, &bond_id.validator)?; - slashes_cache.insert(bond_id.validator.clone(), slashes); - } - let slashes = slashes_cache - .get(&bond_id.validator) - .expect("We must have inserted it if it's not cached already"); - let validator = bond_id.validator.clone(); - let (_bonds, unbonds) = bonds_and_unbonds.entry(bond_id).or_default(); - unbonds.push(make_unbond_details( - params, - &validator, - amount, - (start, withdraw), - slashes, - &mut applied_slashes, - )); - Ok::<_, storage_api::Error>(()) - })?; - - Ok(bonds_and_unbonds - .into_iter() - .map(|(bond_id, (bonds, unbonds))| { - let details = BondsAndUnbondsDetail { - bonds, - unbonds, - slashes: applied_slashes - .get(&bond_id.validator) - .cloned() - .unwrap_or_default(), - }; - (bond_id, details) - }) - .collect()) -} - -fn find_bonds_and_unbonds_details( - storage: &S, - params: &PosParams, - source: Address, - validator: Address, -) -> storage_api::Result -where - S: StorageRead, -{ - let slashes = find_validator_slashes(storage, &validator)?; - let mut applied_slashes = HashMap::>::new(); - - let bonds = find_bonds(storage, &source, &validator)? - .into_iter() - .filter(|(_start, amount)| *amount > token::Amount::zero()) - .map(|(start, amount)| { - make_bond_details( - params, - &validator, - amount, - start, - &slashes, - &mut applied_slashes, - ) - }) - .collect(); - - let unbonds = find_unbonds(storage, &source, &validator)? - .into_iter() - .map(|(epoch_range, change)| { - make_unbond_details( - params, - &validator, - change, - epoch_range, - &slashes, - &mut applied_slashes, - ) + } else { + for bond_start in &modified.epochs_to_remove { + let cur_redel_bond_amount = redelegated_bonds + .at(&start) + .at(src_validator) + .get(storage, bond_start) + .unwrap() + .unwrap_or_default(); + let raw_bonds = rbonds + .entry(src_validator.clone()) + .or_default(); + if modified + .epoch_to_modify + .as_ref() + .map(|epoch| bond_start != epoch) + .unwrap_or(true) + { + raw_bonds + .insert(*bond_start, cur_redel_bond_amount); + } else { + raw_bonds.insert( + *bond_start, + cur_redel_bond_amount + - modified + .new_amount + // Safe unwrap - it shouldn't + // get to + // this if it's None + .unwrap(), + ); + } + } + } + } + (start, rbonds) + } }) .collect(); - let details = BondsAndUnbondsDetail { - bonds, - unbonds, - slashes: applied_slashes.get(&validator).cloned().unwrap_or_default(), - }; - let bond_id = BondId { source, validator }; - Ok(HashMap::from_iter([(bond_id, details)])) -} - -fn make_bond_details( - params: &PosParams, - validator: &Address, - deltas_sum: token::Amount, - start: Epoch, - slashes: &[Slash], - applied_slashes: &mut HashMap>, -) -> BondDetails { - let prev_applied_slashes = applied_slashes - .clone() - .get(validator) - .cloned() - .unwrap_or_default(); - - let mut slash_rates_by_epoch = BTreeMap::::new(); - - let validator_slashes = - applied_slashes.entry(validator.clone()).or_default(); - for slash in slashes { - if slash.epoch >= start { - let cur_rate = slash_rates_by_epoch.entry(slash.epoch).or_default(); - *cur_rate = cmp::min(Dec::one(), *cur_rate + slash.rate); - - if !prev_applied_slashes.iter().any(|s| s == slash) { - validator_slashes.push(slash.clone()); - } - } - } - - let slashed_amount = if slash_rates_by_epoch.is_empty() { - None - } else { - let amount_after_slashing = - get_slashed_amount(params, deltas_sum, &slash_rates_by_epoch) - .unwrap(); - Some(deltas_sum - amount_after_slashing) - }; - - BondDetails { - start, - amount: deltas_sum, - slashed_amount, - } + Ok(new_redelegated_unbonds) } -fn make_unbond_details( - params: &PosParams, - validator: &Address, - amount: token::Amount, - (start, withdraw): (Epoch, Epoch), - slashes: &[Slash], - applied_slashes: &mut HashMap>, -) -> UnbondDetails { - let prev_applied_slashes = applied_slashes - .clone() - .get(validator) - .cloned() - .unwrap_or_default(); - let mut slash_rates_by_epoch = BTreeMap::::new(); - - let validator_slashes = - applied_slashes.entry(validator.clone()).or_default(); - for slash in slashes { - if slash.epoch >= start - && slash.epoch - < withdraw - .checked_sub( - params.unbonding_len - + params.cubic_slashing_window_length, - ) - .unwrap_or_default() - { - let cur_rate = slash_rates_by_epoch.entry(slash.epoch).or_default(); - *cur_rate = cmp::min(Dec::one(), *cur_rate + slash.rate); - - if !prev_applied_slashes.iter().any(|s| s == slash) { - validator_slashes.push(slash.clone()); - } - } - } - - let slashed_amount = if slash_rates_by_epoch.is_empty() { - None - } else { - let amount_after_slashing = - get_slashed_amount(params, amount, &slash_rates_by_epoch).unwrap(); - Some(amount - amount_after_slashing) - }; - - UnbondDetails { - start, - withdraw, - amount, - slashed_amount, - } +/// Arguments to [`become_validator`]. +pub struct BecomeValidator<'a> { + /// Proof-of-stake parameters. + pub params: &'a PosParams, + /// The validator's address. + pub address: &'a Address, + /// The validator's consensus key, used by Tendermint. + pub consensus_key: &'a common::PublicKey, + /// The validator's protocol key. + pub protocol_key: &'a common::PublicKey, + /// The validator's Ethereum bridge cold key. + pub eth_cold_key: &'a common::PublicKey, + /// The validator's Ethereum bridge hot key. + pub eth_hot_key: &'a common::PublicKey, + /// The numeric value of the current epoch. + pub current_epoch: Epoch, + /// Commission rate. + pub commission_rate: Dec, + /// Max commission rate change. + pub max_commission_rate_change: Dec, + /// Validator metadata + pub metadata: ValidatorMetaData, + /// Optional offset to use instead of pipeline offset + pub offset_opt: Option, } -/// Tally a running sum of the fraction of rewards owed to each validator in -/// the consensus set. This is used to keep track of the rewards due to each -/// consensus validator over the lifetime of an epoch. -pub fn log_block_rewards( +/// Initialize data for a new validator. +pub fn become_validator( storage: &mut S, - epoch: impl Into, - proposer_address: &Address, - votes: Vec, + args: BecomeValidator<'_>, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, { - // The votes correspond to the last committed block (n-1 if we are - // finalizing block n) - - let epoch: Epoch = epoch.into(); - let params = read_pos_params(storage)?; - let consensus_validators = consensus_validator_set_handle().at(&epoch); - - // Get total stake of the consensus validator set - let total_consensus_stake = - get_total_consensus_stake(storage, epoch, ¶ms)?; - - // Get set of signing validator addresses and the combined stake of - // these signers - let mut signer_set: HashSet
= HashSet::new(); - let mut total_signing_stake = token::Amount::zero(); - for VoteInfo { - validator_address, - validator_vp, - } in votes - { - if validator_vp == 0 { - continue; - } - // Ensure that the validator is not currently jailed or other - let state = validator_state_handle(&validator_address) - .get(storage, epoch, ¶ms)?; - if state != Some(ValidatorState::Consensus) { - return Err(InflationError::ExpectedValidatorInConsensus( - validator_address, - state, - )) - .into_storage_result(); - } - - let stake_from_deltas = - read_validator_stake(storage, ¶ms, &validator_address, epoch)?; - - // Ensure TM stake updates properly with a debug_assert - if cfg!(debug_assertions) { - debug_assert_eq!( - into_tm_voting_power( - params.tm_votes_per_token, - stake_from_deltas, - ), - i64::try_from(validator_vp).unwrap_or_default(), - ); - } + let BecomeValidator { + params, + address, + consensus_key, + protocol_key, + eth_cold_key, + eth_hot_key, + current_epoch, + commission_rate, + max_commission_rate_change, + metadata, + offset_opt, + } = args; + let offset = offset_opt.unwrap_or(params.pipeline_len); - signer_set.insert(validator_address); - total_signing_stake += stake_from_deltas; + if !address.is_established() { + return Err(storage_api::Error::new_const( + "The given address {address} is not established. Only an \ + established address can become a validator.", + )); } - // Get the block rewards coefficients (proposing, signing/voting, - // consensus set status) - let rewards_calculator = PosRewardsCalculator { - proposer_reward: params.block_proposer_reward, - signer_reward: params.block_vote_reward, - signing_stake: total_signing_stake, - total_stake: total_consensus_stake, - }; - let coeffs = rewards_calculator - .get_reward_coeffs() - .map_err(InflationError::Rewards) - .into_storage_result()?; - tracing::debug!( - "PoS rewards coefficients {coeffs:?}, inputs: {rewards_calculator:?}." - ); - - // tracing::debug!( - // "TOTAL SIGNING STAKE (LOGGING BLOCK REWARDS) = {}", - // signing_stake - // ); - - // Compute the fractional block rewards for each consensus validator and - // update the reward accumulators - let consensus_stake_unscaled: Dec = total_consensus_stake.into(); - let signing_stake_unscaled: Dec = total_signing_stake.into(); - let mut values: HashMap = HashMap::new(); - for validator in consensus_validators.iter(storage)? { - let ( - NestedSubKey::Data { - key: stake, - nested_sub_key: _, - }, - address, - ) = validator?; - - if stake.is_zero() { - continue; - } - - let mut rewards_frac = Dec::zero(); - let stake_unscaled: Dec = stake.into(); - // tracing::debug!( - // "NAMADA VALIDATOR STAKE (LOGGING BLOCK REWARDS) OF EPOCH {} = - // {}", epoch, stake - // ); - - // Proposer reward - if address == *proposer_address { - rewards_frac += coeffs.proposer_coeff; - } - // Signer reward - if signer_set.contains(&address) { - let signing_frac = stake_unscaled / signing_stake_unscaled; - rewards_frac += coeffs.signer_coeff * signing_frac; - } - // Consensus validator reward - rewards_frac += coeffs.active_val_coeff - * (stake_unscaled / consensus_stake_unscaled); - - // To be added to the rewards accumulator - values.insert(address, rewards_frac); - } - for (address, value) in values.into_iter() { - // Update the rewards accumulator - rewards_accumulator_handle().update(storage, address, |prev| { - prev.unwrap_or_default() + value - })?; + if is_validator(storage, address)? { + return Err(storage_api::Error::new_const( + "The given address is already a validator", + )); } - Ok(()) -} + // If the address is not yet a validator, it cannot have self-bonds, but it + // may have delegations. + if has_bonds(storage, address)? { + return Err(storage_api::Error::new_const( + "The given address has delegations and therefore cannot become a \ + validator. Unbond first.", + )); + } -#[derive(Clone, Debug)] -struct Rewards { - product: Dec, - commissions: token::Amount, -} + // This will fail if the key is already being used + try_insert_consensus_key(storage, consensus_key)?; -/// Update validator and delegators rewards products and mint the inflation -/// tokens into the PoS account. -/// Any left-over inflation tokens from rounding error of the sum of the -/// rewards is given to the governance address. -pub fn update_rewards_products_and_mint_inflation( - storage: &mut S, - params: &PosParams, - last_epoch: Epoch, - num_blocks_in_last_epoch: u64, - inflation: token::Amount, - staking_token: &Address, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - // Read the rewards accumulator and calculate the new rewards products - // for the previous epoch - let mut reward_tokens_remaining = inflation; - let mut new_rewards_products: HashMap = HashMap::new(); - let mut accumulators_sum = Dec::zero(); - for acc in rewards_accumulator_handle().iter(storage)? { - let (validator, value) = acc?; - accumulators_sum += value; - - // Get reward token amount for this validator - let fractional_claim = value / num_blocks_in_last_epoch; - let reward_tokens = fractional_claim * inflation; - - // Get validator stake at the last epoch - let stake = Dec::from(read_validator_stake( - storage, params, &validator, last_epoch, - )?); - - let commission_rate = validator_commission_rate_handle(&validator) - .get(storage, last_epoch, params)? - .expect("Should be able to find validator commission rate"); - - // Calculate the reward product from the whole validator stake and take - // out the commissions. Because we're using the whole stake to work with - // a single product, we're also taking out commission on validator's - // self-bonds, but it is then included in the rewards claimable by the - // validator so they get it back. - let product = - (Dec::one() - commission_rate) * Dec::from(reward_tokens) / stake; - - // Tally the commission tokens earned by the validator. - // TODO: think abt Dec rounding and if `new_product` should be used - // instead of `reward_tokens` - let commissions = commission_rate * reward_tokens; - - new_rewards_products.insert( - validator, - Rewards { - product, - commissions, - }, - ); + let pipeline_epoch = current_epoch + offset; + validator_addresses_handle() + .at(&pipeline_epoch) + .insert(storage, address.clone())?; - reward_tokens_remaining -= reward_tokens; - } - for ( - validator, - Rewards { - product, - commissions, - }, - ) in new_rewards_products - { - validator_rewards_products_handle(&validator) - .insert(storage, last_epoch, product)?; - // The commissions belong to the validator - add_rewards_to_counter(storage, &validator, &validator, commissions)?; - } + // Non-epoched validator data + write_validator_address_raw_hash(storage, address, consensus_key)?; + write_validator_max_commission_rate_change( + storage, + address, + max_commission_rate_change, + )?; + write_validator_metadata(storage, address, &metadata)?; - // Mint tokens to the PoS account for the last epoch's inflation - let pos_reward_tokens = inflation - reward_tokens_remaining; - tracing::info!( - "Minting tokens for PoS rewards distribution into the PoS account. \ - Amount: {}. Total inflation: {}, number of blocks in the last epoch: \ - {num_blocks_in_last_epoch}, reward accumulators sum: \ - {accumulators_sum}.", - pos_reward_tokens.to_string_native(), - inflation.to_string_native(), - ); - token::credit_tokens( + // Epoched validator data + validator_consensus_key_handle(address).set( + storage, + consensus_key.clone(), + current_epoch, + offset, + )?; + validator_protocol_key_handle(address).set( + storage, + protocol_key.clone(), + current_epoch, + offset, + )?; + validator_eth_hot_key_handle(address).set( + storage, + eth_hot_key.clone(), + current_epoch, + offset, + )?; + validator_eth_cold_key_handle(address).set( + storage, + eth_cold_key.clone(), + current_epoch, + offset, + )?; + validator_commission_rate_handle(address).set( + storage, + commission_rate, + current_epoch, + offset, + )?; + validator_deltas_handle(address).set( storage, - staking_token, - &address::POS, - pos_reward_tokens, + token::Change::zero(), + current_epoch, + offset, )?; - if reward_tokens_remaining > token::Amount::zero() { - tracing::info!( - "Minting tokens remaining from PoS rewards distribution into the \ - Governance account. Amount: {}.", - reward_tokens_remaining.to_string_native() - ); - token::credit_tokens( - storage, - staking_token, - &address::GOV, - reward_tokens_remaining, - )?; - } + // The validator's stake at initialization is 0, so its state is immediately + // below-threshold + validator_state_handle(address).set( + storage, + ValidatorState::BelowThreshold, + current_epoch, + offset, + )?; - // Clear validator rewards accumulators - storage.delete_prefix( - // The prefix of `rewards_accumulator_handle` - &storage::consensus_validator_rewards_accumulator_key(), + insert_validator_into_validator_set( + storage, + params, + address, + token::Amount::zero(), + current_epoch, + offset, )?; Ok(()) } -/// Calculate the cubic slashing rate using all slashes within a window around -/// the given infraction epoch. There is no cap on the rate applied within this -/// function. -pub fn compute_cubic_slash_rate( - storage: &S, - params: &PosParams, - infraction_epoch: Epoch, -) -> storage_api::Result -where - S: StorageRead, -{ - tracing::debug!( - "Computing the cubic slash rate for infraction epoch \ - {infraction_epoch}." - ); - let mut sum_vp_fraction = Dec::zero(); - let (start_epoch, end_epoch) = - params.cubic_slash_epoch_window(infraction_epoch); - - for epoch in Epoch::iter_bounds_inclusive(start_epoch, end_epoch) { - let consensus_stake = - Dec::from(get_total_consensus_stake(storage, epoch, params)?); - tracing::debug!( - "Total consensus stake in epoch {}: {}", - epoch, - consensus_stake - ); - let processing_epoch = epoch + params.slash_processing_epoch_offset(); - let slashes = enqueued_slashes_handle().at(&processing_epoch); - let infracting_stake = slashes.iter(storage)?.fold( - Ok(Dec::zero()), - |acc: storage_api::Result, res| { - let acc = acc?; - let ( - NestedSubKey::Data { - key: validator, - nested_sub_key: _, - }, - _slash, - ) = res?; - - let validator_stake = - read_validator_stake(storage, params, &validator, epoch)?; - // tracing::debug!("Val {} stake: {}", &validator, - // validator_stake); - - Ok(acc + Dec::from(validator_stake)) - }, - )?; - sum_vp_fraction += infracting_stake / consensus_stake; - } - let cubic_rate = - Dec::new(9, 0).unwrap() * sum_vp_fraction * sum_vp_fraction; - tracing::debug!("Cubic slash rate: {}", cubic_rate); - Ok(cubic_rate) -} - -/// Record a slash for a misbehavior that has been received from Tendermint and -/// then jail the validator, removing it from the validator set. The slash rate -/// will be computed at a later epoch. -#[allow(clippy::too_many_arguments)] -pub fn slash( +/// Consensus key change for a validator +pub fn change_consensus_key( storage: &mut S, - params: &PosParams, - current_epoch: Epoch, - evidence_epoch: Epoch, - evidence_block_height: impl Into, - slash_type: SlashType, validator: &Address, - validator_set_update_epoch: Epoch, + consensus_key: &common::PublicKey, + current_epoch: Epoch, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, { - let evidence_block_height: u64 = evidence_block_height.into(); - let slash = Slash { - epoch: evidence_epoch, - block_height: evidence_block_height, - r#type: slash_type, - rate: Dec::zero(), // Let the rate be 0 initially before processing - }; - // Need `+1` because we process at the beginning of a new epoch - let processing_epoch = - evidence_epoch + params.slash_processing_epoch_offset(); - - // Add the slash to the list of enqueued slashes to be processed at a later - // epoch - enqueued_slashes_handle() - .get_data_handler() - .at(&processing_epoch) - .at(validator) - .push(storage, slash)?; - - // Update the most recent slash (infraction) epoch for the validator - let last_slash_epoch = read_validator_last_slash_epoch(storage, validator)?; - if last_slash_epoch.is_none() - || evidence_epoch.0 > last_slash_epoch.unwrap_or_default().0 - { - write_validator_last_slash_epoch(storage, validator, evidence_epoch)?; + tracing::debug!("Changing consensus key for validator {}", validator); + + // Require that the new consensus key is an Ed25519 key + match consensus_key { + common::PublicKey::Ed25519(_) => {} + common::PublicKey::Secp256k1(_) => { + return Err(ConsensusKeyChangeError::MustBeEd25519.into()); + } } - // Jail the validator and update validator sets - jail_validator( + // Check for uniqueness of the consensus key + try_insert_consensus_key(storage, consensus_key)?; + + // Set the new consensus key at the pipeline epoch + let params = read_pos_params(storage)?; + validator_consensus_key_handle(validator).set( storage, - params, - validator, + consensus_key.clone(), current_epoch, - validator_set_update_epoch, + params.pipeline_len, )?; - // No other actions are performed here until the epoch in which the slash is - // processed. + // Write validator's new raw hash + write_validator_address_raw_hash(storage, validator, consensus_key)?; Ok(()) } -/// Process enqueued slashes that were discovered earlier. This function is -/// called upon a new epoch. The final slash rate considering according to the -/// cubic slashing rate is computed. Then, each slash is recorded in storage -/// along with its computed rate, and stake is deducted from the affected -/// validators. -pub fn process_slashes( +/// Withdraw tokens from those that have been unbonded from proof-of-stake +pub fn withdraw_tokens( storage: &mut S, + source: Option<&Address>, + validator: &Address, current_epoch: Epoch, -) -> storage_api::Result<()> +) -> storage_api::Result where S: StorageRead + StorageWrite, { let params = read_pos_params(storage)?; + let source = source.unwrap_or(validator); - if current_epoch.0 < params.slash_processing_epoch_offset() { - return Ok(()); - } - let infraction_epoch = - current_epoch - params.slash_processing_epoch_offset(); + tracing::debug!("Withdrawing tokens in epoch {current_epoch}"); + tracing::debug!("Source {} --> Validator {}", source, validator); - // Slashes to be processed in the current epoch - let enqueued_slashes = enqueued_slashes_handle().at(¤t_epoch); - if enqueued_slashes.is_empty(storage)? { - return Ok(()); - } - tracing::debug!( - "Processing slashes at the beginning of epoch {} (committed in epoch \ - {})", - current_epoch, - infraction_epoch - ); + let unbond_handle: Unbonds = unbond_handle(source, validator); + let redelegated_unbonds = + delegator_redelegated_unbonds_handle(source).at(validator); - // Compute the cubic slash rate - let cubic_slash_rate = - compute_cubic_slash_rate(storage, ¶ms, infraction_epoch)?; + // Check that there are unbonded tokens available for withdrawal + if unbond_handle.is_empty(storage)? { + return Err(WithdrawError::NoUnbondFound(BondId { + source: source.clone(), + validator: validator.clone(), + }) + .into()); + } - // Collect the enqueued slashes and update their rates - let mut eager_validator_slashes: BTreeMap> = - BTreeMap::new(); - let mut eager_validator_slash_rates: HashMap = HashMap::new(); + let mut unbonds_and_redelegated_unbonds: BTreeMap< + (Epoch, Epoch), + (token::Amount, EagerRedelegatedBondsMap), + > = BTreeMap::new(); - // `slashPerValidator` and `slashesMap` while also updating in storage - for enqueued_slash in enqueued_slashes.iter(storage)? { + for unbond in unbond_handle.iter(storage)? { let ( NestedSubKey::Data { - key: validator, - nested_sub_key: _, + key: start_epoch, + nested_sub_key: SubKey::Data(withdraw_epoch), }, - enqueued_slash, - ) = enqueued_slash?; - debug_assert_eq!(enqueued_slash.epoch, infraction_epoch); - - let slash_rate = cmp::min( - Dec::one(), - cmp::max( - enqueued_slash.r#type.get_slash_rate(¶ms), - cubic_slash_rate, - ), - ); - let updated_slash = Slash { - epoch: enqueued_slash.epoch, - block_height: enqueued_slash.block_height, - r#type: enqueued_slash.r#type, - rate: slash_rate, - }; + amount, + ) = unbond?; - let cur_slashes = eager_validator_slashes - .entry(validator.clone()) - .or_default(); - cur_slashes.push(updated_slash); - let cur_rate = - eager_validator_slash_rates.entry(validator).or_default(); - *cur_rate = cmp::min(Dec::one(), *cur_rate + slash_rate); - } + // Logging + tracing::debug!( + "Unbond delta ({start_epoch}..{withdraw_epoch}), amount {}", + amount.to_string_native() + ); + // Consider only unbonds that are eligible to be withdrawn + if withdraw_epoch > current_epoch { + tracing::debug!( + "Not yet withdrawable until epoch {withdraw_epoch}" + ); + continue; + } - // Update the epochs of enqueued slashes in storage - enqueued_slashes_handle().update_data(storage, ¶ms, current_epoch)?; + let mut eager_redelegated_unbonds = EagerRedelegatedBondsMap::default(); + let matching_redelegated_unbonds = + redelegated_unbonds.at(&start_epoch).at(&withdraw_epoch); + for ub in matching_redelegated_unbonds.iter(storage)? { + let ( + NestedSubKey::Data { + key: address, + nested_sub_key: SubKey::Data(epoch), + }, + amount, + ) = ub?; + eager_redelegated_unbonds + .entry(address) + .or_default() + .entry(epoch) + .or_insert(amount); + } - // `resultSlashing` - let mut map_validator_slash: EagerRedelegatedBondsMap = BTreeMap::new(); - for (validator, slash_rate) in eager_validator_slash_rates { - process_validator_slash( - storage, - ¶ms, - &validator, - slash_rate, - current_epoch, - &mut map_validator_slash, - )?; + unbonds_and_redelegated_unbonds.insert( + (start_epoch, withdraw_epoch), + (amount, eager_redelegated_unbonds), + ); } - tracing::debug!("Slashed amounts for validators: {map_validator_slash:#?}"); - // Now update the remaining parts of storage + let slashes = find_validator_slashes(storage, validator)?; - // Write slashes themselves into storage - for (validator, slashes) in eager_validator_slashes { - let validator_slashes = validator_slashes_handle(&validator); - for slash in slashes { - validator_slashes.push(storage, slash)?; - } - } + // `val resultSlashing` + let result_slashing = compute_amount_after_slashing_withdraw( + storage, + ¶ms, + &unbonds_and_redelegated_unbonds, + slashes, + )?; - // Update the validator stakes - for (validator, slash_amounts) in map_validator_slash { - let mut slash_acc = token::Amount::zero(); + let withdrawable_amount = result_slashing.sum; + tracing::debug!( + "Withdrawing total {}", + withdrawable_amount.to_string_native() + ); - // Update validator sets first because it needs to be able to read - // validator stake before we make any changes to it - for (&epoch, &slash_amount) in &slash_amounts { - let state = validator_state_handle(&validator) - .get(storage, epoch, ¶ms)? - .unwrap(); - if state != ValidatorState::Jailed { - update_validator_set( - storage, - ¶ms, - &validator, - -slash_amount.change(), - epoch, - Some(0), - )?; - } - } - // Then update validator and total deltas - for (epoch, slash_amount) in slash_amounts { - let slash_delta = slash_amount - slash_acc; - slash_acc += slash_delta; + // `updateDelegator` with `unbonded` and `redelegeatedUnbonded` + for ((start_epoch, withdraw_epoch), _unbond_and_redelegations) in + unbonds_and_redelegated_unbonds + { + tracing::debug!("Remove ({start_epoch}..{withdraw_epoch}) from unbond"); + unbond_handle + .at(&start_epoch) + .remove(storage, &withdraw_epoch)?; + redelegated_unbonds + .at(&start_epoch) + .remove_all(storage, &withdraw_epoch)?; - update_validator_deltas( - storage, - ¶ms, - &validator, - -slash_delta.change(), - epoch, - Some(0), - )?; - update_total_deltas( - storage, - ¶ms, - -slash_delta.change(), - epoch, - Some(0), - )?; + if unbond_handle.at(&start_epoch).is_empty(storage)? { + unbond_handle.remove_all(storage, &start_epoch)?; + } + if redelegated_unbonds.at(&start_epoch).is_empty(storage)? { + redelegated_unbonds.remove_all(storage, &start_epoch)?; } - - // TODO: should we clear some storage here as is done in Quint?? - // Possibly make the `unbonded` LazyMaps epoched so that it is done - // automatically? } - Ok(()) + // Transfer the withdrawable tokens from the PoS address back to the source + let staking_token = staking_token_address(storage); + token::transfer( + storage, + &staking_token, + &ADDRESS, + source, + withdrawable_amount, + )?; + + // TODO: Transfer the slashed tokens from the PoS address to the Slash Pool + // address + // token::transfer( + // storage, + // &staking_token, + // &ADDRESS, + // &SLASH_POOL_ADDRESS, + // total_slashed, + // )?; + + Ok(withdrawable_amount) } -/// Process a slash by (i) slashing the misbehaving validator; and (ii) any -/// validator to which it has redelegated some tokens and the slash misbehaving -/// epoch is within the redelegation slashing window. -/// -/// `validator` - the misbehaving validator. -/// `slash_rate` - the slash rate. -/// `slashed_amounts_map` - a map from validator address to a map from epoch to -/// already processed slash amounts. -/// -/// Adds any newly processed slash amount of any involved validator to -/// `slashed_amounts_map`. -// Quint `processSlash` -fn process_validator_slash( +/// Change the commission rate of a validator +pub fn change_validator_commission_rate( storage: &mut S, - params: &PosParams, validator: &Address, - slash_rate: Dec, + new_rate: Dec, current_epoch: Epoch, - slashed_amount_map: &mut EagerRedelegatedBondsMap, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, { - // `resultSlashValidator - let result_slash = slash_validator( - storage, - params, - validator, - slash_rate, - current_epoch, - &slashed_amount_map - .get(validator) - .cloned() - .unwrap_or_default(), - )?; - - // `updatedSlashedAmountMap` - let validator_slashes = - slashed_amount_map.entry(validator.clone()).or_default(); - *validator_slashes = result_slash; - - // `outgoingRedelegation` - let outgoing_redelegations = - validator_outgoing_redelegations_handle(validator); - - // Final loop in `processSlash` - let dest_validators = outgoing_redelegations - .iter(storage)? - .map(|res| { - let ( - NestedSubKey::Data { - key: dest_validator, - nested_sub_key: _, - }, - _redelegation, - ) = res?; - Ok(dest_validator) - }) - .collect::>>()?; - - for dest_validator in dest_validators { - let to_modify = slashed_amount_map - .entry(dest_validator.clone()) - .or_default(); - - tracing::debug!( - "Slashing {} redelegation to {}", - validator, - &dest_validator - ); + if new_rate.is_negative() { + return Err(CommissionRateChangeError::NegativeRate( + new_rate, + validator.clone(), + ) + .into()); + } - // `slashValidatorRedelegation` - slash_validator_redelegation( - storage, - params, - validator, - current_epoch, - &outgoing_redelegations.at(&dest_validator), - &validator_slashes_handle(validator), - &validator_total_redelegated_unbonded_handle(&dest_validator), - slash_rate, - to_modify, - )?; + if new_rate > Dec::one() { + return Err(CommissionRateChangeError::LargerThanOne( + new_rate, + validator.clone(), + ) + .into()); } - Ok(()) -} + let max_change = + read_validator_max_commission_rate_change(storage, validator)?; + if max_change.is_none() { + return Err(CommissionRateChangeError::NoMaxSetInStorage( + validator.clone(), + ) + .into()); + } -/// In the context of a redelegation, the function computes how much a validator -/// (the destination validator of the redelegation) should be slashed due to the -/// misbehaving of a second validator (the source validator of the -/// redelegation). The function computes how much the validator would be -/// slashed at all epochs between the current epoch (curEpoch) + 1 and the -/// current epoch + 1 + PIPELINE_OFFSET, accounting for any tokens of the -/// redelegation already unbonded. -/// -/// - `src_validator` - the source validator -/// - `outgoing_redelegations` - a map from pair of epochs to int that includes -/// all the redelegations from the source validator to the destination -/// validator. -/// - The outer key is epoch at which the bond started at the source -/// validator. -/// - The inner key is epoch at which the redelegation started (the epoch at -/// which was issued). -/// - `slashes` a list of slashes of the source validator. -/// - `dest_total_redelegated_unbonded` - a map of unbonded redelegated tokens -/// at the destination validator. -/// - `slash_rate` - the rate of the slash being processed. -/// - `dest_slashed_amounts` - a map from epoch to already processed slash -/// amounts. -/// -/// Adds any newly processed slash amount to `dest_slashed_amounts`. -#[allow(clippy::too_many_arguments)] -fn slash_validator_redelegation( - storage: &S, - params: &OwnedPosParams, - src_validator: &Address, - current_epoch: Epoch, - outgoing_redelegations: &NestedMap>, - slashes: &Slashes, - dest_total_redelegated_unbonded: &TotalRedelegatedUnbonded, - slash_rate: Dec, - dest_slashed_amounts: &mut BTreeMap, -) -> storage_api::Result<()> -where - S: StorageRead, -{ - let infraction_epoch = - current_epoch - params.slash_processing_epoch_offset(); + let params = read_pos_params(storage)?; + let commission_handle = validator_commission_rate_handle(validator); + let pipeline_epoch = current_epoch + params.pipeline_len; - for res in outgoing_redelegations.iter(storage)? { - let ( - NestedSubKey::Data { - key: bond_start, - nested_sub_key: SubKey::Data(redel_start), - }, - amount, - ) = res?; + let rate_at_pipeline = commission_handle + .get(storage, pipeline_epoch, ¶ms)? + .expect("Could not find a rate in given epoch"); + if new_rate == rate_at_pipeline { + return Ok(()); + } + let rate_before_pipeline = commission_handle + .get(storage, pipeline_epoch.prev(), ¶ms)? + .expect("Could not find a rate in given epoch"); - if params.in_redelegation_slashing_window( - infraction_epoch, - redel_start, - params.redelegation_end_epoch_from_start(redel_start), - ) && bond_start <= infraction_epoch - { - slash_redelegation( - storage, - params, - amount, - bond_start, - params.redelegation_end_epoch_from_start(redel_start), - src_validator, - current_epoch, - slashes, - dest_total_redelegated_unbonded, - slash_rate, - dest_slashed_amounts, - )?; - } + let change_from_prev = new_rate.abs_diff(&rate_before_pipeline); + if change_from_prev > max_change.unwrap() { + return Err(CommissionRateChangeError::RateChangeTooLarge( + change_from_prev, + validator.clone(), + ) + .into()); } - Ok(()) + commission_handle.set(storage, new_rate, current_epoch, params.pipeline_len) } -#[allow(clippy::too_many_arguments)] -fn slash_redelegation( +/// Get the total bond amount, including slashes, for a given bond ID and epoch. +/// Returns the bond amount after slashing. For future epochs the value is +/// subject to change. +pub fn bond_amount( storage: &S, - params: &OwnedPosParams, - amount: token::Amount, - bond_start: Epoch, - redel_bond_start: Epoch, - src_validator: &Address, - current_epoch: Epoch, - slashes: &Slashes, - total_redelegated_unbonded: &TotalRedelegatedUnbonded, - slash_rate: Dec, - slashed_amounts: &mut BTreeMap, -) -> storage_api::Result<()> + bond_id: &BondId, + epoch: Epoch, +) -> storage_api::Result where S: StorageRead, { - tracing::debug!( - "\nSlashing redelegation amount {} - bond start {} and \ - redel_bond_start {} - at rate {}\n", - amount.to_string_native(), - bond_start, - redel_bond_start, - slash_rate - ); + let params = read_pos_params(storage)?; + // Outer key is the start epoch used to calculate slashes. The inner + // keys are discarded after applying slashes. + let mut amounts: BTreeMap = BTreeMap::default(); - let infraction_epoch = - current_epoch - params.slash_processing_epoch_offset(); - - // Slash redelegation destination validator from the next epoch only - // as they won't be jailed - let set_update_epoch = current_epoch.next(); - - let mut init_tot_unbonded = - Epoch::iter_bounds_inclusive(infraction_epoch.next(), set_update_epoch) - .map(|epoch| { - let redelegated_unbonded = total_redelegated_unbonded - .at(&epoch) - .at(&redel_bond_start) - .at(src_validator) - .get(storage, &bond_start)? - .unwrap_or_default(); - Ok(redelegated_unbonded) - }) - .sum::>()?; + // Bonds + let bonds = + bond_handle(&bond_id.source, &bond_id.validator).get_data_handler(); + for next in bonds.iter(storage)? { + let (start, delta) = next?; + if start <= epoch { + let amount = amounts.entry(start).or_default(); + *amount += delta; + } + } - for epoch in Epoch::iter_range(set_update_epoch, params.pipeline_len) { - let updated_total_unbonded = { - let redelegated_unbonded = total_redelegated_unbonded - .at(&epoch) - .at(&redel_bond_start) - .at(src_validator) - .get(storage, &bond_start)? - .unwrap_or_default(); - init_tot_unbonded + redelegated_unbonded - }; + // Add unbonds that are still contributing to stake + let unbonds = unbond_handle(&bond_id.source, &bond_id.validator); + for next in unbonds.iter(storage)? { + let ( + NestedSubKey::Data { + key: start, + nested_sub_key: SubKey::Data(withdrawable_epoch), + }, + delta, + ) = next?; + // This is the first epoch in which the unbond stops contributing to + // voting power + let end = withdrawable_epoch - params.withdrawable_epoch_offset() + + params.pipeline_len; - let list_slashes = slashes - .iter(storage)? - .map(Result::unwrap) - .filter(|slash| { - params.in_redelegation_slashing_window( - slash.epoch, - params.redelegation_start_epoch_from_end(redel_bond_start), - redel_bond_start, - ) && bond_start <= slash.epoch - && slash.epoch + params.slash_processing_epoch_offset() - // We're looking for slashes that were processed before or in the epoch - // in which slashes that are currently being processed - // occurred. Because we're slashing in the beginning of an - // epoch, we're also taking slashes that were processed in - // the infraction epoch as they would still be processed - // before any infraction occurred. - <= infraction_epoch - }) - .collect::>(); + if start <= epoch && end > epoch { + let amount = amounts.entry(start).or_default(); + *amount += delta; + } + } - let slashable_amount = amount - .checked_sub(updated_total_unbonded) - .unwrap_or_default(); + if bond_id.validator != bond_id.source { + // Add outgoing redelegations that are still contributing to the source + // validator's stake + let redelegated_bonds = + delegator_redelegated_bonds_handle(&bond_id.source); + for res in redelegated_bonds.iter(storage)? { + let ( + NestedSubKey::Data { + key: _dest_validator, + nested_sub_key: + NestedSubKey::Data { + key: end, + nested_sub_key: + NestedSubKey::Data { + key: src_validator, + nested_sub_key: SubKey::Data(start), + }, + }, + }, + delta, + ) = res?; + if src_validator == bond_id.validator + && start <= epoch + && end > epoch + { + let amount = amounts.entry(start).or_default(); + *amount += delta; + } + } - let slashed = - apply_list_slashes(params, &list_slashes, slashable_amount) - .mul_ceil(slash_rate); + // Add outgoing redelegation unbonds that are still contributing to + // the source validator's stake + let redelegated_unbonds = + delegator_redelegated_unbonds_handle(&bond_id.source); + for res in redelegated_unbonds.iter(storage)? { + let ( + NestedSubKey::Data { + key: _dest_validator, + nested_sub_key: + NestedSubKey::Data { + key: redelegation_epoch, + nested_sub_key: + NestedSubKey::Data { + key: _withdraw_epoch, + nested_sub_key: + NestedSubKey::Data { + key: src_validator, + nested_sub_key: SubKey::Data(start), + }, + }, + }, + }, + delta, + ) = res?; + if src_validator == bond_id.validator + // If the unbonded bond was redelegated after this epoch ... + && redelegation_epoch > epoch + // ... the start was before or at this epoch + && start <= epoch + { + let amount = amounts.entry(start).or_default(); + *amount += delta; + } + } + } - let list_slashes = slashes - .iter(storage)? - .map(Result::unwrap) - .filter(|slash| { - params.in_redelegation_slashing_window( - slash.epoch, - params.redelegation_start_epoch_from_end(redel_bond_start), - redel_bond_start, - ) && bond_start <= slash.epoch - }) - .collect::>(); + if !amounts.is_empty() { + let slashes = find_validator_slashes(storage, &bond_id.validator)?; - let slashable_stake = - apply_list_slashes(params, &list_slashes, slashable_amount) - .mul_ceil(slash_rate); + // Apply slashes + for (&start, amount) in amounts.iter_mut() { + let list_slashes = slashes + .iter() + .filter(|slash| { + let processing_epoch = + slash.epoch + params.slash_processing_epoch_offset(); + // Only use slashes that were processed before or at the + // epoch associated with the bond amount. This assumes + // that slashes are applied before inflation. + processing_epoch <= epoch && start <= slash.epoch + }) + .cloned() + .collect::>(); - init_tot_unbonded = updated_total_unbonded; - let to_slash = cmp::min(slashed, slashable_stake); - if !to_slash.is_zero() { - let map_value = slashed_amounts.entry(epoch).or_default(); - *map_value += to_slash; + *amount = apply_list_slashes(¶ms, &list_slashes, *amount); } } - Ok(()) + Ok(amounts.values().cloned().sum()) } -/// Computes for a given validator and a slash how much should be slashed at all -/// epochs between the currentÃ¥ epoch (curEpoch) + 1 and the current epoch + 1 + -/// PIPELINE_OFFSET, accounting for any tokens already unbonded. -/// -/// - `validator` - the misbehaving validator. -/// - `slash_rate` - the rate of the slash being processed. -/// - `slashed_amounts_map` - a map from epoch to already processed slash -/// amounts. -/// -/// Returns a map that adds any newly processed slash amount to -/// `slashed_amounts_map`. -// `def slashValidator` -fn slash_validator( +/// Get bond amounts within the `claim_start..=claim_end` epoch range for +/// claiming rewards for a given bond ID. Returns a map of bond amounts +/// associated with every epoch within the given epoch range (accumulative) in +/// which an amount contributed to the validator's stake. +/// This function will only consider slashes that were processed before or at +/// the epoch in which we're calculating the bond amount to correspond to the +/// validator stake that was used to calculate reward products (slashes do *not* +/// retrospectively affect the rewards calculated before slash processing). +pub fn bond_amounts_for_rewards( storage: &S, - params: &OwnedPosParams, - validator: &Address, - slash_rate: Dec, - current_epoch: Epoch, - slashed_amounts_map: &BTreeMap, + bond_id: &BondId, + claim_start: Epoch, + claim_end: Epoch, ) -> storage_api::Result> where S: StorageRead, { - tracing::debug!("Slashing validator {} at rate {}", validator, slash_rate); - let infraction_epoch = - current_epoch - params.slash_processing_epoch_offset(); - - let total_unbonded = total_unbonded_handle(validator); - let total_redelegated_unbonded = - validator_total_redelegated_unbonded_handle(validator); - let total_bonded = total_bonded_handle(validator); - let total_redelegated_bonded = - validator_total_redelegated_bonded_handle(validator); - - let mut slashed_amounts = slashed_amounts_map.clone(); + let params = read_pos_params(storage)?; + // Outer key is every epoch in which the a bond amount contributed to stake + // and the inner key is the start epoch used to calculate slashes. The inner + // keys are discarded after applying slashes. + let mut amounts: BTreeMap> = + BTreeMap::default(); - let mut tot_bonds = total_bonded - .get_data_handler() - .iter(storage)? - .map(Result::unwrap) - .filter(|&(epoch, bonded)| { - epoch <= infraction_epoch && bonded > 0.into() - }) - .collect::>(); - - let mut redelegated_bonds = tot_bonds - .keys() - .filter(|&epoch| { - !total_redelegated_bonded - .at(epoch) - .is_empty(storage) - .unwrap() - }) - .map(|epoch| { - let tot_redel_bonded = total_redelegated_bonded - .at(epoch) - .collect_map(storage) - .unwrap(); - (*epoch, tot_redel_bonded) - }) - .collect::>(); - - let mut sum = token::Amount::zero(); - - let eps = current_epoch - .iter_range(params.pipeline_len) - .collect::>(); - for epoch in eps.into_iter().rev() { - let amount = tot_bonds.iter().fold( - token::Amount::zero(), - |acc, (bond_start, bond_amount)| { - acc + compute_slash_bond_at_epoch( - storage, - params, - validator, - epoch, - infraction_epoch, - *bond_start, - *bond_amount, - redelegated_bonds.get(bond_start), - slash_rate, - ) - .unwrap() - }, - ); + // Only need to do bonds since rewwards are accumulated during + // `unbond_tokens` + let bonds = + bond_handle(&bond_id.source, &bond_id.validator).get_data_handler(); + for next in bonds.iter(storage)? { + let (start, delta) = next?; - let new_bonds = total_unbonded.at(&epoch); - tot_bonds = new_bonds - .collect_map(storage) - .unwrap() - .into_iter() - .filter(|(ep, _)| *ep <= infraction_epoch) - .collect::>(); - - let new_redelegated_bonds = tot_bonds - .keys() - .filter(|&ep| { - !total_redelegated_unbonded.at(ep).is_empty(storage).unwrap() - }) - .map(|ep| { - ( - *ep, - total_redelegated_unbonded - .at(&epoch) - .at(ep) - .collect_map(storage) - .unwrap(), - ) - }) - .collect::>(); + for ep in Epoch::iter_bounds_inclusive(claim_start, claim_end) { + // A bond that wasn't unbonded is added to all epochs up to + // `claim_end` + if start <= ep { + let amount = + amounts.entry(ep).or_default().entry(start).or_default(); + *amount += delta; + } + } + } - redelegated_bonds = new_redelegated_bonds; + if !amounts.is_empty() { + let slashes = find_validator_slashes(storage, &bond_id.validator)?; + let redelegated_bonded = + delegator_redelegated_bonds_handle(&bond_id.source) + .at(&bond_id.validator); - // `newSum` - sum += amount; + // Apply slashes + for (&ep, amounts) in amounts.iter_mut() { + for (&start, amount) in amounts.iter_mut() { + let list_slashes = slashes + .iter() + .filter(|slash| { + let processing_epoch = slash.epoch + + params.slash_processing_epoch_offset(); + // Only use slashes that were processed before or at the + // epoch associated with the bond amount. This assumes + // that slashes are applied before inflation. + processing_epoch <= ep && start <= slash.epoch + }) + .cloned() + .collect::>(); - // `newSlashesMap` - let cur = slashed_amounts.entry(epoch).or_default(); - *cur += sum; - } - // Hack - should this be done differently? (think this is safe) - let pipeline_epoch = current_epoch + params.pipeline_len; - let last_amt = slashed_amounts - .get(&pipeline_epoch.prev()) - .cloned() - .unwrap(); - slashed_amounts.insert(pipeline_epoch, last_amt); + let slash_epoch_filter = + |e: Epoch| e + params.slash_processing_epoch_offset() <= ep; - Ok(slashed_amounts) -} + let redelegated_bonds = + redelegated_bonded.at(&start).collect_map(storage)?; -/// Get the remaining token amount in a bond after applying a set of slashes. -/// -/// - `validator` - the bond's validator -/// - `epoch` - the latest slash epoch to consider. -/// - `start` - the start epoch of the bond -/// - `redelegated_bonds` -fn compute_bond_at_epoch( - storage: &S, - params: &OwnedPosParams, - validator: &Address, - epoch: Epoch, - start: Epoch, - amount: token::Amount, - redelegated_bonds: Option<&EagerRedelegatedBondsMap>, -) -> storage_api::Result -where - S: StorageRead, -{ - let list_slashes = validator_slashes_handle(validator) - .iter(storage)? - .map(Result::unwrap) - .filter(|slash| { - start <= slash.epoch - && slash.epoch + params.slash_processing_epoch_offset() <= epoch - }) - .collect::>(); + let result_fold = fold_and_slash_redelegated_bonds( + storage, + ¶ms, + &redelegated_bonds, + start, + &list_slashes, + slash_epoch_filter, + ); - let slash_epoch_filter = - |e: Epoch| e + params.slash_processing_epoch_offset() <= epoch; + let total_not_redelegated = + *amount - result_fold.total_redelegated; - let result_fold = redelegated_bonds - .map(|redelegated_bonds| { - fold_and_slash_redelegated_bonds( - storage, - params, - redelegated_bonds, - start, - &list_slashes, - slash_epoch_filter, - ) - }) - .unwrap_or_default(); + let after_not_redelegated = apply_list_slashes( + ¶ms, + &list_slashes, + total_not_redelegated, + ); - let total_not_redelegated = amount - result_fold.total_redelegated; - let after_not_redelegated = - apply_list_slashes(params, &list_slashes, total_not_redelegated); + *amount = + after_not_redelegated + result_fold.total_after_slashing; + } + } + } - Ok(after_not_redelegated + result_fold.total_after_slashing) + Ok(amounts + .into_iter() + // Flatten the inner maps to discard bond start epochs + .map(|(ep, amounts)| (ep, amounts.values().cloned().sum())) + .collect()) } -/// Uses `fn compute_bond_at_epoch` to compute the token amount to slash in -/// order to prevent overslashing. -#[allow(clippy::too_many_arguments)] -fn compute_slash_bond_at_epoch( +/// Get the genesis consensus validators stake and consensus key for Tendermint, +/// converted from [`ValidatorSetUpdate`]s using the given function. +pub fn genesis_validator_set_tendermint( storage: &S, - params: &OwnedPosParams, - validator: &Address, - epoch: Epoch, - infraction_epoch: Epoch, - bond_start: Epoch, - bond_amount: token::Amount, - redelegated_bonds: Option<&EagerRedelegatedBondsMap>, - slash_rate: Dec, -) -> storage_api::Result + params: &PosParams, + current_epoch: Epoch, + mut f: impl FnMut(ValidatorSetUpdate) -> T, +) -> storage_api::Result> where S: StorageRead, { - let amount_due = compute_bond_at_epoch( - storage, - params, - validator, - infraction_epoch, - bond_start, - bond_amount, - redelegated_bonds, - )? - .mul_ceil(slash_rate); - let slashable_amount = compute_bond_at_epoch( - storage, - params, - validator, - epoch, - bond_start, - bond_amount, - redelegated_bonds, - )?; - Ok(cmp::min(amount_due, slashable_amount)) + let consensus_validator_handle = + consensus_validator_set_handle().at(¤t_epoch); + let iter = consensus_validator_handle.iter(storage)?; + + iter.map(|validator| { + let ( + NestedSubKey::Data { + key: new_stake, + nested_sub_key: _, + }, + address, + ) = validator?; + let consensus_key = validator_consensus_key_handle(&address) + .get(storage, current_epoch, params)? + .unwrap(); + let converted = f(ValidatorSetUpdate::Consensus(ConsensusValidator { + consensus_key, + bonded_stake: new_stake, + })); + Ok(converted) + }) + .collect() } /// Unjail a validator that is currently jailed. @@ -5240,36 +1922,11 @@ pub fn get_total_consensus_stake( where S: StorageRead, { - total_consensus_stake_key_handle() + total_consensus_stake_handle() .get(storage, epoch, params) .map(|o| o.expect("Total consensus stake could not be retrieved.")) } -/// Find slashes applicable to a validator with inclusive `start` and exclusive -/// `end` epoch. -#[allow(dead_code)] -fn find_slashes_in_range( - storage: &S, - start: Epoch, - end: Option, - validator: &Address, -) -> storage_api::Result> -where - S: StorageRead, -{ - let mut slashes = BTreeMap::::new(); - for slash in validator_slashes_handle(validator).iter(storage)? { - let slash = slash?; - if start <= slash.epoch - && end.map(|end| slash.epoch < end).unwrap_or(true) - { - let cur_rate = slashes.entry(slash.epoch).or_default(); - *cur_rate = cmp::min(*cur_rate + slash.rate, Dec::one()); - } - } - Ok(slashes) -} - /// Redelegate bonded tokens from a source validator to a destination validator pub fn redelegate_tokens( storage: &mut S, @@ -5504,35 +2161,32 @@ where } }; - let pipeline_stake = - read_validator_stake(storage, ¶ms, validator, pipeline_epoch)?; - // Remove the validator from the validator set. If it is in the consensus // set, promote the next validator. match pipeline_state { - ValidatorState::Consensus => deactivate_consensus_validator( - storage, - validator, - pipeline_epoch, - pipeline_stake, - )?, + ValidatorState::Consensus => { + // Remove from the consensus set first + remove_consensus_validator( + storage, + ¶ms, + pipeline_epoch, + validator, + )?; + + // Promote the next below-capacity validator to consensus + promote_next_below_capacity_validator_to_consensus( + storage, + pipeline_epoch, + )?; + } ValidatorState::BelowCapacity => { - let below_capacity_set = below_capacity_validator_set_handle() - .at(&pipeline_epoch) - .at(&pipeline_stake.into()); - // TODO: handle the unwrap better here - let val_position = validator_set_positions_handle() - .at(&pipeline_epoch) - .get(storage, validator)? - .unwrap(); - let removed = below_capacity_set.remove(storage, &val_position)?; - debug_assert_eq!(removed, Some(validator.clone())); - - // Remove position - validator_set_positions_handle() - .at(&pipeline_epoch) - .remove(storage, validator)?; + remove_below_capacity_validator( + storage, + ¶ms, + pipeline_epoch, + validator, + )?; } ValidatorState::BelowThreshold => {} ValidatorState::Inactive => { @@ -5562,65 +2216,6 @@ where Ok(()) } -fn deactivate_consensus_validator( - storage: &mut S, - - validator: &Address, - target_epoch: Epoch, - stake: token::Amount, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - let consensus_set = consensus_validator_set_handle() - .at(&target_epoch) - .at(&stake); - // TODO: handle the unwrap better here - let val_position = validator_set_positions_handle() - .at(&target_epoch) - .get(storage, validator)? - .unwrap(); - let removed = consensus_set.remove(storage, &val_position)?; - debug_assert_eq!(removed, Some(validator.clone())); - - // Remove position - validator_set_positions_handle() - .at(&target_epoch) - .remove(storage, validator)?; - - // Now promote the next below-capacity validator to the consensus - // set - let below_cap_set = below_capacity_validator_set_handle().at(&target_epoch); - let max_below_capacity_validator_amount = - get_max_below_capacity_validator_amount(&below_cap_set, storage)?; - - if let Some(max_bc_amount) = max_below_capacity_validator_amount { - let below_cap_vals_max = below_cap_set.at(&max_bc_amount.into()); - let lowest_position = - find_first_position(&below_cap_vals_max, storage)?.unwrap(); - let removed_max_below_capacity = below_cap_vals_max - .remove(storage, &lowest_position)? - .expect("Must have been removed"); - - insert_validator_into_set( - &consensus_validator_set_handle() - .at(&target_epoch) - .at(&max_bc_amount), - storage, - &target_epoch, - &removed_max_below_capacity, - )?; - validator_state_handle(&removed_max_below_capacity).set( - storage, - ValidatorState::Consensus, - target_epoch, - 0, - )?; - } - - Ok(()) -} - /// Re-activate an inactive validator pub fn reactivate_validator( storage: &mut S, @@ -5867,6 +2462,7 @@ pub mod test_utils { use super::*; use crate::parameters::PosParams; + use crate::storage::read_non_pos_owned_params; use crate::types::GenesisValidator; /// Helper function to initialize storage with PoS data @@ -5945,152 +2541,12 @@ pub mod test_utils { { let gov_params = namada_core::ledger::governance::parameters::GovernanceParameters::default(); gov_params.init_storage(storage)?; - let params = crate::read_non_pos_owned_params(storage, owned)?; + let params = read_non_pos_owned_params(storage, owned)?; init_genesis_helper(storage, ¶ms, validators, current_epoch)?; Ok(params) } } -/// Read PoS validator's email. -pub fn read_validator_email( - storage: &S, - validator: &Address, -) -> storage_api::Result> -where - S: StorageRead, -{ - storage.read(&validator_email_key(validator)) -} - -/// Write PoS validator's email. The email cannot be removed, so an empty string -/// will result in an error. -pub fn write_validator_email( - storage: &mut S, - validator: &Address, - email: &String, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - let key = validator_email_key(validator); - if email.is_empty() { - Err(MetadataError::CannotRemoveEmail.into()) - } else { - storage.write(&key, email) - } -} - -/// Read PoS validator's description. -pub fn read_validator_description( - storage: &S, - validator: &Address, -) -> storage_api::Result> -where - S: StorageRead, -{ - storage.read(&validator_description_key(validator)) -} - -/// Write PoS validator's description. If the provided arg is an empty string, -/// remove the data. -pub fn write_validator_description( - storage: &mut S, - validator: &Address, - description: &String, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - let key = validator_description_key(validator); - if description.is_empty() { - storage.delete(&key) - } else { - storage.write(&key, description) - } -} - -/// Read PoS validator's website. -pub fn read_validator_website( - storage: &S, - validator: &Address, -) -> storage_api::Result> -where - S: StorageRead, -{ - storage.read(&validator_website_key(validator)) -} - -/// Write PoS validator's website. If the provided arg is an empty string, -/// remove the data. -pub fn write_validator_website( - storage: &mut S, - validator: &Address, - website: &String, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - let key = validator_website_key(validator); - if website.is_empty() { - storage.delete(&key) - } else { - storage.write(&key, website) - } -} - -/// Read PoS validator's discord handle. -pub fn read_validator_discord_handle( - storage: &S, - validator: &Address, -) -> storage_api::Result> -where - S: StorageRead, -{ - storage.read(&validator_discord_key(validator)) -} - -/// Write PoS validator's discord handle. If the provided arg is an empty -/// string, remove the data. -pub fn write_validator_discord_handle( - storage: &mut S, - validator: &Address, - discord_handle: &String, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - let key = validator_discord_key(validator); - if discord_handle.is_empty() { - storage.delete(&key) - } else { - storage.write(&key, discord_handle) - } -} - -/// Write validator's metadata. -pub fn write_validator_metadata( - storage: &mut S, - validator: &Address, - metadata: &ValidatorMetaData, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - // Email is the only required field in the metadata - write_validator_email(storage, validator, &metadata.email)?; - - if let Some(description) = metadata.description.as_ref() { - write_validator_description(storage, validator, description)?; - } - if let Some(website) = metadata.website.as_ref() { - write_validator_website(storage, validator, website)?; - } - if let Some(discord) = metadata.discord_handle.as_ref() { - write_validator_discord_handle(storage, validator, discord)?; - } - Ok(()) -} - /// Change validator's metadata. In addition to changing any of the data from /// [`ValidatorMetaData`], the validator's commission rate can be changed within /// here as well. @@ -6131,63 +2587,6 @@ where Ok(()) } -/// Compute the current available rewards amount due only to existing bonds. -/// This does not include pending rewards held in the rewards counter due to -/// unbonds and redelegations. -pub fn compute_current_rewards_from_bonds( - storage: &S, - source: &Address, - validator: &Address, - current_epoch: Epoch, -) -> storage_api::Result -where - S: StorageRead, -{ - if current_epoch == Epoch::default() { - // Nothing to claim in the first epoch - return Ok(token::Amount::zero()); - } - - let last_claim_epoch = - get_last_reward_claim_epoch(storage, source, validator)?; - if let Some(last_epoch) = last_claim_epoch { - if last_epoch == current_epoch { - // Already claimed in this epoch - return Ok(token::Amount::zero()); - } - } - - let mut reward_tokens = token::Amount::zero(); - - // Want to claim from `last_claim_epoch` to `current_epoch.prev()` since - // rewards are computed at the end of an epoch - let (claim_start, claim_end) = ( - last_claim_epoch.unwrap_or_default(), - // Safe because of the check above - current_epoch.prev(), - ); - let bond_amounts = bond_amounts_for_rewards( - storage, - &BondId { - source: source.clone(), - validator: validator.clone(), - }, - claim_start, - claim_end, - )?; - - let rewards_products = validator_rewards_products_handle(validator); - for (ep, bond_amount) in bond_amounts { - debug_assert!(ep >= claim_start); - debug_assert!(ep <= claim_end); - let rp = rewards_products.get(storage, &ep)?.unwrap_or_default(); - let reward = rp * bond_amount; - reward_tokens += reward; - } - - Ok(reward_tokens) -} - /// Claim available rewards, triggering an immediate transfer of tokens from the /// PoS account to the source address. pub fn claim_reward_tokens( @@ -6248,77 +2647,6 @@ where Ok(rewards_from_bonds + rewards_from_counter) } -/// Get the last epoch in which rewards were claimed from storage, if any -pub fn get_last_reward_claim_epoch( - storage: &S, - delegator: &Address, - validator: &Address, -) -> storage_api::Result> -where - S: StorageRead, -{ - let key = last_pos_reward_claim_epoch_key(delegator, validator); - storage.read(&key) -} - -fn write_last_reward_claim_epoch( - storage: &mut S, - delegator: &Address, - validator: &Address, - epoch: Epoch, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - let key = last_pos_reward_claim_epoch_key(delegator, validator); - storage.write(&key, epoch) -} - -/// Read the current token value in the rewards counter. -fn read_rewards_counter( - storage: &S, - source: &Address, - validator: &Address, -) -> storage_api::Result -where - S: StorageRead, -{ - let key = rewards_counter_key(source, validator); - Ok(storage.read::(&key)?.unwrap_or_default()) -} - -/// Add tokens to a rewards counter. -fn add_rewards_to_counter( - storage: &mut S, - source: &Address, - validator: &Address, - new_rewards: token::Amount, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - let key = rewards_counter_key(source, validator); - let current_rewards = - storage.read::(&key)?.unwrap_or_default(); - storage.write(&key, current_rewards + new_rewards) -} - -/// Take tokens from a rewards counter. Deletes the record after reading. -fn take_rewards_from_counter( - storage: &mut S, - source: &Address, - validator: &Address, -) -> storage_api::Result -where - S: StorageRead + StorageWrite, -{ - let key = rewards_counter_key(source, validator); - let current_rewards = - storage.read::(&key)?.unwrap_or_default(); - storage.delete(&key)?; - Ok(current_rewards) -} - /// Jail a validator by removing it from and updating the validator sets and /// changing a its state to `Jailed`. Validators are jailed for liveness and for /// misbehaving. @@ -6353,61 +2681,15 @@ where "Removing validator from the consensus set in epoch {}", epoch ); - let amount_pre = - read_validator_stake(storage, params, validator, epoch)?; - let val_position = validator_set_positions_handle() - .at(&epoch) - .get(storage, validator)? - .expect("Could not find validator's position in storage."); - let _ = consensus_validator_set_handle() - .at(&epoch) - .at(&amount_pre) - .remove(storage, &val_position)?; - validator_set_positions_handle() - .at(&epoch) - .remove(storage, validator)?; + remove_consensus_validator(storage, params, epoch, validator)?; // For the pipeline epoch only: // promote the next max inactive validator to the active // validator set at the pipeline offset if epoch == pipeline_epoch { - let below_capacity_handle = - below_capacity_validator_set_handle().at(&epoch); - let max_below_capacity_amount = - get_max_below_capacity_validator_amount( - &below_capacity_handle, - storage, - )?; - if let Some(max_below_capacity_amount) = - max_below_capacity_amount - { - let position_to_promote = find_first_position( - &below_capacity_handle - .at(&max_below_capacity_amount.into()), - storage, - )? - .expect("Should return a position."); - let max_bc_validator = below_capacity_handle - .at(&max_below_capacity_amount.into()) - .remove(storage, &position_to_promote)? - .expect( - "Should have returned a removed validator.", - ); - insert_validator_into_set( - &consensus_validator_set_handle() - .at(&epoch) - .at(&max_below_capacity_amount), - storage, - &epoch, - &max_bc_validator, - )?; - validator_state_handle(&max_bc_validator).set( - storage, - ValidatorState::Consensus, - current_epoch, - params.pipeline_len, - )?; - } + promote_next_below_capacity_validator_to_consensus( + storage, epoch, + )?; } } ValidatorState::BelowCapacity => { @@ -6416,22 +2698,9 @@ where {}", epoch ); - - let amount_pre = validator_deltas_handle(validator) - .get_sum(storage, epoch, params)? - .unwrap_or_default(); - debug_assert!(amount_pre.non_negative()); - let val_position = validator_set_positions_handle() - .at(&epoch) - .get(storage, validator)? - .expect("Could not find validator's position in storage."); - let _ = below_capacity_validator_set_handle() - .at(&epoch) - .at(&token::Amount::from_change(amount_pre).into()) - .remove(storage, &val_position)?; - validator_set_positions_handle() - .at(&epoch) - .remove(storage, validator)?; + remove_below_capacity_validator( + storage, params, epoch, validator, + )?; } ValidatorState::BelowThreshold => { tracing::debug!( diff --git a/proof_of_stake/src/pos_queries.rs b/proof_of_stake/src/pos_queries.rs index c0d0fbaf28..6effbcfc51 100644 --- a/proof_of_stake/src/pos_queries.rs +++ b/proof_of_stake/src/pos_queries.rs @@ -11,11 +11,12 @@ use namada_core::types::storage::{BlockHeight, Epoch}; use namada_core::types::{key, token}; use thiserror::Error; +use crate::storage::find_validator_by_raw_hash; use crate::types::WeightedValidator; use crate::{ - consensus_validator_set_handle, find_validator_by_raw_hash, - get_total_consensus_stake, read_pos_params, validator_eth_cold_key_handle, - validator_eth_hot_key_handle, ConsensusValidatorSet, PosParams, + consensus_validator_set_handle, get_total_consensus_stake, read_pos_params, + validator_eth_cold_key_handle, validator_eth_hot_key_handle, + ConsensusValidatorSet, PosParams, }; /// Errors returned by [`PosQueries`] operations. diff --git a/proof_of_stake/src/queries.rs b/proof_of_stake/src/queries.rs new file mode 100644 index 0000000000..137d5fdf7a --- /dev/null +++ b/proof_of_stake/src/queries.rs @@ -0,0 +1,457 @@ +//! Queriezzz + +use std::cmp; +use std::collections::{BTreeMap, HashMap, HashSet}; + +use borsh::BorshDeserialize; +use namada_core::ledger::storage_api::collections::lazy_map::{ + NestedSubKey, SubKey, +}; +use namada_core::ledger::storage_api::{self, StorageRead}; +use namada_core::types::address::Address; +use namada_core::types::dec::Dec; +use namada_core::types::storage::Epoch; +use namada_core::types::token; + +use crate::slashing::{find_validator_slashes, get_slashed_amount}; +use crate::storage::{bond_handle, read_pos_params, unbond_handle}; +use crate::types::{ + BondDetails, BondId, BondsAndUnbondsDetail, BondsAndUnbondsDetails, Slash, + UnbondDetails, +}; +use crate::{storage_key, PosParams}; + +/// Find all validators to which a given bond `owner` (or source) has a +/// delegation +pub fn find_delegation_validators( + storage: &S, + owner: &Address, +) -> storage_api::Result> +where + S: StorageRead, +{ + let bonds_prefix = storage_key::bonds_for_source_prefix(owner); + let mut delegations: HashSet
= HashSet::new(); + + for iter_result in storage_api::iter_prefix_bytes(storage, &bonds_prefix)? { + let (key, _bond_bytes) = iter_result?; + let validator_address = storage_key::get_validator_address_from_bond( + &key, + ) + .ok_or_else(|| { + storage_api::Error::new_const( + "Delegation key should contain validator address.", + ) + })?; + delegations.insert(validator_address); + } + Ok(delegations) +} + +/// Find all validators to which a given bond `owner` (or source) has a +/// delegation with the amount +pub fn find_delegations( + storage: &S, + owner: &Address, + epoch: &Epoch, +) -> storage_api::Result> +where + S: StorageRead, +{ + let bonds_prefix = storage_key::bonds_for_source_prefix(owner); + let params = read_pos_params(storage)?; + let mut delegations: HashMap = HashMap::new(); + + for iter_result in storage_api::iter_prefix_bytes(storage, &bonds_prefix)? { + let (key, _bond_bytes) = iter_result?; + let validator_address = storage_key::get_validator_address_from_bond( + &key, + ) + .ok_or_else(|| { + storage_api::Error::new_const( + "Delegation key should contain validator address.", + ) + })?; + let deltas_sum = bond_handle(owner, &validator_address) + .get_sum(storage, *epoch, ¶ms)? + .unwrap_or_default(); + delegations.insert(validator_address, deltas_sum); + } + Ok(delegations) +} + +/// Find if the given source address has any bonds. +pub fn has_bonds(storage: &S, source: &Address) -> storage_api::Result +where + S: StorageRead, +{ + let max_epoch = Epoch(u64::MAX); + let delegations = find_delegations(storage, source, &max_epoch)?; + Ok(!delegations + .values() + .cloned() + .sum::() + .is_zero()) +} + +/// Find raw bond deltas for the given source and validator address. +pub fn find_bonds( + storage: &S, + source: &Address, + validator: &Address, +) -> storage_api::Result> +where + S: StorageRead, +{ + bond_handle(source, validator) + .get_data_handler() + .iter(storage)? + .collect() +} + +/// Find raw unbond deltas for the given source and validator address. +pub fn find_unbonds( + storage: &S, + source: &Address, + validator: &Address, +) -> storage_api::Result> +where + S: StorageRead, +{ + unbond_handle(source, validator) + .iter(storage)? + .map(|next_result| { + let ( + NestedSubKey::Data { + key: start_epoch, + nested_sub_key: SubKey::Data(withdraw_epoch), + }, + amount, + ) = next_result?; + Ok(((start_epoch, withdraw_epoch), amount)) + }) + .collect() +} + +/// Collect the details of all bonds and unbonds that match the source and +/// validator arguments. If either source or validator is `None`, then grab the +/// information for all sources or validators, respectively. +pub fn bonds_and_unbonds( + storage: &S, + source: Option
, + validator: Option
, +) -> storage_api::Result +where + S: StorageRead, +{ + let params = read_pos_params(storage)?; + + match (source.clone(), validator.clone()) { + (Some(source), Some(validator)) => { + find_bonds_and_unbonds_details(storage, ¶ms, source, validator) + } + _ => { + get_multiple_bonds_and_unbonds(storage, ¶ms, source, validator) + } + } +} + +fn get_multiple_bonds_and_unbonds( + storage: &S, + params: &PosParams, + source: Option
, + validator: Option
, +) -> storage_api::Result +where + S: StorageRead, +{ + debug_assert!( + source.is_none() || validator.is_none(), + "Use `find_bonds_and_unbonds_details` when full bond ID is known" + ); + let mut slashes_cache = HashMap::>::new(); + // Applied slashes grouped by validator address + let mut applied_slashes = HashMap::>::new(); + + // TODO: if validator is `Some`, look-up all its bond owners (including + // self-bond, if any) first + + let prefix = match source.as_ref() { + Some(source) => storage_key::bonds_for_source_prefix(source), + None => storage_key::bonds_prefix(), + }; + // We have to iterate raw bytes, cause the epoched data `last_update` field + // gets matched here too + let mut raw_bonds = storage_api::iter_prefix_bytes(storage, &prefix)? + .filter_map(|result| { + if let Ok((key, val_bytes)) = result { + if let Some((bond_id, start)) = storage_key::is_bond_key(&key) { + if source.is_some() + && source.as_ref().unwrap() != &bond_id.source + { + return None; + } + if validator.is_some() + && validator.as_ref().unwrap() != &bond_id.validator + { + return None; + } + let change: token::Amount = + BorshDeserialize::try_from_slice(&val_bytes).ok()?; + if change.is_zero() { + return None; + } + return Some((bond_id, start, change)); + } + } + None + }); + + let prefix = match source.as_ref() { + Some(source) => storage_key::unbonds_for_source_prefix(source), + None => storage_key::unbonds_prefix(), + }; + let mut raw_unbonds = storage_api::iter_prefix_bytes(storage, &prefix)? + .filter_map(|result| { + if let Ok((key, val_bytes)) = result { + if let Some((bond_id, start, withdraw)) = + storage_key::is_unbond_key(&key) + { + if source.is_some() + && source.as_ref().unwrap() != &bond_id.source + { + return None; + } + if validator.is_some() + && validator.as_ref().unwrap() != &bond_id.validator + { + return None; + } + match (source.clone(), validator.clone()) { + (None, Some(validator)) => { + if bond_id.validator != validator { + return None; + } + } + (Some(owner), None) => { + if owner != bond_id.source { + return None; + } + } + _ => {} + } + let amount: token::Amount = + BorshDeserialize::try_from_slice(&val_bytes).ok()?; + return Some((bond_id, start, withdraw, amount)); + } + } + None + }); + + let mut bonds_and_unbonds = + HashMap::, Vec)>::new(); + + raw_bonds.try_for_each(|(bond_id, start, change)| { + if !slashes_cache.contains_key(&bond_id.validator) { + let slashes = find_validator_slashes(storage, &bond_id.validator)?; + slashes_cache.insert(bond_id.validator.clone(), slashes); + } + let slashes = slashes_cache + .get(&bond_id.validator) + .expect("We must have inserted it if it's not cached already"); + let validator = bond_id.validator.clone(); + let (bonds, _unbonds) = bonds_and_unbonds.entry(bond_id).or_default(); + bonds.push(make_bond_details( + params, + &validator, + change, + start, + slashes, + &mut applied_slashes, + )); + Ok::<_, storage_api::Error>(()) + })?; + + raw_unbonds.try_for_each(|(bond_id, start, withdraw, amount)| { + if !slashes_cache.contains_key(&bond_id.validator) { + let slashes = find_validator_slashes(storage, &bond_id.validator)?; + slashes_cache.insert(bond_id.validator.clone(), slashes); + } + let slashes = slashes_cache + .get(&bond_id.validator) + .expect("We must have inserted it if it's not cached already"); + let validator = bond_id.validator.clone(); + let (_bonds, unbonds) = bonds_and_unbonds.entry(bond_id).or_default(); + unbonds.push(make_unbond_details( + params, + &validator, + amount, + (start, withdraw), + slashes, + &mut applied_slashes, + )); + Ok::<_, storage_api::Error>(()) + })?; + + Ok(bonds_and_unbonds + .into_iter() + .map(|(bond_id, (bonds, unbonds))| { + let details = BondsAndUnbondsDetail { + bonds, + unbonds, + slashes: applied_slashes + .get(&bond_id.validator) + .cloned() + .unwrap_or_default(), + }; + (bond_id, details) + }) + .collect()) +} + +fn find_bonds_and_unbonds_details( + storage: &S, + params: &PosParams, + source: Address, + validator: Address, +) -> storage_api::Result +where + S: StorageRead, +{ + let slashes = find_validator_slashes(storage, &validator)?; + let mut applied_slashes = HashMap::>::new(); + + let bonds = find_bonds(storage, &source, &validator)? + .into_iter() + .filter(|(_start, amount)| *amount > token::Amount::zero()) + .map(|(start, amount)| { + make_bond_details( + params, + &validator, + amount, + start, + &slashes, + &mut applied_slashes, + ) + }) + .collect(); + + let unbonds = find_unbonds(storage, &source, &validator)? + .into_iter() + .map(|(epoch_range, change)| { + make_unbond_details( + params, + &validator, + change, + epoch_range, + &slashes, + &mut applied_slashes, + ) + }) + .collect(); + + let details = BondsAndUnbondsDetail { + bonds, + unbonds, + slashes: applied_slashes.get(&validator).cloned().unwrap_or_default(), + }; + let bond_id = BondId { source, validator }; + Ok(HashMap::from_iter([(bond_id, details)])) +} + +fn make_bond_details( + params: &PosParams, + validator: &Address, + deltas_sum: token::Amount, + start: Epoch, + slashes: &[Slash], + applied_slashes: &mut HashMap>, +) -> BondDetails { + let prev_applied_slashes = applied_slashes + .clone() + .get(validator) + .cloned() + .unwrap_or_default(); + + let mut slash_rates_by_epoch = BTreeMap::::new(); + + let validator_slashes = + applied_slashes.entry(validator.clone()).or_default(); + for slash in slashes { + if slash.epoch >= start { + let cur_rate = slash_rates_by_epoch.entry(slash.epoch).or_default(); + *cur_rate = cmp::min(Dec::one(), *cur_rate + slash.rate); + + if !prev_applied_slashes.iter().any(|s| s == slash) { + validator_slashes.push(slash.clone()); + } + } + } + + let slashed_amount = if slash_rates_by_epoch.is_empty() { + None + } else { + let amount_after_slashing = + get_slashed_amount(params, deltas_sum, &slash_rates_by_epoch) + .unwrap(); + Some(deltas_sum - amount_after_slashing) + }; + + BondDetails { + start, + amount: deltas_sum, + slashed_amount, + } +} + +fn make_unbond_details( + params: &PosParams, + validator: &Address, + amount: token::Amount, + (start, withdraw): (Epoch, Epoch), + slashes: &[Slash], + applied_slashes: &mut HashMap>, +) -> UnbondDetails { + let prev_applied_slashes = applied_slashes + .clone() + .get(validator) + .cloned() + .unwrap_or_default(); + let mut slash_rates_by_epoch = BTreeMap::::new(); + + let validator_slashes = + applied_slashes.entry(validator.clone()).or_default(); + for slash in slashes { + if slash.epoch >= start + && slash.epoch + < withdraw + .checked_sub( + params.unbonding_len + + params.cubic_slashing_window_length, + ) + .unwrap_or_default() + { + let cur_rate = slash_rates_by_epoch.entry(slash.epoch).or_default(); + *cur_rate = cmp::min(Dec::one(), *cur_rate + slash.rate); + + if !prev_applied_slashes.iter().any(|s| s == slash) { + validator_slashes.push(slash.clone()); + } + } + } + + let slashed_amount = if slash_rates_by_epoch.is_empty() { + None + } else { + let amount_after_slashing = + get_slashed_amount(params, amount, &slash_rates_by_epoch).unwrap(); + Some(amount - amount_after_slashing) + }; + + UnbondDetails { + start, + withdraw, + amount, + slashed_amount, + } +} diff --git a/proof_of_stake/src/rewards.rs b/proof_of_stake/src/rewards.rs index 449c8a9867..3b19bd6b79 100644 --- a/proof_of_stake/src/rewards.rs +++ b/proof_of_stake/src/rewards.rs @@ -1,10 +1,31 @@ //! PoS rewards distribution. +use std::collections::{HashMap, HashSet}; + +use namada_core::ledger::storage_api::collections::lazy_map::NestedSubKey; +use namada_core::ledger::storage_api::token::credit_tokens; +use namada_core::ledger::storage_api::{ + self, ResultExt, StorageRead, StorageWrite, +}; +use namada_core::types::address::{self, Address}; use namada_core::types::dec::Dec; -use namada_core::types::token::Amount; +use namada_core::types::storage::Epoch; +use namada_core::types::token::{self, Amount}; use namada_core::types::uint::{Uint, I256}; use thiserror::Error; +use crate::storage::{ + consensus_validator_set_handle, get_last_reward_claim_epoch, + read_pos_params, read_validator_stake, rewards_accumulator_handle, + validator_commission_rate_handle, validator_rewards_products_handle, + validator_state_handle, +}; +use crate::types::{into_tm_voting_power, BondId, ValidatorState, VoteInfo}; +use crate::{ + bond_amounts_for_rewards, get_total_consensus_stake, storage_key, + InflationError, PosParams, +}; + /// This is equal to 0.01. const MIN_PROPOSER_REWARD: Dec = Dec(I256(Uint([10000000000u64, 0u64, 0u64, 0u64]))); @@ -99,3 +120,356 @@ impl PosRewardsCalculator { / 3u64 } } + +/// Tally a running sum of the fraction of rewards owed to each validator in +/// the consensus set. This is used to keep track of the rewards due to each +/// consensus validator over the lifetime of an epoch. +pub fn log_block_rewards( + storage: &mut S, + epoch: impl Into, + proposer_address: &Address, + votes: Vec, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + // The votes correspond to the last committed block (n-1 if we are + // finalizing block n) + + let epoch: Epoch = epoch.into(); + let params = read_pos_params(storage)?; + let consensus_validators = consensus_validator_set_handle().at(&epoch); + + // Get total stake of the consensus validator set + let total_consensus_stake = + get_total_consensus_stake(storage, epoch, ¶ms)?; + + // Get set of signing validator addresses and the combined stake of + // these signers + let mut signer_set: HashSet
= HashSet::new(); + let mut total_signing_stake = token::Amount::zero(); + for VoteInfo { + validator_address, + validator_vp, + } in votes + { + if validator_vp == 0 { + continue; + } + // Ensure that the validator is not currently jailed or other + let state = validator_state_handle(&validator_address) + .get(storage, epoch, ¶ms)?; + if state != Some(ValidatorState::Consensus) { + return Err(InflationError::ExpectedValidatorInConsensus( + validator_address, + state, + )) + .into_storage_result(); + } + + let stake_from_deltas = + read_validator_stake(storage, ¶ms, &validator_address, epoch)?; + + // Ensure TM stake updates properly with a debug_assert + if cfg!(debug_assertions) { + debug_assert_eq!( + into_tm_voting_power( + params.tm_votes_per_token, + stake_from_deltas, + ), + i64::try_from(validator_vp).unwrap_or_default(), + ); + } + + signer_set.insert(validator_address); + total_signing_stake += stake_from_deltas; + } + + // Get the block rewards coefficients (proposing, signing/voting, + // consensus set status) + let rewards_calculator = PosRewardsCalculator { + proposer_reward: params.block_proposer_reward, + signer_reward: params.block_vote_reward, + signing_stake: total_signing_stake, + total_stake: total_consensus_stake, + }; + let coeffs = rewards_calculator + .get_reward_coeffs() + .map_err(InflationError::Rewards) + .into_storage_result()?; + tracing::debug!( + "PoS rewards coefficients {coeffs:?}, inputs: {rewards_calculator:?}." + ); + + // tracing::debug!( + // "TOTAL SIGNING STAKE (LOGGING BLOCK REWARDS) = {}", + // signing_stake + // ); + + // Compute the fractional block rewards for each consensus validator and + // update the reward accumulators + let consensus_stake_unscaled: Dec = total_consensus_stake.into(); + let signing_stake_unscaled: Dec = total_signing_stake.into(); + let mut values: HashMap = HashMap::new(); + for validator in consensus_validators.iter(storage)? { + let ( + NestedSubKey::Data { + key: stake, + nested_sub_key: _, + }, + address, + ) = validator?; + + if stake.is_zero() { + continue; + } + + let mut rewards_frac = Dec::zero(); + let stake_unscaled: Dec = stake.into(); + // tracing::debug!( + // "NAMADA VALIDATOR STAKE (LOGGING BLOCK REWARDS) OF EPOCH {} = + // {}", epoch, stake + // ); + + // Proposer reward + if address == *proposer_address { + rewards_frac += coeffs.proposer_coeff; + } + // Signer reward + if signer_set.contains(&address) { + let signing_frac = stake_unscaled / signing_stake_unscaled; + rewards_frac += coeffs.signer_coeff * signing_frac; + } + // Consensus validator reward + rewards_frac += coeffs.active_val_coeff + * (stake_unscaled / consensus_stake_unscaled); + + // To be added to the rewards accumulator + values.insert(address, rewards_frac); + } + for (address, value) in values.into_iter() { + // Update the rewards accumulator + rewards_accumulator_handle().update(storage, address, |prev| { + prev.unwrap_or_default() + value + })?; + } + + Ok(()) +} + +#[derive(Clone, Debug)] +struct Rewards { + product: Dec, + commissions: token::Amount, +} + +/// Update validator and delegators rewards products and mint the inflation +/// tokens into the PoS account. +/// Any left-over inflation tokens from rounding error of the sum of the +/// rewards is given to the governance address. +pub fn update_rewards_products_and_mint_inflation( + storage: &mut S, + params: &PosParams, + last_epoch: Epoch, + num_blocks_in_last_epoch: u64, + inflation: token::Amount, + staking_token: &Address, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + // Read the rewards accumulator and calculate the new rewards products + // for the previous epoch + let mut reward_tokens_remaining = inflation; + let mut new_rewards_products: HashMap = HashMap::new(); + let mut accumulators_sum = Dec::zero(); + for acc in rewards_accumulator_handle().iter(storage)? { + let (validator, value) = acc?; + accumulators_sum += value; + + // Get reward token amount for this validator + let fractional_claim = value / num_blocks_in_last_epoch; + let reward_tokens = fractional_claim * inflation; + + // Get validator stake at the last epoch + let stake = Dec::from(read_validator_stake( + storage, params, &validator, last_epoch, + )?); + + let commission_rate = validator_commission_rate_handle(&validator) + .get(storage, last_epoch, params)? + .expect("Should be able to find validator commission rate"); + + // Calculate the reward product from the whole validator stake and take + // out the commissions. Because we're using the whole stake to work with + // a single product, we're also taking out commission on validator's + // self-bonds, but it is then included in the rewards claimable by the + // validator so they get it back. + let product = + (Dec::one() - commission_rate) * Dec::from(reward_tokens) / stake; + + // Tally the commission tokens earned by the validator. + // TODO: think abt Dec rounding and if `new_product` should be used + // instead of `reward_tokens` + let commissions = commission_rate * reward_tokens; + + new_rewards_products.insert( + validator, + Rewards { + product, + commissions, + }, + ); + + reward_tokens_remaining -= reward_tokens; + } + for ( + validator, + Rewards { + product, + commissions, + }, + ) in new_rewards_products + { + validator_rewards_products_handle(&validator) + .insert(storage, last_epoch, product)?; + // The commissions belong to the validator + add_rewards_to_counter(storage, &validator, &validator, commissions)?; + } + + // Mint tokens to the PoS account for the last epoch's inflation + let pos_reward_tokens = inflation - reward_tokens_remaining; + tracing::info!( + "Minting tokens for PoS rewards distribution into the PoS account. \ + Amount: {}. Total inflation: {}, number of blocks in the last epoch: \ + {num_blocks_in_last_epoch}, reward accumulators sum: \ + {accumulators_sum}.", + pos_reward_tokens.to_string_native(), + inflation.to_string_native(), + ); + credit_tokens(storage, staking_token, &address::POS, pos_reward_tokens)?; + + if reward_tokens_remaining > token::Amount::zero() { + tracing::info!( + "Minting tokens remaining from PoS rewards distribution into the \ + Governance account. Amount: {}.", + reward_tokens_remaining.to_string_native() + ); + credit_tokens( + storage, + staking_token, + &address::GOV, + reward_tokens_remaining, + )?; + } + + // Clear validator rewards accumulators + storage.delete_prefix( + // The prefix of `rewards_accumulator_handle` + &storage_key::consensus_validator_rewards_accumulator_key(), + )?; + + Ok(()) +} + +/// Compute the current available rewards amount due only to existing bonds. +/// This does not include pending rewards held in the rewards counter due to +/// unbonds and redelegations. +pub fn compute_current_rewards_from_bonds( + storage: &S, + source: &Address, + validator: &Address, + current_epoch: Epoch, +) -> storage_api::Result +where + S: StorageRead, +{ + if current_epoch == Epoch::default() { + // Nothing to claim in the first epoch + return Ok(token::Amount::zero()); + } + + let last_claim_epoch = + get_last_reward_claim_epoch(storage, source, validator)?; + if let Some(last_epoch) = last_claim_epoch { + if last_epoch == current_epoch { + // Already claimed in this epoch + return Ok(token::Amount::zero()); + } + } + + let mut reward_tokens = token::Amount::zero(); + + // Want to claim from `last_claim_epoch` to `current_epoch.prev()` since + // rewards are computed at the end of an epoch + let (claim_start, claim_end) = ( + last_claim_epoch.unwrap_or_default(), + // Safe because of the check above + current_epoch.prev(), + ); + let bond_amounts = bond_amounts_for_rewards( + storage, + &BondId { + source: source.clone(), + validator: validator.clone(), + }, + claim_start, + claim_end, + )?; + + let rewards_products = validator_rewards_products_handle(validator); + for (ep, bond_amount) in bond_amounts { + debug_assert!(ep >= claim_start); + debug_assert!(ep <= claim_end); + let rp = rewards_products.get(storage, &ep)?.unwrap_or_default(); + let reward = rp * bond_amount; + reward_tokens += reward; + } + + Ok(reward_tokens) +} + +/// Add tokens to a rewards counter. +pub fn add_rewards_to_counter( + storage: &mut S, + source: &Address, + validator: &Address, + new_rewards: token::Amount, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let key = storage_key::rewards_counter_key(source, validator); + let current_rewards = + storage.read::(&key)?.unwrap_or_default(); + storage.write(&key, current_rewards + new_rewards) +} + +/// Take tokens from a rewards counter. Deletes the record after reading. +pub fn take_rewards_from_counter( + storage: &mut S, + source: &Address, + validator: &Address, +) -> storage_api::Result +where + S: StorageRead + StorageWrite, +{ + let key = storage_key::rewards_counter_key(source, validator); + let current_rewards = + storage.read::(&key)?.unwrap_or_default(); + storage.delete(&key)?; + Ok(current_rewards) +} + +/// Read the current token value in the rewards counter. +pub fn read_rewards_counter( + storage: &S, + source: &Address, + validator: &Address, +) -> storage_api::Result +where + S: StorageRead, +{ + let key = storage_key::rewards_counter_key(source, validator); + Ok(storage.read::(&key)?.unwrap_or_default()) +} diff --git a/proof_of_stake/src/slashing.rs b/proof_of_stake/src/slashing.rs new file mode 100644 index 0000000000..4b91a84527 --- /dev/null +++ b/proof_of_stake/src/slashing.rs @@ -0,0 +1,1130 @@ +//! Slashing tingzzzz + +use std::cmp::{self, Reverse}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; + +use borsh::BorshDeserialize; +use namada_core::ledger::storage_api::collections::lazy_map::{ + Collectable, NestedMap, NestedSubKey, SubKey, +}; +use namada_core::ledger::storage_api::collections::LazyMap; +use namada_core::ledger::storage_api::{self, StorageRead, StorageWrite}; +use namada_core::types::address::Address; +use namada_core::types::dec::Dec; +use namada_core::types::storage::Epoch; +use namada_core::types::token; + +use crate::storage::{ + enqueued_slashes_handle, read_pos_params, read_validator_last_slash_epoch, + read_validator_stake, total_bonded_handle, total_unbonded_handle, + update_total_deltas, update_validator_deltas, + validator_outgoing_redelegations_handle, validator_slashes_handle, + validator_state_handle, validator_total_redelegated_bonded_handle, + validator_total_redelegated_unbonded_handle, + write_validator_last_slash_epoch, +}; +use crate::types::{ + EagerRedelegatedBondsMap, ResultSlashing, Slash, SlashType, SlashedAmount, + Slashes, TotalRedelegatedUnbonded, ValidatorState, +}; +use crate::validator_set_update::update_validator_set; +use crate::{ + fold_and_slash_redelegated_bonds, get_total_consensus_stake, + jail_validator, storage_key, EagerRedelegatedUnbonds, + FoldRedelegatedBondsResult, OwnedPosParams, PosParams, +}; + +/// Record a slash for a misbehavior that has been received from Tendermint and +/// then jail the validator, removing it from the validator set. The slash rate +/// will be computed at a later epoch. +#[allow(clippy::too_many_arguments)] +pub fn slash( + storage: &mut S, + params: &PosParams, + current_epoch: Epoch, + evidence_epoch: Epoch, + evidence_block_height: impl Into, + slash_type: SlashType, + validator: &Address, + validator_set_update_epoch: Epoch, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let evidence_block_height: u64 = evidence_block_height.into(); + let slash = Slash { + epoch: evidence_epoch, + block_height: evidence_block_height, + r#type: slash_type, + rate: Dec::zero(), // Let the rate be 0 initially before processing + }; + // Need `+1` because we process at the beginning of a new epoch + let processing_epoch = + evidence_epoch + params.slash_processing_epoch_offset(); + + // Add the slash to the list of enqueued slashes to be processed at a later + // epoch + enqueued_slashes_handle() + .get_data_handler() + .at(&processing_epoch) + .at(validator) + .push(storage, slash)?; + + // Update the most recent slash (infraction) epoch for the validator + let last_slash_epoch = read_validator_last_slash_epoch(storage, validator)?; + if last_slash_epoch.is_none() + || evidence_epoch.0 > last_slash_epoch.unwrap_or_default().0 + { + write_validator_last_slash_epoch(storage, validator, evidence_epoch)?; + } + + // Jail the validator and update validator sets + jail_validator( + storage, + params, + validator, + current_epoch, + validator_set_update_epoch, + )?; + + // No other actions are performed here until the epoch in which the slash is + // processed. + + Ok(()) +} + +/// Process enqueued slashes that were discovered earlier. This function is +/// called upon a new epoch. The final slash rate considering according to the +/// cubic slashing rate is computed. Then, each slash is recorded in storage +/// along with its computed rate, and stake is deducted from the affected +/// validators. +pub fn process_slashes( + storage: &mut S, + current_epoch: Epoch, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let params = read_pos_params(storage)?; + + if current_epoch.0 < params.slash_processing_epoch_offset() { + return Ok(()); + } + let infraction_epoch = + current_epoch - params.slash_processing_epoch_offset(); + + // Slashes to be processed in the current epoch + let enqueued_slashes = enqueued_slashes_handle().at(¤t_epoch); + if enqueued_slashes.is_empty(storage)? { + return Ok(()); + } + tracing::debug!( + "Processing slashes at the beginning of epoch {} (committed in epoch \ + {})", + current_epoch, + infraction_epoch + ); + + // Compute the cubic slash rate + let cubic_slash_rate = + compute_cubic_slash_rate(storage, ¶ms, infraction_epoch)?; + + // Collect the enqueued slashes and update their rates + let mut eager_validator_slashes: BTreeMap> = + BTreeMap::new(); + let mut eager_validator_slash_rates: HashMap = HashMap::new(); + + // `slashPerValidator` and `slashesMap` while also updating in storage + for enqueued_slash in enqueued_slashes.iter(storage)? { + let ( + NestedSubKey::Data { + key: validator, + nested_sub_key: _, + }, + enqueued_slash, + ) = enqueued_slash?; + debug_assert_eq!(enqueued_slash.epoch, infraction_epoch); + + let slash_rate = cmp::min( + Dec::one(), + cmp::max( + enqueued_slash.r#type.get_slash_rate(¶ms), + cubic_slash_rate, + ), + ); + let updated_slash = Slash { + epoch: enqueued_slash.epoch, + block_height: enqueued_slash.block_height, + r#type: enqueued_slash.r#type, + rate: slash_rate, + }; + + let cur_slashes = eager_validator_slashes + .entry(validator.clone()) + .or_default(); + cur_slashes.push(updated_slash); + let cur_rate = + eager_validator_slash_rates.entry(validator).or_default(); + *cur_rate = cmp::min(Dec::one(), *cur_rate + slash_rate); + } + + // Update the epochs of enqueued slashes in storage + enqueued_slashes_handle().update_data(storage, ¶ms, current_epoch)?; + + // `resultSlashing` + let mut map_validator_slash: EagerRedelegatedBondsMap = BTreeMap::new(); + for (validator, slash_rate) in eager_validator_slash_rates { + process_validator_slash( + storage, + ¶ms, + &validator, + slash_rate, + current_epoch, + &mut map_validator_slash, + )?; + } + tracing::debug!("Slashed amounts for validators: {map_validator_slash:#?}"); + + // Now update the remaining parts of storage + + // Write slashes themselves into storage + for (validator, slashes) in eager_validator_slashes { + let validator_slashes = validator_slashes_handle(&validator); + for slash in slashes { + validator_slashes.push(storage, slash)?; + } + } + + // Update the validator stakes + for (validator, slash_amounts) in map_validator_slash { + let mut slash_acc = token::Amount::zero(); + + // Update validator sets first because it needs to be able to read + // validator stake before we make any changes to it + for (&epoch, &slash_amount) in &slash_amounts { + let state = validator_state_handle(&validator) + .get(storage, epoch, ¶ms)? + .unwrap(); + if state != ValidatorState::Jailed { + update_validator_set( + storage, + ¶ms, + &validator, + -slash_amount.change(), + epoch, + Some(0), + )?; + } + } + // Then update validator and total deltas + for (epoch, slash_amount) in slash_amounts { + let slash_delta = slash_amount - slash_acc; + slash_acc += slash_delta; + + update_validator_deltas( + storage, + ¶ms, + &validator, + -slash_delta.change(), + epoch, + Some(0), + )?; + update_total_deltas( + storage, + ¶ms, + -slash_delta.change(), + epoch, + Some(0), + )?; + } + + // TODO: should we clear some storage here as is done in Quint?? + // Possibly make the `unbonded` LazyMaps epoched so that it is done + // automatically? + } + + Ok(()) +} + +/// In the context of a redelegation, the function computes how much a validator +/// (the destination validator of the redelegation) should be slashed due to the +/// misbehaving of a second validator (the source validator of the +/// redelegation). The function computes how much the validator whould be +/// slashed at all epochs between the current epoch (curEpoch) + 1 and the +/// current epoch + 1 + PIPELINE_OFFSET, accounting for any tokens of the +/// redelegation already unbonded. +/// +/// - `src_validator` - the source validator +/// - `outgoing_redelegations` - a map from pair of epochs to int that includes +/// all the redelegations from the source validator to the destination +/// validator. +/// - The outer key is epoch at which the bond started at the source +/// validator. +/// - The inner key is epoch at which the redelegation started (the epoch at +/// which was issued). +/// - `slashes` a list of slashes of the source validator. +/// - `dest_total_redelegated_unbonded` - a map of unbonded redelegated tokens +/// at the destination validator. +/// - `slash_rate` - the rate of the slash being processed. +/// - `dest_slashed_amounts` - a map from epoch to already processed slash +/// amounts. +/// +/// Adds any newly processed slash amount to `dest_slashed_amounts`. +#[allow(clippy::too_many_arguments)] +pub fn slash_validator_redelegation( + storage: &S, + params: &OwnedPosParams, + src_validator: &Address, + current_epoch: Epoch, + outgoing_redelegations: &NestedMap>, + slashes: &Slashes, + dest_total_redelegated_unbonded: &TotalRedelegatedUnbonded, + slash_rate: Dec, + dest_slashed_amounts: &mut BTreeMap, +) -> storage_api::Result<()> +where + S: StorageRead, +{ + let infraction_epoch = + current_epoch - params.slash_processing_epoch_offset(); + + for res in outgoing_redelegations.iter(storage)? { + let ( + NestedSubKey::Data { + key: bond_start, + nested_sub_key: SubKey::Data(redel_start), + }, + amount, + ) = res?; + + if params.in_redelegation_slashing_window( + infraction_epoch, + redel_start, + params.redelegation_end_epoch_from_start(redel_start), + ) && bond_start <= infraction_epoch + { + slash_redelegation( + storage, + params, + amount, + bond_start, + params.redelegation_end_epoch_from_start(redel_start), + src_validator, + current_epoch, + slashes, + dest_total_redelegated_unbonded, + slash_rate, + dest_slashed_amounts, + )?; + } + } + + Ok(()) +} + +/// Computes how many tokens will be slashed from a redelegated bond, +/// considering that the bond may have been completely or partially unbonded and +/// that the source validator may have misbehaved within the redelegation +/// slashing window. +#[allow(clippy::too_many_arguments)] +pub fn slash_redelegation( + storage: &S, + params: &OwnedPosParams, + amount: token::Amount, + bond_start: Epoch, + redel_bond_start: Epoch, + src_validator: &Address, + current_epoch: Epoch, + slashes: &Slashes, + total_redelegated_unbonded: &TotalRedelegatedUnbonded, + slash_rate: Dec, + slashed_amounts: &mut BTreeMap, +) -> storage_api::Result<()> +where + S: StorageRead, +{ + tracing::debug!( + "\nSlashing redelegation amount {} - bond start {} and \ + redel_bond_start {} - at rate {}\n", + amount.to_string_native(), + bond_start, + redel_bond_start, + slash_rate + ); + + let infraction_epoch = + current_epoch - params.slash_processing_epoch_offset(); + + // Slash redelegation destination validator from the next epoch only + // as they won't be jailed + let set_update_epoch = current_epoch.next(); + + let mut init_tot_unbonded = + Epoch::iter_bounds_inclusive(infraction_epoch.next(), set_update_epoch) + .map(|epoch| { + let redelegated_unbonded = total_redelegated_unbonded + .at(&epoch) + .at(&redel_bond_start) + .at(src_validator) + .get(storage, &bond_start)? + .unwrap_or_default(); + Ok(redelegated_unbonded) + }) + .sum::>()?; + + for epoch in Epoch::iter_range(set_update_epoch, params.pipeline_len) { + let updated_total_unbonded = { + let redelegated_unbonded = total_redelegated_unbonded + .at(&epoch) + .at(&redel_bond_start) + .at(src_validator) + .get(storage, &bond_start)? + .unwrap_or_default(); + init_tot_unbonded + redelegated_unbonded + }; + + let list_slashes = slashes + .iter(storage)? + .map(Result::unwrap) + .filter(|slash| { + params.in_redelegation_slashing_window( + slash.epoch, + params.redelegation_start_epoch_from_end(redel_bond_start), + redel_bond_start, + ) && bond_start <= slash.epoch + && slash.epoch + params.slash_processing_epoch_offset() + // We're looking for slashes that were processed before or in the epoch + // in which slashes that are currently being processed + // occurred. Because we're slashing in the beginning of an + // epoch, we're also taking slashes that were processed in + // the infraction epoch as they would still be processed + // before any infraction occurred. + <= infraction_epoch + }) + .collect::>(); + + let slashable_amount = amount + .checked_sub(updated_total_unbonded) + .unwrap_or_default(); + + let slashed = + apply_list_slashes(params, &list_slashes, slashable_amount) + .mul_ceil(slash_rate); + + let list_slashes = slashes + .iter(storage)? + .map(Result::unwrap) + .filter(|slash| { + params.in_redelegation_slashing_window( + slash.epoch, + params.redelegation_start_epoch_from_end(redel_bond_start), + redel_bond_start, + ) && bond_start <= slash.epoch + }) + .collect::>(); + + let slashable_stake = + apply_list_slashes(params, &list_slashes, slashable_amount) + .mul_ceil(slash_rate); + + init_tot_unbonded = updated_total_unbonded; + let to_slash = cmp::min(slashed, slashable_stake); + if !to_slash.is_zero() { + let map_value = slashed_amounts.entry(epoch).or_default(); + *map_value += to_slash; + } + } + + Ok(()) +} + +/// Computes for a given validator and a slash how much should be slashed at all +/// epochs between the currentÃ¥ epoch (curEpoch) + 1 and the current epoch + 1 + +/// PIPELINE_OFFSET, accounting for any tokens already unbonded. +/// +/// - `validator` - the misbehaving validator. +/// - `slash_rate` - the rate of the slash being processed. +/// - `slashed_amounts_map` - a map from epoch to already processed slash +/// amounts. +/// +/// Returns a map that adds any newly processed slash amount to +/// `slashed_amounts_map`. +// `def slashValidator` +pub fn slash_validator( + storage: &S, + params: &OwnedPosParams, + validator: &Address, + slash_rate: Dec, + current_epoch: Epoch, + slashed_amounts_map: &BTreeMap, +) -> storage_api::Result> +where + S: StorageRead, +{ + tracing::debug!("Slashing validator {} at rate {}", validator, slash_rate); + let infraction_epoch = + current_epoch - params.slash_processing_epoch_offset(); + + let total_unbonded = total_unbonded_handle(validator); + let total_redelegated_unbonded = + validator_total_redelegated_unbonded_handle(validator); + let total_bonded = total_bonded_handle(validator); + let total_redelegated_bonded = + validator_total_redelegated_bonded_handle(validator); + + let mut slashed_amounts = slashed_amounts_map.clone(); + + let mut tot_bonds = total_bonded + .get_data_handler() + .iter(storage)? + .map(Result::unwrap) + .filter(|&(epoch, bonded)| { + epoch <= infraction_epoch && bonded > 0.into() + }) + .collect::>(); + + let mut redelegated_bonds = tot_bonds + .keys() + .filter(|&epoch| { + !total_redelegated_bonded + .at(epoch) + .is_empty(storage) + .unwrap() + }) + .map(|epoch| { + let tot_redel_bonded = total_redelegated_bonded + .at(epoch) + .collect_map(storage) + .unwrap(); + (*epoch, tot_redel_bonded) + }) + .collect::>(); + + let mut sum = token::Amount::zero(); + + let eps = current_epoch + .iter_range(params.pipeline_len) + .collect::>(); + for epoch in eps.into_iter().rev() { + let amount = tot_bonds.iter().fold( + token::Amount::zero(), + |acc, (bond_start, bond_amount)| { + acc + compute_slash_bond_at_epoch( + storage, + params, + validator, + epoch, + infraction_epoch, + *bond_start, + *bond_amount, + redelegated_bonds.get(bond_start), + slash_rate, + ) + .unwrap() + }, + ); + + let new_bonds = total_unbonded.at(&epoch); + tot_bonds = new_bonds + .collect_map(storage) + .unwrap() + .into_iter() + .filter(|(ep, _)| *ep <= infraction_epoch) + .collect::>(); + + let new_redelegated_bonds = tot_bonds + .keys() + .filter(|&ep| { + !total_redelegated_unbonded.at(ep).is_empty(storage).unwrap() + }) + .map(|ep| { + ( + *ep, + total_redelegated_unbonded + .at(&epoch) + .at(ep) + .collect_map(storage) + .unwrap(), + ) + }) + .collect::>(); + + redelegated_bonds = new_redelegated_bonds; + + // `newSum` + sum += amount; + + // `newSlashesMap` + let cur = slashed_amounts.entry(epoch).or_default(); + *cur += sum; + } + // Hack - should this be done differently? (think this is safe) + let pipeline_epoch = current_epoch + params.pipeline_len; + let last_amt = slashed_amounts + .get(&pipeline_epoch.prev()) + .cloned() + .unwrap(); + slashed_amounts.insert(pipeline_epoch, last_amt); + + Ok(slashed_amounts) +} + +/// Get the remaining token amount in a bond after applying a set of slashes. +/// +/// - `validator` - the bond's validator +/// - `epoch` - the latest slash epoch to consider. +/// - `start` - the start epoch of the bond +/// - `redelegated_bonds` +pub fn compute_bond_at_epoch( + storage: &S, + params: &OwnedPosParams, + validator: &Address, + epoch: Epoch, + start: Epoch, + amount: token::Amount, + redelegated_bonds: Option<&EagerRedelegatedBondsMap>, +) -> storage_api::Result +where + S: StorageRead, +{ + let list_slashes = validator_slashes_handle(validator) + .iter(storage)? + .map(Result::unwrap) + .filter(|slash| { + start <= slash.epoch + && slash.epoch + params.slash_processing_epoch_offset() <= epoch + }) + .collect::>(); + + let slash_epoch_filter = + |e: Epoch| e + params.slash_processing_epoch_offset() <= epoch; + + let result_fold = redelegated_bonds + .map(|redelegated_bonds| { + fold_and_slash_redelegated_bonds( + storage, + params, + redelegated_bonds, + start, + &list_slashes, + slash_epoch_filter, + ) + }) + .unwrap_or_default(); + + let total_not_redelegated = amount - result_fold.total_redelegated; + let after_not_redelegated = + apply_list_slashes(params, &list_slashes, total_not_redelegated); + + Ok(after_not_redelegated + result_fold.total_after_slashing) +} + +/// Uses `fn compute_bond_at_epoch` to compute the token amount to slash in +/// order to prevent overslashing. +#[allow(clippy::too_many_arguments)] +pub fn compute_slash_bond_at_epoch( + storage: &S, + params: &OwnedPosParams, + validator: &Address, + epoch: Epoch, + infraction_epoch: Epoch, + bond_start: Epoch, + bond_amount: token::Amount, + redelegated_bonds: Option<&EagerRedelegatedBondsMap>, + slash_rate: Dec, +) -> storage_api::Result +where + S: StorageRead, +{ + let amount_due = compute_bond_at_epoch( + storage, + params, + validator, + infraction_epoch, + bond_start, + bond_amount, + redelegated_bonds, + )? + .mul_ceil(slash_rate); + let slashable_amount = compute_bond_at_epoch( + storage, + params, + validator, + epoch, + bond_start, + bond_amount, + redelegated_bonds, + )?; + Ok(cmp::min(amount_due, slashable_amount)) +} + +/// Find slashes applicable to a validator with inclusive `start` and exclusive +/// `end` epoch. +#[allow(dead_code)] +pub fn find_slashes_in_range( + storage: &S, + start: Epoch, + end: Option, + validator: &Address, +) -> storage_api::Result> +where + S: StorageRead, +{ + let mut slashes = BTreeMap::::new(); + for slash in validator_slashes_handle(validator).iter(storage)? { + let slash = slash?; + if start <= slash.epoch + && end.map(|end| slash.epoch < end).unwrap_or(true) + { + let cur_rate = slashes.entry(slash.epoch).or_default(); + *cur_rate = cmp::min(*cur_rate + slash.rate, Dec::one()); + } + } + Ok(slashes) +} + +/// Computes how much remains from an amount of tokens after applying a list of +/// slashes. +/// +/// - `slashes` - a list of slashes ordered by misbehaving epoch. +/// - `amount` - the amount of slashable tokens. +// `def applyListSlashes` +pub fn apply_list_slashes( + params: &OwnedPosParams, + slashes: &[Slash], + amount: token::Amount, +) -> token::Amount { + let mut final_amount = amount; + let mut computed_slashes = BTreeMap::::new(); + for slash in slashes { + let slashed_amount = + compute_slashable_amount(params, slash, amount, &computed_slashes); + final_amount = + final_amount.checked_sub(slashed_amount).unwrap_or_default(); + computed_slashes.insert(slash.epoch, slashed_amount); + } + final_amount +} + +/// Computes how much is left from a bond or unbond after applying a slash given +/// that a set of slashes may have been previously applied. +// `def computeSlashableAmount` +pub fn compute_slashable_amount( + params: &OwnedPosParams, + slash: &Slash, + amount: token::Amount, + computed_slashes: &BTreeMap, +) -> token::Amount { + let updated_amount = computed_slashes + .iter() + .filter(|(&epoch, _)| { + // Keep slashes that have been applied and processed before the + // current slash occurred. We use `<=` because slashes processed at + // `slash.epoch` (at the start of the epoch) are also processed + // before this slash occurred. + epoch + params.slash_processing_epoch_offset() <= slash.epoch + }) + .fold(amount, |acc, (_, &amnt)| { + acc.checked_sub(amnt).unwrap_or_default() + }); + updated_amount.mul_ceil(slash.rate) +} + +/// Find all slashes and the associated validators in the PoS system +pub fn find_all_slashes( + storage: &S, +) -> storage_api::Result>> +where + S: StorageRead, +{ + let mut slashes: HashMap> = HashMap::new(); + let slashes_iter = storage_api::iter_prefix_bytes( + storage, + &storage_key::slashes_prefix(), + )? + .filter_map(|result| { + if let Ok((key, val_bytes)) = result { + if let Some(validator) = storage_key::is_validator_slashes_key(&key) + { + let slash: Slash = + BorshDeserialize::try_from_slice(&val_bytes).ok()?; + return Some((validator, slash)); + } + } + None + }); + + slashes_iter.for_each(|(address, slash)| match slashes.get(&address) { + Some(vec) => { + let mut vec = vec.clone(); + vec.push(slash); + slashes.insert(address, vec); + } + None => { + slashes.insert(address, vec![slash]); + } + }); + Ok(slashes) +} + +/// Collect the details of all of the enqueued slashes to be processed in future +/// epochs into a nested map +pub fn find_all_enqueued_slashes( + storage: &S, + epoch: Epoch, +) -> storage_api::Result>>> +where + S: StorageRead, +{ + let mut enqueued = HashMap::>>::new(); + for res in enqueued_slashes_handle().get_data_handler().iter(storage)? { + let ( + NestedSubKey::Data { + key: processing_epoch, + nested_sub_key: + NestedSubKey::Data { + key: address, + nested_sub_key: _, + }, + }, + slash, + ) = res?; + if processing_epoch <= epoch { + continue; + } + + let slashes = enqueued + .entry(address) + .or_default() + .entry(processing_epoch) + .or_default(); + slashes.push(slash); + } + Ok(enqueued) +} + +/// Find PoS slashes applied to a validator, if any +pub fn find_validator_slashes( + storage: &S, + validator: &Address, +) -> storage_api::Result> +where + S: StorageRead, +{ + validator_slashes_handle(validator).iter(storage)?.collect() +} + +/// Compute a token amount after slashing, given the initial amount and a set of +/// slashes. It is assumed that the input `slashes` are those commited while the +/// `amount` was contributing to voting power. +pub fn get_slashed_amount( + params: &PosParams, + amount: token::Amount, + slashes: &BTreeMap, +) -> storage_api::Result { + let mut updated_amount = amount; + let mut computed_amounts = Vec::::new(); + + for (&infraction_epoch, &slash_rate) in slashes { + let mut computed_to_remove = BTreeSet::>::new(); + for (ix, slashed_amount) in computed_amounts.iter().enumerate() { + // Update amount with slashes that happened more than unbonding_len + // epochs before this current slash + if slashed_amount.epoch + params.slash_processing_epoch_offset() + <= infraction_epoch + { + updated_amount = updated_amount + .checked_sub(slashed_amount.amount) + .unwrap_or_default(); + computed_to_remove.insert(Reverse(ix)); + } + } + // Invariant: `computed_to_remove` must be in reverse ord to avoid + // left-shift of the `computed_amounts` after call to `remove` + // invalidating the rest of the indices. + for item in computed_to_remove { + computed_amounts.remove(item.0); + } + computed_amounts.push(SlashedAmount { + amount: updated_amount.mul_ceil(slash_rate), + epoch: infraction_epoch, + }); + } + + let total_computed_amounts = computed_amounts + .into_iter() + .map(|slashed| slashed.amount) + .sum(); + + let final_amount = updated_amount + .checked_sub(total_computed_amounts) + .unwrap_or_default(); + + Ok(final_amount) +} + +/// Compute the total amount of tokens from a set of unbonds, both redelegated +/// and not, after applying slashes. Used in `unbond_tokens`. +// `def computeAmountAfterSlashingUnbond` +pub fn compute_amount_after_slashing_unbond( + storage: &S, + params: &OwnedPosParams, + unbonds: &BTreeMap, + redelegated_unbonds: &EagerRedelegatedUnbonds, + slashes: Vec, +) -> storage_api::Result +where + S: StorageRead, +{ + let mut result_slashing = ResultSlashing::default(); + for (&start_epoch, amount) in unbonds { + // `val listSlashes` + let list_slashes: Vec = slashes + .iter() + .filter(|slash| slash.epoch >= start_epoch) + .cloned() + .collect(); + // `val resultFold` + let result_fold = if let Some(redelegated_unbonds) = + redelegated_unbonds.get(&start_epoch) + { + fold_and_slash_redelegated_bonds( + storage, + params, + redelegated_unbonds, + start_epoch, + &list_slashes, + |_| true, + ) + } else { + FoldRedelegatedBondsResult::default() + }; + // `val totalNoRedelegated` + let total_not_redelegated = amount + .checked_sub(result_fold.total_redelegated) + .unwrap_or_default(); + // `val afterNoRedelegated` + let after_not_redelegated = + apply_list_slashes(params, &list_slashes, total_not_redelegated); + // `val amountAfterSlashing` + let amount_after_slashing = + after_not_redelegated + result_fold.total_after_slashing; + // Accumulation step + result_slashing.sum += amount_after_slashing; + result_slashing + .epoch_map + .insert(start_epoch, amount_after_slashing); + } + Ok(result_slashing) +} + +/// Compute the total amount of tokens from a set of unbonds, both redelegated +/// and not, after applying slashes. Used in `withdraw_tokens`. +// `def computeAmountAfterSlashingWithdraw` +pub fn compute_amount_after_slashing_withdraw( + storage: &S, + params: &OwnedPosParams, + unbonds_and_redelegated_unbonds: &BTreeMap< + (Epoch, Epoch), + (token::Amount, EagerRedelegatedBondsMap), + >, + slashes: Vec, +) -> storage_api::Result +where + S: StorageRead, +{ + let mut result_slashing = ResultSlashing::default(); + + for ((start_epoch, withdraw_epoch), (amount, redelegated_unbonds)) in + unbonds_and_redelegated_unbonds.iter() + { + // TODO: check if slashes in the same epoch can be + // folded into one effective slash + let end_epoch = *withdraw_epoch + - params.unbonding_len + - params.cubic_slashing_window_length; + // Find slashes that apply to `start_epoch..end_epoch` + let list_slashes = slashes + .iter() + .filter(|slash| { + // Started before the slash occurred + start_epoch <= &slash.epoch + // Ends after the slash + && end_epoch > slash.epoch + }) + .cloned() + .collect::>(); + + // Find the sum and the sum after slashing of the redelegated unbonds + let result_fold = fold_and_slash_redelegated_bonds( + storage, + params, + redelegated_unbonds, + *start_epoch, + &list_slashes, + |_| true, + ); + + // Unbond amount that didn't come from a redelegation + let total_not_redelegated = *amount - result_fold.total_redelegated; + // Find how much remains after slashing non-redelegated amount + let after_not_redelegated = + apply_list_slashes(params, &list_slashes, total_not_redelegated); + + // Add back the unbond and redelegated unbond amount after slashing + let amount_after_slashing = + after_not_redelegated + result_fold.total_after_slashing; + + result_slashing.sum += amount_after_slashing; + result_slashing + .epoch_map + .insert(*start_epoch, amount_after_slashing); + } + + Ok(result_slashing) +} + +/// Process a slash by (i) slashing the misbehaving validator; and (ii) any +/// validator to which it has redelegated some tokens and the slash misbehaving +/// epoch is wihtin the redelegation slashing window. +/// +/// `validator` - the misbehaving validator. +/// `slash_rate` - the slash rate. +/// `slashed_amounts_map` - a map from validator address to a map from epoch to +/// already processed slash amounts. +/// +/// Adds any newly processed slash amount of any involved validator to +/// `slashed_amounts_map`. +// Quint `processSlash` +fn process_validator_slash( + storage: &mut S, + params: &PosParams, + validator: &Address, + slash_rate: Dec, + current_epoch: Epoch, + slashed_amount_map: &mut EagerRedelegatedBondsMap, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + // `resultSlashValidator + let result_slash = slash_validator( + storage, + params, + validator, + slash_rate, + current_epoch, + &slashed_amount_map + .get(validator) + .cloned() + .unwrap_or_default(), + )?; + + // `updatedSlashedAmountMap` + let validator_slashes = + slashed_amount_map.entry(validator.clone()).or_default(); + *validator_slashes = result_slash; + + // `outgoingRedelegation` + let outgoing_redelegations = + validator_outgoing_redelegations_handle(validator); + + // Final loop in `processSlash` + let dest_validators = outgoing_redelegations + .iter(storage)? + .map(|res| { + let ( + NestedSubKey::Data { + key: dest_validator, + nested_sub_key: _, + }, + _redelegation, + ) = res?; + Ok(dest_validator) + }) + .collect::>>()?; + + for dest_validator in dest_validators { + let to_modify = slashed_amount_map + .entry(dest_validator.clone()) + .or_default(); + + tracing::debug!( + "Slashing {} redelegation to {}", + validator, + &dest_validator + ); + + // `slashValidatorRedelegation` + slash_validator_redelegation( + storage, + params, + validator, + current_epoch, + &outgoing_redelegations.at(&dest_validator), + &validator_slashes_handle(validator), + &validator_total_redelegated_unbonded_handle(&dest_validator), + slash_rate, + to_modify, + )?; + } + + Ok(()) +} + +/// Calculate the cubic slashing rate using all slashes within a window around +/// the given infraction epoch. There is no cap on the rate applied within this +/// function. +fn compute_cubic_slash_rate( + storage: &S, + params: &PosParams, + infraction_epoch: Epoch, +) -> storage_api::Result +where + S: StorageRead, +{ + tracing::debug!( + "Computing the cubic slash rate for infraction epoch \ + {infraction_epoch}." + ); + let mut sum_vp_fraction = Dec::zero(); + let (start_epoch, end_epoch) = + params.cubic_slash_epoch_window(infraction_epoch); + + for epoch in Epoch::iter_bounds_inclusive(start_epoch, end_epoch) { + let consensus_stake = + Dec::from(get_total_consensus_stake(storage, epoch, params)?); + tracing::debug!( + "Total consensus stake in epoch {}: {}", + epoch, + consensus_stake + ); + let processing_epoch = epoch + params.slash_processing_epoch_offset(); + let slashes = enqueued_slashes_handle().at(&processing_epoch); + let infracting_stake = slashes.iter(storage)?.fold( + Ok(Dec::zero()), + |acc: storage_api::Result, res| { + let acc = acc?; + let ( + NestedSubKey::Data { + key: validator, + nested_sub_key: _, + }, + _slash, + ) = res?; + + let validator_stake = + read_validator_stake(storage, params, &validator, epoch)?; + // tracing::debug!("Val {} stake: {}", &validator, + // validator_stake); + + Ok(acc + Dec::from(validator_stake)) + }, + )?; + sum_vp_fraction += infracting_stake / consensus_stake; + } + let cubic_rate = + Dec::new(9, 0).unwrap() * sum_vp_fraction * sum_vp_fraction; + tracing::debug!("Cubic slash rate: {}", cubic_rate); + Ok(cubic_rate) +} diff --git a/proof_of_stake/src/storage.rs b/proof_of_stake/src/storage.rs index 2991526760..bab677b3a0 100644 --- a/proof_of_stake/src/storage.rs +++ b/proof_of_stake/src/storage.rs @@ -1,1037 +1,848 @@ -//! Proof-of-Stake storage keys and storage integration. +//! PoS functions for reading and writing to storage and lazy collection handles +//! associated with given `storage_key`s. -use namada_core::ledger::storage_api::collections::{lazy_map, lazy_vec}; +use std::collections::{BTreeSet, HashSet}; + +use namada_core::ledger::storage_api::collections::lazy_map::NestedSubKey; +use namada_core::ledger::storage_api::collections::{LazyCollection, LazySet}; +use namada_core::ledger::storage_api::governance::get_max_proposal_period; +use namada_core::ledger::storage_api::{ + self, Result, StorageRead, StorageWrite, +}; use namada_core::types::address::Address; -use namada_core::types::storage::{DbKeySeg, Epoch, Key, KeySeg}; - -use super::ADDRESS; -use crate::epoched; -use crate::types::BondId; - -const PARAMS_STORAGE_KEY: &str = "params"; -const VALIDATOR_ADDRESSES_KEY: &str = "validator_addresses"; -#[allow(missing_docs)] -pub const VALIDATOR_STORAGE_PREFIX: &str = "validator"; -const VALIDATOR_ADDRESS_RAW_HASH: &str = "address_raw_hash"; -const VALIDATOR_CONSENSUS_KEY_STORAGE_KEY: &str = "consensus_key"; -const VALIDATOR_ETH_COLD_KEY_STORAGE_KEY: &str = "eth_cold_key"; -const VALIDATOR_ETH_HOT_KEY_STORAGE_KEY: &str = "eth_hot_key"; -const VALIDATOR_STATE_STORAGE_KEY: &str = "state"; -const VALIDATOR_DELTAS_STORAGE_KEY: &str = "deltas"; -const VALIDATOR_COMMISSION_RATE_STORAGE_KEY: &str = "commission_rate"; -const VALIDATOR_MAX_COMMISSION_CHANGE_STORAGE_KEY: &str = - "max_commission_rate_change"; -const VALIDATOR_REWARDS_PRODUCT_KEY: &str = "validator_rewards_product"; -const VALIDATOR_LAST_KNOWN_PRODUCT_EPOCH_KEY: &str = - "last_known_rewards_product_epoch"; -const SLASHES_PREFIX: &str = "slash"; -const ENQUEUED_SLASHES_KEY: &str = "enqueued_slashes"; -const VALIDATOR_LAST_SLASH_EPOCH: &str = "last_slash_epoch"; -const BOND_STORAGE_KEY: &str = "bond"; -const UNBOND_STORAGE_KEY: &str = "unbond"; -const VALIDATOR_TOTAL_BONDED_STORAGE_KEY: &str = "total_bonded"; -const VALIDATOR_TOTAL_UNBONDED_STORAGE_KEY: &str = "total_unbonded"; -const VALIDATOR_SETS_STORAGE_PREFIX: &str = "validator_sets"; -const CONSENSUS_VALIDATOR_SET_STORAGE_KEY: &str = "consensus"; -const BELOW_CAPACITY_VALIDATOR_SET_STORAGE_KEY: &str = "below_capacity"; -const TOTAL_CONSENSUS_STAKE_STORAGE_KEY: &str = "total_consensus_stake"; -const TOTAL_DELTAS_STORAGE_KEY: &str = "total_deltas"; -const VALIDATOR_SET_POSITIONS_KEY: &str = "validator_set_positions"; -const CONSENSUS_KEYS: &str = "consensus_keys"; -const LAST_BLOCK_PROPOSER_STORAGE_KEY: &str = "last_block_proposer"; -const CONSENSUS_VALIDATOR_SET_ACCUMULATOR_STORAGE_KEY: &str = - "validator_rewards_accumulator"; -const LAST_REWARD_CLAIM_EPOCH: &str = "last_reward_claim_epoch"; -const REWARDS_COUNTER_KEY: &str = "validator_rewards_commissions"; -const VALIDATOR_INCOMING_REDELEGATIONS_KEY: &str = "incoming_redelegations"; -const VALIDATOR_OUTGOING_REDELEGATIONS_KEY: &str = "outgoing_redelegations"; -const VALIDATOR_TOTAL_REDELEGATED_BONDED_KEY: &str = "total_redelegated_bonded"; -const VALIDATOR_TOTAL_REDELEGATED_UNBONDED_KEY: &str = - "total_redelegated_unbonded"; -const DELEGATOR_REDELEGATED_BONDS_KEY: &str = "delegator_redelegated_bonds"; -const DELEGATOR_REDELEGATED_UNBONDS_KEY: &str = "delegator_redelegated_unbonds"; -const VALIDATOR_EMAIL_KEY: &str = "email"; -const VALIDATOR_DESCRIPTION_KEY: &str = "description"; -const VALIDATOR_WEBSITE_KEY: &str = "website"; -const VALIDATOR_DISCORD_KEY: &str = "discord_handle"; -const LIVENESS_PREFIX: &str = "liveness"; -const LIVENESS_MISSED_VOTES: &str = "missed_votes"; -const LIVENESS_MISSED_VOTES_SUM: &str = "sum_missed_votes"; - -/// Is the given key a PoS storage key? -pub fn is_pos_key(key: &Key) -> bool { - match &key.segments.get(0) { - Some(DbKeySeg::AddressSeg(addr)) => addr == &ADDRESS, - _ => false, - } +use namada_core::types::dec::Dec; +use namada_core::types::key::{ + common, protocol_pk_key, tm_consensus_key_raw_hash, +}; +use namada_core::types::storage::Epoch; +use namada_core::types::token; + +use crate::storage_key::consensus_keys_key; +use crate::types::{ + BelowCapacityValidatorSets, BondId, Bonds, CommissionRates, + ConsensusValidatorSets, DelegatorRedelegatedBonded, + DelegatorRedelegatedUnbonded, EpochedSlashes, IncomingRedelegations, + LivenessMissedVotes, LivenessSumMissedVotes, OutgoingRedelegations, + ReverseOrdTokenAmount, RewardsAccumulator, RewardsProducts, Slashes, + TotalConsensusStakes, TotalDeltas, TotalRedelegatedBonded, + TotalRedelegatedUnbonded, Unbonds, ValidatorAddresses, + ValidatorConsensusKeys, ValidatorDeltas, ValidatorEthColdKeys, + ValidatorEthHotKeys, ValidatorMetaData, ValidatorProtocolKeys, + ValidatorSetPositions, ValidatorState, ValidatorStates, + ValidatorTotalUnbonded, WeightedValidator, +}; +use crate::{storage_key, MetadataError, OwnedPosParams, PosParams}; + +// ---- Storage handles ---- + +/// Get the storage handle to the epoched consensus validator set +pub fn consensus_validator_set_handle() -> ConsensusValidatorSets { + let key = storage_key::consensus_validator_set_key(); + ConsensusValidatorSets::open(key) +} + +/// Get the storage handle to the epoched below-capacity validator set +pub fn below_capacity_validator_set_handle() -> BelowCapacityValidatorSets { + let key = storage_key::below_capacity_validator_set_key(); + BelowCapacityValidatorSets::open(key) +} + +/// Get the storage handle to a PoS validator's consensus key (used for +/// signing block votes). +pub fn validator_consensus_key_handle( + validator: &Address, +) -> ValidatorConsensusKeys { + let key = storage_key::validator_consensus_key_key(validator); + ValidatorConsensusKeys::open(key) } -/// Storage key for PoS parameters. -pub fn params_key() -> Key { - Key::from(ADDRESS.to_db_key()) - .push(&PARAMS_STORAGE_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Is storage key for PoS parameters? -pub fn is_params_key(key: &Key) -> bool { - matches!(&key.segments[..], [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(key)] if addr == &ADDRESS && key == PARAMS_STORAGE_KEY) -} - -/// Storage key prefix for validator data. -fn validator_prefix(validator: &Address) -> Key { - Key::from(ADDRESS.to_db_key()) - .push(&VALIDATOR_STORAGE_PREFIX.to_owned()) - .expect("Cannot obtain a storage key") - .push(&validator.to_db_key()) - .expect("Cannot obtain a storage key") -} - -/// Storage key for validator's address raw hash for look-up from raw hash of an -/// address to address. -pub fn validator_address_raw_hash_key(raw_hash: impl AsRef) -> Key { - let raw_hash = raw_hash.as_ref().to_owned(); - Key::from(ADDRESS.to_db_key()) - .push(&VALIDATOR_ADDRESS_RAW_HASH.to_owned()) - .expect("Cannot obtain a storage key") - .push(&raw_hash) - .expect("Cannot obtain a storage key") -} - -/// Is storage key for validator's address raw hash? -pub fn is_validator_address_raw_hash_key(key: &Key) -> Option<&str> { - match &key.segments[..] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::StringSeg(raw_hash), - ] if addr == &ADDRESS && prefix == VALIDATOR_ADDRESS_RAW_HASH => { - Some(raw_hash) - } - _ => None, - } +/// Get the storage handle to a PoS validator's protocol key key. +pub fn validator_protocol_key_handle( + validator: &Address, +) -> ValidatorProtocolKeys { + let key = protocol_pk_key(validator); + ValidatorProtocolKeys::open(key) } -/// Storage key for validator's consensus key. -pub fn validator_consensus_key_key(validator: &Address) -> Key { - validator_prefix(validator) - .push(&VALIDATOR_CONSENSUS_KEY_STORAGE_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Is storage key for validator's consensus key? -pub fn is_validator_consensus_key_key(key: &Key) -> Option<&Address> { - if key.segments.len() >= 4 { - match &key.segments[..4] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::AddressSeg(validator), - DbKeySeg::StringSeg(key), - ] if addr == &ADDRESS - && prefix == VALIDATOR_STORAGE_PREFIX - && key == VALIDATOR_CONSENSUS_KEY_STORAGE_KEY => - { - Some(validator) - } - _ => None, - } - } else { - None - } +/// Get the storage handle to a PoS validator's eth hot key. +pub fn validator_eth_hot_key_handle( + validator: &Address, +) -> ValidatorEthHotKeys { + let key = storage_key::validator_eth_hot_key_key(validator); + ValidatorEthHotKeys::open(key) } -/// Storage key for validator's eth cold key. -pub fn validator_eth_cold_key_key(validator: &Address) -> Key { - validator_prefix(validator) - .push(&VALIDATOR_ETH_COLD_KEY_STORAGE_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Is storage key for validator's eth cold key? -pub fn is_validator_eth_cold_key_key(key: &Key) -> Option<&Address> { - if key.segments.len() >= 4 { - match &key.segments[..4] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::AddressSeg(validator), - DbKeySeg::StringSeg(key), - ] if addr == &ADDRESS - && prefix == VALIDATOR_STORAGE_PREFIX - && key == VALIDATOR_ETH_COLD_KEY_STORAGE_KEY => - { - Some(validator) - } - _ => None, - } - } else { - None - } +/// Get the storage handle to a PoS validator's eth cold key. +pub fn validator_eth_cold_key_handle( + validator: &Address, +) -> ValidatorEthColdKeys { + let key = storage_key::validator_eth_cold_key_key(validator); + ValidatorEthColdKeys::open(key) } -/// Storage key for validator's eth hot key. -pub fn validator_eth_hot_key_key(validator: &Address) -> Key { - validator_prefix(validator) - .push(&VALIDATOR_ETH_HOT_KEY_STORAGE_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Is storage key for validator's eth hot key? -pub fn is_validator_eth_hot_key_key(key: &Key) -> Option<&Address> { - if key.segments.len() >= 4 { - match &key.segments[..4] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::AddressSeg(validator), - DbKeySeg::StringSeg(key), - ] if addr == &ADDRESS - && prefix == VALIDATOR_STORAGE_PREFIX - && key == VALIDATOR_ETH_HOT_KEY_STORAGE_KEY => - { - Some(validator) - } - _ => None, - } - } else { - None - } +/// Get the storage handle to the total consensus validator stake +pub fn total_consensus_stake_handle() -> TotalConsensusStakes { + let key = storage_key::total_consensus_stake_key(); + TotalConsensusStakes::open(key) } -/// Storage key for validator's commission rate. -pub fn validator_commission_rate_key(validator: &Address) -> Key { - validator_prefix(validator) - .push(&VALIDATOR_COMMISSION_RATE_STORAGE_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Is storage key for validator's commission rate? -pub fn is_validator_commission_rate_key(key: &Key) -> Option<&Address> { - if key.segments.len() >= 4 { - match &key.segments[..4] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::AddressSeg(validator), - DbKeySeg::StringSeg(key), - ] if addr == &ADDRESS - && prefix == VALIDATOR_STORAGE_PREFIX - && key == VALIDATOR_COMMISSION_RATE_STORAGE_KEY => - { - Some(validator) - } - _ => None, - } - } else { - None - } +/// Get the storage handle to a PoS validator's state +pub fn validator_state_handle(validator: &Address) -> ValidatorStates { + let key = storage_key::validator_state_key(validator); + ValidatorStates::open(key) } -/// Storage key for validator's maximum commission rate change per epoch. -pub fn validator_max_commission_rate_change_key(validator: &Address) -> Key { - validator_prefix(validator) - .push(&VALIDATOR_MAX_COMMISSION_CHANGE_STORAGE_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Is storage key for validator's maximum commission rate change per epoch? -pub fn is_validator_max_commission_rate_change_key( - key: &Key, -) -> Option<&Address> { - match &key.segments[..] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::AddressSeg(validator), - DbKeySeg::StringSeg(key), - ] if addr == &ADDRESS - && prefix == VALIDATOR_STORAGE_PREFIX - && key == VALIDATOR_MAX_COMMISSION_CHANGE_STORAGE_KEY => - { - Some(validator) - } - _ => None, - } +/// Get the storage handle to a PoS validator's deltas +pub fn validator_deltas_handle(validator: &Address) -> ValidatorDeltas { + let key = storage_key::validator_deltas_key(validator); + ValidatorDeltas::open(key) } -/// Is storage key for some piece of validator metadata? -pub fn is_validator_metadata_key(key: &Key) -> Option<&Address> { - match &key.segments[..] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::AddressSeg(validator), - DbKeySeg::StringSeg(metadata), - ] if addr == &ADDRESS - && prefix == VALIDATOR_STORAGE_PREFIX - && matches!( - metadata.as_str(), - VALIDATOR_EMAIL_KEY - | VALIDATOR_DESCRIPTION_KEY - | VALIDATOR_WEBSITE_KEY - | VALIDATOR_DISCORD_KEY - ) => - { - Some(validator) - } - _ => None, - } +/// Get the storage handle to the total deltas +pub fn total_deltas_handle() -> TotalDeltas { + let key = storage_key::total_deltas_key(); + TotalDeltas::open(key) } -/// Storage key for validator's rewards products. -pub fn validator_rewards_product_key(validator: &Address) -> Key { - validator_prefix(validator) - .push(&VALIDATOR_REWARDS_PRODUCT_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Is storage key for validator's rewards products? -pub fn is_validator_rewards_product_key(key: &Key) -> Option<&Address> { - match &key.segments[..] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::AddressSeg(validator), - DbKeySeg::StringSeg(key), - ] if addr == &ADDRESS - && prefix == VALIDATOR_STORAGE_PREFIX - && key == VALIDATOR_REWARDS_PRODUCT_KEY => - { - Some(validator) - } - _ => None, - } +/// Get the storage handle to the set of all validators +pub fn validator_addresses_handle() -> ValidatorAddresses { + let key = storage_key::validator_addresses_key(); + ValidatorAddresses::open(key) } -/// Storage prefix for rewards counter. -pub fn rewards_counter_prefix() -> Key { - Key::from(ADDRESS.to_db_key()) - .push(&REWARDS_COUNTER_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Storage key for rewards counter. -pub fn rewards_counter_key(source: &Address, validator: &Address) -> Key { - rewards_counter_prefix() - .push(&source.to_db_key()) - .expect("Cannot obtain a storage key") - .push(&validator.to_db_key()) - .expect("Cannot obtain a storage key") -} - -/// Is the storage key for rewards counter? -pub fn is_rewards_counter_key(key: &Key) -> Option { - match &key.segments[..] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(key), - DbKeySeg::AddressSeg(source), - DbKeySeg::AddressSeg(validator), - ] if addr == &ADDRESS && key == REWARDS_COUNTER_KEY => Some(BondId { - source: source.clone(), - validator: validator.clone(), - }), - _ => None, - } +/// Get the storage handle to a PoS validator's commission rate +pub fn validator_commission_rate_handle( + validator: &Address, +) -> CommissionRates { + let key = storage_key::validator_commission_rate_key(validator); + CommissionRates::open(key) } -/// Storage key for a validator's incoming redelegations, where the prefixed -/// validator is the destination validator. -pub fn validator_incoming_redelegations_key(validator: &Address) -> Key { - validator_prefix(validator) - .push(&VALIDATOR_INCOMING_REDELEGATIONS_KEY.to_owned()) - .expect("Cannot obtain a storage key") +/// Get the storage handle to a bond, which is dynamically updated with when +/// unbonding +pub fn bond_handle(source: &Address, validator: &Address) -> Bonds { + let bond_id = BondId { + source: source.clone(), + validator: validator.clone(), + }; + let key = storage_key::bond_key(&bond_id); + Bonds::open(key) } -/// Storage key for a validator's outgoing redelegations, where the prefixed -/// validator is the source validator. -pub fn validator_outgoing_redelegations_key(validator: &Address) -> Key { - validator_prefix(validator) - .push(&VALIDATOR_OUTGOING_REDELEGATIONS_KEY.to_owned()) - .expect("Cannot obtain a storage key") +/// Get the storage handle to a validator's total bonds, which are not updated +/// due to unbonding +pub fn total_bonded_handle(validator: &Address) -> Bonds { + let key = storage_key::validator_total_bonded_key(validator); + Bonds::open(key) } -/// Storage key for validator's total-redelegated-bonded amount to track for -/// slashing -pub fn validator_total_redelegated_bonded_key(validator: &Address) -> Key { - validator_prefix(validator) - .push(&VALIDATOR_TOTAL_REDELEGATED_BONDED_KEY.to_owned()) - .expect("Cannot obtain a storage key") +/// Get the storage handle to an unbond +pub fn unbond_handle(source: &Address, validator: &Address) -> Unbonds { + let bond_id = BondId { + source: source.clone(), + validator: validator.clone(), + }; + let key = storage_key::unbond_key(&bond_id); + Unbonds::open(key) } -/// Storage key for validator's total-redelegated-unbonded amount to track for -/// slashing -pub fn validator_total_redelegated_unbonded_key(validator: &Address) -> Key { - validator_prefix(validator) - .push(&VALIDATOR_TOTAL_REDELEGATED_UNBONDED_KEY.to_owned()) - .expect("Cannot obtain a storage key") +/// Get the storage handle to a validator's total-unbonded map +pub fn total_unbonded_handle(validator: &Address) -> ValidatorTotalUnbonded { + let key = storage_key::validator_total_unbonded_key(validator); + ValidatorTotalUnbonded::open(key) } -/// Is the storage key's prefix matching one of validator's: -/// -/// - incoming or outgoing redelegations -/// - total redelegated bonded or unbond amounts -pub fn is_validator_redelegations_key(key: &Key) -> bool { - if key.segments.len() >= 4 { - match &key.segments[..4] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(val_prefix), - DbKeySeg::AddressSeg(_validator), - DbKeySeg::StringSeg(prefix), - ] => { - addr == &ADDRESS - && val_prefix == VALIDATOR_STORAGE_PREFIX - && (prefix == VALIDATOR_INCOMING_REDELEGATIONS_KEY - || prefix == VALIDATOR_OUTGOING_REDELEGATIONS_KEY - || prefix == VALIDATOR_TOTAL_REDELEGATED_BONDED_KEY - || prefix == VALIDATOR_TOTAL_REDELEGATED_UNBONDED_KEY) - } - _ => false, - } - } else { - false - } +/// Get the storage handle to a PoS validator's deltas +pub fn validator_set_positions_handle() -> ValidatorSetPositions { + let key = storage_key::validator_set_positions_key(); + ValidatorSetPositions::open(key) } -/// Storage key prefix for all delegators' redelegated bonds. -pub fn delegator_redelegated_bonds_prefix() -> Key { - Key::from(ADDRESS.to_db_key()) - .push(&DELEGATOR_REDELEGATED_BONDS_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Storage key for a particular delegator's redelegated bond information. -pub fn delegator_redelegated_bonds_key(delegator: &Address) -> Key { - delegator_redelegated_bonds_prefix() - .push(&delegator.to_db_key()) - .expect("Cannot obtain a storage key") -} - -/// Storage key prefix for all delegators' redelegated unbonds. -pub fn delegator_redelegated_unbonds_prefix() -> Key { - Key::from(ADDRESS.to_db_key()) - .push(&DELEGATOR_REDELEGATED_UNBONDS_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Storage key for a particular delegator's redelegated unbond information. -pub fn delegator_redelegated_unbonds_key(delegator: &Address) -> Key { - delegator_redelegated_unbonds_prefix() - .push(&delegator.to_db_key()) - .expect("Cannot obtain a storage key") -} - -/// Is the storage key's prefix matching delegator's total redelegated bonded or -/// unbond amounts? If so, returns the delegator's address. -pub fn is_delegator_redelegations_key(key: &Key) -> Option<&Address> { - if key.segments.len() >= 3 { - match &key.segments[..3] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::AddressSeg(delegator), - ] if addr == &ADDRESS - && (prefix == DELEGATOR_REDELEGATED_BONDS_KEY - || prefix == DELEGATOR_REDELEGATED_UNBONDS_KEY) => - { - Some(delegator) - } - - _ => None, - } - } else { - None - } +/// Get the storage handle to a PoS validator's slashes +pub fn validator_slashes_handle(validator: &Address) -> Slashes { + let key = storage_key::validator_slashes_key(validator); + Slashes::open(key) } -/// Storage key for validator's last known rewards product epoch. -pub fn validator_last_known_product_epoch_key(validator: &Address) -> Key { - validator_prefix(validator) - .push(&VALIDATOR_LAST_KNOWN_PRODUCT_EPOCH_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Is storage key for validator's last known rewards product epoch? -pub fn is_validator_last_known_product_epoch_key( - key: &Key, -) -> Option<&Address> { - match &key.segments[..] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::AddressSeg(validator), - DbKeySeg::StringSeg(key), - ] if addr == &ADDRESS - && prefix == VALIDATOR_STORAGE_PREFIX - && key == VALIDATOR_LAST_KNOWN_PRODUCT_EPOCH_KEY => - { - Some(validator) - } - _ => None, - } +/// Get the storage handle to list of all slashes to be processed and ultimately +/// placed in the `validator_slashes_handle` +pub fn enqueued_slashes_handle() -> EpochedSlashes { + let key = storage_key::enqueued_slashes_key(); + EpochedSlashes::open(key) } -/// Storage key for validator's consensus key. -pub fn validator_state_key(validator: &Address) -> Key { - validator_prefix(validator) - .push(&VALIDATOR_STATE_STORAGE_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Is storage key for validator's state? -pub fn is_validator_state_key(key: &Key) -> Option<(&Address, Epoch)> { - match &key.segments[..] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::AddressSeg(validator), - DbKeySeg::StringSeg(key), - DbKeySeg::StringSeg(lazy_map), - DbKeySeg::StringSeg(data), - DbKeySeg::StringSeg(epoch), - ] if addr == &ADDRESS - && prefix == VALIDATOR_STORAGE_PREFIX - && key == VALIDATOR_STATE_STORAGE_KEY - && lazy_map == epoched::LAZY_MAP_SUB_KEY - && data == lazy_map::DATA_SUBKEY => - { - let epoch = Epoch::parse(epoch.clone()) - .expect("Should be able to parse the epoch"); - Some((validator, epoch)) - } - _ => None, - } +/// Get the storage handle to the rewards accumulator for the consensus +/// validators in a given epoch +pub fn rewards_accumulator_handle() -> RewardsAccumulator { + let key = storage_key::consensus_validator_rewards_accumulator_key(); + RewardsAccumulator::open(key) } -/// Is storage key for a validator state's last update or oldest epoch? -pub fn is_validator_state_epoched_meta_key(key: &Key) -> bool { - if key.segments.len() >= 5 { - match &key.segments[..5] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::AddressSeg(_validator), - DbKeySeg::StringSeg(key), - DbKeySeg::StringSeg(data), - ] => { - addr == &ADDRESS - && prefix == VALIDATOR_STORAGE_PREFIX - && key == VALIDATOR_STATE_STORAGE_KEY - && (data == epoched::LAST_UPDATE_SUB_KEY - || data == epoched::OLDEST_EPOCH_SUB_KEY) - } - _ => false, - } - } else { - false - } +/// Get the storage handle to a validator's rewards products +pub fn validator_rewards_products_handle( + validator: &Address, +) -> RewardsProducts { + let key = storage_key::validator_rewards_product_key(validator); + RewardsProducts::open(key) } -/// Storage key for validator's deltas. -pub fn validator_deltas_key(validator: &Address) -> Key { - validator_prefix(validator) - .push(&VALIDATOR_DELTAS_STORAGE_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Is storage key for validator's total deltas? -pub fn is_validator_deltas_key(key: &Key) -> bool { - if key.segments.len() >= 4 { - match &key.segments[..4] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::AddressSeg(_validator), - DbKeySeg::StringSeg(key), - ] => { - addr == &ADDRESS - && prefix == VALIDATOR_STORAGE_PREFIX - && key == VALIDATOR_DELTAS_STORAGE_KEY - } - _ => false, - } - } else { - false - } +/// Get the storage handle to a validator's incoming redelegations +pub fn validator_incoming_redelegations_handle( + validator: &Address, +) -> IncomingRedelegations { + let key = storage_key::validator_incoming_redelegations_key(validator); + IncomingRedelegations::open(key) } -/// Storage prefix for all active validators (consensus, below-capacity, -/// below-threshold, inactive, jailed) -pub fn validator_addresses_key() -> Key { - Key::from(ADDRESS.to_db_key()) - .push(&VALIDATOR_ADDRESSES_KEY.to_owned()) - .expect("Cannot obtain a storage key") +/// Get the storage handle to a validator's outgoing redelegations +pub fn validator_outgoing_redelegations_handle( + validator: &Address, +) -> OutgoingRedelegations { + let key = storage_key::validator_outgoing_redelegations_key(validator); + OutgoingRedelegations::open(key) } -/// Is the storage key a prefix for all active validators? -pub fn is_validator_addresses_key(key: &Key) -> bool { - if key.segments.len() >= 2 { - match &key.segments[..2] { - [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix)] => { - addr == &ADDRESS && prefix == VALIDATOR_ADDRESSES_KEY - } - _ => false, - } - } else { - false - } +/// Get the storage handle to a validator's total redelegated bonds +pub fn validator_total_redelegated_bonded_handle( + validator: &Address, +) -> TotalRedelegatedBonded { + let key = storage_key::validator_total_redelegated_bonded_key(validator); + TotalRedelegatedBonded::open(key) } -/// Storage prefix for slashes. -pub fn slashes_prefix() -> Key { - Key::from(ADDRESS.to_db_key()) - .push(&SLASHES_PREFIX.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Storage key for all slashes. -pub fn enqueued_slashes_key() -> Key { - // slashes_prefix() - Key::from(ADDRESS.to_db_key()) - .push(&ENQUEUED_SLASHES_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Storage key for validator's slashes. -pub fn validator_slashes_key(validator: &Address) -> Key { - slashes_prefix() - .push(&validator.to_db_key()) - .expect("Cannot obtain a storage key") -} - -/// Is storage key for a validator's slashes -pub fn is_validator_slashes_key(key: &Key) -> Option
{ - if key.segments.len() >= 5 { - match &key.segments[..5] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::AddressSeg(validator), - DbKeySeg::StringSeg(data), - DbKeySeg::StringSeg(_index), - ] if addr == &ADDRESS - && prefix == SLASHES_PREFIX - && data == lazy_vec::DATA_SUBKEY => - { - Some(validator.clone()) - } - _ => None, - } - } else { - None - } +/// Get the storage handle to a validator's outgoing redelegations +pub fn validator_total_redelegated_unbonded_handle( + validator: &Address, +) -> TotalRedelegatedUnbonded { + let key = storage_key::validator_total_redelegated_unbonded_key(validator); + TotalRedelegatedUnbonded::open(key) } -/// Storage key for the last (most recent) epoch in which a slashable offense -/// was detected for a given validator -pub fn validator_last_slash_key(validator: &Address) -> Key { - validator_prefix(validator) - .push(&VALIDATOR_LAST_SLASH_EPOCH.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Storage key prefix for all bonds. -pub fn bonds_prefix() -> Key { - Key::from(ADDRESS.to_db_key()) - .push(&BOND_STORAGE_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Storage key prefix for all bonds of the given source address. -pub fn bonds_for_source_prefix(source: &Address) -> Key { - bonds_prefix() - .push(&source.to_db_key()) - .expect("Cannot obtain a storage key") -} - -/// Storage key for a bond with the given ID (source and validator). -pub fn bond_key(bond_id: &BondId) -> Key { - bonds_for_source_prefix(&bond_id.source) - .push(&bond_id.validator.to_db_key()) - .expect("Cannot obtain a storage key") -} - -/// Is storage key for a bond? Returns the bond ID and bond start epoch if so. -pub fn is_bond_key(key: &Key) -> Option<(BondId, Epoch)> { - if key.segments.len() >= 7 { - match &key.segments[..7] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::AddressSeg(source), - DbKeySeg::AddressSeg(validator), - DbKeySeg::StringSeg(lazy_map), - DbKeySeg::StringSeg(data), - DbKeySeg::StringSeg(epoch_str), - ] if addr == &ADDRESS - && prefix == BOND_STORAGE_KEY - && lazy_map == epoched::LAZY_MAP_SUB_KEY - && data == lazy_map::DATA_SUBKEY => - { - let start = Epoch::parse(epoch_str.clone()).ok()?; - Some(( - BondId { - source: source.clone(), - validator: validator.clone(), - }, - start, - )) - } - _ => None, - } - } else { - None - } +/// Get the storage handle to a delegator's redelegated bonds information +pub fn delegator_redelegated_bonds_handle( + delegator: &Address, +) -> DelegatorRedelegatedBonded { + let key = storage_key::delegator_redelegated_bonds_key(delegator); + DelegatorRedelegatedBonded::open(key) } -/// Is storage key for a bond last update or oldest epoch? Returns the bond ID -/// if so. -pub fn is_bond_epoched_meta_key(key: &Key) -> Option { - if key.segments.len() >= 5 { - match &key.segments[..5] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::AddressSeg(source), - DbKeySeg::AddressSeg(validator), - DbKeySeg::StringSeg(data), - ] if addr == &ADDRESS - && prefix == BOND_STORAGE_KEY - && (data == epoched::LAST_UPDATE_SUB_KEY - || data == epoched::OLDEST_EPOCH_SUB_KEY) => - { - Some(BondId { - source: source.clone(), - validator: validator.clone(), - }) - } - _ => None, - } - } else { - None - } +/// Get the storage handle to a delegator's redelegated unbonds information +pub fn delegator_redelegated_unbonds_handle( + delegator: &Address, +) -> DelegatorRedelegatedUnbonded { + let key = storage_key::delegator_redelegated_unbonds_key(delegator); + DelegatorRedelegatedUnbonded::open(key) +} + +/// Get the storage handle to the missed votes for liveness tracking +pub fn liveness_missed_votes_handle() -> LivenessMissedVotes { + let key = storage_key::liveness_missed_votes_key(); + LivenessMissedVotes::open(key) +} + +/// Get the storage handle to the sum of missed votes for liveness tracking +pub fn liveness_sum_missed_votes_handle() -> LivenessSumMissedVotes { + let key = storage_key::liveness_sum_missed_votes_key(); + LivenessSumMissedVotes::open(key) +} + +// ---- Storage read + write ---- + +/// Read PoS parameters +pub fn read_pos_params(storage: &S) -> storage_api::Result +where + S: StorageRead, +{ + let params = storage + .read(&storage_key::params_key()) + .transpose() + .expect("PosParams should always exist in storage after genesis")?; + read_non_pos_owned_params(storage, params) +} + +/// Read non-PoS-owned parameters to add them to `OwnedPosParams` to construct +/// `PosParams`. +pub fn read_non_pos_owned_params( + storage: &S, + owned: OwnedPosParams, +) -> storage_api::Result +where + S: StorageRead, +{ + let max_proposal_period = get_max_proposal_period(storage)?; + Ok(PosParams { + owned, + max_proposal_period, + }) +} + +/// Write PoS parameters +pub fn write_pos_params( + storage: &mut S, + params: &OwnedPosParams, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let key = storage_key::params_key(); + storage.write(&key, params) +} + +/// Get the validator address given the raw hash of the Tendermint consensus key +pub fn find_validator_by_raw_hash( + storage: &S, + raw_hash: impl AsRef, +) -> storage_api::Result> +where + S: StorageRead, +{ + let key = storage_key::validator_address_raw_hash_key(raw_hash); + storage.read(&key) +} + +/// Write PoS validator's address raw hash. +pub fn write_validator_address_raw_hash( + storage: &mut S, + validator: &Address, + consensus_key: &common::PublicKey, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let raw_hash = tm_consensus_key_raw_hash(consensus_key); + storage.write( + &storage_key::validator_address_raw_hash_key(raw_hash), + validator, + ) +} + +/// Read PoS validator's max commission rate change. +pub fn read_validator_max_commission_rate_change( + storage: &S, + validator: &Address, +) -> storage_api::Result> +where + S: StorageRead, +{ + let key = storage_key::validator_max_commission_rate_change_key(validator); + storage.read(&key) } -/// Storage key for the total bonds for a given validator. -pub fn validator_total_bonded_key(validator: &Address) -> Key { - validator_prefix(validator) - .push(&VALIDATOR_TOTAL_BONDED_STORAGE_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Is the storage key for the total bonds or unbonds for a validator? -pub fn is_validator_total_bond_or_unbond_key(key: &Key) -> bool { - if key.segments.len() >= 4 { - match &key.segments[..4] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(val_prefix), - DbKeySeg::AddressSeg(_validator), - DbKeySeg::StringSeg(prefix), - ] => { - addr == &ADDRESS - && val_prefix == VALIDATOR_STORAGE_PREFIX - && (prefix == VALIDATOR_TOTAL_BONDED_STORAGE_KEY - || prefix == VALIDATOR_TOTAL_UNBONDED_STORAGE_KEY) - } - _ => false, - } - } else { - false - } +/// Write PoS validator's max commission rate change. +pub fn write_validator_max_commission_rate_change( + storage: &mut S, + validator: &Address, + change: Dec, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let key = storage_key::validator_max_commission_rate_change_key(validator); + storage.write(&key, change) +} + +/// Read the most recent slash epoch for the given epoch +pub fn read_validator_last_slash_epoch( + storage: &S, + validator: &Address, +) -> storage_api::Result> +where + S: StorageRead, +{ + let key = storage_key::validator_last_slash_key(validator); + storage.read(&key) } -/// Storage key prefix for all unbonds. -pub fn unbonds_prefix() -> Key { - Key::from(ADDRESS.to_db_key()) - .push(&UNBOND_STORAGE_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Storage key prefix for all unbonds of the given source address. -pub fn unbonds_for_source_prefix(source: &Address) -> Key { - unbonds_prefix() - .push(&source.to_db_key()) - .expect("Cannot obtain a storage key") -} - -/// Storage key for an unbond with the given ID (source and validator). -pub fn unbond_key(bond_id: &BondId) -> Key { - unbonds_for_source_prefix(&bond_id.source) - .push(&bond_id.validator.to_db_key()) - .expect("Cannot obtain a storage key") -} - -/// Is storage key for an unbond? Returns the bond ID and unbond start and -/// withdraw epoch if it is. -pub fn is_unbond_key(key: &Key) -> Option<(BondId, Epoch, Epoch)> { - if key.segments.len() >= 8 { - match &key.segments[..8] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(prefix), - DbKeySeg::AddressSeg(source), - DbKeySeg::AddressSeg(validator), - DbKeySeg::StringSeg(data_1), - DbKeySeg::StringSeg(start_epoch_str), - DbKeySeg::StringSeg(data_2), - DbKeySeg::StringSeg(withdraw_epoch_str), - ] if addr == &ADDRESS - && prefix == UNBOND_STORAGE_KEY - && data_1 == lazy_map::DATA_SUBKEY - && data_2 == lazy_map::DATA_SUBKEY => - { - let withdraw = Epoch::parse(withdraw_epoch_str.clone()).ok()?; - let start = Epoch::parse(start_epoch_str.clone()).ok()?; - Some(( - BondId { - source: source.clone(), - validator: validator.clone(), +/// Write the most recent slash epoch for the given epoch +pub fn write_validator_last_slash_epoch( + storage: &mut S, + validator: &Address, + epoch: Epoch, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let key = storage_key::validator_last_slash_key(validator); + storage.write(&key, epoch) +} + +/// Read last block proposer address. +pub fn read_last_block_proposer_address( + storage: &S, +) -> storage_api::Result> +where + S: StorageRead, +{ + let key = storage_key::last_block_proposer_key(); + storage.read(&key) +} + +/// Write last block proposer address. +pub fn write_last_block_proposer_address( + storage: &mut S, + address: Address, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let key = storage_key::last_block_proposer_key(); + storage.write(&key, address) +} + +/// Read PoS validator's delta value. +pub fn read_validator_deltas_value( + storage: &S, + validator: &Address, + epoch: &namada_core::types::storage::Epoch, +) -> storage_api::Result> +where + S: StorageRead, +{ + let handle = validator_deltas_handle(validator); + handle.get_delta_val(storage, *epoch) +} + +/// Read PoS validator's stake (sum of deltas). +/// For non-validators and validators with `0` stake, this returns the default - +/// `token::Amount::zero()`. +pub fn read_validator_stake( + storage: &S, + params: &PosParams, + validator: &Address, + epoch: namada_core::types::storage::Epoch, +) -> storage_api::Result +where + S: StorageRead, +{ + let handle = validator_deltas_handle(validator); + let amount = handle + .get_sum(storage, epoch, params)? + .map(|change| { + debug_assert!(change.non_negative()); + token::Amount::from_change(change) + }) + .unwrap_or_default(); + Ok(amount) +} + +/// Add or remove PoS validator's stake delta value +pub fn update_validator_deltas( + storage: &mut S, + params: &OwnedPosParams, + validator: &Address, + delta: token::Change, + current_epoch: namada_core::types::storage::Epoch, + offset_opt: Option, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let handle = validator_deltas_handle(validator); + let offset = offset_opt.unwrap_or(params.pipeline_len); + let val = handle + .get_delta_val(storage, current_epoch + offset)? + .unwrap_or_default(); + handle.set( + storage, + val.checked_add(&delta) + .expect("Validator deltas updated amount should not overflow"), + current_epoch, + offset, + ) +} + +/// Read PoS total stake (sum of deltas). +pub fn read_total_stake( + storage: &S, + params: &PosParams, + epoch: namada_core::types::storage::Epoch, +) -> storage_api::Result +where + S: StorageRead, +{ + let handle = total_deltas_handle(); + let amnt = handle + .get_sum(storage, epoch, params)? + .map(|change| { + debug_assert!(change.non_negative()); + token::Amount::from_change(change) + }) + .unwrap_or_default(); + Ok(amnt) +} + +/// Read all addresses from consensus validator set. +pub fn read_consensus_validator_set_addresses( + storage: &S, + epoch: namada_core::types::storage::Epoch, +) -> storage_api::Result> +where + S: StorageRead, +{ + consensus_validator_set_handle() + .at(&epoch) + .iter(storage)? + .map(|res| res.map(|(_sub_key, address)| address)) + .collect() +} + +/// Read all addresses from below-capacity validator set. +pub fn read_below_capacity_validator_set_addresses( + storage: &S, + epoch: namada_core::types::storage::Epoch, +) -> storage_api::Result> +where + S: StorageRead, +{ + below_capacity_validator_set_handle() + .at(&epoch) + .iter(storage)? + .map(|res| res.map(|(_sub_key, address)| address)) + .collect() +} + +/// Read all addresses from the below-threshold set +pub fn read_below_threshold_validator_set_addresses( + storage: &S, + epoch: namada_core::types::storage::Epoch, +) -> storage_api::Result> +where + S: StorageRead, +{ + let params = read_pos_params(storage)?; + Ok(validator_addresses_handle() + .at(&epoch) + .iter(storage)? + .map(Result::unwrap) + .filter(|address| { + matches!( + validator_state_handle(address).get(storage, epoch, ¶ms), + Ok(Some(ValidatorState::BelowThreshold)) + ) + }) + .collect()) +} + +/// Read all addresses from consensus validator set with their stake. +pub fn read_consensus_validator_set_addresses_with_stake( + storage: &S, + epoch: namada_core::types::storage::Epoch, +) -> storage_api::Result> +where + S: StorageRead, +{ + consensus_validator_set_handle() + .at(&epoch) + .iter(storage)? + .map(|res| { + res.map( + |( + NestedSubKey::Data { + key: bonded_stake, + nested_sub_key: _, }, - start, - withdraw, - )) - } - _ => None, - } - } else { - None - } -} - -/// Storage key for validator's total-unbonded amount to track for slashing -pub fn validator_total_unbonded_key(validator: &Address) -> Key { - validator_prefix(validator) - .push(&VALIDATOR_TOTAL_UNBONDED_STORAGE_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Storage prefix for validator sets. -pub fn validator_sets_prefix() -> Key { - Key::from(ADDRESS.to_db_key()) - .push(&VALIDATOR_SETS_STORAGE_PREFIX.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Storage key for consensus validator set -pub fn consensus_validator_set_key() -> Key { - validator_sets_prefix() - .push(&CONSENSUS_VALIDATOR_SET_STORAGE_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Storage key for below-capacity validator set -pub fn below_capacity_validator_set_key() -> Key { - validator_sets_prefix() - .push(&BELOW_CAPACITY_VALIDATOR_SET_STORAGE_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Is storage key for the consensus validator set? -pub fn is_consensus_validator_set_key(key: &Key) -> bool { - matches!(&key.segments[..], [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(key), DbKeySeg::StringSeg(set_type), DbKeySeg::StringSeg(lazy_map), DbKeySeg::StringSeg(data), DbKeySeg::StringSeg(_epoch), DbKeySeg::StringSeg(_), DbKeySeg::StringSeg(_amount), DbKeySeg::StringSeg(_), DbKeySeg::StringSeg(_position)] if addr == &ADDRESS && key == VALIDATOR_SETS_STORAGE_PREFIX && set_type == CONSENSUS_VALIDATOR_SET_STORAGE_KEY && lazy_map == epoched::LAZY_MAP_SUB_KEY && data == lazy_map::DATA_SUBKEY) -} - -/// Is storage key for the below-capacity validator set? -pub fn is_below_capacity_validator_set_key(key: &Key) -> bool { - matches!(&key.segments[..], [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(key), DbKeySeg::StringSeg(set_type), DbKeySeg::StringSeg(lazy_map), DbKeySeg::StringSeg(data), DbKeySeg::StringSeg(_epoch), DbKeySeg::StringSeg(_), DbKeySeg::StringSeg(_amount), DbKeySeg::StringSeg(_), DbKeySeg::StringSeg(_position)] if addr == &ADDRESS && key == VALIDATOR_SETS_STORAGE_PREFIX && set_type == BELOW_CAPACITY_VALIDATOR_SET_STORAGE_KEY && lazy_map == epoched::LAZY_MAP_SUB_KEY && data == lazy_map::DATA_SUBKEY) -} - -/// Storage key for total consensus stake -pub fn total_consensus_stake_key() -> Key { - Key::from(ADDRESS.to_db_key()) - .push(&TOTAL_CONSENSUS_STAKE_STORAGE_KEY.to_owned()) - .expect("Cannot obtain a total consensus stake key") -} - -/// Is storage key for the total consensus stake? -pub fn is_total_consensus_stake_key(key: &Key) -> bool { - matches!(&key.segments[..], [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(key) - ] if addr == &ADDRESS && key == TOTAL_CONSENSUS_STAKE_STORAGE_KEY) -} - -/// Storage key for total deltas of all validators. -pub fn total_deltas_key() -> Key { - Key::from(ADDRESS.to_db_key()) - .push(&TOTAL_DELTAS_STORAGE_KEY.to_owned()) - .expect("Cannot obtain a storage key") + address, + )| { + WeightedValidator { + address, + bonded_stake, + } + }, + ) + }) + .collect() +} + +/// Count the number of consensus validators +pub fn get_num_consensus_validators( + storage: &S, + epoch: namada_core::types::storage::Epoch, +) -> storage_api::Result +where + S: StorageRead, +{ + Ok(consensus_validator_set_handle() + .at(&epoch) + .iter(storage)? + .count() as u64) +} + +/// Read all addresses from below-capacity validator set with their stake. +pub fn read_below_capacity_validator_set_addresses_with_stake( + storage: &S, + epoch: namada_core::types::storage::Epoch, +) -> storage_api::Result> +where + S: StorageRead, +{ + below_capacity_validator_set_handle() + .at(&epoch) + .iter(storage)? + .map(|res| { + res.map( + |( + NestedSubKey::Data { + key: ReverseOrdTokenAmount(bonded_stake), + nested_sub_key: _, + }, + address, + )| { + WeightedValidator { + address, + bonded_stake, + } + }, + ) + }) + .collect() +} + +/// Read all validator addresses. +pub fn read_all_validator_addresses( + storage: &S, + epoch: namada_core::types::storage::Epoch, +) -> storage_api::Result> +where + S: StorageRead, +{ + validator_addresses_handle() + .at(&epoch) + .iter(storage)? + .collect() +} + +/// Update PoS total deltas. +/// Note: for EpochedDelta, write the value to change storage by +pub fn update_total_deltas( + storage: &mut S, + params: &OwnedPosParams, + delta: token::Change, + current_epoch: namada_core::types::storage::Epoch, + offset_opt: Option, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let handle = total_deltas_handle(); + let offset = offset_opt.unwrap_or(params.pipeline_len); + let val = handle + .get_delta_val(storage, current_epoch + offset)? + .unwrap_or_default(); + handle.set( + storage, + val.checked_add(&delta) + .expect("Total deltas updated amount should not overflow"), + current_epoch, + offset, + ) +} + +/// Read PoS validator's email. +pub fn read_validator_email( + storage: &S, + validator: &Address, +) -> storage_api::Result> +where + S: StorageRead, +{ + storage.read(&storage_key::validator_email_key(validator)) } -/// Is storage key for total deltas of all validators? -pub fn is_total_deltas_key(key: &Key) -> bool { - if key.segments.len() >= 2 { - match &key.segments[..2] { - [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix)] => { - addr == &ADDRESS && prefix == TOTAL_DELTAS_STORAGE_KEY - } - _ => false, - } +/// Write PoS validator's email. The email cannot be removed, so an empty string +/// will result in an error. +pub fn write_validator_email( + storage: &mut S, + validator: &Address, + email: &String, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let key = storage_key::validator_email_key(validator); + if email.is_empty() { + Err(MetadataError::CannotRemoveEmail.into()) } else { - false + storage.write(&key, email) } } -/// Storage key for block proposer address of the previous block. -pub fn last_block_proposer_key() -> Key { - Key::from(ADDRESS.to_db_key()) - .push(&LAST_BLOCK_PROPOSER_STORAGE_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Is storage key for block proposer address of the previous block? -pub fn is_last_block_proposer_key(key: &Key) -> bool { - matches!(&key.segments[..], [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(key)] if addr == &ADDRESS && key == LAST_BLOCK_PROPOSER_STORAGE_KEY) -} - -/// Storage key for the consensus validator set rewards accumulator. -pub fn consensus_validator_rewards_accumulator_key() -> Key { - Key::from(ADDRESS.to_db_key()) - .push(&CONSENSUS_VALIDATOR_SET_ACCUMULATOR_STORAGE_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Is storage key for the consensus validator set? -pub fn is_consensus_validator_set_accumulator_key(key: &Key) -> bool { - matches!(&key.segments[..], [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(key), - ] if addr == &ADDRESS - && key == CONSENSUS_VALIDATOR_SET_ACCUMULATOR_STORAGE_KEY) -} - -/// Storage prefix for epoch at which an account last claimed PoS inflationary -/// rewards. -pub fn last_pos_reward_claim_epoch_prefix() -> Key { - Key::from(ADDRESS.to_db_key()) - .push(&LAST_REWARD_CLAIM_EPOCH.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Storage key for epoch at which an account last claimed PoS inflationary -/// rewards. -pub fn last_pos_reward_claim_epoch_key( - delegator: &Address, +/// Read PoS validator's description. +pub fn read_validator_description( + storage: &S, validator: &Address, -) -> Key { - last_pos_reward_claim_epoch_prefix() - .push(&delegator.to_db_key()) - .expect("Cannot obtain a storage key") - .push(&validator.to_db_key()) - .expect("Cannot obtain a storage key") -} - -/// Is the storage key for epoch at which an account last claimed PoS -/// inflationary rewards? Return the bond ID if so. -pub fn is_last_pos_reward_claim_epoch_key(key: &Key) -> Option { - match &key.segments[..] { - [ - DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(key), - DbKeySeg::AddressSeg(source), - DbKeySeg::AddressSeg(validator), - ] if addr == &ADDRESS && key == LAST_REWARD_CLAIM_EPOCH => { - Some(BondId { - source: source.clone(), - validator: validator.clone(), - }) - } - _ => None, - } +) -> storage_api::Result> +where + S: StorageRead, +{ + storage.read(&storage_key::validator_description_key(validator)) } -/// Get validator address from bond key -pub fn get_validator_address_from_bond(key: &Key) -> Option
{ - match key.get_at(3) { - Some(segment) => match segment { - DbKeySeg::AddressSeg(addr) => Some(addr.clone()), - DbKeySeg::StringSeg(_) => None, - }, - None => None, +/// Write PoS validator's description. If the provided arg is an empty string, +/// remove the data. +pub fn write_validator_description( + storage: &mut S, + validator: &Address, + description: &String, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let key = storage_key::validator_description_key(validator); + if description.is_empty() { + storage.delete(&key) + } else { + storage.write(&key, description) } } -/// Storage key for validator set positions -pub fn validator_set_positions_key() -> Key { - Key::from(ADDRESS.to_db_key()) - .push(&VALIDATOR_SET_POSITIONS_KEY.to_owned()) - .expect("Cannot obtain a storage key") +/// Read PoS validator's website. +pub fn read_validator_website( + storage: &S, + validator: &Address, +) -> storage_api::Result> +where + S: StorageRead, +{ + storage.read(&storage_key::validator_website_key(validator)) } -/// Is the storage key for validator set positions? -pub fn is_validator_set_positions_key(key: &Key) -> bool { - if key.segments.len() >= 2 { - match &key.segments[..2] { - [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix)] => { - addr == &ADDRESS && prefix == VALIDATOR_SET_POSITIONS_KEY - } - _ => false, - } +/// Write PoS validator's website. If the provided arg is an empty string, +/// remove the data. +pub fn write_validator_website( + storage: &mut S, + validator: &Address, + website: &String, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let key = storage_key::validator_website_key(validator); + if website.is_empty() { + storage.delete(&key) } else { - false + storage.write(&key, website) } } -/// Storage key for consensus keys set. -pub fn consensus_keys_key() -> Key { - Key::from(ADDRESS.to_db_key()) - .push(&CONSENSUS_KEYS.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Is storage key for consensus keys set? -pub fn is_consensus_keys_key(key: &Key) -> bool { - matches!(&key.segments[..], [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(key)] if addr == &ADDRESS && key == CONSENSUS_KEYS) -} - -/// Storage key for a validator's email -pub fn validator_email_key(validator: &Address) -> Key { - validator_prefix(validator) - .push(&VALIDATOR_EMAIL_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Storage key for a validator's description -pub fn validator_description_key(validator: &Address) -> Key { - validator_prefix(validator) - .push(&VALIDATOR_DESCRIPTION_KEY.to_owned()) - .expect("Cannot obtain a storage key") -} - -/// Storage key for a validator's website -pub fn validator_website_key(validator: &Address) -> Key { - validator_prefix(validator) - .push(&VALIDATOR_WEBSITE_KEY.to_owned()) - .expect("Cannot obtain a storage key") +/// Read PoS validator's discord handle. +pub fn read_validator_discord_handle( + storage: &S, + validator: &Address, +) -> storage_api::Result> +where + S: StorageRead, +{ + storage.read(&storage_key::validator_discord_key(validator)) } -/// Storage key for a validator's discord handle -pub fn validator_discord_key(validator: &Address) -> Key { - validator_prefix(validator) - .push(&VALIDATOR_DISCORD_KEY.to_owned()) - .expect("Cannot obtain a storage key") +/// Write PoS validator's discord handle. If the provided arg is an empty +/// string, remove the data. +pub fn write_validator_discord_handle( + storage: &mut S, + validator: &Address, + discord_handle: &String, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let key = storage_key::validator_discord_key(validator); + if discord_handle.is_empty() { + storage.delete(&key) + } else { + storage.write(&key, discord_handle) + } } -/// Storage prefix for the liveness data of the cosnensus validator set. -pub fn liveness_data_prefix() -> Key { - Key::from(ADDRESS.to_db_key()) - .push(&LIVENESS_PREFIX.to_owned()) - .expect("Cannot obtain a storage key") +/// Write validator's metadata. +pub fn write_validator_metadata( + storage: &mut S, + validator: &Address, + metadata: &ValidatorMetaData, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + // Email is the only required field in the metadata + write_validator_email(storage, validator, &metadata.email)?; + + if let Some(description) = metadata.description.as_ref() { + write_validator_description(storage, validator, description)?; + } + if let Some(website) = metadata.website.as_ref() { + write_validator_website(storage, validator, website)?; + } + if let Some(discord) = metadata.discord_handle.as_ref() { + write_validator_discord_handle(storage, validator, discord)?; + } + Ok(()) } -/// Storage key for the liveness records. -pub fn liveness_missed_votes_key() -> Key { - liveness_data_prefix() - .push(&LIVENESS_MISSED_VOTES.to_owned()) - .expect("Cannot obtain a storage key") +/// Get the last epoch in which rewards were claimed from storage, if any +pub fn get_last_reward_claim_epoch( + storage: &S, + delegator: &Address, + validator: &Address, +) -> storage_api::Result> +where + S: StorageRead, +{ + let key = + storage_key::last_pos_reward_claim_epoch_key(delegator, validator); + storage.read(&key) +} + +/// Write the last epoch in which rewards were claimed for the +/// delegator-validator pair +pub fn write_last_reward_claim_epoch( + storage: &mut S, + delegator: &Address, + validator: &Address, + epoch: Epoch, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let key = + storage_key::last_pos_reward_claim_epoch_key(delegator, validator); + storage.write(&key, epoch) } -/// Storage key for the liveness data. -pub fn liveness_sum_missed_votes_key() -> Key { - liveness_data_prefix() - .push(&LIVENESS_MISSED_VOTES_SUM.to_owned()) - .expect("Cannot obtain a storage key") +/// Check if the given consensus key is already being used to ensure uniqueness. +/// +/// If it's not being used, it will be inserted into the set that's being used +/// for this. If it's already used, this will return an Error. +pub fn try_insert_consensus_key( + storage: &mut S, + consensus_key: &common::PublicKey, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let key = consensus_keys_key(); + LazySet::open(key).try_insert(storage, consensus_key.clone()) +} + +/// Get the unique set of consensus keys in storage +pub fn get_consensus_key_set( + storage: &S, +) -> storage_api::Result> +where + S: StorageRead, +{ + let key = consensus_keys_key(); + let lazy_set = LazySet::::open(key); + Ok(lazy_set.iter(storage)?.map(Result::unwrap).collect()) +} + +/// Check if the given consensus key is already being used to ensure uniqueness. +pub fn is_consensus_key_used( + storage: &S, + consensus_key: &common::PublicKey, +) -> storage_api::Result +where + S: StorageRead, +{ + let key = consensus_keys_key(); + let handle = LazySet::open(key); + handle.contains(storage, consensus_key) } diff --git a/proof_of_stake/src/storage_key.rs b/proof_of_stake/src/storage_key.rs new file mode 100644 index 0000000000..b76760650b --- /dev/null +++ b/proof_of_stake/src/storage_key.rs @@ -0,0 +1,852 @@ +//! Proof-of-Stake storage keys and storage integration. + +use namada_core::ledger::storage_api::collections::{lazy_map, lazy_vec}; +use namada_core::types::address::Address; +use namada_core::types::storage::{DbKeySeg, Epoch, Key, KeySeg}; + +use super::ADDRESS; +use crate::epoched::LAZY_MAP_SUB_KEY; +use crate::types::BondId; + +const PARAMS_STORAGE_KEY: &str = "params"; +const VALIDATOR_ADDRESSES_KEY: &str = "validator_addresses"; +#[allow(missing_docs)] +pub const VALIDATOR_STORAGE_PREFIX: &str = "validator"; +const VALIDATOR_ADDRESS_RAW_HASH: &str = "address_raw_hash"; +const VALIDATOR_CONSENSUS_KEY_STORAGE_KEY: &str = "consensus_key"; +const VALIDATOR_ETH_COLD_KEY_STORAGE_KEY: &str = "eth_cold_key"; +const VALIDATOR_ETH_HOT_KEY_STORAGE_KEY: &str = "eth_hot_key"; +const VALIDATOR_STATE_STORAGE_KEY: &str = "state"; +const VALIDATOR_DELTAS_STORAGE_KEY: &str = "deltas"; +const VALIDATOR_COMMISSION_RATE_STORAGE_KEY: &str = "commission_rate"; +const VALIDATOR_MAX_COMMISSION_CHANGE_STORAGE_KEY: &str = + "max_commission_rate_change"; +const VALIDATOR_REWARDS_PRODUCT_KEY: &str = "validator_rewards_product"; +const VALIDATOR_LAST_KNOWN_PRODUCT_EPOCH_KEY: &str = + "last_known_rewards_product_epoch"; +const SLASHES_PREFIX: &str = "slash"; +const ENQUEUED_SLASHES_KEY: &str = "enqueued_slashes"; +const VALIDATOR_LAST_SLASH_EPOCH: &str = "last_slash_epoch"; +const BOND_STORAGE_KEY: &str = "bond"; +const UNBOND_STORAGE_KEY: &str = "unbond"; +const VALIDATOR_TOTAL_BONDED_STORAGE_KEY: &str = "total_bonded"; +const VALIDATOR_TOTAL_UNBONDED_STORAGE_KEY: &str = "total_unbonded"; +const VALIDATOR_SETS_STORAGE_PREFIX: &str = "validator_sets"; +const CONSENSUS_VALIDATOR_SET_STORAGE_KEY: &str = "consensus"; +const BELOW_CAPACITY_VALIDATOR_SET_STORAGE_KEY: &str = "below_capacity"; +const TOTAL_CONSENSUS_STAKE_STORAGE_KEY: &str = "total_consensus_stake"; +const TOTAL_DELTAS_STORAGE_KEY: &str = "total_deltas"; +const VALIDATOR_SET_POSITIONS_KEY: &str = "validator_set_positions"; +const CONSENSUS_KEYS: &str = "consensus_keys"; +const LAST_BLOCK_PROPOSER_STORAGE_KEY: &str = "last_block_proposer"; +const CONSENSUS_VALIDATOR_SET_ACCUMULATOR_STORAGE_KEY: &str = + "validator_rewards_accumulator"; +const LAST_REWARD_CLAIM_EPOCH: &str = "last_reward_claim_epoch"; +const REWARDS_COUNTER_KEY: &str = "validator_rewards_commissions"; +const VALIDATOR_INCOMING_REDELEGATIONS_KEY: &str = "incoming_redelegations"; +const VALIDATOR_OUTGOING_REDELEGATIONS_KEY: &str = "outgoing_redelegations"; +const VALIDATOR_TOTAL_REDELEGATED_BONDED_KEY: &str = "total_redelegated_bonded"; +const VALIDATOR_TOTAL_REDELEGATED_UNBONDED_KEY: &str = + "total_redelegated_unbonded"; +const DELEGATOR_REDELEGATED_BONDS_KEY: &str = "delegator_redelegated_bonds"; +const DELEGATOR_REDELEGATED_UNBONDS_KEY: &str = "delegator_redelegated_unbonds"; +const VALIDATOR_EMAIL_KEY: &str = "email"; +const VALIDATOR_DESCRIPTION_KEY: &str = "description"; +const VALIDATOR_WEBSITE_KEY: &str = "website"; +const VALIDATOR_DISCORD_KEY: &str = "discord_handle"; +const LIVENESS_PREFIX: &str = "liveness"; +const LIVENESS_MISSED_VOTES: &str = "missed_votes"; +const LIVENESS_MISSED_VOTES_SUM: &str = "sum_missed_votes"; + +/// Is the given key a PoS storage key? +pub fn is_pos_key(key: &Key) -> bool { + match &key.segments.get(0) { + Some(DbKeySeg::AddressSeg(addr)) => addr == &ADDRESS, + _ => false, + } +} + +/// Storage key for PoS parameters. +pub fn params_key() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&PARAMS_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for PoS parameters? +pub fn is_params_key(key: &Key) -> bool { + matches!(&key.segments[..], [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(key)] if addr == &ADDRESS && key == PARAMS_STORAGE_KEY) +} + +/// Storage key prefix for validator data. +fn validator_prefix(validator: &Address) -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&VALIDATOR_STORAGE_PREFIX.to_owned()) + .expect("Cannot obtain a storage key") + .push(&validator.to_db_key()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for validator's address raw hash for look-up from raw hash of an +/// address to address. +pub fn validator_address_raw_hash_key(raw_hash: impl AsRef) -> Key { + let raw_hash = raw_hash.as_ref().to_owned(); + Key::from(ADDRESS.to_db_key()) + .push(&VALIDATOR_ADDRESS_RAW_HASH.to_owned()) + .expect("Cannot obtain a storage key") + .push(&raw_hash) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for validator's address raw hash? +pub fn is_validator_address_raw_hash_key(key: &Key) -> Option<&str> { + match &key.segments[..] { + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::StringSeg(raw_hash), + ] if addr == &ADDRESS && prefix == VALIDATOR_ADDRESS_RAW_HASH => { + Some(raw_hash) + } + _ => None, + } +} + +/// Storage key for validator's consensus key. +pub fn validator_consensus_key_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_CONSENSUS_KEY_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for validator's consensus key? +pub fn is_validator_consensus_key_key(key: &Key) -> Option<&Address> { + match &key.segments[..] { + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::AddressSeg(validator), + DbKeySeg::StringSeg(key), + ] if addr == &ADDRESS + && prefix == VALIDATOR_STORAGE_PREFIX + && key == VALIDATOR_CONSENSUS_KEY_STORAGE_KEY => + { + Some(validator) + } + _ => None, + } +} + +/// Storage key for validator's eth cold key. +pub fn validator_eth_cold_key_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_ETH_COLD_KEY_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for validator's eth cold key? +pub fn is_validator_eth_cold_key_key(key: &Key) -> Option<&Address> { + match &key.segments[..] { + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::AddressSeg(validator), + DbKeySeg::StringSeg(key), + ] if addr == &ADDRESS + && prefix == VALIDATOR_STORAGE_PREFIX + && key == VALIDATOR_ETH_COLD_KEY_STORAGE_KEY => + { + Some(validator) + } + _ => None, + } +} + +/// Storage key for validator's eth hot key. +pub fn validator_eth_hot_key_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_ETH_HOT_KEY_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for validator's eth hot key? +pub fn is_validator_eth_hot_key_key(key: &Key) -> Option<&Address> { + match &key.segments[..] { + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::AddressSeg(validator), + DbKeySeg::StringSeg(key), + ] if addr == &ADDRESS + && prefix == VALIDATOR_STORAGE_PREFIX + && key == VALIDATOR_ETH_HOT_KEY_STORAGE_KEY => + { + Some(validator) + } + _ => None, + } +} + +/// Storage key for validator's commission rate. +pub fn validator_commission_rate_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_COMMISSION_RATE_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for validator's commission rate? +pub fn is_validator_commission_rate_key( + key: &Key, +) -> Option<(&Address, Epoch)> { + match &key.segments[..] { + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::AddressSeg(validator), + DbKeySeg::StringSeg(key), + DbKeySeg::StringSeg(lazy_map), + DbKeySeg::StringSeg(data), + DbKeySeg::StringSeg(epoch), + ] if addr == &ADDRESS + && prefix == VALIDATOR_STORAGE_PREFIX + && key == VALIDATOR_COMMISSION_RATE_STORAGE_KEY + && lazy_map == LAZY_MAP_SUB_KEY + && data == lazy_map::DATA_SUBKEY => + { + let epoch = Epoch::parse(epoch.clone()) + .expect("Should be able to parse the epoch"); + Some((validator, epoch)) + } + _ => None, + } +} + +/// Storage key for validator's maximum commission rate change per epoch. +pub fn validator_max_commission_rate_change_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_MAX_COMMISSION_CHANGE_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for validator's maximum commission rate change per epoch? +pub fn is_validator_max_commission_rate_change_key( + key: &Key, +) -> Option<&Address> { + match &key.segments[..] { + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::AddressSeg(validator), + DbKeySeg::StringSeg(key), + ] if addr == &ADDRESS + && prefix == VALIDATOR_STORAGE_PREFIX + && key == VALIDATOR_MAX_COMMISSION_CHANGE_STORAGE_KEY => + { + Some(validator) + } + _ => None, + } +} + +/// Is storage key for some piece of validator metadata? +pub fn is_validator_metadata_key(key: &Key) -> Option<&Address> { + match &key.segments[..] { + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::AddressSeg(validator), + DbKeySeg::StringSeg(metadata), + ] if addr == &ADDRESS + && prefix == VALIDATOR_STORAGE_PREFIX + && matches!( + metadata.as_str(), + VALIDATOR_EMAIL_KEY + | VALIDATOR_DESCRIPTION_KEY + | VALIDATOR_WEBSITE_KEY + | VALIDATOR_DISCORD_KEY + ) => + { + Some(validator) + } + _ => None, + } +} + +/// Storage key for validator's rewards products. +pub fn validator_rewards_product_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_REWARDS_PRODUCT_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for validator's rewards products? +pub fn is_validator_rewards_product_key(key: &Key) -> Option<&Address> { + match &key.segments[..] { + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::AddressSeg(validator), + DbKeySeg::StringSeg(key), + ] if addr == &ADDRESS + && prefix == VALIDATOR_STORAGE_PREFIX + && key == VALIDATOR_REWARDS_PRODUCT_KEY => + { + Some(validator) + } + _ => None, + } +} + +/// Storage prefix for rewards counter. +pub fn rewards_counter_prefix() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&REWARDS_COUNTER_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for rewards counter. +pub fn rewards_counter_key(source: &Address, validator: &Address) -> Key { + rewards_counter_prefix() + .push(&source.to_db_key()) + .expect("Cannot obtain a storage key") + .push(&validator.to_db_key()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for a validator's incoming redelegations, where the prefixed +/// validator is the destination validator. +pub fn validator_incoming_redelegations_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_INCOMING_REDELEGATIONS_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for a validator's outgoing redelegations, where the prefixed +/// validator is the source validator. +pub fn validator_outgoing_redelegations_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_OUTGOING_REDELEGATIONS_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for validator's total-redelegated-bonded amount to track for +/// slashing +pub fn validator_total_redelegated_bonded_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_TOTAL_REDELEGATED_BONDED_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for validator's total-redelegated-unbonded amount to track for +/// slashing +pub fn validator_total_redelegated_unbonded_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_TOTAL_REDELEGATED_UNBONDED_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key prefix for all delegators' redelegated bonds. +pub fn delegator_redelegated_bonds_prefix() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&DELEGATOR_REDELEGATED_BONDS_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for a particular delegator's redelegated bond information. +pub fn delegator_redelegated_bonds_key(delegator: &Address) -> Key { + delegator_redelegated_bonds_prefix() + .push(&delegator.to_db_key()) + .expect("Cannot obtain a storage key") +} + +/// Storage key prefix for all delegators' redelegated unbonds. +pub fn delegator_redelegated_unbonds_prefix() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&DELEGATOR_REDELEGATED_UNBONDS_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for a particular delegator's redelegated unbond information. +pub fn delegator_redelegated_unbonds_key(delegator: &Address) -> Key { + delegator_redelegated_unbonds_prefix() + .push(&delegator.to_db_key()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for validator's last known rewards product epoch. +pub fn validator_last_known_product_epoch_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_LAST_KNOWN_PRODUCT_EPOCH_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for validator's last known rewards product epoch? +pub fn is_validator_last_known_product_epoch_key( + key: &Key, +) -> Option<&Address> { + match &key.segments[..] { + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::AddressSeg(validator), + DbKeySeg::StringSeg(key), + ] if addr == &ADDRESS + && prefix == VALIDATOR_STORAGE_PREFIX + && key == VALIDATOR_LAST_KNOWN_PRODUCT_EPOCH_KEY => + { + Some(validator) + } + _ => None, + } +} + +/// Storage key for validator's consensus key. +pub fn validator_state_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_STATE_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for validator's state? +pub fn is_validator_state_key(key: &Key) -> Option<(&Address, Epoch)> { + match &key.segments[..] { + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::AddressSeg(validator), + DbKeySeg::StringSeg(key), + DbKeySeg::StringSeg(lazy_map), + DbKeySeg::StringSeg(data), + DbKeySeg::StringSeg(epoch), + ] if addr == &ADDRESS + && prefix == VALIDATOR_STORAGE_PREFIX + && key == VALIDATOR_STATE_STORAGE_KEY + && lazy_map == LAZY_MAP_SUB_KEY + && data == lazy_map::DATA_SUBKEY => + { + let epoch = Epoch::parse(epoch.clone()) + .expect("Should be able to parse the epoch"); + Some((validator, epoch)) + } + _ => None, + } +} + +/// Storage key for validator's deltas. +pub fn validator_deltas_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_DELTAS_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for validator's total deltas? +pub fn is_validator_deltas_key(key: &Key) -> Option<&Address> { + match &key.segments[..] { + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::AddressSeg(validator), + DbKeySeg::StringSeg(key), + DbKeySeg::StringSeg(lazy_map), + DbKeySeg::StringSeg(data), + DbKeySeg::StringSeg(_epoch), + ] if addr == &ADDRESS + && prefix == VALIDATOR_STORAGE_PREFIX + && key == VALIDATOR_DELTAS_STORAGE_KEY + && lazy_map == LAZY_MAP_SUB_KEY + && data == lazy_map::DATA_SUBKEY => + { + Some(validator) + } + _ => None, + } +} + +/// Storage prefix for all active validators (consensus, below-capacity, jailed) +pub fn validator_addresses_key() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&VALIDATOR_ADDRESSES_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage prefix for slashes. +pub fn slashes_prefix() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&SLASHES_PREFIX.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for all slashes. +pub fn enqueued_slashes_key() -> Key { + // slashes_prefix() + Key::from(ADDRESS.to_db_key()) + .push(&ENQUEUED_SLASHES_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for validator's slashes. +pub fn validator_slashes_key(validator: &Address) -> Key { + slashes_prefix() + .push(&validator.to_db_key()) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for a validator's slashes +pub fn is_validator_slashes_key(key: &Key) -> Option
{ + if key.segments.len() >= 5 { + match &key.segments[..] { + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::AddressSeg(validator), + DbKeySeg::StringSeg(data), + DbKeySeg::StringSeg(_index), + ] if addr == &ADDRESS + && prefix == SLASHES_PREFIX + && data == lazy_vec::DATA_SUBKEY => + { + Some(validator.clone()) + } + _ => None, + } + } else { + None + } +} + +/// Storage key for the last (most recent) epoch in which a slashable offense +/// was detected for a given validator +pub fn validator_last_slash_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_LAST_SLASH_EPOCH.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key prefix for all bonds. +pub fn bonds_prefix() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&BOND_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key prefix for all bonds of the given source address. +pub fn bonds_for_source_prefix(source: &Address) -> Key { + bonds_prefix() + .push(&source.to_db_key()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for a bond with the given ID (source and validator). +pub fn bond_key(bond_id: &BondId) -> Key { + bonds_for_source_prefix(&bond_id.source) + .push(&bond_id.validator.to_db_key()) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for a bond? Returns the bond ID and bond start epoch if so. +pub fn is_bond_key(key: &Key) -> Option<(BondId, Epoch)> { + if key.segments.len() >= 7 { + match &key.segments[..7] { + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::AddressSeg(source), + DbKeySeg::AddressSeg(validator), + DbKeySeg::StringSeg(lazy_map), + DbKeySeg::StringSeg(data), + DbKeySeg::StringSeg(epoch_str), + ] if addr == &ADDRESS + && prefix == BOND_STORAGE_KEY + && lazy_map == crate::epoched::LAZY_MAP_SUB_KEY + && data == lazy_map::DATA_SUBKEY => + { + let start = Epoch::parse(epoch_str.clone()).ok()?; + Some(( + BondId { + source: source.clone(), + validator: validator.clone(), + }, + start, + )) + } + _ => None, + } + } else { + None + } +} + +/// Storage key for the total bonds for a given validator. +pub fn validator_total_bonded_key(validator: &Address) -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&VALIDATOR_TOTAL_BONDED_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") + .push(&validator.to_db_key()) + .expect("Cannot obtain a storage key") +} + +/// Storage key prefix for all unbonds. +pub fn unbonds_prefix() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&UNBOND_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key prefix for all unbonds of the given source address. +pub fn unbonds_for_source_prefix(source: &Address) -> Key { + unbonds_prefix() + .push(&source.to_db_key()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for an unbond with the given ID (source and validator). +pub fn unbond_key(bond_id: &BondId) -> Key { + unbonds_for_source_prefix(&bond_id.source) + .push(&bond_id.validator.to_db_key()) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for an unbond? Returns the bond ID and unbond start and +/// withdraw epoch if it is. +pub fn is_unbond_key(key: &Key) -> Option<(BondId, Epoch, Epoch)> { + if key.segments.len() >= 8 { + match &key.segments[..8] { + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::AddressSeg(source), + DbKeySeg::AddressSeg(validator), + DbKeySeg::StringSeg(data_1), + DbKeySeg::StringSeg(start_epoch_str), + DbKeySeg::StringSeg(data_2), + DbKeySeg::StringSeg(withdraw_epoch_str), + ] if addr == &ADDRESS + && prefix == UNBOND_STORAGE_KEY + && data_1 == lazy_map::DATA_SUBKEY + && data_2 == lazy_map::DATA_SUBKEY => + { + let withdraw = Epoch::parse(withdraw_epoch_str.clone()).ok()?; + let start = Epoch::parse(start_epoch_str.clone()).ok()?; + Some(( + BondId { + source: source.clone(), + validator: validator.clone(), + }, + start, + withdraw, + )) + } + _ => None, + } + } else { + None + } +} + +/// Storage key for validator's total-unbonded amount to track for slashing +pub fn validator_total_unbonded_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_TOTAL_UNBONDED_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage prefix for validator sets. +pub fn validator_sets_prefix() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&VALIDATOR_SETS_STORAGE_PREFIX.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for consensus validator set +pub fn consensus_validator_set_key() -> Key { + validator_sets_prefix() + .push(&CONSENSUS_VALIDATOR_SET_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for below-capacity validator set +pub fn below_capacity_validator_set_key() -> Key { + validator_sets_prefix() + .push(&BELOW_CAPACITY_VALIDATOR_SET_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for the consensus validator set? +pub fn is_consensus_validator_set_key(key: &Key) -> bool { + matches!(&key.segments[..], [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(key), DbKeySeg::StringSeg(set_type), DbKeySeg::StringSeg(lazy_map), DbKeySeg::StringSeg(data), DbKeySeg::StringSeg(_epoch), DbKeySeg::StringSeg(_), DbKeySeg::StringSeg(_amount), DbKeySeg::StringSeg(_), DbKeySeg::StringSeg(_position)] if addr == &ADDRESS && key == VALIDATOR_SETS_STORAGE_PREFIX && set_type == CONSENSUS_VALIDATOR_SET_STORAGE_KEY && lazy_map == LAZY_MAP_SUB_KEY && data == lazy_map::DATA_SUBKEY) +} + +/// Is storage key for the below-capacity validator set? +pub fn is_below_capacity_validator_set_key(key: &Key) -> bool { + matches!(&key.segments[..], [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(key), DbKeySeg::StringSeg(set_type), DbKeySeg::StringSeg(lazy_map), DbKeySeg::StringSeg(data), DbKeySeg::StringSeg(_epoch), DbKeySeg::StringSeg(_), DbKeySeg::StringSeg(_amount), DbKeySeg::StringSeg(_), DbKeySeg::StringSeg(_position)] if addr == &ADDRESS && key == VALIDATOR_SETS_STORAGE_PREFIX && set_type == BELOW_CAPACITY_VALIDATOR_SET_STORAGE_KEY && lazy_map == LAZY_MAP_SUB_KEY && data == lazy_map::DATA_SUBKEY) +} + +/// Storage key for total consensus stake +pub fn total_consensus_stake_key() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&TOTAL_CONSENSUS_STAKE_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a total consensus stake key") +} + +/// Is storage key for the total consensus stake? +pub fn is_total_consensus_stake_key(key: &Key) -> bool { + matches!(&key.segments[..], [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(key) + ] if addr == &ADDRESS && key == TOTAL_CONSENSUS_STAKE_STORAGE_KEY) +} + +/// Storage key for total deltas of all validators. +pub fn total_deltas_key() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&TOTAL_DELTAS_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for total deltas of all validators? +pub fn is_total_deltas_key(key: &Key) -> Option<&String> { + match &key.segments[..] { + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(key), + DbKeySeg::StringSeg(lazy_map), + DbKeySeg::StringSeg(data), + DbKeySeg::StringSeg(epoch), + ] if addr == &ADDRESS + && key == TOTAL_DELTAS_STORAGE_KEY + && lazy_map == LAZY_MAP_SUB_KEY + && data == lazy_map::DATA_SUBKEY => + { + Some(epoch) + } + _ => None, + } +} + +/// Storage key for block proposer address of the previous block. +pub fn last_block_proposer_key() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&LAST_BLOCK_PROPOSER_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for block proposer address of the previous block? +pub fn is_last_block_proposer_key(key: &Key) -> bool { + matches!(&key.segments[..], [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(key)] if addr == &ADDRESS && key == LAST_BLOCK_PROPOSER_STORAGE_KEY) +} + +/// Storage key for the consensus validator set rewards accumulator. +pub fn consensus_validator_rewards_accumulator_key() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&CONSENSUS_VALIDATOR_SET_ACCUMULATOR_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for the consensus validator set? +pub fn is_consensus_validator_set_accumulator_key(key: &Key) -> bool { + matches!(&key.segments[..], [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(key), + ] if addr == &ADDRESS + && key == CONSENSUS_VALIDATOR_SET_ACCUMULATOR_STORAGE_KEY) +} + +/// Storage prefix for epoch at which an account last claimed PoS inflationary +/// rewards. +pub fn last_pos_reward_claim_epoch_prefix() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&LAST_REWARD_CLAIM_EPOCH.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for epoch at which an account last claimed PoS inflationary +/// rewards. +pub fn last_pos_reward_claim_epoch_key( + delegator: &Address, + validator: &Address, +) -> Key { + last_pos_reward_claim_epoch_prefix() + .push(&delegator.to_db_key()) + .expect("Cannot obtain a storage key") + .push(&validator.to_db_key()) + .expect("Cannot obtain a storage key") +} + +/// Get validator address from bond key +pub fn get_validator_address_from_bond(key: &Key) -> Option
{ + match key.get_at(3) { + Some(segment) => match segment { + DbKeySeg::AddressSeg(addr) => Some(addr.clone()), + DbKeySeg::StringSeg(_) => None, + }, + None => None, + } +} + +/// Storage key for validator set positions +pub fn validator_set_positions_key() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&VALIDATOR_SET_POSITIONS_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for consensus keys set. +pub fn consensus_keys_key() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&CONSENSUS_KEYS.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for consensus keys set? +pub fn is_consensus_keys_key(key: &Key) -> bool { + matches!(&key.segments[..], [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(key)] if addr == &ADDRESS && key == CONSENSUS_KEYS) +} + +/// Storage key for a validator's email +pub fn validator_email_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_EMAIL_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for a validator's description +pub fn validator_description_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_DESCRIPTION_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for a validator's website +pub fn validator_website_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_WEBSITE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for a validator's discord handle +pub fn validator_discord_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_DISCORD_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage prefix for the liveness data of the cosnensus validator set. +pub fn liveness_data_prefix() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&LIVENESS_PREFIX.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for the liveness records. +pub fn liveness_missed_votes_key() -> Key { + liveness_data_prefix() + .push(&LIVENESS_MISSED_VOTES.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for the liveness data. +pub fn liveness_sum_missed_votes_key() -> Key { + liveness_data_prefix() + .push(&LIVENESS_MISSED_VOTES_SUM.to_owned()) + .expect("Cannot obtain a storage key") +} diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs deleted file mode 100644 index cd25d2fed5..0000000000 --- a/proof_of_stake/src/tests.rs +++ /dev/null @@ -1,6999 +0,0 @@ -//! PoS system tests - -mod state_machine; -mod state_machine_v2; -mod utils; - -use std::cmp::{max, min}; -use std::collections::{BTreeMap, BTreeSet, HashSet}; -use std::ops::{Deref, Range}; -use std::str::FromStr; - -use assert_matches::assert_matches; -use namada_core::ledger::storage::testing::TestWlStorage; -use namada_core::ledger::storage::WlStorage; -use namada_core::ledger::storage_api::collections::lazy_map::{ - self, Collectable, NestedMap, -}; -use namada_core::ledger::storage_api::collections::LazyCollection; -use namada_core::ledger::storage_api::token::{credit_tokens, read_balance}; -use namada_core::ledger::storage_api::StorageRead; -use namada_core::types::address::testing::{ - address_from_simple_seed, arb_established_address, established_address_1, - established_address_2, established_address_3, -}; -use namada_core::types::address::{Address, EstablishedAddressGen}; -use namada_core::types::dec::Dec; -use namada_core::types::key::common::{PublicKey, SecretKey}; -use namada_core::types::key::testing::{ - arb_common_keypair, common_sk_from_simple_seed, gen_keypair, -}; -use namada_core::types::key::RefTo; -use namada_core::types::storage::{BlockHeight, Epoch, Key}; -use namada_core::types::token::testing::arb_amount_non_zero_ceiled; -use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; -use namada_core::types::{address, key, token}; -use proptest::prelude::*; -use proptest::test_runner::Config; -// Use `RUST_LOG=info` (or another tracing level) and `--nocapture` to see -// `tracing` logs from tests -use test_log::test; - -use crate::epoched::DEFAULT_NUM_PAST_EPOCHS; -use crate::parameters::testing::arb_pos_params; -use crate::parameters::{OwnedPosParams, PosParams}; -use crate::rewards::PosRewardsCalculator; -use crate::test_utils::{init_genesis_helper, test_init_genesis}; -use crate::types::{ - into_tm_voting_power, BondDetails, BondId, BondsAndUnbondsDetails, - ConsensusValidator, EagerRedelegatedBondsMap, GenesisValidator, Position, - RedelegatedTokens, ReverseOrdTokenAmount, Slash, SlashType, UnbondDetails, - ValidatorSetUpdate, ValidatorState, VoteInfo, WeightedValidator, -}; -use crate::{ - apply_list_slashes, become_validator, below_capacity_validator_set_handle, - bond_handle, bond_tokens, bonds_and_unbonds, change_consensus_key, - compute_amount_after_slashing_unbond, - compute_amount_after_slashing_withdraw, - compute_and_store_total_consensus_stake, compute_bond_at_epoch, - compute_modified_redelegation, compute_new_redelegated_unbonds, - compute_slash_bond_at_epoch, compute_slashable_amount, - consensus_validator_set_handle, copy_validator_sets_and_positions, - delegator_redelegated_bonds_handle, delegator_redelegated_unbonds_handle, - find_bonds_to_remove, find_validator_by_raw_hash, - fold_and_slash_redelegated_bonds, get_consensus_key_set, - get_num_consensus_validators, insert_validator_into_validator_set, - is_validator, process_slashes, - read_below_capacity_validator_set_addresses_with_stake, - read_below_threshold_validator_set_addresses, - read_consensus_validator_set_addresses_with_stake, read_total_stake, - read_validator_deltas_value, read_validator_stake, slash, - slash_redelegation, slash_validator, slash_validator_redelegation, - staking_token_address, total_bonded_handle, total_deltas_handle, - total_unbonded_handle, unbond_handle, unbond_tokens, unjail_validator, - update_validator_deltas, update_validator_set, validator_addresses_handle, - validator_consensus_key_handle, validator_incoming_redelegations_handle, - validator_outgoing_redelegations_handle, validator_set_positions_handle, - validator_set_update_tendermint, validator_slashes_handle, - validator_state_handle, validator_total_redelegated_bonded_handle, - validator_total_redelegated_unbonded_handle, withdraw_tokens, - write_pos_params, write_validator_address_raw_hash, BecomeValidator, - EagerRedelegatedUnbonds, FoldRedelegatedBondsResult, ModifiedRedelegation, - RedelegationError, -}; - -proptest! { - // Generate arb valid input for `test_test_init_genesis_aux` - #![proptest_config(Config { - cases: 100, - .. Config::default() - })] - #[test] - fn test_test_init_genesis( - - (pos_params, genesis_validators) in arb_params_and_genesis_validators(Some(5), 1..10), - start_epoch in (0_u64..1000).prop_map(Epoch), - - ) { - test_test_init_genesis_aux(pos_params, start_epoch, genesis_validators) - } -} - -proptest! { - // Generate arb valid input for `test_bonds_aux` - #![proptest_config(Config { - cases: 100, - .. Config::default() - })] - #[test] - fn test_bonds( - - (pos_params, genesis_validators) in arb_params_and_genesis_validators(Some(5), 1..3), - - ) { - test_bonds_aux(pos_params, genesis_validators) - } -} - -proptest! { - // Generate arb valid input for `test_become_validator_aux` - #![proptest_config(Config { - cases: 100, - .. Config::default() - })] - #[test] - fn test_become_validator( - - (pos_params, genesis_validators) in arb_params_and_genesis_validators(Some(5), 1..3), - new_validator in arb_established_address().prop_map(Address::Established), - new_validator_consensus_key in arb_common_keypair(), - - ) { - test_become_validator_aux(pos_params, new_validator, - new_validator_consensus_key, genesis_validators) - } -} - -proptest! { - // Generate arb valid input for `test_purge_validator_information_aux` - #![proptest_config(Config { - cases: 1, - .. Config::default() - })] - #[test] - fn test_purge_validator_information( - - genesis_validators in arb_genesis_validators(4..5, None), - - ) { - test_purge_validator_information_aux( genesis_validators) - } -} - -proptest! { - // Generate arb valid input for `test_slashes_with_unbonding_aux` - #![proptest_config(Config { - cases: 100, - .. Config::default() - })] - #[test] - fn test_slashes_with_unbonding( - (params, genesis_validators, unbond_delay) - in test_slashes_with_unbonding_params() - ) { - test_slashes_with_unbonding_aux( - params, genesis_validators, unbond_delay) - } -} - -proptest! { - // Generate arb valid input for `test_unjail_validator_aux` - #![proptest_config(Config { - cases: 100, - .. Config::default() - })] - #[test] - fn test_unjail_validator( - (pos_params, genesis_validators) - in arb_params_and_genesis_validators(Some(4),6..9) - ) { - test_unjail_validator_aux(pos_params, - genesis_validators) - } -} - -proptest! { - // Generate arb valid input for `test_simple_redelegation_aux` - #![proptest_config(Config { - cases: 100, - .. Config::default() - })] - #[test] - fn test_simple_redelegation( - - genesis_validators in arb_genesis_validators(2..4, None), - (amount_delegate, amount_redelegate, amount_unbond) in arb_redelegation_amounts(20) - - ) { - test_simple_redelegation_aux(genesis_validators, amount_delegate, amount_redelegate, amount_unbond) - } -} - -proptest! { - // Generate arb valid input for `test_simple_redelegation_aux` - #![proptest_config(Config { - cases: 100, - .. Config::default() - })] - #[test] - fn test_redelegation_with_slashing( - - genesis_validators in arb_genesis_validators(2..4, None), - (amount_delegate, amount_redelegate, amount_unbond) in arb_redelegation_amounts(20) - - ) { - test_redelegation_with_slashing_aux(genesis_validators, amount_delegate, amount_redelegate, amount_unbond) - } -} - -proptest! { - // Generate arb valid input for `test_chain_redelegations_aux` - #![proptest_config(Config { - cases: 100, - .. Config::default() - })] - #[test] - fn test_chain_redelegations( - - genesis_validators in arb_genesis_validators(3..4, None), - - ) { - test_chain_redelegations_aux(genesis_validators) - } -} - -proptest! { - // Generate arb valid input for `test_overslashing_aux` - #![proptest_config(Config { - cases: 1, - .. Config::default() - })] - #[test] - fn test_overslashing( - - genesis_validators in arb_genesis_validators(4..5, None), - - ) { - test_overslashing_aux(genesis_validators) - } -} - -proptest! { - // Generate arb valid input for `test_unslashed_bond_amount_aux` - #![proptest_config(Config { - cases: 1, - .. Config::default() - })] - #[test] - fn test_unslashed_bond_amount( - - genesis_validators in arb_genesis_validators(4..5, None), - - ) { - test_unslashed_bond_amount_aux(genesis_validators) - } -} - -proptest! { - // Generate arb valid input for `test_slashed_bond_amount_aux` - #![proptest_config(Config { - cases: 1, - .. Config::default() - })] - #[test] - fn test_slashed_bond_amount( - - genesis_validators in arb_genesis_validators(4..5, None), - - ) { - test_slashed_bond_amount_aux(genesis_validators) - } -} - -proptest! { - // Generate arb valid input for `test_log_block_rewards_aux` - #![proptest_config(Config { - cases: 1, - .. Config::default() - })] - #[test] - fn test_log_block_rewards( - genesis_validators in arb_genesis_validators(4..10, None), - params in arb_pos_params(Some(5)) - - ) { - test_log_block_rewards_aux(genesis_validators, params) - } -} - -proptest! { - // Generate arb valid input for `test_update_rewards_products_aux` - #![proptest_config(Config { - cases: 1, - .. Config::default() - })] - #[test] - fn test_update_rewards_products( - genesis_validators in arb_genesis_validators(4..10, None), - - ) { - test_update_rewards_products_aux(genesis_validators) - } -} - -proptest! { - // Generate arb valid input for `test_consensus_key_change` - #![proptest_config(Config { - cases: 1, - .. Config::default() - })] - #[test] - fn test_consensus_key_change( - - genesis_validators in arb_genesis_validators(1..2, None), - - ) { - test_consensus_key_change_aux(genesis_validators) - } -} - -proptest! { - // Generate arb valid input for `test_is_delegator` - #![proptest_config(Config { - cases: 100, - .. Config::default() - })] - #[test] - fn test_is_delegator( - - genesis_validators in arb_genesis_validators(2..3, None), - - ) { - test_is_delegator_aux(genesis_validators) - } -} - -fn arb_params_and_genesis_validators( - num_max_validator_slots: Option, - val_size: Range, -) -> impl Strategy)> { - let params = arb_pos_params(num_max_validator_slots); - params.prop_flat_map(move |params| { - let validators = arb_genesis_validators( - val_size.clone(), - Some(params.validator_stake_threshold), - ); - (Just(params), validators) - }) -} - -fn test_slashes_with_unbonding_params() --> impl Strategy, u64)> { - let params = arb_pos_params(Some(5)); - params.prop_flat_map(|params| { - let unbond_delay = 0..(params.slash_processing_epoch_offset() * 2); - // Must have at least 4 validators so we can slash one and the cubic - // slash rate will be less than 100% - let validators = arb_genesis_validators(4..10, None); - (Just(params), validators, unbond_delay) - }) -} - -/// Test genesis initialization -fn test_test_init_genesis_aux( - params: OwnedPosParams, - start_epoch: Epoch, - mut validators: Vec, -) { - println!( - "Test inputs: {params:?}, {start_epoch}, genesis validators: \ - {validators:#?}" - ); - let mut s = TestWlStorage::default(); - s.storage.block.epoch = start_epoch; - - validators.sort_by(|a, b| b.tokens.cmp(&a.tokens)); - let params = test_init_genesis( - &mut s, - params, - validators.clone().into_iter(), - start_epoch, - ) - .unwrap(); - - let mut bond_details = bonds_and_unbonds(&s, None, None).unwrap(); - assert!(bond_details.iter().all(|(_id, details)| { - details.unbonds.is_empty() && details.slashes.is_empty() - })); - - for (i, validator) in validators.into_iter().enumerate() { - let addr = &validator.address; - let self_bonds = bond_details - .remove(&BondId { - source: addr.clone(), - validator: addr.clone(), - }) - .unwrap(); - assert_eq!(self_bonds.bonds.len(), 1); - assert_eq!( - self_bonds.bonds[0], - BondDetails { - start: start_epoch, - amount: validator.tokens, - slashed_amount: None, - } - ); - - let state = validator_state_handle(&validator.address) - .get(&s, start_epoch, ¶ms) - .unwrap(); - if (i as u64) < params.max_validator_slots - && validator.tokens >= params.validator_stake_threshold - { - // should be in consensus set - let handle = consensus_validator_set_handle().at(&start_epoch); - assert!(handle.at(&validator.tokens).iter(&s).unwrap().any( - |result| { - let (_pos, addr) = result.unwrap(); - addr == validator.address - } - )); - assert_eq!(state, Some(ValidatorState::Consensus)); - } else if validator.tokens >= params.validator_stake_threshold { - // Should be in below-capacity set if its tokens are greater than - // `validator_stake_threshold` - let handle = below_capacity_validator_set_handle().at(&start_epoch); - assert!(handle.at(&validator.tokens.into()).iter(&s).unwrap().any( - |result| { - let (_pos, addr) = result.unwrap(); - addr == validator.address - } - )); - assert_eq!(state, Some(ValidatorState::BelowCapacity)); - } else { - // Should be in below-threshold - let bt_addresses = - read_below_threshold_validator_set_addresses(&s, start_epoch) - .unwrap(); - assert!( - bt_addresses - .into_iter() - .any(|addr| { addr == validator.address }) - ); - assert_eq!(state, Some(ValidatorState::BelowThreshold)); - } - } -} - -/// Test bonding -/// NOTE: copy validator sets each time we advance the epoch -fn test_bonds_aux(params: OwnedPosParams, validators: Vec) { - // This can be useful for debugging: - // params.pipeline_len = 2; - // params.unbonding_len = 4; - println!("\nTest inputs: {params:?}, genesis validators: {validators:#?}"); - let mut s = TestWlStorage::default(); - - // Genesis - let start_epoch = s.storage.block.epoch; - let mut current_epoch = s.storage.block.epoch; - let params = test_init_genesis( - &mut s, - params, - validators.clone().into_iter(), - current_epoch, - ) - .unwrap(); - s.commit_block().unwrap(); - - // Advance to epoch 1 - current_epoch = advance_epoch(&mut s, ¶ms); - let self_bond_epoch = current_epoch; - - let validator = validators.first().unwrap(); - - // Read some data before submitting bond - let pipeline_epoch = current_epoch + params.pipeline_len; - let staking_token = staking_token_address(&s); - let pos_balance_pre = s - .read::(&token::balance_key( - &staking_token, - &super::ADDRESS, - )) - .unwrap() - .unwrap_or_default(); - let total_stake_before = - read_total_stake(&s, ¶ms, pipeline_epoch).unwrap(); - - // Self-bond - let amount_self_bond = token::Amount::from_uint(100_500_000, 0).unwrap(); - credit_tokens(&mut s, &staking_token, &validator.address, amount_self_bond) - .unwrap(); - bond_tokens( - &mut s, - None, - &validator.address, - amount_self_bond, - current_epoch, - None, - ) - .unwrap(); - - // Check the bond delta - let self_bond = bond_handle(&validator.address, &validator.address); - let delta = self_bond.get_delta_val(&s, pipeline_epoch).unwrap(); - assert_eq!(delta, Some(amount_self_bond)); - - // Check the validator in the validator set - let set = - read_consensus_validator_set_addresses_with_stake(&s, pipeline_epoch) - .unwrap(); - assert!(set.into_iter().any( - |WeightedValidator { - bonded_stake, - address, - }| { - address == validator.address - && bonded_stake == validator.tokens + amount_self_bond - } - )); - - let val_deltas = - read_validator_deltas_value(&s, &validator.address, &pipeline_epoch) - .unwrap(); - assert_eq!(val_deltas, Some(amount_self_bond.change())); - - let total_deltas_handle = total_deltas_handle(); - assert_eq!( - current_epoch, - total_deltas_handle.get_last_update(&s).unwrap().unwrap() - ); - let total_stake_after = - read_total_stake(&s, ¶ms, pipeline_epoch).unwrap(); - assert_eq!(total_stake_before + amount_self_bond, total_stake_after); - - // Check bond details after self-bond - let self_bond_id = BondId { - source: validator.address.clone(), - validator: validator.address.clone(), - }; - let check_bond_details = |ix, bond_details: BondsAndUnbondsDetails| { - println!("Check index {ix}"); - let details = bond_details.get(&self_bond_id).unwrap(); - assert_eq!( - details.bonds.len(), - 2, - "Contains genesis and newly added self-bond" - ); - dbg!(&details.bonds); - assert_eq!( - details.bonds[0], - BondDetails { - start: start_epoch, - amount: validator.tokens, - slashed_amount: None - }, - ); - assert_eq!( - details.bonds[1], - BondDetails { - start: pipeline_epoch, - amount: amount_self_bond, - slashed_amount: None - }, - ); - }; - // Try to call it with different combinations of owner/validator args - check_bond_details(0, bonds_and_unbonds(&s, None, None).unwrap()); - check_bond_details( - 1, - bonds_and_unbonds(&s, Some(validator.address.clone()), None).unwrap(), - ); - check_bond_details( - 2, - bonds_and_unbonds(&s, None, Some(validator.address.clone())).unwrap(), - ); - check_bond_details( - 3, - bonds_and_unbonds( - &s, - Some(validator.address.clone()), - Some(validator.address.clone()), - ) - .unwrap(), - ); - - // Get a non-validating account with tokens - let delegator = address::testing::gen_implicit_address(); - let amount_del = token::Amount::from_uint(201_000_000, 0).unwrap(); - credit_tokens(&mut s, &staking_token, &delegator, amount_del).unwrap(); - let balance_key = token::balance_key(&staking_token, &delegator); - let balance = s - .read::(&balance_key) - .unwrap() - .unwrap_or_default(); - assert_eq!(balance, amount_del); - - // Advance to epoch 3 - advance_epoch(&mut s, ¶ms); - current_epoch = advance_epoch(&mut s, ¶ms); - let pipeline_epoch = current_epoch + params.pipeline_len; - - // Delegation - let delegation_epoch = current_epoch; - bond_tokens( - &mut s, - Some(&delegator), - &validator.address, - amount_del, - current_epoch, - None, - ) - .unwrap(); - let val_stake_pre = read_validator_stake( - &s, - ¶ms, - &validator.address, - pipeline_epoch.prev(), - ) - .unwrap(); - let val_stake_post = - read_validator_stake(&s, ¶ms, &validator.address, pipeline_epoch) - .unwrap(); - assert_eq!(validator.tokens + amount_self_bond, val_stake_pre); - assert_eq!( - validator.tokens + amount_self_bond + amount_del, - val_stake_post - ); - let delegation = bond_handle(&delegator, &validator.address); - assert_eq!( - delegation - .get_sum(&s, pipeline_epoch.prev(), ¶ms) - .unwrap() - .unwrap_or_default(), - token::Amount::zero() - ); - assert_eq!( - delegation - .get_sum(&s, pipeline_epoch, ¶ms) - .unwrap() - .unwrap_or_default(), - amount_del - ); - - // Check delegation bonds details after delegation - let delegation_bond_id = BondId { - source: delegator.clone(), - validator: validator.address.clone(), - }; - let check_bond_details = |ix, bond_details: BondsAndUnbondsDetails| { - println!("Check index {ix}"); - assert_eq!(bond_details.len(), 1); - let details = bond_details.get(&delegation_bond_id).unwrap(); - assert_eq!(details.bonds.len(), 1,); - dbg!(&details.bonds); - assert_eq!( - details.bonds[0], - BondDetails { - start: pipeline_epoch, - amount: amount_del, - slashed_amount: None - }, - ); - }; - // Try to call it with different combinations of owner/validator args - check_bond_details( - 0, - bonds_and_unbonds(&s, Some(delegator.clone()), None).unwrap(), - ); - check_bond_details( - 1, - bonds_and_unbonds( - &s, - Some(delegator.clone()), - Some(validator.address.clone()), - ) - .unwrap(), - ); - - // Check all bond details (self-bonds and delegation) - let check_bond_details = |ix, bond_details: BondsAndUnbondsDetails| { - println!("Check index {ix}"); - let self_bond_details = bond_details.get(&self_bond_id).unwrap(); - let delegation_details = bond_details.get(&delegation_bond_id).unwrap(); - assert_eq!( - self_bond_details.bonds.len(), - 2, - "Contains genesis and newly added self-bond" - ); - assert_eq!( - self_bond_details.bonds[0], - BondDetails { - start: start_epoch, - amount: validator.tokens, - slashed_amount: None - }, - ); - assert_eq!(self_bond_details.bonds[1].amount, amount_self_bond); - assert_eq!( - delegation_details.bonds[0], - BondDetails { - start: pipeline_epoch, - amount: amount_del, - slashed_amount: None - }, - ); - }; - // Try to call it with different combinations of owner/validator args - check_bond_details(0, bonds_and_unbonds(&s, None, None).unwrap()); - check_bond_details( - 1, - bonds_and_unbonds(&s, None, Some(validator.address.clone())).unwrap(), - ); - - // Advance to epoch 5 - for _ in 0..2 { - current_epoch = advance_epoch(&mut s, ¶ms); - } - let pipeline_epoch = current_epoch + params.pipeline_len; - - // Unbond the self-bond with an amount that will remove all of the self-bond - // executed after genesis and some of the genesis bond - let amount_self_unbond: token::Amount = - amount_self_bond + (validator.tokens / 2); - // When the difference is 0, only the non-genesis self-bond is unbonded - let unbonded_genesis_self_bond = - amount_self_unbond - amount_self_bond != token::Amount::zero(); - dbg!( - amount_self_unbond, - amount_self_bond, - unbonded_genesis_self_bond - ); - let self_unbond_epoch = s.storage.block.epoch; - - unbond_tokens( - &mut s, - None, - &validator.address, - amount_self_unbond, - current_epoch, - false, - ) - .unwrap(); - - let val_stake_pre = read_validator_stake( - &s, - ¶ms, - &validator.address, - pipeline_epoch.prev(), - ) - .unwrap(); - - let val_stake_post = - read_validator_stake(&s, ¶ms, &validator.address, pipeline_epoch) - .unwrap(); - - let val_delta = - read_validator_deltas_value(&s, &validator.address, &pipeline_epoch) - .unwrap(); - let unbond = unbond_handle(&validator.address, &validator.address); - - assert_eq!(val_delta, Some(-amount_self_unbond.change())); - assert_eq!( - unbond - .at(&Epoch::default()) - .get( - &s, - &(pipeline_epoch - + params.unbonding_len - + params.cubic_slashing_window_length) - ) - .unwrap(), - if unbonded_genesis_self_bond { - Some(amount_self_unbond - amount_self_bond) - } else { - None - } - ); - assert_eq!( - unbond - .at(&(self_bond_epoch + params.pipeline_len)) - .get( - &s, - &(pipeline_epoch - + params.unbonding_len - + params.cubic_slashing_window_length) - ) - .unwrap(), - Some(amount_self_bond) - ); - assert_eq!( - val_stake_pre, - validator.tokens + amount_self_bond + amount_del - ); - assert_eq!( - val_stake_post, - validator.tokens + amount_self_bond + amount_del - amount_self_unbond - ); - - // Check all bond and unbond details (self-bonds and delegation) - let check_bond_details = |ix, bond_details: BondsAndUnbondsDetails| { - println!("Check index {ix}"); - dbg!(&bond_details); - assert_eq!(bond_details.len(), 2); - let self_bond_details = bond_details.get(&self_bond_id).unwrap(); - let delegation_details = bond_details.get(&delegation_bond_id).unwrap(); - assert_eq!( - self_bond_details.bonds.len(), - 1, - "Contains only part of the genesis bond now" - ); - assert_eq!( - self_bond_details.bonds[0], - BondDetails { - start: start_epoch, - amount: validator.tokens + amount_self_bond - - amount_self_unbond, - slashed_amount: None - }, - ); - assert_eq!( - delegation_details.bonds[0], - BondDetails { - start: delegation_epoch + params.pipeline_len, - amount: amount_del, - slashed_amount: None - }, - ); - assert_eq!( - self_bond_details.unbonds.len(), - if unbonded_genesis_self_bond { 2 } else { 1 }, - "Contains a full unbond of the last self-bond and an unbond from \ - the genesis bond" - ); - if unbonded_genesis_self_bond { - assert_eq!( - self_bond_details.unbonds[0], - UnbondDetails { - start: start_epoch, - withdraw: self_unbond_epoch - + params.pipeline_len - + params.unbonding_len - + params.cubic_slashing_window_length, - amount: amount_self_unbond - amount_self_bond, - slashed_amount: None - } - ); - } - assert_eq!( - self_bond_details.unbonds[usize::from(unbonded_genesis_self_bond)], - UnbondDetails { - start: self_bond_epoch + params.pipeline_len, - withdraw: self_unbond_epoch - + params.pipeline_len - + params.unbonding_len - + params.cubic_slashing_window_length, - amount: amount_self_bond, - slashed_amount: None - } - ); - }; - check_bond_details( - 0, - bonds_and_unbonds(&s, None, Some(validator.address.clone())).unwrap(), - ); - - // Unbond delegation - let amount_undel = token::Amount::from_uint(1_000_000, 0).unwrap(); - unbond_tokens( - &mut s, - Some(&delegator), - &validator.address, - amount_undel, - current_epoch, - false, - ) - .unwrap(); - - let val_stake_pre = read_validator_stake( - &s, - ¶ms, - &validator.address, - pipeline_epoch.prev(), - ) - .unwrap(); - let val_stake_post = - read_validator_stake(&s, ¶ms, &validator.address, pipeline_epoch) - .unwrap(); - let val_delta = - read_validator_deltas_value(&s, &validator.address, &pipeline_epoch) - .unwrap(); - let unbond = unbond_handle(&delegator, &validator.address); - - assert_eq!( - val_delta, - Some(-(amount_self_unbond + amount_undel).change()) - ); - assert_eq!( - unbond - .at(&(delegation_epoch + params.pipeline_len)) - .get( - &s, - &(pipeline_epoch - + params.unbonding_len - + params.cubic_slashing_window_length) - ) - .unwrap(), - Some(amount_undel) - ); - assert_eq!( - val_stake_pre, - validator.tokens + amount_self_bond + amount_del - ); - assert_eq!( - val_stake_post, - validator.tokens + amount_self_bond - amount_self_unbond + amount_del - - amount_undel - ); - - let withdrawable_offset = params.unbonding_len - + params.pipeline_len - + params.cubic_slashing_window_length; - - // Advance to withdrawable epoch - for _ in 0..withdrawable_offset { - current_epoch = advance_epoch(&mut s, ¶ms); - } - - dbg!(current_epoch); - - let pos_balance = s - .read::(&token::balance_key( - &staking_token, - &super::ADDRESS, - )) - .unwrap(); - - assert_eq!( - Some(pos_balance_pre + amount_self_bond + amount_del), - pos_balance - ); - - // Withdraw the self-unbond - withdraw_tokens(&mut s, None, &validator.address, current_epoch).unwrap(); - let unbond = unbond_handle(&validator.address, &validator.address); - let unbond_iter = unbond.iter(&s).unwrap().next(); - assert!(unbond_iter.is_none()); - - let pos_balance = s - .read::(&token::balance_key( - &staking_token, - &super::ADDRESS, - )) - .unwrap(); - assert_eq!( - Some( - pos_balance_pre + amount_self_bond - amount_self_unbond - + amount_del - ), - pos_balance - ); - - // Withdraw the delegation unbond - withdraw_tokens( - &mut s, - Some(&delegator), - &validator.address, - current_epoch, - ) - .unwrap(); - let unbond = unbond_handle(&delegator, &validator.address); - let unbond_iter = unbond.iter(&s).unwrap().next(); - assert!(unbond_iter.is_none()); - - let pos_balance = s - .read::(&token::balance_key( - &staking_token, - &super::ADDRESS, - )) - .unwrap(); - assert_eq!( - Some( - pos_balance_pre + amount_self_bond - amount_self_unbond - + amount_del - - amount_undel - ), - pos_balance - ); -} - -/// Test validator initialization. -fn test_become_validator_aux( - params: OwnedPosParams, - new_validator: Address, - new_validator_consensus_key: SecretKey, - validators: Vec, -) { - println!( - "Test inputs: {params:?}, new validator: {new_validator}, genesis \ - validators: {validators:#?}" - ); - - let mut s = TestWlStorage::default(); - - // Genesis - let mut current_epoch = s.storage.block.epoch; - let params = test_init_genesis( - &mut s, - params, - validators.clone().into_iter(), - current_epoch, - ) - .unwrap(); - s.commit_block().unwrap(); - - // Advance to epoch 1 - current_epoch = advance_epoch(&mut s, ¶ms); - - let num_consensus_before = - get_num_consensus_validators(&s, current_epoch + params.pipeline_len) - .unwrap(); - let num_validators_over_thresh = validators - .iter() - .filter(|validator| { - validator.tokens >= params.validator_stake_threshold - }) - .count(); - - assert_eq!( - min( - num_validators_over_thresh as u64, - params.max_validator_slots - ), - num_consensus_before - ); - assert!(!is_validator(&s, &new_validator).unwrap()); - - // Credit the `new_validator` account - let staking_token = staking_token_address(&s); - let amount = token::Amount::from_uint(100_500_000, 0).unwrap(); - // Credit twice the amount as we're gonna bond it in delegation first, then - // self-bond - credit_tokens(&mut s, &staking_token, &new_validator, amount * 2).unwrap(); - - // Add a delegation from `new_validator` to `genesis_validator` - let genesis_validator = &validators.first().unwrap().address; - bond_tokens( - &mut s, - Some(&new_validator), - genesis_validator, - amount, - current_epoch, - None, - ) - .unwrap(); - - let consensus_key = new_validator_consensus_key.to_public(); - let protocol_sk = common_sk_from_simple_seed(0); - let protocol_key = protocol_sk.to_public(); - let eth_hot_key = key::common::PublicKey::Secp256k1( - key::testing::gen_keypair::().ref_to(), - ); - let eth_cold_key = key::common::PublicKey::Secp256k1( - key::testing::gen_keypair::().ref_to(), - ); - - // Try to become a validator - it should fail as there is a delegation - let result = become_validator( - &mut s, - BecomeValidator { - params: ¶ms, - address: &new_validator, - consensus_key: &consensus_key, - protocol_key: &protocol_key, - eth_cold_key: ð_cold_key, - eth_hot_key: ð_hot_key, - current_epoch, - commission_rate: Dec::new(5, 2).expect("Dec creation failed"), - max_commission_rate_change: Dec::new(5, 2) - .expect("Dec creation failed"), - metadata: Default::default(), - offset_opt: None, - }, - ); - assert!(result.is_err()); - assert!(!is_validator(&s, &new_validator).unwrap()); - - // Unbond the delegation - unbond_tokens( - &mut s, - Some(&new_validator), - genesis_validator, - amount, - current_epoch, - false, - ) - .unwrap(); - - // Try to become a validator account again - it should pass now - become_validator( - &mut s, - BecomeValidator { - params: ¶ms, - address: &new_validator, - consensus_key: &consensus_key, - protocol_key: &protocol_key, - eth_cold_key: ð_cold_key, - eth_hot_key: ð_hot_key, - current_epoch, - commission_rate: Dec::new(5, 2).expect("Dec creation failed"), - max_commission_rate_change: Dec::new(5, 2) - .expect("Dec creation failed"), - metadata: Default::default(), - offset_opt: None, - }, - ) - .unwrap(); - assert!(is_validator(&s, &new_validator).unwrap()); - - let num_consensus_after = - get_num_consensus_validators(&s, current_epoch + params.pipeline_len) - .unwrap(); - // The new validator is initialized with no stake and thus is in the - // below-threshold set - assert_eq!(num_consensus_before, num_consensus_after); - - // Advance to epoch 2 - current_epoch = advance_epoch(&mut s, ¶ms); - - // Self-bond to the new validator - bond_tokens(&mut s, None, &new_validator, amount, current_epoch, None) - .unwrap(); - - // Check the bond delta - let bond_handle = bond_handle(&new_validator, &new_validator); - let pipeline_epoch = current_epoch + params.pipeline_len; - let delta = bond_handle.get_delta_val(&s, pipeline_epoch).unwrap(); - assert_eq!(delta, Some(amount)); - - // Check the validator in the validator set - - // If the consensus validator slots are full and all the genesis validators - // have stake GTE the new validator's self-bond amount, the validator should - // be added to the below-capacity set, or the consensus otherwise - if params.max_validator_slots <= validators.len() as u64 - && validators - .iter() - .all(|validator| validator.tokens >= amount) - { - let set = read_below_capacity_validator_set_addresses_with_stake( - &s, - pipeline_epoch, - ) - .unwrap(); - assert!(set.into_iter().any( - |WeightedValidator { - bonded_stake, - address, - }| { - address == new_validator && bonded_stake == amount - } - )); - } else { - let set = read_consensus_validator_set_addresses_with_stake( - &s, - pipeline_epoch, - ) - .unwrap(); - assert!(set.into_iter().any( - |WeightedValidator { - bonded_stake, - address, - }| { - address == new_validator && bonded_stake == amount - } - )); - } - - // Advance to epoch 3 - current_epoch = advance_epoch(&mut s, ¶ms); - - // Unbond the self-bond - unbond_tokens(&mut s, None, &new_validator, amount, current_epoch, false) - .unwrap(); - - let withdrawable_offset = params.unbonding_len + params.pipeline_len; - - // Advance to withdrawable epoch - for _ in 0..withdrawable_offset { - current_epoch = advance_epoch(&mut s, ¶ms); - } - - // Withdraw the self-bond - withdraw_tokens(&mut s, None, &new_validator, current_epoch).unwrap(); -} - -fn test_slashes_with_unbonding_aux( - mut params: OwnedPosParams, - validators: Vec, - unbond_delay: u64, -) { - // This can be useful for debugging: - params.pipeline_len = 2; - params.unbonding_len = 4; - println!("\nTest inputs: {params:?}, genesis validators: {validators:#?}"); - let mut s = TestWlStorage::default(); - - // Find the validator with the least stake to avoid the cubic slash rate - // going to 100% - let validator = - itertools::Itertools::sorted_by_key(validators.iter(), |v| v.tokens) - .next() - .unwrap(); - let val_addr = &validator.address; - let val_tokens = validator.tokens; - println!( - "Validator that will misbehave addr {val_addr}, tokens {}", - val_tokens.to_string_native() - ); - - // Genesis - // let start_epoch = s.storage.block.epoch; - let mut current_epoch = s.storage.block.epoch; - let params = test_init_genesis( - &mut s, - params, - validators.clone().into_iter(), - current_epoch, - ) - .unwrap(); - s.commit_block().unwrap(); - - current_epoch = advance_epoch(&mut s, ¶ms); - super::process_slashes(&mut s, current_epoch).unwrap(); - - // Discover first slash - let slash_0_evidence_epoch = current_epoch; - // let slash_0_processing_epoch = - // slash_0_evidence_epoch + params.slash_processing_epoch_offset(); - let evidence_block_height = BlockHeight(0); // doesn't matter for slashing logic - let slash_0_type = SlashType::DuplicateVote; - slash( - &mut s, - ¶ms, - current_epoch, - slash_0_evidence_epoch, - evidence_block_height, - slash_0_type, - val_addr, - current_epoch.next(), - ) - .unwrap(); - - // Advance to an epoch in which we can unbond - let unfreeze_epoch = - slash_0_evidence_epoch + params.slash_processing_epoch_offset(); - while current_epoch < unfreeze_epoch { - current_epoch = advance_epoch(&mut s, ¶ms); - super::process_slashes(&mut s, current_epoch).unwrap(); - } - - // Advance more epochs randomly from the generated delay - for _ in 0..unbond_delay { - current_epoch = advance_epoch(&mut s, ¶ms); - } - - // Unbond half of the tokens - let unbond_amount = Dec::new(5, 1).unwrap() * val_tokens; - println!("Going to unbond {}", unbond_amount.to_string_native()); - let unbond_epoch = current_epoch; - unbond_tokens(&mut s, None, val_addr, unbond_amount, unbond_epoch, false) - .unwrap(); - - // Discover second slash - let slash_1_evidence_epoch = current_epoch; - // Ensure that both slashes happen before `unbond_epoch + pipeline` - let _slash_1_processing_epoch = - slash_1_evidence_epoch + params.slash_processing_epoch_offset(); - let evidence_block_height = BlockHeight(0); // doesn't matter for slashing logic - let slash_1_type = SlashType::DuplicateVote; - slash( - &mut s, - ¶ms, - current_epoch, - slash_1_evidence_epoch, - evidence_block_height, - slash_1_type, - val_addr, - current_epoch.next(), - ) - .unwrap(); - - // Advance to an epoch in which we can withdraw - let withdraw_epoch = unbond_epoch + params.withdrawable_epoch_offset(); - while current_epoch < withdraw_epoch { - current_epoch = advance_epoch(&mut s, ¶ms); - super::process_slashes(&mut s, current_epoch).unwrap(); - } - let token = staking_token_address(&s); - let val_balance_pre = read_balance(&s, &token, val_addr).unwrap(); - - let bond_id = BondId { - source: val_addr.clone(), - validator: val_addr.clone(), - }; - let binding = - super::bonds_and_unbonds(&s, None, Some(val_addr.clone())).unwrap(); - let details = binding.get(&bond_id).unwrap(); - let exp_withdraw_from_details = details.unbonds[0].amount - - details.unbonds[0].slashed_amount.unwrap_or_default(); - - withdraw_tokens(&mut s, None, val_addr, current_epoch).unwrap(); - - let val_balance_post = read_balance(&s, &token, val_addr).unwrap(); - let withdrawn_tokens = val_balance_post - val_balance_pre; - println!("Withdrew {} tokens", withdrawn_tokens.to_string_native()); - - assert_eq!(exp_withdraw_from_details, withdrawn_tokens); - - let slash_rate_0 = validator_slashes_handle(val_addr) - .get(&s, 0) - .unwrap() - .unwrap() - .rate; - let slash_rate_1 = validator_slashes_handle(val_addr) - .get(&s, 1) - .unwrap() - .unwrap() - .rate; - println!("Slash 0 rate {slash_rate_0}, slash 1 rate {slash_rate_1}"); - - let expected_withdrawn_amount = Dec::from( - (Dec::one() - slash_rate_1) - * (Dec::one() - slash_rate_0) - * unbond_amount, - ); - // Allow some rounding error, 1 NAMNAM per each slash - let rounding_error_tolerance = - Dec::new(2, NATIVE_MAX_DECIMAL_PLACES).unwrap(); - assert!( - dbg!(expected_withdrawn_amount.abs_diff(&Dec::from(withdrawn_tokens))) - <= rounding_error_tolerance - ); - - // TODO: finish once implemented - // let slash_0 = decimal_mult_amount(slash_rate_0, val_tokens); - // let slash_1 = decimal_mult_amount(slash_rate_1, val_tokens - slash_0); - // let expected_slash_pool = slash_0 + slash_1; - // let slash_pool_balance = - // read_balance(&s, &token, &SLASH_POOL_ADDRESS).unwrap(); - // assert_eq!(expected_slash_pool, slash_pool_balance); -} - -#[test] -fn test_validator_raw_hash() { - let mut storage = TestWlStorage::default(); - let address = address::testing::established_address_1(); - let consensus_sk = key::testing::keypair_1(); - let consensus_pk = consensus_sk.to_public(); - let expected_raw_hash = key::tm_consensus_key_raw_hash(&consensus_pk); - - assert!( - find_validator_by_raw_hash(&storage, &expected_raw_hash) - .unwrap() - .is_none() - ); - write_validator_address_raw_hash(&mut storage, &address, &consensus_pk) - .unwrap(); - let found = - find_validator_by_raw_hash(&storage, &expected_raw_hash).unwrap(); - assert_eq!(found, Some(address)); -} - -#[test] -fn test_validator_sets() { - let mut s = TestWlStorage::default(); - // Only 3 consensus validator slots - let params = OwnedPosParams { - max_validator_slots: 3, - ..Default::default() - }; - let addr_seed = "seed"; - let mut address_gen = EstablishedAddressGen::new(addr_seed); - let mut sk_seed = 0; - let mut gen_validator = || { - let res = ( - address_gen.generate_address(addr_seed), - key::testing::common_sk_from_simple_seed(sk_seed).to_public(), - ); - // bump the sk seed - sk_seed += 1; - res - }; - - // Create genesis validators - let ((val1, pk1), stake1) = - (gen_validator(), token::Amount::native_whole(1)); - let ((val2, pk2), stake2) = - (gen_validator(), token::Amount::native_whole(1)); - let ((val3, pk3), stake3) = - (gen_validator(), token::Amount::native_whole(10)); - let ((val4, pk4), stake4) = - (gen_validator(), token::Amount::native_whole(1)); - let ((val5, pk5), stake5) = - (gen_validator(), token::Amount::native_whole(100)); - let ((val6, pk6), stake6) = - (gen_validator(), token::Amount::native_whole(1)); - let ((val7, pk7), stake7) = - (gen_validator(), token::Amount::native_whole(1)); - println!("\nval1: {val1}, {pk1}, {}", stake1.to_string_native()); - println!("val2: {val2}, {pk2}, {}", stake2.to_string_native()); - println!("val3: {val3}, {pk3}, {}", stake3.to_string_native()); - println!("val4: {val4}, {pk4}, {}", stake4.to_string_native()); - println!("val5: {val5}, {pk5}, {}", stake5.to_string_native()); - println!("val6: {val6}, {pk6}, {}", stake6.to_string_native()); - println!("val7: {val7}, {pk7}, {}", stake7.to_string_native()); - - let start_epoch = Epoch::default(); - let epoch = start_epoch; - - let protocol_sk_1 = common_sk_from_simple_seed(0); - let protocol_sk_2 = common_sk_from_simple_seed(1); - - let params = test_init_genesis( - &mut s, - params, - [ - GenesisValidator { - address: val1.clone(), - tokens: stake1, - consensus_key: pk1.clone(), - protocol_key: protocol_sk_1.to_public(), - eth_hot_key: key::common::PublicKey::Secp256k1( - key::testing::gen_keypair::() - .ref_to(), - ), - eth_cold_key: key::common::PublicKey::Secp256k1( - key::testing::gen_keypair::() - .ref_to(), - ), - commission_rate: Dec::new(1, 1).expect("Dec creation failed"), - max_commission_rate_change: Dec::new(1, 1) - .expect("Dec creation failed"), - metadata: Default::default(), - }, - GenesisValidator { - address: val2.clone(), - tokens: stake2, - consensus_key: pk2.clone(), - protocol_key: protocol_sk_2.to_public(), - eth_hot_key: key::common::PublicKey::Secp256k1( - key::testing::gen_keypair::() - .ref_to(), - ), - eth_cold_key: key::common::PublicKey::Secp256k1( - key::testing::gen_keypair::() - .ref_to(), - ), - commission_rate: Dec::new(1, 1).expect("Dec creation failed"), - max_commission_rate_change: Dec::new(1, 1) - .expect("Dec creation failed"), - metadata: Default::default(), - }, - ] - .into_iter(), - epoch, - ) - .unwrap(); - - // A helper to insert a non-genesis validator - let insert_validator = |s: &mut TestWlStorage, - addr, - pk: &PublicKey, - stake: token::Amount, - epoch: Epoch| { - insert_validator_into_validator_set( - s, - ¶ms, - addr, - stake, - epoch, - params.pipeline_len, - ) - .unwrap(); - - update_validator_deltas(s, ¶ms, addr, stake.change(), epoch, None) - .unwrap(); - - // Set their consensus key (needed for - // `validator_set_update_tendermint` fn) - validator_consensus_key_handle(addr) - .set(s, pk.clone(), epoch, params.pipeline_len) - .unwrap(); - }; - - // Advance to EPOCH 1 - // - // We cannot call `get_tendermint_set_updates` for the genesis state as - // `validator_set_update_tendermint` is only called 2 blocks before the - // start of an epoch and so we need to give it a predecessor epoch (see - // `get_tendermint_set_updates`), which we cannot have on the first - // epoch. In any way, the initial validator set is given to Tendermint - // from InitChain, so `validator_set_update_tendermint` is - // not being used for it. - let epoch = advance_epoch(&mut s, ¶ms); - let pipeline_epoch = epoch + params.pipeline_len; - - // Insert another validator with the greater stake 10 NAM - insert_validator(&mut s, &val3, &pk3, stake3, epoch); - // Insert validator with stake 1 NAM - insert_validator(&mut s, &val4, &pk4, stake4, epoch); - - // Validator `val3` and `val4` will be added at pipeline offset (2) - epoch - // 3 - let val3_and_4_epoch = pipeline_epoch; - - let consensus_vals: Vec<_> = consensus_validator_set_handle() - .at(&pipeline_epoch) - .iter(&s) - .unwrap() - .map(Result::unwrap) - .collect(); - - assert_eq!(consensus_vals.len(), 3); - assert!(matches!( - &consensus_vals[0], - (lazy_map::NestedSubKey::Data { - key: stake, - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val1 && stake == &stake1 && *position == Position(0) - )); - assert!(matches!( - &consensus_vals[1], - (lazy_map::NestedSubKey::Data { - key: stake, - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val2 && stake == &stake2 && *position == Position(1) - )); - assert!(matches!( - &consensus_vals[2], - (lazy_map::NestedSubKey::Data { - key: stake, - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val3 && stake == &stake3 && *position == Position(0) - )); - - // Check tendermint validator set updates - there should be none - let tm_updates = get_tendermint_set_updates(&s, ¶ms, epoch); - assert!(tm_updates.is_empty()); - - // Advance to EPOCH 2 - let epoch = advance_epoch(&mut s, ¶ms); - let pipeline_epoch = epoch + params.pipeline_len; - - // Insert another validator with a greater stake still 1000 NAM. It should - // replace 2nd consensus validator with stake 1, which should become - // below-capacity - insert_validator(&mut s, &val5, &pk5, stake5, epoch); - // Validator `val5` will be added at pipeline offset (2) - epoch 4 - let val5_epoch = pipeline_epoch; - - let consensus_vals: Vec<_> = consensus_validator_set_handle() - .at(&pipeline_epoch) - .iter(&s) - .unwrap() - .map(Result::unwrap) - .collect(); - - assert_eq!(consensus_vals.len(), 3); - assert!(matches!( - &consensus_vals[0], - (lazy_map::NestedSubKey::Data { - key: stake, - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val1 && stake == &stake1 && *position == Position(0) - )); - assert!(matches!( - &consensus_vals[1], - (lazy_map::NestedSubKey::Data { - key: stake, - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val3 && stake == &stake3 && *position == Position(0) - )); - assert!(matches!( - &consensus_vals[2], - (lazy_map::NestedSubKey::Data { - key: stake, - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val5 && stake == &stake5 && *position == Position(0) - )); - - let below_capacity_vals: Vec<_> = below_capacity_validator_set_handle() - .at(&pipeline_epoch) - .iter(&s) - .unwrap() - .map(Result::unwrap) - .collect(); - - assert_eq!(below_capacity_vals.len(), 2); - assert!(matches!( - &below_capacity_vals[0], - (lazy_map::NestedSubKey::Data { - key: ReverseOrdTokenAmount(stake), - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val4 && stake == &stake4 && *position == Position(0) - )); - assert!(matches!( - &below_capacity_vals[1], - (lazy_map::NestedSubKey::Data { - key: ReverseOrdTokenAmount(stake), - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val2 && stake == &stake2 && *position == Position(1) - )); - - // Advance to EPOCH 3 - let epoch = advance_epoch(&mut s, ¶ms); - let pipeline_epoch = epoch + params.pipeline_len; - - // Check tendermint validator set updates - assert_eq!( - val3_and_4_epoch, epoch, - "val3 and val4 are in the validator sets now" - ); - let tm_updates = get_tendermint_set_updates(&s, ¶ms, epoch); - // `val4` is newly added below-capacity, must be skipped in updated in TM - assert_eq!(tm_updates.len(), 1); - assert_eq!( - tm_updates[0], - ValidatorSetUpdate::Consensus(ConsensusValidator { - consensus_key: pk3, - bonded_stake: stake3, - }) - ); - - // Insert another validator with a stake 1 NAM. It should be added to the - // below-capacity set - insert_validator(&mut s, &val6, &pk6, stake6, epoch); - // Validator `val6` will be added at pipeline offset (2) - epoch 5 - let val6_epoch = pipeline_epoch; - - let below_capacity_vals: Vec<_> = below_capacity_validator_set_handle() - .at(&pipeline_epoch) - .iter(&s) - .unwrap() - .map(Result::unwrap) - .collect(); - - assert_eq!(below_capacity_vals.len(), 3); - assert!(matches!( - &below_capacity_vals[0], - (lazy_map::NestedSubKey::Data { - key: ReverseOrdTokenAmount(stake), - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val4 && stake == &stake4 && *position == Position(0) - )); - assert!(matches!( - &below_capacity_vals[1], - (lazy_map::NestedSubKey::Data { - key: ReverseOrdTokenAmount(stake), - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val2 && stake == &stake2 && *position == Position(1) - )); - assert!(matches!( - &below_capacity_vals[2], - (lazy_map::NestedSubKey::Data { - key: ReverseOrdTokenAmount(stake), - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val6 && stake == &stake6 && *position == Position(2) - )); - - // Advance to EPOCH 4 - let epoch = advance_epoch(&mut s, ¶ms); - let pipeline_epoch = epoch + params.pipeline_len; - - // Check tendermint validator set updates - assert_eq!(val5_epoch, epoch, "val5 is in the validator sets now"); - let tm_updates = get_tendermint_set_updates(&s, ¶ms, epoch); - assert_eq!(tm_updates.len(), 2); - assert_eq!( - tm_updates[0], - ValidatorSetUpdate::Consensus(ConsensusValidator { - consensus_key: pk5, - bonded_stake: stake5, - }) - ); - assert_eq!(tm_updates[1], ValidatorSetUpdate::Deactivated(pk2)); - - // Unbond some stake from val1, it should be be swapped with the greatest - // below-capacity validator val2 into the below-capacity set. The stake of - // val1 will go below 1 NAM, which is the validator_stake_threshold, so it - // will enter the below-threshold validator set. - let unbond = token::Amount::from_uint(500_000, 0).unwrap(); - let stake1 = stake1 - unbond; - println!("val1 {val1} new stake {}", stake1.to_string_native()); - // Because `update_validator_set` and `update_validator_deltas` are - // effective from pipeline offset, we use pipeline epoch for the rest of the - // checks - update_validator_set(&mut s, ¶ms, &val1, -unbond.change(), epoch, None) - .unwrap(); - update_validator_deltas( - &mut s, - ¶ms, - &val1, - -unbond.change(), - epoch, - None, - ) - .unwrap(); - // Epoch 6 - let val1_unbond_epoch = pipeline_epoch; - - let consensus_vals: Vec<_> = consensus_validator_set_handle() - .at(&pipeline_epoch) - .iter(&s) - .unwrap() - .map(Result::unwrap) - .collect(); - - assert_eq!(consensus_vals.len(), 3); - assert!(matches!( - &consensus_vals[0], - (lazy_map::NestedSubKey::Data { - key: stake, - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val4 && stake == &stake4 && *position == Position(0) - )); - assert!(matches!( - &consensus_vals[1], - (lazy_map::NestedSubKey::Data { - key: stake, - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val3 && stake == &stake3 && *position == Position(0) - )); - assert!(matches!( - &consensus_vals[2], - (lazy_map::NestedSubKey::Data { - key: stake, - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val5 && stake == &stake5 && *position == Position(0) - )); - - let below_capacity_vals: Vec<_> = below_capacity_validator_set_handle() - .at(&pipeline_epoch) - .iter(&s) - .unwrap() - .map(Result::unwrap) - .collect(); - - assert_eq!(below_capacity_vals.len(), 2); - assert!(matches!( - &below_capacity_vals[0], - (lazy_map::NestedSubKey::Data { - key: ReverseOrdTokenAmount(stake), - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val2 && stake == &stake2 && *position == Position(1) - )); - assert!(matches!( - &below_capacity_vals[1], - (lazy_map::NestedSubKey::Data { - key: ReverseOrdTokenAmount(stake), - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val6 && stake == &stake6 && *position == Position(2) - )); - - let below_threshold_vals = - read_below_threshold_validator_set_addresses(&s, pipeline_epoch) - .unwrap() - .into_iter() - .collect::>(); - - assert_eq!(below_threshold_vals.len(), 1); - assert_eq!(&below_threshold_vals[0], &val1); - - // Advance to EPOCH 5 - let epoch = advance_epoch(&mut s, ¶ms); - let pipeline_epoch = epoch + params.pipeline_len; - - // Check tendermint validator set updates - assert_eq!(val6_epoch, epoch, "val6 is in the validator sets now"); - let tm_updates = get_tendermint_set_updates(&s, ¶ms, epoch); - assert!(tm_updates.is_empty()); - - // Insert another validator with stake 1 - it should be added to below - // capacity set - insert_validator(&mut s, &val7, &pk7, stake7, epoch); - // Epoch 7 - let val7_epoch = pipeline_epoch; - - let consensus_vals: Vec<_> = consensus_validator_set_handle() - .at(&pipeline_epoch) - .iter(&s) - .unwrap() - .map(Result::unwrap) - .collect(); - - assert_eq!(consensus_vals.len(), 3); - assert!(matches!( - &consensus_vals[0], - (lazy_map::NestedSubKey::Data { - key: stake, - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val4 && stake == &stake4 && *position == Position(0) - )); - assert!(matches!( - &consensus_vals[1], - (lazy_map::NestedSubKey::Data { - key: stake, - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val3 && stake == &stake3 && *position == Position(0) - )); - assert!(matches!( - &consensus_vals[2], - (lazy_map::NestedSubKey::Data { - key: stake, - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val5 && stake == &stake5 && *position == Position(0) - )); - - let below_capacity_vals: Vec<_> = below_capacity_validator_set_handle() - .at(&pipeline_epoch) - .iter(&s) - .unwrap() - .map(Result::unwrap) - .collect(); - - assert_eq!(below_capacity_vals.len(), 3); - assert!(matches!( - &below_capacity_vals[0], - (lazy_map::NestedSubKey::Data { - key: ReverseOrdTokenAmount(stake), - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val2 && stake == &stake2 && *position == Position(1) - )); - assert!(matches!( - &below_capacity_vals[1], - (lazy_map::NestedSubKey::Data { - key: ReverseOrdTokenAmount(stake), - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val6 && stake == &stake6 && *position == Position(2) - )); - assert!(matches!( - &below_capacity_vals[2], - ( - lazy_map::NestedSubKey::Data { - key: ReverseOrdTokenAmount(stake), - nested_sub_key: lazy_map::SubKey::Data(position), - }, - address - ) - if address == &val7 && stake == &stake7 && *position == Position(3) - )); - - let below_threshold_vals = - read_below_threshold_validator_set_addresses(&s, pipeline_epoch) - .unwrap() - .into_iter() - .collect::>(); - - assert_eq!(below_threshold_vals.len(), 1); - assert_eq!(&below_threshold_vals[0], &val1); - - // Advance to EPOCH 6 - let epoch = advance_epoch(&mut s, ¶ms); - let pipeline_epoch = epoch + params.pipeline_len; - - // Check tendermint validator set updates - assert_eq!(val1_unbond_epoch, epoch, "val1's unbond is applied now"); - let tm_updates = get_tendermint_set_updates(&s, ¶ms, epoch); - assert_eq!(tm_updates.len(), 2); - assert_eq!( - tm_updates[0], - ValidatorSetUpdate::Consensus(ConsensusValidator { - consensus_key: pk4.clone(), - bonded_stake: stake4, - }) - ); - assert_eq!(tm_updates[1], ValidatorSetUpdate::Deactivated(pk1)); - - // Bond some stake to val6, it should be be swapped with the lowest - // consensus validator val2 into the consensus set - let bond = token::Amount::from_uint(500_000, 0).unwrap(); - let stake6 = stake6 + bond; - println!("val6 {val6} new stake {}", stake6.to_string_native()); - update_validator_set(&mut s, ¶ms, &val6, bond.change(), epoch, None) - .unwrap(); - update_validator_deltas(&mut s, ¶ms, &val6, bond.change(), epoch, None) - .unwrap(); - let val6_bond_epoch = pipeline_epoch; - - let consensus_vals: Vec<_> = consensus_validator_set_handle() - .at(&pipeline_epoch) - .iter(&s) - .unwrap() - .map(Result::unwrap) - .collect(); - - assert_eq!(consensus_vals.len(), 3); - assert!(matches!( - &consensus_vals[0], - (lazy_map::NestedSubKey::Data { - key: stake, - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val6 && stake == &stake6 && *position == Position(0) - )); - assert!(matches!( - &consensus_vals[1], - (lazy_map::NestedSubKey::Data { - key: stake, - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val3 && stake == &stake3 && *position == Position(0) - )); - assert!(matches!( - &consensus_vals[2], - (lazy_map::NestedSubKey::Data { - key: stake, - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val5 && stake == &stake5 && *position == Position(0) - )); - - let below_capacity_vals: Vec<_> = below_capacity_validator_set_handle() - .at(&pipeline_epoch) - .iter(&s) - .unwrap() - .map(Result::unwrap) - .collect(); - - assert_eq!(below_capacity_vals.len(), 3); - dbg!(&below_capacity_vals); - assert!(matches!( - &below_capacity_vals[0], - (lazy_map::NestedSubKey::Data { - key: ReverseOrdTokenAmount(stake), - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val2 && stake == &stake2 && *position == Position(1) - )); - assert!(matches!( - &below_capacity_vals[1], - (lazy_map::NestedSubKey::Data { - key: ReverseOrdTokenAmount(stake), - nested_sub_key: lazy_map::SubKey::Data(position), - }, address) - if address == &val7 && stake == &stake7 && *position == Position(3) - )); - assert!(matches!( - &below_capacity_vals[2], - ( - lazy_map::NestedSubKey::Data { - key: ReverseOrdTokenAmount(stake), - nested_sub_key: lazy_map::SubKey::Data(position), - }, - address - ) - if address == &val4 && stake == &stake4 && *position == Position(4) - )); - - let below_threshold_vals = - read_below_threshold_validator_set_addresses(&s, pipeline_epoch) - .unwrap() - .into_iter() - .collect::>(); - - assert_eq!(below_threshold_vals.len(), 1); - assert_eq!(&below_threshold_vals[0], &val1); - - // Advance to EPOCH 7 - let epoch = advance_epoch(&mut s, ¶ms); - assert_eq!(val7_epoch, epoch, "val6 is in the validator sets now"); - - // Check tendermint validator set updates - let tm_updates = get_tendermint_set_updates(&s, ¶ms, epoch); - assert!(tm_updates.is_empty()); - - // Advance to EPOCH 8 - let epoch = advance_epoch(&mut s, ¶ms); - - // Check tendermint validator set updates - assert_eq!(val6_bond_epoch, epoch, "val5's bond is applied now"); - let tm_updates = get_tendermint_set_updates(&s, ¶ms, epoch); - dbg!(&tm_updates); - assert_eq!(tm_updates.len(), 2); - assert_eq!( - tm_updates[0], - ValidatorSetUpdate::Consensus(ConsensusValidator { - consensus_key: pk6, - bonded_stake: stake6, - }) - ); - assert_eq!(tm_updates[1], ValidatorSetUpdate::Deactivated(pk4)); - - // Check that the below-capacity validator set was purged for the old epochs - // but that the consensus_validator_set was not - let last_epoch = epoch; - for e in Epoch::iter_bounds_inclusive( - start_epoch, - last_epoch - .sub_or_default(Epoch(DEFAULT_NUM_PAST_EPOCHS)) - .sub_or_default(Epoch(1)), - ) { - assert!( - !consensus_validator_set_handle() - .at(&e) - .is_empty(&s) - .unwrap() - ); - assert!( - below_capacity_validator_set_handle() - .at(&e) - .is_empty(&s) - .unwrap() - ); - } -} - -/// When a consensus set validator with 0 voting power adds a bond in the same -/// epoch as another below-capacity set validator with 0 power, but who adds -/// more bonds than the validator who is in the consensus set, they get swapped -/// in the sets. But if both of their new voting powers are still 0 after -/// bonding, the newly below-capacity validator must not be given to tendermint -/// with 0 voting power, because it wasn't it its set before -#[test] -fn test_validator_sets_swap() { - let mut s = TestWlStorage::default(); - // Only 2 consensus validator slots - let params = OwnedPosParams { - max_validator_slots: 2, - // Set the stake threshold to 0 so no validators are in the - // below-threshold set - validator_stake_threshold: token::Amount::zero(), - // Set 0.1 votes per token - tm_votes_per_token: Dec::new(1, 1).expect("Dec creation failed"), - ..Default::default() - }; - - let addr_seed = "seed"; - let mut address_gen = EstablishedAddressGen::new(addr_seed); - let mut sk_seed = 0; - let mut gen_validator = || { - let res = ( - address_gen.generate_address(addr_seed), - key::testing::common_sk_from_simple_seed(sk_seed).to_public(), - ); - // bump the sk seed - sk_seed += 1; - res - }; - - // Start with two genesis validators, one with 1 voting power and other 0 - let epoch = Epoch::default(); - // 1M voting power - let ((val1, pk1), stake1) = - (gen_validator(), token::Amount::native_whole(10)); - // 0 voting power - let ((val2, pk2), stake2) = - (gen_validator(), token::Amount::from_uint(5, 0).unwrap()); - // 0 voting power - let ((val3, pk3), stake3) = - (gen_validator(), token::Amount::from_uint(5, 0).unwrap()); - println!("val1: {val1}, {pk1}, {}", stake1.to_string_native()); - println!("val2: {val2}, {pk2}, {}", stake2.to_string_native()); - println!("val3: {val3}, {pk3}, {}", stake3.to_string_native()); - - let protocol_sk_1 = common_sk_from_simple_seed(0); - let protocol_sk_2 = common_sk_from_simple_seed(1); - - let params = test_init_genesis( - &mut s, - params, - [ - GenesisValidator { - address: val1, - tokens: stake1, - consensus_key: pk1, - protocol_key: protocol_sk_1.to_public(), - eth_hot_key: key::common::PublicKey::Secp256k1( - key::testing::gen_keypair::() - .ref_to(), - ), - eth_cold_key: key::common::PublicKey::Secp256k1( - key::testing::gen_keypair::() - .ref_to(), - ), - commission_rate: Dec::new(1, 1).expect("Dec creation failed"), - max_commission_rate_change: Dec::new(1, 1) - .expect("Dec creation failed"), - metadata: Default::default(), - }, - GenesisValidator { - address: val2.clone(), - tokens: stake2, - consensus_key: pk2, - protocol_key: protocol_sk_2.to_public(), - eth_hot_key: key::common::PublicKey::Secp256k1( - key::testing::gen_keypair::() - .ref_to(), - ), - eth_cold_key: key::common::PublicKey::Secp256k1( - key::testing::gen_keypair::() - .ref_to(), - ), - commission_rate: Dec::new(1, 1).expect("Dec creation failed"), - max_commission_rate_change: Dec::new(1, 1) - .expect("Dec creation failed"), - metadata: Default::default(), - }, - ] - .into_iter(), - epoch, - ) - .unwrap(); - - // A helper to insert a non-genesis validator - let insert_validator = |s: &mut TestWlStorage, - addr, - pk: &PublicKey, - stake: token::Amount, - epoch: Epoch| { - insert_validator_into_validator_set( - s, - ¶ms, - addr, - stake, - epoch, - params.pipeline_len, - ) - .unwrap(); - - update_validator_deltas(s, ¶ms, addr, stake.change(), epoch, None) - .unwrap(); - - // Set their consensus key (needed for - // `validator_set_update_tendermint` fn) - validator_consensus_key_handle(addr) - .set(s, pk.clone(), epoch, params.pipeline_len) - .unwrap(); - }; - - // Advance to EPOCH 1 - let epoch = advance_epoch(&mut s, ¶ms); - let pipeline_epoch = epoch + params.pipeline_len; - - // Insert another validator with 0 voting power - insert_validator(&mut s, &val3, &pk3, stake3, epoch); - - assert_eq!(stake2, stake3); - - // Add 2 bonds, one for val2 and greater one for val3 - let bonds_epoch_1 = pipeline_epoch; - let bond2 = token::Amount::from_uint(1, 0).unwrap(); - let stake2 = stake2 + bond2; - let bond3 = token::Amount::from_uint(4, 0).unwrap(); - let stake3 = stake3 + bond3; - - assert!(stake2 < stake3); - assert_eq!(into_tm_voting_power(params.tm_votes_per_token, stake2), 0); - assert_eq!(into_tm_voting_power(params.tm_votes_per_token, stake3), 0); - - update_validator_set(&mut s, ¶ms, &val2, bond2.change(), epoch, None) - .unwrap(); - update_validator_deltas( - &mut s, - ¶ms, - &val2, - bond2.change(), - epoch, - None, - ) - .unwrap(); - - update_validator_set(&mut s, ¶ms, &val3, bond3.change(), epoch, None) - .unwrap(); - update_validator_deltas( - &mut s, - ¶ms, - &val3, - bond3.change(), - epoch, - None, - ) - .unwrap(); - - // Advance to EPOCH 2 - let epoch = advance_epoch(&mut s, ¶ms); - let pipeline_epoch = epoch + params.pipeline_len; - - // Add 2 more bonds, same amount for `val2` and val3` - let bonds_epoch_2 = pipeline_epoch; - let bonds = token::Amount::native_whole(1); - let stake2 = stake2 + bonds; - let stake3 = stake3 + bonds; - assert!(stake2 < stake3); - assert_eq!( - into_tm_voting_power(params.tm_votes_per_token, stake2), - into_tm_voting_power(params.tm_votes_per_token, stake3) - ); - - update_validator_set(&mut s, ¶ms, &val2, bonds.change(), epoch, None) - .unwrap(); - update_validator_deltas( - &mut s, - ¶ms, - &val2, - bonds.change(), - epoch, - None, - ) - .unwrap(); - - update_validator_set(&mut s, ¶ms, &val3, bonds.change(), epoch, None) - .unwrap(); - update_validator_deltas( - &mut s, - ¶ms, - &val3, - bonds.change(), - epoch, - None, - ) - .unwrap(); - - // Advance to EPOCH 3 - let epoch = advance_epoch(&mut s, ¶ms); - - // Check tendermint validator set updates - assert_eq!(bonds_epoch_1, epoch); - let tm_updates = get_tendermint_set_updates(&s, ¶ms, epoch); - // `val2` must not be given to tendermint - even though it was in the - // consensus set, its voting power was 0, so it wasn't in TM set before the - // bond - assert!(tm_updates.is_empty()); - - // Advance to EPOCH 4 - let epoch = advance_epoch(&mut s, ¶ms); - - // Check tendermint validator set updates - assert_eq!(bonds_epoch_2, epoch); - let tm_updates = get_tendermint_set_updates(&s, ¶ms, epoch); - dbg!(&tm_updates); - assert_eq!(tm_updates.len(), 1); - // `val2` must not be given to tendermint as it was and still is below - // capacity - assert_eq!( - tm_updates[0], - ValidatorSetUpdate::Consensus(ConsensusValidator { - consensus_key: pk3, - bonded_stake: stake3, - }) - ); -} - -fn get_tendermint_set_updates( - s: &TestWlStorage, - params: &PosParams, - Epoch(epoch): Epoch, -) -> Vec { - // Because the `validator_set_update_tendermint` is called 2 blocks before - // the start of a new epoch, it expects to receive the epoch that is before - // the start of a new one too and so we give it the predecessor of the - // current epoch here to actually get the update for the current epoch. - let epoch = Epoch(epoch - 1); - validator_set_update_tendermint(s, params, epoch, |update| update).unwrap() -} - -/// Advance to the next epoch. Returns the new epoch. -fn advance_epoch(s: &mut TestWlStorage, params: &PosParams) -> Epoch { - s.storage.block.epoch = s.storage.block.epoch.next(); - let current_epoch = s.storage.block.epoch; - compute_and_store_total_consensus_stake(s, current_epoch).unwrap(); - copy_validator_sets_and_positions( - s, - params, - current_epoch, - current_epoch + params.pipeline_len, - ) - .unwrap(); - // purge_validator_sets_for_old_epoch(s, current_epoch).unwrap(); - // process_slashes(s, current_epoch).unwrap(); - // dbg!(current_epoch); - current_epoch -} - -fn arb_genesis_validators( - size: Range, - threshold: Option, -) -> impl Strategy> { - let threshold = threshold - .unwrap_or_else(|| PosParams::default().validator_stake_threshold); - let tokens: Vec<_> = (0..size.end) - .map(|ix| { - if ix == 0 { - // Make sure that at least one validator has at least a stake - // greater or equal to the threshold to avoid having an empty - // consensus set. - threshold.raw_amount().as_u64()..=10_000_000_u64 - } else { - 1..=10_000_000_u64 - } - .prop_map(token::Amount::from) - }) - .collect(); - (size, tokens) - .prop_map(|(size, token_amounts)| { - // use unique seeds to generate validators' address and consensus - // key - let seeds = (0_u64..).take(size); - seeds - .zip(token_amounts) - .map(|(seed, tokens)| { - let address = address_from_simple_seed(seed); - let consensus_sk = common_sk_from_simple_seed(seed); - let consensus_key = consensus_sk.to_public(); - - let protocol_sk = common_sk_from_simple_seed(seed); - let protocol_key = protocol_sk.to_public(); - - let eth_hot_key = key::common::PublicKey::Secp256k1( - key::testing::gen_keypair::( - ) - .ref_to(), - ); - let eth_cold_key = key::common::PublicKey::Secp256k1( - key::testing::gen_keypair::( - ) - .ref_to(), - ); - - let commission_rate = Dec::new(5, 2).expect("Test failed"); - let max_commission_rate_change = - Dec::new(1, 2).expect("Test failed"); - GenesisValidator { - address, - tokens, - consensus_key, - protocol_key, - eth_hot_key, - eth_cold_key, - commission_rate, - max_commission_rate_change, - metadata: Default::default(), - } - }) - .collect() - }) - .prop_filter( - "Must have at least one genesis validator with stake above the \ - provided threshold, if any.", - move |gen_vals: &Vec| { - gen_vals.iter().any(|val| val.tokens >= threshold) - }, - ) -} - -fn test_unjail_validator_aux( - params: OwnedPosParams, - mut validators: Vec, -) { - println!("\nTest inputs: {params:?}, genesis validators: {validators:#?}"); - let mut s = TestWlStorage::default(); - - // Find the validator with the most stake and 100x his stake to keep the - // cubic slash rate small - let num_vals = validators.len(); - validators.sort_by_key(|a| a.tokens); - validators[num_vals - 1].tokens = 100 * validators[num_vals - 1].tokens; - - // Get second highest stake validator tomisbehave - let val_addr = &validators[num_vals - 2].address; - let val_tokens = validators[num_vals - 2].tokens; - println!( - "Validator that will misbehave addr {val_addr}, tokens {}", - val_tokens.to_string_native() - ); - - // Genesis - let mut current_epoch = s.storage.block.epoch; - let params = test_init_genesis( - &mut s, - params, - validators.clone().into_iter(), - current_epoch, - ) - .unwrap(); - s.commit_block().unwrap(); - - current_epoch = advance_epoch(&mut s, ¶ms); - super::process_slashes(&mut s, current_epoch).unwrap(); - - // Discover first slash - let slash_0_evidence_epoch = current_epoch; - let evidence_block_height = BlockHeight(0); // doesn't matter for slashing logic - let slash_0_type = SlashType::DuplicateVote; - slash( - &mut s, - ¶ms, - current_epoch, - slash_0_evidence_epoch, - evidence_block_height, - slash_0_type, - val_addr, - current_epoch.next(), - ) - .unwrap(); - - assert_eq!( - validator_state_handle(val_addr) - .get(&s, current_epoch, ¶ms) - .unwrap(), - Some(ValidatorState::Consensus) - ); - - for epoch in Epoch::iter_bounds_inclusive( - current_epoch.next(), - current_epoch + params.pipeline_len, - ) { - // Check the validator state - assert_eq!( - validator_state_handle(val_addr) - .get(&s, epoch, ¶ms) - .unwrap(), - Some(ValidatorState::Jailed) - ); - // Check the validator set positions - assert!( - validator_set_positions_handle() - .at(&epoch) - .get(&s, val_addr) - .unwrap() - .is_none(), - ); - } - - // Advance past an epoch in which we can unbond - let unfreeze_epoch = - slash_0_evidence_epoch + params.slash_processing_epoch_offset(); - while current_epoch < unfreeze_epoch + 4u64 { - current_epoch = advance_epoch(&mut s, ¶ms); - super::process_slashes(&mut s, current_epoch).unwrap(); - } - - // Unjail the validator - unjail_validator(&mut s, val_addr, current_epoch).unwrap(); - - // Check the validator state - for epoch in - Epoch::iter_bounds_inclusive(current_epoch, current_epoch.next()) - { - assert_eq!( - validator_state_handle(val_addr) - .get(&s, epoch, ¶ms) - .unwrap(), - Some(ValidatorState::Jailed) - ); - } - - assert_eq!( - validator_state_handle(val_addr) - .get(&s, current_epoch + params.pipeline_len, ¶ms) - .unwrap(), - Some(ValidatorState::Consensus) - ); - assert!( - validator_set_positions_handle() - .at(&(current_epoch + params.pipeline_len)) - .get(&s, val_addr) - .unwrap() - .is_some(), - ); - - // Advance another epoch - current_epoch = advance_epoch(&mut s, ¶ms); - super::process_slashes(&mut s, current_epoch).unwrap(); - - let second_att = unjail_validator(&mut s, val_addr, current_epoch); - assert!(second_att.is_err()); -} - -/// `iterateBondsUpToAmountTest` -#[test] -fn test_find_bonds_to_remove() { - let mut storage = TestWlStorage::default(); - let gov_params = namada_core::ledger::governance::parameters::GovernanceParameters::default(); - gov_params.init_storage(&mut storage).unwrap(); - write_pos_params(&mut storage, &OwnedPosParams::default()).unwrap(); - - let source = established_address_1(); - let validator = established_address_2(); - let bond_handle = bond_handle(&source, &validator); - - let (e1, e2, e6) = (Epoch(1), Epoch(2), Epoch(6)); - - bond_handle - .set(&mut storage, token::Amount::from(5), e1, 0) - .unwrap(); - bond_handle - .set(&mut storage, token::Amount::from(3), e2, 0) - .unwrap(); - bond_handle - .set(&mut storage, token::Amount::from(8), e6, 0) - .unwrap(); - - // Test 1 - let bonds_for_removal = find_bonds_to_remove( - &storage, - &bond_handle.get_data_handler(), - token::Amount::from(8), - ) - .unwrap(); - assert_eq!( - bonds_for_removal.epochs, - vec![e6].into_iter().collect::>() - ); - assert!(bonds_for_removal.new_entry.is_none()); - - // Test 2 - let bonds_for_removal = find_bonds_to_remove( - &storage, - &bond_handle.get_data_handler(), - token::Amount::from(10), - ) - .unwrap(); - assert_eq!( - bonds_for_removal.epochs, - vec![e6].into_iter().collect::>() - ); - assert_eq!( - bonds_for_removal.new_entry, - Some((Epoch(2), token::Amount::from(1))) - ); - - // Test 3 - let bonds_for_removal = find_bonds_to_remove( - &storage, - &bond_handle.get_data_handler(), - token::Amount::from(11), - ) - .unwrap(); - assert_eq!( - bonds_for_removal.epochs, - vec![e6, e2].into_iter().collect::>() - ); - assert!(bonds_for_removal.new_entry.is_none()); - - // Test 4 - let bonds_for_removal = find_bonds_to_remove( - &storage, - &bond_handle.get_data_handler(), - token::Amount::from(12), - ) - .unwrap(); - assert_eq!( - bonds_for_removal.epochs, - vec![e6, e2].into_iter().collect::>() - ); - assert_eq!( - bonds_for_removal.new_entry, - Some((Epoch(1), token::Amount::from(4))) - ); -} - -/// `computeModifiedRedelegationTest` -#[test] -fn test_compute_modified_redelegation() { - let mut storage = TestWlStorage::default(); - let validator1 = established_address_1(); - let validator2 = established_address_2(); - let owner = established_address_3(); - let outer_epoch = Epoch(0); - - let mut alice = validator1.clone(); - let mut bob = validator2.clone(); - - // Ensure a ranking order of alice > bob - if bob > alice { - alice = validator2; - bob = validator1; - } - println!("\n\nalice = {}\nbob = {}\n", &alice, &bob); - - // Fill redelegated bonds in storage - let redelegated_bonds_map = delegator_redelegated_bonds_handle(&owner) - .at(&alice) - .at(&outer_epoch); - redelegated_bonds_map - .at(&alice) - .insert(&mut storage, Epoch(2), token::Amount::from(6)) - .unwrap(); - redelegated_bonds_map - .at(&alice) - .insert(&mut storage, Epoch(4), token::Amount::from(7)) - .unwrap(); - redelegated_bonds_map - .at(&bob) - .insert(&mut storage, Epoch(1), token::Amount::from(5)) - .unwrap(); - redelegated_bonds_map - .at(&bob) - .insert(&mut storage, Epoch(4), token::Amount::from(7)) - .unwrap(); - - // Test cases 1 and 2 - let mr1 = compute_modified_redelegation( - &storage, - &redelegated_bonds_map, - Epoch(5), - token::Amount::from(25), - ) - .unwrap(); - let mr2 = compute_modified_redelegation( - &storage, - &redelegated_bonds_map, - Epoch(5), - token::Amount::from(30), - ) - .unwrap(); - - let exp_mr = ModifiedRedelegation { - epoch: Some(Epoch(5)), - ..Default::default() - }; - - assert_eq!(mr1, exp_mr); - assert_eq!(mr2, exp_mr); - - // Test case 3 - let mr3 = compute_modified_redelegation( - &storage, - &redelegated_bonds_map, - Epoch(5), - token::Amount::from(7), - ) - .unwrap(); - - let exp_mr = ModifiedRedelegation { - epoch: Some(Epoch(5)), - validators_to_remove: BTreeSet::from_iter([bob.clone()]), - validator_to_modify: Some(bob.clone()), - epochs_to_remove: BTreeSet::from_iter([Epoch(4)]), - ..Default::default() - }; - assert_eq!(mr3, exp_mr); - - // Test case 4 - let mr4 = compute_modified_redelegation( - &storage, - &redelegated_bonds_map, - Epoch(5), - token::Amount::from(8), - ) - .unwrap(); - - let exp_mr = ModifiedRedelegation { - epoch: Some(Epoch(5)), - validators_to_remove: BTreeSet::from_iter([bob.clone()]), - validator_to_modify: Some(bob.clone()), - epochs_to_remove: BTreeSet::from_iter([Epoch(1), Epoch(4)]), - epoch_to_modify: Some(Epoch(1)), - new_amount: Some(4.into()), - }; - assert_eq!(mr4, exp_mr); - - // Test case 5 - let mr5 = compute_modified_redelegation( - &storage, - &redelegated_bonds_map, - Epoch(5), - 12.into(), - ) - .unwrap(); - - let exp_mr = ModifiedRedelegation { - epoch: Some(Epoch(5)), - validators_to_remove: BTreeSet::from_iter([bob.clone()]), - ..Default::default() - }; - assert_eq!(mr5, exp_mr); - - // Test case 6 - let mr6 = compute_modified_redelegation( - &storage, - &redelegated_bonds_map, - Epoch(5), - 14.into(), - ) - .unwrap(); - - let exp_mr = ModifiedRedelegation { - epoch: Some(Epoch(5)), - validators_to_remove: BTreeSet::from_iter([alice.clone(), bob.clone()]), - validator_to_modify: Some(alice.clone()), - epochs_to_remove: BTreeSet::from_iter([Epoch(4)]), - epoch_to_modify: Some(Epoch(4)), - new_amount: Some(5.into()), - }; - assert_eq!(mr6, exp_mr); - - // Test case 7 - let mr7 = compute_modified_redelegation( - &storage, - &redelegated_bonds_map, - Epoch(5), - 19.into(), - ) - .unwrap(); - - let exp_mr = ModifiedRedelegation { - epoch: Some(Epoch(5)), - validators_to_remove: BTreeSet::from_iter([alice.clone(), bob.clone()]), - validator_to_modify: Some(alice.clone()), - epochs_to_remove: BTreeSet::from_iter([Epoch(4)]), - ..Default::default() - }; - assert_eq!(mr7, exp_mr); - - // Test case 8 - let mr8 = compute_modified_redelegation( - &storage, - &redelegated_bonds_map, - Epoch(5), - 21.into(), - ) - .unwrap(); - - let exp_mr = ModifiedRedelegation { - epoch: Some(Epoch(5)), - validators_to_remove: BTreeSet::from_iter([alice.clone(), bob]), - validator_to_modify: Some(alice), - epochs_to_remove: BTreeSet::from_iter([Epoch(2), Epoch(4)]), - epoch_to_modify: Some(Epoch(2)), - new_amount: Some(4.into()), - }; - assert_eq!(mr8, exp_mr); -} - -/// `computeBondAtEpochTest` -#[test] -fn test_compute_bond_at_epoch() { - let mut storage = TestWlStorage::default(); - let params = OwnedPosParams { - pipeline_len: 2, - unbonding_len: 4, - cubic_slashing_window_length: 1, - ..Default::default() - }; - let alice = established_address_1(); - let bob = established_address_2(); - - // Test 1 - let res = compute_bond_at_epoch( - &storage, - ¶ms, - &bob, - 12.into(), - 3.into(), - 23.into(), - Some(&Default::default()), - ) - .unwrap(); - - pretty_assertions::assert_eq!(res, 23.into()); - - // Test 2 - validator_slashes_handle(&bob) - .push( - &mut storage, - Slash { - epoch: 4.into(), - block_height: 0, - r#type: SlashType::DuplicateVote, - rate: Dec::one(), - }, - ) - .unwrap(); - let res = compute_bond_at_epoch( - &storage, - ¶ms, - &bob, - 12.into(), - 3.into(), - 23.into(), - Some(&Default::default()), - ) - .unwrap(); - - pretty_assertions::assert_eq!(res, 0.into()); - - // Test 3 - validator_slashes_handle(&bob).pop(&mut storage).unwrap(); - let mut redel_bonds = EagerRedelegatedBondsMap::default(); - redel_bonds.insert( - alice.clone(), - BTreeMap::from_iter([(Epoch(1), token::Amount::from(5))]), - ); - let res = compute_bond_at_epoch( - &storage, - ¶ms, - &bob, - 12.into(), - 3.into(), - 23.into(), - Some(&redel_bonds), - ) - .unwrap(); - - pretty_assertions::assert_eq!(res, 23.into()); - - // Test 4 - validator_slashes_handle(&bob) - .push( - &mut storage, - Slash { - epoch: 4.into(), - block_height: 0, - r#type: SlashType::DuplicateVote, - rate: Dec::one(), - }, - ) - .unwrap(); - let res = compute_bond_at_epoch( - &storage, - ¶ms, - &bob, - 12.into(), - 3.into(), - 23.into(), - Some(&redel_bonds), - ) - .unwrap(); - - pretty_assertions::assert_eq!(res, 0.into()); - - // Test 5 - validator_slashes_handle(&bob).pop(&mut storage).unwrap(); - validator_slashes_handle(&alice) - .push( - &mut storage, - Slash { - epoch: 6.into(), - block_height: 0, - r#type: SlashType::DuplicateVote, - rate: Dec::one(), - }, - ) - .unwrap(); - let res = compute_bond_at_epoch( - &storage, - ¶ms, - &bob, - 12.into(), - 3.into(), - 23.into(), - Some(&redel_bonds), - ) - .unwrap(); - - pretty_assertions::assert_eq!(res, 23.into()); - - // Test 6 - validator_slashes_handle(&alice).pop(&mut storage).unwrap(); - validator_slashes_handle(&alice) - .push( - &mut storage, - Slash { - epoch: 4.into(), - block_height: 0, - r#type: SlashType::DuplicateVote, - rate: Dec::one(), - }, - ) - .unwrap(); - let res = compute_bond_at_epoch( - &storage, - ¶ms, - &bob, - 18.into(), - 9.into(), - 23.into(), - Some(&redel_bonds), - ) - .unwrap(); - - pretty_assertions::assert_eq!(res, 18.into()); -} - -/// `computeSlashBondAtEpochTest` -#[test] -fn test_compute_slash_bond_at_epoch() { - let mut storage = TestWlStorage::default(); - let params = OwnedPosParams { - pipeline_len: 2, - unbonding_len: 4, - cubic_slashing_window_length: 1, - ..Default::default() - }; - let alice = established_address_1(); - let bob = established_address_2(); - - let current_epoch = Epoch(20); - let infraction_epoch = - current_epoch - params.slash_processing_epoch_offset(); - - let redelegated_bond = BTreeMap::from_iter([( - alice, - BTreeMap::from_iter([(infraction_epoch - 4, token::Amount::from(10))]), - )]); - - // Test 1 - let res = compute_slash_bond_at_epoch( - &storage, - ¶ms, - &bob, - current_epoch.next(), - infraction_epoch, - infraction_epoch - 2, - 30.into(), - Some(&Default::default()), - Dec::one(), - ) - .unwrap(); - - pretty_assertions::assert_eq!(res, 30.into()); - - // Test 2 - let res = compute_slash_bond_at_epoch( - &storage, - ¶ms, - &bob, - current_epoch.next(), - infraction_epoch, - infraction_epoch - 2, - 30.into(), - Some(&redelegated_bond), - Dec::one(), - ) - .unwrap(); - - pretty_assertions::assert_eq!(res, 30.into()); - - // Test 3 - validator_slashes_handle(&bob) - .push( - &mut storage, - Slash { - epoch: infraction_epoch.prev(), - block_height: 0, - r#type: SlashType::DuplicateVote, - rate: Dec::one(), - }, - ) - .unwrap(); - let res = compute_slash_bond_at_epoch( - &storage, - ¶ms, - &bob, - current_epoch.next(), - infraction_epoch, - infraction_epoch - 2, - 30.into(), - Some(&Default::default()), - Dec::one(), - ) - .unwrap(); - - pretty_assertions::assert_eq!(res, 0.into()); - - // Test 4 - let res = compute_slash_bond_at_epoch( - &storage, - ¶ms, - &bob, - current_epoch.next(), - infraction_epoch, - infraction_epoch - 2, - 30.into(), - Some(&redelegated_bond), - Dec::one(), - ) - .unwrap(); - - pretty_assertions::assert_eq!(res, 0.into()); -} - -/// `computeNewRedelegatedUnbondsTest` -#[test] -fn test_compute_new_redelegated_unbonds() { - let mut storage = TestWlStorage::default(); - let alice = established_address_1(); - let bob = established_address_2(); - - let key = Key::parse("testing").unwrap(); - let redelegated_bonds = NestedMap::::open(key); - - // Populate the lazy and eager maps - let (ep1, ep2, ep4, ep5, ep6, ep7) = - (Epoch(1), Epoch(2), Epoch(4), Epoch(5), Epoch(6), Epoch(7)); - let keys_and_values = vec![ - (ep5, alice.clone(), ep2, 1), - (ep5, alice.clone(), ep4, 1), - (ep7, alice.clone(), ep2, 1), - (ep7, alice.clone(), ep4, 1), - (ep5, bob.clone(), ep1, 1), - (ep5, bob.clone(), ep4, 2), - (ep7, bob.clone(), ep1, 1), - (ep7, bob.clone(), ep4, 2), - ]; - let mut eager_map = BTreeMap::::new(); - for (outer_ep, address, inner_ep, amount) in keys_and_values { - redelegated_bonds - .at(&outer_ep) - .at(&address) - .insert(&mut storage, inner_ep, token::Amount::from(amount)) - .unwrap(); - eager_map - .entry(outer_ep) - .or_default() - .entry(address.clone()) - .or_default() - .insert(inner_ep, token::Amount::from(amount)); - } - - // Different ModifiedRedelegation objects for testing - let empty_mr = ModifiedRedelegation::default(); - let all_mr = ModifiedRedelegation { - epoch: Some(ep7), - validators_to_remove: BTreeSet::from_iter([alice.clone(), bob.clone()]), - validator_to_modify: None, - epochs_to_remove: Default::default(), - epoch_to_modify: None, - new_amount: None, - }; - let mod_val_mr = ModifiedRedelegation { - epoch: Some(ep7), - validators_to_remove: BTreeSet::from_iter([alice.clone()]), - validator_to_modify: None, - epochs_to_remove: Default::default(), - epoch_to_modify: None, - new_amount: None, - }; - let mod_val_partial_mr = ModifiedRedelegation { - epoch: Some(ep7), - validators_to_remove: BTreeSet::from_iter([alice.clone(), bob.clone()]), - validator_to_modify: Some(bob.clone()), - epochs_to_remove: BTreeSet::from_iter([ep1]), - epoch_to_modify: None, - new_amount: None, - }; - let mod_epoch_partial_mr = ModifiedRedelegation { - epoch: Some(ep7), - validators_to_remove: BTreeSet::from_iter([alice, bob.clone()]), - validator_to_modify: Some(bob.clone()), - epochs_to_remove: BTreeSet::from_iter([ep1, ep4]), - epoch_to_modify: Some(ep4), - new_amount: Some(token::Amount::from(1)), - }; - - // Test case 1 - let res = compute_new_redelegated_unbonds( - &storage, - &redelegated_bonds, - &Default::default(), - &empty_mr, - ) - .unwrap(); - assert_eq!(res, Default::default()); - - let set5 = BTreeSet::::from_iter([ep5]); - let set56 = BTreeSet::::from_iter([ep5, ep6]); - - // Test case 2 - let res = compute_new_redelegated_unbonds( - &storage, - &redelegated_bonds, - &set5, - &empty_mr, - ) - .unwrap(); - let mut exp_res = eager_map.clone(); - exp_res.remove(&ep7); - assert_eq!(res, exp_res); - - // Test case 3 - let res = compute_new_redelegated_unbonds( - &storage, - &redelegated_bonds, - &set56, - &empty_mr, - ) - .unwrap(); - assert_eq!(res, exp_res); - - // Test case 4 - println!("\nTEST CASE 4\n"); - let res = compute_new_redelegated_unbonds( - &storage, - &redelegated_bonds, - &set56, - &all_mr, - ) - .unwrap(); - assert_eq!(res, eager_map); - - // Test case 5 - let res = compute_new_redelegated_unbonds( - &storage, - &redelegated_bonds, - &set56, - &mod_val_mr, - ) - .unwrap(); - exp_res = eager_map.clone(); - exp_res.entry(ep7).or_default().remove(&bob); - assert_eq!(res, exp_res); - - // Test case 6 - let res = compute_new_redelegated_unbonds( - &storage, - &redelegated_bonds, - &set56, - &mod_val_partial_mr, - ) - .unwrap(); - exp_res = eager_map.clone(); - exp_res - .entry(ep7) - .or_default() - .entry(bob.clone()) - .or_default() - .remove(&ep4); - assert_eq!(res, exp_res); - - // Test case 7 - let res = compute_new_redelegated_unbonds( - &storage, - &redelegated_bonds, - &set56, - &mod_epoch_partial_mr, - ) - .unwrap(); - exp_res - .entry(ep7) - .or_default() - .entry(bob) - .or_default() - .insert(ep4, token::Amount::from(1)); - assert_eq!(res, exp_res); -} - -/// `applyListSlashesTest` -#[test] -fn test_apply_list_slashes() { - let init_epoch = Epoch(2); - let params = OwnedPosParams { - unbonding_len: 4, - ..Default::default() - }; - // let unbonding_len = 4u64; - // let cubic_offset = 1u64; - - let slash1 = Slash { - epoch: init_epoch, - block_height: Default::default(), - r#type: SlashType::DuplicateVote, - rate: Dec::one(), - }; - let slash2 = Slash { - epoch: init_epoch - + params.unbonding_len - + params.cubic_slashing_window_length - + 1u64, - block_height: Default::default(), - r#type: SlashType::DuplicateVote, - rate: Dec::one(), - }; - - let list1 = vec![slash1.clone()]; - let list2 = vec![slash1.clone(), slash2.clone()]; - let list3 = vec![slash1.clone(), slash1.clone()]; - let list4 = vec![slash1.clone(), slash1, slash2]; - - let res = apply_list_slashes(¶ms, &[], token::Amount::from(100)); - assert_eq!(res, token::Amount::from(100)); - - let res = apply_list_slashes(¶ms, &list1, token::Amount::from(100)); - assert_eq!(res, token::Amount::zero()); - - let res = apply_list_slashes(¶ms, &list2, token::Amount::from(100)); - assert_eq!(res, token::Amount::zero()); - - let res = apply_list_slashes(¶ms, &list3, token::Amount::from(100)); - assert_eq!(res, token::Amount::zero()); - - let res = apply_list_slashes(¶ms, &list4, token::Amount::from(100)); - assert_eq!(res, token::Amount::zero()); -} - -/// `computeSlashableAmountTest` -#[test] -fn test_compute_slashable_amount() { - let init_epoch = Epoch(2); - let params = OwnedPosParams { - unbonding_len: 4, - ..Default::default() - }; - - let slash1 = Slash { - epoch: init_epoch - + params.unbonding_len - + params.cubic_slashing_window_length, - block_height: Default::default(), - r#type: SlashType::DuplicateVote, - rate: Dec::one(), - }; - - let slash2 = Slash { - epoch: init_epoch - + params.unbonding_len - + params.cubic_slashing_window_length - + 1u64, - block_height: Default::default(), - r#type: SlashType::DuplicateVote, - rate: Dec::one(), - }; - - let test_map = vec![(init_epoch, token::Amount::from(50))] - .into_iter() - .collect::>(); - - let res = compute_slashable_amount( - ¶ms, - &slash1, - token::Amount::from(100), - &BTreeMap::new(), - ); - assert_eq!(res, token::Amount::from(100)); - - let res = compute_slashable_amount( - ¶ms, - &slash2, - token::Amount::from(100), - &test_map, - ); - assert_eq!(res, token::Amount::from(50)); - - let res = compute_slashable_amount( - ¶ms, - &slash1, - token::Amount::from(100), - &test_map, - ); - assert_eq!(res, token::Amount::from(100)); -} - -/// `foldAndSlashRedelegatedBondsMapTest` -#[test] -fn test_fold_and_slash_redelegated_bonds() { - let mut storage = TestWlStorage::default(); - let params = OwnedPosParams { - unbonding_len: 4, - ..Default::default() - }; - let start_epoch = Epoch(7); - - let alice = established_address_1(); - let bob = established_address_2(); - - println!("\n\nAlice: {}", alice); - println!("Bob: {}\n", bob); - - let test_slash = Slash { - epoch: Default::default(), - block_height: Default::default(), - r#type: SlashType::DuplicateVote, - rate: Dec::one(), - }; - - let test_data = vec![ - (alice.clone(), vec![(2, 1), (4, 1)]), - (bob, vec![(1, 1), (4, 2)]), - ]; - let mut eager_redel_bonds = EagerRedelegatedBondsMap::default(); - for (address, pair) in test_data { - for (epoch, amount) in pair { - eager_redel_bonds - .entry(address.clone()) - .or_default() - .insert(Epoch(epoch), token::Amount::from(amount)); - } - } - - // Test case 1 - let res = fold_and_slash_redelegated_bonds( - &storage, - ¶ms, - &eager_redel_bonds, - start_epoch, - &[], - |_| true, - ); - assert_eq!( - res, - FoldRedelegatedBondsResult { - total_redelegated: token::Amount::from(5), - total_after_slashing: token::Amount::from(5), - } - ); - - // Test case 2 - let res = fold_and_slash_redelegated_bonds( - &storage, - ¶ms, - &eager_redel_bonds, - start_epoch, - &[test_slash], - |_| true, - ); - assert_eq!( - res, - FoldRedelegatedBondsResult { - total_redelegated: token::Amount::from(5), - total_after_slashing: token::Amount::zero(), - } - ); - - // Test case 3 - let alice_slash = Slash { - epoch: Epoch(6), - block_height: Default::default(), - r#type: SlashType::DuplicateVote, - rate: Dec::one(), - }; - validator_slashes_handle(&alice) - .push(&mut storage, alice_slash) - .unwrap(); - - let res = fold_and_slash_redelegated_bonds( - &storage, - ¶ms, - &eager_redel_bonds, - start_epoch, - &[], - |_| true, - ); - assert_eq!( - res, - FoldRedelegatedBondsResult { - total_redelegated: token::Amount::from(5), - total_after_slashing: token::Amount::from(3), - } - ); -} - -/// `slashRedelegationTest` -#[test] -fn test_slash_redelegation() { - let mut storage = TestWlStorage::default(); - let params = OwnedPosParams { - unbonding_len: 4, - ..Default::default() - }; - let alice = established_address_1(); - - let total_redelegated_unbonded = - validator_total_redelegated_unbonded_handle(&alice); - total_redelegated_unbonded - .at(&Epoch(13)) - .at(&Epoch(10)) - .at(&alice) - .insert(&mut storage, Epoch(7), token::Amount::from(2)) - .unwrap(); - - let slashes = validator_slashes_handle(&alice); - - let mut slashed_amounts_map = BTreeMap::from_iter([ - (Epoch(15), token::Amount::zero()), - (Epoch(16), token::Amount::zero()), - ]); - let empty_slash_amounts = slashed_amounts_map.clone(); - - // Test case 1 - slash_redelegation( - &storage, - ¶ms, - token::Amount::from(7), - Epoch(7), - Epoch(10), - &alice, - Epoch(14), - &slashes, - &total_redelegated_unbonded, - Dec::one(), - &mut slashed_amounts_map, - ) - .unwrap(); - assert_eq!( - slashed_amounts_map, - BTreeMap::from_iter([ - (Epoch(15), token::Amount::from(5)), - (Epoch(16), token::Amount::from(5)), - ]) - ); - - // Test case 2 - slashed_amounts_map = empty_slash_amounts.clone(); - slash_redelegation( - &storage, - ¶ms, - token::Amount::from(7), - Epoch(7), - Epoch(11), - &alice, - Epoch(14), - &slashes, - &total_redelegated_unbonded, - Dec::one(), - &mut slashed_amounts_map, - ) - .unwrap(); - assert_eq!( - slashed_amounts_map, - BTreeMap::from_iter([ - (Epoch(15), token::Amount::from(7)), - (Epoch(16), token::Amount::from(7)), - ]) - ); - - // Test case 3 - slashed_amounts_map = BTreeMap::from_iter([ - (Epoch(15), token::Amount::from(2)), - (Epoch(16), token::Amount::from(3)), - ]); - slash_redelegation( - &storage, - ¶ms, - token::Amount::from(7), - Epoch(7), - Epoch(10), - &alice, - Epoch(14), - &slashes, - &total_redelegated_unbonded, - Dec::one(), - &mut slashed_amounts_map, - ) - .unwrap(); - assert_eq!( - slashed_amounts_map, - BTreeMap::from_iter([ - (Epoch(15), token::Amount::from(7)), - (Epoch(16), token::Amount::from(8)), - ]) - ); - - // Test case 4 - slashes - .push( - &mut storage, - Slash { - epoch: Epoch(8), - block_height: Default::default(), - r#type: SlashType::DuplicateVote, - rate: Dec::one(), - }, - ) - .unwrap(); - slashed_amounts_map = empty_slash_amounts.clone(); - slash_redelegation( - &storage, - ¶ms, - token::Amount::from(7), - Epoch(7), - Epoch(10), - &alice, - Epoch(14), - &slashes, - &total_redelegated_unbonded, - Dec::one(), - &mut slashed_amounts_map, - ) - .unwrap(); - assert_eq!(slashed_amounts_map, empty_slash_amounts); - - // Test case 5 - slashes.pop(&mut storage).unwrap(); - slashes - .push( - &mut storage, - Slash { - epoch: Epoch(9), - block_height: Default::default(), - r#type: SlashType::DuplicateVote, - rate: Dec::one(), - }, - ) - .unwrap(); - slash_redelegation( - &storage, - ¶ms, - token::Amount::from(7), - Epoch(7), - Epoch(10), - &alice, - Epoch(14), - &slashes, - &total_redelegated_unbonded, - Dec::one(), - &mut slashed_amounts_map, - ) - .unwrap(); - assert_eq!(slashed_amounts_map, empty_slash_amounts); - - // Test case 6 - slashes - .push( - &mut storage, - Slash { - epoch: Epoch(8), - block_height: Default::default(), - r#type: SlashType::DuplicateVote, - rate: Dec::one(), - }, - ) - .unwrap(); - slash_redelegation( - &storage, - ¶ms, - token::Amount::from(7), - Epoch(7), - Epoch(10), - &alice, - Epoch(14), - &slashes, - &total_redelegated_unbonded, - Dec::one(), - &mut slashed_amounts_map, - ) - .unwrap(); - assert_eq!(slashed_amounts_map, empty_slash_amounts); -} - -/// `slashValidatorRedelegationTest` -#[test] -fn test_slash_validator_redelegation() { - let mut storage = TestWlStorage::default(); - let params = OwnedPosParams { - unbonding_len: 4, - ..Default::default() - }; - let gov_params = namada_core::ledger::governance::parameters::GovernanceParameters::default(); - gov_params.init_storage(&mut storage).unwrap(); - write_pos_params(&mut storage, ¶ms).unwrap(); - - let alice = established_address_1(); - let bob = established_address_2(); - - let total_redelegated_unbonded = - validator_total_redelegated_unbonded_handle(&alice); - total_redelegated_unbonded - .at(&Epoch(13)) - .at(&Epoch(10)) - .at(&alice) - .insert(&mut storage, Epoch(7), token::Amount::from(2)) - .unwrap(); - - let outgoing_redelegations = - validator_outgoing_redelegations_handle(&alice).at(&bob); - - let slashes = validator_slashes_handle(&alice); - - let mut slashed_amounts_map = BTreeMap::from_iter([ - (Epoch(15), token::Amount::zero()), - (Epoch(16), token::Amount::zero()), - ]); - let empty_slash_amounts = slashed_amounts_map.clone(); - - // Test case 1 - slash_validator_redelegation( - &storage, - ¶ms, - &alice, - Epoch(14), - &outgoing_redelegations, - &slashes, - &total_redelegated_unbonded, - Dec::one(), - &mut slashed_amounts_map, - ) - .unwrap(); - assert_eq!(slashed_amounts_map, empty_slash_amounts); - - // Test case 2 - total_redelegated_unbonded - .remove_all(&mut storage, &Epoch(13)) - .unwrap(); - slash_validator_redelegation( - &storage, - ¶ms, - &alice, - Epoch(14), - &outgoing_redelegations, - &slashes, - &total_redelegated_unbonded, - Dec::one(), - &mut slashed_amounts_map, - ) - .unwrap(); - assert_eq!(slashed_amounts_map, empty_slash_amounts); - - // Test case 3 - total_redelegated_unbonded - .at(&Epoch(13)) - .at(&Epoch(10)) - .at(&alice) - .insert(&mut storage, Epoch(7), token::Amount::from(2)) - .unwrap(); - outgoing_redelegations - .at(&Epoch(6)) - .insert(&mut storage, Epoch(8), token::Amount::from(7)) - .unwrap(); - slash_validator_redelegation( - &storage, - ¶ms, - &alice, - Epoch(14), - &outgoing_redelegations, - &slashes, - &total_redelegated_unbonded, - Dec::one(), - &mut slashed_amounts_map, - ) - .unwrap(); - assert_eq!( - slashed_amounts_map, - BTreeMap::from_iter([ - (Epoch(15), token::Amount::from(7)), - (Epoch(16), token::Amount::from(7)), - ]) - ); - - // Test case 4 - slashed_amounts_map = empty_slash_amounts.clone(); - outgoing_redelegations - .remove_all(&mut storage, &Epoch(6)) - .unwrap(); - outgoing_redelegations - .at(&Epoch(7)) - .insert(&mut storage, Epoch(8), token::Amount::from(7)) - .unwrap(); - slash_validator_redelegation( - &storage, - ¶ms, - &alice, - Epoch(14), - &outgoing_redelegations, - &slashes, - &total_redelegated_unbonded, - Dec::one(), - &mut slashed_amounts_map, - ) - .unwrap(); - assert_eq!( - slashed_amounts_map, - BTreeMap::from_iter([ - (Epoch(15), token::Amount::from(5)), - (Epoch(16), token::Amount::from(5)), - ]) - ); - - // Test case 5 - slashed_amounts_map = BTreeMap::from_iter([ - (Epoch(15), token::Amount::from(2)), - (Epoch(16), token::Amount::from(3)), - ]); - slash_validator_redelegation( - &storage, - ¶ms, - &alice, - Epoch(14), - &outgoing_redelegations, - &slashes, - &total_redelegated_unbonded, - Dec::one(), - &mut slashed_amounts_map, - ) - .unwrap(); - assert_eq!( - slashed_amounts_map, - BTreeMap::from_iter([ - (Epoch(15), token::Amount::from(7)), - (Epoch(16), token::Amount::from(8)), - ]) - ); - - // Test case 6 - slashed_amounts_map = empty_slash_amounts.clone(); - slashes - .push( - &mut storage, - Slash { - epoch: Epoch(8), - block_height: Default::default(), - r#type: SlashType::DuplicateVote, - rate: Dec::one(), - }, - ) - .unwrap(); - slash_validator_redelegation( - &storage, - ¶ms, - &alice, - Epoch(14), - &outgoing_redelegations, - &slashes, - &total_redelegated_unbonded, - Dec::one(), - &mut slashed_amounts_map, - ) - .unwrap(); - assert_eq!(slashed_amounts_map, empty_slash_amounts); -} - -/// `slashValidatorTest` -#[test] -fn test_slash_validator() { - let mut storage = TestWlStorage::default(); - let params = OwnedPosParams { - unbonding_len: 4, - ..Default::default() - }; - let gov_params = namada_core::ledger::governance::parameters::GovernanceParameters::default(); - gov_params.init_storage(&mut storage).unwrap(); - write_pos_params(&mut storage, ¶ms).unwrap(); - - let alice = established_address_1(); - let bob = established_address_2(); - - let total_bonded = total_bonded_handle(&bob); - let total_unbonded = total_unbonded_handle(&bob); - let total_redelegated_bonded = - validator_total_redelegated_bonded_handle(&bob); - let total_redelegated_unbonded = - validator_total_redelegated_unbonded_handle(&bob); - - let infraction_stake = token::Amount::from(23); - - let initial_stakes = BTreeMap::from_iter([ - (Epoch(11), infraction_stake), - (Epoch(12), infraction_stake), - (Epoch(13), infraction_stake), - ]); - let mut exp_res = initial_stakes.clone(); - - let current_epoch = Epoch(10); - let infraction_epoch = - current_epoch - params.slash_processing_epoch_offset(); - let processing_epoch = current_epoch.next(); - let slash_rate = Dec::one(); - - // Test case 1 - println!("\nTEST 1:"); - - total_bonded - .set(&mut storage, 23.into(), infraction_epoch - 2, 0) - .unwrap(); - let res = slash_validator( - &storage, - ¶ms, - &bob, - slash_rate, - processing_epoch, - &Default::default(), - ) - .unwrap(); - assert_eq!(res, exp_res); - - // Test case 2 - println!("\nTEST 2:"); - total_bonded - .set(&mut storage, 17.into(), infraction_epoch - 2, 0) - .unwrap(); - total_unbonded - .at(&(current_epoch + params.pipeline_len)) - .insert(&mut storage, infraction_epoch - 2, 6.into()) - .unwrap(); - let res = slash_validator( - &storage, - ¶ms, - &bob, - slash_rate, - processing_epoch, - &Default::default(), - ) - .unwrap(); - exp_res.insert(Epoch(12), 17.into()); - exp_res.insert(Epoch(13), 17.into()); - assert_eq!(res, exp_res); - - // Test case 3 - println!("\nTEST 3:"); - total_redelegated_bonded - .at(&infraction_epoch.prev()) - .at(&alice) - .insert(&mut storage, Epoch(2), 5.into()) - .unwrap(); - total_redelegated_bonded - .at(&infraction_epoch.prev()) - .at(&alice) - .insert(&mut storage, Epoch(3), 1.into()) - .unwrap(); - - let res = slash_validator( - &storage, - ¶ms, - &bob, - slash_rate, - processing_epoch, - &Default::default(), - ) - .unwrap(); - assert_eq!(res, exp_res); - - // Test case 4 - println!("\nTEST 4:"); - total_unbonded_handle(&bob) - .at(&(current_epoch + params.pipeline_len)) - .remove(&mut storage, &(infraction_epoch - 2)) - .unwrap(); - total_unbonded_handle(&bob) - .at(&(current_epoch + params.pipeline_len)) - .insert(&mut storage, infraction_epoch - 1, 6.into()) - .unwrap(); - total_redelegated_unbonded - .at(&(current_epoch + params.pipeline_len)) - .at(&infraction_epoch.prev()) - .at(&alice) - .insert(&mut storage, Epoch(2), 5.into()) - .unwrap(); - total_redelegated_unbonded - .at(&(current_epoch + params.pipeline_len)) - .at(&infraction_epoch.prev()) - .at(&alice) - .insert(&mut storage, Epoch(3), 1.into()) - .unwrap(); - let res = slash_validator( - &storage, - ¶ms, - &bob, - slash_rate, - processing_epoch, - &Default::default(), - ) - .unwrap(); - assert_eq!(res, exp_res); - - // Test case 5 - println!("\nTEST 5:"); - total_bonded_handle(&bob) - .set(&mut storage, 19.into(), infraction_epoch - 2, 0) - .unwrap(); - total_unbonded_handle(&bob) - .at(&(current_epoch + params.pipeline_len)) - .insert(&mut storage, infraction_epoch - 1, 4.into()) - .unwrap(); - total_redelegated_bonded - .at(¤t_epoch) - .at(&alice) - .insert(&mut storage, Epoch(2), token::Amount::from(1)) - .unwrap(); - total_redelegated_unbonded - .at(&(current_epoch + params.pipeline_len)) - .at(&infraction_epoch.prev()) - .at(&alice) - .remove(&mut storage, &Epoch(3)) - .unwrap(); - total_redelegated_unbonded - .at(&(current_epoch + params.pipeline_len)) - .at(&infraction_epoch.prev()) - .at(&alice) - .insert(&mut storage, Epoch(2), 4.into()) - .unwrap(); - let res = slash_validator( - &storage, - ¶ms, - &bob, - slash_rate, - processing_epoch, - &Default::default(), - ) - .unwrap(); - exp_res.insert(Epoch(12), 19.into()); - exp_res.insert(Epoch(13), 19.into()); - assert_eq!(res, exp_res); - - // Test case 6 - println!("\nTEST 6:"); - total_unbonded_handle(&bob) - .remove_all(&mut storage, &(current_epoch + params.pipeline_len)) - .unwrap(); - total_redelegated_unbonded - .remove_all(&mut storage, &(current_epoch + params.pipeline_len)) - .unwrap(); - total_redelegated_bonded - .remove_all(&mut storage, ¤t_epoch) - .unwrap(); - total_bonded_handle(&bob) - .set(&mut storage, 23.into(), infraction_epoch - 2, 0) - .unwrap(); - total_bonded_handle(&bob) - .set(&mut storage, 6.into(), current_epoch, 0) - .unwrap(); - - let res = slash_validator( - &storage, - ¶ms, - &bob, - slash_rate, - processing_epoch, - &Default::default(), - ) - .unwrap(); - exp_res = initial_stakes; - assert_eq!(res, exp_res); - - // Test case 7 - println!("\nTEST 7:"); - total_bonded - .get_data_handler() - .remove(&mut storage, ¤t_epoch) - .unwrap(); - total_unbonded - .at(¤t_epoch.next()) - .insert(&mut storage, current_epoch, 6.into()) - .unwrap(); - let res = slash_validator( - &storage, - ¶ms, - &bob, - slash_rate, - processing_epoch, - &Default::default(), - ) - .unwrap(); - assert_eq!(res, exp_res); - - // Test case 8 - println!("\nTEST 8:"); - total_bonded - .get_data_handler() - .insert(&mut storage, current_epoch, 3.into()) - .unwrap(); - total_unbonded - .at(¤t_epoch.next()) - .insert(&mut storage, current_epoch, 3.into()) - .unwrap(); - let res = slash_validator( - &storage, - ¶ms, - &bob, - slash_rate, - processing_epoch, - &Default::default(), - ) - .unwrap(); - assert_eq!(res, exp_res); - - // Test case 9 - println!("\nTEST 9:"); - total_unbonded - .remove_all(&mut storage, ¤t_epoch.next()) - .unwrap(); - total_bonded - .set(&mut storage, 6.into(), current_epoch, 0) - .unwrap(); - total_redelegated_bonded - .at(¤t_epoch) - .at(&alice) - .insert(&mut storage, 2.into(), 5.into()) - .unwrap(); - total_redelegated_bonded - .at(¤t_epoch) - .at(&alice) - .insert(&mut storage, 3.into(), 1.into()) - .unwrap(); - let res = slash_validator( - &storage, - ¶ms, - &bob, - slash_rate, - processing_epoch, - &Default::default(), - ) - .unwrap(); - assert_eq!(res, exp_res); - - // Test case 10 - println!("\nTEST 10:"); - total_redelegated_bonded - .remove_all(&mut storage, ¤t_epoch) - .unwrap(); - total_bonded - .get_data_handler() - .remove(&mut storage, ¤t_epoch) - .unwrap(); - total_redelegated_unbonded - .at(¤t_epoch.next()) - .at(¤t_epoch) - .at(&alice) - .insert(&mut storage, 2.into(), 5.into()) - .unwrap(); - total_redelegated_unbonded - .at(¤t_epoch.next()) - .at(¤t_epoch) - .at(&alice) - .insert(&mut storage, 3.into(), 1.into()) - .unwrap(); - let res = slash_validator( - &storage, - ¶ms, - &bob, - slash_rate, - processing_epoch, - &Default::default(), - ) - .unwrap(); - assert_eq!(res, exp_res); - - // Test case 11 - println!("\nTEST 11:"); - total_bonded - .set(&mut storage, 2.into(), current_epoch, 0) - .unwrap(); - total_redelegated_unbonded - .at(¤t_epoch.next()) - .at(¤t_epoch) - .at(&alice) - .insert(&mut storage, 2.into(), 4.into()) - .unwrap(); - total_redelegated_unbonded - .at(¤t_epoch.next()) - .at(¤t_epoch) - .at(&alice) - .remove(&mut storage, &3.into()) - .unwrap(); - total_redelegated_bonded - .at(¤t_epoch) - .at(&alice) - .insert(&mut storage, 2.into(), 1.into()) - .unwrap(); - total_redelegated_bonded - .at(¤t_epoch) - .at(&alice) - .insert(&mut storage, 3.into(), 1.into()) - .unwrap(); - let res = slash_validator( - &storage, - ¶ms, - &bob, - slash_rate, - processing_epoch, - &Default::default(), - ) - .unwrap(); - assert_eq!(res, exp_res); - - // Test case 12 - println!("\nTEST 12:"); - total_bonded - .set(&mut storage, 6.into(), current_epoch, 0) - .unwrap(); - total_bonded - .set(&mut storage, 2.into(), current_epoch.next(), 0) - .unwrap(); - total_redelegated_bonded - .remove_all(&mut storage, ¤t_epoch) - .unwrap(); - total_redelegated_bonded - .at(¤t_epoch.next()) - .at(&alice) - .insert(&mut storage, 2.into(), 1.into()) - .unwrap(); - total_redelegated_bonded - .at(¤t_epoch.next()) - .at(&alice) - .insert(&mut storage, 3.into(), 1.into()) - .unwrap(); - let res = slash_validator( - &storage, - ¶ms, - &bob, - slash_rate, - processing_epoch, - &Default::default(), - ) - .unwrap(); - assert_eq!(res, exp_res); - - // Test case 13 - println!("\nTEST 13:"); - validator_slashes_handle(&bob) - .push( - &mut storage, - Slash { - epoch: infraction_epoch.prev(), - block_height: 0, - r#type: SlashType::DuplicateVote, - rate: Dec::one(), - }, - ) - .unwrap(); - total_redelegated_unbonded - .remove_all(&mut storage, ¤t_epoch.next()) - .unwrap(); - total_bonded - .get_data_handler() - .remove(&mut storage, ¤t_epoch.next()) - .unwrap(); - total_redelegated_bonded - .remove_all(&mut storage, ¤t_epoch.next()) - .unwrap(); - let res = slash_validator( - &storage, - ¶ms, - &bob, - slash_rate, - processing_epoch, - &Default::default(), - ) - .unwrap(); - exp_res.insert(Epoch(11), 0.into()); - exp_res.insert(Epoch(12), 0.into()); - exp_res.insert(Epoch(13), 0.into()); - assert_eq!(res, exp_res); -} - -/// `computeAmountAfterSlashingUnbondTest` -#[test] -fn compute_amount_after_slashing_unbond_test() { - let mut storage = TestWlStorage::default(); - let params = OwnedPosParams { - unbonding_len: 4, - ..Default::default() - }; - - // Test data - let alice = established_address_1(); - let bob = established_address_2(); - let unbonds: BTreeMap = BTreeMap::from_iter([ - ((Epoch(2)), token::Amount::from(5)), - ((Epoch(4)), token::Amount::from(6)), - ]); - let redelegated_unbonds: EagerRedelegatedUnbonds = BTreeMap::from_iter([( - Epoch(2), - BTreeMap::from_iter([( - alice.clone(), - BTreeMap::from_iter([(Epoch(1), token::Amount::from(1))]), - )]), - )]); - - // Test case 1 - let slashes = vec![]; - let result = compute_amount_after_slashing_unbond( - &storage, - ¶ms, - &unbonds, - &redelegated_unbonds, - slashes, - ) - .unwrap(); - assert_eq!(result.sum, 11.into()); - itertools::assert_equal( - result.epoch_map, - [(2.into(), 5.into()), (4.into(), 6.into())], - ); - - // Test case 2 - let bob_slash = Slash { - epoch: Epoch(5), - block_height: Default::default(), - r#type: SlashType::DuplicateVote, - rate: Dec::one(), - }; - let slashes = vec![bob_slash.clone()]; - validator_slashes_handle(&bob) - .push(&mut storage, bob_slash) - .unwrap(); - let result = compute_amount_after_slashing_unbond( - &storage, - ¶ms, - &unbonds, - &redelegated_unbonds, - slashes, - ) - .unwrap(); - assert_eq!(result.sum, 0.into()); - itertools::assert_equal( - result.epoch_map, - [(2.into(), 0.into()), (4.into(), 0.into())], - ); - - // Test case 3 - let alice_slash = Slash { - epoch: Epoch(0), - block_height: Default::default(), - r#type: SlashType::DuplicateVote, - rate: Dec::one(), - }; - let slashes = vec![alice_slash.clone()]; - validator_slashes_handle(&alice) - .push(&mut storage, alice_slash) - .unwrap(); - validator_slashes_handle(&bob).pop(&mut storage).unwrap(); - let result = compute_amount_after_slashing_unbond( - &storage, - ¶ms, - &unbonds, - &redelegated_unbonds, - slashes, - ) - .unwrap(); - assert_eq!(result.sum, 11.into()); - itertools::assert_equal( - result.epoch_map, - [(2.into(), 5.into()), (4.into(), 6.into())], - ); - - // Test case 4 - let alice_slash = Slash { - epoch: Epoch(1), - block_height: Default::default(), - r#type: SlashType::DuplicateVote, - rate: Dec::one(), - }; - let slashes = vec![alice_slash.clone()]; - validator_slashes_handle(&alice).pop(&mut storage).unwrap(); - validator_slashes_handle(&alice) - .push(&mut storage, alice_slash) - .unwrap(); - let result = compute_amount_after_slashing_unbond( - &storage, - ¶ms, - &unbonds, - &redelegated_unbonds, - slashes, - ) - .unwrap(); - assert_eq!(result.sum, 10.into()); - itertools::assert_equal( - result.epoch_map, - [(2.into(), 4.into()), (4.into(), 6.into())], - ); -} - -/// `computeAmountAfterSlashingWithdrawTest` -#[test] -fn compute_amount_after_slashing_withdraw_test() { - let mut storage = TestWlStorage::default(); - let params = OwnedPosParams { - unbonding_len: 4, - ..Default::default() - }; - - // Test data - let alice = established_address_1(); - let bob = established_address_2(); - let unbonds_and_redelegated_unbonds: BTreeMap< - (Epoch, Epoch), - (token::Amount, EagerRedelegatedBondsMap), - > = BTreeMap::from_iter([ - ( - (Epoch(2), Epoch(20)), - ( - // unbond - token::Amount::from(5), - // redelegations - BTreeMap::from_iter([( - alice.clone(), - BTreeMap::from_iter([(Epoch(1), token::Amount::from(1))]), - )]), - ), - ), - ( - (Epoch(4), Epoch(20)), - ( - // unbond - token::Amount::from(6), - // redelegations - BTreeMap::default(), - ), - ), - ]); - - // Test case 1 - let slashes = vec![]; - let result = compute_amount_after_slashing_withdraw( - &storage, - ¶ms, - &unbonds_and_redelegated_unbonds, - slashes, - ) - .unwrap(); - assert_eq!(result.sum, 11.into()); - itertools::assert_equal( - result.epoch_map, - [(2.into(), 5.into()), (4.into(), 6.into())], - ); - - // Test case 2 - let bob_slash = Slash { - epoch: Epoch(5), - block_height: Default::default(), - r#type: SlashType::DuplicateVote, - rate: Dec::one(), - }; - let slashes = vec![bob_slash.clone()]; - validator_slashes_handle(&bob) - .push(&mut storage, bob_slash) - .unwrap(); - let result = compute_amount_after_slashing_withdraw( - &storage, - ¶ms, - &unbonds_and_redelegated_unbonds, - slashes, - ) - .unwrap(); - assert_eq!(result.sum, 0.into()); - itertools::assert_equal( - result.epoch_map, - [(2.into(), 0.into()), (4.into(), 0.into())], - ); - - // Test case 3 - let alice_slash = Slash { - epoch: Epoch(0), - block_height: Default::default(), - r#type: SlashType::DuplicateVote, - rate: Dec::one(), - }; - let slashes = vec![alice_slash.clone()]; - validator_slashes_handle(&alice) - .push(&mut storage, alice_slash) - .unwrap(); - validator_slashes_handle(&bob).pop(&mut storage).unwrap(); - let result = compute_amount_after_slashing_withdraw( - &storage, - ¶ms, - &unbonds_and_redelegated_unbonds, - slashes, - ) - .unwrap(); - assert_eq!(result.sum, 11.into()); - itertools::assert_equal( - result.epoch_map, - [(2.into(), 5.into()), (4.into(), 6.into())], - ); - - // Test case 4 - let alice_slash = Slash { - epoch: Epoch(1), - block_height: Default::default(), - r#type: SlashType::DuplicateVote, - rate: Dec::one(), - }; - let slashes = vec![alice_slash.clone()]; - validator_slashes_handle(&alice).pop(&mut storage).unwrap(); - validator_slashes_handle(&alice) - .push(&mut storage, alice_slash) - .unwrap(); - let result = compute_amount_after_slashing_withdraw( - &storage, - ¶ms, - &unbonds_and_redelegated_unbonds, - slashes, - ) - .unwrap(); - assert_eq!(result.sum, 10.into()); - itertools::assert_equal( - result.epoch_map, - [(2.into(), 4.into()), (4.into(), 6.into())], - ); -} - -fn arb_redelegation_amounts( - max_delegation: u64, -) -> impl Strategy { - let arb_delegation = arb_amount_non_zero_ceiled(max_delegation); - let amounts = arb_delegation.prop_flat_map(move |amount_delegate| { - let amount_redelegate = arb_amount_non_zero_ceiled(max( - 1, - u64::try_from(amount_delegate.raw_amount()).unwrap() - 1, - )); - (Just(amount_delegate), amount_redelegate) - }); - amounts.prop_flat_map(move |(amount_delegate, amount_redelegate)| { - let amount_unbond = arb_amount_non_zero_ceiled(max( - 1, - u64::try_from(amount_redelegate.raw_amount()).unwrap() - 1, - )); - ( - Just(amount_delegate), - Just(amount_redelegate), - amount_unbond, - ) - }) -} - -fn test_simple_redelegation_aux( - mut validators: Vec, - amount_delegate: token::Amount, - amount_redelegate: token::Amount, - amount_unbond: token::Amount, -) { - validators.sort_by(|a, b| b.tokens.cmp(&a.tokens)); - - let src_validator = validators[0].address.clone(); - let dest_validator = validators[1].address.clone(); - - let mut storage = TestWlStorage::default(); - let params = OwnedPosParams { - unbonding_len: 4, - ..Default::default() - }; - - // Genesis - let mut current_epoch = storage.storage.block.epoch; - let params = test_init_genesis( - &mut storage, - params, - validators.clone().into_iter(), - current_epoch, - ) - .unwrap(); - storage.commit_block().unwrap(); - - // Get a delegator with some tokens - let staking_token = staking_token_address(&storage); - let delegator = address::testing::gen_implicit_address(); - let del_balance = token::Amount::from_uint(1_000_000, 0).unwrap(); - credit_tokens(&mut storage, &staking_token, &delegator, del_balance) - .unwrap(); - - // Ensure that we cannot redelegate with the same src and dest validator - let err = super::redelegate_tokens( - &mut storage, - &delegator, - &src_validator, - &src_validator, - current_epoch, - amount_redelegate, - ) - .unwrap_err(); - let err_str = err.to_string(); - assert_matches!( - err.downcast::().unwrap().deref(), - RedelegationError::RedelegationSrcEqDest, - "Redelegation with the same src and dest validator must be rejected, \ - got {err_str}", - ); - - for _ in 0..5 { - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - } - - let init_epoch = current_epoch; - - // Delegate in epoch 1 to src_validator - println!( - "\nBONDING {} TOKENS TO {}\n", - amount_delegate.to_string_native(), - &src_validator - ); - super::bond_tokens( - &mut storage, - Some(&delegator), - &src_validator, - amount_delegate, - current_epoch, - None, - ) - .unwrap(); - - println!("\nAFTER DELEGATION\n"); - let bonds = bond_handle(&delegator, &src_validator) - .get_data_handler() - .collect_map(&storage) - .unwrap(); - let bonds_dest = bond_handle(&delegator, &dest_validator) - .get_data_handler() - .collect_map(&storage) - .unwrap(); - let unbonds = unbond_handle(&delegator, &src_validator) - .collect_map(&storage) - .unwrap(); - let tot_bonds = total_bonded_handle(&src_validator) - .get_data_handler() - .collect_map(&storage) - .unwrap(); - let tot_unbonds = total_unbonded_handle(&src_validator) - .collect_map(&storage) - .unwrap(); - dbg!(&bonds, &bonds_dest, &unbonds, &tot_bonds, &tot_unbonds); - - // Advance three epochs - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - - // Redelegate in epoch 3 - println!( - "\nREDELEGATING {} TOKENS TO {}\n", - amount_redelegate.to_string_native(), - &dest_validator - ); - - super::redelegate_tokens( - &mut storage, - &delegator, - &src_validator, - &dest_validator, - current_epoch, - amount_redelegate, - ) - .unwrap(); - - println!("\nAFTER REDELEGATION\n"); - println!("\nDELEGATOR\n"); - let bonds_src = bond_handle(&delegator, &src_validator) - .get_data_handler() - .collect_map(&storage) - .unwrap(); - let bonds_dest = bond_handle(&delegator, &dest_validator) - .get_data_handler() - .collect_map(&storage) - .unwrap(); - let unbonds_src = unbond_handle(&delegator, &src_validator) - .collect_map(&storage) - .unwrap(); - let unbonds_dest = unbond_handle(&delegator, &dest_validator) - .collect_map(&storage) - .unwrap(); - let redel_bonds = delegator_redelegated_bonds_handle(&delegator) - .collect_map(&storage) - .unwrap(); - let redel_unbonds = delegator_redelegated_unbonds_handle(&delegator) - .collect_map(&storage) - .unwrap(); - - dbg!( - &bonds_src, - &bonds_dest, - &unbonds_src, - &unbonds_dest, - &redel_bonds, - &redel_unbonds - ); - - // Dest val - println!("\nDEST VALIDATOR\n"); - - let incoming_redels_dest = - validator_incoming_redelegations_handle(&dest_validator) - .collect_map(&storage) - .unwrap(); - let outgoing_redels_dest = - validator_outgoing_redelegations_handle(&dest_validator) - .collect_map(&storage) - .unwrap(); - let tot_bonds_dest = total_bonded_handle(&dest_validator) - .get_data_handler() - .collect_map(&storage) - .unwrap(); - let tot_unbonds_dest = total_unbonded_handle(&dest_validator) - .collect_map(&storage) - .unwrap(); - let tot_redel_bonds_dest = - validator_total_redelegated_bonded_handle(&dest_validator) - .collect_map(&storage) - .unwrap(); - let tot_redel_unbonds_dest = - validator_total_redelegated_unbonded_handle(&dest_validator) - .collect_map(&storage) - .unwrap(); - dbg!( - &incoming_redels_dest, - &outgoing_redels_dest, - &tot_bonds_dest, - &tot_unbonds_dest, - &tot_redel_bonds_dest, - &tot_redel_unbonds_dest - ); - - // Src val - println!("\nSRC VALIDATOR\n"); - - let incoming_redels_src = - validator_incoming_redelegations_handle(&src_validator) - .collect_map(&storage) - .unwrap(); - let outgoing_redels_src = - validator_outgoing_redelegations_handle(&src_validator) - .collect_map(&storage) - .unwrap(); - let tot_bonds_src = total_bonded_handle(&src_validator) - .get_data_handler() - .collect_map(&storage) - .unwrap(); - let tot_unbonds_src = total_unbonded_handle(&src_validator) - .collect_map(&storage) - .unwrap(); - let tot_redel_bonds_src = - validator_total_redelegated_bonded_handle(&src_validator) - .collect_map(&storage) - .unwrap(); - let tot_redel_unbonds_src = - validator_total_redelegated_unbonded_handle(&src_validator) - .collect_map(&storage) - .unwrap(); - dbg!( - &incoming_redels_src, - &outgoing_redels_src, - &tot_bonds_src, - &tot_unbonds_src, - &tot_redel_bonds_src, - &tot_redel_unbonds_src - ); - - // Checks - let redelegated = delegator_redelegated_bonds_handle(&delegator) - .at(&dest_validator) - .at(&(current_epoch + params.pipeline_len)) - .at(&src_validator) - .get(&storage, &(init_epoch + params.pipeline_len)) - .unwrap() - .unwrap(); - assert_eq!(redelegated, amount_redelegate); - - let redel_start_epoch = - validator_incoming_redelegations_handle(&dest_validator) - .get(&storage, &delegator) - .unwrap() - .unwrap(); - assert_eq!(redel_start_epoch, current_epoch + params.pipeline_len); - - let redelegated = validator_outgoing_redelegations_handle(&src_validator) - .at(&dest_validator) - .at(¤t_epoch.prev()) - .get(&storage, ¤t_epoch) - .unwrap() - .unwrap(); - assert_eq!(redelegated, amount_redelegate); - - // Advance three epochs - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - - // Unbond in epoch 5 from dest_validator - println!( - "\nUNBONDING {} TOKENS FROM {}\n", - amount_unbond.to_string_native(), - &dest_validator - ); - let _ = unbond_tokens( - &mut storage, - Some(&delegator), - &dest_validator, - amount_unbond, - current_epoch, - false, - ) - .unwrap(); - - println!("\nAFTER UNBONDING\n"); - println!("\nDELEGATOR\n"); - - let bonds_src = bond_handle(&delegator, &src_validator) - .get_data_handler() - .collect_map(&storage) - .unwrap(); - let bonds_dest = bond_handle(&delegator, &dest_validator) - .get_data_handler() - .collect_map(&storage) - .unwrap(); - let unbonds_src = unbond_handle(&delegator, &src_validator) - .collect_map(&storage) - .unwrap(); - let unbonds_dest = unbond_handle(&delegator, &dest_validator) - .collect_map(&storage) - .unwrap(); - let redel_bonds = delegator_redelegated_bonds_handle(&delegator) - .collect_map(&storage) - .unwrap(); - let redel_unbonds = delegator_redelegated_unbonds_handle(&delegator) - .collect_map(&storage) - .unwrap(); - - dbg!( - &bonds_src, - &bonds_dest, - &unbonds_src, - &unbonds_dest, - &redel_bonds, - &redel_unbonds - ); - - println!("\nDEST VALIDATOR\n"); - - let incoming_redels_dest = - validator_incoming_redelegations_handle(&dest_validator) - .collect_map(&storage) - .unwrap(); - let outgoing_redels_dest = - validator_outgoing_redelegations_handle(&dest_validator) - .collect_map(&storage) - .unwrap(); - let tot_bonds_dest = total_bonded_handle(&dest_validator) - .get_data_handler() - .collect_map(&storage) - .unwrap(); - let tot_unbonds_dest = total_unbonded_handle(&dest_validator) - .collect_map(&storage) - .unwrap(); - let tot_redel_bonds_dest = - validator_total_redelegated_bonded_handle(&dest_validator) - .collect_map(&storage) - .unwrap(); - let tot_redel_unbonds_dest = - validator_total_redelegated_unbonded_handle(&dest_validator) - .collect_map(&storage) - .unwrap(); - dbg!( - &incoming_redels_dest, - &outgoing_redels_dest, - &tot_bonds_dest, - &tot_unbonds_dest, - &tot_redel_bonds_dest, - &tot_redel_unbonds_dest - ); - - let bond_start = init_epoch + params.pipeline_len; - let redelegation_end = bond_start + params.pipeline_len + 1u64; - let unbond_end = - redelegation_end + params.withdrawable_epoch_offset() + 1u64; - let unbond_materialized = redelegation_end + params.pipeline_len + 1u64; - - // Checks - let redelegated_remaining = delegator_redelegated_bonds_handle(&delegator) - .at(&dest_validator) - .at(&redelegation_end) - .at(&src_validator) - .get(&storage, &bond_start) - .unwrap() - .unwrap_or_default(); - assert_eq!(redelegated_remaining, amount_redelegate - amount_unbond); - - let redel_unbonded = delegator_redelegated_unbonds_handle(&delegator) - .at(&dest_validator) - .at(&redelegation_end) - .at(&unbond_end) - .at(&src_validator) - .get(&storage, &bond_start) - .unwrap() - .unwrap(); - assert_eq!(redel_unbonded, amount_unbond); - - dbg!(unbond_materialized, redelegation_end, bond_start); - let total_redel_unbonded = - validator_total_redelegated_unbonded_handle(&dest_validator) - .at(&unbond_materialized) - .at(&redelegation_end) - .at(&src_validator) - .get(&storage, &bond_start) - .unwrap() - .unwrap(); - assert_eq!(total_redel_unbonded, amount_unbond); - - // Advance to withdrawal epoch - loop { - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - if current_epoch == unbond_end { - break; - } - } - - // Withdraw - withdraw_tokens( - &mut storage, - Some(&delegator), - &dest_validator, - current_epoch, - ) - .unwrap(); - - assert!( - delegator_redelegated_unbonds_handle(&delegator) - .at(&dest_validator) - .is_empty(&storage) - .unwrap() - ); - - let delegator_balance = storage - .read::(&token::balance_key(&staking_token, &delegator)) - .unwrap() - .unwrap_or_default(); - assert_eq!( - delegator_balance, - del_balance - amount_delegate + amount_unbond - ); -} - -fn test_redelegation_with_slashing_aux( - mut validators: Vec, - amount_delegate: token::Amount, - amount_redelegate: token::Amount, - amount_unbond: token::Amount, -) { - validators.sort_by(|a, b| b.tokens.cmp(&a.tokens)); - - let src_validator = validators[0].address.clone(); - let dest_validator = validators[1].address.clone(); - - let mut storage = TestWlStorage::default(); - let params = OwnedPosParams { - unbonding_len: 4, - // Avoid empty consensus set by removing the threshold - validator_stake_threshold: token::Amount::zero(), - ..Default::default() - }; - - // Genesis - let mut current_epoch = storage.storage.block.epoch; - let params = test_init_genesis( - &mut storage, - params, - validators.clone().into_iter(), - current_epoch, - ) - .unwrap(); - storage.commit_block().unwrap(); - - // Get a delegator with some tokens - let staking_token = staking_token_address(&storage); - let delegator = address::testing::gen_implicit_address(); - let del_balance = token::Amount::from_uint(1_000_000, 0).unwrap(); - credit_tokens(&mut storage, &staking_token, &delegator, del_balance) - .unwrap(); - - for _ in 0..5 { - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - } - - let init_epoch = current_epoch; - - // Delegate in epoch 5 to src_validator - println!( - "\nBONDING {} TOKENS TO {}\n", - amount_delegate.to_string_native(), - &src_validator - ); - super::bond_tokens( - &mut storage, - Some(&delegator), - &src_validator, - amount_delegate, - current_epoch, - None, - ) - .unwrap(); - - println!("\nAFTER DELEGATION\n"); - let bonds = bond_handle(&delegator, &src_validator) - .get_data_handler() - .collect_map(&storage) - .unwrap(); - let bonds_dest = bond_handle(&delegator, &dest_validator) - .get_data_handler() - .collect_map(&storage) - .unwrap(); - let unbonds = unbond_handle(&delegator, &src_validator) - .collect_map(&storage) - .unwrap(); - let tot_bonds = total_bonded_handle(&src_validator) - .get_data_handler() - .collect_map(&storage) - .unwrap(); - let tot_unbonds = total_unbonded_handle(&src_validator) - .collect_map(&storage) - .unwrap(); - dbg!(&bonds, &bonds_dest, &unbonds, &tot_bonds, &tot_unbonds); - - // Advance three epochs - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - - // Redelegate in epoch 8 - println!( - "\nREDELEGATING {} TOKENS TO {}\n", - amount_redelegate.to_string_native(), - &dest_validator - ); - - super::redelegate_tokens( - &mut storage, - &delegator, - &src_validator, - &dest_validator, - current_epoch, - amount_redelegate, - ) - .unwrap(); - - println!("\nAFTER REDELEGATION\n"); - println!("\nDELEGATOR\n"); - let bonds_src = bond_handle(&delegator, &src_validator) - .get_data_handler() - .collect_map(&storage) - .unwrap(); - let bonds_dest = bond_handle(&delegator, &dest_validator) - .get_data_handler() - .collect_map(&storage) - .unwrap(); - let unbonds_src = unbond_handle(&delegator, &src_validator) - .collect_map(&storage) - .unwrap(); - let unbonds_dest = unbond_handle(&delegator, &dest_validator) - .collect_map(&storage) - .unwrap(); - let redel_bonds = delegator_redelegated_bonds_handle(&delegator) - .collect_map(&storage) - .unwrap(); - let redel_unbonds = delegator_redelegated_unbonds_handle(&delegator) - .collect_map(&storage) - .unwrap(); - - dbg!( - &bonds_src, - &bonds_dest, - &unbonds_src, - &unbonds_dest, - &redel_bonds, - &redel_unbonds - ); - - // Dest val - println!("\nDEST VALIDATOR\n"); - - let incoming_redels_dest = - validator_incoming_redelegations_handle(&dest_validator) - .collect_map(&storage) - .unwrap(); - let outgoing_redels_dest = - validator_outgoing_redelegations_handle(&dest_validator) - .collect_map(&storage) - .unwrap(); - let tot_bonds_dest = total_bonded_handle(&dest_validator) - .get_data_handler() - .collect_map(&storage) - .unwrap(); - let tot_unbonds_dest = total_unbonded_handle(&dest_validator) - .collect_map(&storage) - .unwrap(); - let tot_redel_bonds_dest = - validator_total_redelegated_bonded_handle(&dest_validator) - .collect_map(&storage) - .unwrap(); - let tot_redel_unbonds_dest = - validator_total_redelegated_unbonded_handle(&dest_validator) - .collect_map(&storage) - .unwrap(); - dbg!( - &incoming_redels_dest, - &outgoing_redels_dest, - &tot_bonds_dest, - &tot_unbonds_dest, - &tot_redel_bonds_dest, - &tot_redel_unbonds_dest - ); - - // Src val - println!("\nSRC VALIDATOR\n"); - - let incoming_redels_src = - validator_incoming_redelegations_handle(&src_validator) - .collect_map(&storage) - .unwrap(); - let outgoing_redels_src = - validator_outgoing_redelegations_handle(&src_validator) - .collect_map(&storage) - .unwrap(); - let tot_bonds_src = total_bonded_handle(&src_validator) - .get_data_handler() - .collect_map(&storage) - .unwrap(); - let tot_unbonds_src = total_unbonded_handle(&src_validator) - .collect_map(&storage) - .unwrap(); - let tot_redel_bonds_src = - validator_total_redelegated_bonded_handle(&src_validator) - .collect_map(&storage) - .unwrap(); - let tot_redel_unbonds_src = - validator_total_redelegated_unbonded_handle(&src_validator) - .collect_map(&storage) - .unwrap(); - dbg!( - &incoming_redels_src, - &outgoing_redels_src, - &tot_bonds_src, - &tot_unbonds_src, - &tot_redel_bonds_src, - &tot_redel_unbonds_src - ); - - // Checks - let redelegated = delegator_redelegated_bonds_handle(&delegator) - .at(&dest_validator) - .at(&(current_epoch + params.pipeline_len)) - .at(&src_validator) - .get(&storage, &(init_epoch + params.pipeline_len)) - .unwrap() - .unwrap(); - assert_eq!(redelegated, amount_redelegate); - - let redel_start_epoch = - validator_incoming_redelegations_handle(&dest_validator) - .get(&storage, &delegator) - .unwrap() - .unwrap(); - assert_eq!(redel_start_epoch, current_epoch + params.pipeline_len); - - let redelegated = validator_outgoing_redelegations_handle(&src_validator) - .at(&dest_validator) - .at(¤t_epoch.prev()) - .get(&storage, ¤t_epoch) - .unwrap() - .unwrap(); - assert_eq!(redelegated, amount_redelegate); - - // Advance three epochs - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - - // Unbond in epoch 11 from dest_validator - println!( - "\nUNBONDING {} TOKENS FROM {}\n", - amount_unbond.to_string_native(), - &dest_validator - ); - let _ = unbond_tokens( - &mut storage, - Some(&delegator), - &dest_validator, - amount_unbond, - current_epoch, - false, - ) - .unwrap(); - - println!("\nAFTER UNBONDING\n"); - println!("\nDELEGATOR\n"); - - let bonds_src = bond_handle(&delegator, &src_validator) - .get_data_handler() - .collect_map(&storage) - .unwrap(); - let bonds_dest = bond_handle(&delegator, &dest_validator) - .get_data_handler() - .collect_map(&storage) - .unwrap(); - let unbonds_src = unbond_handle(&delegator, &src_validator) - .collect_map(&storage) - .unwrap(); - let unbonds_dest = unbond_handle(&delegator, &dest_validator) - .collect_map(&storage) - .unwrap(); - let redel_bonds = delegator_redelegated_bonds_handle(&delegator) - .collect_map(&storage) - .unwrap(); - let redel_unbonds = delegator_redelegated_unbonds_handle(&delegator) - .collect_map(&storage) - .unwrap(); - - dbg!( - &bonds_src, - &bonds_dest, - &unbonds_src, - &unbonds_dest, - &redel_bonds, - &redel_unbonds - ); - - println!("\nDEST VALIDATOR\n"); - - let incoming_redels_dest = - validator_incoming_redelegations_handle(&dest_validator) - .collect_map(&storage) - .unwrap(); - let outgoing_redels_dest = - validator_outgoing_redelegations_handle(&dest_validator) - .collect_map(&storage) - .unwrap(); - let tot_bonds_dest = total_bonded_handle(&dest_validator) - .get_data_handler() - .collect_map(&storage) - .unwrap(); - let tot_unbonds_dest = total_unbonded_handle(&dest_validator) - .collect_map(&storage) - .unwrap(); - let tot_redel_bonds_dest = - validator_total_redelegated_bonded_handle(&dest_validator) - .collect_map(&storage) - .unwrap(); - let tot_redel_unbonds_dest = - validator_total_redelegated_unbonded_handle(&dest_validator) - .collect_map(&storage) - .unwrap(); - dbg!( - &incoming_redels_dest, - &outgoing_redels_dest, - &tot_bonds_dest, - &tot_unbonds_dest, - &tot_redel_bonds_dest, - &tot_redel_unbonds_dest - ); - - // Advance one epoch - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - - // Discover evidence - slash( - &mut storage, - ¶ms, - current_epoch, - init_epoch + 2 * params.pipeline_len, - 0u64, - SlashType::DuplicateVote, - &src_validator, - current_epoch.next(), - ) - .unwrap(); - - let bond_start = init_epoch + params.pipeline_len; - let redelegation_end = bond_start + params.pipeline_len + 1u64; - let unbond_end = - redelegation_end + params.withdrawable_epoch_offset() + 1u64; - let unbond_materialized = redelegation_end + params.pipeline_len + 1u64; - - // Checks - let redelegated_remaining = delegator_redelegated_bonds_handle(&delegator) - .at(&dest_validator) - .at(&redelegation_end) - .at(&src_validator) - .get(&storage, &bond_start) - .unwrap() - .unwrap_or_default(); - assert_eq!(redelegated_remaining, amount_redelegate - amount_unbond); - - let redel_unbonded = delegator_redelegated_unbonds_handle(&delegator) - .at(&dest_validator) - .at(&redelegation_end) - .at(&unbond_end) - .at(&src_validator) - .get(&storage, &bond_start) - .unwrap() - .unwrap(); - assert_eq!(redel_unbonded, amount_unbond); - - dbg!(unbond_materialized, redelegation_end, bond_start); - let total_redel_unbonded = - validator_total_redelegated_unbonded_handle(&dest_validator) - .at(&unbond_materialized) - .at(&redelegation_end) - .at(&src_validator) - .get(&storage, &bond_start) - .unwrap() - .unwrap(); - assert_eq!(total_redel_unbonded, amount_unbond); - - // Advance to withdrawal epoch - loop { - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - if current_epoch == unbond_end { - break; - } - } - - // Withdraw - withdraw_tokens( - &mut storage, - Some(&delegator), - &dest_validator, - current_epoch, - ) - .unwrap(); - - assert!( - delegator_redelegated_unbonds_handle(&delegator) - .at(&dest_validator) - .is_empty(&storage) - .unwrap() - ); - - let delegator_balance = storage - .read::(&token::balance_key(&staking_token, &delegator)) - .unwrap() - .unwrap_or_default(); - assert_eq!(delegator_balance, del_balance - amount_delegate); -} - -fn test_chain_redelegations_aux(mut validators: Vec) { - validators.sort_by(|a, b| b.tokens.cmp(&a.tokens)); - - let src_validator = validators[0].address.clone(); - let _init_stake_src = validators[0].tokens; - let dest_validator = validators[1].address.clone(); - let _init_stake_dest = validators[1].tokens; - let dest_validator_2 = validators[2].address.clone(); - let _init_stake_dest_2 = validators[2].tokens; - - let mut storage = TestWlStorage::default(); - let params = OwnedPosParams { - unbonding_len: 4, - ..Default::default() - }; - - // Genesis - let mut current_epoch = storage.storage.block.epoch; - let params = test_init_genesis( - &mut storage, - params, - validators.clone().into_iter(), - current_epoch, - ) - .unwrap(); - storage.commit_block().unwrap(); - - // Get a delegator with some tokens - let staking_token = staking_token_address(&storage); - let delegator = address::testing::gen_implicit_address(); - let del_balance = token::Amount::from_uint(1_000_000, 0).unwrap(); - credit_tokens(&mut storage, &staking_token, &delegator, del_balance) - .unwrap(); - - // Delegate in epoch 0 to src_validator - let bond_amount: token::Amount = 100.into(); - super::bond_tokens( - &mut storage, - Some(&delegator), - &src_validator, - bond_amount, - current_epoch, - None, - ) - .unwrap(); - - let bond_start = current_epoch + params.pipeline_len; - - // Advance one epoch - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - - // Redelegate in epoch 1 to dest_validator - let redel_amount_1: token::Amount = 58.into(); - super::redelegate_tokens( - &mut storage, - &delegator, - &src_validator, - &dest_validator, - current_epoch, - redel_amount_1, - ) - .unwrap(); - - let redel_start = current_epoch; - let redel_end = current_epoch + params.pipeline_len; - - // Checks ---------------- - - // Dest validator should have an incoming redelegation - let incoming_redelegation = - validator_incoming_redelegations_handle(&dest_validator) - .get(&storage, &delegator) - .unwrap(); - assert_eq!(incoming_redelegation, Some(redel_end)); - - // Src validator should have an outoging redelegation - let outgoing_redelegation = - validator_outgoing_redelegations_handle(&src_validator) - .at(&dest_validator) - .at(&bond_start) - .get(&storage, &redel_start) - .unwrap(); - assert_eq!(outgoing_redelegation, Some(redel_amount_1)); - - // Delegator should have redelegated bonds - let del_total_redelegated_bonded = - delegator_redelegated_bonds_handle(&delegator) - .at(&dest_validator) - .at(&redel_end) - .at(&src_validator) - .get(&storage, &bond_start) - .unwrap() - .unwrap_or_default(); - assert_eq!(del_total_redelegated_bonded, redel_amount_1); - - // There should be delegator bonds for both src and dest validators - let bonded_src = bond_handle(&delegator, &src_validator); - let bonded_dest = bond_handle(&delegator, &dest_validator); - assert_eq!( - bonded_src - .get_delta_val(&storage, bond_start) - .unwrap() - .unwrap_or_default(), - bond_amount - redel_amount_1 - ); - assert_eq!( - bonded_dest - .get_delta_val(&storage, redel_end) - .unwrap() - .unwrap_or_default(), - redel_amount_1 - ); - - // The dest validator should have total redelegated bonded tokens - let dest_total_redelegated_bonded = - validator_total_redelegated_bonded_handle(&dest_validator) - .at(&redel_end) - .at(&src_validator) - .get(&storage, &bond_start) - .unwrap() - .unwrap_or_default(); - assert_eq!(dest_total_redelegated_bonded, redel_amount_1); - - // The dest validator's total bonded should have an entry for the genesis - // bond and the redelegation - let dest_total_bonded = total_bonded_handle(&dest_validator) - .get_data_handler() - .collect_map(&storage) - .unwrap(); - assert!( - dest_total_bonded.len() == 2 - && dest_total_bonded.contains_key(&Epoch::default()) - ); - assert_eq!( - dest_total_bonded - .get(&redel_end) - .cloned() - .unwrap_or_default(), - redel_amount_1 - ); - - // The src validator should have a total bonded entry for the original bond - // accounting for the redelegation - assert_eq!( - total_bonded_handle(&src_validator) - .get_delta_val(&storage, bond_start) - .unwrap() - .unwrap_or_default(), - bond_amount - redel_amount_1 - ); - - // The src validator should have a total unbonded entry due to the - // redelegation - let src_total_unbonded = total_unbonded_handle(&src_validator) - .at(&redel_end) - .get(&storage, &bond_start) - .unwrap() - .unwrap_or_default(); - assert_eq!(src_total_unbonded, redel_amount_1); - - // Attempt to redelegate in epoch 3 to dest_validator - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - - let redel_amount_2: token::Amount = 23.into(); - let redel_att = super::redelegate_tokens( - &mut storage, - &delegator, - &dest_validator, - &dest_validator_2, - current_epoch, - redel_amount_2, - ); - assert!(redel_att.is_err()); - - // Advance to right before the redelegation can be redelegated again - assert_eq!(redel_end, current_epoch); - let epoch_can_redel = - redel_end.prev() + params.slash_processing_epoch_offset(); - loop { - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - if current_epoch == epoch_can_redel.prev() { - break; - } - } - - // Attempt to redelegate in epoch before we actually are able to - let redel_att = super::redelegate_tokens( - &mut storage, - &delegator, - &dest_validator, - &dest_validator_2, - current_epoch, - redel_amount_2, - ); - assert!(redel_att.is_err()); - - // Advance one more epoch - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - - // Redelegate from dest_validator to dest_validator_2 now - super::redelegate_tokens( - &mut storage, - &delegator, - &dest_validator, - &dest_validator_2, - current_epoch, - redel_amount_2, - ) - .unwrap(); - - let redel_2_start = current_epoch; - let redel_2_end = current_epoch + params.pipeline_len; - - // Checks ----------------------------------- - - // Both the dest validator and dest validator 2 should have incoming - // redelegations - let incoming_redelegation_1 = - validator_incoming_redelegations_handle(&dest_validator) - .get(&storage, &delegator) - .unwrap(); - assert_eq!(incoming_redelegation_1, Some(redel_end)); - let incoming_redelegation_2 = - validator_incoming_redelegations_handle(&dest_validator_2) - .get(&storage, &delegator) - .unwrap(); - assert_eq!(incoming_redelegation_2, Some(redel_2_end)); - - // Both the src validator and dest validator should have outgoing - // redelegations - let outgoing_redelegation_1 = - validator_outgoing_redelegations_handle(&src_validator) - .at(&dest_validator) - .at(&bond_start) - .get(&storage, &redel_start) - .unwrap(); - assert_eq!(outgoing_redelegation_1, Some(redel_amount_1)); - - let outgoing_redelegation_2 = - validator_outgoing_redelegations_handle(&dest_validator) - .at(&dest_validator_2) - .at(&redel_end) - .get(&storage, &redel_2_start) - .unwrap(); - assert_eq!(outgoing_redelegation_2, Some(redel_amount_2)); - - // All three validators should have bonds - let bonded_dest2 = bond_handle(&delegator, &dest_validator_2); - assert_eq!( - bonded_src - .get_delta_val(&storage, bond_start) - .unwrap() - .unwrap_or_default(), - bond_amount - redel_amount_1 - ); - assert_eq!( - bonded_dest - .get_delta_val(&storage, redel_end) - .unwrap() - .unwrap_or_default(), - redel_amount_1 - redel_amount_2 - ); - assert_eq!( - bonded_dest2 - .get_delta_val(&storage, redel_2_end) - .unwrap() - .unwrap_or_default(), - redel_amount_2 - ); - - // There should be no unbond entries - let unbond_src = unbond_handle(&delegator, &src_validator); - let unbond_dest = unbond_handle(&delegator, &dest_validator); - assert!(unbond_src.is_empty(&storage).unwrap()); - assert!(unbond_dest.is_empty(&storage).unwrap()); - - // The dest validator should have some total unbonded due to the second - // redelegation - let dest_total_unbonded = total_unbonded_handle(&dest_validator) - .at(&redel_2_end) - .get(&storage, &redel_end) - .unwrap(); - assert_eq!(dest_total_unbonded, Some(redel_amount_2)); - - // Delegator should have redelegated bonds due to both redelegations - let del_redelegated_bonds = delegator_redelegated_bonds_handle(&delegator); - assert_eq!( - Some(redel_amount_1 - redel_amount_2), - del_redelegated_bonds - .at(&dest_validator) - .at(&redel_end) - .at(&src_validator) - .get(&storage, &bond_start) - .unwrap() - ); - assert_eq!( - Some(redel_amount_2), - del_redelegated_bonds - .at(&dest_validator_2) - .at(&redel_2_end) - .at(&dest_validator) - .get(&storage, &redel_end) - .unwrap() - ); - - // Delegator redelegated unbonds should be empty - assert!( - delegator_redelegated_unbonds_handle(&delegator) - .is_empty(&storage) - .unwrap() - ); - - // Both the dest validator and dest validator 2 should have total - // redelegated bonds - let dest_redelegated_bonded = - validator_total_redelegated_bonded_handle(&dest_validator) - .at(&redel_end) - .at(&src_validator) - .get(&storage, &bond_start) - .unwrap() - .unwrap_or_default(); - let dest2_redelegated_bonded = - validator_total_redelegated_bonded_handle(&dest_validator_2) - .at(&redel_2_end) - .at(&dest_validator) - .get(&storage, &redel_end) - .unwrap() - .unwrap_or_default(); - assert_eq!(dest_redelegated_bonded, redel_amount_1 - redel_amount_2); - assert_eq!(dest2_redelegated_bonded, redel_amount_2); - - // Total redelegated unbonded should be empty for src_validator and - // dest_validator_2 - assert!( - validator_total_redelegated_unbonded_handle(&dest_validator_2) - .is_empty(&storage) - .unwrap() - ); - assert!( - validator_total_redelegated_unbonded_handle(&src_validator) - .is_empty(&storage) - .unwrap() - ); - - // The dest_validator should have total_redelegated unbonded - let tot_redel_unbonded = - validator_total_redelegated_unbonded_handle(&dest_validator) - .at(&redel_2_end) - .at(&redel_end) - .at(&src_validator) - .get(&storage, &bond_start) - .unwrap() - .unwrap_or_default(); - assert_eq!(tot_redel_unbonded, redel_amount_2); -} - -/// SM test case 1 from Brent -#[test] -fn test_from_sm_case_1() { - use namada_core::types::address::testing::established_address_4; - - let mut storage = TestWlStorage::default(); - let gov_params = namada_core::ledger::governance::parameters::GovernanceParameters::default(); - gov_params.init_storage(&mut storage).unwrap(); - write_pos_params(&mut storage, &OwnedPosParams::default()).unwrap(); - - let validator = established_address_1(); - let redeleg_src_1 = established_address_2(); - let redeleg_src_2 = established_address_3(); - let owner = established_address_4(); - let unbond_amount = token::Amount::from(3130688); - println!( - "Owner: {owner}\nValidator: {validator}\nRedeleg src 1: \ - {redeleg_src_1}\nRedeleg src 2: {redeleg_src_2}" - ); - - // Validator's incoming redelegations - let outer_epoch_1 = Epoch(27); - // from redeleg_src_1 - let epoch_1_redeleg_1 = token::Amount::from(8516); - // from redeleg_src_2 - let epoch_1_redeleg_2 = token::Amount::from(5704386); - let outer_epoch_2 = Epoch(30); - // from redeleg_src_2 - let epoch_2_redeleg_2 = token::Amount::from(1035191); - - // Insert the data - bonds and redelegated bonds - let bonds_handle = bond_handle(&owner, &validator); - bonds_handle - .add( - &mut storage, - epoch_1_redeleg_1 + epoch_1_redeleg_2, - outer_epoch_1, - 0, - ) - .unwrap(); - bonds_handle - .add(&mut storage, epoch_2_redeleg_2, outer_epoch_2, 0) - .unwrap(); - - let redelegated_bonds_map_1 = delegator_redelegated_bonds_handle(&owner) - .at(&validator) - .at(&outer_epoch_1); - redelegated_bonds_map_1 - .at(&redeleg_src_1) - .insert(&mut storage, Epoch(14), epoch_1_redeleg_1) - .unwrap(); - redelegated_bonds_map_1 - .at(&redeleg_src_2) - .insert(&mut storage, Epoch(18), epoch_1_redeleg_2) - .unwrap(); - let redelegated_bonds_map_1 = delegator_redelegated_bonds_handle(&owner) - .at(&validator) - .at(&outer_epoch_1); - - let redelegated_bonds_map_2 = delegator_redelegated_bonds_handle(&owner) - .at(&validator) - .at(&outer_epoch_2); - redelegated_bonds_map_2 - .at(&redeleg_src_2) - .insert(&mut storage, Epoch(18), epoch_2_redeleg_2) - .unwrap(); - - // Find the modified redelegation the same way as `unbond_tokens` - let bonds_to_unbond = find_bonds_to_remove( - &storage, - &bonds_handle.get_data_handler(), - unbond_amount, - ) - .unwrap(); - dbg!(&bonds_to_unbond); - - let (new_entry_epoch, new_bond_amount) = bonds_to_unbond.new_entry.unwrap(); - assert_eq!(outer_epoch_1, new_entry_epoch); - // The modified bond should be sum of all redelegations less the unbonded - // amouunt - assert_eq!( - epoch_1_redeleg_1 + epoch_1_redeleg_2 + epoch_2_redeleg_2 - - unbond_amount, - new_bond_amount - ); - // The current bond should be sum of redelegations from the modified epoch - let cur_bond_amount = bonds_handle - .get_delta_val(&storage, new_entry_epoch) - .unwrap() - .unwrap_or_default(); - assert_eq!(epoch_1_redeleg_1 + epoch_1_redeleg_2, cur_bond_amount); - - let mr = compute_modified_redelegation( - &storage, - &redelegated_bonds_map_1, - new_entry_epoch, - cur_bond_amount - new_bond_amount, - ) - .unwrap(); - - let exp_mr = ModifiedRedelegation { - epoch: Some(Epoch(27)), - validators_to_remove: BTreeSet::from_iter([redeleg_src_2.clone()]), - validator_to_modify: Some(redeleg_src_2), - epochs_to_remove: BTreeSet::from_iter([Epoch(18)]), - epoch_to_modify: Some(Epoch(18)), - new_amount: Some(token::Amount::from(3608889)), - }; - - pretty_assertions::assert_eq!(mr, exp_mr); -} - -/// Test precisely that we are not overslashing, as originally discovered by Tomas in this issue: https://github.com/informalsystems/partnership-heliax/issues/74 -fn test_overslashing_aux(mut validators: Vec) { - assert_eq!(validators.len(), 4); - - let params = OwnedPosParams { - unbonding_len: 4, - ..Default::default() - }; - - let offending_stake = token::Amount::native_whole(110); - let other_stake = token::Amount::native_whole(100); - - // Set stakes so we know we will get a slashing rate between 0.5 -1.0 - validators[0].tokens = offending_stake; - validators[1].tokens = other_stake; - validators[2].tokens = other_stake; - validators[3].tokens = other_stake; - - // Get the offending validator - let validator = validators[0].address.clone(); - - println!("\nTest inputs: {params:?}, genesis validators: {validators:#?}"); - let mut storage = TestWlStorage::default(); - - // Genesis - let mut current_epoch = storage.storage.block.epoch; - let params = test_init_genesis( - &mut storage, - params, - validators.clone().into_iter(), - current_epoch, - ) - .unwrap(); - storage.commit_block().unwrap(); - - // Get a delegator with some tokens - let staking_token = storage.storage.native_token.clone(); - let delegator = address::testing::gen_implicit_address(); - let amount_del = token::Amount::native_whole(5); - credit_tokens(&mut storage, &staking_token, &delegator, amount_del) - .unwrap(); - - // Delegate tokens in epoch 0 to validator - bond_tokens( - &mut storage, - Some(&delegator), - &validator, - amount_del, - current_epoch, - None, - ) - .unwrap(); - - let self_bond_epoch = current_epoch; - let delegation_epoch = current_epoch + params.pipeline_len; - - // Advance to pipeline epoch - for _ in 0..params.pipeline_len { - current_epoch = advance_epoch(&mut storage, ¶ms); - } - assert_eq!(delegation_epoch, current_epoch); - - // Find a misbehavior committed in epoch 0 - slash( - &mut storage, - ¶ms, - current_epoch, - self_bond_epoch, - 0_u64, - SlashType::DuplicateVote, - &validator, - current_epoch.next(), - ) - .unwrap(); - - // Find a misbehavior committed in current epoch - slash( - &mut storage, - ¶ms, - current_epoch, - delegation_epoch, - 0_u64, - SlashType::DuplicateVote, - &validator, - current_epoch.next(), - ) - .unwrap(); - - let processing_epoch_1 = - self_bond_epoch + params.slash_processing_epoch_offset(); - let processing_epoch_2 = - delegation_epoch + params.slash_processing_epoch_offset(); - - // Advance to processing epoch 1 - loop { - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - if current_epoch == processing_epoch_1 { - break; - } - } - - let total_stake_1 = offending_stake + 3 * other_stake; - let stake_frac = Dec::from(offending_stake) / Dec::from(total_stake_1); - let slash_rate_1 = Dec::from_str("9.0").unwrap() * stake_frac * stake_frac; - dbg!(&slash_rate_1); - - let exp_slashed_1 = offending_stake.mul_ceil(slash_rate_1); - - // Check that the proper amount was slashed - let epoch = current_epoch.next(); - let validator_stake = - read_validator_stake(&storage, ¶ms, &validator, epoch).unwrap(); - let exp_validator_stake = offending_stake - exp_slashed_1 + amount_del; - assert_eq!(validator_stake, exp_validator_stake); - - let total_stake = read_total_stake(&storage, ¶ms, epoch).unwrap(); - let exp_total_stake = - offending_stake - exp_slashed_1 + amount_del + 3 * other_stake; - assert_eq!(total_stake, exp_total_stake); - - let self_bond_id = BondId { - source: validator.clone(), - validator: validator.clone(), - }; - let bond_amount = - crate::bond_amount(&storage, &self_bond_id, epoch).unwrap(); - let exp_bond_amount = offending_stake - exp_slashed_1; - assert_eq!(bond_amount, exp_bond_amount); - - // Advance to processing epoch 2 - loop { - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - if current_epoch == processing_epoch_2 { - break; - } - } - - let total_stake_2 = offending_stake + amount_del + 3 * other_stake; - let stake_frac = - Dec::from(offending_stake + amount_del) / Dec::from(total_stake_2); - let slash_rate_2 = Dec::from_str("9.0").unwrap() * stake_frac * stake_frac; - dbg!(&slash_rate_2); - - let exp_slashed_from_delegation = amount_del.mul_ceil(slash_rate_2); - - // Check that the proper amount was slashed. We expect that all of the - // validator self-bond has been slashed and some of the delegation has been - // slashed due to the second infraction. - let epoch = current_epoch.next(); - - let validator_stake = - read_validator_stake(&storage, ¶ms, &validator, epoch).unwrap(); - let exp_validator_stake = amount_del - exp_slashed_from_delegation; - assert_eq!(validator_stake, exp_validator_stake); - - let total_stake = read_total_stake(&storage, ¶ms, epoch).unwrap(); - let exp_total_stake = - amount_del - exp_slashed_from_delegation + 3 * other_stake; - assert_eq!(total_stake, exp_total_stake); - - let delegation_id = BondId { - source: delegator.clone(), - validator: validator.clone(), - }; - let delegation_amount = - crate::bond_amount(&storage, &delegation_id, epoch).unwrap(); - let exp_del_amount = amount_del - exp_slashed_from_delegation; - assert_eq!(delegation_amount, exp_del_amount); - - let self_bond_amount = - crate::bond_amount(&storage, &self_bond_id, epoch).unwrap(); - let exp_bond_amount = token::Amount::zero(); - assert_eq!(self_bond_amount, exp_bond_amount); -} - -fn test_unslashed_bond_amount_aux(validators: Vec) { - let mut storage = TestWlStorage::default(); - let params = OwnedPosParams { - unbonding_len: 4, - ..Default::default() - }; - - // Genesis - let mut current_epoch = storage.storage.block.epoch; - let params = test_init_genesis( - &mut storage, - params, - validators.clone().into_iter(), - current_epoch, - ) - .unwrap(); - storage.commit_block().unwrap(); - - let validator1 = validators[0].address.clone(); - let validator2 = validators[1].address.clone(); - - // Get a delegator with some tokens - let staking_token = staking_token_address(&storage); - let delegator = address::testing::gen_implicit_address(); - let del_balance = token::Amount::from_uint(1_000_000, 0).unwrap(); - credit_tokens(&mut storage, &staking_token, &delegator, del_balance) - .unwrap(); - - // Bond to validator 1 - super::bond_tokens( - &mut storage, - Some(&delegator), - &validator1, - 10_000.into(), - current_epoch, - None, - ) - .unwrap(); - - // Unbond some from validator 1 - super::unbond_tokens( - &mut storage, - Some(&delegator), - &validator1, - 1_342.into(), - current_epoch, - false, - ) - .unwrap(); - - // Redelegate some from validator 1 -> 2 - super::redelegate_tokens( - &mut storage, - &delegator, - &validator1, - &validator2, - current_epoch, - 1_875.into(), - ) - .unwrap(); - - // Unbond some from validator 2 - super::unbond_tokens( - &mut storage, - Some(&delegator), - &validator2, - 584.into(), - current_epoch, - false, - ) - .unwrap(); - - // Advance an epoch - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - - // Bond to validator 1 - super::bond_tokens( - &mut storage, - Some(&delegator), - &validator1, - 384.into(), - current_epoch, - None, - ) - .unwrap(); - - // Unbond some from validator 1 - super::unbond_tokens( - &mut storage, - Some(&delegator), - &validator1, - 144.into(), - current_epoch, - false, - ) - .unwrap(); - - // Redelegate some from validator 1 -> 2 - super::redelegate_tokens( - &mut storage, - &delegator, - &validator1, - &validator2, - current_epoch, - 3_448.into(), - ) - .unwrap(); - - // Unbond some from validator 2 - super::unbond_tokens( - &mut storage, - Some(&delegator), - &validator2, - 699.into(), - current_epoch, - false, - ) - .unwrap(); - - // Advance an epoch - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - - // Bond to validator 1 - super::bond_tokens( - &mut storage, - Some(&delegator), - &validator1, - 4_384.into(), - current_epoch, - None, - ) - .unwrap(); - - // Redelegate some from validator 1 -> 2 - super::redelegate_tokens( - &mut storage, - &delegator, - &validator1, - &validator2, - current_epoch, - 1_008.into(), - ) - .unwrap(); - - // Unbond some from validator 2 - super::unbond_tokens( - &mut storage, - Some(&delegator), - &validator2, - 3_500.into(), - current_epoch, - false, - ) - .unwrap(); - - // Checks - let val1_init_stake = validators[0].tokens; - - for epoch in Epoch::iter_bounds_inclusive( - Epoch(0), - current_epoch + params.pipeline_len, - ) { - let bond_amount = crate::bond_amount( - &storage, - &BondId { - source: delegator.clone(), - validator: validator1.clone(), - }, - epoch, - ) - .unwrap_or_default(); - - let val_stake = - crate::read_validator_stake(&storage, ¶ms, &validator1, epoch) - .unwrap(); - // dbg!(&bond_amount); - assert_eq!(val_stake - val1_init_stake, bond_amount); - } -} - -fn test_log_block_rewards_aux( - validators: Vec, - params: OwnedPosParams, -) { - tracing::info!( - "New case with {} validators: {:#?}", - validators.len(), - validators - .iter() - .map(|v| (&v.address, v.tokens.to_string_native())) - .collect::>() - ); - let mut s = TestWlStorage::default(); - // Init genesis - let current_epoch = s.storage.block.epoch; - let params = test_init_genesis( - &mut s, - params, - validators.clone().into_iter(), - current_epoch, - ) - .unwrap(); - s.commit_block().unwrap(); - let total_stake = - crate::get_total_consensus_stake(&s, current_epoch, ¶ms).unwrap(); - let consensus_set = - crate::read_consensus_validator_set_addresses(&s, current_epoch) - .unwrap(); - let proposer_address = consensus_set.iter().next().unwrap().clone(); - - tracing::info!( - ?params.block_proposer_reward, - ?params.block_vote_reward, - ); - tracing::info!(?proposer_address,); - - // Rewards accumulator should be empty at first - let rewards_handle = crate::rewards_accumulator_handle(); - assert!(rewards_handle.is_empty(&s).unwrap()); - - let mut last_rewards = BTreeMap::default(); - - let num_blocks = 100; - // Loop through `num_blocks`, log rewards & check results - for i in 0..num_blocks { - tracing::info!(""); - tracing::info!("Block {}", i + 1); - - // A helper closure to prepare minimum required votes - let prep_votes = |epoch| { - // Ceil of 2/3 of total stake - let min_required_votes = total_stake.mul_ceil(Dec::two() / 3); - - let mut total_votes = token::Amount::zero(); - let mut non_voters = HashSet::
::default(); - let mut prep_vote = |validator| { - // Add validator vote if it's in consensus set and if we don't - // yet have min required votes - if consensus_set.contains(validator) - && total_votes < min_required_votes - { - let stake = - read_validator_stake(&s, ¶ms, validator, epoch) - .unwrap(); - total_votes += stake; - let validator_vp = - into_tm_voting_power(params.tm_votes_per_token, stake) - as u64; - tracing::info!("Validator {validator} signed"); - Some(VoteInfo { - validator_address: validator.clone(), - validator_vp, - }) - } else { - non_voters.insert(validator.clone()); - None - } - }; - - let votes: Vec = validators - .iter() - .rev() - .filter_map(|validator| prep_vote(&validator.address)) - .collect(); - (votes, total_votes, non_voters) - }; - - let (votes, signing_stake, non_voters) = prep_votes(current_epoch); - crate::log_block_rewards( - &mut s, - current_epoch, - &proposer_address, - votes.clone(), - ) - .unwrap(); - - assert!(!rewards_handle.is_empty(&s).unwrap()); - - let rewards_calculator = PosRewardsCalculator { - proposer_reward: params.block_proposer_reward, - signer_reward: params.block_vote_reward, - signing_stake, - total_stake, - }; - let coeffs = rewards_calculator.get_reward_coeffs().unwrap(); - tracing::info!(?coeffs); - - // Check proposer reward - let stake = - read_validator_stake(&s, ¶ms, &proposer_address, current_epoch) - .unwrap(); - let proposer_signing_reward = votes.iter().find_map(|vote| { - if vote.validator_address == proposer_address { - let signing_fraction = - Dec::from(stake) / Dec::from(signing_stake); - Some(coeffs.signer_coeff * signing_fraction) - } else { - None - } - }); - let expected_proposer_rewards = last_rewards.get(&proposer_address).copied().unwrap_or_default() + - // Proposer reward - coeffs.proposer_coeff - // Consensus validator reward - + (coeffs.active_val_coeff - * (Dec::from(stake) / Dec::from(total_stake))) - // Signing reward (if proposer voted) - + proposer_signing_reward - .unwrap_or_default(); - tracing::info!( - "Expected proposer rewards: {expected_proposer_rewards}. Signed \ - block: {}", - proposer_signing_reward.is_some() - ); - assert_eq!( - rewards_handle.get(&s, &proposer_address).unwrap(), - Some(expected_proposer_rewards) - ); - - // Check voters rewards - for VoteInfo { - validator_address, .. - } in votes.iter() - { - // Skip proposer, in case voted - already checked - if validator_address == &proposer_address { - continue; - } - - let stake = read_validator_stake( - &s, - ¶ms, - validator_address, - current_epoch, - ) - .unwrap(); - let signing_fraction = Dec::from(stake) / Dec::from(signing_stake); - let expected_signer_rewards = last_rewards - .get(validator_address) - .copied() - .unwrap_or_default() - + coeffs.signer_coeff * signing_fraction - + (coeffs.active_val_coeff - * (Dec::from(stake) / Dec::from(total_stake))); - tracing::info!( - "Expected signer {validator_address} rewards: \ - {expected_signer_rewards}" - ); - assert_eq!( - rewards_handle.get(&s, validator_address).unwrap(), - Some(expected_signer_rewards) - ); - } - - // Check non-voters rewards, if any - for address in non_voters { - // Skip proposer, in case it didn't vote - already checked - if address == proposer_address { - continue; - } - - if consensus_set.contains(&address) { - let stake = - read_validator_stake(&s, ¶ms, &address, current_epoch) - .unwrap(); - let expected_non_signer_rewards = - last_rewards.get(&address).copied().unwrap_or_default() - + coeffs.active_val_coeff - * (Dec::from(stake) / Dec::from(total_stake)); - tracing::info!( - "Expected non-signer {address} rewards: \ - {expected_non_signer_rewards}" - ); - assert_eq!( - rewards_handle.get(&s, &address).unwrap(), - Some(expected_non_signer_rewards) - ); - } else { - let last_reward = last_rewards.get(&address).copied(); - assert_eq!( - rewards_handle.get(&s, &address).unwrap(), - last_reward - ); - } - } - s.commit_block().unwrap(); - - last_rewards = - crate::rewards_accumulator_handle().collect_map(&s).unwrap(); - - let rewards_sum: Dec = last_rewards.values().copied().sum(); - let expected_sum = Dec::one() * (i as u64 + 1); - let err_tolerance = Dec::new(1, 9).unwrap(); - let fail_msg = format!( - "Expected rewards sum at block {} to be {expected_sum}, got \ - {rewards_sum}. Error tolerance {err_tolerance}.", - i + 1 - ); - assert!(expected_sum <= rewards_sum + err_tolerance, "{fail_msg}"); - assert!(rewards_sum <= expected_sum, "{fail_msg}"); - } -} - -fn test_update_rewards_products_aux(validators: Vec) { - tracing::info!( - "New case with {} validators: {:#?}", - validators.len(), - validators - .iter() - .map(|v| (&v.address, v.tokens.to_string_native())) - .collect::>() - ); - let mut s = TestWlStorage::default(); - // Init genesis - let current_epoch = s.storage.block.epoch; - let params = OwnedPosParams::default(); - let params = test_init_genesis( - &mut s, - params, - validators.into_iter(), - current_epoch, - ) - .unwrap(); - s.commit_block().unwrap(); - - let staking_token = staking_token_address(&s); - let consensus_set = - crate::read_consensus_validator_set_addresses(&s, current_epoch) - .unwrap(); - - // Start a new epoch - let current_epoch = advance_epoch(&mut s, ¶ms); - - // Read some data before applying rewards - let pos_balance_pre = - read_balance(&s, &staking_token, &address::POS).unwrap(); - let gov_balance_pre = - read_balance(&s, &staking_token, &address::GOV).unwrap(); - - let num_consensus_validators = consensus_set.len() as u64; - let accum_val = Dec::one() / num_consensus_validators; - let num_blocks_in_last_epoch = 1000; - - // Assign some reward accumulator values to consensus validator - for validator in &consensus_set { - crate::rewards_accumulator_handle() - .insert( - &mut s, - validator.clone(), - accum_val * num_blocks_in_last_epoch, - ) - .unwrap(); - } - - // Distribute inflation into rewards - let last_epoch = current_epoch.prev(); - let inflation = token::Amount::native_whole(10_000_000); - crate::update_rewards_products_and_mint_inflation( - &mut s, - ¶ms, - last_epoch, - num_blocks_in_last_epoch, - inflation, - &staking_token, - ) - .unwrap(); - - let pos_balance_post = - read_balance(&s, &staking_token, &address::POS).unwrap(); - let gov_balance_post = - read_balance(&s, &staking_token, &address::GOV).unwrap(); - - assert_eq!( - pos_balance_pre + gov_balance_pre + inflation, - pos_balance_post + gov_balance_post, - "Expected inflation to be minted to PoS and left-over amount to Gov" - ); - - let pos_credit = pos_balance_post - pos_balance_pre; - let gov_credit = gov_balance_post - gov_balance_pre; - assert!( - pos_credit > gov_credit, - "PoS must receive more tokens than Gov, but got {} in PoS and {} in \ - Gov", - pos_credit.to_string_native(), - gov_credit.to_string_native() - ); - - // Rewards accumulator must be cleared out - let rewards_handle = crate::rewards_accumulator_handle(); - assert!(rewards_handle.is_empty(&s).unwrap()); -} - -fn test_slashed_bond_amount_aux(validators: Vec) { - let mut storage = TestWlStorage::default(); - let params = OwnedPosParams { - unbonding_len: 4, - ..Default::default() - }; - - let init_tot_stake = validators - .clone() - .into_iter() - .fold(token::Amount::zero(), |acc, v| acc + v.tokens); - let val1_init_stake = validators[0].tokens; - - let mut validators = validators; - validators[0].tokens = (init_tot_stake - val1_init_stake) / 30; - - // Genesis - let mut current_epoch = storage.storage.block.epoch; - let params = test_init_genesis( - &mut storage, - params, - validators.clone().into_iter(), - current_epoch, - ) - .unwrap(); - storage.commit_block().unwrap(); - - let validator1 = validators[0].address.clone(); - let validator2 = validators[1].address.clone(); - - // Get a delegator with some tokens - let staking_token = staking_token_address(&storage); - let delegator = address::testing::gen_implicit_address(); - let del_balance = token::Amount::from_uint(1_000_000, 0).unwrap(); - credit_tokens(&mut storage, &staking_token, &delegator, del_balance) - .unwrap(); - - // Bond to validator 1 - super::bond_tokens( - &mut storage, - Some(&delegator), - &validator1, - 10_000.into(), - current_epoch, - None, - ) - .unwrap(); - - // Unbond some from validator 1 - super::unbond_tokens( - &mut storage, - Some(&delegator), - &validator1, - 1_342.into(), - current_epoch, - false, - ) - .unwrap(); - - // Redelegate some from validator 1 -> 2 - super::redelegate_tokens( - &mut storage, - &delegator, - &validator1, - &validator2, - current_epoch, - 1_875.into(), - ) - .unwrap(); - - // Unbond some from validator 2 - super::unbond_tokens( - &mut storage, - Some(&delegator), - &validator2, - 584.into(), - current_epoch, - false, - ) - .unwrap(); - - // Advance an epoch to 1 - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - - // Bond to validator 1 - super::bond_tokens( - &mut storage, - Some(&delegator), - &validator1, - 384.into(), - current_epoch, - None, - ) - .unwrap(); - - // Unbond some from validator 1 - super::unbond_tokens( - &mut storage, - Some(&delegator), - &validator1, - 144.into(), - current_epoch, - false, - ) - .unwrap(); - - // Redelegate some from validator 1 -> 2 - super::redelegate_tokens( - &mut storage, - &delegator, - &validator1, - &validator2, - current_epoch, - 3_448.into(), - ) - .unwrap(); - - // Unbond some from validator 2 - super::unbond_tokens( - &mut storage, - Some(&delegator), - &validator2, - 699.into(), - current_epoch, - false, - ) - .unwrap(); - - // Advance an epoch to ep 2 - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - - // Bond to validator 1 - super::bond_tokens( - &mut storage, - Some(&delegator), - &validator1, - 4_384.into(), - current_epoch, - None, - ) - .unwrap(); - - // Redelegate some from validator 1 -> 2 - super::redelegate_tokens( - &mut storage, - &delegator, - &validator1, - &validator2, - current_epoch, - 1_008.into(), - ) - .unwrap(); - - // Unbond some from validator 2 - super::unbond_tokens( - &mut storage, - Some(&delegator), - &validator2, - 3_500.into(), - current_epoch, - false, - ) - .unwrap(); - - // Advance two epochs to ep 4 - for _ in 0..2 { - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - } - - // Find some slashes committed in various epochs - super::slash( - &mut storage, - ¶ms, - current_epoch, - Epoch(1), - 1_u64, - SlashType::DuplicateVote, - &validator1, - current_epoch, - ) - .unwrap(); - super::slash( - &mut storage, - ¶ms, - current_epoch, - Epoch(2), - 1_u64, - SlashType::DuplicateVote, - &validator1, - current_epoch, - ) - .unwrap(); - super::slash( - &mut storage, - ¶ms, - current_epoch, - Epoch(2), - 1_u64, - SlashType::DuplicateVote, - &validator1, - current_epoch, - ) - .unwrap(); - super::slash( - &mut storage, - ¶ms, - current_epoch, - Epoch(3), - 1_u64, - SlashType::DuplicateVote, - &validator1, - current_epoch, - ) - .unwrap(); - - // Advance such that these slashes are all processed - for _ in 0..params.slash_processing_epoch_offset() { - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - } - - let pipeline_epoch = current_epoch + params.pipeline_len; - - let del_bond_amount = crate::bond_amount( - &storage, - &BondId { - source: delegator.clone(), - validator: validator1.clone(), - }, - pipeline_epoch, - ) - .unwrap_or_default(); - - let self_bond_amount = crate::bond_amount( - &storage, - &BondId { - source: validator1.clone(), - validator: validator1.clone(), - }, - pipeline_epoch, - ) - .unwrap_or_default(); - - let val_stake = crate::read_validator_stake( - &storage, - ¶ms, - &validator1, - pipeline_epoch, - ) - .unwrap(); - // dbg!(&val_stake); - // dbg!(&del_bond_amount); - // dbg!(&self_bond_amount); - - let diff = val_stake - self_bond_amount - del_bond_amount; - assert!(diff <= 2.into()); -} - -fn test_consensus_key_change_aux(validators: Vec) { - assert_eq!(validators.len(), 1); - - let params = OwnedPosParams { - unbonding_len: 4, - ..Default::default() - }; - let validator = validators[0].address.clone(); - - println!("\nTest inputs: {params:?}, genesis validators: {validators:#?}"); - let mut storage = TestWlStorage::default(); - - // Genesis - let mut current_epoch = storage.storage.block.epoch; - let params = test_init_genesis( - &mut storage, - params, - validators.into_iter(), - current_epoch, - ) - .unwrap(); - storage.commit_block().unwrap(); - - // Check that there is one consensus key in the network - let consensus_keys = get_consensus_key_set(&storage).unwrap(); - assert_eq!(consensus_keys.len(), 1); - let ck = consensus_keys.first().cloned().unwrap(); - let og_ck = validator_consensus_key_handle(&validator) - .get(&storage, current_epoch, ¶ms) - .unwrap() - .unwrap(); - assert_eq!(ck, og_ck); - - // Attempt to change to a new secp256k1 consensus key (disallowed) - let secp_ck = gen_keypair::(); - let secp_ck = key::common::SecretKey::Secp256k1(secp_ck).ref_to(); - let res = - change_consensus_key(&mut storage, &validator, &secp_ck, current_epoch); - assert!(res.is_err()); - - // Change consensus keys - let ck_2 = common_sk_from_simple_seed(1).ref_to(); - change_consensus_key(&mut storage, &validator, &ck_2, current_epoch) - .unwrap(); - - // Check that there is a new consensus key - let consensus_keys = get_consensus_key_set(&storage).unwrap(); - assert_eq!(consensus_keys.len(), 2); - - for epoch in current_epoch.iter_range(params.pipeline_len) { - let ck = validator_consensus_key_handle(&validator) - .get(&storage, epoch, ¶ms) - .unwrap() - .unwrap(); - assert_eq!(ck, og_ck); - } - let pipeline_epoch = current_epoch + params.pipeline_len; - let ck = validator_consensus_key_handle(&validator) - .get(&storage, pipeline_epoch, ¶ms) - .unwrap() - .unwrap(); - assert_eq!(ck, ck_2); - - // Advance to the pipeline epoch - loop { - current_epoch = advance_epoch(&mut storage, ¶ms); - if current_epoch == pipeline_epoch { - break; - } - } - - // Check the consensus keys again - let consensus_keys = get_consensus_key_set(&storage).unwrap(); - assert_eq!(consensus_keys.len(), 2); - - for epoch in current_epoch.iter_range(params.pipeline_len + 1) { - let ck = validator_consensus_key_handle(&validator) - .get(&storage, epoch, ¶ms) - .unwrap() - .unwrap(); - assert_eq!(ck, ck_2); - } - - // Now change the consensus key again and bond in the same epoch - let ck_3 = common_sk_from_simple_seed(3).ref_to(); - change_consensus_key(&mut storage, &validator, &ck_3, current_epoch) - .unwrap(); - - let staking_token = storage.storage.native_token.clone(); - let amount_del = token::Amount::native_whole(5); - credit_tokens(&mut storage, &staking_token, &validator, amount_del) - .unwrap(); - bond_tokens( - &mut storage, - None, - &validator, - token::Amount::native_whole(1), - current_epoch, - None, - ) - .unwrap(); - - // Check consensus keys again - let consensus_keys = get_consensus_key_set(&storage).unwrap(); - assert_eq!(consensus_keys.len(), 3); - - for epoch in current_epoch.iter_range(params.pipeline_len) { - let ck = validator_consensus_key_handle(&validator) - .get(&storage, epoch, ¶ms) - .unwrap() - .unwrap(); - assert_eq!(ck, ck_2); - } - let pipeline_epoch = current_epoch + params.pipeline_len; - let ck = validator_consensus_key_handle(&validator) - .get(&storage, pipeline_epoch, ¶ms) - .unwrap() - .unwrap(); - assert_eq!(ck, ck_3); - - // Advance to the pipeline epoch to ensure that the validator set updates to - // tendermint will work - loop { - current_epoch = advance_epoch(&mut storage, ¶ms); - if current_epoch == pipeline_epoch { - break; - } - } - assert_eq!(current_epoch.0, 2 * params.pipeline_len); -} - -fn test_is_delegator_aux(mut validators: Vec) { - validators.sort_by(|a, b| b.tokens.cmp(&a.tokens)); - - let validator1 = validators[0].address.clone(); - let validator2 = validators[1].address.clone(); - - let mut storage = TestWlStorage::default(); - let params = OwnedPosParams { - unbonding_len: 4, - ..Default::default() - }; - - // Genesis - let mut current_epoch = storage.storage.block.epoch; - let params = test_init_genesis( - &mut storage, - params, - validators.clone().into_iter(), - current_epoch, - ) - .unwrap(); - storage.commit_block().unwrap(); - - // Get delegators with some tokens - let staking_token = staking_token_address(&storage); - let delegator1 = address::testing::gen_implicit_address(); - let delegator2 = address::testing::gen_implicit_address(); - let del_balance = token::Amount::native_whole(1000); - credit_tokens(&mut storage, &staking_token, &delegator1, del_balance) - .unwrap(); - credit_tokens(&mut storage, &staking_token, &delegator2, del_balance) - .unwrap(); - - // Advance to epoch 1 - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - - // Delegate in epoch 1 to validator1 - let del1_epoch = current_epoch; - super::bond_tokens( - &mut storage, - Some(&delegator1), - &validator1, - 1000.into(), - current_epoch, - None, - ) - .unwrap(); - - // Advance to epoch 2 - current_epoch = advance_epoch(&mut storage, ¶ms); - super::process_slashes(&mut storage, current_epoch).unwrap(); - - // Delegate in epoch 2 to validator2 - let del2_epoch = current_epoch; - super::bond_tokens( - &mut storage, - Some(&delegator2), - &validator2, - 1000.into(), - current_epoch, - None, - ) - .unwrap(); - - // Checks - assert!(super::is_validator(&storage, &validator1).unwrap()); - assert!(super::is_validator(&storage, &validator2).unwrap()); - assert!(!super::is_delegator(&storage, &validator1, None).unwrap()); - assert!(!super::is_delegator(&storage, &validator2, None).unwrap()); - - assert!(!super::is_validator(&storage, &delegator1).unwrap()); - assert!(!super::is_validator(&storage, &delegator2).unwrap()); - assert!(super::is_delegator(&storage, &delegator1, None).unwrap()); - assert!(super::is_delegator(&storage, &delegator2, None).unwrap()); - - for epoch in Epoch::default().iter_range(del1_epoch.0 + params.pipeline_len) - { - assert!( - !super::is_delegator(&storage, &delegator1, Some(epoch)).unwrap() - ); - } - assert!( - super::is_delegator( - &storage, - &delegator1, - Some(del1_epoch + params.pipeline_len) - ) - .unwrap() - ); - for epoch in Epoch::default().iter_range(del2_epoch.0 + params.pipeline_len) - { - assert!( - !super::is_delegator(&storage, &delegator2, Some(epoch)).unwrap() - ); - } - assert!( - super::is_delegator( - &storage, - &delegator2, - Some(del2_epoch + params.pipeline_len) - ) - .unwrap() - ); -} - -/// Test validator initialization. -fn test_purge_validator_information_aux(validators: Vec) { - let owned = OwnedPosParams { - unbonding_len: 4, - ..Default::default() - }; - - let mut s = TestWlStorage::default(); - let mut current_epoch = s.storage.block.epoch; - - // Genesis - let gov_params = - namada_core::ledger::governance::parameters::GovernanceParameters { - max_proposal_period: 5, - ..Default::default() - }; - - gov_params.init_storage(&mut s).unwrap(); - let params = crate::read_non_pos_owned_params(&s, owned).unwrap(); - init_genesis_helper(&mut s, ¶ms, validators.into_iter(), current_epoch) - .unwrap(); - - s.commit_block().unwrap(); - - let default_past_epochs = 2; - let consensus_val_set_len = - gov_params.max_proposal_period + default_past_epochs; - - let consensus_val_set = consensus_validator_set_handle(); - // let below_cap_val_set = below_capacity_validator_set_handle(); - let validator_positions = validator_set_positions_handle(); - let all_validator_addresses = validator_addresses_handle(); - - let check_is_data = |storage: &WlStorage<_, _>, - start: Epoch, - end: Epoch| { - for ep in Epoch::iter_bounds_inclusive(start, end) { - assert!(!consensus_val_set.at(&ep).is_empty(storage).unwrap()); - // assert!(!below_cap_val_set.at(&ep).is_empty(storage). - // unwrap()); - assert!(!validator_positions.at(&ep).is_empty(storage).unwrap()); - assert!( - !all_validator_addresses.at(&ep).is_empty(storage).unwrap() - ); - } - }; - - // Check that there is validator data for epochs 0 - pipeline_len - check_is_data(&s, current_epoch, Epoch(params.owned.pipeline_len)); - - // Advance to epoch 1 - for _ in 0..default_past_epochs { - current_epoch = advance_epoch(&mut s, ¶ms); - } - assert_eq!(s.storage.block.epoch.0, default_past_epochs); - assert_eq!(current_epoch.0, default_past_epochs); - - check_is_data( - &s, - Epoch(0), - Epoch(params.owned.pipeline_len + default_past_epochs), - ); - - current_epoch = advance_epoch(&mut s, ¶ms); - assert_eq!(current_epoch.0, default_past_epochs + 1); - - check_is_data( - &s, - Epoch(1), - Epoch(params.pipeline_len + default_past_epochs + 1), - ); - assert!(!consensus_val_set.at(&Epoch(0)).is_empty(&s).unwrap()); - assert!(validator_positions.at(&Epoch(0)).is_empty(&s).unwrap()); - assert!(all_validator_addresses.at(&Epoch(0)).is_empty(&s).unwrap()); - - // Advance to the epoch `consensus_val_set_len` + 1 - loop { - assert!(!consensus_val_set.at(&Epoch(0)).is_empty(&s).unwrap()); - - current_epoch = advance_epoch(&mut s, ¶ms); - if current_epoch.0 == consensus_val_set_len + 1 { - break; - } - } - - assert!(consensus_val_set.at(&Epoch(0)).is_empty(&s).unwrap()); - - current_epoch = advance_epoch(&mut s, ¶ms); - for ep in Epoch::default().iter_range(2) { - assert!(consensus_val_set.at(&ep).is_empty(&s).unwrap()); - } - for ep in Epoch::iter_bounds_inclusive( - Epoch(2), - current_epoch + params.pipeline_len, - ) { - assert!(!consensus_val_set.at(&ep).is_empty(&s).unwrap()); - } -} diff --git a/proof_of_stake/src/tests/helpers.rs b/proof_of_stake/src/tests/helpers.rs new file mode 100644 index 0000000000..ddf8bba4c6 --- /dev/null +++ b/proof_of_stake/src/tests/helpers.rs @@ -0,0 +1,173 @@ +use std::cmp::max; +use std::ops::Range; + +use namada_core::ledger::storage::testing::TestWlStorage; +use namada_core::types::address::testing::address_from_simple_seed; +use namada_core::types::dec::Dec; +use namada_core::types::key::testing::common_sk_from_simple_seed; +use namada_core::types::key::{self, RefTo}; +use namada_core::types::storage::Epoch; +use namada_core::types::token; +use namada_core::types::token::testing::arb_amount_non_zero_ceiled; +use proptest::strategy::{Just, Strategy}; + +use crate::parameters::testing::arb_pos_params; +use crate::types::{GenesisValidator, ValidatorSetUpdate}; +use crate::validator_set_update::{ + copy_validator_sets_and_positions, validator_set_update_tendermint, +}; +use crate::{ + compute_and_store_total_consensus_stake, OwnedPosParams, PosParams, +}; + +pub fn arb_params_and_genesis_validators( + num_max_validator_slots: Option, + val_size: Range, +) -> impl Strategy)> { + let params = arb_pos_params(num_max_validator_slots); + params.prop_flat_map(move |params| { + let validators = arb_genesis_validators( + val_size.clone(), + Some(params.validator_stake_threshold), + ); + (Just(params), validators) + }) +} + +pub fn test_slashes_with_unbonding_params() +-> impl Strategy, u64)> { + let params = arb_pos_params(Some(5)); + params.prop_flat_map(|params| { + let unbond_delay = 0..(params.slash_processing_epoch_offset() * 2); + // Must have at least 4 validators so we can slash one and the cubic + // slash rate will be less than 100% + let validators = arb_genesis_validators(4..10, None); + (Just(params), validators, unbond_delay) + }) +} + +pub fn get_tendermint_set_updates( + s: &TestWlStorage, + params: &PosParams, + Epoch(epoch): Epoch, +) -> Vec { + // Because the `validator_set_update_tendermint` is called 2 blocks before + // the start of a new epoch, it expects to receive the epoch that is before + // the start of a new one too and so we give it the predecessor of the + // current epoch here to actually get the update for the current epoch. + let epoch = Epoch(epoch - 1); + validator_set_update_tendermint(s, params, epoch, |update| update).unwrap() +} + +/// Advance to the next epoch. Returns the new epoch. +pub fn advance_epoch(s: &mut TestWlStorage, params: &PosParams) -> Epoch { + s.storage.block.epoch = s.storage.block.epoch.next(); + let current_epoch = s.storage.block.epoch; + compute_and_store_total_consensus_stake(s, current_epoch).unwrap(); + copy_validator_sets_and_positions( + s, + params, + current_epoch, + current_epoch + params.pipeline_len, + ) + .unwrap(); + // purge_validator_sets_for_old_epoch(s, current_epoch).unwrap(); + // process_slashes(s, current_epoch).unwrap(); + // dbg!(current_epoch); + current_epoch +} + +pub fn arb_genesis_validators( + size: Range, + threshold: Option, +) -> impl Strategy> { + let threshold = threshold + .unwrap_or_else(|| PosParams::default().validator_stake_threshold); + let tokens: Vec<_> = (0..size.end) + .map(|ix| { + if ix == 0 { + // Make sure that at least one validator has at least a stake + // greater or equal to the threshold to avoid having an empty + // consensus set. + threshold.raw_amount().as_u64()..=10_000_000_u64 + } else { + 1..=10_000_000_u64 + } + .prop_map(token::Amount::from) + }) + .collect(); + (size, tokens) + .prop_map(|(size, token_amounts)| { + // use unique seeds to generate validators' address and consensus + // key + let seeds = (0_u64..).take(size); + seeds + .zip(token_amounts) + .map(|(seed, tokens)| { + let address = address_from_simple_seed(seed); + let consensus_sk = common_sk_from_simple_seed(seed); + let consensus_key = consensus_sk.to_public(); + + let protocol_sk = common_sk_from_simple_seed(seed); + let protocol_key = protocol_sk.to_public(); + + let eth_hot_key = key::common::PublicKey::Secp256k1( + key::testing::gen_keypair::( + ) + .ref_to(), + ); + let eth_cold_key = key::common::PublicKey::Secp256k1( + key::testing::gen_keypair::( + ) + .ref_to(), + ); + + let commission_rate = Dec::new(5, 2).expect("Test failed"); + let max_commission_rate_change = + Dec::new(1, 2).expect("Test failed"); + GenesisValidator { + address, + tokens, + consensus_key, + protocol_key, + eth_hot_key, + eth_cold_key, + commission_rate, + max_commission_rate_change, + metadata: Default::default(), + } + }) + .collect() + }) + .prop_filter( + "Must have at least one genesis validator with stake above the \ + provided threshold, if any.", + move |gen_vals: &Vec| { + gen_vals.iter().any(|val| val.tokens >= threshold) + }, + ) +} + +pub fn arb_redelegation_amounts( + max_delegation: u64, +) -> impl Strategy { + let arb_delegation = arb_amount_non_zero_ceiled(max_delegation); + let amounts = arb_delegation.prop_flat_map(move |amount_delegate| { + let amount_redelegate = arb_amount_non_zero_ceiled(max( + 1, + u64::try_from(amount_delegate.raw_amount()).unwrap() - 1, + )); + (Just(amount_delegate), amount_redelegate) + }); + amounts.prop_flat_map(move |(amount_delegate, amount_redelegate)| { + let amount_unbond = arb_amount_non_zero_ceiled(max( + 1, + u64::try_from(amount_redelegate.raw_amount()).unwrap() - 1, + )); + ( + Just(amount_delegate), + Just(amount_redelegate), + amount_unbond, + ) + }) +} diff --git a/proof_of_stake/src/tests/mod.rs b/proof_of_stake/src/tests/mod.rs new file mode 100644 index 0000000000..86cb3d6ca1 --- /dev/null +++ b/proof_of_stake/src/tests/mod.rs @@ -0,0 +1,8 @@ +mod helpers; +mod state_machine; +mod state_machine_v2; +mod test_helper_fns; +mod test_pos; +mod test_slash_and_redel; +mod test_validator; +mod utils; diff --git a/proof_of_stake/src/tests/state_machine.rs b/proof_of_stake/src/tests/state_machine.rs index 781d276105..1d07d6465b 100644 --- a/proof_of_stake/src/tests/state_machine.rs +++ b/proof_of_stake/src/tests/state_machine.rs @@ -29,14 +29,20 @@ use test_log::test; use crate::parameters::testing::arb_rate; use crate::parameters::PosParams; -use crate::tests::arb_params_and_genesis_validators; +use crate::storage::{ + enqueued_slashes_handle, read_all_validator_addresses, + read_below_capacity_validator_set_addresses, + read_below_capacity_validator_set_addresses_with_stake, + read_below_threshold_validator_set_addresses, + read_consensus_validator_set_addresses_with_stake, +}; +use crate::tests::helpers::{advance_epoch, arb_params_and_genesis_validators}; use crate::types::{ BondId, EagerRedelegatedBondsMap, GenesisValidator, ReverseOrdTokenAmount, Slash, SlashType, ValidatorState, WeightedValidator, }; use crate::{ below_capacity_validator_set_handle, consensus_validator_set_handle, - enqueued_slashes_handle, read_below_threshold_validator_set_addresses, read_pos_params, redelegate_tokens, validator_deltas_handle, validator_slashes_handle, validator_state_handle, BondsForRemovalRes, EagerRedelegatedUnbonds, FoldRedelegatedBondsResult, ModifiedRedelegation, @@ -243,11 +249,12 @@ impl StateMachineTest for ConcretePosState { match transition { Transition::NextEpoch => { tracing::debug!("\nCONCRETE Next epoch"); - super::advance_epoch(&mut state.s, ¶ms); + advance_epoch(&mut state.s, ¶ms); // Need to apply some slashing let current_epoch = state.s.storage.block.epoch; - super::process_slashes(&mut state.s, current_epoch).unwrap(); + crate::slashing::process_slashes(&mut state.s, current_epoch) + .unwrap(); let params = read_pos_params(&state.s).unwrap(); state.check_next_epoch_post_conditions(¶ms); @@ -264,9 +271,9 @@ impl StateMachineTest for ConcretePosState { tracing::debug!("\nCONCRETE Init validator"); let current_epoch = state.current_epoch(); - super::become_validator( + crate::become_validator( &mut state.s, - super::BecomeValidator { + crate::BecomeValidator { params: ¶ms, address: &address, consensus_key: &consensus_key, @@ -336,7 +343,7 @@ impl StateMachineTest for ConcretePosState { ); // Apply the bond - super::bond_tokens( + crate::bond_tokens( &mut state.s, Some(&id.source), &id.validator, @@ -403,7 +410,7 @@ impl StateMachineTest for ConcretePosState { .unwrap(); // Apply the unbond - super::unbond_tokens( + crate::unbond_tokens( &mut state.s, Some(&id.source), &id.validator, @@ -454,7 +461,7 @@ impl StateMachineTest for ConcretePosState { .unwrap(); // Apply the withdrawal - let withdrawn = super::withdraw_tokens( + let withdrawn = crate::withdraw_tokens( &mut state.s, Some(&source), &validator, @@ -547,9 +554,10 @@ impl StateMachineTest for ConcretePosState { .unwrap(); // Find delegations - let delegations_pre = - crate::find_delegations(&state.s, &id.source, &pipeline) - .unwrap(); + let delegations_pre = crate::queries::find_delegations( + &state.s, &id.source, &pipeline, + ) + .unwrap(); // Apply redelegation let result = redelegate_tokens( @@ -668,7 +676,7 @@ impl StateMachineTest for ConcretePosState { // updated with redelegation. For the source reduced by the // redelegation amount and for the destination increased by // the redelegation amount, less any slashes. - let delegations_post = crate::find_delegations( + let delegations_post = crate::queries::find_delegations( &state.s, &id.source, &pipeline, ) .unwrap(); @@ -707,7 +715,7 @@ impl StateMachineTest for ConcretePosState { tracing::debug!("\nCONCRETE Misbehavior"); let current_epoch = state.current_epoch(); // Record the slash evidence - super::slash( + crate::slashing::slash( &mut state.s, ¶ms, current_epoch, @@ -736,7 +744,7 @@ impl StateMachineTest for ConcretePosState { let current_epoch = state.current_epoch(); // Unjail the validator - super::unjail_validator(&mut state.s, &address, current_epoch) + crate::unjail_validator(&mut state.s, &address, current_epoch) .unwrap(); // Post-conditions @@ -769,13 +777,13 @@ impl ConcretePosState { // Post-condition: Consensus validator sets at pipeline offset // must be the same as at the epoch before it. let consensus_set_before_pipeline = - crate::read_consensus_validator_set_addresses_with_stake( + read_consensus_validator_set_addresses_with_stake( &self.s, before_pipeline, ) .unwrap(); let consensus_set_at_pipeline = - crate::read_consensus_validator_set_addresses_with_stake( + read_consensus_validator_set_addresses_with_stake( &self.s, pipeline, ) .unwrap(); @@ -787,13 +795,13 @@ impl ConcretePosState { // Post-condition: Below-capacity validator sets at pipeline // offset must be the same as at the epoch before it. let below_cap_before_pipeline = - crate::read_below_capacity_validator_set_addresses_with_stake( + read_below_capacity_validator_set_addresses_with_stake( &self.s, before_pipeline, ) .unwrap(); let below_cap_at_pipeline = - crate::read_below_capacity_validator_set_addresses_with_stake( + read_below_capacity_validator_set_addresses_with_stake( &self.s, pipeline, ) .unwrap(); @@ -846,7 +854,7 @@ impl ConcretePosState { ) { let pipeline = submit_epoch + params.pipeline_len; - let cur_stake = super::read_validator_stake( + let cur_stake = crate::read_validator_stake( &self.s, params, &id.validator, @@ -858,7 +866,7 @@ impl ConcretePosState { // change assert_eq!(cur_stake, validator_stake_before_bond_cur); - let stake_at_pipeline = super::read_validator_stake( + let stake_at_pipeline = crate::read_validator_stake( &self.s, params, &id.validator, @@ -915,7 +923,7 @@ impl ConcretePosState { ) { let pipeline = submit_epoch + params.pipeline_len; - let cur_stake = super::read_validator_stake( + let cur_stake = crate::read_validator_stake( &self.s, params, &id.validator, @@ -927,7 +935,7 @@ impl ConcretePosState { // change assert_eq!(cur_stake, validator_stake_before_unbond_cur); - let stake_at_pipeline = super::read_validator_stake( + let stake_at_pipeline = crate::read_validator_stake( &self.s, params, &id.validator, @@ -1171,21 +1179,18 @@ impl ConcretePosState { || (num_occurrences == 0 && validator_is_jailed) ); - let consensus_set = - crate::read_consensus_validator_set_addresses_with_stake( - &self.s, pipeline, - ) - .unwrap(); + let consensus_set = read_consensus_validator_set_addresses_with_stake( + &self.s, pipeline, + ) + .unwrap(); let below_cap_set = - crate::read_below_capacity_validator_set_addresses_with_stake( + read_below_capacity_validator_set_addresses_with_stake( &self.s, pipeline, ) .unwrap(); let below_thresh_set = - crate::read_below_threshold_validator_set_addresses( - &self.s, pipeline, - ) - .unwrap(); + read_below_threshold_validator_set_addresses(&self.s, pipeline) + .unwrap(); let weighted = WeightedValidator { bonded_stake: stake_at_pipeline, address: id.validator, @@ -1310,21 +1315,17 @@ impl ConcretePosState { .contains(address) ); assert!( - !crate::read_below_capacity_validator_set_addresses( - &self.s, epoch - ) - .unwrap() - .contains(address) + !read_below_capacity_validator_set_addresses(&self.s, epoch) + .unwrap() + .contains(address) ); assert!( - !crate::read_below_threshold_validator_set_addresses( - &self.s, epoch - ) - .unwrap() - .contains(address) + !read_below_threshold_validator_set_addresses(&self.s, epoch) + .unwrap() + .contains(address) ); assert!( - !crate::read_all_validator_addresses(&self.s, epoch) + !read_all_validator_addresses(&self.s, epoch) .unwrap() .contains(address) ); @@ -1333,17 +1334,14 @@ impl ConcretePosState { crate::read_consensus_validator_set_addresses(&self.s, pipeline) .unwrap() .contains(address); - let in_bc = crate::read_below_capacity_validator_set_addresses( - &self.s, pipeline, - ) - .unwrap() - .contains(address); + let in_bc = + read_below_capacity_validator_set_addresses(&self.s, pipeline) + .unwrap() + .contains(address); let in_below_thresh = - crate::read_below_threshold_validator_set_addresses( - &self.s, pipeline, - ) - .unwrap() - .contains(address); + read_below_threshold_validator_set_addresses(&self.s, pipeline) + .unwrap() + .contains(address); assert!(in_below_thresh && !in_consensus && !in_bc); } @@ -1410,7 +1408,7 @@ impl ConcretePosState { let abs_enqueued = ref_state.enqueued_slashes.clone(); let mut conc_enqueued: BTreeMap>> = BTreeMap::new(); - crate::enqueued_slashes_handle() + enqueued_slashes_handle() .get_data_handler() .iter(&self.s) .unwrap() @@ -1764,7 +1762,7 @@ impl ConcretePosState { for WeightedValidator { bonded_stake, address: validator, - } in crate::read_consensus_validator_set_addresses_with_stake( + } in read_consensus_validator_set_addresses_with_stake( &self.s, epoch, ) .unwrap() @@ -1821,11 +1819,10 @@ impl ConcretePosState { for WeightedValidator { bonded_stake, address: validator, - } in - crate::read_below_capacity_validator_set_addresses_with_stake( - &self.s, epoch, - ) - .unwrap() + } in read_below_capacity_validator_set_addresses_with_stake( + &self.s, epoch, + ) + .unwrap() { let deltas_stake = validator_deltas_handle(&validator) .get_sum(&self.s, epoch, params) @@ -1873,10 +1870,8 @@ impl ConcretePosState { } for validator in - crate::read_below_threshold_validator_set_addresses( - &self.s, epoch, - ) - .unwrap() + read_below_threshold_validator_set_addresses(&self.s, epoch) + .unwrap() { let stake = crate::read_validator_stake( &self.s, params, &validator, epoch, @@ -1921,7 +1916,7 @@ impl ConcretePosState { // Jailed validators not in a set let all_validators = - crate::read_all_validator_addresses(&self.s, epoch).unwrap(); + read_all_validator_addresses(&self.s, epoch).unwrap(); for validator in all_validators { let state = validator_state_handle(&validator) diff --git a/proof_of_stake/src/tests/state_machine_v2.rs b/proof_of_stake/src/tests/state_machine_v2.rs index cf59a59238..c75d629995 100644 --- a/proof_of_stake/src/tests/state_machine_v2.rs +++ b/proof_of_stake/src/tests/state_machine_v2.rs @@ -29,10 +29,20 @@ use proptest_state_machine::{ use test_log::test; use yansi::Paint; +use super::helpers::advance_epoch; use super::utils::DbgPrintDiff; use crate::parameters::testing::arb_rate; use crate::parameters::PosParams; -use crate::tests::arb_params_and_genesis_validators; +use crate::queries::find_delegations; +use crate::slashing::find_slashes_in_range; +use crate::storage::{ + enqueued_slashes_handle, read_all_validator_addresses, + read_below_capacity_validator_set_addresses, + read_below_capacity_validator_set_addresses_with_stake, + read_below_threshold_validator_set_addresses, + read_consensus_validator_set_addresses_with_stake, +}; +use crate::tests::helpers::arb_params_and_genesis_validators; use crate::tests::utils::pause_for_enter; use crate::types::{ BondId, GenesisValidator, ReverseOrdTokenAmount, Slash, SlashType, @@ -41,10 +51,8 @@ use crate::types::{ use crate::{ below_capacity_validator_set_handle, bond_handle, consensus_validator_set_handle, delegator_redelegated_bonds_handle, - enqueued_slashes_handle, find_slashes_in_range, - read_below_threshold_validator_set_addresses, read_pos_params, - redelegate_tokens, validator_deltas_handle, validator_slashes_handle, - validator_state_handle, RedelegationError, + read_pos_params, redelegate_tokens, validator_deltas_handle, + validator_slashes_handle, validator_state_handle, RedelegationError, }; prop_state_machine! { @@ -1971,11 +1979,12 @@ impl StateMachineTest for ConcretePosState { match transition { Transition::NextEpoch => { tracing::debug!("\nCONCRETE Next epoch"); - super::advance_epoch(&mut state.s, ¶ms); + advance_epoch(&mut state.s, ¶ms); // Need to apply some slashing let current_epoch = state.s.storage.block.epoch; - super::process_slashes(&mut state.s, current_epoch).unwrap(); + crate::slashing::process_slashes(&mut state.s, current_epoch) + .unwrap(); let params = read_pos_params(&state.s).unwrap(); state.check_next_epoch_post_conditions(¶ms); @@ -1992,9 +2001,9 @@ impl StateMachineTest for ConcretePosState { tracing::debug!("\nCONCRETE Init validator"); let current_epoch = state.current_epoch(); - super::become_validator( + crate::become_validator( &mut state.s, - super::BecomeValidator { + crate::BecomeValidator { params: ¶ms, address: &address, consensus_key: &consensus_key, @@ -2064,7 +2073,7 @@ impl StateMachineTest for ConcretePosState { ); // Apply the bond - super::bond_tokens( + crate::bond_tokens( &mut state.s, Some(&id.source), &id.validator, @@ -2129,7 +2138,7 @@ impl StateMachineTest for ConcretePosState { .unwrap(); // Apply the unbond - super::unbond_tokens( + crate::unbond_tokens( &mut state.s, Some(&id.source), &id.validator, @@ -2205,7 +2214,7 @@ impl StateMachineTest for ConcretePosState { // .unwrap(); // Apply the withdrawal - let withdrawn = super::withdraw_tokens( + let withdrawn = crate::withdraw_tokens( &mut state.s, Some(&source), &validator, @@ -2454,8 +2463,7 @@ impl StateMachineTest for ConcretePosState { // Find delegations let delegations_pre = - crate::find_delegations(&state.s, &id.source, &pipeline) - .unwrap(); + find_delegations(&state.s, &id.source, &pipeline).unwrap(); // Apply redelegation let result = redelegate_tokens( @@ -2607,10 +2615,9 @@ impl StateMachineTest for ConcretePosState { // updated with redelegation. For the source reduced by the // redelegation amount and for the destination increased by // the redelegation amount, less any slashes. - let delegations_post = crate::find_delegations( - &state.s, &id.source, &pipeline, - ) - .unwrap(); + let delegations_post = + find_delegations(&state.s, &id.source, &pipeline) + .unwrap(); let src_delegation_pre = delegations_pre .get(&id.validator) .cloned() @@ -2663,7 +2670,7 @@ impl StateMachineTest for ConcretePosState { tracing::debug!("\nCONCRETE Misbehavior"); let current_epoch = state.current_epoch(); // Record the slash evidence - super::slash( + crate::slashing::slash( &mut state.s, ¶ms, current_epoch, @@ -2692,7 +2699,7 @@ impl StateMachineTest for ConcretePosState { let current_epoch = state.current_epoch(); // Unjail the validator - super::unjail_validator(&mut state.s, &address, current_epoch) + crate::unjail_validator(&mut state.s, &address, current_epoch) .unwrap(); // Post-conditions @@ -2725,13 +2732,13 @@ impl ConcretePosState { // Post-condition: Consensus validator sets at pipeline offset // must be the same as at the epoch before it. let consensus_set_before_pipeline = - crate::read_consensus_validator_set_addresses_with_stake( + read_consensus_validator_set_addresses_with_stake( &self.s, before_pipeline, ) .unwrap(); let consensus_set_at_pipeline = - crate::read_consensus_validator_set_addresses_with_stake( + read_consensus_validator_set_addresses_with_stake( &self.s, pipeline, ) .unwrap(); @@ -2743,13 +2750,13 @@ impl ConcretePosState { // Post-condition: Below-capacity validator sets at pipeline // offset must be the same as at the epoch before it. let below_cap_before_pipeline = - crate::read_below_capacity_validator_set_addresses_with_stake( + read_below_capacity_validator_set_addresses_with_stake( &self.s, before_pipeline, ) .unwrap(); let below_cap_at_pipeline = - crate::read_below_capacity_validator_set_addresses_with_stake( + read_below_capacity_validator_set_addresses_with_stake( &self.s, pipeline, ) .unwrap(); @@ -2802,7 +2809,7 @@ impl ConcretePosState { ) { let pipeline = submit_epoch + params.pipeline_len; - let cur_stake = super::read_validator_stake( + let cur_stake = crate::read_validator_stake( &self.s, params, &id.validator, @@ -2814,7 +2821,7 @@ impl ConcretePosState { // change assert_eq!(cur_stake, validator_stake_before_bond_cur); - let stake_at_pipeline = super::read_validator_stake( + let stake_at_pipeline = crate::read_validator_stake( &self.s, params, &id.validator, @@ -2848,7 +2855,7 @@ impl ConcretePosState { ) { let pipeline = submit_epoch + params.pipeline_len; - let cur_stake = super::read_validator_stake( + let cur_stake = crate::read_validator_stake( &self.s, params, &id.validator, @@ -2860,7 +2867,7 @@ impl ConcretePosState { // change assert_eq!(cur_stake, validator_stake_before_unbond_cur); - let stake_at_pipeline = super::read_validator_stake( + let stake_at_pipeline = crate::read_validator_stake( &self.s, params, &id.validator, @@ -2938,21 +2945,18 @@ impl ConcretePosState { || (num_occurrences == 0 && validator_is_jailed) ); - let consensus_set = - crate::read_consensus_validator_set_addresses_with_stake( - &self.s, pipeline, - ) - .unwrap(); + let consensus_set = read_consensus_validator_set_addresses_with_stake( + &self.s, pipeline, + ) + .unwrap(); let below_cap_set = - crate::read_below_capacity_validator_set_addresses_with_stake( + read_below_capacity_validator_set_addresses_with_stake( &self.s, pipeline, ) .unwrap(); let below_thresh_set = - crate::read_below_threshold_validator_set_addresses( - &self.s, pipeline, - ) - .unwrap(); + read_below_threshold_validator_set_addresses(&self.s, pipeline) + .unwrap(); let weighted = WeightedValidator { bonded_stake: stake_at_pipeline, address: id.validator, @@ -3015,21 +3019,17 @@ impl ConcretePosState { .contains(address) ); assert!( - !crate::read_below_capacity_validator_set_addresses( - &self.s, epoch - ) - .unwrap() - .contains(address) + !read_below_capacity_validator_set_addresses(&self.s, epoch) + .unwrap() + .contains(address) ); assert!( - !crate::read_below_threshold_validator_set_addresses( - &self.s, epoch - ) - .unwrap() - .contains(address) + !read_below_threshold_validator_set_addresses(&self.s, epoch) + .unwrap() + .contains(address) ); assert!( - !crate::read_all_validator_addresses(&self.s, epoch) + !read_all_validator_addresses(&self.s, epoch) .unwrap() .contains(address) ); @@ -3038,17 +3038,14 @@ impl ConcretePosState { crate::read_consensus_validator_set_addresses(&self.s, pipeline) .unwrap() .contains(address); - let in_bc = crate::read_below_capacity_validator_set_addresses( - &self.s, pipeline, - ) - .unwrap() - .contains(address); + let in_bc = + read_below_capacity_validator_set_addresses(&self.s, pipeline) + .unwrap() + .contains(address); let in_below_thresh = - crate::read_below_threshold_validator_set_addresses( - &self.s, pipeline, - ) - .unwrap() - .contains(address); + read_below_threshold_validator_set_addresses(&self.s, pipeline) + .unwrap() + .contains(address); assert!(in_below_thresh && !in_consensus && !in_bc); } @@ -3204,7 +3201,7 @@ impl ConcretePosState { for WeightedValidator { bonded_stake, address: validator, - } in crate::read_consensus_validator_set_addresses_with_stake( + } in read_consensus_validator_set_addresses_with_stake( &self.s, epoch, ) .unwrap() @@ -3274,11 +3271,10 @@ impl ConcretePosState { for WeightedValidator { bonded_stake, address: validator, - } in - crate::read_below_capacity_validator_set_addresses_with_stake( - &self.s, epoch, - ) - .unwrap() + } in read_below_capacity_validator_set_addresses_with_stake( + &self.s, epoch, + ) + .unwrap() { let deltas_stake = validator_deltas_handle(&validator) .get_sum(&self.s, epoch, params) @@ -3359,10 +3355,8 @@ impl ConcretePosState { } for validator in - crate::read_below_threshold_validator_set_addresses( - &self.s, epoch, - ) - .unwrap() + read_below_threshold_validator_set_addresses(&self.s, epoch) + .unwrap() { let conc_stake = validator_deltas_handle(&validator) .get_sum(&self.s, epoch, params) @@ -3425,7 +3419,7 @@ impl ConcretePosState { // Jailed validators not in a set let all_validators = - crate::read_all_validator_addresses(&self.s, epoch).unwrap(); + read_all_validator_addresses(&self.s, epoch).unwrap(); for val in all_validators { let state = validator_state_handle(&val) diff --git a/proof_of_stake/src/tests/test_helper_fns.rs b/proof_of_stake/src/tests/test_helper_fns.rs new file mode 100644 index 0000000000..594965fe43 --- /dev/null +++ b/proof_of_stake/src/tests/test_helper_fns.rs @@ -0,0 +1,2034 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use namada_core::ledger::storage::testing::TestWlStorage; +use namada_core::ledger::storage_api::collections::lazy_map::NestedMap; +use namada_core::ledger::storage_api::collections::LazyCollection; +use namada_core::types::address::testing::{ + established_address_1, established_address_2, established_address_3, +}; +use namada_core::types::dec::Dec; +use namada_core::types::storage::{Epoch, Key}; +use namada_core::types::token; + +use crate::slashing::{ + apply_list_slashes, compute_amount_after_slashing_unbond, + compute_amount_after_slashing_withdraw, compute_bond_at_epoch, + compute_slash_bond_at_epoch, compute_slashable_amount, slash_redelegation, + slash_validator, slash_validator_redelegation, +}; +use crate::storage::{ + bond_handle, delegator_redelegated_bonds_handle, total_bonded_handle, + total_unbonded_handle, validator_outgoing_redelegations_handle, + validator_slashes_handle, validator_total_redelegated_bonded_handle, + validator_total_redelegated_unbonded_handle, write_pos_params, +}; +use crate::types::{ + EagerRedelegatedBondsMap, RedelegatedTokens, Slash, SlashType, +}; +use crate::{ + compute_modified_redelegation, compute_new_redelegated_unbonds, + find_bonds_to_remove, fold_and_slash_redelegated_bonds, + EagerRedelegatedUnbonds, FoldRedelegatedBondsResult, ModifiedRedelegation, + OwnedPosParams, +}; + +/// `iterateBondsUpToAmountTest` +#[test] +fn test_find_bonds_to_remove() { + let mut storage = TestWlStorage::default(); + let gov_params = namada_core::ledger::governance::parameters::GovernanceParameters::default(); + gov_params.init_storage(&mut storage).unwrap(); + write_pos_params(&mut storage, &OwnedPosParams::default()).unwrap(); + + let source = established_address_1(); + let validator = established_address_2(); + let bond_handle = bond_handle(&source, &validator); + + let (e1, e2, e6) = (Epoch(1), Epoch(2), Epoch(6)); + + bond_handle + .set(&mut storage, token::Amount::from(5), e1, 0) + .unwrap(); + bond_handle + .set(&mut storage, token::Amount::from(3), e2, 0) + .unwrap(); + bond_handle + .set(&mut storage, token::Amount::from(8), e6, 0) + .unwrap(); + + // Test 1 + let bonds_for_removal = find_bonds_to_remove( + &storage, + &bond_handle.get_data_handler(), + token::Amount::from(8), + ) + .unwrap(); + assert_eq!( + bonds_for_removal.epochs, + vec![e6].into_iter().collect::>() + ); + assert!(bonds_for_removal.new_entry.is_none()); + + // Test 2 + let bonds_for_removal = find_bonds_to_remove( + &storage, + &bond_handle.get_data_handler(), + token::Amount::from(10), + ) + .unwrap(); + assert_eq!( + bonds_for_removal.epochs, + vec![e6].into_iter().collect::>() + ); + assert_eq!( + bonds_for_removal.new_entry, + Some((Epoch(2), token::Amount::from(1))) + ); + + // Test 3 + let bonds_for_removal = find_bonds_to_remove( + &storage, + &bond_handle.get_data_handler(), + token::Amount::from(11), + ) + .unwrap(); + assert_eq!( + bonds_for_removal.epochs, + vec![e6, e2].into_iter().collect::>() + ); + assert!(bonds_for_removal.new_entry.is_none()); + + // Test 4 + let bonds_for_removal = find_bonds_to_remove( + &storage, + &bond_handle.get_data_handler(), + token::Amount::from(12), + ) + .unwrap(); + assert_eq!( + bonds_for_removal.epochs, + vec![e6, e2].into_iter().collect::>() + ); + assert_eq!( + bonds_for_removal.new_entry, + Some((Epoch(1), token::Amount::from(4))) + ); +} + +/// `computeModifiedRedelegationTest` +#[test] +fn test_compute_modified_redelegation() { + let mut storage = TestWlStorage::default(); + let validator1 = established_address_1(); + let validator2 = established_address_2(); + let owner = established_address_3(); + let outer_epoch = Epoch(0); + + let mut alice = validator1.clone(); + let mut bob = validator2.clone(); + + // Ensure a ranking order of alice > bob + if bob > alice { + alice = validator2; + bob = validator1; + } + println!("\n\nalice = {}\nbob = {}\n", &alice, &bob); + + // Fill redelegated bonds in storage + let redelegated_bonds_map = delegator_redelegated_bonds_handle(&owner) + .at(&alice) + .at(&outer_epoch); + redelegated_bonds_map + .at(&alice) + .insert(&mut storage, Epoch(2), token::Amount::from(6)) + .unwrap(); + redelegated_bonds_map + .at(&alice) + .insert(&mut storage, Epoch(4), token::Amount::from(7)) + .unwrap(); + redelegated_bonds_map + .at(&bob) + .insert(&mut storage, Epoch(1), token::Amount::from(5)) + .unwrap(); + redelegated_bonds_map + .at(&bob) + .insert(&mut storage, Epoch(4), token::Amount::from(7)) + .unwrap(); + + // Test cases 1 and 2 + let mr1 = compute_modified_redelegation( + &storage, + &redelegated_bonds_map, + Epoch(5), + token::Amount::from(25), + ) + .unwrap(); + let mr2 = compute_modified_redelegation( + &storage, + &redelegated_bonds_map, + Epoch(5), + token::Amount::from(30), + ) + .unwrap(); + + let exp_mr = ModifiedRedelegation { + epoch: Some(Epoch(5)), + ..Default::default() + }; + + assert_eq!(mr1, exp_mr); + assert_eq!(mr2, exp_mr); + + // Test case 3 + let mr3 = compute_modified_redelegation( + &storage, + &redelegated_bonds_map, + Epoch(5), + token::Amount::from(7), + ) + .unwrap(); + + let exp_mr = ModifiedRedelegation { + epoch: Some(Epoch(5)), + validators_to_remove: BTreeSet::from_iter([bob.clone()]), + validator_to_modify: Some(bob.clone()), + epochs_to_remove: BTreeSet::from_iter([Epoch(4)]), + ..Default::default() + }; + assert_eq!(mr3, exp_mr); + + // Test case 4 + let mr4 = compute_modified_redelegation( + &storage, + &redelegated_bonds_map, + Epoch(5), + token::Amount::from(8), + ) + .unwrap(); + + let exp_mr = ModifiedRedelegation { + epoch: Some(Epoch(5)), + validators_to_remove: BTreeSet::from_iter([bob.clone()]), + validator_to_modify: Some(bob.clone()), + epochs_to_remove: BTreeSet::from_iter([Epoch(1), Epoch(4)]), + epoch_to_modify: Some(Epoch(1)), + new_amount: Some(4.into()), + }; + assert_eq!(mr4, exp_mr); + + // Test case 5 + let mr5 = compute_modified_redelegation( + &storage, + &redelegated_bonds_map, + Epoch(5), + 12.into(), + ) + .unwrap(); + + let exp_mr = ModifiedRedelegation { + epoch: Some(Epoch(5)), + validators_to_remove: BTreeSet::from_iter([bob.clone()]), + ..Default::default() + }; + assert_eq!(mr5, exp_mr); + + // Test case 6 + let mr6 = compute_modified_redelegation( + &storage, + &redelegated_bonds_map, + Epoch(5), + 14.into(), + ) + .unwrap(); + + let exp_mr = ModifiedRedelegation { + epoch: Some(Epoch(5)), + validators_to_remove: BTreeSet::from_iter([alice.clone(), bob.clone()]), + validator_to_modify: Some(alice.clone()), + epochs_to_remove: BTreeSet::from_iter([Epoch(4)]), + epoch_to_modify: Some(Epoch(4)), + new_amount: Some(5.into()), + }; + assert_eq!(mr6, exp_mr); + + // Test case 7 + let mr7 = compute_modified_redelegation( + &storage, + &redelegated_bonds_map, + Epoch(5), + 19.into(), + ) + .unwrap(); + + let exp_mr = ModifiedRedelegation { + epoch: Some(Epoch(5)), + validators_to_remove: BTreeSet::from_iter([alice.clone(), bob.clone()]), + validator_to_modify: Some(alice.clone()), + epochs_to_remove: BTreeSet::from_iter([Epoch(4)]), + ..Default::default() + }; + assert_eq!(mr7, exp_mr); + + // Test case 8 + let mr8 = compute_modified_redelegation( + &storage, + &redelegated_bonds_map, + Epoch(5), + 21.into(), + ) + .unwrap(); + + let exp_mr = ModifiedRedelegation { + epoch: Some(Epoch(5)), + validators_to_remove: BTreeSet::from_iter([alice.clone(), bob]), + validator_to_modify: Some(alice), + epochs_to_remove: BTreeSet::from_iter([Epoch(2), Epoch(4)]), + epoch_to_modify: Some(Epoch(2)), + new_amount: Some(4.into()), + }; + assert_eq!(mr8, exp_mr); +} + +/// `computeBondAtEpochTest` +#[test] +fn test_compute_bond_at_epoch() { + let mut storage = TestWlStorage::default(); + let params = OwnedPosParams { + pipeline_len: 2, + unbonding_len: 4, + cubic_slashing_window_length: 1, + ..Default::default() + }; + let alice = established_address_1(); + let bob = established_address_2(); + + // Test 1 + let res = compute_bond_at_epoch( + &storage, + ¶ms, + &bob, + 12.into(), + 3.into(), + 23.into(), + Some(&Default::default()), + ) + .unwrap(); + + pretty_assertions::assert_eq!(res, 23.into()); + + // Test 2 + validator_slashes_handle(&bob) + .push( + &mut storage, + Slash { + epoch: 4.into(), + block_height: 0, + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }, + ) + .unwrap(); + let res = compute_bond_at_epoch( + &storage, + ¶ms, + &bob, + 12.into(), + 3.into(), + 23.into(), + Some(&Default::default()), + ) + .unwrap(); + + pretty_assertions::assert_eq!(res, 0.into()); + + // Test 3 + validator_slashes_handle(&bob).pop(&mut storage).unwrap(); + let mut redel_bonds = EagerRedelegatedBondsMap::default(); + redel_bonds.insert( + alice.clone(), + BTreeMap::from_iter([(Epoch(1), token::Amount::from(5))]), + ); + let res = compute_bond_at_epoch( + &storage, + ¶ms, + &bob, + 12.into(), + 3.into(), + 23.into(), + Some(&redel_bonds), + ) + .unwrap(); + + pretty_assertions::assert_eq!(res, 23.into()); + + // Test 4 + validator_slashes_handle(&bob) + .push( + &mut storage, + Slash { + epoch: 4.into(), + block_height: 0, + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }, + ) + .unwrap(); + let res = compute_bond_at_epoch( + &storage, + ¶ms, + &bob, + 12.into(), + 3.into(), + 23.into(), + Some(&redel_bonds), + ) + .unwrap(); + + pretty_assertions::assert_eq!(res, 0.into()); + + // Test 5 + validator_slashes_handle(&bob).pop(&mut storage).unwrap(); + validator_slashes_handle(&alice) + .push( + &mut storage, + Slash { + epoch: 6.into(), + block_height: 0, + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }, + ) + .unwrap(); + let res = compute_bond_at_epoch( + &storage, + ¶ms, + &bob, + 12.into(), + 3.into(), + 23.into(), + Some(&redel_bonds), + ) + .unwrap(); + + pretty_assertions::assert_eq!(res, 23.into()); + + // Test 6 + validator_slashes_handle(&alice).pop(&mut storage).unwrap(); + validator_slashes_handle(&alice) + .push( + &mut storage, + Slash { + epoch: 4.into(), + block_height: 0, + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }, + ) + .unwrap(); + let res = compute_bond_at_epoch( + &storage, + ¶ms, + &bob, + 18.into(), + 9.into(), + 23.into(), + Some(&redel_bonds), + ) + .unwrap(); + + pretty_assertions::assert_eq!(res, 18.into()); +} + +/// `computeSlashBondAtEpochTest` +#[test] +fn test_compute_slash_bond_at_epoch() { + let mut storage = TestWlStorage::default(); + let params = OwnedPosParams { + pipeline_len: 2, + unbonding_len: 4, + cubic_slashing_window_length: 1, + ..Default::default() + }; + let alice = established_address_1(); + let bob = established_address_2(); + + let current_epoch = Epoch(20); + let infraction_epoch = + current_epoch - params.slash_processing_epoch_offset(); + + let redelegated_bond = BTreeMap::from_iter([( + alice, + BTreeMap::from_iter([(infraction_epoch - 4, token::Amount::from(10))]), + )]); + + // Test 1 + let res = compute_slash_bond_at_epoch( + &storage, + ¶ms, + &bob, + current_epoch.next(), + infraction_epoch, + infraction_epoch - 2, + 30.into(), + Some(&Default::default()), + Dec::one(), + ) + .unwrap(); + + pretty_assertions::assert_eq!(res, 30.into()); + + // Test 2 + let res = compute_slash_bond_at_epoch( + &storage, + ¶ms, + &bob, + current_epoch.next(), + infraction_epoch, + infraction_epoch - 2, + 30.into(), + Some(&redelegated_bond), + Dec::one(), + ) + .unwrap(); + + pretty_assertions::assert_eq!(res, 30.into()); + + // Test 3 + validator_slashes_handle(&bob) + .push( + &mut storage, + Slash { + epoch: infraction_epoch.prev(), + block_height: 0, + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }, + ) + .unwrap(); + let res = compute_slash_bond_at_epoch( + &storage, + ¶ms, + &bob, + current_epoch.next(), + infraction_epoch, + infraction_epoch - 2, + 30.into(), + Some(&Default::default()), + Dec::one(), + ) + .unwrap(); + + pretty_assertions::assert_eq!(res, 0.into()); + + // Test 4 + let res = compute_slash_bond_at_epoch( + &storage, + ¶ms, + &bob, + current_epoch.next(), + infraction_epoch, + infraction_epoch - 2, + 30.into(), + Some(&redelegated_bond), + Dec::one(), + ) + .unwrap(); + + pretty_assertions::assert_eq!(res, 0.into()); +} + +/// `computeNewRedelegatedUnbondsTest` +#[test] +fn test_compute_new_redelegated_unbonds() { + let mut storage = TestWlStorage::default(); + let alice = established_address_1(); + let bob = established_address_2(); + + let key = Key::parse("testing").unwrap(); + let redelegated_bonds = NestedMap::::open(key); + + // Populate the lazy and eager maps + let (ep1, ep2, ep4, ep5, ep6, ep7) = + (Epoch(1), Epoch(2), Epoch(4), Epoch(5), Epoch(6), Epoch(7)); + let keys_and_values = vec![ + (ep5, alice.clone(), ep2, 1), + (ep5, alice.clone(), ep4, 1), + (ep7, alice.clone(), ep2, 1), + (ep7, alice.clone(), ep4, 1), + (ep5, bob.clone(), ep1, 1), + (ep5, bob.clone(), ep4, 2), + (ep7, bob.clone(), ep1, 1), + (ep7, bob.clone(), ep4, 2), + ]; + let mut eager_map = BTreeMap::::new(); + for (outer_ep, address, inner_ep, amount) in keys_and_values { + redelegated_bonds + .at(&outer_ep) + .at(&address) + .insert(&mut storage, inner_ep, token::Amount::from(amount)) + .unwrap(); + eager_map + .entry(outer_ep) + .or_default() + .entry(address.clone()) + .or_default() + .insert(inner_ep, token::Amount::from(amount)); + } + + // Different ModifiedRedelegation objects for testing + let empty_mr = ModifiedRedelegation::default(); + let all_mr = ModifiedRedelegation { + epoch: Some(ep7), + validators_to_remove: BTreeSet::from_iter([alice.clone(), bob.clone()]), + validator_to_modify: None, + epochs_to_remove: Default::default(), + epoch_to_modify: None, + new_amount: None, + }; + let mod_val_mr = ModifiedRedelegation { + epoch: Some(ep7), + validators_to_remove: BTreeSet::from_iter([alice.clone()]), + validator_to_modify: None, + epochs_to_remove: Default::default(), + epoch_to_modify: None, + new_amount: None, + }; + let mod_val_partial_mr = ModifiedRedelegation { + epoch: Some(ep7), + validators_to_remove: BTreeSet::from_iter([alice.clone(), bob.clone()]), + validator_to_modify: Some(bob.clone()), + epochs_to_remove: BTreeSet::from_iter([ep1]), + epoch_to_modify: None, + new_amount: None, + }; + let mod_epoch_partial_mr = ModifiedRedelegation { + epoch: Some(ep7), + validators_to_remove: BTreeSet::from_iter([alice, bob.clone()]), + validator_to_modify: Some(bob.clone()), + epochs_to_remove: BTreeSet::from_iter([ep1, ep4]), + epoch_to_modify: Some(ep4), + new_amount: Some(token::Amount::from(1)), + }; + + // Test case 1 + let res = compute_new_redelegated_unbonds( + &storage, + &redelegated_bonds, + &Default::default(), + &empty_mr, + ) + .unwrap(); + assert_eq!(res, Default::default()); + + let set5 = BTreeSet::::from_iter([ep5]); + let set56 = BTreeSet::::from_iter([ep5, ep6]); + + // Test case 2 + let res = compute_new_redelegated_unbonds( + &storage, + &redelegated_bonds, + &set5, + &empty_mr, + ) + .unwrap(); + let mut exp_res = eager_map.clone(); + exp_res.remove(&ep7); + assert_eq!(res, exp_res); + + // Test case 3 + let res = compute_new_redelegated_unbonds( + &storage, + &redelegated_bonds, + &set56, + &empty_mr, + ) + .unwrap(); + assert_eq!(res, exp_res); + + // Test case 4 + println!("\nTEST CASE 4\n"); + let res = compute_new_redelegated_unbonds( + &storage, + &redelegated_bonds, + &set56, + &all_mr, + ) + .unwrap(); + assert_eq!(res, eager_map); + + // Test case 5 + let res = compute_new_redelegated_unbonds( + &storage, + &redelegated_bonds, + &set56, + &mod_val_mr, + ) + .unwrap(); + exp_res = eager_map.clone(); + exp_res.entry(ep7).or_default().remove(&bob); + assert_eq!(res, exp_res); + + // Test case 6 + let res = compute_new_redelegated_unbonds( + &storage, + &redelegated_bonds, + &set56, + &mod_val_partial_mr, + ) + .unwrap(); + exp_res = eager_map.clone(); + exp_res + .entry(ep7) + .or_default() + .entry(bob.clone()) + .or_default() + .remove(&ep4); + assert_eq!(res, exp_res); + + // Test case 7 + let res = compute_new_redelegated_unbonds( + &storage, + &redelegated_bonds, + &set56, + &mod_epoch_partial_mr, + ) + .unwrap(); + exp_res + .entry(ep7) + .or_default() + .entry(bob) + .or_default() + .insert(ep4, token::Amount::from(1)); + assert_eq!(res, exp_res); +} + +/// `applyListSlashesTest` +#[test] +fn test_apply_list_slashes() { + let init_epoch = Epoch(2); + let params = OwnedPosParams { + unbonding_len: 4, + ..Default::default() + }; + // let unbonding_len = 4u64; + // let cubic_offset = 1u64; + + let slash1 = Slash { + epoch: init_epoch, + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }; + let slash2 = Slash { + epoch: init_epoch + + params.unbonding_len + + params.cubic_slashing_window_length + + 1u64, + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }; + + let list1 = vec![slash1.clone()]; + let list2 = vec![slash1.clone(), slash2.clone()]; + let list3 = vec![slash1.clone(), slash1.clone()]; + let list4 = vec![slash1.clone(), slash1, slash2]; + + let res = apply_list_slashes(¶ms, &[], token::Amount::from(100)); + assert_eq!(res, token::Amount::from(100)); + + let res = apply_list_slashes(¶ms, &list1, token::Amount::from(100)); + assert_eq!(res, token::Amount::zero()); + + let res = apply_list_slashes(¶ms, &list2, token::Amount::from(100)); + assert_eq!(res, token::Amount::zero()); + + let res = apply_list_slashes(¶ms, &list3, token::Amount::from(100)); + assert_eq!(res, token::Amount::zero()); + + let res = apply_list_slashes(¶ms, &list4, token::Amount::from(100)); + assert_eq!(res, token::Amount::zero()); +} + +/// `computeSlashableAmountTest` +#[test] +fn test_compute_slashable_amount() { + let init_epoch = Epoch(2); + let params = OwnedPosParams { + unbonding_len: 4, + ..Default::default() + }; + + let slash1 = Slash { + epoch: init_epoch + + params.unbonding_len + + params.cubic_slashing_window_length, + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }; + + let slash2 = Slash { + epoch: init_epoch + + params.unbonding_len + + params.cubic_slashing_window_length + + 1u64, + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }; + + let test_map = vec![(init_epoch, token::Amount::from(50))] + .into_iter() + .collect::>(); + + let res = compute_slashable_amount( + ¶ms, + &slash1, + token::Amount::from(100), + &BTreeMap::new(), + ); + assert_eq!(res, token::Amount::from(100)); + + let res = compute_slashable_amount( + ¶ms, + &slash2, + token::Amount::from(100), + &test_map, + ); + assert_eq!(res, token::Amount::from(50)); + + let res = compute_slashable_amount( + ¶ms, + &slash1, + token::Amount::from(100), + &test_map, + ); + assert_eq!(res, token::Amount::from(100)); +} + +/// `foldAndSlashRedelegatedBondsMapTest` +#[test] +fn test_fold_and_slash_redelegated_bonds() { + let mut storage = TestWlStorage::default(); + let params = OwnedPosParams { + unbonding_len: 4, + ..Default::default() + }; + let start_epoch = Epoch(7); + + let alice = established_address_1(); + let bob = established_address_2(); + + println!("\n\nAlice: {}", alice); + println!("Bob: {}\n", bob); + + let test_slash = Slash { + epoch: Default::default(), + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }; + + let test_data = vec![ + (alice.clone(), vec![(2, 1), (4, 1)]), + (bob, vec![(1, 1), (4, 2)]), + ]; + let mut eager_redel_bonds = EagerRedelegatedBondsMap::default(); + for (address, pair) in test_data { + for (epoch, amount) in pair { + eager_redel_bonds + .entry(address.clone()) + .or_default() + .insert(Epoch(epoch), token::Amount::from(amount)); + } + } + + // Test case 1 + let res = fold_and_slash_redelegated_bonds( + &storage, + ¶ms, + &eager_redel_bonds, + start_epoch, + &[], + |_| true, + ); + assert_eq!( + res, + FoldRedelegatedBondsResult { + total_redelegated: token::Amount::from(5), + total_after_slashing: token::Amount::from(5), + } + ); + + // Test case 2 + let res = fold_and_slash_redelegated_bonds( + &storage, + ¶ms, + &eager_redel_bonds, + start_epoch, + &[test_slash], + |_| true, + ); + assert_eq!( + res, + FoldRedelegatedBondsResult { + total_redelegated: token::Amount::from(5), + total_after_slashing: token::Amount::zero(), + } + ); + + // Test case 3 + let alice_slash = Slash { + epoch: Epoch(6), + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }; + validator_slashes_handle(&alice) + .push(&mut storage, alice_slash) + .unwrap(); + + let res = fold_and_slash_redelegated_bonds( + &storage, + ¶ms, + &eager_redel_bonds, + start_epoch, + &[], + |_| true, + ); + assert_eq!( + res, + FoldRedelegatedBondsResult { + total_redelegated: token::Amount::from(5), + total_after_slashing: token::Amount::from(3), + } + ); +} + +/// `slashRedelegationTest` +#[test] +fn test_slash_redelegation() { + let mut storage = TestWlStorage::default(); + let params = OwnedPosParams { + unbonding_len: 4, + ..Default::default() + }; + let alice = established_address_1(); + + let total_redelegated_unbonded = + validator_total_redelegated_unbonded_handle(&alice); + total_redelegated_unbonded + .at(&Epoch(13)) + .at(&Epoch(10)) + .at(&alice) + .insert(&mut storage, Epoch(7), token::Amount::from(2)) + .unwrap(); + + let slashes = validator_slashes_handle(&alice); + + let mut slashed_amounts_map = BTreeMap::from_iter([ + (Epoch(15), token::Amount::zero()), + (Epoch(16), token::Amount::zero()), + ]); + let empty_slash_amounts = slashed_amounts_map.clone(); + + // Test case 1 + slash_redelegation( + &storage, + ¶ms, + token::Amount::from(7), + Epoch(7), + Epoch(10), + &alice, + Epoch(14), + &slashes, + &total_redelegated_unbonded, + Dec::one(), + &mut slashed_amounts_map, + ) + .unwrap(); + assert_eq!( + slashed_amounts_map, + BTreeMap::from_iter([ + (Epoch(15), token::Amount::from(5)), + (Epoch(16), token::Amount::from(5)), + ]) + ); + + // Test case 2 + slashed_amounts_map = empty_slash_amounts.clone(); + slash_redelegation( + &storage, + ¶ms, + token::Amount::from(7), + Epoch(7), + Epoch(11), + &alice, + Epoch(14), + &slashes, + &total_redelegated_unbonded, + Dec::one(), + &mut slashed_amounts_map, + ) + .unwrap(); + assert_eq!( + slashed_amounts_map, + BTreeMap::from_iter([ + (Epoch(15), token::Amount::from(7)), + (Epoch(16), token::Amount::from(7)), + ]) + ); + + // Test case 3 + slashed_amounts_map = BTreeMap::from_iter([ + (Epoch(15), token::Amount::from(2)), + (Epoch(16), token::Amount::from(3)), + ]); + slash_redelegation( + &storage, + ¶ms, + token::Amount::from(7), + Epoch(7), + Epoch(10), + &alice, + Epoch(14), + &slashes, + &total_redelegated_unbonded, + Dec::one(), + &mut slashed_amounts_map, + ) + .unwrap(); + assert_eq!( + slashed_amounts_map, + BTreeMap::from_iter([ + (Epoch(15), token::Amount::from(7)), + (Epoch(16), token::Amount::from(8)), + ]) + ); + + // Test case 4 + slashes + .push( + &mut storage, + Slash { + epoch: Epoch(8), + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }, + ) + .unwrap(); + slashed_amounts_map = empty_slash_amounts.clone(); + slash_redelegation( + &storage, + ¶ms, + token::Amount::from(7), + Epoch(7), + Epoch(10), + &alice, + Epoch(14), + &slashes, + &total_redelegated_unbonded, + Dec::one(), + &mut slashed_amounts_map, + ) + .unwrap(); + assert_eq!(slashed_amounts_map, empty_slash_amounts); + + // Test case 5 + slashes.pop(&mut storage).unwrap(); + slashes + .push( + &mut storage, + Slash { + epoch: Epoch(9), + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }, + ) + .unwrap(); + slash_redelegation( + &storage, + ¶ms, + token::Amount::from(7), + Epoch(7), + Epoch(10), + &alice, + Epoch(14), + &slashes, + &total_redelegated_unbonded, + Dec::one(), + &mut slashed_amounts_map, + ) + .unwrap(); + assert_eq!(slashed_amounts_map, empty_slash_amounts); + + // Test case 6 + slashes + .push( + &mut storage, + Slash { + epoch: Epoch(8), + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }, + ) + .unwrap(); + slash_redelegation( + &storage, + ¶ms, + token::Amount::from(7), + Epoch(7), + Epoch(10), + &alice, + Epoch(14), + &slashes, + &total_redelegated_unbonded, + Dec::one(), + &mut slashed_amounts_map, + ) + .unwrap(); + assert_eq!(slashed_amounts_map, empty_slash_amounts); +} + +/// `slashValidatorRedelegationTest` +#[test] +fn test_slash_validator_redelegation() { + let mut storage = TestWlStorage::default(); + let params = OwnedPosParams { + unbonding_len: 4, + ..Default::default() + }; + let gov_params = namada_core::ledger::governance::parameters::GovernanceParameters::default(); + gov_params.init_storage(&mut storage).unwrap(); + write_pos_params(&mut storage, ¶ms).unwrap(); + + let alice = established_address_1(); + let bob = established_address_2(); + + let total_redelegated_unbonded = + validator_total_redelegated_unbonded_handle(&alice); + total_redelegated_unbonded + .at(&Epoch(13)) + .at(&Epoch(10)) + .at(&alice) + .insert(&mut storage, Epoch(7), token::Amount::from(2)) + .unwrap(); + + let outgoing_redelegations = + validator_outgoing_redelegations_handle(&alice).at(&bob); + + let slashes = validator_slashes_handle(&alice); + + let mut slashed_amounts_map = BTreeMap::from_iter([ + (Epoch(15), token::Amount::zero()), + (Epoch(16), token::Amount::zero()), + ]); + let empty_slash_amounts = slashed_amounts_map.clone(); + + // Test case 1 + slash_validator_redelegation( + &storage, + ¶ms, + &alice, + Epoch(14), + &outgoing_redelegations, + &slashes, + &total_redelegated_unbonded, + Dec::one(), + &mut slashed_amounts_map, + ) + .unwrap(); + assert_eq!(slashed_amounts_map, empty_slash_amounts); + + // Test case 2 + total_redelegated_unbonded + .remove_all(&mut storage, &Epoch(13)) + .unwrap(); + slash_validator_redelegation( + &storage, + ¶ms, + &alice, + Epoch(14), + &outgoing_redelegations, + &slashes, + &total_redelegated_unbonded, + Dec::one(), + &mut slashed_amounts_map, + ) + .unwrap(); + assert_eq!(slashed_amounts_map, empty_slash_amounts); + + // Test case 3 + total_redelegated_unbonded + .at(&Epoch(13)) + .at(&Epoch(10)) + .at(&alice) + .insert(&mut storage, Epoch(7), token::Amount::from(2)) + .unwrap(); + outgoing_redelegations + .at(&Epoch(6)) + .insert(&mut storage, Epoch(8), token::Amount::from(7)) + .unwrap(); + slash_validator_redelegation( + &storage, + ¶ms, + &alice, + Epoch(14), + &outgoing_redelegations, + &slashes, + &total_redelegated_unbonded, + Dec::one(), + &mut slashed_amounts_map, + ) + .unwrap(); + assert_eq!( + slashed_amounts_map, + BTreeMap::from_iter([ + (Epoch(15), token::Amount::from(7)), + (Epoch(16), token::Amount::from(7)), + ]) + ); + + // Test case 4 + slashed_amounts_map = empty_slash_amounts.clone(); + outgoing_redelegations + .remove_all(&mut storage, &Epoch(6)) + .unwrap(); + outgoing_redelegations + .at(&Epoch(7)) + .insert(&mut storage, Epoch(8), token::Amount::from(7)) + .unwrap(); + slash_validator_redelegation( + &storage, + ¶ms, + &alice, + Epoch(14), + &outgoing_redelegations, + &slashes, + &total_redelegated_unbonded, + Dec::one(), + &mut slashed_amounts_map, + ) + .unwrap(); + assert_eq!( + slashed_amounts_map, + BTreeMap::from_iter([ + (Epoch(15), token::Amount::from(5)), + (Epoch(16), token::Amount::from(5)), + ]) + ); + + // Test case 5 + slashed_amounts_map = BTreeMap::from_iter([ + (Epoch(15), token::Amount::from(2)), + (Epoch(16), token::Amount::from(3)), + ]); + slash_validator_redelegation( + &storage, + ¶ms, + &alice, + Epoch(14), + &outgoing_redelegations, + &slashes, + &total_redelegated_unbonded, + Dec::one(), + &mut slashed_amounts_map, + ) + .unwrap(); + assert_eq!( + slashed_amounts_map, + BTreeMap::from_iter([ + (Epoch(15), token::Amount::from(7)), + (Epoch(16), token::Amount::from(8)), + ]) + ); + + // Test case 6 + slashed_amounts_map = empty_slash_amounts.clone(); + slashes + .push( + &mut storage, + Slash { + epoch: Epoch(8), + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }, + ) + .unwrap(); + slash_validator_redelegation( + &storage, + ¶ms, + &alice, + Epoch(14), + &outgoing_redelegations, + &slashes, + &total_redelegated_unbonded, + Dec::one(), + &mut slashed_amounts_map, + ) + .unwrap(); + assert_eq!(slashed_amounts_map, empty_slash_amounts); +} + +/// `slashValidatorTest` +#[test] +fn test_slash_validator() { + let mut storage = TestWlStorage::default(); + let params = OwnedPosParams { + unbonding_len: 4, + ..Default::default() + }; + let gov_params = namada_core::ledger::governance::parameters::GovernanceParameters::default(); + gov_params.init_storage(&mut storage).unwrap(); + write_pos_params(&mut storage, ¶ms).unwrap(); + + let alice = established_address_1(); + let bob = established_address_2(); + + let total_bonded = total_bonded_handle(&bob); + let total_unbonded = total_unbonded_handle(&bob); + let total_redelegated_bonded = + validator_total_redelegated_bonded_handle(&bob); + let total_redelegated_unbonded = + validator_total_redelegated_unbonded_handle(&bob); + + let infraction_stake = token::Amount::from(23); + + let initial_stakes = BTreeMap::from_iter([ + (Epoch(11), infraction_stake), + (Epoch(12), infraction_stake), + (Epoch(13), infraction_stake), + ]); + let mut exp_res = initial_stakes.clone(); + + let current_epoch = Epoch(10); + let infraction_epoch = + current_epoch - params.slash_processing_epoch_offset(); + let processing_epoch = current_epoch.next(); + let slash_rate = Dec::one(); + + // Test case 1 + total_bonded + .set(&mut storage, 23.into(), infraction_epoch - 2, 0) + .unwrap(); + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + assert_eq!(res, exp_res); + + // Test case 2 + total_bonded + .set(&mut storage, 17.into(), infraction_epoch - 2, 0) + .unwrap(); + total_unbonded + .at(&(current_epoch + params.pipeline_len)) + .insert(&mut storage, infraction_epoch - 2, 6.into()) + .unwrap(); + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + exp_res.insert(Epoch(12), 17.into()); + exp_res.insert(Epoch(13), 17.into()); + assert_eq!(res, exp_res); + + // Test case 3 + total_redelegated_bonded + .at(&infraction_epoch.prev()) + .at(&alice) + .insert(&mut storage, Epoch(2), 5.into()) + .unwrap(); + total_redelegated_bonded + .at(&infraction_epoch.prev()) + .at(&alice) + .insert(&mut storage, Epoch(3), 1.into()) + .unwrap(); + + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + assert_eq!(res, exp_res); + + // Test case 4 + total_unbonded_handle(&bob) + .at(&(current_epoch + params.pipeline_len)) + .remove(&mut storage, &(infraction_epoch - 2)) + .unwrap(); + total_unbonded_handle(&bob) + .at(&(current_epoch + params.pipeline_len)) + .insert(&mut storage, infraction_epoch - 1, 6.into()) + .unwrap(); + total_redelegated_unbonded + .at(&(current_epoch + params.pipeline_len)) + .at(&infraction_epoch.prev()) + .at(&alice) + .insert(&mut storage, Epoch(2), 5.into()) + .unwrap(); + total_redelegated_unbonded + .at(&(current_epoch + params.pipeline_len)) + .at(&infraction_epoch.prev()) + .at(&alice) + .insert(&mut storage, Epoch(3), 1.into()) + .unwrap(); + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + assert_eq!(res, exp_res); + + // Test case 5 + total_bonded_handle(&bob) + .set(&mut storage, 19.into(), infraction_epoch - 2, 0) + .unwrap(); + total_unbonded_handle(&bob) + .at(&(current_epoch + params.pipeline_len)) + .insert(&mut storage, infraction_epoch - 1, 4.into()) + .unwrap(); + total_redelegated_bonded + .at(¤t_epoch) + .at(&alice) + .insert(&mut storage, Epoch(2), token::Amount::from(1)) + .unwrap(); + total_redelegated_unbonded + .at(&(current_epoch + params.pipeline_len)) + .at(&infraction_epoch.prev()) + .at(&alice) + .remove(&mut storage, &Epoch(3)) + .unwrap(); + total_redelegated_unbonded + .at(&(current_epoch + params.pipeline_len)) + .at(&infraction_epoch.prev()) + .at(&alice) + .insert(&mut storage, Epoch(2), 4.into()) + .unwrap(); + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + exp_res.insert(Epoch(12), 19.into()); + exp_res.insert(Epoch(13), 19.into()); + assert_eq!(res, exp_res); + + // Test case 6 + total_unbonded_handle(&bob) + .remove_all(&mut storage, &(current_epoch + params.pipeline_len)) + .unwrap(); + total_redelegated_unbonded + .remove_all(&mut storage, &(current_epoch + params.pipeline_len)) + .unwrap(); + total_redelegated_bonded + .remove_all(&mut storage, ¤t_epoch) + .unwrap(); + total_bonded_handle(&bob) + .set(&mut storage, 23.into(), infraction_epoch - 2, 0) + .unwrap(); + total_bonded_handle(&bob) + .set(&mut storage, 6.into(), current_epoch, 0) + .unwrap(); + + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + exp_res = initial_stakes; + assert_eq!(res, exp_res); + + // Test case 7 + total_bonded + .get_data_handler() + .remove(&mut storage, ¤t_epoch) + .unwrap(); + total_unbonded + .at(¤t_epoch.next()) + .insert(&mut storage, current_epoch, 6.into()) + .unwrap(); + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + assert_eq!(res, exp_res); + + // Test case 8 + total_bonded + .get_data_handler() + .insert(&mut storage, current_epoch, 3.into()) + .unwrap(); + total_unbonded + .at(¤t_epoch.next()) + .insert(&mut storage, current_epoch, 3.into()) + .unwrap(); + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + assert_eq!(res, exp_res); + + // Test case 9 + total_unbonded + .remove_all(&mut storage, ¤t_epoch.next()) + .unwrap(); + total_bonded + .set(&mut storage, 6.into(), current_epoch, 0) + .unwrap(); + total_redelegated_bonded + .at(¤t_epoch) + .at(&alice) + .insert(&mut storage, 2.into(), 5.into()) + .unwrap(); + total_redelegated_bonded + .at(¤t_epoch) + .at(&alice) + .insert(&mut storage, 3.into(), 1.into()) + .unwrap(); + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + assert_eq!(res, exp_res); + + // Test case 10 + total_redelegated_bonded + .remove_all(&mut storage, ¤t_epoch) + .unwrap(); + total_bonded + .get_data_handler() + .remove(&mut storage, ¤t_epoch) + .unwrap(); + total_redelegated_unbonded + .at(¤t_epoch.next()) + .at(¤t_epoch) + .at(&alice) + .insert(&mut storage, 2.into(), 5.into()) + .unwrap(); + total_redelegated_unbonded + .at(¤t_epoch.next()) + .at(¤t_epoch) + .at(&alice) + .insert(&mut storage, 3.into(), 1.into()) + .unwrap(); + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + assert_eq!(res, exp_res); + + // Test case 11 + total_bonded + .set(&mut storage, 2.into(), current_epoch, 0) + .unwrap(); + total_redelegated_unbonded + .at(¤t_epoch.next()) + .at(¤t_epoch) + .at(&alice) + .insert(&mut storage, 2.into(), 4.into()) + .unwrap(); + total_redelegated_unbonded + .at(¤t_epoch.next()) + .at(¤t_epoch) + .at(&alice) + .remove(&mut storage, &3.into()) + .unwrap(); + total_redelegated_bonded + .at(¤t_epoch) + .at(&alice) + .insert(&mut storage, 2.into(), 1.into()) + .unwrap(); + total_redelegated_bonded + .at(¤t_epoch) + .at(&alice) + .insert(&mut storage, 3.into(), 1.into()) + .unwrap(); + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + assert_eq!(res, exp_res); + + // Test case 12 + total_bonded + .set(&mut storage, 6.into(), current_epoch, 0) + .unwrap(); + total_bonded + .set(&mut storage, 2.into(), current_epoch.next(), 0) + .unwrap(); + total_redelegated_bonded + .remove_all(&mut storage, ¤t_epoch) + .unwrap(); + total_redelegated_bonded + .at(¤t_epoch.next()) + .at(&alice) + .insert(&mut storage, 2.into(), 1.into()) + .unwrap(); + total_redelegated_bonded + .at(¤t_epoch.next()) + .at(&alice) + .insert(&mut storage, 3.into(), 1.into()) + .unwrap(); + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + assert_eq!(res, exp_res); + + // Test case 13 + validator_slashes_handle(&bob) + .push( + &mut storage, + Slash { + epoch: infraction_epoch.prev(), + block_height: 0, + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }, + ) + .unwrap(); + total_redelegated_unbonded + .remove_all(&mut storage, ¤t_epoch.next()) + .unwrap(); + total_bonded + .get_data_handler() + .remove(&mut storage, ¤t_epoch.next()) + .unwrap(); + total_redelegated_bonded + .remove_all(&mut storage, ¤t_epoch.next()) + .unwrap(); + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + exp_res.insert(Epoch(11), 0.into()); + exp_res.insert(Epoch(12), 0.into()); + exp_res.insert(Epoch(13), 0.into()); + assert_eq!(res, exp_res); +} + +/// `computeAmountAfterSlashingUnbondTest` +#[test] +fn test_compute_amount_after_slashing_unbond() { + let mut storage = TestWlStorage::default(); + let params = OwnedPosParams { + unbonding_len: 4, + ..Default::default() + }; + + // Test data + let alice = established_address_1(); + let bob = established_address_2(); + let unbonds: BTreeMap = BTreeMap::from_iter([ + ((Epoch(2)), token::Amount::from(5)), + ((Epoch(4)), token::Amount::from(6)), + ]); + let redelegated_unbonds: EagerRedelegatedUnbonds = BTreeMap::from_iter([( + Epoch(2), + BTreeMap::from_iter([( + alice.clone(), + BTreeMap::from_iter([(Epoch(1), token::Amount::from(1))]), + )]), + )]); + + // Test case 1 + let slashes = vec![]; + let result = compute_amount_after_slashing_unbond( + &storage, + ¶ms, + &unbonds, + &redelegated_unbonds, + slashes, + ) + .unwrap(); + assert_eq!(result.sum, 11.into()); + itertools::assert_equal( + result.epoch_map, + [(2.into(), 5.into()), (4.into(), 6.into())], + ); + + // Test case 2 + let bob_slash = Slash { + epoch: Epoch(5), + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }; + let slashes = vec![bob_slash.clone()]; + validator_slashes_handle(&bob) + .push(&mut storage, bob_slash) + .unwrap(); + let result = compute_amount_after_slashing_unbond( + &storage, + ¶ms, + &unbonds, + &redelegated_unbonds, + slashes, + ) + .unwrap(); + assert_eq!(result.sum, 0.into()); + itertools::assert_equal( + result.epoch_map, + [(2.into(), 0.into()), (4.into(), 0.into())], + ); + + // Test case 3 + let alice_slash = Slash { + epoch: Epoch(0), + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }; + let slashes = vec![alice_slash.clone()]; + validator_slashes_handle(&alice) + .push(&mut storage, alice_slash) + .unwrap(); + validator_slashes_handle(&bob).pop(&mut storage).unwrap(); + let result = compute_amount_after_slashing_unbond( + &storage, + ¶ms, + &unbonds, + &redelegated_unbonds, + slashes, + ) + .unwrap(); + assert_eq!(result.sum, 11.into()); + itertools::assert_equal( + result.epoch_map, + [(2.into(), 5.into()), (4.into(), 6.into())], + ); + + // Test case 4 + let alice_slash = Slash { + epoch: Epoch(1), + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }; + let slashes = vec![alice_slash.clone()]; + validator_slashes_handle(&alice).pop(&mut storage).unwrap(); + validator_slashes_handle(&alice) + .push(&mut storage, alice_slash) + .unwrap(); + let result = compute_amount_after_slashing_unbond( + &storage, + ¶ms, + &unbonds, + &redelegated_unbonds, + slashes, + ) + .unwrap(); + assert_eq!(result.sum, 10.into()); + itertools::assert_equal( + result.epoch_map, + [(2.into(), 4.into()), (4.into(), 6.into())], + ); +} + +/// `computeAmountAfterSlashingWithdrawTest` +#[test] +fn test_compute_amount_after_slashing_withdraw() { + let mut storage = TestWlStorage::default(); + let params = OwnedPosParams { + unbonding_len: 4, + ..Default::default() + }; + + // Test data + let alice = established_address_1(); + let bob = established_address_2(); + let unbonds_and_redelegated_unbonds: BTreeMap< + (Epoch, Epoch), + (token::Amount, EagerRedelegatedBondsMap), + > = BTreeMap::from_iter([ + ( + (Epoch(2), Epoch(20)), + ( + // unbond + token::Amount::from(5), + // redelegations + BTreeMap::from_iter([( + alice.clone(), + BTreeMap::from_iter([(Epoch(1), token::Amount::from(1))]), + )]), + ), + ), + ( + (Epoch(4), Epoch(20)), + ( + // unbond + token::Amount::from(6), + // redelegations + BTreeMap::default(), + ), + ), + ]); + + // Test case 1 + let slashes = vec![]; + let result = compute_amount_after_slashing_withdraw( + &storage, + ¶ms, + &unbonds_and_redelegated_unbonds, + slashes, + ) + .unwrap(); + assert_eq!(result.sum, 11.into()); + itertools::assert_equal( + result.epoch_map, + [(2.into(), 5.into()), (4.into(), 6.into())], + ); + + // Test case 2 + let bob_slash = Slash { + epoch: Epoch(5), + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }; + let slashes = vec![bob_slash.clone()]; + validator_slashes_handle(&bob) + .push(&mut storage, bob_slash) + .unwrap(); + let result = compute_amount_after_slashing_withdraw( + &storage, + ¶ms, + &unbonds_and_redelegated_unbonds, + slashes, + ) + .unwrap(); + assert_eq!(result.sum, 0.into()); + itertools::assert_equal( + result.epoch_map, + [(2.into(), 0.into()), (4.into(), 0.into())], + ); + + // Test case 3 + let alice_slash = Slash { + epoch: Epoch(0), + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }; + let slashes = vec![alice_slash.clone()]; + validator_slashes_handle(&alice) + .push(&mut storage, alice_slash) + .unwrap(); + validator_slashes_handle(&bob).pop(&mut storage).unwrap(); + let result = compute_amount_after_slashing_withdraw( + &storage, + ¶ms, + &unbonds_and_redelegated_unbonds, + slashes, + ) + .unwrap(); + assert_eq!(result.sum, 11.into()); + itertools::assert_equal( + result.epoch_map, + [(2.into(), 5.into()), (4.into(), 6.into())], + ); + + // Test case 4 + let alice_slash = Slash { + epoch: Epoch(1), + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }; + let slashes = vec![alice_slash.clone()]; + validator_slashes_handle(&alice).pop(&mut storage).unwrap(); + validator_slashes_handle(&alice) + .push(&mut storage, alice_slash) + .unwrap(); + let result = compute_amount_after_slashing_withdraw( + &storage, + ¶ms, + &unbonds_and_redelegated_unbonds, + slashes, + ) + .unwrap(); + assert_eq!(result.sum, 10.into()); + itertools::assert_equal( + result.epoch_map, + [(2.into(), 4.into()), (4.into(), 6.into())], + ); +} + +/// SM test case 1 from Brent +#[test] +fn test_from_sm_case_1() { + use namada_core::types::address::testing::established_address_4; + + let mut storage = TestWlStorage::default(); + let gov_params = namada_core::ledger::governance::parameters::GovernanceParameters::default(); + gov_params.init_storage(&mut storage).unwrap(); + write_pos_params(&mut storage, &OwnedPosParams::default()).unwrap(); + + let validator = established_address_1(); + let redeleg_src_1 = established_address_2(); + let redeleg_src_2 = established_address_3(); + let owner = established_address_4(); + let unbond_amount = token::Amount::from(3130688); + println!( + "Owner: {owner}\nValidator: {validator}\nRedeleg src 1: \ + {redeleg_src_1}\nRedeleg src 2: {redeleg_src_2}" + ); + + // Validator's incoming redelegations + let outer_epoch_1 = Epoch(27); + // from redeleg_src_1 + let epoch_1_redeleg_1 = token::Amount::from(8516); + // from redeleg_src_2 + let epoch_1_redeleg_2 = token::Amount::from(5704386); + let outer_epoch_2 = Epoch(30); + // from redeleg_src_2 + let epoch_2_redeleg_2 = token::Amount::from(1035191); + + // Insert the data - bonds and redelegated bonds + let bonds_handle = bond_handle(&owner, &validator); + bonds_handle + .add( + &mut storage, + epoch_1_redeleg_1 + epoch_1_redeleg_2, + outer_epoch_1, + 0, + ) + .unwrap(); + bonds_handle + .add(&mut storage, epoch_2_redeleg_2, outer_epoch_2, 0) + .unwrap(); + + let redelegated_bonds_map_1 = delegator_redelegated_bonds_handle(&owner) + .at(&validator) + .at(&outer_epoch_1); + redelegated_bonds_map_1 + .at(&redeleg_src_1) + .insert(&mut storage, Epoch(14), epoch_1_redeleg_1) + .unwrap(); + redelegated_bonds_map_1 + .at(&redeleg_src_2) + .insert(&mut storage, Epoch(18), epoch_1_redeleg_2) + .unwrap(); + let redelegated_bonds_map_1 = delegator_redelegated_bonds_handle(&owner) + .at(&validator) + .at(&outer_epoch_1); + + let redelegated_bonds_map_2 = delegator_redelegated_bonds_handle(&owner) + .at(&validator) + .at(&outer_epoch_2); + redelegated_bonds_map_2 + .at(&redeleg_src_2) + .insert(&mut storage, Epoch(18), epoch_2_redeleg_2) + .unwrap(); + + // Find the modified redelegation the same way as `unbond_tokens` + let bonds_to_unbond = find_bonds_to_remove( + &storage, + &bonds_handle.get_data_handler(), + unbond_amount, + ) + .unwrap(); + dbg!(&bonds_to_unbond); + + let (new_entry_epoch, new_bond_amount) = bonds_to_unbond.new_entry.unwrap(); + assert_eq!(outer_epoch_1, new_entry_epoch); + // The modified bond should be sum of all redelegations less the unbonded + // amouunt + assert_eq!( + epoch_1_redeleg_1 + epoch_1_redeleg_2 + epoch_2_redeleg_2 + - unbond_amount, + new_bond_amount + ); + // The current bond should be sum of redelegations fom the modified epoch + let cur_bond_amount = bonds_handle + .get_delta_val(&storage, new_entry_epoch) + .unwrap() + .unwrap_or_default(); + assert_eq!(epoch_1_redeleg_1 + epoch_1_redeleg_2, cur_bond_amount); + + let mr = compute_modified_redelegation( + &storage, + &redelegated_bonds_map_1, + new_entry_epoch, + cur_bond_amount - new_bond_amount, + ) + .unwrap(); + + let exp_mr = ModifiedRedelegation { + epoch: Some(Epoch(27)), + validators_to_remove: BTreeSet::from_iter([redeleg_src_2.clone()]), + validator_to_modify: Some(redeleg_src_2), + epochs_to_remove: BTreeSet::from_iter([Epoch(18)]), + epoch_to_modify: Some(Epoch(18)), + new_amount: Some(token::Amount::from(3608889)), + }; + + pretty_assertions::assert_eq!(mr, exp_mr); +} diff --git a/proof_of_stake/src/tests/test_pos.rs b/proof_of_stake/src/tests/test_pos.rs new file mode 100644 index 0000000000..a37268a1a6 --- /dev/null +++ b/proof_of_stake/src/tests/test_pos.rs @@ -0,0 +1,1653 @@ +//! PoS system tests + +use std::collections::{BTreeMap, HashSet}; + +use namada_core::ledger::storage::testing::TestWlStorage; +use namada_core::ledger::storage_api::collections::lazy_map::Collectable; +use namada_core::ledger::storage_api::token::{credit_tokens, read_balance}; +use namada_core::ledger::storage_api::StorageRead; +use namada_core::types::address::Address; +use namada_core::types::dec::Dec; +use namada_core::types::key::testing::{ + common_sk_from_simple_seed, gen_keypair, +}; +use namada_core::types::key::RefTo; +use namada_core::types::storage::{BlockHeight, Epoch}; +use namada_core::types::{address, key, token}; +use proptest::prelude::*; +use proptest::test_runner::Config; +// Use `RUST_LOG=info` (or another tracing level) and `--nocapture` to see +// `tracing` logs from tests +use test_log::test; + +use crate::parameters::testing::arb_pos_params; +use crate::parameters::OwnedPosParams; +use crate::queries::bonds_and_unbonds; +use crate::rewards::{ + log_block_rewards, update_rewards_products_and_mint_inflation, + PosRewardsCalculator, +}; +use crate::slashing::{process_slashes, slash}; +use crate::storage::{ + get_consensus_key_set, read_below_threshold_validator_set_addresses, + read_consensus_validator_set_addresses_with_stake, read_total_stake, + read_validator_deltas_value, rewards_accumulator_handle, + total_deltas_handle, +}; +use crate::test_utils::test_init_genesis; +use crate::tests::helpers::{ + advance_epoch, arb_genesis_validators, arb_params_and_genesis_validators, +}; +use crate::types::{ + into_tm_voting_power, BondDetails, BondId, BondsAndUnbondsDetails, + GenesisValidator, SlashType, UnbondDetails, ValidatorState, VoteInfo, + WeightedValidator, +}; +use crate::{ + below_capacity_validator_set_handle, bond_handle, bond_tokens, + change_consensus_key, consensus_validator_set_handle, is_delegator, + is_validator, read_validator_stake, redelegate_tokens, + staking_token_address, unbond_handle, unbond_tokens, unjail_validator, + validator_consensus_key_handle, validator_set_positions_handle, + validator_state_handle, withdraw_tokens, +}; + +proptest! { + // Generate arb valid input for `test_test_init_genesis_aux` + #![proptest_config(Config { + cases: 100, + .. Config::default() + })] + #[test] + fn test_test_init_genesis( + + (pos_params, genesis_validators) in arb_params_and_genesis_validators(Some(5), 1..10), + start_epoch in (0_u64..1000).prop_map(Epoch), + + ) { + test_test_init_genesis_aux(pos_params, start_epoch, genesis_validators) + } +} + +proptest! { + // Generate arb valid input for `test_bonds_aux` + #![proptest_config(Config { + cases: 100, + .. Config::default() + })] + #[test] + fn test_bonds( + + (pos_params, genesis_validators) in arb_params_and_genesis_validators(Some(5), 1..3), + + ) { + test_bonds_aux(pos_params, genesis_validators) + } +} + +proptest! { + // Generate arb valid input for `test_unjail_validator_aux` + #![proptest_config(Config { + cases: 100, + .. Config::default() + })] + #[test] + fn test_unjail_validator( + (pos_params, genesis_validators) + in arb_params_and_genesis_validators(Some(4),6..9) + ) { + test_unjail_validator_aux(pos_params, + genesis_validators) + } +} + +proptest! { + // Generate arb valid input for `test_unslashed_bond_amount_aux` + #![proptest_config(Config { + cases: 1, + .. Config::default() + })] + #[test] + fn test_unslashed_bond_amount( + + genesis_validators in arb_genesis_validators(4..5, None), + + ) { + test_unslashed_bond_amount_aux(genesis_validators) + } +} + +proptest! { + // Generate arb valid input for `test_log_block_rewards_aux` + #![proptest_config(Config { + cases: 1, + .. Config::default() + })] + #[test] + fn test_log_block_rewards( + genesis_validators in arb_genesis_validators(4..10, None), + params in arb_pos_params(Some(5)) + + ) { + test_log_block_rewards_aux(genesis_validators, params) + } +} + +proptest! { + // Generate arb valid input for `test_update_rewards_products_aux` + #![proptest_config(Config { + cases: 1, + .. Config::default() + })] + #[test] + fn test_update_rewards_products( + genesis_validators in arb_genesis_validators(4..10, None), + + ) { + test_update_rewards_products_aux(genesis_validators) + } +} + +proptest! { + // Generate arb valid input for `test_consensus_key_change` + #![proptest_config(Config { + cases: 1, + .. Config::default() + })] + #[test] + fn test_consensus_key_change( + + genesis_validators in arb_genesis_validators(1..2, None), + + ) { + test_consensus_key_change_aux(genesis_validators) + } +} + +proptest! { + // Generate arb valid input for `test_is_delegator` + #![proptest_config(Config { + cases: 100, + .. Config::default() + })] + #[test] + fn test_is_delegator( + + genesis_validators in arb_genesis_validators(2..3, None), + + ) { + test_is_delegator_aux(genesis_validators) + } +} + +/// Test genesis initialization +fn test_test_init_genesis_aux( + params: OwnedPosParams, + start_epoch: Epoch, + mut validators: Vec, +) { + // println!( + // "Test inputs: {params:?}, {start_epoch}, genesis validators: \ + // {validators:#?}" + // ); + let mut s = TestWlStorage::default(); + s.storage.block.epoch = start_epoch; + + validators.sort_by(|a, b| b.tokens.cmp(&a.tokens)); + let params = test_init_genesis( + &mut s, + params, + validators.clone().into_iter(), + start_epoch, + ) + .unwrap(); + + let mut bond_details = bonds_and_unbonds(&s, None, None).unwrap(); + assert!(bond_details.iter().all(|(_id, details)| { + details.unbonds.is_empty() && details.slashes.is_empty() + })); + + for (i, validator) in validators.into_iter().enumerate() { + let addr = &validator.address; + let self_bonds = bond_details + .remove(&BondId { + source: addr.clone(), + validator: addr.clone(), + }) + .unwrap(); + assert_eq!(self_bonds.bonds.len(), 1); + assert_eq!( + self_bonds.bonds[0], + BondDetails { + start: start_epoch, + amount: validator.tokens, + slashed_amount: None, + } + ); + + let state = validator_state_handle(&validator.address) + .get(&s, start_epoch, ¶ms) + .unwrap(); + if (i as u64) < params.max_validator_slots + && validator.tokens >= params.validator_stake_threshold + { + // should be in consensus set + let handle = consensus_validator_set_handle().at(&start_epoch); + assert!(handle.at(&validator.tokens).iter(&s).unwrap().any( + |result| { + let (_pos, addr) = result.unwrap(); + addr == validator.address + } + )); + assert_eq!(state, Some(ValidatorState::Consensus)); + } else if validator.tokens >= params.validator_stake_threshold { + // Should be in below-capacity set if its tokens are greater than + // `validator_stake_threshold` + let handle = below_capacity_validator_set_handle().at(&start_epoch); + assert!(handle.at(&validator.tokens.into()).iter(&s).unwrap().any( + |result| { + let (_pos, addr) = result.unwrap(); + addr == validator.address + } + )); + assert_eq!(state, Some(ValidatorState::BelowCapacity)); + } else { + // Should be in below-threshold + let bt_addresses = + read_below_threshold_validator_set_addresses(&s, start_epoch) + .unwrap(); + assert!( + bt_addresses + .into_iter() + .any(|addr| { addr == validator.address }) + ); + assert_eq!(state, Some(ValidatorState::BelowThreshold)); + } + } +} + +/// Test bonding +/// NOTE: copy validator sets each time we advance the epoch +fn test_bonds_aux(params: OwnedPosParams, validators: Vec) { + // This can be useful for debugging: + // params.pipeline_len = 2; + // params.unbonding_len = 4; + // println!("\nTest inputs: {params:?}, genesis validators: + // {validators:#?}"); + let mut s = TestWlStorage::default(); + + // Genesis + let start_epoch = s.storage.block.epoch; + let mut current_epoch = s.storage.block.epoch; + let params = test_init_genesis( + &mut s, + params, + validators.clone().into_iter(), + current_epoch, + ) + .unwrap(); + s.commit_block().unwrap(); + + // Advance to epoch 1 + current_epoch = advance_epoch(&mut s, ¶ms); + let self_bond_epoch = current_epoch; + + let validator = validators.first().unwrap(); + + // Read some data before submitting bond + let pipeline_epoch = current_epoch + params.pipeline_len; + let staking_token = staking_token_address(&s); + let pos_balance_pre = s + .read::(&token::balance_key( + &staking_token, + &crate::ADDRESS, + )) + .unwrap() + .unwrap_or_default(); + let total_stake_before = + read_total_stake(&s, ¶ms, pipeline_epoch).unwrap(); + + // Self-bond + let amount_self_bond = token::Amount::from_uint(100_500_000, 0).unwrap(); + credit_tokens(&mut s, &staking_token, &validator.address, amount_self_bond) + .unwrap(); + bond_tokens( + &mut s, + None, + &validator.address, + amount_self_bond, + current_epoch, + None, + ) + .unwrap(); + + // Check the bond delta + let self_bond = bond_handle(&validator.address, &validator.address); + let delta = self_bond.get_delta_val(&s, pipeline_epoch).unwrap(); + assert_eq!(delta, Some(amount_self_bond)); + + // Check the validator in the validator set + let set = + read_consensus_validator_set_addresses_with_stake(&s, pipeline_epoch) + .unwrap(); + assert!(set.into_iter().any( + |WeightedValidator { + bonded_stake, + address, + }| { + address == validator.address + && bonded_stake == validator.tokens + amount_self_bond + } + )); + + let val_deltas = + read_validator_deltas_value(&s, &validator.address, &pipeline_epoch) + .unwrap(); + assert_eq!(val_deltas, Some(amount_self_bond.change())); + + let total_deltas_handle = total_deltas_handle(); + assert_eq!( + current_epoch, + total_deltas_handle.get_last_update(&s).unwrap().unwrap() + ); + let total_stake_after = + read_total_stake(&s, ¶ms, pipeline_epoch).unwrap(); + assert_eq!(total_stake_before + amount_self_bond, total_stake_after); + + // Check bond details after self-bond + let self_bond_id = BondId { + source: validator.address.clone(), + validator: validator.address.clone(), + }; + let check_bond_details = |ix, bond_details: BondsAndUnbondsDetails| { + println!("Check index {ix}"); + let details = bond_details.get(&self_bond_id).unwrap(); + assert_eq!( + details.bonds.len(), + 2, + "Contains genesis and newly added self-bond" + ); + // dbg!(&details.bonds); + assert_eq!( + details.bonds[0], + BondDetails { + start: start_epoch, + amount: validator.tokens, + slashed_amount: None + }, + ); + assert_eq!( + details.bonds[1], + BondDetails { + start: pipeline_epoch, + amount: amount_self_bond, + slashed_amount: None + }, + ); + }; + // Try to call it with different combinations of owner/validator args + check_bond_details(0, bonds_and_unbonds(&s, None, None).unwrap()); + check_bond_details( + 1, + bonds_and_unbonds(&s, Some(validator.address.clone()), None).unwrap(), + ); + check_bond_details( + 2, + bonds_and_unbonds(&s, None, Some(validator.address.clone())).unwrap(), + ); + check_bond_details( + 3, + bonds_and_unbonds( + &s, + Some(validator.address.clone()), + Some(validator.address.clone()), + ) + .unwrap(), + ); + + // Get a non-validating account with tokens + let delegator = address::testing::gen_implicit_address(); + let amount_del = token::Amount::from_uint(201_000_000, 0).unwrap(); + credit_tokens(&mut s, &staking_token, &delegator, amount_del).unwrap(); + let balance_key = token::balance_key(&staking_token, &delegator); + let balance = s + .read::(&balance_key) + .unwrap() + .unwrap_or_default(); + assert_eq!(balance, amount_del); + + // Advance to epoch 3 + advance_epoch(&mut s, ¶ms); + current_epoch = advance_epoch(&mut s, ¶ms); + let pipeline_epoch = current_epoch + params.pipeline_len; + + // Delegation + let delegation_epoch = current_epoch; + bond_tokens( + &mut s, + Some(&delegator), + &validator.address, + amount_del, + current_epoch, + None, + ) + .unwrap(); + let val_stake_pre = read_validator_stake( + &s, + ¶ms, + &validator.address, + pipeline_epoch.prev(), + ) + .unwrap(); + let val_stake_post = + read_validator_stake(&s, ¶ms, &validator.address, pipeline_epoch) + .unwrap(); + assert_eq!(validator.tokens + amount_self_bond, val_stake_pre); + assert_eq!( + validator.tokens + amount_self_bond + amount_del, + val_stake_post + ); + let delegation = bond_handle(&delegator, &validator.address); + assert_eq!( + delegation + .get_sum(&s, pipeline_epoch.prev(), ¶ms) + .unwrap() + .unwrap_or_default(), + token::Amount::zero() + ); + assert_eq!( + delegation + .get_sum(&s, pipeline_epoch, ¶ms) + .unwrap() + .unwrap_or_default(), + amount_del + ); + + // Check delegation bonds details after delegation + let delegation_bond_id = BondId { + source: delegator.clone(), + validator: validator.address.clone(), + }; + let check_bond_details = |ix, bond_details: BondsAndUnbondsDetails| { + println!("Check index {ix}"); + assert_eq!(bond_details.len(), 1); + let details = bond_details.get(&delegation_bond_id).unwrap(); + assert_eq!(details.bonds.len(), 1,); + // dbg!(&details.bonds); + assert_eq!( + details.bonds[0], + BondDetails { + start: pipeline_epoch, + amount: amount_del, + slashed_amount: None + }, + ); + }; + // Try to call it with different combinations of owner/validator args + check_bond_details( + 0, + bonds_and_unbonds(&s, Some(delegator.clone()), None).unwrap(), + ); + check_bond_details( + 1, + bonds_and_unbonds( + &s, + Some(delegator.clone()), + Some(validator.address.clone()), + ) + .unwrap(), + ); + + // Check all bond details (self-bonds and delegation) + let check_bond_details = |ix, bond_details: BondsAndUnbondsDetails| { + println!("Check index {ix}"); + let self_bond_details = bond_details.get(&self_bond_id).unwrap(); + let delegation_details = bond_details.get(&delegation_bond_id).unwrap(); + assert_eq!( + self_bond_details.bonds.len(), + 2, + "Contains genesis and newly added self-bond" + ); + assert_eq!( + self_bond_details.bonds[0], + BondDetails { + start: start_epoch, + amount: validator.tokens, + slashed_amount: None + }, + ); + assert_eq!(self_bond_details.bonds[1].amount, amount_self_bond); + assert_eq!( + delegation_details.bonds[0], + BondDetails { + start: pipeline_epoch, + amount: amount_del, + slashed_amount: None + }, + ); + }; + // Try to call it with different combinations of owner/validator args + check_bond_details(0, bonds_and_unbonds(&s, None, None).unwrap()); + check_bond_details( + 1, + bonds_and_unbonds(&s, None, Some(validator.address.clone())).unwrap(), + ); + + // Advance to epoch 5 + for _ in 0..2 { + current_epoch = advance_epoch(&mut s, ¶ms); + } + let pipeline_epoch = current_epoch + params.pipeline_len; + + // Unbond the self-bond with an amount that will remove all of the self-bond + // executed after genesis and some of the genesis bond + let amount_self_unbond: token::Amount = + amount_self_bond + (validator.tokens / 2); + // When the difference is 0, only the non-genesis self-bond is unbonded + let unbonded_genesis_self_bond = + amount_self_unbond - amount_self_bond != token::Amount::zero(); + + let self_unbond_epoch = s.storage.block.epoch; + + unbond_tokens( + &mut s, + None, + &validator.address, + amount_self_unbond, + current_epoch, + false, + ) + .unwrap(); + + let val_stake_pre = read_validator_stake( + &s, + ¶ms, + &validator.address, + pipeline_epoch.prev(), + ) + .unwrap(); + + let val_stake_post = + read_validator_stake(&s, ¶ms, &validator.address, pipeline_epoch) + .unwrap(); + + let val_delta = + read_validator_deltas_value(&s, &validator.address, &pipeline_epoch) + .unwrap(); + let unbond = unbond_handle(&validator.address, &validator.address); + + assert_eq!(val_delta, Some(-amount_self_unbond.change())); + assert_eq!( + unbond + .at(&Epoch::default()) + .get( + &s, + &(pipeline_epoch + + params.unbonding_len + + params.cubic_slashing_window_length) + ) + .unwrap(), + if unbonded_genesis_self_bond { + Some(amount_self_unbond - amount_self_bond) + } else { + None + } + ); + assert_eq!( + unbond + .at(&(self_bond_epoch + params.pipeline_len)) + .get( + &s, + &(pipeline_epoch + + params.unbonding_len + + params.cubic_slashing_window_length) + ) + .unwrap(), + Some(amount_self_bond) + ); + assert_eq!( + val_stake_pre, + validator.tokens + amount_self_bond + amount_del + ); + assert_eq!( + val_stake_post, + validator.tokens + amount_self_bond + amount_del - amount_self_unbond + ); + + // Check all bond and unbond details (self-bonds and delegation) + let check_bond_details = |ix, bond_details: BondsAndUnbondsDetails| { + println!("Check index {ix}"); + // dbg!(&bond_details); + assert_eq!(bond_details.len(), 2); + let self_bond_details = bond_details.get(&self_bond_id).unwrap(); + let delegation_details = bond_details.get(&delegation_bond_id).unwrap(); + assert_eq!( + self_bond_details.bonds.len(), + 1, + "Contains only part of the genesis bond now" + ); + assert_eq!( + self_bond_details.bonds[0], + BondDetails { + start: start_epoch, + amount: validator.tokens + amount_self_bond + - amount_self_unbond, + slashed_amount: None + }, + ); + assert_eq!( + delegation_details.bonds[0], + BondDetails { + start: delegation_epoch + params.pipeline_len, + amount: amount_del, + slashed_amount: None + }, + ); + assert_eq!( + self_bond_details.unbonds.len(), + if unbonded_genesis_self_bond { 2 } else { 1 }, + "Contains a full unbond of the last self-bond and an unbond from \ + the genesis bond" + ); + if unbonded_genesis_self_bond { + assert_eq!( + self_bond_details.unbonds[0], + UnbondDetails { + start: start_epoch, + withdraw: self_unbond_epoch + + params.pipeline_len + + params.unbonding_len + + params.cubic_slashing_window_length, + amount: amount_self_unbond - amount_self_bond, + slashed_amount: None + } + ); + } + assert_eq!( + self_bond_details.unbonds[usize::from(unbonded_genesis_self_bond)], + UnbondDetails { + start: self_bond_epoch + params.pipeline_len, + withdraw: self_unbond_epoch + + params.pipeline_len + + params.unbonding_len + + params.cubic_slashing_window_length, + amount: amount_self_bond, + slashed_amount: None + } + ); + }; + check_bond_details( + 0, + bonds_and_unbonds(&s, None, Some(validator.address.clone())).unwrap(), + ); + + // Unbond delegation + let amount_undel = token::Amount::from_uint(1_000_000, 0).unwrap(); + unbond_tokens( + &mut s, + Some(&delegator), + &validator.address, + amount_undel, + current_epoch, + false, + ) + .unwrap(); + + let val_stake_pre = read_validator_stake( + &s, + ¶ms, + &validator.address, + pipeline_epoch.prev(), + ) + .unwrap(); + let val_stake_post = + read_validator_stake(&s, ¶ms, &validator.address, pipeline_epoch) + .unwrap(); + let val_delta = + read_validator_deltas_value(&s, &validator.address, &pipeline_epoch) + .unwrap(); + let unbond = unbond_handle(&delegator, &validator.address); + + assert_eq!( + val_delta, + Some(-(amount_self_unbond + amount_undel).change()) + ); + assert_eq!( + unbond + .at(&(delegation_epoch + params.pipeline_len)) + .get( + &s, + &(pipeline_epoch + + params.unbonding_len + + params.cubic_slashing_window_length) + ) + .unwrap(), + Some(amount_undel) + ); + assert_eq!( + val_stake_pre, + validator.tokens + amount_self_bond + amount_del + ); + assert_eq!( + val_stake_post, + validator.tokens + amount_self_bond - amount_self_unbond + amount_del + - amount_undel + ); + + let withdrawable_offset = params.unbonding_len + + params.pipeline_len + + params.cubic_slashing_window_length; + + // Advance to withdrawable epoch + for _ in 0..withdrawable_offset { + current_epoch = advance_epoch(&mut s, ¶ms); + } + + let pos_balance = s + .read::(&token::balance_key( + &staking_token, + &crate::ADDRESS, + )) + .unwrap(); + + assert_eq!( + Some(pos_balance_pre + amount_self_bond + amount_del), + pos_balance + ); + + // Withdraw the self-unbond + withdraw_tokens(&mut s, None, &validator.address, current_epoch).unwrap(); + let unbond = unbond_handle(&validator.address, &validator.address); + let unbond_iter = unbond.iter(&s).unwrap().next(); + assert!(unbond_iter.is_none()); + + let pos_balance = s + .read::(&token::balance_key( + &staking_token, + &crate::ADDRESS, + )) + .unwrap(); + assert_eq!( + Some( + pos_balance_pre + amount_self_bond - amount_self_unbond + + amount_del + ), + pos_balance + ); + + // Withdraw the delegation unbond + withdraw_tokens( + &mut s, + Some(&delegator), + &validator.address, + current_epoch, + ) + .unwrap(); + let unbond = unbond_handle(&delegator, &validator.address); + let unbond_iter = unbond.iter(&s).unwrap().next(); + assert!(unbond_iter.is_none()); + + let pos_balance = s + .read::(&token::balance_key( + &staking_token, + &crate::ADDRESS, + )) + .unwrap(); + assert_eq!( + Some( + pos_balance_pre + amount_self_bond - amount_self_unbond + + amount_del + - amount_undel + ), + pos_balance + ); +} + +fn test_unjail_validator_aux( + params: OwnedPosParams, + mut validators: Vec, +) { + // println!("\nTest inputs: {params:?}, genesis validators: + // {validators:#?}"); + let mut s = TestWlStorage::default(); + + // Find the validator with the most stake and 100x his stake to keep the + // cubic slash rate small + let num_vals = validators.len(); + validators.sort_by_key(|a| a.tokens); + validators[num_vals - 1].tokens = 100 * validators[num_vals - 1].tokens; + + // Get second highest stake validator to misbehave + let val_addr = &validators[num_vals - 2].address; + // let val_tokens = validators[num_vals - 2].tokens; + + // Genesis + let mut current_epoch = s.storage.block.epoch; + let params = test_init_genesis( + &mut s, + params, + validators.clone().into_iter(), + current_epoch, + ) + .unwrap(); + s.commit_block().unwrap(); + + current_epoch = advance_epoch(&mut s, ¶ms); + process_slashes(&mut s, current_epoch).unwrap(); + + // Discover first slash + let slash_0_evidence_epoch = current_epoch; + let evidence_block_height = BlockHeight(0); // doesn't matter for slashing logic + let slash_0_type = SlashType::DuplicateVote; + slash( + &mut s, + ¶ms, + current_epoch, + slash_0_evidence_epoch, + evidence_block_height, + slash_0_type, + val_addr, + current_epoch.next(), + ) + .unwrap(); + + assert_eq!( + validator_state_handle(val_addr) + .get(&s, current_epoch, ¶ms) + .unwrap(), + Some(ValidatorState::Consensus) + ); + + for epoch in Epoch::iter_bounds_inclusive( + current_epoch.next(), + current_epoch + params.pipeline_len, + ) { + // Check the validator state + assert_eq!( + validator_state_handle(val_addr) + .get(&s, epoch, ¶ms) + .unwrap(), + Some(ValidatorState::Jailed) + ); + // Check the validator set positions + assert!( + validator_set_positions_handle() + .at(&epoch) + .get(&s, val_addr) + .unwrap() + .is_none(), + ); + } + + // Advance past an epoch in which we can unbond + let unfreeze_epoch = + slash_0_evidence_epoch + params.slash_processing_epoch_offset(); + while current_epoch < unfreeze_epoch + 4u64 { + current_epoch = advance_epoch(&mut s, ¶ms); + process_slashes(&mut s, current_epoch).unwrap(); + } + + // Unjail the validator + unjail_validator(&mut s, val_addr, current_epoch).unwrap(); + + // Check the validator state + for epoch in + Epoch::iter_bounds_inclusive(current_epoch, current_epoch.next()) + { + assert_eq!( + validator_state_handle(val_addr) + .get(&s, epoch, ¶ms) + .unwrap(), + Some(ValidatorState::Jailed) + ); + } + + assert_eq!( + validator_state_handle(val_addr) + .get(&s, current_epoch + params.pipeline_len, ¶ms) + .unwrap(), + Some(ValidatorState::Consensus) + ); + assert!( + validator_set_positions_handle() + .at(&(current_epoch + params.pipeline_len)) + .get(&s, val_addr) + .unwrap() + .is_some(), + ); + + // Advance another epoch + current_epoch = advance_epoch(&mut s, ¶ms); + process_slashes(&mut s, current_epoch).unwrap(); + + let second_att = unjail_validator(&mut s, val_addr, current_epoch); + assert!(second_att.is_err()); +} + +fn test_unslashed_bond_amount_aux(validators: Vec) { + let mut storage = TestWlStorage::default(); + let params = OwnedPosParams { + unbonding_len: 4, + ..Default::default() + }; + + // Genesis + let mut current_epoch = storage.storage.block.epoch; + let params = test_init_genesis( + &mut storage, + params, + validators.clone().into_iter(), + current_epoch, + ) + .unwrap(); + storage.commit_block().unwrap(); + + let validator1 = validators[0].address.clone(); + let validator2 = validators[1].address.clone(); + + // Get a delegator with some tokens + let staking_token = staking_token_address(&storage); + let delegator = address::testing::gen_implicit_address(); + let del_balance = token::Amount::from_uint(1_000_000, 0).unwrap(); + credit_tokens(&mut storage, &staking_token, &delegator, del_balance) + .unwrap(); + + // Bond to validator 1 + bond_tokens( + &mut storage, + Some(&delegator), + &validator1, + 10_000.into(), + current_epoch, + None, + ) + .unwrap(); + + // Unbond some from validator 1 + unbond_tokens( + &mut storage, + Some(&delegator), + &validator1, + 1_342.into(), + current_epoch, + false, + ) + .unwrap(); + + // Redelegate some from validator 1 -> 2 + redelegate_tokens( + &mut storage, + &delegator, + &validator1, + &validator2, + current_epoch, + 1_875.into(), + ) + .unwrap(); + + // Unbond some from validator 2 + unbond_tokens( + &mut storage, + Some(&delegator), + &validator2, + 584.into(), + current_epoch, + false, + ) + .unwrap(); + + // Advance an epoch + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + + // Bond to validator 1 + bond_tokens( + &mut storage, + Some(&delegator), + &validator1, + 384.into(), + current_epoch, + None, + ) + .unwrap(); + + // Unbond some from validator 1 + unbond_tokens( + &mut storage, + Some(&delegator), + &validator1, + 144.into(), + current_epoch, + false, + ) + .unwrap(); + + // Redelegate some from validator 1 -> 2 + redelegate_tokens( + &mut storage, + &delegator, + &validator1, + &validator2, + current_epoch, + 3_448.into(), + ) + .unwrap(); + + // Unbond some from validator 2 + unbond_tokens( + &mut storage, + Some(&delegator), + &validator2, + 699.into(), + current_epoch, + false, + ) + .unwrap(); + + // Advance an epoch + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + + // Bond to validator 1 + bond_tokens( + &mut storage, + Some(&delegator), + &validator1, + 4_384.into(), + current_epoch, + None, + ) + .unwrap(); + + // Redelegate some from validator 1 -> 2 + redelegate_tokens( + &mut storage, + &delegator, + &validator1, + &validator2, + current_epoch, + 1_008.into(), + ) + .unwrap(); + + // Unbond some from validator 2 + unbond_tokens( + &mut storage, + Some(&delegator), + &validator2, + 3_500.into(), + current_epoch, + false, + ) + .unwrap(); + + // Checks + let val1_init_stake = validators[0].tokens; + + for epoch in Epoch::iter_bounds_inclusive( + Epoch(0), + current_epoch + params.pipeline_len, + ) { + let bond_amount = crate::bond_amount( + &storage, + &BondId { + source: delegator.clone(), + validator: validator1.clone(), + }, + epoch, + ) + .unwrap_or_default(); + + let val_stake = + crate::read_validator_stake(&storage, ¶ms, &validator1, epoch) + .unwrap(); + // dbg!(&bond_amount); + assert_eq!(val_stake - val1_init_stake, bond_amount); + } +} + +fn test_log_block_rewards_aux( + validators: Vec, + params: OwnedPosParams, +) { + tracing::info!( + "New case with {} validators: {:#?}", + validators.len(), + validators + .iter() + .map(|v| (&v.address, v.tokens.to_string_native())) + .collect::>() + ); + let mut s = TestWlStorage::default(); + // Init genesis + let current_epoch = s.storage.block.epoch; + let params = test_init_genesis( + &mut s, + params, + validators.clone().into_iter(), + current_epoch, + ) + .unwrap(); + s.commit_block().unwrap(); + let total_stake = + crate::get_total_consensus_stake(&s, current_epoch, ¶ms).unwrap(); + let consensus_set = + crate::read_consensus_validator_set_addresses(&s, current_epoch) + .unwrap(); + let proposer_address = consensus_set.iter().next().unwrap().clone(); + + tracing::info!( + ?params.block_proposer_reward, + ?params.block_vote_reward, + ); + tracing::info!(?proposer_address,); + + // Rewards accumulator should be empty at first + let rewards_handle = rewards_accumulator_handle(); + assert!(rewards_handle.is_empty(&s).unwrap()); + + let mut last_rewards = BTreeMap::default(); + + let num_blocks = 100; + // Loop through `num_blocks`, log rewards & check results + for i in 0..num_blocks { + tracing::info!(""); + tracing::info!("Block {}", i + 1); + + // A helper closure to prepare minimum required votes + let prep_votes = |epoch| { + // Ceil of 2/3 of total stake + let min_required_votes = total_stake.mul_ceil(Dec::two() / 3); + + let mut total_votes = token::Amount::zero(); + let mut non_voters = HashSet::
::default(); + let mut prep_vote = |validator| { + // Add validator vote if it's in consensus set and if we don't + // yet have min required votes + if consensus_set.contains(validator) + && total_votes < min_required_votes + { + let stake = + read_validator_stake(&s, ¶ms, validator, epoch) + .unwrap(); + total_votes += stake; + let validator_vp = + into_tm_voting_power(params.tm_votes_per_token, stake) + as u64; + tracing::info!("Validator {validator} signed"); + Some(VoteInfo { + validator_address: validator.clone(), + validator_vp, + }) + } else { + non_voters.insert(validator.clone()); + None + } + }; + + let votes: Vec = validators + .iter() + .rev() + .filter_map(|validator| prep_vote(&validator.address)) + .collect(); + (votes, total_votes, non_voters) + }; + + let (votes, signing_stake, non_voters) = prep_votes(current_epoch); + log_block_rewards( + &mut s, + current_epoch, + &proposer_address, + votes.clone(), + ) + .unwrap(); + + assert!(!rewards_handle.is_empty(&s).unwrap()); + + let rewards_calculator = PosRewardsCalculator { + proposer_reward: params.block_proposer_reward, + signer_reward: params.block_vote_reward, + signing_stake, + total_stake, + }; + let coeffs = rewards_calculator.get_reward_coeffs().unwrap(); + tracing::info!(?coeffs); + + // Check proposer reward + let stake = + read_validator_stake(&s, ¶ms, &proposer_address, current_epoch) + .unwrap(); + let proposer_signing_reward = votes.iter().find_map(|vote| { + if vote.validator_address == proposer_address { + let signing_fraction = + Dec::from(stake) / Dec::from(signing_stake); + Some(coeffs.signer_coeff * signing_fraction) + } else { + None + } + }); + let expected_proposer_rewards = last_rewards.get(&proposer_address).copied().unwrap_or_default() + + // Proposer reward + coeffs.proposer_coeff + // Consensus validator reward + + (coeffs.active_val_coeff + * (Dec::from(stake) / Dec::from(total_stake))) + // Signing reward (if proposer voted) + + proposer_signing_reward + .unwrap_or_default(); + tracing::info!( + "Expected proposer rewards: {expected_proposer_rewards}. Signed \ + block: {}", + proposer_signing_reward.is_some() + ); + assert_eq!( + rewards_handle.get(&s, &proposer_address).unwrap(), + Some(expected_proposer_rewards) + ); + + // Check voters rewards + for VoteInfo { + validator_address, .. + } in votes.iter() + { + // Skip proposer, in case voted - already checked + if validator_address == &proposer_address { + continue; + } + + let stake = read_validator_stake( + &s, + ¶ms, + validator_address, + current_epoch, + ) + .unwrap(); + let signing_fraction = Dec::from(stake) / Dec::from(signing_stake); + let expected_signer_rewards = last_rewards + .get(validator_address) + .copied() + .unwrap_or_default() + + coeffs.signer_coeff * signing_fraction + + (coeffs.active_val_coeff + * (Dec::from(stake) / Dec::from(total_stake))); + tracing::info!( + "Expected signer {validator_address} rewards: \ + {expected_signer_rewards}" + ); + assert_eq!( + rewards_handle.get(&s, validator_address).unwrap(), + Some(expected_signer_rewards) + ); + } + + // Check non-voters rewards, if any + for address in non_voters { + // Skip proposer, in case it didn't vote - already checked + if address == proposer_address { + continue; + } + + if consensus_set.contains(&address) { + let stake = + read_validator_stake(&s, ¶ms, &address, current_epoch) + .unwrap(); + let expected_non_signer_rewards = + last_rewards.get(&address).copied().unwrap_or_default() + + coeffs.active_val_coeff + * (Dec::from(stake) / Dec::from(total_stake)); + tracing::info!( + "Expected non-signer {address} rewards: \ + {expected_non_signer_rewards}" + ); + assert_eq!( + rewards_handle.get(&s, &address).unwrap(), + Some(expected_non_signer_rewards) + ); + } else { + let last_reward = last_rewards.get(&address).copied(); + assert_eq!( + rewards_handle.get(&s, &address).unwrap(), + last_reward + ); + } + } + s.commit_block().unwrap(); + + last_rewards = rewards_accumulator_handle().collect_map(&s).unwrap(); + + let rewards_sum: Dec = last_rewards.values().copied().sum(); + let expected_sum = Dec::one() * (i as u64 + 1); + let err_tolerance = Dec::new(1, 9).unwrap(); + let fail_msg = format!( + "Expected rewards sum at block {} to be {expected_sum}, got \ + {rewards_sum}. Error tolerance {err_tolerance}.", + i + 1 + ); + assert!(expected_sum <= rewards_sum + err_tolerance, "{fail_msg}"); + assert!(rewards_sum <= expected_sum, "{fail_msg}"); + } +} + +fn test_update_rewards_products_aux(validators: Vec) { + tracing::info!( + "New case with {} validators: {:#?}", + validators.len(), + validators + .iter() + .map(|v| (&v.address, v.tokens.to_string_native())) + .collect::>() + ); + let mut s = TestWlStorage::default(); + // Init genesis + let current_epoch = s.storage.block.epoch; + let params = OwnedPosParams::default(); + let params = test_init_genesis( + &mut s, + params, + validators.into_iter(), + current_epoch, + ) + .unwrap(); + s.commit_block().unwrap(); + + let staking_token = staking_token_address(&s); + let consensus_set = + crate::read_consensus_validator_set_addresses(&s, current_epoch) + .unwrap(); + + // Start a new epoch + let current_epoch = advance_epoch(&mut s, ¶ms); + + // Read some data before applying rewards + let pos_balance_pre = + read_balance(&s, &staking_token, &address::POS).unwrap(); + let gov_balance_pre = + read_balance(&s, &staking_token, &address::GOV).unwrap(); + + let num_consensus_validators = consensus_set.len() as u64; + let accum_val = Dec::one() / num_consensus_validators; + let num_blocks_in_last_epoch = 1000; + + // Assign some reward accumulator values to consensus validator + for validator in &consensus_set { + rewards_accumulator_handle() + .insert( + &mut s, + validator.clone(), + accum_val * num_blocks_in_last_epoch, + ) + .unwrap(); + } + + // Distribute inflation into rewards + let last_epoch = current_epoch.prev(); + let inflation = token::Amount::native_whole(10_000_000); + update_rewards_products_and_mint_inflation( + &mut s, + ¶ms, + last_epoch, + num_blocks_in_last_epoch, + inflation, + &staking_token, + ) + .unwrap(); + + let pos_balance_post = + read_balance(&s, &staking_token, &address::POS).unwrap(); + let gov_balance_post = + read_balance(&s, &staking_token, &address::GOV).unwrap(); + + assert_eq!( + pos_balance_pre + gov_balance_pre + inflation, + pos_balance_post + gov_balance_post, + "Expected inflation to be minted to PoS and left-over amount to Gov" + ); + + let pos_credit = pos_balance_post - pos_balance_pre; + let gov_credit = gov_balance_post - gov_balance_pre; + assert!( + pos_credit > gov_credit, + "PoS must receive more tokens than Gov, but got {} in PoS and {} in \ + Gov", + pos_credit.to_string_native(), + gov_credit.to_string_native() + ); + + // Rewards accumulator must be cleared out + let rewards_handle = rewards_accumulator_handle(); + assert!(rewards_handle.is_empty(&s).unwrap()); +} + +fn test_consensus_key_change_aux(validators: Vec) { + assert_eq!(validators.len(), 1); + + let params = OwnedPosParams { + unbonding_len: 4, + ..Default::default() + }; + let validator = validators[0].address.clone(); + + // println!("\nTest inputs: {params:?}, genesis validators: + // {validators:#?}"); + let mut storage = TestWlStorage::default(); + + // Genesis + let mut current_epoch = storage.storage.block.epoch; + let params = test_init_genesis( + &mut storage, + params, + validators.into_iter(), + current_epoch, + ) + .unwrap(); + storage.commit_block().unwrap(); + + // Check that there is one consensus key in the network + let consensus_keys = get_consensus_key_set(&storage).unwrap(); + assert_eq!(consensus_keys.len(), 1); + let ck = consensus_keys.first().cloned().unwrap(); + let og_ck = validator_consensus_key_handle(&validator) + .get(&storage, current_epoch, ¶ms) + .unwrap() + .unwrap(); + assert_eq!(ck, og_ck); + + // Attempt to change to a new secp256k1 consensus key (disallowed) + let secp_ck = gen_keypair::(); + let secp_ck = key::common::SecretKey::Secp256k1(secp_ck).ref_to(); + let res = + change_consensus_key(&mut storage, &validator, &secp_ck, current_epoch); + assert!(res.is_err()); + + // Change consensus keys + let ck_2 = common_sk_from_simple_seed(1).ref_to(); + change_consensus_key(&mut storage, &validator, &ck_2, current_epoch) + .unwrap(); + + // Check that there is a new consensus key + let consensus_keys = get_consensus_key_set(&storage).unwrap(); + assert_eq!(consensus_keys.len(), 2); + + for epoch in current_epoch.iter_range(params.pipeline_len) { + let ck = validator_consensus_key_handle(&validator) + .get(&storage, epoch, ¶ms) + .unwrap() + .unwrap(); + assert_eq!(ck, og_ck); + } + let pipeline_epoch = current_epoch + params.pipeline_len; + let ck = validator_consensus_key_handle(&validator) + .get(&storage, pipeline_epoch, ¶ms) + .unwrap() + .unwrap(); + assert_eq!(ck, ck_2); + + // Advance to the pipeline epoch + loop { + current_epoch = advance_epoch(&mut storage, ¶ms); + if current_epoch == pipeline_epoch { + break; + } + } + + // Check the consensus keys again + let consensus_keys = get_consensus_key_set(&storage).unwrap(); + assert_eq!(consensus_keys.len(), 2); + + for epoch in current_epoch.iter_range(params.pipeline_len + 1) { + let ck = validator_consensus_key_handle(&validator) + .get(&storage, epoch, ¶ms) + .unwrap() + .unwrap(); + assert_eq!(ck, ck_2); + } + + // Now change the consensus key again and bond in the same epoch + let ck_3 = common_sk_from_simple_seed(3).ref_to(); + change_consensus_key(&mut storage, &validator, &ck_3, current_epoch) + .unwrap(); + + let staking_token = storage.storage.native_token.clone(); + let amount_del = token::Amount::native_whole(5); + credit_tokens(&mut storage, &staking_token, &validator, amount_del) + .unwrap(); + bond_tokens( + &mut storage, + None, + &validator, + token::Amount::native_whole(1), + current_epoch, + None, + ) + .unwrap(); + + // Check consensus keys again + let consensus_keys = get_consensus_key_set(&storage).unwrap(); + assert_eq!(consensus_keys.len(), 3); + + for epoch in current_epoch.iter_range(params.pipeline_len) { + let ck = validator_consensus_key_handle(&validator) + .get(&storage, epoch, ¶ms) + .unwrap() + .unwrap(); + assert_eq!(ck, ck_2); + } + let pipeline_epoch = current_epoch + params.pipeline_len; + let ck = validator_consensus_key_handle(&validator) + .get(&storage, pipeline_epoch, ¶ms) + .unwrap() + .unwrap(); + assert_eq!(ck, ck_3); + + // Advance to the pipeline epoch to ensure that the validator set updates to + // tendermint will work + loop { + current_epoch = advance_epoch(&mut storage, ¶ms); + if current_epoch == pipeline_epoch { + break; + } + } + assert_eq!(current_epoch.0, 2 * params.pipeline_len); +} + +fn test_is_delegator_aux(mut validators: Vec) { + validators.sort_by(|a, b| b.tokens.cmp(&a.tokens)); + + let validator1 = validators[0].address.clone(); + let validator2 = validators[1].address.clone(); + + let mut storage = TestWlStorage::default(); + let params = OwnedPosParams { + unbonding_len: 4, + ..Default::default() + }; + + // Genesis + let mut current_epoch = storage.storage.block.epoch; + let params = test_init_genesis( + &mut storage, + params, + validators.clone().into_iter(), + current_epoch, + ) + .unwrap(); + storage.commit_block().unwrap(); + + // Get delegators with some tokens + let staking_token = staking_token_address(&storage); + let delegator1 = address::testing::gen_implicit_address(); + let delegator2 = address::testing::gen_implicit_address(); + let del_balance = token::Amount::native_whole(1000); + credit_tokens(&mut storage, &staking_token, &delegator1, del_balance) + .unwrap(); + credit_tokens(&mut storage, &staking_token, &delegator2, del_balance) + .unwrap(); + + // Advance to epoch 1 + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + + // Delegate in epoch 1 to validator1 + let del1_epoch = current_epoch; + bond_tokens( + &mut storage, + Some(&delegator1), + &validator1, + 1000.into(), + current_epoch, + None, + ) + .unwrap(); + + // Advance to epoch 2 + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + + // Delegate in epoch 2 to validator2 + let del2_epoch = current_epoch; + bond_tokens( + &mut storage, + Some(&delegator2), + &validator2, + 1000.into(), + current_epoch, + None, + ) + .unwrap(); + + // Checks + assert!(is_validator(&storage, &validator1).unwrap()); + assert!(is_validator(&storage, &validator2).unwrap()); + assert!(!is_delegator(&storage, &validator1, None).unwrap()); + assert!(!is_delegator(&storage, &validator2, None).unwrap()); + + assert!(!is_validator(&storage, &delegator1).unwrap()); + assert!(!is_validator(&storage, &delegator2).unwrap()); + assert!(is_delegator(&storage, &delegator1, None).unwrap()); + assert!(is_delegator(&storage, &delegator2, None).unwrap()); + + for epoch in Epoch::default().iter_range(del1_epoch.0 + params.pipeline_len) + { + assert!(!is_delegator(&storage, &delegator1, Some(epoch)).unwrap()); + } + assert!( + is_delegator( + &storage, + &delegator1, + Some(del1_epoch + params.pipeline_len) + ) + .unwrap() + ); + for epoch in Epoch::default().iter_range(del2_epoch.0 + params.pipeline_len) + { + assert!(!is_delegator(&storage, &delegator2, Some(epoch)).unwrap()); + } + assert!( + is_delegator( + &storage, + &delegator2, + Some(del2_epoch + params.pipeline_len) + ) + .unwrap() + ); +} diff --git a/proof_of_stake/src/tests/test_slash_and_redel.rs b/proof_of_stake/src/tests/test_slash_and_redel.rs new file mode 100644 index 0000000000..17b73494b6 --- /dev/null +++ b/proof_of_stake/src/tests/test_slash_and_redel.rs @@ -0,0 +1,1495 @@ +use std::ops::Deref; +use std::str::FromStr; + +use assert_matches::assert_matches; +use namada_core::ledger::storage::testing::TestWlStorage; +use namada_core::ledger::storage_api::collections::lazy_map::Collectable; +use namada_core::ledger::storage_api::token::{credit_tokens, read_balance}; +use namada_core::ledger::storage_api::StorageRead; +use namada_core::types::dec::Dec; +use namada_core::types::storage::{BlockHeight, Epoch}; +use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; +use namada_core::types::{address, token}; +use proptest::prelude::*; +use proptest::test_runner::Config; +// Use `RUST_LOG=info` (or another tracing level) and `--nocapture` to see +// `tracing` logs from tests +use test_log::test; + +use crate::queries::bonds_and_unbonds; +use crate::slashing::{process_slashes, slash}; +use crate::storage::{ + bond_handle, delegator_redelegated_bonds_handle, + delegator_redelegated_unbonds_handle, read_total_stake, + read_validator_stake, total_bonded_handle, total_unbonded_handle, + unbond_handle, validator_incoming_redelegations_handle, + validator_outgoing_redelegations_handle, validator_slashes_handle, + validator_total_redelegated_bonded_handle, + validator_total_redelegated_unbonded_handle, +}; +use crate::test_utils::test_init_genesis; +use crate::tests::helpers::{ + advance_epoch, arb_genesis_validators, arb_redelegation_amounts, + test_slashes_with_unbonding_params, +}; +use crate::types::{BondId, GenesisValidator, SlashType}; +use crate::{ + bond_tokens, redelegate_tokens, staking_token_address, unbond_tokens, + withdraw_tokens, OwnedPosParams, RedelegationError, +}; + +proptest! { + // Generate arb valid input for `test_simple_redelegation_aux` + #![proptest_config(Config { + cases: 100, + .. Config::default() + })] + #[test] + fn test_simple_redelegation( + + genesis_validators in arb_genesis_validators(2..4, None), + (amount_delegate, amount_redelegate, amount_unbond) in arb_redelegation_amounts(20) + + ) { + test_simple_redelegation_aux(genesis_validators, amount_delegate, amount_redelegate, amount_unbond) + } +} + +fn test_simple_redelegation_aux( + mut validators: Vec, + amount_delegate: token::Amount, + amount_redelegate: token::Amount, + amount_unbond: token::Amount, +) { + validators.sort_by(|a, b| b.tokens.cmp(&a.tokens)); + + let src_validator = validators[0].address.clone(); + let dest_validator = validators[1].address.clone(); + + let mut storage = TestWlStorage::default(); + let params = OwnedPosParams { + unbonding_len: 4, + ..Default::default() + }; + + // Genesis + let mut current_epoch = storage.storage.block.epoch; + let params = test_init_genesis( + &mut storage, + params, + validators.clone().into_iter(), + current_epoch, + ) + .unwrap(); + storage.commit_block().unwrap(); + + // Get a delegator with some tokens + let staking_token = staking_token_address(&storage); + let delegator = address::testing::gen_implicit_address(); + let del_balance = token::Amount::from_uint(1_000_000, 0).unwrap(); + credit_tokens(&mut storage, &staking_token, &delegator, del_balance) + .unwrap(); + + // Ensure that we cannot redelegate with the same src and dest validator + let err = redelegate_tokens( + &mut storage, + &delegator, + &src_validator, + &src_validator, + current_epoch, + amount_redelegate, + ) + .unwrap_err(); + let err_str = err.to_string(); + assert_matches!( + err.downcast::().unwrap().deref(), + RedelegationError::RedelegationSrcEqDest, + "Redelegation with the same src and dest validator must be rejected, \ + got {err_str}", + ); + + for _ in 0..5 { + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + } + + let init_epoch = current_epoch; + + // Delegate in epoch 1 to src_validator + bond_tokens( + &mut storage, + Some(&delegator), + &src_validator, + amount_delegate, + current_epoch, + None, + ) + .unwrap(); + + // Advance three epochs + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + + // Redelegate in epoch 3 + redelegate_tokens( + &mut storage, + &delegator, + &src_validator, + &dest_validator, + current_epoch, + amount_redelegate, + ) + .unwrap(); + + // Dest val + + // Src val + + // Checks + let redelegated = delegator_redelegated_bonds_handle(&delegator) + .at(&dest_validator) + .at(&(current_epoch + params.pipeline_len)) + .at(&src_validator) + .get(&storage, &(init_epoch + params.pipeline_len)) + .unwrap() + .unwrap(); + assert_eq!(redelegated, amount_redelegate); + + let redel_start_epoch = + validator_incoming_redelegations_handle(&dest_validator) + .get(&storage, &delegator) + .unwrap() + .unwrap(); + assert_eq!(redel_start_epoch, current_epoch + params.pipeline_len); + + let redelegated = validator_outgoing_redelegations_handle(&src_validator) + .at(&dest_validator) + .at(¤t_epoch.prev()) + .get(&storage, ¤t_epoch) + .unwrap() + .unwrap(); + assert_eq!(redelegated, amount_redelegate); + + // Advance three epochs + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + + // Unbond in epoch 5 from dest_validator + let _ = unbond_tokens( + &mut storage, + Some(&delegator), + &dest_validator, + amount_unbond, + current_epoch, + false, + ) + .unwrap(); + + let bond_start = init_epoch + params.pipeline_len; + let redelegation_end = bond_start + params.pipeline_len + 1u64; + let unbond_end = + redelegation_end + params.withdrawable_epoch_offset() + 1u64; + let unbond_materialized = redelegation_end + params.pipeline_len + 1u64; + + // Checks + let redelegated_remaining = delegator_redelegated_bonds_handle(&delegator) + .at(&dest_validator) + .at(&redelegation_end) + .at(&src_validator) + .get(&storage, &bond_start) + .unwrap() + .unwrap_or_default(); + assert_eq!(redelegated_remaining, amount_redelegate - amount_unbond); + + let redel_unbonded = delegator_redelegated_unbonds_handle(&delegator) + .at(&dest_validator) + .at(&redelegation_end) + .at(&unbond_end) + .at(&src_validator) + .get(&storage, &bond_start) + .unwrap() + .unwrap(); + assert_eq!(redel_unbonded, amount_unbond); + + let total_redel_unbonded = + validator_total_redelegated_unbonded_handle(&dest_validator) + .at(&unbond_materialized) + .at(&redelegation_end) + .at(&src_validator) + .get(&storage, &bond_start) + .unwrap() + .unwrap(); + assert_eq!(total_redel_unbonded, amount_unbond); + + // Advance to withdrawal epoch + loop { + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + if current_epoch == unbond_end { + break; + } + } + + // Withdraw + withdraw_tokens( + &mut storage, + Some(&delegator), + &dest_validator, + current_epoch, + ) + .unwrap(); + + assert!( + delegator_redelegated_unbonds_handle(&delegator) + .at(&dest_validator) + .is_empty(&storage) + .unwrap() + ); + + let delegator_balance = storage + .read::(&token::balance_key(&staking_token, &delegator)) + .unwrap() + .unwrap_or_default(); + assert_eq!( + delegator_balance, + del_balance - amount_delegate + amount_unbond + ); +} + +proptest! { + // Generate arb valid input for `test_slashes_with_unbonding_aux` + #![proptest_config(Config { + cases: 100, + .. Config::default() + })] + #[test] + fn test_slashes_with_unbonding( + (params, genesis_validators, unbond_delay) + in test_slashes_with_unbonding_params() + ) { + test_slashes_with_unbonding_aux( + params, genesis_validators, unbond_delay) + } +} + +fn test_slashes_with_unbonding_aux( + mut params: OwnedPosParams, + validators: Vec, + unbond_delay: u64, +) { + // This can be useful for debugging: + params.pipeline_len = 2; + params.unbonding_len = 4; + // println!("\nTest inputs: {params:?}, genesis validators: + // {validators:#?}"); + let mut s = TestWlStorage::default(); + + // Find the validator with the least stake to avoid the cubic slash rate + // going to 100% + let validator = + itertools::Itertools::sorted_by_key(validators.iter(), |v| v.tokens) + .next() + .unwrap(); + let val_addr = &validator.address; + let val_tokens = validator.tokens; + + // Genesis + // let start_epoch = s.storage.block.epoch; + let mut current_epoch = s.storage.block.epoch; + let params = test_init_genesis( + &mut s, + params, + validators.clone().into_iter(), + current_epoch, + ) + .unwrap(); + s.commit_block().unwrap(); + + current_epoch = advance_epoch(&mut s, ¶ms); + process_slashes(&mut s, current_epoch).unwrap(); + + // Discover first slash + let slash_0_evidence_epoch = current_epoch; + // let slash_0_processing_epoch = + // slash_0_evidence_epoch + params.slash_processing_epoch_offset(); + let evidence_block_height = BlockHeight(0); // doesn't matter for slashing logic + let slash_0_type = SlashType::DuplicateVote; + slash( + &mut s, + ¶ms, + current_epoch, + slash_0_evidence_epoch, + evidence_block_height, + slash_0_type, + val_addr, + current_epoch.next(), + ) + .unwrap(); + + // Advance to an epoch in which we can unbond + let unfreeze_epoch = + slash_0_evidence_epoch + params.slash_processing_epoch_offset(); + while current_epoch < unfreeze_epoch { + current_epoch = advance_epoch(&mut s, ¶ms); + process_slashes(&mut s, current_epoch).unwrap(); + } + + // Advance more epochs randomly from the generated delay + for _ in 0..unbond_delay { + current_epoch = advance_epoch(&mut s, ¶ms); + } + + // Unbond half of the tokens + let unbond_amount = Dec::new(5, 1).unwrap() * val_tokens; + let unbond_epoch = current_epoch; + unbond_tokens(&mut s, None, val_addr, unbond_amount, unbond_epoch, false) + .unwrap(); + + // Discover second slash + let slash_1_evidence_epoch = current_epoch; + // Ensure that both slashes happen before `unbond_epoch + pipeline` + let _slash_1_processing_epoch = + slash_1_evidence_epoch + params.slash_processing_epoch_offset(); + let evidence_block_height = BlockHeight(0); // doesn't matter for slashing logic + let slash_1_type = SlashType::DuplicateVote; + slash( + &mut s, + ¶ms, + current_epoch, + slash_1_evidence_epoch, + evidence_block_height, + slash_1_type, + val_addr, + current_epoch.next(), + ) + .unwrap(); + + // Advance to an epoch in which we can withdraw + let withdraw_epoch = unbond_epoch + params.withdrawable_epoch_offset(); + while current_epoch < withdraw_epoch { + current_epoch = advance_epoch(&mut s, ¶ms); + process_slashes(&mut s, current_epoch).unwrap(); + } + let token = staking_token_address(&s); + let val_balance_pre = read_balance(&s, &token, val_addr).unwrap(); + + let bond_id = BondId { + source: val_addr.clone(), + validator: val_addr.clone(), + }; + let binding = bonds_and_unbonds(&s, None, Some(val_addr.clone())).unwrap(); + let details = binding.get(&bond_id).unwrap(); + let exp_withdraw_from_details = details.unbonds[0].amount + - details.unbonds[0].slashed_amount.unwrap_or_default(); + + withdraw_tokens(&mut s, None, val_addr, current_epoch).unwrap(); + + let val_balance_post = read_balance(&s, &token, val_addr).unwrap(); + let withdrawn_tokens = val_balance_post - val_balance_pre; + + assert_eq!(exp_withdraw_from_details, withdrawn_tokens); + + let slash_rate_0 = validator_slashes_handle(val_addr) + .get(&s, 0) + .unwrap() + .unwrap() + .rate; + let slash_rate_1 = validator_slashes_handle(val_addr) + .get(&s, 1) + .unwrap() + .unwrap() + .rate; + + let expected_withdrawn_amount = Dec::from( + (Dec::one() - slash_rate_1) + * (Dec::one() - slash_rate_0) + * unbond_amount, + ); + // Allow some rounding error, 1 NAMNAM per each slash + let rounding_error_tolerance = + Dec::new(2, NATIVE_MAX_DECIMAL_PLACES).unwrap(); + assert!( + expected_withdrawn_amount.abs_diff(&Dec::from(withdrawn_tokens)) + <= rounding_error_tolerance + ); + + // TODO: finish once implemented + // let slash_0 = decimal_mult_amount(slash_rate_0, val_tokens); + // let slash_1 = decimal_mult_amount(slash_rate_1, val_tokens - slash_0); + // let expected_slash_pool = slash_0 + slash_1; + // let slash_pool_balance = + // read_balance(&s, &token, &SLASH_POOL_ADDRESS).unwrap(); + // assert_eq!(expected_slash_pool, slash_pool_balance); +} + +proptest! { + // Generate arb valid input for `test_simple_redelegation_aux` + #![proptest_config(Config { + cases: 100, + .. Config::default() + })] + #[test] + fn test_redelegation_with_slashing( + + genesis_validators in arb_genesis_validators(2..4, None), + (amount_delegate, amount_redelegate, amount_unbond) in arb_redelegation_amounts(20) + + ) { + test_redelegation_with_slashing_aux(genesis_validators, amount_delegate, amount_redelegate, amount_unbond) + } +} + +fn test_redelegation_with_slashing_aux( + mut validators: Vec, + amount_delegate: token::Amount, + amount_redelegate: token::Amount, + amount_unbond: token::Amount, +) { + validators.sort_by(|a, b| b.tokens.cmp(&a.tokens)); + + let src_validator = validators[0].address.clone(); + let dest_validator = validators[1].address.clone(); + + let mut storage = TestWlStorage::default(); + let params = OwnedPosParams { + unbonding_len: 4, + // Avoid empty consensus set by removing the threshold + validator_stake_threshold: token::Amount::zero(), + ..Default::default() + }; + + // Genesis + let mut current_epoch = storage.storage.block.epoch; + let params = test_init_genesis( + &mut storage, + params, + validators.clone().into_iter(), + current_epoch, + ) + .unwrap(); + storage.commit_block().unwrap(); + + // Get a delegator with some tokens + let staking_token = staking_token_address(&storage); + let delegator = address::testing::gen_implicit_address(); + let del_balance = token::Amount::from_uint(1_000_000, 0).unwrap(); + credit_tokens(&mut storage, &staking_token, &delegator, del_balance) + .unwrap(); + + for _ in 0..5 { + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + } + + let init_epoch = current_epoch; + + // Delegate in epoch 5 to src_validator + bond_tokens( + &mut storage, + Some(&delegator), + &src_validator, + amount_delegate, + current_epoch, + None, + ) + .unwrap(); + + // Advance three epochs + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + + // Redelegate in epoch 8 + redelegate_tokens( + &mut storage, + &delegator, + &src_validator, + &dest_validator, + current_epoch, + amount_redelegate, + ) + .unwrap(); + + // Checks + let redelegated = delegator_redelegated_bonds_handle(&delegator) + .at(&dest_validator) + .at(&(current_epoch + params.pipeline_len)) + .at(&src_validator) + .get(&storage, &(init_epoch + params.pipeline_len)) + .unwrap() + .unwrap(); + assert_eq!(redelegated, amount_redelegate); + + let redel_start_epoch = + validator_incoming_redelegations_handle(&dest_validator) + .get(&storage, &delegator) + .unwrap() + .unwrap(); + assert_eq!(redel_start_epoch, current_epoch + params.pipeline_len); + + let redelegated = validator_outgoing_redelegations_handle(&src_validator) + .at(&dest_validator) + .at(¤t_epoch.prev()) + .get(&storage, ¤t_epoch) + .unwrap() + .unwrap(); + assert_eq!(redelegated, amount_redelegate); + + // Advance three epochs + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + + // Unbond in epoch 11 from dest_validator + let _ = unbond_tokens( + &mut storage, + Some(&delegator), + &dest_validator, + amount_unbond, + current_epoch, + false, + ) + .unwrap(); + + // Advance one epoch + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + + // Discover evidence + slash( + &mut storage, + ¶ms, + current_epoch, + init_epoch + 2 * params.pipeline_len, + 0u64, + SlashType::DuplicateVote, + &src_validator, + current_epoch.next(), + ) + .unwrap(); + + let bond_start = init_epoch + params.pipeline_len; + let redelegation_end = bond_start + params.pipeline_len + 1u64; + let unbond_end = + redelegation_end + params.withdrawable_epoch_offset() + 1u64; + let unbond_materialized = redelegation_end + params.pipeline_len + 1u64; + + // Checks + let redelegated_remaining = delegator_redelegated_bonds_handle(&delegator) + .at(&dest_validator) + .at(&redelegation_end) + .at(&src_validator) + .get(&storage, &bond_start) + .unwrap() + .unwrap_or_default(); + assert_eq!(redelegated_remaining, amount_redelegate - amount_unbond); + + let redel_unbonded = delegator_redelegated_unbonds_handle(&delegator) + .at(&dest_validator) + .at(&redelegation_end) + .at(&unbond_end) + .at(&src_validator) + .get(&storage, &bond_start) + .unwrap() + .unwrap(); + assert_eq!(redel_unbonded, amount_unbond); + + let total_redel_unbonded = + validator_total_redelegated_unbonded_handle(&dest_validator) + .at(&unbond_materialized) + .at(&redelegation_end) + .at(&src_validator) + .get(&storage, &bond_start) + .unwrap() + .unwrap(); + assert_eq!(total_redel_unbonded, amount_unbond); + + // Advance to withdrawal epoch + loop { + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + if current_epoch == unbond_end { + break; + } + } + + // Withdraw + withdraw_tokens( + &mut storage, + Some(&delegator), + &dest_validator, + current_epoch, + ) + .unwrap(); + + assert!( + delegator_redelegated_unbonds_handle(&delegator) + .at(&dest_validator) + .is_empty(&storage) + .unwrap() + ); + + let delegator_balance = storage + .read::(&token::balance_key(&staking_token, &delegator)) + .unwrap() + .unwrap_or_default(); + assert_eq!(delegator_balance, del_balance - amount_delegate); +} + +proptest! { + // Generate arb valid input for `test_chain_redelegations_aux` + #![proptest_config(Config { + cases: 100, + .. Config::default() + })] + #[test] + fn test_chain_redelegations( + + genesis_validators in arb_genesis_validators(3..4, None), + + ) { + test_chain_redelegations_aux(genesis_validators) + } +} + +fn test_chain_redelegations_aux(mut validators: Vec) { + validators.sort_by(|a, b| b.tokens.cmp(&a.tokens)); + + let src_validator = validators[0].address.clone(); + let _init_stake_src = validators[0].tokens; + let dest_validator = validators[1].address.clone(); + let _init_stake_dest = validators[1].tokens; + let dest_validator_2 = validators[2].address.clone(); + let _init_stake_dest_2 = validators[2].tokens; + + let mut storage = TestWlStorage::default(); + let params = OwnedPosParams { + unbonding_len: 4, + ..Default::default() + }; + + // Genesis + let mut current_epoch = storage.storage.block.epoch; + let params = test_init_genesis( + &mut storage, + params, + validators.clone().into_iter(), + current_epoch, + ) + .unwrap(); + storage.commit_block().unwrap(); + + // Get a delegator with some tokens + let staking_token = staking_token_address(&storage); + let delegator = address::testing::gen_implicit_address(); + let del_balance = token::Amount::from_uint(1_000_000, 0).unwrap(); + credit_tokens(&mut storage, &staking_token, &delegator, del_balance) + .unwrap(); + + // Delegate in epoch 0 to src_validator + let bond_amount: token::Amount = 100.into(); + bond_tokens( + &mut storage, + Some(&delegator), + &src_validator, + bond_amount, + current_epoch, + None, + ) + .unwrap(); + + let bond_start = current_epoch + params.pipeline_len; + + // Advance one epoch + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + + // Redelegate in epoch 1 to dest_validator + let redel_amount_1: token::Amount = 58.into(); + redelegate_tokens( + &mut storage, + &delegator, + &src_validator, + &dest_validator, + current_epoch, + redel_amount_1, + ) + .unwrap(); + + let redel_start = current_epoch; + let redel_end = current_epoch + params.pipeline_len; + + // Checks ---------------- + + // Dest validator should have an incoming redelegation + let incoming_redelegation = + validator_incoming_redelegations_handle(&dest_validator) + .get(&storage, &delegator) + .unwrap(); + assert_eq!(incoming_redelegation, Some(redel_end)); + + // Src validator should have an outoging redelegation + let outgoing_redelegation = + validator_outgoing_redelegations_handle(&src_validator) + .at(&dest_validator) + .at(&bond_start) + .get(&storage, &redel_start) + .unwrap(); + assert_eq!(outgoing_redelegation, Some(redel_amount_1)); + + // Delegator should have redelegated bonds + let del_total_redelegated_bonded = + delegator_redelegated_bonds_handle(&delegator) + .at(&dest_validator) + .at(&redel_end) + .at(&src_validator) + .get(&storage, &bond_start) + .unwrap() + .unwrap_or_default(); + assert_eq!(del_total_redelegated_bonded, redel_amount_1); + + // There should be delegator bonds for both src and dest validators + let bonded_src = bond_handle(&delegator, &src_validator); + let bonded_dest = bond_handle(&delegator, &dest_validator); + assert_eq!( + bonded_src + .get_delta_val(&storage, bond_start) + .unwrap() + .unwrap_or_default(), + bond_amount - redel_amount_1 + ); + assert_eq!( + bonded_dest + .get_delta_val(&storage, redel_end) + .unwrap() + .unwrap_or_default(), + redel_amount_1 + ); + + // The dest validator should have total redelegated bonded tokens + let dest_total_redelegated_bonded = + validator_total_redelegated_bonded_handle(&dest_validator) + .at(&redel_end) + .at(&src_validator) + .get(&storage, &bond_start) + .unwrap() + .unwrap_or_default(); + assert_eq!(dest_total_redelegated_bonded, redel_amount_1); + + // The dest validator's total bonded should have an entry for the genesis + // bond and the redelegation + let dest_total_bonded = total_bonded_handle(&dest_validator) + .get_data_handler() + .collect_map(&storage) + .unwrap(); + assert!( + dest_total_bonded.len() == 2 + && dest_total_bonded.contains_key(&Epoch::default()) + ); + assert_eq!( + dest_total_bonded + .get(&redel_end) + .cloned() + .unwrap_or_default(), + redel_amount_1 + ); + + // The src validator should have a total bonded entry for the original bond + // accounting for the redelegation + assert_eq!( + total_bonded_handle(&src_validator) + .get_delta_val(&storage, bond_start) + .unwrap() + .unwrap_or_default(), + bond_amount - redel_amount_1 + ); + + // The src validator should have a total unbonded entry due to the + // redelegation + let src_total_unbonded = total_unbonded_handle(&src_validator) + .at(&redel_end) + .get(&storage, &bond_start) + .unwrap() + .unwrap_or_default(); + assert_eq!(src_total_unbonded, redel_amount_1); + + // Attempt to redelegate in epoch 3 to dest_validator + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + + let redel_amount_2: token::Amount = 23.into(); + let redel_att = redelegate_tokens( + &mut storage, + &delegator, + &dest_validator, + &dest_validator_2, + current_epoch, + redel_amount_2, + ); + assert!(redel_att.is_err()); + + // Advance to right before the redelegation can be redelegated again + assert_eq!(redel_end, current_epoch); + let epoch_can_redel = + redel_end.prev() + params.slash_processing_epoch_offset(); + loop { + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + if current_epoch == epoch_can_redel.prev() { + break; + } + } + + // Attempt to redelegate in epoch before we actually are able to + let redel_att = redelegate_tokens( + &mut storage, + &delegator, + &dest_validator, + &dest_validator_2, + current_epoch, + redel_amount_2, + ); + assert!(redel_att.is_err()); + + // Advance one more epoch + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + + // Redelegate from dest_validator to dest_validator_2 now + redelegate_tokens( + &mut storage, + &delegator, + &dest_validator, + &dest_validator_2, + current_epoch, + redel_amount_2, + ) + .unwrap(); + + let redel_2_start = current_epoch; + let redel_2_end = current_epoch + params.pipeline_len; + + // Checks ----------------------------------- + + // Both the dest validator and dest validator 2 should have incoming + // redelegations + let incoming_redelegation_1 = + validator_incoming_redelegations_handle(&dest_validator) + .get(&storage, &delegator) + .unwrap(); + assert_eq!(incoming_redelegation_1, Some(redel_end)); + let incoming_redelegation_2 = + validator_incoming_redelegations_handle(&dest_validator_2) + .get(&storage, &delegator) + .unwrap(); + assert_eq!(incoming_redelegation_2, Some(redel_2_end)); + + // Both the src validator and dest validator should have outgoing + // redelegations + let outgoing_redelegation_1 = + validator_outgoing_redelegations_handle(&src_validator) + .at(&dest_validator) + .at(&bond_start) + .get(&storage, &redel_start) + .unwrap(); + assert_eq!(outgoing_redelegation_1, Some(redel_amount_1)); + + let outgoing_redelegation_2 = + validator_outgoing_redelegations_handle(&dest_validator) + .at(&dest_validator_2) + .at(&redel_end) + .get(&storage, &redel_2_start) + .unwrap(); + assert_eq!(outgoing_redelegation_2, Some(redel_amount_2)); + + // All three validators should have bonds + let bonded_dest2 = bond_handle(&delegator, &dest_validator_2); + assert_eq!( + bonded_src + .get_delta_val(&storage, bond_start) + .unwrap() + .unwrap_or_default(), + bond_amount - redel_amount_1 + ); + assert_eq!( + bonded_dest + .get_delta_val(&storage, redel_end) + .unwrap() + .unwrap_or_default(), + redel_amount_1 - redel_amount_2 + ); + assert_eq!( + bonded_dest2 + .get_delta_val(&storage, redel_2_end) + .unwrap() + .unwrap_or_default(), + redel_amount_2 + ); + + // There should be no unbond entries + let unbond_src = unbond_handle(&delegator, &src_validator); + let unbond_dest = unbond_handle(&delegator, &dest_validator); + assert!(unbond_src.is_empty(&storage).unwrap()); + assert!(unbond_dest.is_empty(&storage).unwrap()); + + // The dest validator should have some total unbonded due to the second + // redelegation + let dest_total_unbonded = total_unbonded_handle(&dest_validator) + .at(&redel_2_end) + .get(&storage, &redel_end) + .unwrap(); + assert_eq!(dest_total_unbonded, Some(redel_amount_2)); + + // Delegator should have redelegated bonds due to both redelegations + let del_redelegated_bonds = delegator_redelegated_bonds_handle(&delegator); + assert_eq!( + Some(redel_amount_1 - redel_amount_2), + del_redelegated_bonds + .at(&dest_validator) + .at(&redel_end) + .at(&src_validator) + .get(&storage, &bond_start) + .unwrap() + ); + assert_eq!( + Some(redel_amount_2), + del_redelegated_bonds + .at(&dest_validator_2) + .at(&redel_2_end) + .at(&dest_validator) + .get(&storage, &redel_end) + .unwrap() + ); + + // Delegator redelegated unbonds should be empty + assert!( + delegator_redelegated_unbonds_handle(&delegator) + .is_empty(&storage) + .unwrap() + ); + + // Both the dest validator and dest validator 2 should have total + // redelegated bonds + let dest_redelegated_bonded = + validator_total_redelegated_bonded_handle(&dest_validator) + .at(&redel_end) + .at(&src_validator) + .get(&storage, &bond_start) + .unwrap() + .unwrap_or_default(); + let dest2_redelegated_bonded = + validator_total_redelegated_bonded_handle(&dest_validator_2) + .at(&redel_2_end) + .at(&dest_validator) + .get(&storage, &redel_end) + .unwrap() + .unwrap_or_default(); + assert_eq!(dest_redelegated_bonded, redel_amount_1 - redel_amount_2); + assert_eq!(dest2_redelegated_bonded, redel_amount_2); + + // Total redelegated unbonded should be empty for src_validator and + // dest_validator_2 + assert!( + validator_total_redelegated_unbonded_handle(&dest_validator_2) + .is_empty(&storage) + .unwrap() + ); + assert!( + validator_total_redelegated_unbonded_handle(&src_validator) + .is_empty(&storage) + .unwrap() + ); + + // The dest_validator should have total_redelegated unbonded + let tot_redel_unbonded = + validator_total_redelegated_unbonded_handle(&dest_validator) + .at(&redel_2_end) + .at(&redel_end) + .at(&src_validator) + .get(&storage, &bond_start) + .unwrap() + .unwrap_or_default(); + assert_eq!(tot_redel_unbonded, redel_amount_2); +} + +proptest! { + // Generate arb valid input for `test_overslashing_aux` + #![proptest_config(Config { + cases: 1, + .. Config::default() + })] + #[test] + fn test_overslashing( + + genesis_validators in arb_genesis_validators(4..5, None), + + ) { + test_overslashing_aux(genesis_validators) + } +} + +/// Test precisely that we are not overslashing, as originally discovered by Tomas in this issue: https://github.com/informalsystems/partnership-heliax/issues/74 +fn test_overslashing_aux(mut validators: Vec) { + assert_eq!(validators.len(), 4); + + let params = OwnedPosParams { + unbonding_len: 4, + ..Default::default() + }; + + let offending_stake = token::Amount::native_whole(110); + let other_stake = token::Amount::native_whole(100); + + // Set stakes so we know we will get a slashing rate between 0.5 -1.0 + validators[0].tokens = offending_stake; + validators[1].tokens = other_stake; + validators[2].tokens = other_stake; + validators[3].tokens = other_stake; + + // Get the offending validator + let validator = validators[0].address.clone(); + + // println!("\nTest inputs: {params:?}, genesis validators: + // {validators:#?}"); + let mut storage = TestWlStorage::default(); + + // Genesis + let mut current_epoch = storage.storage.block.epoch; + let params = test_init_genesis( + &mut storage, + params, + validators.clone().into_iter(), + current_epoch, + ) + .unwrap(); + storage.commit_block().unwrap(); + + // Get a delegator with some tokens + let staking_token = storage.storage.native_token.clone(); + let delegator = address::testing::gen_implicit_address(); + let amount_del = token::Amount::native_whole(5); + credit_tokens(&mut storage, &staking_token, &delegator, amount_del) + .unwrap(); + + // Delegate tokens in epoch 0 to validator + bond_tokens( + &mut storage, + Some(&delegator), + &validator, + amount_del, + current_epoch, + None, + ) + .unwrap(); + + let self_bond_epoch = current_epoch; + let delegation_epoch = current_epoch + params.pipeline_len; + + // Advance to pipeline epoch + for _ in 0..params.pipeline_len { + current_epoch = advance_epoch(&mut storage, ¶ms); + } + assert_eq!(delegation_epoch, current_epoch); + + // Find a misbehavior committed in epoch 0 + slash( + &mut storage, + ¶ms, + current_epoch, + self_bond_epoch, + 0_u64, + SlashType::DuplicateVote, + &validator, + current_epoch.next(), + ) + .unwrap(); + + // Find a misbehavior committed in current epoch + slash( + &mut storage, + ¶ms, + current_epoch, + delegation_epoch, + 0_u64, + SlashType::DuplicateVote, + &validator, + current_epoch.next(), + ) + .unwrap(); + + let processing_epoch_1 = + self_bond_epoch + params.slash_processing_epoch_offset(); + let processing_epoch_2 = + delegation_epoch + params.slash_processing_epoch_offset(); + + // Advance to processing epoch 1 + loop { + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + if current_epoch == processing_epoch_1 { + break; + } + } + + let total_stake_1 = offending_stake + 3 * other_stake; + let stake_frac = Dec::from(offending_stake) / Dec::from(total_stake_1); + let slash_rate_1 = Dec::from_str("9.0").unwrap() * stake_frac * stake_frac; + + let exp_slashed_1 = offending_stake.mul_ceil(slash_rate_1); + + // Check that the proper amount was slashed + let epoch = current_epoch.next(); + let validator_stake = + read_validator_stake(&storage, ¶ms, &validator, epoch).unwrap(); + let exp_validator_stake = offending_stake - exp_slashed_1 + amount_del; + assert_eq!(validator_stake, exp_validator_stake); + + let total_stake = read_total_stake(&storage, ¶ms, epoch).unwrap(); + let exp_total_stake = + offending_stake - exp_slashed_1 + amount_del + 3 * other_stake; + assert_eq!(total_stake, exp_total_stake); + + let self_bond_id = BondId { + source: validator.clone(), + validator: validator.clone(), + }; + let bond_amount = + crate::bond_amount(&storage, &self_bond_id, epoch).unwrap(); + let exp_bond_amount = offending_stake - exp_slashed_1; + assert_eq!(bond_amount, exp_bond_amount); + + // Advance to processing epoch 2 + loop { + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + if current_epoch == processing_epoch_2 { + break; + } + } + + let total_stake_2 = offending_stake + amount_del + 3 * other_stake; + let stake_frac = + Dec::from(offending_stake + amount_del) / Dec::from(total_stake_2); + let slash_rate_2 = Dec::from_str("9.0").unwrap() * stake_frac * stake_frac; + + let exp_slashed_from_delegation = amount_del.mul_ceil(slash_rate_2); + + // Check that the proper amount was slashed. We expect that all of the + // validator self-bond has been slashed and some of the delegation has been + // slashed due to the second infraction. + let epoch = current_epoch.next(); + + let validator_stake = + read_validator_stake(&storage, ¶ms, &validator, epoch).unwrap(); + let exp_validator_stake = amount_del - exp_slashed_from_delegation; + assert_eq!(validator_stake, exp_validator_stake); + + let total_stake = read_total_stake(&storage, ¶ms, epoch).unwrap(); + let exp_total_stake = + amount_del - exp_slashed_from_delegation + 3 * other_stake; + assert_eq!(total_stake, exp_total_stake); + + let delegation_id = BondId { + source: delegator.clone(), + validator: validator.clone(), + }; + let delegation_amount = + crate::bond_amount(&storage, &delegation_id, epoch).unwrap(); + let exp_del_amount = amount_del - exp_slashed_from_delegation; + assert_eq!(delegation_amount, exp_del_amount); + + let self_bond_amount = + crate::bond_amount(&storage, &self_bond_id, epoch).unwrap(); + let exp_bond_amount = token::Amount::zero(); + assert_eq!(self_bond_amount, exp_bond_amount); +} + +proptest! { + // Generate arb valid input for `test_slashed_bond_amount_aux` + #![proptest_config(Config { + cases: 1, + .. Config::default() + })] + #[test] + fn test_slashed_bond_amount( + + genesis_validators in arb_genesis_validators(4..5, None), + + ) { + test_slashed_bond_amount_aux(genesis_validators) + } +} + +fn test_slashed_bond_amount_aux(validators: Vec) { + let mut storage = TestWlStorage::default(); + let params = OwnedPosParams { + unbonding_len: 4, + ..Default::default() + }; + + let init_tot_stake = validators + .clone() + .into_iter() + .fold(token::Amount::zero(), |acc, v| acc + v.tokens); + let val1_init_stake = validators[0].tokens; + + let mut validators = validators; + validators[0].tokens = (init_tot_stake - val1_init_stake) / 30; + + // Genesis + let mut current_epoch = storage.storage.block.epoch; + let params = test_init_genesis( + &mut storage, + params, + validators.clone().into_iter(), + current_epoch, + ) + .unwrap(); + storage.commit_block().unwrap(); + + let validator1 = validators[0].address.clone(); + let validator2 = validators[1].address.clone(); + + // Get a delegator with some tokens + let staking_token = staking_token_address(&storage); + let delegator = address::testing::gen_implicit_address(); + let del_balance = token::Amount::from_uint(1_000_000, 0).unwrap(); + credit_tokens(&mut storage, &staking_token, &delegator, del_balance) + .unwrap(); + + // Bond to validator 1 + bond_tokens( + &mut storage, + Some(&delegator), + &validator1, + 10_000.into(), + current_epoch, + None, + ) + .unwrap(); + + // Unbond some from validator 1 + unbond_tokens( + &mut storage, + Some(&delegator), + &validator1, + 1_342.into(), + current_epoch, + false, + ) + .unwrap(); + + // Redelegate some from validator 1 -> 2 + redelegate_tokens( + &mut storage, + &delegator, + &validator1, + &validator2, + current_epoch, + 1_875.into(), + ) + .unwrap(); + + // Unbond some from validator 2 + unbond_tokens( + &mut storage, + Some(&delegator), + &validator2, + 584.into(), + current_epoch, + false, + ) + .unwrap(); + + // Advance an epoch to 1 + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + + // Bond to validator 1 + bond_tokens( + &mut storage, + Some(&delegator), + &validator1, + 384.into(), + current_epoch, + None, + ) + .unwrap(); + + // Unbond some from validator 1 + unbond_tokens( + &mut storage, + Some(&delegator), + &validator1, + 144.into(), + current_epoch, + false, + ) + .unwrap(); + + // Redelegate some from validator 1 -> 2 + redelegate_tokens( + &mut storage, + &delegator, + &validator1, + &validator2, + current_epoch, + 3_448.into(), + ) + .unwrap(); + + // Unbond some from validator 2 + unbond_tokens( + &mut storage, + Some(&delegator), + &validator2, + 699.into(), + current_epoch, + false, + ) + .unwrap(); + + // Advance an epoch to ep 2 + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + + // Bond to validator 1 + bond_tokens( + &mut storage, + Some(&delegator), + &validator1, + 4_384.into(), + current_epoch, + None, + ) + .unwrap(); + + // Redelegate some from validator 1 -> 2 + redelegate_tokens( + &mut storage, + &delegator, + &validator1, + &validator2, + current_epoch, + 1_008.into(), + ) + .unwrap(); + + // Unbond some from validator 2 + unbond_tokens( + &mut storage, + Some(&delegator), + &validator2, + 3_500.into(), + current_epoch, + false, + ) + .unwrap(); + + // Advance two epochs to ep 4 + for _ in 0..2 { + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + } + + // Find some slashes committed in various epochs + slash( + &mut storage, + ¶ms, + current_epoch, + Epoch(1), + 1_u64, + SlashType::DuplicateVote, + &validator1, + current_epoch, + ) + .unwrap(); + slash( + &mut storage, + ¶ms, + current_epoch, + Epoch(2), + 1_u64, + SlashType::DuplicateVote, + &validator1, + current_epoch, + ) + .unwrap(); + slash( + &mut storage, + ¶ms, + current_epoch, + Epoch(2), + 1_u64, + SlashType::DuplicateVote, + &validator1, + current_epoch, + ) + .unwrap(); + slash( + &mut storage, + ¶ms, + current_epoch, + Epoch(3), + 1_u64, + SlashType::DuplicateVote, + &validator1, + current_epoch, + ) + .unwrap(); + + // Advance such that these slashes are all processed + for _ in 0..params.slash_processing_epoch_offset() { + current_epoch = advance_epoch(&mut storage, ¶ms); + process_slashes(&mut storage, current_epoch).unwrap(); + } + + let pipeline_epoch = current_epoch + params.pipeline_len; + + let del_bond_amount = crate::bond_amount( + &storage, + &BondId { + source: delegator.clone(), + validator: validator1.clone(), + }, + pipeline_epoch, + ) + .unwrap_or_default(); + + let self_bond_amount = crate::bond_amount( + &storage, + &BondId { + source: validator1.clone(), + validator: validator1.clone(), + }, + pipeline_epoch, + ) + .unwrap_or_default(); + + let val_stake = crate::read_validator_stake( + &storage, + ¶ms, + &validator1, + pipeline_epoch, + ) + .unwrap(); + + let diff = val_stake - self_bond_amount - del_bond_amount; + assert!(diff <= 2.into()); +} diff --git a/proof_of_stake/src/tests/test_validator.rs b/proof_of_stake/src/tests/test_validator.rs new file mode 100644 index 0000000000..cb37c876bf --- /dev/null +++ b/proof_of_stake/src/tests/test_validator.rs @@ -0,0 +1,1191 @@ +use std::cmp::min; + +use namada_core::ledger::storage::testing::TestWlStorage; +use namada_core::ledger::storage_api::collections::lazy_map; +use namada_core::ledger::storage_api::token::credit_tokens; +use namada_core::types::address::testing::arb_established_address; +use namada_core::types::address::{self, Address, EstablishedAddressGen}; +use namada_core::types::dec::Dec; +use namada_core::types::key::testing::{ + arb_common_keypair, common_sk_from_simple_seed, +}; +use namada_core::types::key::{self, common, RefTo}; +use namada_core::types::storage::Epoch; +use namada_core::types::token; +use proptest::prelude::*; +use proptest::test_runner::Config; +// Use `RUST_LOG=info` (or another tracing level) and `--nocapture` to see +// `tracing` logs from tests +use test_log::test; + +use crate::epoched::DEFAULT_NUM_PAST_EPOCHS; +use crate::storage::{ + below_capacity_validator_set_handle, bond_handle, + consensus_validator_set_handle, find_validator_by_raw_hash, + get_num_consensus_validators, + read_below_capacity_validator_set_addresses_with_stake, + read_below_threshold_validator_set_addresses, + read_consensus_validator_set_addresses_with_stake, update_validator_deltas, + validator_consensus_key_handle, write_validator_address_raw_hash, +}; +use crate::test_utils::test_init_genesis; +use crate::tests::helpers::{ + advance_epoch, arb_params_and_genesis_validators, + get_tendermint_set_updates, +}; +use crate::types::{ + into_tm_voting_power, ConsensusValidator, GenesisValidator, Position, + ReverseOrdTokenAmount, ValidatorSetUpdate, WeightedValidator, +}; +use crate::validator_set_update::{ + insert_validator_into_validator_set, update_validator_set, +}; +use crate::{ + become_validator, bond_tokens, is_validator, staking_token_address, + unbond_tokens, withdraw_tokens, BecomeValidator, OwnedPosParams, +}; + +proptest! { + // Generate arb valid input for `test_become_validator_aux` + #![proptest_config(Config { + cases: 100, + .. Config::default() + })] + #[test] + fn test_become_validator( + + (pos_params, genesis_validators) in arb_params_and_genesis_validators(Some(5), 1..3), + new_validator in arb_established_address().prop_map(Address::Established), + new_validator_consensus_key in arb_common_keypair(), + + ) { + test_become_validator_aux(pos_params, new_validator, + new_validator_consensus_key, genesis_validators) + } +} + +/// Test validator initialization. +fn test_become_validator_aux( + params: OwnedPosParams, + new_validator: Address, + new_validator_consensus_key: common::SecretKey, + validators: Vec, +) { + // println!( + // "Test inputs: {params:?}, new validator: {new_validator}, genesis \ + // validators: {validators:#?}" + // ); + + let mut s = TestWlStorage::default(); + + // Genesis + let mut current_epoch = s.storage.block.epoch; + let params = test_init_genesis( + &mut s, + params, + validators.clone().into_iter(), + current_epoch, + ) + .unwrap(); + s.commit_block().unwrap(); + + // Advance to epoch 1 + current_epoch = advance_epoch(&mut s, ¶ms); + + let num_consensus_before = + get_num_consensus_validators(&s, current_epoch + params.pipeline_len) + .unwrap(); + let num_validators_over_thresh = validators + .iter() + .filter(|validator| { + validator.tokens >= params.validator_stake_threshold + }) + .count(); + + assert_eq!( + min( + num_validators_over_thresh as u64, + params.max_validator_slots + ), + num_consensus_before + ); + assert!(!is_validator(&s, &new_validator).unwrap()); + + // Credit the `new_validator` account + let staking_token = staking_token_address(&s); + let amount = token::Amount::from_uint(100_500_000, 0).unwrap(); + // Credit twice the amount as we're gonna bond it in delegation first, then + // self-bond + credit_tokens(&mut s, &staking_token, &new_validator, amount * 2).unwrap(); + + // Add a delegation from `new_validator` to `genesis_validator` + let genesis_validator = &validators.first().unwrap().address; + bond_tokens( + &mut s, + Some(&new_validator), + genesis_validator, + amount, + current_epoch, + None, + ) + .unwrap(); + + let consensus_key = new_validator_consensus_key.to_public(); + let protocol_sk = common_sk_from_simple_seed(0); + let protocol_key = protocol_sk.to_public(); + let eth_hot_key = key::common::PublicKey::Secp256k1( + key::testing::gen_keypair::().ref_to(), + ); + let eth_cold_key = key::common::PublicKey::Secp256k1( + key::testing::gen_keypair::().ref_to(), + ); + + // Try to become a validator - it should fail as there is a delegation + let result = become_validator( + &mut s, + BecomeValidator { + params: ¶ms, + address: &new_validator, + consensus_key: &consensus_key, + protocol_key: &protocol_key, + eth_cold_key: ð_cold_key, + eth_hot_key: ð_hot_key, + current_epoch, + commission_rate: Dec::new(5, 2).expect("Dec creation failed"), + max_commission_rate_change: Dec::new(5, 2) + .expect("Dec creation failed"), + metadata: Default::default(), + offset_opt: None, + }, + ); + assert!(result.is_err()); + assert!(!is_validator(&s, &new_validator).unwrap()); + + // Unbond the delegation + unbond_tokens( + &mut s, + Some(&new_validator), + genesis_validator, + amount, + current_epoch, + false, + ) + .unwrap(); + + // Try to become a validator account again - it should pass now + become_validator( + &mut s, + BecomeValidator { + params: ¶ms, + address: &new_validator, + consensus_key: &consensus_key, + protocol_key: &protocol_key, + eth_cold_key: ð_cold_key, + eth_hot_key: ð_hot_key, + current_epoch, + commission_rate: Dec::new(5, 2).expect("Dec creation failed"), + max_commission_rate_change: Dec::new(5, 2) + .expect("Dec creation failed"), + metadata: Default::default(), + offset_opt: None, + }, + ) + .unwrap(); + assert!(is_validator(&s, &new_validator).unwrap()); + + let num_consensus_after = + get_num_consensus_validators(&s, current_epoch + params.pipeline_len) + .unwrap(); + // The new validator is initialized with no stake and thus is in the + // below-threshold set + assert_eq!(num_consensus_before, num_consensus_after); + + // Advance to epoch 2 + current_epoch = advance_epoch(&mut s, ¶ms); + + // Self-bond to the new validator + bond_tokens(&mut s, None, &new_validator, amount, current_epoch, None) + .unwrap(); + + // Check the bond delta + let bond_handle = bond_handle(&new_validator, &new_validator); + let pipeline_epoch = current_epoch + params.pipeline_len; + let delta = bond_handle.get_delta_val(&s, pipeline_epoch).unwrap(); + assert_eq!(delta, Some(amount)); + + // Check the validator in the validator set - + // If the consensus validator slots are full and all the genesis validators + // have stake GTE the new validator's self-bond amount, the validator should + // be added to the below-capacity set, or the consensus otherwise + if params.max_validator_slots <= validators.len() as u64 + && validators + .iter() + .all(|validator| validator.tokens >= amount) + { + let set = read_below_capacity_validator_set_addresses_with_stake( + &s, + pipeline_epoch, + ) + .unwrap(); + assert!(set.into_iter().any( + |WeightedValidator { + bonded_stake, + address, + }| { + address == new_validator && bonded_stake == amount + } + )); + } else { + let set = read_consensus_validator_set_addresses_with_stake( + &s, + pipeline_epoch, + ) + .unwrap(); + assert!(set.into_iter().any( + |WeightedValidator { + bonded_stake, + address, + }| { + address == new_validator && bonded_stake == amount + } + )); + } + + // Advance to epoch 3 + current_epoch = advance_epoch(&mut s, ¶ms); + + // Unbond the self-bond + unbond_tokens(&mut s, None, &new_validator, amount, current_epoch, false) + .unwrap(); + + let withdrawable_offset = params.unbonding_len + params.pipeline_len; + + // Advance to withdrawable epoch + for _ in 0..withdrawable_offset { + current_epoch = advance_epoch(&mut s, ¶ms); + } + + // Withdraw the self-bond + withdraw_tokens(&mut s, None, &new_validator, current_epoch).unwrap(); +} + +#[test] +fn test_validator_raw_hash() { + let mut storage = TestWlStorage::default(); + let address = address::testing::established_address_1(); + let consensus_sk = key::testing::keypair_1(); + let consensus_pk = consensus_sk.to_public(); + let expected_raw_hash = key::tm_consensus_key_raw_hash(&consensus_pk); + + assert!( + find_validator_by_raw_hash(&storage, &expected_raw_hash) + .unwrap() + .is_none() + ); + write_validator_address_raw_hash(&mut storage, &address, &consensus_pk) + .unwrap(); + let found = + find_validator_by_raw_hash(&storage, &expected_raw_hash).unwrap(); + assert_eq!(found, Some(address)); +} + +#[test] +fn test_validator_sets() { + let mut s = TestWlStorage::default(); + // Only 3 consensus validator slots + let params = OwnedPosParams { + max_validator_slots: 3, + ..Default::default() + }; + let addr_seed = "seed"; + let mut address_gen = EstablishedAddressGen::new(addr_seed); + let mut sk_seed = 0; + let mut gen_validator = || { + let res = ( + address_gen.generate_address(addr_seed), + key::testing::common_sk_from_simple_seed(sk_seed).to_public(), + ); + // bump the sk seed + sk_seed += 1; + res + }; + + // Create genesis validators + let ((val1, pk1), stake1) = + (gen_validator(), token::Amount::native_whole(1)); + let ((val2, pk2), stake2) = + (gen_validator(), token::Amount::native_whole(1)); + let ((val3, pk3), stake3) = + (gen_validator(), token::Amount::native_whole(10)); + let ((val4, pk4), stake4) = + (gen_validator(), token::Amount::native_whole(1)); + let ((val5, pk5), stake5) = + (gen_validator(), token::Amount::native_whole(100)); + let ((val6, pk6), stake6) = + (gen_validator(), token::Amount::native_whole(1)); + let ((val7, pk7), stake7) = + (gen_validator(), token::Amount::native_whole(1)); + // println!("\nval1: {val1}, {pk1}, {}", stake1.to_string_native()); + // println!("val2: {val2}, {pk2}, {}", stake2.to_string_native()); + // println!("val3: {val3}, {pk3}, {}", stake3.to_string_native()); + // println!("val4: {val4}, {pk4}, {}", stake4.to_string_native()); + // println!("val5: {val5}, {pk5}, {}", stake5.to_string_native()); + // println!("val6: {val6}, {pk6}, {}", stake6.to_string_native()); + // println!("val7: {val7}, {pk7}, {}", stake7.to_string_native()); + + let start_epoch = Epoch::default(); + let epoch = start_epoch; + + let protocol_sk_1 = common_sk_from_simple_seed(0); + let protocol_sk_2 = common_sk_from_simple_seed(1); + + let params = test_init_genesis( + &mut s, + params, + [ + GenesisValidator { + address: val1.clone(), + tokens: stake1, + consensus_key: pk1.clone(), + protocol_key: protocol_sk_1.to_public(), + eth_hot_key: key::common::PublicKey::Secp256k1( + key::testing::gen_keypair::() + .ref_to(), + ), + eth_cold_key: key::common::PublicKey::Secp256k1( + key::testing::gen_keypair::() + .ref_to(), + ), + commission_rate: Dec::new(1, 1).expect("Dec creation failed"), + max_commission_rate_change: Dec::new(1, 1) + .expect("Dec creation failed"), + metadata: Default::default(), + }, + GenesisValidator { + address: val2.clone(), + tokens: stake2, + consensus_key: pk2.clone(), + protocol_key: protocol_sk_2.to_public(), + eth_hot_key: key::common::PublicKey::Secp256k1( + key::testing::gen_keypair::() + .ref_to(), + ), + eth_cold_key: key::common::PublicKey::Secp256k1( + key::testing::gen_keypair::() + .ref_to(), + ), + commission_rate: Dec::new(1, 1).expect("Dec creation failed"), + max_commission_rate_change: Dec::new(1, 1) + .expect("Dec creation failed"), + metadata: Default::default(), + }, + ] + .into_iter(), + epoch, + ) + .unwrap(); + + // A helper to insert a non-genesis validator + let insert_validator = |s: &mut TestWlStorage, + addr, + pk: &common::PublicKey, + stake: token::Amount, + epoch: Epoch| { + insert_validator_into_validator_set( + s, + ¶ms, + addr, + stake, + epoch, + params.pipeline_len, + ) + .unwrap(); + + update_validator_deltas(s, ¶ms, addr, stake.change(), epoch, None) + .unwrap(); + + // Set their consensus key (needed for + // `validator_set_update_tendermint` fn) + validator_consensus_key_handle(addr) + .set(s, pk.clone(), epoch, params.pipeline_len) + .unwrap(); + }; + + // Advance to EPOCH 1 + // + // We cannot call `get_tendermint_set_updates` for the genesis state as + // `validator_set_update_tendermint` is only called 2 blocks before the + // start of an epoch and so we need to give it a predecessor epoch (see + // `get_tendermint_set_updates`), which we cannot have on the first + // epoch. In any way, the initial validator set is given to Tendermint + // from InitChain, so `validator_set_update_tendermint` is + // not being used for it. + let epoch = advance_epoch(&mut s, ¶ms); + let pipeline_epoch = epoch + params.pipeline_len; + + // Insert another validator with the greater stake 10 NAM + insert_validator(&mut s, &val3, &pk3, stake3, epoch); + // Insert validator with stake 1 NAM + insert_validator(&mut s, &val4, &pk4, stake4, epoch); + + // Validator `val3` and `val4` will be added at pipeline offset (2) - epoch + // 3 + let val3_and_4_epoch = pipeline_epoch; + + let consensus_vals: Vec<_> = consensus_validator_set_handle() + .at(&pipeline_epoch) + .iter(&s) + .unwrap() + .map(Result::unwrap) + .collect(); + + assert_eq!(consensus_vals.len(), 3); + assert!(matches!( + &consensus_vals[0], + (lazy_map::NestedSubKey::Data { + key: stake, + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val1 && stake == &stake1 && *position == Position(0) + )); + assert!(matches!( + &consensus_vals[1], + (lazy_map::NestedSubKey::Data { + key: stake, + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val2 && stake == &stake2 && *position == Position(1) + )); + assert!(matches!( + &consensus_vals[2], + (lazy_map::NestedSubKey::Data { + key: stake, + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val3 && stake == &stake3 && *position == Position(0) + )); + + // Check tendermint validator set updates - there should be none + let tm_updates = get_tendermint_set_updates(&s, ¶ms, epoch); + assert!(tm_updates.is_empty()); + + // Advance to EPOCH 2 + let epoch = advance_epoch(&mut s, ¶ms); + let pipeline_epoch = epoch + params.pipeline_len; + + // Insert another validator with a greater stake still 1000 NAM. It should + // replace 2nd consensus validator with stake 1, which should become + // below-capacity + insert_validator(&mut s, &val5, &pk5, stake5, epoch); + // Validator `val5` will be added at pipeline offset (2) - epoch 4 + let val5_epoch = pipeline_epoch; + + let consensus_vals: Vec<_> = consensus_validator_set_handle() + .at(&pipeline_epoch) + .iter(&s) + .unwrap() + .map(Result::unwrap) + .collect(); + + assert_eq!(consensus_vals.len(), 3); + assert!(matches!( + &consensus_vals[0], + (lazy_map::NestedSubKey::Data { + key: stake, + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val1 && stake == &stake1 && *position == Position(0) + )); + assert!(matches!( + &consensus_vals[1], + (lazy_map::NestedSubKey::Data { + key: stake, + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val3 && stake == &stake3 && *position == Position(0) + )); + assert!(matches!( + &consensus_vals[2], + (lazy_map::NestedSubKey::Data { + key: stake, + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val5 && stake == &stake5 && *position == Position(0) + )); + + let below_capacity_vals: Vec<_> = below_capacity_validator_set_handle() + .at(&pipeline_epoch) + .iter(&s) + .unwrap() + .map(Result::unwrap) + .collect(); + + assert_eq!(below_capacity_vals.len(), 2); + assert!(matches!( + &below_capacity_vals[0], + (lazy_map::NestedSubKey::Data { + key: ReverseOrdTokenAmount(stake), + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val4 && stake == &stake4 && *position == Position(0) + )); + assert!(matches!( + &below_capacity_vals[1], + (lazy_map::NestedSubKey::Data { + key: ReverseOrdTokenAmount(stake), + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val2 && stake == &stake2 && *position == Position(1) + )); + + // Advance to EPOCH 3 + let epoch = advance_epoch(&mut s, ¶ms); + let pipeline_epoch = epoch + params.pipeline_len; + + // Check tendermint validator set updates + assert_eq!( + val3_and_4_epoch, epoch, + "val3 and val4 are in the validator sets now" + ); + let tm_updates = get_tendermint_set_updates(&s, ¶ms, epoch); + // `val4` is newly added below-capacity, must be skipped in updated in TM + assert_eq!(tm_updates.len(), 1); + assert_eq!( + tm_updates[0], + ValidatorSetUpdate::Consensus(ConsensusValidator { + consensus_key: pk3, + bonded_stake: stake3, + }) + ); + + // Insert another validator with a stake 1 NAM. It should be added to the + // below-capacity set + insert_validator(&mut s, &val6, &pk6, stake6, epoch); + // Validator `val6` will be added at pipeline offset (2) - epoch 5 + let val6_epoch = pipeline_epoch; + + let below_capacity_vals: Vec<_> = below_capacity_validator_set_handle() + .at(&pipeline_epoch) + .iter(&s) + .unwrap() + .map(Result::unwrap) + .collect(); + + assert_eq!(below_capacity_vals.len(), 3); + assert!(matches!( + &below_capacity_vals[0], + (lazy_map::NestedSubKey::Data { + key: ReverseOrdTokenAmount(stake), + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val4 && stake == &stake4 && *position == Position(0) + )); + assert!(matches!( + &below_capacity_vals[1], + (lazy_map::NestedSubKey::Data { + key: ReverseOrdTokenAmount(stake), + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val2 && stake == &stake2 && *position == Position(1) + )); + assert!(matches!( + &below_capacity_vals[2], + (lazy_map::NestedSubKey::Data { + key: ReverseOrdTokenAmount(stake), + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val6 && stake == &stake6 && *position == Position(2) + )); + + // Advance to EPOCH 4 + let epoch = advance_epoch(&mut s, ¶ms); + let pipeline_epoch = epoch + params.pipeline_len; + + // Check tendermint validator set updates + assert_eq!(val5_epoch, epoch, "val5 is in the validator sets now"); + let tm_updates = get_tendermint_set_updates(&s, ¶ms, epoch); + assert_eq!(tm_updates.len(), 2); + assert_eq!( + tm_updates[0], + ValidatorSetUpdate::Consensus(ConsensusValidator { + consensus_key: pk5, + bonded_stake: stake5, + }) + ); + assert_eq!(tm_updates[1], ValidatorSetUpdate::Deactivated(pk2)); + + // Unbond some stake from val1, it should be be swapped with the greatest + // below-capacity validator val2 into the below-capacity set. The stake of + // val1 will go below 1 NAM, which is the validator_stake_threshold, so it + // will enter the below-threshold validator set. + let unbond = token::Amount::from_uint(500_000, 0).unwrap(); + // let stake1 = stake1 - unbond; + + // Because `update_validator_set` and `update_validator_deltas` are + // effective from pipeline offset, we use pipeline epoch for the rest of the + // checks + update_validator_set(&mut s, ¶ms, &val1, -unbond.change(), epoch, None) + .unwrap(); + update_validator_deltas( + &mut s, + ¶ms, + &val1, + -unbond.change(), + epoch, + None, + ) + .unwrap(); + // Epoch 6 + let val1_unbond_epoch = pipeline_epoch; + + let consensus_vals: Vec<_> = consensus_validator_set_handle() + .at(&pipeline_epoch) + .iter(&s) + .unwrap() + .map(Result::unwrap) + .collect(); + + assert_eq!(consensus_vals.len(), 3); + assert!(matches!( + &consensus_vals[0], + (lazy_map::NestedSubKey::Data { + key: stake, + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val4 && stake == &stake4 && *position == Position(0) + )); + assert!(matches!( + &consensus_vals[1], + (lazy_map::NestedSubKey::Data { + key: stake, + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val3 && stake == &stake3 && *position == Position(0) + )); + assert!(matches!( + &consensus_vals[2], + (lazy_map::NestedSubKey::Data { + key: stake, + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val5 && stake == &stake5 && *position == Position(0) + )); + + let below_capacity_vals: Vec<_> = below_capacity_validator_set_handle() + .at(&pipeline_epoch) + .iter(&s) + .unwrap() + .map(Result::unwrap) + .collect(); + + assert_eq!(below_capacity_vals.len(), 2); + assert!(matches!( + &below_capacity_vals[0], + (lazy_map::NestedSubKey::Data { + key: ReverseOrdTokenAmount(stake), + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val2 && stake == &stake2 && *position == Position(1) + )); + assert!(matches!( + &below_capacity_vals[1], + (lazy_map::NestedSubKey::Data { + key: ReverseOrdTokenAmount(stake), + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val6 && stake == &stake6 && *position == Position(2) + )); + + let below_threshold_vals = + read_below_threshold_validator_set_addresses(&s, pipeline_epoch) + .unwrap() + .into_iter() + .collect::>(); + + assert_eq!(below_threshold_vals.len(), 1); + assert_eq!(&below_threshold_vals[0], &val1); + + // Advance to EPOCH 5 + let epoch = advance_epoch(&mut s, ¶ms); + let pipeline_epoch = epoch + params.pipeline_len; + + // Check tendermint validator set updates + assert_eq!(val6_epoch, epoch, "val6 is in the validator sets now"); + let tm_updates = get_tendermint_set_updates(&s, ¶ms, epoch); + assert!(tm_updates.is_empty()); + + // Insert another validator with stake 1 - it should be added to below + // capacity set + insert_validator(&mut s, &val7, &pk7, stake7, epoch); + // Epoch 7 + let val7_epoch = pipeline_epoch; + + let consensus_vals: Vec<_> = consensus_validator_set_handle() + .at(&pipeline_epoch) + .iter(&s) + .unwrap() + .map(Result::unwrap) + .collect(); + + assert_eq!(consensus_vals.len(), 3); + assert!(matches!( + &consensus_vals[0], + (lazy_map::NestedSubKey::Data { + key: stake, + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val4 && stake == &stake4 && *position == Position(0) + )); + assert!(matches!( + &consensus_vals[1], + (lazy_map::NestedSubKey::Data { + key: stake, + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val3 && stake == &stake3 && *position == Position(0) + )); + assert!(matches!( + &consensus_vals[2], + (lazy_map::NestedSubKey::Data { + key: stake, + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val5 && stake == &stake5 && *position == Position(0) + )); + + let below_capacity_vals: Vec<_> = below_capacity_validator_set_handle() + .at(&pipeline_epoch) + .iter(&s) + .unwrap() + .map(Result::unwrap) + .collect(); + + assert_eq!(below_capacity_vals.len(), 3); + assert!(matches!( + &below_capacity_vals[0], + (lazy_map::NestedSubKey::Data { + key: ReverseOrdTokenAmount(stake), + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val2 && stake == &stake2 && *position == Position(1) + )); + assert!(matches!( + &below_capacity_vals[1], + (lazy_map::NestedSubKey::Data { + key: ReverseOrdTokenAmount(stake), + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val6 && stake == &stake6 && *position == Position(2) + )); + assert!(matches!( + &below_capacity_vals[2], + ( + lazy_map::NestedSubKey::Data { + key: ReverseOrdTokenAmount(stake), + nested_sub_key: lazy_map::SubKey::Data(position), + }, + address + ) + if address == &val7 && stake == &stake7 && *position == Position(3) + )); + + let below_threshold_vals = + read_below_threshold_validator_set_addresses(&s, pipeline_epoch) + .unwrap() + .into_iter() + .collect::>(); + + assert_eq!(below_threshold_vals.len(), 1); + assert_eq!(&below_threshold_vals[0], &val1); + + // Advance to EPOCH 6 + let epoch = advance_epoch(&mut s, ¶ms); + let pipeline_epoch = epoch + params.pipeline_len; + + // Check tendermint validator set updates + assert_eq!(val1_unbond_epoch, epoch, "val1's unbond is applied now"); + let tm_updates = get_tendermint_set_updates(&s, ¶ms, epoch); + assert_eq!(tm_updates.len(), 2); + assert_eq!( + tm_updates[0], + ValidatorSetUpdate::Consensus(ConsensusValidator { + consensus_key: pk4.clone(), + bonded_stake: stake4, + }) + ); + assert_eq!(tm_updates[1], ValidatorSetUpdate::Deactivated(pk1)); + + // Bond some stake to val6, it should be be swapped with the lowest + // consensus validator val2 into the consensus set + let bond = token::Amount::from_uint(500_000, 0).unwrap(); + let stake6 = stake6 + bond; + + update_validator_set(&mut s, ¶ms, &val6, bond.change(), epoch, None) + .unwrap(); + update_validator_deltas(&mut s, ¶ms, &val6, bond.change(), epoch, None) + .unwrap(); + let val6_bond_epoch = pipeline_epoch; + + let consensus_vals: Vec<_> = consensus_validator_set_handle() + .at(&pipeline_epoch) + .iter(&s) + .unwrap() + .map(Result::unwrap) + .collect(); + + assert_eq!(consensus_vals.len(), 3); + assert!(matches!( + &consensus_vals[0], + (lazy_map::NestedSubKey::Data { + key: stake, + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val6 && stake == &stake6 && *position == Position(0) + )); + assert!(matches!( + &consensus_vals[1], + (lazy_map::NestedSubKey::Data { + key: stake, + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val3 && stake == &stake3 && *position == Position(0) + )); + assert!(matches!( + &consensus_vals[2], + (lazy_map::NestedSubKey::Data { + key: stake, + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val5 && stake == &stake5 && *position == Position(0) + )); + + let below_capacity_vals: Vec<_> = below_capacity_validator_set_handle() + .at(&pipeline_epoch) + .iter(&s) + .unwrap() + .map(Result::unwrap) + .collect(); + + assert_eq!(below_capacity_vals.len(), 3); + + assert!(matches!( + &below_capacity_vals[0], + (lazy_map::NestedSubKey::Data { + key: ReverseOrdTokenAmount(stake), + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val2 && stake == &stake2 && *position == Position(1) + )); + assert!(matches!( + &below_capacity_vals[1], + (lazy_map::NestedSubKey::Data { + key: ReverseOrdTokenAmount(stake), + nested_sub_key: lazy_map::SubKey::Data(position), + }, address) + if address == &val7 && stake == &stake7 && *position == Position(3) + )); + assert!(matches!( + &below_capacity_vals[2], + ( + lazy_map::NestedSubKey::Data { + key: ReverseOrdTokenAmount(stake), + nested_sub_key: lazy_map::SubKey::Data(position), + }, + address + ) + if address == &val4 && stake == &stake4 && *position == Position(4) + )); + + let below_threshold_vals = + read_below_threshold_validator_set_addresses(&s, pipeline_epoch) + .unwrap() + .into_iter() + .collect::>(); + + assert_eq!(below_threshold_vals.len(), 1); + assert_eq!(&below_threshold_vals[0], &val1); + + // Advance to EPOCH 7 + let epoch = advance_epoch(&mut s, ¶ms); + assert_eq!(val7_epoch, epoch, "val6 is in the validator sets now"); + + // Check tendermint validator set updates + let tm_updates = get_tendermint_set_updates(&s, ¶ms, epoch); + assert!(tm_updates.is_empty()); + + // Advance to EPOCH 8 + let epoch = advance_epoch(&mut s, ¶ms); + + // Check tendermint validator set updates + assert_eq!(val6_bond_epoch, epoch, "val5's bond is applied now"); + let tm_updates = get_tendermint_set_updates(&s, ¶ms, epoch); + // dbg!(&tm_updates); + assert_eq!(tm_updates.len(), 2); + assert_eq!( + tm_updates[0], + ValidatorSetUpdate::Consensus(ConsensusValidator { + consensus_key: pk6, + bonded_stake: stake6, + }) + ); + assert_eq!(tm_updates[1], ValidatorSetUpdate::Deactivated(pk4)); + + // Check that the below-capacity validator set was purged for the old epochs + // but that the consensus_validator_set was not + let last_epoch = epoch; + for e in Epoch::iter_bounds_inclusive( + start_epoch, + last_epoch + .sub_or_default(Epoch(DEFAULT_NUM_PAST_EPOCHS)) + .sub_or_default(Epoch(1)), + ) { + assert!( + !consensus_validator_set_handle() + .at(&e) + .is_empty(&s) + .unwrap() + ); + assert!( + below_capacity_validator_set_handle() + .at(&e) + .is_empty(&s) + .unwrap() + ); + } +} + +/// When a consensus set validator with 0 voting power adds a bond in the same +/// epoch as another below-capacity set validator with 0 power, but who adds +/// more bonds than the validator who is in the consensus set, they get swapped +/// in the sets. But if both of their new voting powers are still 0 after +/// bonding, the newly below-capacity validator must not be given to tendermint +/// with 0 voting power, because it wasn't it its set before +#[test] +fn test_validator_sets_swap() { + let mut s = TestWlStorage::default(); + // Only 2 consensus validator slots + let params = OwnedPosParams { + max_validator_slots: 2, + // Set the stake threshold to 0 so no validators are in the + // below-threshold set + validator_stake_threshold: token::Amount::zero(), + // Set 0.1 votes per token + tm_votes_per_token: Dec::new(1, 1).expect("Dec creation failed"), + ..Default::default() + }; + + let addr_seed = "seed"; + let mut address_gen = EstablishedAddressGen::new(addr_seed); + let mut sk_seed = 0; + let mut gen_validator = || { + let res = ( + address_gen.generate_address(addr_seed), + key::testing::common_sk_from_simple_seed(sk_seed).to_public(), + ); + // bump the sk seed + sk_seed += 1; + res + }; + + // Start with two genesis validators, one with 1 voting power and other 0 + let epoch = Epoch::default(); + // 1M voting power + let ((val1, pk1), stake1) = + (gen_validator(), token::Amount::native_whole(10)); + // 0 voting power + let ((val2, pk2), stake2) = + (gen_validator(), token::Amount::from_uint(5, 0).unwrap()); + // 0 voting power + let ((val3, pk3), stake3) = + (gen_validator(), token::Amount::from_uint(5, 0).unwrap()); + // println!("val1: {val1}, {pk1}, {}", stake1.to_string_native()); + // println!("val2: {val2}, {pk2}, {}", stake2.to_string_native()); + // println!("val3: {val3}, {pk3}, {}", stake3.to_string_native()); + + let protocol_sk_1 = common_sk_from_simple_seed(0); + let protocol_sk_2 = common_sk_from_simple_seed(1); + + let params = test_init_genesis( + &mut s, + params, + [ + GenesisValidator { + address: val1, + tokens: stake1, + consensus_key: pk1, + protocol_key: protocol_sk_1.to_public(), + eth_hot_key: key::common::PublicKey::Secp256k1( + key::testing::gen_keypair::() + .ref_to(), + ), + eth_cold_key: key::common::PublicKey::Secp256k1( + key::testing::gen_keypair::() + .ref_to(), + ), + commission_rate: Dec::new(1, 1).expect("Dec creation failed"), + max_commission_rate_change: Dec::new(1, 1) + .expect("Dec creation failed"), + metadata: Default::default(), + }, + GenesisValidator { + address: val2.clone(), + tokens: stake2, + consensus_key: pk2, + protocol_key: protocol_sk_2.to_public(), + eth_hot_key: key::common::PublicKey::Secp256k1( + key::testing::gen_keypair::() + .ref_to(), + ), + eth_cold_key: key::common::PublicKey::Secp256k1( + key::testing::gen_keypair::() + .ref_to(), + ), + commission_rate: Dec::new(1, 1).expect("Dec creation failed"), + max_commission_rate_change: Dec::new(1, 1) + .expect("Dec creation failed"), + metadata: Default::default(), + }, + ] + .into_iter(), + epoch, + ) + .unwrap(); + + // A helper to insert a non-genesis validator + let insert_validator = |s: &mut TestWlStorage, + addr, + pk: &common::PublicKey, + stake: token::Amount, + epoch: Epoch| { + insert_validator_into_validator_set( + s, + ¶ms, + addr, + stake, + epoch, + params.pipeline_len, + ) + .unwrap(); + + update_validator_deltas(s, ¶ms, addr, stake.change(), epoch, None) + .unwrap(); + + // Set their consensus key (needed for + // `validator_set_update_tendermint` fn) + validator_consensus_key_handle(addr) + .set(s, pk.clone(), epoch, params.pipeline_len) + .unwrap(); + }; + + // Advance to EPOCH 1 + let epoch = advance_epoch(&mut s, ¶ms); + let pipeline_epoch = epoch + params.pipeline_len; + + // Insert another validator with 0 voting power + insert_validator(&mut s, &val3, &pk3, stake3, epoch); + + assert_eq!(stake2, stake3); + + // Add 2 bonds, one for val2 and greater one for val3 + let bonds_epoch_1 = pipeline_epoch; + let bond2 = token::Amount::from_uint(1, 0).unwrap(); + let stake2 = stake2 + bond2; + let bond3 = token::Amount::from_uint(4, 0).unwrap(); + let stake3 = stake3 + bond3; + + assert!(stake2 < stake3); + assert_eq!(into_tm_voting_power(params.tm_votes_per_token, stake2), 0); + assert_eq!(into_tm_voting_power(params.tm_votes_per_token, stake3), 0); + + update_validator_set(&mut s, ¶ms, &val2, bond2.change(), epoch, None) + .unwrap(); + update_validator_deltas( + &mut s, + ¶ms, + &val2, + bond2.change(), + epoch, + None, + ) + .unwrap(); + + update_validator_set(&mut s, ¶ms, &val3, bond3.change(), epoch, None) + .unwrap(); + update_validator_deltas( + &mut s, + ¶ms, + &val3, + bond3.change(), + epoch, + None, + ) + .unwrap(); + + // Advance to EPOCH 2 + let epoch = advance_epoch(&mut s, ¶ms); + let pipeline_epoch = epoch + params.pipeline_len; + + // Add 2 more bonds, same amount for `val2` and val3` + let bonds_epoch_2 = pipeline_epoch; + let bonds = token::Amount::native_whole(1); + let stake2 = stake2 + bonds; + let stake3 = stake3 + bonds; + assert!(stake2 < stake3); + assert_eq!( + into_tm_voting_power(params.tm_votes_per_token, stake2), + into_tm_voting_power(params.tm_votes_per_token, stake3) + ); + + update_validator_set(&mut s, ¶ms, &val2, bonds.change(), epoch, None) + .unwrap(); + update_validator_deltas( + &mut s, + ¶ms, + &val2, + bonds.change(), + epoch, + None, + ) + .unwrap(); + + update_validator_set(&mut s, ¶ms, &val3, bonds.change(), epoch, None) + .unwrap(); + update_validator_deltas( + &mut s, + ¶ms, + &val3, + bonds.change(), + epoch, + None, + ) + .unwrap(); + + // Advance to EPOCH 3 + let epoch = advance_epoch(&mut s, ¶ms); + + // Check tendermint validator set updates + assert_eq!(bonds_epoch_1, epoch); + let tm_updates = get_tendermint_set_updates(&s, ¶ms, epoch); + // `val2` must not be given to tendermint - even though it was in the + // consensus set, its voting power was 0, so it wasn't in TM set before the + // bond + assert!(tm_updates.is_empty()); + + // Advance to EPOCH 4 + let epoch = advance_epoch(&mut s, ¶ms); + + // Check tendermint validator set updates + assert_eq!(bonds_epoch_2, epoch); + let tm_updates = get_tendermint_set_updates(&s, ¶ms, epoch); + // dbg!(&tm_updates); + assert_eq!(tm_updates.len(), 1); + // `val2` must not be given to tendermint as it was and still is below + // capacity + assert_eq!( + tm_updates[0], + ValidatorSetUpdate::Consensus(ConsensusValidator { + consensus_key: pk3, + bonded_stake: stake3, + }) + ); +} diff --git a/proof_of_stake/src/types.rs b/proof_of_stake/src/types/mod.rs similarity index 98% rename from proof_of_stake/src/types.rs rename to proof_of_stake/src/types/mod.rs index 0149f2d365..2d297bd72f 100644 --- a/proof_of_stake/src/types.rs +++ b/proof_of_stake/src/types/mod.rs @@ -590,6 +590,17 @@ pub struct VoteInfo { pub validator_vp: u64, } +/// Temp: In quint this is from `ResultUnbondTx` field `resultSlashing: {sum: +/// int, epochMap: Epoch -> int}` +#[derive(Debug, Default)] +pub struct ResultSlashing { + /// The token amount unbonded from the validator stake after accounting for + /// slashes + pub sum: token::Amount, + /// Map from bond start epoch to token amount after slashing + pub epoch_map: BTreeMap, +} + /// Bonds and unbonds with all details (slashes and rewards, if any) /// grouped by their bond IDs. pub type BondsAndUnbondsDetails = HashMap; diff --git a/proof_of_stake/src/validator_set_update.rs b/proof_of_stake/src/validator_set_update.rs new file mode 100644 index 0000000000..9ad53c20b1 --- /dev/null +++ b/proof_of_stake/src/validator_set_update.rs @@ -0,0 +1,1069 @@ +//! Validator set updates + +use std::collections::{HashMap, HashSet}; + +use namada_core::ledger::storage_api::collections::lazy_map::{ + NestedSubKey, SubKey, +}; +use namada_core::ledger::storage_api::{self, StorageRead, StorageWrite}; +use namada_core::types::address::Address; +use namada_core::types::key::PublicKeyTmRawHash; +use namada_core::types::storage::Epoch; +use namada_core::types::token; +use once_cell::unsync::Lazy; + +use crate::storage::{ + below_capacity_validator_set_handle, consensus_validator_set_handle, + get_num_consensus_validators, read_validator_stake, + validator_addresses_handle, validator_consensus_key_handle, + validator_set_positions_handle, validator_state_handle, +}; +use crate::types::{ + into_tm_voting_power, BelowCapacityValidatorSet, ConsensusValidator, + ConsensusValidatorSet, Position, ReverseOrdTokenAmount, + ValidatorPositionAddresses, ValidatorSetUpdate, ValidatorState, +}; +use crate::PosParams; + +/// Update validator set at the pipeline epoch when a validator receives a new +/// bond and when its bond is unbonded (self-bond or delegation). +pub fn update_validator_set( + storage: &mut S, + params: &PosParams, + validator: &Address, + token_change: token::Change, + current_epoch: Epoch, + offset: Option, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + if token_change.is_zero() { + return Ok(()); + } + let offset = offset.unwrap_or(params.pipeline_len); + let epoch = current_epoch + offset; + tracing::debug!( + "Update epoch for validator set: {epoch}, validator: {validator}" + ); + let consensus_validator_set = consensus_validator_set_handle(); + let below_capacity_validator_set = below_capacity_validator_set_handle(); + + // Validator sets at the pipeline offset + let consensus_val_handle = consensus_validator_set.at(&epoch); + let below_capacity_val_handle = below_capacity_validator_set.at(&epoch); + + let tokens_pre = read_validator_stake(storage, params, validator, epoch)?; + + let tokens_post = tokens_pre + .change() + .checked_add(&token_change) + .expect("Post-validator set update token amount has overflowed"); + debug_assert!(tokens_post.non_negative()); + let tokens_post = token::Amount::from_change(tokens_post); + + // If token amounts both before and after the action are below the threshold + // stake, do nothing + if tokens_pre < params.validator_stake_threshold + && tokens_post < params.validator_stake_threshold + { + return Ok(()); + } + + // The position is only set when the validator is in consensus or + // below_capacity set (not in below_threshold set) + let position = + read_validator_set_position(storage, validator, epoch, params)?; + if let Some(position) = position { + let consensus_vals_pre = consensus_val_handle.at(&tokens_pre); + + let in_consensus = if consensus_vals_pre.contains(storage, &position)? { + let val_address = consensus_vals_pre.get(storage, &position)?; + debug_assert!(val_address.is_some()); + val_address == Some(validator.clone()) + } else { + false + }; + + if in_consensus { + // It's initially consensus + tracing::debug!("Target validator is consensus"); + + // First remove the consensus validator + consensus_vals_pre.remove(storage, &position)?; + + let max_below_capacity_validator_amount = + get_max_below_capacity_validator_amount( + &below_capacity_val_handle, + storage, + )? + .unwrap_or_default(); + + if tokens_post < params.validator_stake_threshold { + tracing::debug!( + "Demoting this validator to the below-threshold set" + ); + // Set the validator state as below-threshold + validator_state_handle(validator).set( + storage, + ValidatorState::BelowThreshold, + current_epoch, + offset, + )?; + + // Remove the validator's position from storage + validator_set_positions_handle() + .at(&epoch) + .remove(storage, validator)?; + + // Promote the next below-cap validator if there is one + if let Some(max_bc_amount) = + get_max_below_capacity_validator_amount( + &below_capacity_val_handle, + storage, + )? + { + // Remove the max below-capacity validator first + let below_capacity_vals_max = + below_capacity_val_handle.at(&max_bc_amount.into()); + let lowest_position = + find_first_position(&below_capacity_vals_max, storage)? + .unwrap(); + let removed_max_below_capacity = below_capacity_vals_max + .remove(storage, &lowest_position)? + .expect("Must have been removed"); + + // Insert the previous max below-capacity validator into the + // consensus set + insert_validator_into_set( + &consensus_val_handle.at(&max_bc_amount), + storage, + &epoch, + &removed_max_below_capacity, + )?; + validator_state_handle(&removed_max_below_capacity).set( + storage, + ValidatorState::Consensus, + current_epoch, + offset, + )?; + } + } else if tokens_post < max_below_capacity_validator_amount { + tracing::debug!( + "Demoting this validator to the below-capacity set and \ + promoting another to the consensus set" + ); + // Place the validator into the below-capacity set and promote + // the lowest position max below-capacity + // validator. + + // Remove the max below-capacity validator first + let below_capacity_vals_max = below_capacity_val_handle + .at(&max_below_capacity_validator_amount.into()); + let lowest_position = + find_first_position(&below_capacity_vals_max, storage)? + .unwrap(); + let removed_max_below_capacity = below_capacity_vals_max + .remove(storage, &lowest_position)? + .expect("Must have been removed"); + + // Insert the previous max below-capacity validator into the + // consensus set + insert_validator_into_set( + &consensus_val_handle + .at(&max_below_capacity_validator_amount), + storage, + &epoch, + &removed_max_below_capacity, + )?; + validator_state_handle(&removed_max_below_capacity).set( + storage, + ValidatorState::Consensus, + current_epoch, + offset, + )?; + + // Insert the current validator into the below-capacity set + insert_validator_into_set( + &below_capacity_val_handle.at(&tokens_post.into()), + storage, + &epoch, + validator, + )?; + validator_state_handle(validator).set( + storage, + ValidatorState::BelowCapacity, + current_epoch, + offset, + )?; + } else { + tracing::debug!("Validator remains in consensus set"); + // The current validator should remain in the consensus set - + // place it into a new position + insert_validator_into_set( + &consensus_val_handle.at(&tokens_post), + storage, + &epoch, + validator, + )?; + } + } else { + // It's initially below-capacity + tracing::debug!("Target validator is below-capacity"); + + let below_capacity_vals_pre = + below_capacity_val_handle.at(&tokens_pre.into()); + let removed = below_capacity_vals_pre.remove(storage, &position)?; + debug_assert!(removed.is_some()); + debug_assert_eq!(&removed.unwrap(), validator); + + let min_consensus_validator_amount = + get_min_consensus_validator_amount( + &consensus_val_handle, + storage, + )?; + + if tokens_post > min_consensus_validator_amount { + // Place the validator into the consensus set and demote the + // last position min consensus validator to the + // below-capacity set + tracing::debug!( + "Inserting validator into the consensus set and demoting \ + a consensus validator to the below-capacity set" + ); + + insert_into_consensus_and_demote_to_below_cap( + storage, + validator, + tokens_post, + min_consensus_validator_amount, + current_epoch, + offset, + &consensus_val_handle, + &below_capacity_val_handle, + )?; + } else if tokens_post >= params.validator_stake_threshold { + tracing::debug!("Validator remains in below-capacity set"); + // The current validator should remain in the below-capacity set + insert_validator_into_set( + &below_capacity_val_handle.at(&tokens_post.into()), + storage, + &epoch, + validator, + )?; + validator_state_handle(validator).set( + storage, + ValidatorState::BelowCapacity, + current_epoch, + offset, + )?; + } else { + // The current validator is demoted to the below-threshold set + tracing::debug!( + "Demoting this validator to the below-threshold set" + ); + + validator_state_handle(validator).set( + storage, + ValidatorState::BelowThreshold, + current_epoch, + offset, + )?; + + // Remove the validator's position from storage + validator_set_positions_handle() + .at(&epoch) + .remove(storage, validator)?; + } + } + } else { + // At non-zero offset (0 is genesis only) + if offset > 0 { + // If there is no position at pipeline offset, then the validator + // must be in the below-threshold set + debug_assert!(tokens_pre < params.validator_stake_threshold); + } + tracing::debug!("Target validator is below-threshold"); + + // Move the validator into the appropriate set + let num_consensus_validators = + get_num_consensus_validators(storage, epoch)?; + if num_consensus_validators < params.max_validator_slots { + // Just insert into the consensus set + tracing::debug!("Inserting validator into the consensus set"); + + insert_validator_into_set( + &consensus_val_handle.at(&tokens_post), + storage, + &epoch, + validator, + )?; + validator_state_handle(validator).set( + storage, + ValidatorState::Consensus, + current_epoch, + offset, + )?; + } else { + let min_consensus_validator_amount = + get_min_consensus_validator_amount( + &consensus_val_handle, + storage, + )?; + if tokens_post > min_consensus_validator_amount { + // Insert this validator into consensus and demote one into the + // below-capacity + tracing::debug!( + "Inserting validator into the consensus set and demoting \ + a consensus validator to the below-capacity set" + ); + + insert_into_consensus_and_demote_to_below_cap( + storage, + validator, + tokens_post, + min_consensus_validator_amount, + current_epoch, + offset, + &consensus_val_handle, + &below_capacity_val_handle, + )?; + } else { + // Insert this validator into below-capacity + tracing::debug!( + "Inserting validator into the below-capacity set" + ); + + insert_validator_into_set( + &below_capacity_val_handle.at(&tokens_post.into()), + storage, + &epoch, + validator, + )?; + validator_state_handle(validator).set( + storage, + ValidatorState::BelowCapacity, + current_epoch, + offset, + )?; + } + } + } + + Ok(()) +} + +/// Insert the new validator into the right validator set (depending on its +/// stake) +pub fn insert_validator_into_validator_set( + storage: &mut S, + params: &PosParams, + address: &Address, + stake: token::Amount, + current_epoch: Epoch, + offset: u64, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let target_epoch = current_epoch + offset; + let consensus_set = consensus_validator_set_handle().at(&target_epoch); + let below_cap_set = below_capacity_validator_set_handle().at(&target_epoch); + + let num_consensus_validators = + get_num_consensus_validators(storage, target_epoch)?; + + if stake < params.validator_stake_threshold { + validator_state_handle(address).set( + storage, + ValidatorState::BelowThreshold, + current_epoch, + offset, + )?; + } else if num_consensus_validators < params.max_validator_slots { + insert_validator_into_set( + &consensus_set.at(&stake), + storage, + &target_epoch, + address, + )?; + validator_state_handle(address).set( + storage, + ValidatorState::Consensus, + current_epoch, + offset, + )?; + } else { + // Check to see if the current genesis validator should replace one + // already in the consensus set + let min_consensus_amount = + get_min_consensus_validator_amount(&consensus_set, storage)?; + if stake > min_consensus_amount { + // Swap this genesis validator in and demote the last min consensus + // validator + let min_consensus_handle = consensus_set.at(&min_consensus_amount); + // Remove last min consensus validator + let last_min_consensus_position = + find_last_position(&min_consensus_handle, storage)?.expect( + "There must be always be at least 1 consensus validator", + ); + let removed = min_consensus_handle + .remove(storage, &last_min_consensus_position)? + .expect( + "There must be always be at least 1 consensus validator", + ); + // Insert last min consensus validator into the below-capacity set + insert_validator_into_set( + &below_cap_set.at(&min_consensus_amount.into()), + storage, + &target_epoch, + &removed, + )?; + validator_state_handle(&removed).set( + storage, + ValidatorState::BelowCapacity, + current_epoch, + offset, + )?; + // Insert the current genesis validator into the consensus set + insert_validator_into_set( + &consensus_set.at(&stake), + storage, + &target_epoch, + address, + )?; + // Update and set the validator states + validator_state_handle(address).set( + storage, + ValidatorState::Consensus, + current_epoch, + offset, + )?; + } else { + // Insert the current genesis validator into the below-capacity set + insert_validator_into_set( + &below_cap_set.at(&stake.into()), + storage, + &target_epoch, + address, + )?; + validator_state_handle(address).set( + storage, + ValidatorState::BelowCapacity, + current_epoch, + offset, + )?; + } + } + Ok(()) +} + +/// Remove a validator from the consensus validator set +pub fn remove_consensus_validator( + storage: &mut S, + params: &PosParams, + epoch: Epoch, + validator: &Address, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let stake = read_validator_stake(storage, params, validator, epoch)?; + let consensus_set = consensus_validator_set_handle().at(&epoch).at(&stake); + let val_position = validator_set_positions_handle() + .at(&epoch) + .get(storage, validator)? + .expect("Could not find validator's position in storage."); + + // Removal + let removed = consensus_set.remove(storage, &val_position)?; + debug_assert_eq!(removed, Some(validator.clone())); + + validator_set_positions_handle() + .at(&epoch) + .remove(storage, validator)?; + + Ok(()) +} + +/// Remove a validator from the below-capacity set +pub fn remove_below_capacity_validator( + storage: &mut S, + params: &PosParams, + epoch: Epoch, + validator: &Address, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let stake = read_validator_stake(storage, params, validator, epoch)?; + let below_cap_set = below_capacity_validator_set_handle() + .at(&epoch) + .at(&stake.into()); + let val_position = validator_set_positions_handle() + .at(&epoch) + .get(storage, validator)? + .expect("Could not find validator's position in storage."); + + // Removal + let removed = below_cap_set.remove(storage, &val_position)?; + debug_assert_eq!(removed, Some(validator.clone())); + + validator_set_positions_handle() + .at(&epoch) + .remove(storage, validator)?; + + Ok(()) +} + +/// Promote the next below-capacity validator to the consensus validator set, +/// determined as the validator in the below-capacity set with the largest stake +/// and the lowest `Position`. Assumes that there is adequate space within the +/// consensus set already. +pub fn promote_next_below_capacity_validator_to_consensus( + storage: &mut S, + epoch: Epoch, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let below_cap_set = below_capacity_validator_set_handle().at(&epoch); + let max_below_capacity_amount = + get_max_below_capacity_validator_amount(&below_cap_set, storage)?; + + if let Some(max_below_capacity_amount) = max_below_capacity_amount { + let max_bc_vals = below_cap_set.at(&max_below_capacity_amount.into()); + let position_to_promote = find_first_position(&max_bc_vals, storage)? + .expect("Should be at least one below-capacity validator"); + + let promoted_validator = max_bc_vals + .remove(storage, &position_to_promote)? + .expect("Should have returned a removed validator."); + + insert_validator_into_set( + &consensus_validator_set_handle() + .at(&epoch) + .at(&max_below_capacity_amount), + storage, + &epoch, + &promoted_validator, + )?; + validator_state_handle(&promoted_validator).set( + storage, + ValidatorState::Consensus, + epoch, + 0, + )?; + } + + Ok(()) +} + +/// Communicate imminent validator set updates to Tendermint. This function is +/// called two blocks before the start of a new epoch because Tendermint +/// validator updates become active two blocks after the updates are submitted. +pub fn validator_set_update_tendermint( + storage: &S, + params: &PosParams, + current_epoch: Epoch, + f: impl FnMut(ValidatorSetUpdate) -> T, +) -> storage_api::Result> +where + S: StorageRead, +{ + tracing::debug!("Communicating validator set updates to Tendermint."); + // Because this is called 2 blocks before a start on an epoch, we're gonna + // give Tendermint updates for the next epoch + let next_epoch = current_epoch.next(); + + let new_consensus_validator_handle = + consensus_validator_set_handle().at(&next_epoch); + let prev_consensus_validator_handle = + consensus_validator_set_handle().at(¤t_epoch); + + let new_consensus_validators = new_consensus_validator_handle + .iter(storage)? + .map(|validator| { + let ( + NestedSubKey::Data { + key: new_stake, + nested_sub_key: _, + }, + address, + ) = validator.unwrap(); + + tracing::debug!( + "Consensus validator address {address}, stake {}", + new_stake.to_string_native() + ); + + let new_consensus_key = validator_consensus_key_handle(&address) + .get(storage, next_epoch, params) + .unwrap() + .unwrap(); + + let old_consensus_key = validator_consensus_key_handle(&address) + .get(storage, current_epoch, params) + .unwrap(); + + // Check if the validator was consensus in the previous epoch with + // the same stake. If so, no updated is needed. + // Look up previous state and prev and current voting powers + if !prev_consensus_validator_handle.is_empty(storage).unwrap() { + let prev_state = validator_state_handle(&address) + .get(storage, current_epoch, params) + .unwrap(); + let prev_tm_voting_power = Lazy::new(|| { + let prev_validator_stake = read_validator_stake( + storage, + params, + &address, + current_epoch, + ) + .unwrap(); + into_tm_voting_power( + params.tm_votes_per_token, + prev_validator_stake, + ) + }); + let new_tm_voting_power = Lazy::new(|| { + into_tm_voting_power(params.tm_votes_per_token, new_stake) + }); + + // If it was in `Consensus` before and voting power has not + // changed, skip the update + if matches!(prev_state, Some(ValidatorState::Consensus)) + && *prev_tm_voting_power == *new_tm_voting_power + { + if old_consensus_key.as_ref().unwrap() == &new_consensus_key + { + tracing::debug!( + "skipping validator update, {address} is in \ + consensus set but voting power hasn't changed" + ); + return vec![]; + } else { + return vec![ + ValidatorSetUpdate::Consensus(ConsensusValidator { + consensus_key: new_consensus_key, + bonded_stake: new_stake, + }), + ValidatorSetUpdate::Deactivated( + old_consensus_key.unwrap(), + ), + ]; + } + } + // If both previous and current voting powers are 0, and the + // validator_stake_threshold is 0, skip update + if params.validator_stake_threshold.is_zero() + && *prev_tm_voting_power == 0 + && *new_tm_voting_power == 0 + { + tracing::info!( + "skipping validator update, {address} is in consensus \ + set but without voting power" + ); + return vec![]; + } + } + + tracing::debug!( + "{address} consensus key {}", + new_consensus_key.tm_raw_hash() + ); + + if old_consensus_key.as_ref() == Some(&new_consensus_key) + || old_consensus_key.is_none() + { + vec![ValidatorSetUpdate::Consensus(ConsensusValidator { + consensus_key: new_consensus_key, + bonded_stake: new_stake, + })] + } else { + vec![ + ValidatorSetUpdate::Consensus(ConsensusValidator { + consensus_key: new_consensus_key, + bonded_stake: new_stake, + }), + ValidatorSetUpdate::Deactivated(old_consensus_key.unwrap()), + ] + } + }); + + let prev_consensus_validators = prev_consensus_validator_handle + .iter(storage)? + .map(|validator| { + let ( + NestedSubKey::Data { + key: _prev_stake, + nested_sub_key: _, + }, + address, + ) = validator.unwrap(); + + let new_state = validator_state_handle(&address) + .get(storage, next_epoch, params) + .unwrap(); + + let prev_tm_voting_power = Lazy::new(|| { + let prev_validator_stake = read_validator_stake( + storage, + params, + &address, + current_epoch, + ) + .unwrap(); + into_tm_voting_power( + params.tm_votes_per_token, + prev_validator_stake, + ) + }); + + let old_consensus_key = validator_consensus_key_handle(&address) + .get(storage, current_epoch, params) + .unwrap() + .unwrap(); + + // If the validator is still in the Consensus set, we accounted for + // it in the `new_consensus_validators` iterator above + if matches!(new_state, Some(ValidatorState::Consensus)) { + return vec![]; + } else if params.validator_stake_threshold.is_zero() + && *prev_tm_voting_power == 0 + { + // If the new state is not Consensus but its prev voting power + // was 0 and the stake threshold is 0, we can also skip the + // update + tracing::info!( + "skipping validator update, {address} is in consensus set \ + but without voting power" + ); + return vec![]; + } + + // The remaining validators were previously Consensus but no longer + // are, so they must be deactivated + let consensus_key = validator_consensus_key_handle(&address) + .get(storage, next_epoch, params) + .unwrap() + .unwrap(); + tracing::debug!( + "{address} consensus key {}", + consensus_key.tm_raw_hash() + ); + vec![ValidatorSetUpdate::Deactivated(old_consensus_key)] + }); + + Ok(new_consensus_validators + .chain(prev_consensus_validators) + .flatten() + .map(f) + .collect()) +} + +/// Copy the consensus and below-capacity validator sets and positions into a +/// future epoch. Also copies the epoched set of all known validators in the +/// network. +pub fn copy_validator_sets_and_positions( + storage: &mut S, + params: &PosParams, + current_epoch: Epoch, + target_epoch: Epoch, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let prev_epoch = target_epoch.prev(); + + let consensus_validator_set = consensus_validator_set_handle(); + let below_capacity_validator_set = below_capacity_validator_set_handle(); + + let (consensus, below_capacity) = ( + consensus_validator_set.at(&prev_epoch), + below_capacity_validator_set.at(&prev_epoch), + ); + debug_assert!(!consensus.is_empty(storage)?); + + // Need to copy into memory here to avoid borrowing a ref + // simultaneously as immutable and mutable + let mut consensus_in_mem: HashMap<(token::Amount, Position), Address> = + HashMap::new(); + let mut below_cap_in_mem: HashMap< + (ReverseOrdTokenAmount, Position), + Address, + > = HashMap::new(); + + for val in consensus.iter(storage)? { + let ( + NestedSubKey::Data { + key: stake, + nested_sub_key: SubKey::Data(position), + }, + address, + ) = val?; + consensus_in_mem.insert((stake, position), address); + } + for val in below_capacity.iter(storage)? { + let ( + NestedSubKey::Data { + key: stake, + nested_sub_key: SubKey::Data(position), + }, + address, + ) = val?; + below_cap_in_mem.insert((stake, position), address); + } + + for ((val_stake, val_position), val_address) in consensus_in_mem.into_iter() + { + consensus_validator_set + .at(&target_epoch) + .at(&val_stake) + .insert(storage, val_position, val_address)?; + } + + for ((val_stake, val_position), val_address) in below_cap_in_mem.into_iter() + { + below_capacity_validator_set + .at(&target_epoch) + .at(&val_stake) + .insert(storage, val_position, val_address)?; + } + // Purge consensus and below-capacity validator sets + consensus_validator_set.update_data(storage, params, current_epoch)?; + below_capacity_validator_set.update_data(storage, params, current_epoch)?; + + // Copy validator positions + let mut positions = HashMap::::default(); + let validator_set_positions_handle = validator_set_positions_handle(); + let positions_handle = validator_set_positions_handle.at(&prev_epoch); + + for result in positions_handle.iter(storage)? { + let (validator, position) = result?; + positions.insert(validator, position); + } + + let new_positions_handle = validator_set_positions_handle.at(&target_epoch); + for (validator, position) in positions { + let prev = new_positions_handle.insert(storage, validator, position)?; + debug_assert!(prev.is_none()); + } + validator_set_positions_handle.set_last_update(storage, current_epoch)?; + + // Purge old epochs of validator positions + validator_set_positions_handle.update_data( + storage, + params, + current_epoch, + )?; + + // Copy set of all validator addresses + let mut all_validators = HashSet::
::default(); + let validator_addresses_handle = validator_addresses_handle(); + let all_validators_handle = validator_addresses_handle.at(&prev_epoch); + for result in all_validators_handle.iter(storage)? { + let validator = result?; + all_validators.insert(validator); + } + let new_all_validators_handle = + validator_addresses_handle.at(&target_epoch); + for validator in all_validators { + let was_in = new_all_validators_handle.insert(storage, validator)?; + debug_assert!(!was_in); + } + + // Purge old epochs of all validator addresses + validator_addresses_handle.update_data(storage, params, current_epoch)?; + + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn insert_into_consensus_and_demote_to_below_cap( + storage: &mut S, + validator: &Address, + tokens_post: token::Amount, + min_consensus_amount: token::Amount, + current_epoch: Epoch, + offset: u64, + consensus_set: &ConsensusValidatorSet, + below_capacity_set: &BelowCapacityValidatorSet, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + // First, remove the last position min consensus validator + let consensus_vals_min = consensus_set.at(&min_consensus_amount); + let last_position_of_min_consensus_vals = + find_last_position(&consensus_vals_min, storage)? + .expect("There must be always be at least 1 consensus validator"); + let removed_min_consensus = consensus_vals_min + .remove(storage, &last_position_of_min_consensus_vals)? + .expect("There must be always be at least 1 consensus validator"); + + let offset_epoch = current_epoch + offset; + + // Insert the min consensus validator into the below-capacity + // set + insert_validator_into_set( + &below_capacity_set.at(&min_consensus_amount.into()), + storage, + &offset_epoch, + &removed_min_consensus, + )?; + validator_state_handle(&removed_min_consensus).set( + storage, + ValidatorState::BelowCapacity, + current_epoch, + offset, + )?; + + // Insert the current validator into the consensus set + insert_validator_into_set( + &consensus_set.at(&tokens_post), + storage, + &offset_epoch, + validator, + )?; + validator_state_handle(validator).set( + storage, + ValidatorState::Consensus, + current_epoch, + offset, + )?; + Ok(()) +} + +/// Find the first (lowest) position in a validator set if it is not empty +fn find_first_position( + handle: &ValidatorPositionAddresses, + storage: &S, +) -> storage_api::Result> +where + S: StorageRead, +{ + let lowest_position = handle + .iter(storage)? + .next() + .transpose()? + .map(|(position, _addr)| position); + Ok(lowest_position) +} + +/// Find the last (greatest) position in a validator set if it is not empty +fn find_last_position( + handle: &ValidatorPositionAddresses, + storage: &S, +) -> storage_api::Result> +where + S: StorageRead, +{ + let position = handle + .iter(storage)? + .last() + .transpose()? + .map(|(position, _addr)| position); + Ok(position) +} + +/// Find next position in a validator set or 0 if empty +fn find_next_position( + handle: &ValidatorPositionAddresses, + storage: &S, +) -> storage_api::Result +where + S: StorageRead, +{ + let position_iter = handle.iter(storage)?; + let next = position_iter + .last() + .transpose()? + .map(|(position, _address)| position.next()) + .unwrap_or_default(); + Ok(next) +} + +fn get_min_consensus_validator_amount( + handle: &ConsensusValidatorSet, + storage: &S, +) -> storage_api::Result +where + S: StorageRead, +{ + Ok(handle + .iter(storage)? + .next() + .transpose()? + .map(|(subkey, _address)| match subkey { + NestedSubKey::Data { + key, + nested_sub_key: _, + } => key, + }) + .unwrap_or_default()) +} + +/// Returns `Ok(None)` when the below capacity set is empty. +fn get_max_below_capacity_validator_amount( + handle: &BelowCapacityValidatorSet, + storage: &S, +) -> storage_api::Result> +where + S: StorageRead, +{ + Ok(handle + .iter(storage)? + .next() + .transpose()? + .map(|(subkey, _address)| match subkey { + NestedSubKey::Data { + key, + nested_sub_key: _, + } => token::Amount::from(key), + })) +} + +/// Inserts a validator into the provided `handle` within some validator set at +/// the next position. Also updates the validator set position for the +/// validator. +fn insert_validator_into_set( + handle: &ValidatorPositionAddresses, + storage: &mut S, + epoch: &Epoch, + address: &Address, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let next_position = find_next_position(handle, storage)?; + tracing::debug!( + "Inserting validator {} into position {:?} at epoch {}", + address.clone(), + next_position.clone(), + epoch.clone() + ); + handle.insert(storage, next_position, address.clone())?; + validator_set_positions_handle().at(epoch).insert( + storage, + address.clone(), + next_position, + )?; + Ok(()) +} + +/// Read the position of the validator in the subset of validators that have the +/// same bonded stake. This information is held in its own epoched structure in +/// addition to being inside the validator sets. +fn read_validator_set_position( + storage: &S, + validator: &Address, + epoch: Epoch, + _params: &PosParams, +) -> storage_api::Result> +where + S: StorageRead, +{ + let handle = validator_set_positions_handle(); + handle.get_data_handler().at(&epoch).get(storage, validator) +} diff --git a/sdk/src/queries/vp/pos.rs b/sdk/src/queries/vp/pos.rs index 1011964588..ff1b073dc4 100644 --- a/sdk/src/queries/vp/pos.rs +++ b/sdk/src/queries/vp/pos.rs @@ -12,14 +12,14 @@ use namada_core::types::key::common; use namada_core::types::storage::Epoch; use namada_core::types::token; use namada_proof_of_stake::parameters::PosParams; -use namada_proof_of_stake::types::{ - BondId, BondsAndUnbondsDetail, BondsAndUnbondsDetails, CommissionPair, - Slash, ValidatorMetaData, ValidatorState, WeightedValidator, +use namada_proof_of_stake::queries::{ + find_delegation_validators, find_delegations, }; -use namada_proof_of_stake::{ - self, bond_amount, bond_handle, find_all_enqueued_slashes, - find_all_slashes, find_delegation_validators, find_delegations, - query_reward_tokens, read_all_validator_addresses, +use namada_proof_of_stake::slashing::{ + find_all_enqueued_slashes, find_all_slashes, +}; +use namada_proof_of_stake::storage::{ + bond_handle, read_all_validator_addresses, read_below_capacity_validator_set_addresses_with_stake, read_consensus_validator_set_addresses_with_stake, read_pos_params, read_total_stake, read_validator_description, @@ -29,6 +29,11 @@ use namada_proof_of_stake::{ validator_commission_rate_handle, validator_incoming_redelegations_handle, validator_slashes_handle, validator_state_handle, }; +use namada_proof_of_stake::types::{ + BondId, BondsAndUnbondsDetail, BondsAndUnbondsDetails, CommissionPair, + Slash, ValidatorMetaData, ValidatorState, WeightedValidator, +}; +use namada_proof_of_stake::{self, bond_amount, query_reward_tokens}; use crate::queries::types::RequestCtx; @@ -546,7 +551,11 @@ where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { - namada_proof_of_stake::bonds_and_unbonds(ctx.wl_storage, source, validator) + namada_proof_of_stake::queries::bonds_and_unbonds( + ctx.wl_storage, + source, + validator, + ) } /// Find all the validator addresses to whom the given `owner` address has @@ -622,7 +631,10 @@ where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { - namada_proof_of_stake::find_validator_by_raw_hash(ctx.wl_storage, tm_addr) + namada_proof_of_stake::storage::find_validator_by_raw_hash( + ctx.wl_storage, + tm_addr, + ) } /// Native validator address by looking up the Tendermint address @@ -633,7 +645,7 @@ where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { - namada_proof_of_stake::get_consensus_key_set(ctx.wl_storage) + namada_proof_of_stake::storage::get_consensus_key_set(ctx.wl_storage) } /// Find if the given source address has any bonds. @@ -645,7 +657,7 @@ where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { - namada_proof_of_stake::has_bonds(ctx.wl_storage, &source) + namada_proof_of_stake::queries::has_bonds(ctx.wl_storage, &source) } /// Client-only methods for the router type are composed from router functions. diff --git a/shared/src/ledger/native_vp/ibc/mod.rs b/shared/src/ledger/native_vp/ibc/mod.rs index a4ca254537..e4778ff82f 100644 --- a/shared/src/ledger/native_vp/ibc/mod.rs +++ b/shared/src/ledger/native_vp/ibc/mod.rs @@ -19,7 +19,7 @@ use namada_core::ledger::storage::{self as ledger_storage, StorageHasher}; use namada_core::proto::Tx; use namada_core::types::address::Address; use namada_core::types::storage::Key; -use namada_proof_of_stake::read_pos_params; +use namada_proof_of_stake::storage::read_pos_params; use thiserror::Error; use crate::ibc::core::host::types::identifiers::ChainId as IbcChainId; diff --git a/shared/src/ledger/pos/vp.rs b/shared/src/ledger/pos/vp.rs index 506ef489ca..298746340c 100644 --- a/shared/src/ledger/pos/vp.rs +++ b/shared/src/ledger/pos/vp.rs @@ -7,11 +7,11 @@ use namada_core::ledger::storage_api::governance; pub use namada_proof_of_stake; pub use namada_proof_of_stake::parameters::PosParams; // use namada_proof_of_stake::validation::validate; -use namada_proof_of_stake::read_pos_params; +use namada_proof_of_stake::storage::read_pos_params; +use namada_proof_of_stake::storage_key::is_params_key; pub use namada_proof_of_stake::types; use thiserror::Error; -use super::is_params_key; use crate::ledger::native_vp::{self, Ctx, NativeVp}; // use crate::ledger::pos::{ // is_validator_address_raw_hash_key, diff --git a/tests/src/native_vp/pos.rs b/tests/src/native_vp/pos.rs index 82d52aa746..1bbe50f362 100644 --- a/tests/src/native_vp/pos.rs +++ b/tests/src/native_vp/pos.rs @@ -574,11 +574,11 @@ pub mod testing { use namada::proof_of_stake::epoched::DynEpochOffset; use namada::proof_of_stake::parameters::testing::arb_rate; use namada::proof_of_stake::parameters::PosParams; - use namada::proof_of_stake::types::{BondId, ValidatorState}; - use namada::proof_of_stake::{ + use namada::proof_of_stake::storage::{ get_num_consensus_validators, read_pos_params, unbond_handle, - ADDRESS as POS_ADDRESS, }; + use namada::proof_of_stake::types::{BondId, ValidatorState}; + use namada::proof_of_stake::ADDRESS as POS_ADDRESS; use namada::types::key::common::PublicKey; use namada::types::key::RefTo; use namada::types::storage::Epoch; diff --git a/tx_prelude/src/proof_of_stake.rs b/tx_prelude/src/proof_of_stake.rs index 15250e760a..3b7883361d 100644 --- a/tx_prelude/src/proof_of_stake.rs +++ b/tx_prelude/src/proof_of_stake.rs @@ -5,15 +5,15 @@ use namada_core::types::key::common; use namada_core::types::transaction::pos::BecomeValidator; use namada_core::types::{key, token}; pub use namada_proof_of_stake::parameters::PosParams; -use namada_proof_of_stake::types::ValidatorMetaData; +use namada_proof_of_stake::storage::read_pos_params; +use namada_proof_of_stake::types::{ResultSlashing, ValidatorMetaData}; use namada_proof_of_stake::{ become_validator, bond_tokens, change_consensus_key, change_validator_commission_rate, change_validator_metadata, claim_reward_tokens, deactivate_validator, reactivate_validator, - read_pos_params, redelegate_tokens, unbond_tokens, unjail_validator, - withdraw_tokens, + redelegate_tokens, unbond_tokens, unjail_validator, withdraw_tokens, }; -pub use namada_proof_of_stake::{parameters, types, ResultSlashing}; +pub use namada_proof_of_stake::{parameters, types}; use super::*; diff --git a/wasm/wasm_source/src/tx_bond.rs b/wasm/wasm_source/src/tx_bond.rs index 419c1e00a0..733e021c59 100644 --- a/wasm/wasm_source/src/tx_bond.rs +++ b/wasm/wasm_source/src/tx_bond.rs @@ -21,11 +21,11 @@ mod tests { use std::collections::BTreeSet; use namada::ledger::pos::{OwnedPosParams, PosVP}; - use namada::proof_of_stake::types::{GenesisValidator, WeightedValidator}; - use namada::proof_of_stake::{ + use namada::proof_of_stake::storage::{ bond_handle, read_consensus_validator_set_addresses_with_stake, read_total_stake, read_validator_stake, }; + use namada::proof_of_stake::types::{GenesisValidator, WeightedValidator}; use namada::types::dec::Dec; use namada::types::storage::Epoch; use namada_tests::log::test; diff --git a/wasm/wasm_source/src/tx_change_validator_commission.rs b/wasm/wasm_source/src/tx_change_validator_commission.rs index 33433b59b3..4569c4d603 100644 --- a/wasm/wasm_source/src/tx_change_validator_commission.rs +++ b/wasm/wasm_source/src/tx_change_validator_commission.rs @@ -23,8 +23,8 @@ mod tests { use std::cmp; use namada::ledger::pos::{OwnedPosParams, PosVP}; + use namada::proof_of_stake::storage::validator_commission_rate_handle; use namada::proof_of_stake::types::GenesisValidator; - use namada::proof_of_stake::validator_commission_rate_handle; use namada::types::dec::{Dec, POS_DECIMAL_PRECISION}; use namada::types::storage::Epoch; use namada_tests::log::test; diff --git a/wasm/wasm_source/src/tx_redelegate.rs b/wasm/wasm_source/src/tx_redelegate.rs index 82f63cd9e4..a02970a0c1 100644 --- a/wasm/wasm_source/src/tx_redelegate.rs +++ b/wasm/wasm_source/src/tx_redelegate.rs @@ -25,11 +25,11 @@ mod tests { use std::collections::BTreeSet; use namada::ledger::pos::{OwnedPosParams, PosVP}; - use namada::proof_of_stake::types::{GenesisValidator, WeightedValidator}; - use namada::proof_of_stake::{ + use namada::proof_of_stake::storage::{ bond_handle, read_consensus_validator_set_addresses_with_stake, read_total_stake, read_validator_stake, unbond_handle, }; + use namada::proof_of_stake::types::{GenesisValidator, WeightedValidator}; use namada::types::dec::Dec; use namada::types::storage::Epoch; use namada_tests::log::test; diff --git a/wasm/wasm_source/src/tx_unbond.rs b/wasm/wasm_source/src/tx_unbond.rs index 3747f91d33..f7f11b14bc 100644 --- a/wasm/wasm_source/src/tx_unbond.rs +++ b/wasm/wasm_source/src/tx_unbond.rs @@ -28,11 +28,11 @@ mod tests { use std::collections::BTreeSet; use namada::ledger::pos::{OwnedPosParams, PosVP}; - use namada::proof_of_stake::types::{GenesisValidator, WeightedValidator}; - use namada::proof_of_stake::{ + use namada::proof_of_stake::storage::{ bond_handle, read_consensus_validator_set_addresses_with_stake, read_total_stake, read_validator_stake, unbond_handle, }; + use namada::proof_of_stake::types::{GenesisValidator, WeightedValidator}; use namada::types::dec::Dec; use namada::types::storage::Epoch; use namada_tests::log::test; diff --git a/wasm/wasm_source/src/tx_withdraw.rs b/wasm/wasm_source/src/tx_withdraw.rs index 1fb10dc588..f8984b1bef 100644 --- a/wasm/wasm_source/src/tx_withdraw.rs +++ b/wasm/wasm_source/src/tx_withdraw.rs @@ -24,8 +24,8 @@ fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { #[cfg(test)] mod tests { use namada::ledger::pos::{OwnedPosParams, PosVP}; + use namada::proof_of_stake::storage::unbond_handle; use namada::proof_of_stake::types::GenesisValidator; - use namada::proof_of_stake::unbond_handle; use namada::types::dec::Dec; use namada::types::storage::Epoch; use namada_tests::log::test; diff --git a/wasm/wasm_source/src/vp_implicit.rs b/wasm/wasm_source/src/vp_implicit.rs index 7be4a0755f..de105c304f 100644 --- a/wasm/wasm_source/src/vp_implicit.rs +++ b/wasm/wasm_source/src/vp_implicit.rs @@ -42,7 +42,7 @@ impl<'a> From<&'a storage::Key> for KeyType<'a> { Self::TokenMinted } else if let Some(minter) = token::is_any_minter_key(key) { Self::TokenMinter(minter) - } else if proof_of_stake::storage::is_pos_key(key) { + } else if proof_of_stake::storage_key::is_pos_key(key) { Self::PoS } else if let Some(address) = pgf_storage::keys::is_stewards_key(key) { Self::PgfSteward(address) diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index c657505c92..a9d535c28f 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -15,6 +15,12 @@ use core::ops::Deref; use namada_vp_prelude::*; use once_cell::unsync::Lazy; +use proof_of_stake::storage::{read_pos_params, validator_state_handle}; +use proof_of_stake::storage_key::{ + is_bond_key, is_pos_key, is_unbond_key, is_validator_commission_rate_key, + is_validator_metadata_key, is_validator_state_key, +}; +use proof_of_stake::types::ValidatorState; enum KeyType<'a> { TokenBalance { owner: &'a Address }, @@ -37,7 +43,7 @@ impl<'a> From<&'a storage::Key> for KeyType<'a> { Self::TokenMinted } else if let Some(minter) = token::is_any_minter_key(key) { Self::TokenMinter(minter) - } else if proof_of_stake::storage::is_pos_key(key) { + } else if is_pos_key(key) { Self::PoS } else if gov_storage::keys::is_vote_key(key) { let voter_address = gov_storage::keys::get_voter_address(key);