From 178f293b7bfa14881458e7b3fe1bff41d4a2d1fd Mon Sep 17 00:00:00 2001 From: brentstone Date: Fri, 26 May 2023 13:45:17 +0200 Subject: [PATCH 01/31] cubic and general slashing algorithms and transactions --- apps/src/lib/cli.rs | 24 +- apps/src/lib/client/tx.rs | 14 + apps/src/lib/config/genesis.rs | 5 + .../lib/node/ledger/shell/finalize_block.rs | 7 +- apps/src/lib/node/ledger/shell/mod.rs | 23 +- .../storage_api/collections/lazy_vec.rs | 19 +- .../src/ledger/storage_api/collections/mod.rs | 2 +- core/src/types/storage.rs | 12 +- core/src/types/token.rs | 11 +- proof_of_stake/src/lib.rs | 1475 ++++++++++++++--- proof_of_stake/src/parameters.rs | 8 +- proof_of_stake/src/storage.rs | 60 +- proof_of_stake/src/types.rs | 57 +- shared/src/ledger/args.rs | 11 + shared/src/ledger/tx.rs | 41 + tests/src/native_vp/pos.rs | 6 +- tx_prelude/src/proof_of_stake.rs | 8 +- wasm/wasm_source/src/lib.rs | 3 +- wasm/wasm_source/src/tx_unjail_validator.rs | 14 + 19 files changed, 1531 insertions(+), 269 deletions(-) create mode 100644 wasm/wasm_source/src/tx_unjail_validator.rs diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index daecbf5eee..d4087a3027 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1684,6 +1684,7 @@ pub mod args { pub const TX_WITHDRAW_WASM: &str = "tx_withdraw.wasm"; pub const TX_CHANGE_COMMISSION_WASM: &str = "tx_change_validator_commission.wasm"; + pub const TX_UNJAIL_VALIDATOR_WASM: &str = "tx_unjail_validator.wasm"; pub const ADDRESS: Arg = arg("address"); pub const ALIAS_OPT: ArgOpt = ALIAS.opt(); @@ -3015,7 +3016,7 @@ pub mod args { } fn def(app: App) -> App { - app.add_args::>() + app.add_args::>() .arg(VALIDATOR.def().about( "The validator's address whose commission rate to change.", )) @@ -3027,6 +3028,27 @@ pub mod args { } } + impl Args for TxUnjailValidator { + fn parse(matches: &ArgMatches) -> Self { + let tx = Tx::parse(matches); + let validator = VALIDATOR.parse(matches); + let tx_code_path = PathBuf::from(TX_UNJAIL_VALIDATOR_WASM); + Self { + tx, + validator, + tx_code_path, + } + } + + fn def(app: App) -> App { + app.add_args::>().arg( + VALIDATOR.def().about( + "The address of the jailed validator to re-activate.", + ), + ) + } + } + impl CliToSdk> for QueryCommissionRate { fn to_sdk(self, ctx: &mut Context) -> QueryCommissionRate { QueryCommissionRate:: { diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index ce334adb41..a413e3bdb1 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -990,6 +990,20 @@ pub async fn submit_validator_commission_change< .await } +pub async fn submit_unjail_validator< + C: namada::ledger::queries::Client + Sync, +>( + client: &C, + mut ctx: Context, + mut args: args::TxUnjailValidator, +) -> Result<(), tx::Error> { + args.tx.chain_id = args + .tx + .chain_id + .or_else(|| Some(ctx.config.ledger.chain_id.clone())); + tx::submit_unjail_validator::(client, &mut ctx.wallet, args).await +} + /// Submit transaction and wait for result. Returns a list of addresses /// initialized in the transaction if any. In dry run, this is always empty. async fn process_tx( diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index 922b0c445f..fb9ae608dc 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -305,6 +305,9 @@ pub mod genesis_config { // light client attack. // XXX: u64 doesn't work with toml-rs! pub light_client_attack_min_slash_rate: Decimal, + /// Number of epochs above and below (separately) the current epoch to + /// consider when doing cubic slashing + pub cubic_slashing_window_length: u64, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -647,6 +650,7 @@ pub mod genesis_config { target_staked_ratio, duplicate_vote_min_slash_rate, light_client_attack_min_slash_rate, + cubic_slashing_window_length, } = pos_params; let pos_params = PosParams { max_validator_slots, @@ -659,6 +663,7 @@ pub mod genesis_config { target_staked_ratio, duplicate_vote_min_slash_rate, light_client_attack_min_slash_rate, + cubic_slashing_window_length, }; let mut genesis = Genesis { diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index f252b03cbc..04462b8d52 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -103,7 +103,10 @@ where // Invariant: This has to be applied after // `copy_validator_sets_and_positions` if we're starting a new epoch - self.slash(); + self.record_slashes_from_evidence(); + if new_epoch { + self.process_slashes(); + } let wrapper_fees = self.get_wrapper_tx_fees(); let mut stats = InternalStats::default(); @@ -600,7 +603,7 @@ where /// executed while finalizing the first block of a new epoch and is applied /// with respect to the previous epoch. fn apply_inflation(&mut self, current_epoch: Epoch) -> Result<()> { - let last_epoch = current_epoch - 1; + let last_epoch = current_epoch.prev(); // Get input values needed for the PD controller for PoS and MASP. // Run the PD controllers to calculate new rates. // diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index 3f69be490e..772239cea4 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -34,7 +34,7 @@ use namada::ledger::storage::{ }; use namada::ledger::storage_api::{self, StorageRead}; use namada::ledger::{ibc, pos, protocol, replay_protection}; -use namada::proof_of_stake::{self, read_pos_params, slash}; +use namada::proof_of_stake::{self, process_slashes, read_pos_params, slash}; use namada::proto::{self, Tx}; use namada::types::address::{masp, masp_tx_key, Address}; use namada::types::chain::ChainId; @@ -491,14 +491,16 @@ where } /// Apply PoS slashes from the evidence - fn slash(&mut self) { + fn record_slashes_from_evidence(&mut self) { if !self.byzantine_validators.is_empty() { + println!("BYZANTINE VALIDATORS NOT EMPTY"); let byzantine_validators = mem::take(&mut self.byzantine_validators); // TODO: resolve this unwrap() better let pos_params = read_pos_params(&self.wl_storage).unwrap(); let current_epoch = self.wl_storage.storage.block.epoch; for evidence in byzantine_validators { + // dbg!(&evidence); tracing::info!("Processing evidence {evidence:?}."); let evidence_height = match u64::try_from(evidence.height) { Ok(height) => height, @@ -526,7 +528,9 @@ where continue; } }; - if evidence_epoch + pos_params.unbonding_len <= current_epoch { + // Disregard evidences that should have already been processed + // at this time + if evidence_epoch + pos_params.unbonding_len < current_epoch { tracing::info!( "Skipping outdated evidence from epoch \ {evidence_epoch}" @@ -606,6 +610,19 @@ where } } + /// Process and apply slashes that have already been recorded for the + /// current epoch + fn process_slashes(&mut self) { + let current_epoch = self.wl_storage.storage.block.epoch; + if let Err(err) = process_slashes(&mut self.wl_storage, current_epoch) { + tracing::error!( + "Error while processing slashes queued for epoch {}: {}", + current_epoch, + err + ); + } + } + /// INVARIANT: This method must be stateless. #[cfg(feature = "abcipp")] pub fn extend_vote( diff --git a/core/src/ledger/storage_api/collections/lazy_vec.rs b/core/src/ledger/storage_api/collections/lazy_vec.rs index 1e83456814..d21ca1c515 100644 --- a/core/src/ledger/storage_api/collections/lazy_vec.rs +++ b/core/src/ledger/storage_api/collections/lazy_vec.rs @@ -407,7 +407,7 @@ impl LazyVec { // `LazyVec` methods with borsh encoded values `T` impl LazyVec where - T: BorshSerialize + BorshDeserialize + 'static, + T: BorshSerialize + BorshDeserialize + 'static + Debug, { /// Appends an element to the back of a collection. pub fn push(&self, storage: &mut S, val: T) -> Result<()> @@ -470,6 +470,23 @@ where storage.read(&self.get_data_key(index)) } + /// Read the first element + pub fn front(&self, storage: &S) -> Result> + where + S: StorageRead, + { + self.get(storage, 0) + } + + /// Read the last element + pub fn back(&self, storage: &S) -> Result> + where + S: StorageRead, + { + let len = self.len(storage)?; + self.get(storage, len - 1) + } + /// An iterator visiting all elements. The iterator element type is /// `Result`, because iterator's call to `next` may fail with e.g. out of /// gas or data decoding error. diff --git a/core/src/ledger/storage_api/collections/mod.rs b/core/src/ledger/storage_api/collections/mod.rs index 6301d151be..ff1136acd5 100644 --- a/core/src/ledger/storage_api/collections/mod.rs +++ b/core/src/ledger/storage_api/collections/mod.rs @@ -60,7 +60,7 @@ pub trait LazyCollection { type SubKeyWithData: Debug; /// A type of a value in the inner-most collection - type Value: BorshDeserialize; + type Value: BorshDeserialize + Debug; /// Create or use an existing vector with the given storage `key`. fn open(key: storage::Key) -> Self; diff --git a/core/src/types/storage.rs b/core/src/types/storage.rs index 9ceddecf15..cedb58164f 100644 --- a/core/src/types/storage.rs +++ b/core/src/types/storage.rs @@ -284,7 +284,7 @@ impl core::fmt::Debug for BlockHash { /// The data from Tendermint header /// relevant for Namada storage -#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)] +#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Default)] pub struct Header { /// Merkle root hash of block pub hash: Hash, @@ -959,6 +959,16 @@ impl Epoch { (start_ix..end_ix).map(Epoch::from) } + /// Iterate a range of epochs, inclusive of the start and end. + pub fn iter_bounds_inclusive( + start: Self, + end: Self, + ) -> impl Iterator + Clone { + let start_ix = start.0; + let end_ix = end.0; + (start_ix..=end_ix).map(Epoch::from) + } + /// Checked epoch subtraction. Computes self - rhs, returning None if /// overflow occurred. #[must_use = "this returns the result of the operation, without modifying \ diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 9c1433b464..957876c537 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -1,6 +1,7 @@ //! A basic fungible token use std::fmt::Display; +use std::iter::Sum; use std::ops::{Add, AddAssign, Mul, Sub, SubAssign}; use std::str::FromStr; @@ -150,13 +151,13 @@ impl<'de> serde::Deserialize<'de> for Amount { impl From for Decimal { fn from(amount: Amount) -> Self { - Into::::into(amount.micro) / Into::::into(SCALE) + Into::::into(amount.micro) } } impl From for Amount { fn from(micro: Decimal) -> Self { - let res = (micro * Into::::into(SCALE)).to_u64().unwrap(); + let res = micro.to_u64().unwrap(); Self { micro: res } } } @@ -239,6 +240,12 @@ impl SubAssign for Amount { } } +impl Sum for Amount { + fn sum>(iter: I) -> Self { + iter.fold(Amount::default(), |acc, next| acc + next) + } +} + impl KeySeg for Amount { fn parse(string: String) -> super::storage::Result where diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 63da01e83a..6cab766aa9 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -25,11 +25,11 @@ pub mod types; mod tests; use core::fmt::Debug; -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::cmp::{self, Reverse}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::num::TryFromIntError; use borsh::BorshDeserialize; -use epoched::{EpochOffset, OffsetPipelineLen}; use namada_core::ledger::storage_api::collections::lazy_map::{ NestedSubKey, SubKey, }; @@ -42,29 +42,31 @@ use namada_core::types::address::{Address, InternalAddress}; use namada_core::types::key::{ common, tm_consensus_key_raw_hash, PublicKeyTmRawHash, }; -pub use namada_core::types::storage::Epoch; +pub use namada_core::types::storage::{Epoch, Key, KeySeg}; use namada_core::types::token; use once_cell::unsync::Lazy; use parameters::PosParams; use rewards::PosRewardsCalculator; use rust_decimal::Decimal; +use rust_decimal_macros::dec; use storage::{ bonds_for_source_prefix, bonds_prefix, consensus_keys_key, - get_validator_address_from_bond, into_tm_voting_power, is_bond_key, - is_unbond_key, is_validator_slashes_key, last_block_proposer_key, - mult_amount, mult_change_to_amount, num_consensus_validators_key, - params_key, slashes_prefix, unbonds_for_source_prefix, unbonds_prefix, - validator_address_raw_hash_key, validator_max_commission_rate_change_key, + decimal_mult_amount, get_validator_address_from_bond, into_tm_voting_power, + is_bond_key, is_unbond_key, is_validator_slashes_key, + last_block_proposer_key, mult_change_to_amount, params_key, slashes_prefix, + unbonds_for_source_prefix, unbonds_prefix, validator_address_raw_hash_key, + validator_last_slash_key, validator_max_commission_rate_change_key, BondDetails, BondsAndUnbondsDetail, BondsAndUnbondsDetails, - ReverseOrdTokenAmount, RewardsAccumulator, UnbondDetails, + ReverseOrdTokenAmount, RewardsAccumulator, SlashedAmount, UnbondDetails, + ValidatorUnbondRecords, }; use thiserror::Error; use types::{ - decimal_mult_i128, decimal_mult_u64, BelowCapacityValidatorSet, - BelowCapacityValidatorSets, BondId, Bonds, CommissionRates, - ConsensusValidator, ConsensusValidatorSet, ConsensusValidatorSets, - GenesisValidator, Position, RewardsProducts, Slash, SlashType, Slashes, - TotalDeltas, Unbonds, ValidatorConsensusKeys, ValidatorDeltas, + decimal_mult_i128, BelowCapacityValidatorSet, BelowCapacityValidatorSets, + BondId, Bonds, CommissionRates, ConsensusValidator, ConsensusValidatorSet, + ConsensusValidatorSets, EpochedSlashes, GenesisValidator, Position, + RewardsProducts, Slash, SlashType, Slashes, TotalDeltas, Unbonds, + ValidatorAddresses, ValidatorConsensusKeys, ValidatorDeltas, ValidatorPositionAddresses, ValidatorSetPositions, ValidatorSetUpdate, ValidatorState, ValidatorStates, VoteInfo, WeightedValidator, }; @@ -135,6 +137,8 @@ pub enum UnbondError { ValidatorHasNoVotingPower(Address), #[error("Voting power overflow: {0}")] VotingPowerOverflow(TryFromIntError), + #[error("Trying to unbond from a frozen validator: {0}")] + ValidatorIsFrozen(Address), } #[allow(missing_docs)] @@ -178,10 +182,19 @@ pub enum CommissionRateChangeError { CannotRead(Address), } -// ------------------------------------------------------------------------------------------ -// ------------------------------------------------------------------------------------------ -// ------------------------------------------------------------------------------------------ -// ------------------------------------------------------------------------------------------ +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum UnjailValidatorError { + #[error("The given address {0} is not a validator address")] + NotAValidator(Address), + #[error("The given address {0} is not jailed in epoch {1}")] + NotJailed(Address, Epoch), + #[error( + "The given address {0} is not eligible for unnjailing until epoch \ + {1}: current epoch is {2}" + )] + NotEligible(Address, Epoch, Epoch), +} impl From for storage_api::Error { fn from(err: BecomeValidatorError) -> Self { @@ -219,6 +232,12 @@ impl From for storage_api::Error { } } +impl From for storage_api::Error { + fn from(err: UnjailValidatorError) -> Self { + Self::new(err) + } +} + /// Get the storage handle to the epoched consensus validator set pub fn consensus_validator_set_handle() -> ConsensusValidatorSets { let key = storage::consensus_validator_set_key(); @@ -258,6 +277,12 @@ pub fn total_deltas_handle() -> TotalDeltas { 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, @@ -288,6 +313,12 @@ pub fn unbond_handle(source: &Address, validator: &Address) -> Unbonds { Unbonds::open(key) } +/// Get the storage handle to a validator's total-unbonded map +pub fn unbond_records_handle(validator: &Address) -> ValidatorUnbondRecords { + let key = storage::validator_total_unbonded_key(validator); + ValidatorUnbondRecords::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(); @@ -300,6 +331,13 @@ pub fn validator_slashes_handle(validator: &Address) -> Slashes { 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 { @@ -341,6 +379,7 @@ where consensus_validator_set_handle().init(storage, current_epoch)?; below_capacity_validator_set_handle().init(storage, current_epoch)?; validator_set_positions_handle().init(storage, current_epoch)?; + validator_addresses_handle().init(storage, current_epoch)?; for GenesisValidator { address, @@ -367,6 +406,10 @@ where 0, )?; + validator_addresses_handle() + .at(¤t_epoch) + .insert(storage, address.clone())?; + // Write other validator data to storage write_validator_address_raw_hash(storage, &address, &consensus_key)?; write_validator_max_commission_rate_change( @@ -403,6 +446,7 @@ where token::Change::from(total_bonded), current_epoch, )?; + // Credit bonded token amount to the PoS account let staking_token = staking_token_address(storage); credit_tokens(storage, &staking_token, &ADDRESS, total_bonded)?; @@ -495,26 +539,29 @@ where storage.write(&key, change) } -/// Read number of consensus PoS validators. -pub fn read_num_consensus_validators(storage: &S) -> storage_api::Result +/// 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, { - Ok(storage - .read(&num_consensus_validators_key())? - .unwrap_or_default()) + let key = validator_last_slash_key(validator); + storage.read(&key) } -/// Read number of consensus PoS validators. -pub fn write_num_consensus_validators( +/// Write the most recent slash epoch for the given epoch +pub fn write_validator_last_slash_epoch( storage: &mut S, - new_num: u64, + validator: &Address, + epoch: Epoch, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, { - let key = num_consensus_validators_key(); - storage.write(&key, new_num) + let key = validator_last_slash_key(validator); + storage.write(&key, epoch) } /// Read last block proposer address. @@ -581,12 +628,12 @@ pub fn update_validator_deltas( validator: &Address, delta: token::Change, current_epoch: namada_core::types::storage::Epoch, + offset: u64, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, { let handle = validator_deltas_handle(validator); - let offset = OffsetPipelineLen::value(params); let val = handle .get_delta_val(storage, current_epoch + offset, params)? .unwrap_or_default(); @@ -670,6 +717,20 @@ where .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, @@ -701,6 +762,8 @@ where } /// Read all validator addresses. +/// TODO: expand this to include the jailed validators as well, as it currently +/// only does consensus and bc pub fn read_all_validator_addresses( storage: &S, epoch: namada_core::types::storage::Epoch, @@ -708,11 +771,10 @@ pub fn read_all_validator_addresses( where S: StorageRead, { - let mut addresses = read_consensus_validator_set_addresses(storage, epoch)?; - let bc_addresses = - read_below_capacity_validator_set_addresses(storage, epoch)?; - addresses.extend(bc_addresses.into_iter()); - Ok(addresses) + validator_addresses_handle() + .at(&epoch) + .iter(storage)? + .collect() } /// Update PoS total deltas. @@ -722,12 +784,12 @@ pub fn update_total_deltas( params: &PosParams, delta: token::Change, current_epoch: namada_core::types::storage::Epoch, + offset: u64, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, { let handle = total_deltas_handle(); - let offset = OffsetPipelineLen::value(params); let val = handle .get_delta_val(storage, current_epoch + offset, params)? .unwrap_or_default(); @@ -806,16 +868,12 @@ where ); } } - let state = validator_state_handle(validator).get( - storage, - pipeline_epoch, - ¶ms, - )?; + let validator_state_handle = validator_state_handle(validator); + let state = validator_state_handle.get(storage, pipeline_epoch, ¶ms)?; if state.is_none() { return Err(BondError::NotAValidator(validator.clone()).into()); } - let validator_state_handle = validator_state_handle(validator); let source = source.unwrap_or(validator); let bond_handle = bond_handle(source, validator); @@ -829,20 +887,51 @@ where } } + println!("\nBonds before incrementing:"); + for ep in Epoch::default().iter_range(current_epoch.0 + 3) { + let delta = bond_handle + .get_delta_val(storage, ep, ¶ms)? + .unwrap_or_default(); + if delta != 0 { + println!("bond ∆ at epoch {}: {}", ep, delta); + } + } + // Initialize or update the bond at the pipeline offset let offset = params.pipeline_len; let cur_remain = bond_handle .get_delta_val(storage, current_epoch + offset, ¶ms)? .unwrap_or_default(); - tracing::debug!( - "Bond remain at offset epoch {}: {}", - current_epoch + offset, - cur_remain - ); bond_handle.set(storage, cur_remain + amount, current_epoch, offset)?; + println!("\nBonds after incrementing:"); + for ep in Epoch::default().iter_range(current_epoch.0 + 3) { + let delta = bond_handle + .get_delta_val(storage, ep, ¶ms)? + .unwrap_or_default(); + if delta != 0 { + println!("bond ∆ at epoch {}: {}", ep, delta); + } + } + // Update the validator set - update_validator_set(storage, ¶ms, validator, amount, current_epoch)?; + // We allow bonding 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_at_pipeline = matches!( + validator_state_handle + .get(storage, pipeline_epoch, ¶ms)? + .unwrap(), + ValidatorState::Jailed + ); + if !is_jailed_at_pipeline { + update_validator_set( + storage, + ¶ms, + validator, + amount, + current_epoch, + )?; + } // Update the validator and total deltas update_validator_deltas( @@ -851,9 +940,10 @@ where validator, amount, current_epoch, + offset, )?; - update_total_deltas(storage, ¶ms, amount, current_epoch)?; + update_total_deltas(storage, ¶ms, amount, current_epoch, offset)?; // Transfer the bonded tokens from the source to PoS let staking_token = staking_token_address(storage); @@ -885,8 +975,9 @@ where let consensus_set = &consensus_validator_set_handle().at(&target_epoch); let below_cap_set = &below_capacity_validator_set_handle().at(&target_epoch); - // TODO make epoched - let num_consensus_validators = read_num_consensus_validators(storage)?; + + let num_consensus_validators = + get_num_consensus_validators(storage, target_epoch)?; if num_consensus_validators < params.max_validator_slots { insert_validator_into_set( &consensus_set.at(&stake), @@ -900,7 +991,6 @@ where current_epoch, offset, )?; - write_num_consensus_validators(storage, num_consensus_validators + 1)?; } else { // Check to see if the current genesis validator should replace one // already in the consensus set @@ -940,13 +1030,13 @@ where &target_epoch, address, )?; + // Update and set the validator states validator_state_handle(address).set( storage, ValidatorState::Consensus, current_epoch, offset, )?; - // Update and set the validator states } else { // Insert the current genesis validator into the below-capacity set insert_validator_into_set( @@ -1029,7 +1119,8 @@ where get_max_below_capacity_validator_amount( &below_capacity_val_handle, storage, - )?; + )? + .unwrap_or_default(); if tokens_post < max_below_capacity_validator_amount { tracing::debug!("Need to swap validators"); @@ -1173,7 +1264,7 @@ pub fn copy_validator_sets_and_positions( where S: StorageRead + StorageWrite, { - let prev_epoch = target_epoch - 1; + let prev_epoch = target_epoch.prev(); let (consensus, below_capacity) = ( consensus_validator_set.at(&prev_epoch), @@ -1249,6 +1340,20 @@ where } validator_set_positions_handle().set_last_update(storage, current_epoch)?; + // Copy set of all validator addresses + let mut all_validators = HashSet::
::default(); + 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); + } + Ok(()) } @@ -1337,10 +1442,11 @@ where .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 +) -> storage_api::Result> where S: StorageRead, { @@ -1352,10 +1458,8 @@ where NestedSubKey::Data { key, nested_sub_key: _, - } => key, - }) - .unwrap_or_default() - .into()) + } => token::Amount::from(key), + })) } fn insert_validator_into_set( @@ -1383,7 +1487,8 @@ where Ok(()) } -/// Unbond. +/// Unbond tokens that are bonded between a validator and a source (self or +/// delegator) pub fn unbond_tokens( storage: &mut S, source: Option<&Address>, @@ -1404,6 +1509,7 @@ where .unwrap_or_default() ); + // Make sure source is not some other validator if let Some(source) = source { if source != validator && is_validator(storage, source, ¶ms, pipeline_epoch)? @@ -1413,12 +1519,19 @@ where ); } } + // Make sure the target is actually a validator if !is_validator(storage, validator, ¶ms, pipeline_epoch)? { 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()); + } + + // Should be able to unbond inactive validators - // TODO: Should be able to unbond inactive validators, but we'll need to - // prevent jailed unbonding with slashing + // Check that validator is not inactive at anywhere between the current + // epoch and pipeline offset // let validator_state_handle = validator_state_handle(validator); // for epoch in current_epoch.iter_range(params.pipeline_len) { // if let Some(ValidatorState::Inactive) = @@ -1429,12 +1542,20 @@ where // } let source = source.unwrap_or(validator); - let _bond_amount_handle = bond_handle(source, validator); - let bond_remain_handle = bond_handle(source, validator); + let bonds_handle = bond_handle(source, validator); + + println!("\nBonds before decrementing:"); + for ep in Epoch::default().iter_range(current_epoch.0 + 3) { + let delta = bonds_handle + .get_delta_val(storage, ep, ¶ms)? + .unwrap_or_default(); + if delta != 0 { + println!("bond ∆ at epoch {}: {}", ep, delta); + } + } // Make sure there are enough tokens left in the bond at the pipeline offset - let pipeline_epoch = current_epoch + params.pipeline_len; - let remaining_at_pipeline = bond_remain_handle + let remaining_at_pipeline = bonds_handle .get_sum(storage, pipeline_epoch, ¶ms)? .unwrap_or_default(); if amount > remaining_at_pipeline { @@ -1445,106 +1566,203 @@ where .into()); } - // Iterate thru this, find non-zero delta entries starting from most recent, - // then just start decrementing those values For every delta val that - // gets decremented down to 0, need a unique unbond object to have a clear - // start epoch + let unbonds = unbond_handle(source, validator); + // TODO: think if this should be +1 or not!!! + let withdrawable_epoch = current_epoch + + params.pipeline_len + + params.unbonding_len + + params.cubic_slashing_window_length; - // TODO: do we want to apply slashing here? (It is done here previously) - - let unbond_handle = unbond_handle(source, validator); - let withdrawable_epoch = - current_epoch + params.pipeline_len + params.unbonding_len; - let mut to_decrement = token::Amount::from_change(amount); + let mut remaining = token::Amount::from_change(amount); + let mut amount_after_slashing = token::Change::default(); - // We read all matched bonds into memory to do reverse iteration + // Iterate thru bonds, find non-zero delta entries starting from + // future-most, then decrement those values. For every val that + // gets decremented down to 0, need a unique unbond object. + // Read all matched bonds into memory to do reverse iteration #[allow(clippy::needless_collect)] - let bonds: Vec> = bond_remain_handle - .get_data_handler() - .iter(storage)? - .collect(); - // tracing::debug!("Bonds before decrementing:"); - // for ep in Epoch::default().iter_range(params.unbonding_len * 3) { - // tracing::debug!( - // "bond delta at epoch {}: {}", - // ep, - // bond_remain_handle - // .get_delta_val(storage, ep, ¶ms)? - // .unwrap_or_default() - // ) - // } + let bonds: Vec> = + bonds_handle.get_data_handler().iter(storage)?.collect(); + let mut bond_iter = bonds.into_iter().rev(); // Map: { bond start epoch, (new bond value, unbond value) } let mut new_bond_values_map = HashMap::::new(); - while to_decrement > token::Amount::default() { + while remaining > token::Amount::default() { let bond = bond_iter.next().transpose()?; if bond.is_none() { continue; } let (bond_epoch, bond_amnt) = bond.unwrap(); - let bond_amnt = token::Amount::from_change(bond_amnt); - - if to_decrement < bond_amnt { - // Decrement the amount in this bond and create the unbond object - // with amount `to_decrement` and starting epoch `bond_epoch` - let new_bond_amnt = bond_amnt - to_decrement; - new_bond_values_map - .insert(bond_epoch, (new_bond_amnt, to_decrement)); - to_decrement = token::Amount::default(); - } else { - // Set the bond remaining delta to 0 then continue decrementing - new_bond_values_map - .insert(bond_epoch, (token::Amount::default(), bond_amnt)); - to_decrement -= bond_amnt; + println!("\nBond (epoch, amnt) = ({}, {})", bond_epoch, bond_amnt); + println!("remaining = {}", remaining); + + let bond_amount = token::Amount::from_change(bond_amnt); + + let to_unbond = cmp::min(bond_amount, remaining); + let new_bond_amount = bond_amount - to_unbond; + new_bond_values_map.insert(bond_epoch, (new_bond_amount, to_unbond)); + println!("to_unbond (init) = {}", to_unbond); + + let mut slashes_for_this_bond = BTreeMap::::new(); + for slash in validator_slashes_handle(validator).iter(storage)? { + let slash = slash?; + if bond_epoch <= slash.epoch { + println!( + "Slash (epoch, rate) = ({}, {})", + &slash.epoch, &slash.rate + ); + let cur_rate = + slashes_for_this_bond.entry(slash.epoch).or_default(); + *cur_rate = cmp::min(*cur_rate + slash.rate, Decimal::ONE); + } } + + amount_after_slashing += + get_slashed_amount(¶ms, to_unbond, &slashes_for_this_bond)?; + println!("Cur amnt after slashing = {}", &amount_after_slashing); + + // Update the unbond records + let cur_amnt = unbond_records_handle(validator) + .at(&pipeline_epoch) + .get(storage, &bond_epoch)? + .unwrap_or_default(); + unbond_records_handle(validator) + .at(&pipeline_epoch) + .insert(storage, bond_epoch, cur_amnt + to_unbond)?; + + remaining -= to_unbond; } drop(bond_iter); // Write the in-memory bond and unbond values back to storage - for (bond_epoch, (new_bond_amnt, unbond_amnt)) in + for (bond_epoch, (new_bond_amount, unbond_amount)) in new_bond_values_map.into_iter() { - bond_remain_handle.set(storage, new_bond_amnt.into(), bond_epoch, 0)?; + bonds_handle.set(storage, new_bond_amount.into(), bond_epoch, 0)?; update_unbond( - &unbond_handle, + &unbonds, storage, &withdrawable_epoch, &bond_epoch, - unbond_amnt, + unbond_amount, )?; } - // tracing::debug!("Bonds after decrementing:"); - // for ep in Epoch::default().iter_range(params.unbonding_len * 3) { - // tracing::debug!( - // "bond delta at epoch {}: {}", - // ep, - // bond_remain_handle - // .get_delta_val(storage, ep, ¶ms)? - // .unwrap_or_default() - // ) - // } - - tracing::debug!("Updating validator set for unbonding"); - // Update the validator set at the pipeline offset - update_validator_set(storage, ¶ms, validator, -amount, current_epoch)?; + println!("Bonds after decrementing:"); + for ep in Epoch::default().iter_range(current_epoch.0 + 3) { + let delta = bonds_handle + .get_delta_val(storage, ep, ¶ms)? + .unwrap_or_default(); + if delta != 0 { + println!("bond ∆ at epoch {}: {}", ep, delta); + } + } + let stake_at_pipeline = + read_validator_stake(storage, ¶ms, validator, pipeline_epoch)? + .unwrap_or_default() + .change(); + let token_change = cmp::min(amount_after_slashing, stake_at_pipeline); + + // 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_at_pipeline = matches!( + validator_state_handle(validator) + .get(storage, pipeline_epoch, ¶ms)? + .unwrap(), + ValidatorState::Jailed + ); + if !is_jailed_at_pipeline { + tracing::debug!("Updating validator set for unbonding"); + update_validator_set( + storage, + ¶ms, + validator, + -token_change, + current_epoch, + )?; + } // Update the validator and total deltas at the pipeline offset update_validator_deltas( storage, ¶ms, validator, - -amount, + -token_change, + current_epoch, + params.pipeline_len, + )?; + update_total_deltas( + storage, + ¶ms, + -token_change, current_epoch, + params.pipeline_len, )?; - update_total_deltas(storage, ¶ms, -amount, current_epoch)?; Ok(()) } +/// 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. +/// +/// TODO: consider if we want to optimize this +fn get_slashed_amount( + params: &PosParams, + amount: token::Amount, + slashes: &BTreeMap, +) -> storage_api::Result { + println!("FN `get_slashed_amount`"); + + let mut updated_amount = amount; + let mut computed_amounts = Vec::::new(); + + for (infraction_epoch, slash_rate) in slashes { + println!("Slash epoch: {}, rate: {}", infraction_epoch, slash_rate); + 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 + // TODO: understand this better (from Informal) + // TODO: do bounds of this need to be changed with a +/- 1?? + if slashed_amount.epoch + params.unbonding_len < *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: decimal_mult_amount(*slash_rate, updated_amount), + epoch: *infraction_epoch, + }); + } + println!("Finished loop over slashes in `get_slashed_amount`"); + println!("Updated amount: {:?}", &updated_amount); + println!("Computed amounts: {:?}", &computed_amounts); + + 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.change()) +} + fn update_unbond( handle: &Unbonds, storage: &mut S, @@ -1583,6 +1801,11 @@ where // This will fail if the key is already being used try_insert_consensus_key(storage, consensus_key)?; + let pipeline_epoch = current_epoch + params.pipeline_len; + 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( @@ -1624,7 +1847,7 @@ where Ok(()) } -/// Withdraw. +/// Withdraw tokens from those that have been unbonded from proof-of-stake pub fn withdraw_tokens( storage: &mut S, source: Option<&Address>, @@ -1634,19 +1857,26 @@ pub fn withdraw_tokens( where S: StorageRead + StorageWrite, { - tracing::debug!("Withdrawing tokens in epoch {current_epoch}"); + println!("Withdrawing tokens in epoch {current_epoch}"); let params = read_pos_params(storage)?; let source = source.unwrap_or(validator); - let slashes = validator_slashes_handle(validator); let unbond_handle = unbond_handle(source, validator); + if unbond_handle.is_empty(storage)? { + return Err(WithdrawError::NoUnbondFound(BondId { + source: source.clone(), + validator: validator.clone(), + }) + .into()); + } - let mut slashed = token::Amount::default(); + let mut total_slashed = token::Amount::default(); let mut withdrawable_amount = token::Amount::default(); + // (withdraw_epoch, start_epoch) let mut unbonds_to_remove: Vec<(Epoch, Epoch)> = Vec::new(); - // TODO: use `find_unbonds` - let unbond_iter = unbond_handle.iter(storage)?; - for unbond in unbond_iter { + + for unbond in unbond_handle.iter(storage)? { + // println!("\nUNBOND ITER\n"); let ( NestedSubKey::Data { key: withdraw_epoch, @@ -1659,38 +1889,49 @@ where "Unbond delta ({start_epoch}..{withdraw_epoch}), amount {amount}", ); - // TODO: worry about updating this later after PR 740 perhaps - // 1. cubic slashing - // 2. adding slash rates in same epoch, applying cumulatively in dif + // TODO: adding slash rates in same epoch, applying cumulatively in dif // epochs if withdraw_epoch > current_epoch { tracing::debug!("Not yet withdrawable"); continue; } - for slash in slashes.iter(storage)? { - let Slash { - epoch, - block_height: _, - r#type: slash_type, - } = slash?; - if epoch > start_epoch - && epoch + let mut slashes_for_this_unbond = BTreeMap::::new(); + for slash in validator_slashes_handle(validator).iter(storage)? { + let slash = slash?; + if start_epoch <= slash.epoch + && slash.epoch < withdraw_epoch - .checked_sub(Epoch(params.unbonding_len)) - .unwrap_or_default() + - params.unbonding_len + - params.cubic_slashing_window_length { - let slash_rate = slash_type.get_slash_rate(¶ms); - let to_slash = token::Amount::from(decimal_mult_u64( - slash_rate, - u64::from(amount), - )); - slashed += to_slash; + println!( + "Slash (epoch, rate) = ({}, {})", + &slash.epoch, &slash.rate + ); + let cur_rate = + slashes_for_this_unbond.entry(slash.epoch).or_default(); + *cur_rate = cmp::min(*cur_rate + slash.rate, Decimal::ONE); } } - withdrawable_amount += amount; + + // let mut slashes_for_this_unbond = Vec::::new(); + // for slash in validator_slashes_handle(validator).iter(storage)? { + // let slash = slash?; + // if start_epoch <= slash.epoch + // && slash.epoch < withdraw_epoch - params.unbonding_len + // { + // slashes_for_this_unbond.push(slash); + // } + // } + let amount_after_slashing = + get_slashed_amount(¶ms, amount, &slashes_for_this_unbond)?; + + total_slashed += + amount - token::Amount::from_change(amount_after_slashing); + withdrawable_amount += + token::Amount::from_change(amount_after_slashing); unbonds_to_remove.push((withdraw_epoch, start_epoch)); } - withdrawable_amount -= slashed; tracing::debug!("Withdrawing total {withdrawable_amount}"); // Remove the unbond data from storage @@ -1703,7 +1944,7 @@ where // so, may need to implement remove/delete for nested map } - // Transfer the tokens from the PoS address back to the source + // Transfer the withdrawable tokens from the PoS address back to the source let staking_token = staking_token_address(storage); transfer_tokens( storage, @@ -1712,6 +1953,15 @@ where &ADDRESS, source, )?; + // Transfer the slashed tokens from the PoS address to the Slash Pool + // address + transfer_tokens( + storage, + &staking_token, + total_slashed, + &ADDRESS, + &SLASH_POOL_ADDRESS, + )?; Ok(withdrawable_amount) } @@ -1754,7 +2004,7 @@ where return Ok(()); } let rate_before_pipeline = commission_handle - .get(storage, pipeline_epoch - 1, ¶ms)? + .get(storage, pipeline_epoch.prev(), ¶ms)? .expect("Could not find a rate in given epoch"); let change_from_prev = new_rate - rate_before_pipeline; if change_from_prev.abs() > max_change.unwrap() { @@ -1768,65 +2018,6 @@ where commission_handle.set(storage, new_rate, current_epoch, params.pipeline_len) } -/// apply a slash and write it to storage -pub fn slash( - storage: &mut S, - params: &PosParams, - current_epoch: Epoch, - evidence_epoch: Epoch, - evidence_block_height: impl Into, - slash_type: SlashType, - validator: &Address, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - let rate = slash_type.get_slash_rate(params); - let slash = Slash { - epoch: evidence_epoch, - block_height: evidence_block_height.into(), - r#type: slash_type, - }; - - let current_stake = - read_validator_stake(storage, params, validator, current_epoch)? - .unwrap_or_default(); - let slashed_amount = decimal_mult_u64(rate, u64::from(current_stake)); - let token_change = -token::Change::from(slashed_amount); - - // Update validator sets and deltas at the pipeline length - update_validator_set( - storage, - params, - validator, - token_change, - current_epoch, - )?; - update_validator_deltas( - storage, - params, - validator, - token_change, - current_epoch, - )?; - update_total_deltas(storage, params, token_change, current_epoch)?; - - // Write the validator slash to storage - validator_slashes_handle(validator).push(storage, slash)?; - - // Transfer the slashed tokens from PoS account to Slash Fund address - let staking_token = staking_token_address(storage); - transfer_tokens( - storage, - &staking_token, - token::Amount::from(slashed_amount), - &ADDRESS, - &SLASH_POOL_ADDRESS, - )?; - - Ok(()) -} - /// Transfer tokens between accounts /// TODO: may want to move this into core crate pub fn transfer_tokens( @@ -1902,7 +2093,7 @@ where handle.contains(storage, consensus_key) } -/// Get the total bond amount for a given bond ID at a given epoch +/// Get the total bond amount, including slashes, for a given bond ID and epoch pub fn bond_amount( storage: &S, params: &PosParams, @@ -1923,17 +2114,27 @@ where // if bond_epoch > epoch { // break; // } - for slash in slashes.iter() { + + // TODO: do we need to consider the adjusted amounts of previous bonds + // and their slashes when iterating? + for slash in &slashes { let Slash { epoch: slash_epoch, block_height: _, r#type: slash_type, + rate: _, } = slash; - if slash_epoch > &bond_epoch { + if *slash_epoch < bond_epoch { continue; } - let current_slashed = - decimal_mult_i128(slash_type.get_slash_rate(params), delta); + // TODO: consider edge cases with the cubic slashing window + let cubic_rate = get_final_cubic_slash_rate( + storage, + params, + *slash_epoch, + *slash_type, + )?; + let current_slashed = decimal_mult_i128(cubic_rate, delta); let delta = token::Amount::from_change(delta - current_slashed); total += delta; if bond_epoch <= epoch { @@ -2181,7 +2382,7 @@ where validator_slashes_handle(validator).iter(storage)?.collect() } -/// Find bond deltas for the given source and validator address. +/// Find raw bond deltas for the given source and validator address. pub fn find_bonds( storage: &S, source: &Address, @@ -2196,7 +2397,7 @@ where .collect() } -/// Find unbond deltas for the given source and validator address. +/// Find raw unbond deltas for the given source and validator address. pub fn find_unbonds( storage: &S, source: &Address, @@ -2484,21 +2685,32 @@ where Ok(HashMap::from_iter([(bond_id, details)])) } +// TODO: update for cubic slashing fn make_bond_details( - _storage: &S, + storage: &S, params: &PosParams, validator: &Address, change: token::Change, start: Epoch, slashes: &[Slash], applied_slashes: &mut HashMap>, -) -> BondDetails { +) -> BondDetails +where + S: StorageRead, +{ let amount = token::Amount::from_change(change); let slashed_amount = slashes .iter() .fold(None, |acc: Option, slash| { if slash.epoch >= start { + let slash_rate = get_final_cubic_slash_rate( + storage, + params, + slash.epoch, + slash.r#type, + ) + .unwrap(); let validator_slashes = applied_slashes.entry(validator.clone()).or_default(); if !validator_slashes.contains(slash) { @@ -2506,10 +2718,7 @@ fn make_bond_details( } return Some( acc.unwrap_or_default() - + mult_change_to_amount( - slash.r#type.get_slash_rate(params), - change, - ), + + mult_change_to_amount(slash_rate, change), ); } None @@ -2521,15 +2730,19 @@ fn make_bond_details( } } +// TODO: update for cubic slashing fn make_unbond_details( - _storage: &S, + storage: &S, params: &PosParams, validator: &Address, amount: token::Amount, (start, withdraw): (Epoch, Epoch), slashes: &[Slash], applied_slashes: &mut HashMap>, -) -> UnbondDetails { +) -> UnbondDetails +where + S: StorageRead, +{ let slashed_amount = slashes .iter() @@ -2540,6 +2753,13 @@ fn make_unbond_details( .checked_sub(Epoch(params.unbonding_len)) .unwrap_or_default() { + let slash_rate = get_final_cubic_slash_rate( + storage, + params, + slash.epoch, + slash.r#type, + ) + .unwrap(); let validator_slashes = applied_slashes.entry(validator.clone()).or_default(); if !validator_slashes.contains(slash) { @@ -2547,10 +2767,7 @@ fn make_unbond_details( } return Some( acc.unwrap_or_default() - + mult_amount( - slash.r#type.get_slash_rate(params), - amount, - ), + + decimal_mult_amount(slash_rate, amount), ); } None @@ -2607,6 +2824,12 @@ where 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) { + continue; + } let stake_from_deltas = read_validator_stake(storage, ¶ms, &validator_address, epoch)? @@ -2703,3 +2926,787 @@ where Ok(()) } + +/// Calculate the cubic slashing rate using all slashes within a window around +/// the given infraction epoch +pub fn compute_cubic_slash_rate( + storage: &S, + params: &PosParams, + infraction_epoch: Epoch, +) -> storage_api::Result +where + S: StorageRead, +{ + println!("COMPUTING CUBIC SLASH RATE"); + let mut sum_vp_fraction = Decimal::ZERO; + let start_epoch = infraction_epoch + .sub_or_default(Epoch(params.cubic_slashing_window_length)); + let end_epoch = infraction_epoch + params.cubic_slashing_window_length; + + for epoch in Epoch::iter_bounds_inclusive(start_epoch, end_epoch) { + let consensus_stake = + Decimal::from(get_total_consensus_stake(storage, epoch)?); + println!("Consensus stake in epoch {}: {}", epoch, consensus_stake); + + let processing_epoch = epoch + + params.unbonding_len + + 1_u64 + + params.cubic_slashing_window_length; + let slashes = enqueued_slashes_handle().at(&processing_epoch); + let infracting_stake = slashes + .iter(storage)? + .map(|res| { + let ( + NestedSubKey::Data { + key: validator, + nested_sub_key: _, + }, + _slash, + ) = res?; + + let validator_stake = + read_validator_stake(storage, params, &validator, epoch)? + .unwrap_or_default(); + println!("Val {} stake: {}", &validator, validator_stake); + + Ok(Decimal::from(validator_stake)) + // TODO: does something more complex need to be done + // here in the event some of these slashes correspond to + // the same validator? + }) + .sum::>()?; + sum_vp_fraction += infracting_stake / consensus_stake; + } + println!("sum_vp_fraction: {}", sum_vp_fraction); + + // TODO: make sure `sum_vp_fraction` does not exceed 1/3 or handle with care + // another way + Ok(dec!(9) * sum_vp_fraction * sum_vp_fraction) +} + +/// Get final cubic slashing rate that is bound from below by some minimum value +/// and capped at 100% +pub fn get_final_cubic_slash_rate( + storage: &S, + params: &PosParams, + infraction_epoch: Epoch, + current_slash_type: SlashType, +) -> storage_api::Result +where + S: StorageRead, +{ + let cubic_rate = + compute_cubic_slash_rate(storage, params, infraction_epoch)?; + // Need some truncation right now to max the rate at 100% + let rate = cubic_rate + .clamp(current_slash_type.get_slash_rate(params), Decimal::ONE); + Ok(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. +pub fn slash( + storage: &mut S, + params: &PosParams, + current_epoch: Epoch, + evidence_epoch: Epoch, + evidence_block_height: impl Into, + slash_type: SlashType, + validator: &Address, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + println!("SLASHING ON NEW EVIDENCE FROM {}", validator); + println!( + "Current state = {:?}", + validator_state_handle(validator) + .get(storage, current_epoch, params) + .unwrap() + .unwrap() + ); + + 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: Decimal::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.unbonding_len + + params.cubic_slashing_window_length + + 1_u64; + let pipeline_epoch = current_epoch + params.pipeline_len; + + // 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)?; + } + + // Remove the validator from the set starting at the next epoch and up thru + // the pipeline epoch. + for epoch in + Epoch::iter_bounds_inclusive(current_epoch.next(), pipeline_epoch) + { + let prev_state = validator_state_handle(validator) + .get(storage, epoch, params)? + .expect("Expected to find a valid validator."); + match prev_state { + ValidatorState::Consensus => { + let amount_pre = validator_deltas_handle(validator) + .get_sum(storage, epoch, params)? + .unwrap_or_default(); + 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(&token::Amount::from_change(amount_pre)) + .remove(storage, &val_position)?; + + // 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, + )?; + } + } + } + ValidatorState::BelowCapacity => { + let amount_pre = validator_deltas_handle(validator) + .get_sum(storage, epoch, params)? + .unwrap_or_default(); + 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)?; + } + ValidatorState::Inactive => { + println!("INACTIVE"); + panic!( + "SHouldn't be here - haven't implemented inactive vals yet" + ) + } + ValidatorState::Jailed => { + println!("Validator already jailed"); + // return Ok(()); + } + } + } + + println!( + "\nWRITING VALIDATOR {} STATE AS JAILED STARTING IN EPOCH {}\n", + validator, + current_epoch.next() + ); + // Set the validator state as `Jailed` thru the pipeline epoch + for offset in 1..=params.pipeline_len { + validator_state_handle(validator).set( + storage, + ValidatorState::Jailed, + current_epoch, + offset, + )?; + } + + // Debugging + // println!("POST Validator Set"); + + // for offset in 0..=params.pipeline_len { + // println!("Epoch {}", current_epoch.0 + offset); + // for wv in read_consensus_validator_set_addresses_with_stake( + // storage, + // current_epoch + offset, + // )? { + // println!( + // "Consensus val {}, stake {}, state {:?}", + // &wv.address, + // u64::from(wv.bonded_stake), + // validator_state_handle(&wv.address).get( + // storage, + // current_epoch + offset, + // params + // ) + // ); + // } + // for wv in read_below_capacity_validator_set_addresses_with_stake( + // storage, + // current_epoch + offset, + // )? { + // println!( + // "Below-cap val {}, stake {}, state {:?}", + // &wv.address, + // u64::from(wv.bonded_stake), + // validator_state_handle(&wv.address).get( + // storage, + // current_epoch + offset, + // params + // ) + // ); + // } + // } + + // No other actions are performed here until the epoch in which the slash is + // processed. + + Ok(()) +} + +/// Process slashes that have been queued up after discovery. Calculate the +/// cubic slashing rate, store the finalized slashes, update the deltas, then +/// transfer slashed tokens from PoS to the Slash Pool. This function is called +/// at the beginning of the epoch that is `unbonding_length + 1 + +/// cubic_slashing_window_length` epochs after the infraction epoch. +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.unbonding_len + 1 + params.cubic_slashing_window_length + { + return Ok(()); + } + let infraction_epoch = current_epoch + - params.unbonding_len + - params.cubic_slashing_window_length + - 1; + + // Slashes to be processed in the current epoch + let enqueued_slashes = enqueued_slashes_handle().at(¤t_epoch); + if enqueued_slashes.is_empty(storage)? { + println!("No slashes found"); + return Ok(()); + } + println!("Found slashes"); + + // 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 validators_and_slashes: HashMap> = + HashMap::new(); + 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( + Decimal::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, + }; + println!( + "Processing slash for val {} committed in epoch {}, with rate {}", + &validator, enqueued_slash.epoch, slash_rate + ); + + let cur_slashes = validators_and_slashes.entry(validator).or_default(); + cur_slashes.push(updated_slash); + } + + let mut deltas_for_update: HashMap> = + HashMap::new(); + + // Store the final processed slashes to their corresponding validators, then + // update the deltas + for (validator, enqueued_slashes) in validators_and_slashes.into_iter() { + let validator_stake_at_infraction = read_validator_stake( + storage, + ¶ms, + &validator, + infraction_epoch, + )? + .unwrap_or_default(); + + println!( + "Val {} stake at infraction epoch {} = {}", + &validator, + infraction_epoch, + u64::from(validator_stake_at_infraction) + ); + + let mut total_rate = Decimal::ZERO; + + for enqueued_slash in &enqueued_slashes { + // Add this slash to the list of validator's slashes in storage + validator_slashes_handle(&validator) + .push(storage, enqueued_slash.clone())?; + + total_rate += enqueued_slash.rate; + } + total_rate = cmp::min(Decimal::ONE, total_rate); + + // Find the total amount deducted from the deltas due to unbonds that + // became active after the infraction epoch, accounting for slashes + let mut total_unbonded = token::Amount::default(); + + // Start from after the infraction epoch up thru last epoch before + // processing + for epoch in Epoch::iter_bounds_inclusive( + infraction_epoch.next(), + current_epoch.prev(), /* TODO: should this have a prev() or + * should it even go to pipeline ??? */ + ) { + println!("\nEpoch {}", epoch); + let unbonds = unbond_records_handle(&validator).at(&epoch); + for unbond in unbonds.iter(storage)? { + let (start, unbond_amount) = unbond?; + println!( + "UnbondRecord: amount = {}, start_epoch {}", + &u64::from(unbond_amount), + &start + ); + if start > infraction_epoch { + continue; + } + + let mut prev_slashes = BTreeMap::::new(); + for val_slash in + validator_slashes_handle(&validator).iter(storage)? + { + let val_slash = val_slash?; + println!( + "Past slash at epoch {} with rate {}", + val_slash.epoch, val_slash.rate + ); + // TODO: is the 2nd condition correct?? + if start <= val_slash.epoch + && val_slash.epoch + params.unbonding_len + < infraction_epoch + // TODO: this `<` should maybe be a `<=` + { + println!("Collecting this slash"); + let cur_rate = + prev_slashes.entry(val_slash.epoch).or_default(); + *cur_rate = + cmp::min(*cur_rate + val_slash.rate, Decimal::ONE); + } + } + println!("Slashes for this unbond: {:?}", prev_slashes); + + total_unbonded += token::Amount::from_change( + get_slashed_amount(¶ms, unbond_amount, &prev_slashes)?, + ); + + println!( + "Total unbonded (epoch {}) w slashing = {}", + epoch, total_unbonded + ); + } + } + println!("Computing adjusted amounts now"); + + // How to handle if there is are slashes from earlier epochs that were + // not processed by this current infraction epoch (so were recently + // processed before this current epoch). + // + // Bonds that became + + // TODO: optimize this, maybe make the validator slashes a map from + // epoch to slash + // let prev_slash_epoch = validator_slashes_handle(&validator) + // .iter(storage)? + // .fold(None, |acc, s| { + // let slash_epoch = s.as_ref().unwrap().epoch; + // if slash_epoch > infraction_epoch { + // acc + // } else if acc.is_none() { + // Some(slash_epoch) + // } else if acc.unwrap() < slash_epoch { + // Some(slash_epoch) + // } else { + // acc + // } + // }); + // let prev_total_rate = if let Some(epoch) = prev_slash_epoch { + // validator_slashes_handle(&validator).iter(storage)?.fold( + // Decimal::ZERO, + // |acc, s| { + // let slash_epoch = s.as_ref().unwrap().epoch; + // if acc > Decimal::ONE { + // Decimal::ONE + // } else if slash_epoch == epoch { + // acc + s.as_ref().unwrap().rate + // } else { + // acc + // } + // }, + // ) + // } else { + // Decimal::ZERO + // }; + + // Compute the adjusted validator deltas and slashed amounts from the + // current up until the pipeline epoch + let mut last_slash = token::Change::default(); + for offset in 0..params.pipeline_len { + println!( + "Epoch {}\nLast slash = {}", + current_epoch + offset, + last_slash + ); + let unbonds = + unbond_records_handle(&validator).at(&(current_epoch + offset)); + + for unbond in unbonds.iter(storage)? { + let (start, unbond_amount) = unbond?; + println!( + "UnbondRecord: amount = {}, start_epoch {}", + &unbond_amount, &start + ); + if start > infraction_epoch { + continue; + } + + let mut prev_slashes = BTreeMap::::new(); + for val_slash in + validator_slashes_handle(&validator).iter(storage)? + { + let val_slash = val_slash?; + println!( + "Past slash at epoch {} with rate {}", + val_slash.epoch, val_slash.rate + ); + if start <= val_slash.epoch + && val_slash.epoch + params.unbonding_len + < infraction_epoch + // TODO: this `<` should maybe be a `<=` + { + println!("Collecting this slash"); + let cur_rate = + prev_slashes.entry(val_slash.epoch).or_default(); + *cur_rate = + cmp::min(*cur_rate + val_slash.rate, Decimal::ONE); + } + } + println!("Slashes for this unbond: {:?}", prev_slashes); + + total_unbonded += token::Amount::from_change( + get_slashed_amount(¶ms, unbond_amount, &prev_slashes)?, + ); + println!( + "Total unbonded (offset {}) w slashing = {}", + offset, total_unbonded + ); + } + + let this_slash = decimal_mult_amount( + total_rate, + validator_stake_at_infraction - total_unbonded, + ) + .change(); + println!("This slash = {}", this_slash); + + let diff_slashed_amount = last_slash - this_slash; + println!("Diff slashed amount = {}", diff_slashed_amount); + + let val_updates = + deltas_for_update.entry(validator.clone()).or_default(); + val_updates.push((offset, diff_slashed_amount)); + + // total_slashed -= diff_slashed_amount; + last_slash = this_slash; + // total_unbonded = token::Amount::default(); + } + } + println!("\nUpdating deltas"); + // Update the deltas in storage + let mut total_slashed = token::Change::default(); + for (validator, updates) in deltas_for_update { + for (offset, delta) in updates { + println!("Val {}, offset {}, delta {}", &validator, offset, delta); + let validator_stake_at_offset = read_validator_stake( + storage, + ¶ms, + &validator, + current_epoch + offset, + )? + .unwrap_or_default() + .change(); + // Want to compute the of sum of remaining bonds that have become + // active after the infraction epoch (this may be + // computationally expensive) + let sum_post_bonds = get_validator_bond_sums( + storage, + &validator, + infraction_epoch.next(), + current_epoch + offset, + )?; + println!("\nUnslashable bonds = {}", sum_post_bonds); + let slashable_stake_at_offset = + validator_stake_at_offset - sum_post_bonds.change(); + // let slashable_stake_at_offset = validator_stake_at_offset; + assert!(slashable_stake_at_offset >= token::Change::default()); + + println!("Stake at offset = {}", validator_stake_at_offset); + println!( + "Slashable stake at offset = {}", + slashable_stake_at_offset + ); + let change = if slashable_stake_at_offset + delta + < token::Change::default() + { + -slashable_stake_at_offset + } else { + delta + }; + println!("Change = {}", change); + total_slashed -= change; + + update_validator_deltas( + storage, + ¶ms, + &validator, + change, + current_epoch, + offset, + )?; + update_total_deltas( + storage, + ¶ms, + change, + current_epoch, + offset, + )?; + } + } + + println!("Total slashed = {}", total_slashed); + debug_assert!(total_slashed >= token::Change::default()); + + // Transfer all slashed tokens from PoS account to Slash Pool address + let staking_token = staking_token_address(storage); + transfer_tokens( + storage, + &staking_token, + token::Amount::from_change(total_slashed), + &ADDRESS, + &SLASH_POOL_ADDRESS, + )?; + + Ok(()) +} + +/// Unjail a validator that is currently jailed +pub fn unjail_validator( + storage: &mut S, + validator: &Address, + current_epoch: Epoch, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let params = read_pos_params(storage)?; + + // Check that the validator is jailed up to the pipeline epoch + for epoch in current_epoch.iter_range(params.pipeline_len + 1) { + let state = validator_state_handle(validator).get( + storage, + current_epoch, + ¶ms, + )?; + if let Some(state) = state { + if state != ValidatorState::Jailed { + return Err(UnjailValidatorError::NotJailed( + validator.clone(), + epoch, + ) + .into()); + } + } else { + return Err( + UnjailValidatorError::NotAValidator(validator.clone()).into() + ); + } + } + + // Check that the unjailing tx can be submitted given the current epoch + // and the most recent infraction epoch + let last_slash_epoch = read_validator_last_slash_epoch(storage, validator)? + .unwrap_or_default(); + let eligible_epoch = last_slash_epoch + + params.unbonding_len + + params.cubic_slashing_window_length; // TODO: check this is the correct epoch to submit this tx + if current_epoch < eligible_epoch { + return Err(UnjailValidatorError::NotEligible( + validator.clone(), + eligible_epoch, + current_epoch, + ) + .into()); + } + // TODO: any other checks that are needed? (deltas, etc)? + + // Re-insert the validator into the validator set and update its state + let pipeline_epoch = current_epoch + params.pipeline_len; + let stake = + read_validator_stake(storage, ¶ms, validator, pipeline_epoch)? + .unwrap_or_default(); + dbg!(&stake); + + insert_validator_into_validator_set( + storage, + ¶ms, + validator, + stake, + current_epoch, + params.pipeline_len, + )?; + Ok(()) +} + +/// Check if a validator is frozen. A validator is frozen until after all of its +/// enqueued slashes have been processed, i.e. until `unbonding_len + 1 + +/// cubic_slashing_window_length` epochs after its most recent infraction epoch. +pub fn is_validator_frozen( + storage: &S, + validator: &Address, + current_epoch: Epoch, + params: &PosParams, +) -> storage_api::Result +where + S: StorageRead, +{ + let last_infraction_epoch = + read_validator_last_slash_epoch(storage, validator)?; + if let Some(last_epoch) = last_infraction_epoch { + let is_frozen = current_epoch + < last_epoch + + params.unbonding_len + + 1_u64 + + params.cubic_slashing_window_length; + Ok(is_frozen) + } else { + Ok(false) + } +} + +fn get_total_consensus_stake( + storage: &S, + epoch: Epoch, +) -> storage_api::Result +where + S: StorageRead, +{ + let mut total = token::Amount::default(); + for res in consensus_validator_set_handle().at(&epoch).iter(storage)? { + let ( + NestedSubKey::Data { + key: bonded_stake, + nested_sub_key: _, + }, + _validator, + ) = res?; + total += bonded_stake; + } + Ok(total) +} + +fn get_validator_bond_sums( + storage: &S, + validator: &Address, + start_epoch: Epoch, + end_epoch: Epoch, +) -> storage_api::Result +where + S: StorageRead, +{ + let prefix = bonds_prefix(); + // We have to iterate raw bytes, cause the epoched data `last_update` field + // gets matched here too + let 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 start < start_epoch || start > end_epoch { + return None; + } + if validator.clone() != bond_id.validator { + return None; + } + + let change: token::Change = + BorshDeserialize::try_from_slice(&val_bytes).ok()?; + println!("Bond start, amnt = ({}, {})", start, change); + return Some(change); + } + } + None + }); + Ok(raw_bonds.fold(token::Amount::default(), |acc, delta| { + acc + token::Amount::from_change(delta) + })) +} diff --git a/proof_of_stake/src/parameters.rs b/proof_of_stake/src/parameters.rs index 1422971089..351ea11368 100644 --- a/proof_of_stake/src/parameters.rs +++ b/proof_of_stake/src/parameters.rs @@ -39,6 +39,9 @@ pub struct PosParams { /// Fraction of validator's stake that should be slashed on a light client /// attack. pub light_client_attack_min_slash_rate: Decimal, + /// Number of epochs above and below (separately) the current epoch to + /// consider when doing cubic slashing + pub cubic_slashing_window_length: u64, } impl Default for PosParams { @@ -60,6 +63,7 @@ impl Default for PosParams { duplicate_vote_min_slash_rate: dec!(0.001), // slash 0.1% light_client_attack_min_slash_rate: dec!(0.001), + cubic_slashing_window_length: 1, } } } @@ -177,8 +181,8 @@ pub mod testing { prop_compose! { /// Generate arbitrary valid ([`PosParams::validate`]) PoS parameters. pub fn arb_pos_params(num_max_validator_slots: Option) - (pipeline_len in 2..8_u64) - (max_validator_slots in 1..num_max_validator_slots.unwrap_or(128), + (pipeline_len in Just(2)) + (max_validator_slots in 3..num_max_validator_slots.unwrap_or(128), // `unbonding_len` > `pipeline_len` unbonding_len in pipeline_len + 1..pipeline_len + 8, pipeline_len in Just(pipeline_len), diff --git a/proof_of_stake/src/storage.rs b/proof_of_stake/src/storage.rs index 6b7a9add98..5b17bcdcd1 100644 --- a/proof_of_stake/src/storage.rs +++ b/proof_of_stake/src/storage.rs @@ -9,7 +9,9 @@ use crate::epoched::LAZY_MAP_SUB_KEY; pub use crate::types::*; // TODO: not sure why this needs to be public const PARAMS_STORAGE_KEY: &str = "params"; -const VALIDATOR_STORAGE_PREFIX: &str = "validator"; +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_STATE_STORAGE_KEY: &str = "state"; @@ -23,11 +25,13 @@ const VALIDATOR_DELEGATION_REWARDS_PRODUCT_KEY: &str = 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_UNBONDED_STORAGE_KEY: &str = "total_unbonded"; const VALIDATOR_SETS_STORAGE_PREFIX: &str = "validator_sets"; const CONSENSUS_VALIDATOR_SET_STORAGE_KEY: &str = "consensus"; -const NUM_CONSENSUS_VALIDATORS_STORAGE_KEY: &str = "num_consensus"; const BELOW_CAPACITY_VALIDATOR_SET_STORAGE_KEY: &str = "below_capacity"; const TOTAL_DELTAS_STORAGE_KEY: &str = "total_deltas"; const VALIDATOR_SET_POSITIONS_KEY: &str = "validator_set_positions"; @@ -254,18 +258,25 @@ pub fn validator_state_key(validator: &Address) -> Key { } /// Is storage key for validator's state? -pub fn is_validator_state_key(key: &Key) -> Option<&Address> { +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 => + && key == VALIDATOR_STATE_STORAGE_KEY + && lazy_map == LAZY_MAP_SUB_KEY + && data == lazy_map::DATA_SUBKEY => { - Some(validator) + let epoch = Epoch::parse(epoch.clone()) + .expect("Should be able to parse the epoch"); + Some((validator, epoch)) } _ => None, } @@ -301,6 +312,13 @@ pub fn is_validator_deltas_key(key: &Key) -> Option<&Address> { } } +/// 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()) @@ -308,6 +326,14 @@ pub fn slashes_prefix() -> Key { .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() @@ -315,7 +341,7 @@ pub fn validator_slashes_key(validator: &Address) -> Key { .expect("Cannot obtain a storage key") } -/// NEW: Is storage key for validator's slashes +/// 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[..] { @@ -338,6 +364,14 @@ pub fn is_validator_slashes_key(key: &Key) -> Option
{ } } +/// 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()) @@ -450,6 +484,13 @@ pub fn is_unbond_key(key: &Key) -> Option<(BondId, Epoch, Epoch)> { } } +/// 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()) @@ -464,13 +505,6 @@ pub fn consensus_validator_set_key() -> Key { .expect("Cannot obtain a storage key") } -/// Storage key for the number of consensus validators -pub fn num_consensus_validators_key() -> Key { - validator_sets_prefix() - .push(&NUM_CONSENSUS_VALIDATORS_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() diff --git a/proof_of_stake/src/types.rs b/proof_of_stake/src/types.rs index 3d546397bc..9233b280af 100644 --- a/proof_of_stake/src/types.rs +++ b/proof_of_stake/src/types.rs @@ -24,7 +24,7 @@ use rust_decimal::prelude::{Decimal, ToPrimitive}; use crate::parameters::PosParams; -// const U64_MAX: u64 = u64::MAX; +const U64_MAX: u64 = u64::MAX; // TODO: add this to the spec /// Stored positions of validators in validator sets @@ -128,15 +128,55 @@ pub type CommissionRates = pub type Bonds = crate::epoched::EpochedDelta< token::Change, crate::epoched::OffsetPipelineLen, + U64_MAX, +>; + +/// An epoched lazy set of all known active validator addresses (consensus, +/// below-capacity, jailed) +pub type ValidatorAddresses = crate::epoched::NestedEpoched< + LazySet
, + crate::epoched::OffsetPipelineLen, +>; + +/// Slashes indexed by validator address and then block height (for easier +/// retrieval and iteration when processing) +pub type ValidatorSlashes = NestedMap; + +/// Epoched slashes, where the outer epoch key is the epoch in which the slash +/// is processed +/// NOTE: the `enqueued_slashes_handle` this is used for shouldn't need these +/// slashes earlier than `cubic_window_width` epochs behind the current +pub type EpochedSlashes = crate::epoched::NestedEpoched< + ValidatorSlashes, + crate::epoched::OffsetUnbondingLen, 23, >; -/// Epochs validator's unbonds +/// Epoched validator's unbonds pub type Unbonds = NestedMap>; /// Consensus keys set, used to ensure uniqueness pub type ConsensusKeys = LazySet; +/// Total unbonded for validators needed for slashing computations. +/// The outer `Epoch` corresponds to the epoch at which the unbond is active +/// (affects the deltas, pipeline after submission). The inner `Epoch` +/// corresponds to the epoch from which the underlying bond became active +/// (affected deltas). +pub type ValidatorUnbondRecords = + NestedMap>; + +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Eq, Hash, PartialEq, +)] +/// TODO: slashed amount for thing +pub struct SlashedAmount { + /// Perlangus + pub amount: token::Amount, + /// Churms + pub epoch: Epoch, +} + #[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] /// Commission rate and max commission rate change per epoch for a validator pub struct CommissionPair { @@ -325,6 +365,9 @@ pub enum ValidatorState { /// A validator who is deactivated via a tx when a validator no longer /// wants to be one (not implemented yet) Inactive, + /// A `Jailed` validator has been prohibited from participating in + /// consensus due to a misbehavior + Jailed, } /// A slash applied to validator, to punish byzantine behavior by removing @@ -348,16 +391,19 @@ pub struct Slash { pub block_height: u64, /// A type of slashable event. pub r#type: SlashType, + /// The cubic slashing rate for this validator + pub rate: Decimal, } /// Slashes applied to validator, to punish byzantine behavior by removing /// their staked tokens at and before the epoch of the slash. pub type Slashes = LazyVec; -/// A type of slashsable event. +/// A type of slashable event. #[derive( Debug, Clone, + Copy, BorshDeserialize, BorshSerialize, BorshSchema, @@ -491,7 +537,10 @@ pub fn mult_change_to_amount( /// Multiply a value of type Decimal with one of type Amount and then return the /// truncated Amount -pub fn mult_amount(dec: Decimal, amount: token::Amount) -> token::Amount { +pub fn decimal_mult_amount( + dec: Decimal, + amount: token::Amount, +) -> token::Amount { let prod = dec * Decimal::from(amount); // truncate the number to the floor token::Amount::from(prod.to_u64().expect("Product is out of bounds")) diff --git a/shared/src/ledger/args.rs b/shared/src/ledger/args.rs index 738c371ef3..5d7878bf4d 100644 --- a/shared/src/ledger/args.rs +++ b/shared/src/ledger/args.rs @@ -329,6 +329,17 @@ pub struct TxCommissionRateChange { pub tx_code_path: C::Data, } +#[derive(Clone, Debug)] +/// Re-activate a jailed validator args +pub struct TxUnjailValidator { + /// Common tx arguments + pub tx: Tx, + /// Validator address (should be self) + pub validator: C::Address, + /// Path to the TX WASM code file + pub tx_code_path: C::Data, +} + /// Query PoS commission rate #[derive(Clone, Debug)] pub struct QueryCommissionRate { diff --git a/shared/src/ledger/tx.rs b/shared/src/ledger/tx.rs index 8b2a9d85ed..2c2ada4f2e 100644 --- a/shared/src/ledger/tx.rs +++ b/shared/src/ledger/tx.rs @@ -605,6 +605,47 @@ pub async fn submit_validator_commission_change< Ok(()) } +/// Submit transaction to unjail a jailed validator +pub async fn submit_unjail_validator< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + args: args::TxUnjailValidator, +) -> Result<(), Error> { + let args::TxUnjailValidator { + tx: tx_args, + validator, + tx_code_path, + } = args; + if !rpc::is_validator(client, &validator).await { + eprintln!("The given address {validator} is not a validator."); + if !tx_args.force { + return Err(Error::InvalidValidatorAddress(validator)); + } + } + + let data = validator + .try_to_vec() + .expect("Encoding tx data shouldn't fail"); + + let chain_id = tx_args.chain_id.clone().unwrap(); + let tx = Tx::new(tx_code_path, Some(data), chain_id, tx_args.expiration); + let default_signer = validator; + process_tx( + client, + wallet, + &tx_args, + tx, + TxSigningKey::WalletAddress(default_signer), + #[cfg(not(feature = "mainnet"))] + false, + ) + .await?; + Ok(()) +} + /// Submit transaction to withdraw an unbond pub async fn submit_withdraw< C: crate::ledger::queries::Client + Sync, diff --git a/tests/src/native_vp/pos.rs b/tests/src/native_vp/pos.rs index b60e627804..0848597270 100644 --- a/tests/src/native_vp/pos.rs +++ b/tests/src/native_vp/pos.rs @@ -568,7 +568,7 @@ pub mod testing { use namada::proof_of_stake::storage::BondId; use namada::proof_of_stake::types::ValidatorState; use namada::proof_of_stake::{ - read_num_consensus_validators, read_pos_params, unbond_handle, + get_num_consensus_validators, read_pos_params, unbond_handle, ADDRESS as POS_ADDRESS, }; use namada::types::key::common::PublicKey; @@ -1566,10 +1566,10 @@ pub mod testing { /// Find if there are any vacant consensus validator slots pub fn has_vacant_consensus_validator_slots( params: &PosParams, - _current_epoch: Epoch, + current_epoch: Epoch, ) -> bool { let num_consensus_validators = - read_num_consensus_validators(tx::ctx()).unwrap(); + get_num_consensus_validators(tx::ctx(), current_epoch).unwrap(); params.max_validator_slots > num_consensus_validators } } diff --git a/tx_prelude/src/proof_of_stake.rs b/tx_prelude/src/proof_of_stake.rs index 22c00ca599..3f80556aca 100644 --- a/tx_prelude/src/proof_of_stake.rs +++ b/tx_prelude/src/proof_of_stake.rs @@ -5,7 +5,7 @@ use namada_core::types::{key, token}; pub use namada_proof_of_stake::parameters::PosParams; use namada_proof_of_stake::{ become_validator, bond_tokens, change_validator_commission_rate, - read_pos_params, unbond_tokens, withdraw_tokens, + read_pos_params, unbond_tokens, unjail_validator, withdraw_tokens, }; pub use namada_proof_of_stake::{parameters, types}; use rust_decimal::Decimal; @@ -61,6 +61,12 @@ impl Ctx { change_validator_commission_rate(self, validator, *rate, current_epoch) } + /// Unjail a jailed validator and re-enter the validator sets. + pub fn unjail_validator(&mut self, validator: &Address) -> TxResult { + let current_epoch = self.get_block_epoch()?; + unjail_validator(self, validator, current_epoch) + } + /// NEW: Attempt to initialize a validator account. On success, returns the /// initialized validator account's address. pub fn init_validator( diff --git a/wasm/wasm_source/src/lib.rs b/wasm/wasm_source/src/lib.rs index 98704112f2..f051dd64c5 100644 --- a/wasm/wasm_source/src/lib.rs +++ b/wasm/wasm_source/src/lib.rs @@ -16,6 +16,8 @@ pub mod tx_reveal_pk; pub mod tx_transfer; #[cfg(feature = "tx_unbond")] pub mod tx_unbond; +#[cfg(feature = "tx_unjail_validator")] +pub mod tx_unjail_validator; #[cfg(feature = "tx_update_vp")] pub mod tx_update_vp; #[cfg(feature = "tx_vote_proposal")] @@ -33,6 +35,5 @@ pub mod vp_testnet_faucet; pub mod vp_token; #[cfg(feature = "vp_user")] pub mod vp_user; - #[cfg(feature = "vp_validator")] pub mod vp_validator; diff --git a/wasm/wasm_source/src/tx_unjail_validator.rs b/wasm/wasm_source/src/tx_unjail_validator.rs new file mode 100644 index 0000000000..9cdf803b12 --- /dev/null +++ b/wasm/wasm_source/src/tx_unjail_validator.rs @@ -0,0 +1,14 @@ +//! A tx for a jailed validator to unjail themselves and re-enter the +//! validator sets. + +use namada_tx_prelude::*; + +#[transaction] +fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { + let signed = SignedTxData::try_from_slice(&tx_data[..]) + .wrap_err("failed to decode SignedTxData")?; + let data = signed.data.ok_or_err_msg("Missing data")?; + let validator = Address::try_from_slice(&data[..]) + .wrap_err("failed to decode an Address")?; + ctx.unjail_validator(&validator) +} From 8099d27663c5894f0e4cb9eadeecc3ae0df10b70 Mon Sep 17 00:00:00 2001 From: brentstone Date: Fri, 26 May 2023 13:45:45 +0200 Subject: [PATCH 02/31] Makefile and Cargo.toml --- wasm/wasm_source/Cargo.toml | 6 +++--- wasm/wasm_source/Makefile | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/wasm/wasm_source/Cargo.toml b/wasm/wasm_source/Cargo.toml index 85194462e6..6a88945a22 100644 --- a/wasm/wasm_source/Cargo.toml +++ b/wasm/wasm_source/Cargo.toml @@ -13,7 +13,7 @@ crate-type = ["cdylib"] # Newly added wasms should also be added into the Makefile `$(wasms)` list. [features] tx_bond = ["namada_tx_prelude"] -tx_from_intent = ["namada_tx_prelude"] +tx_change_validator_commission = ["namada_tx_prelude"] tx_ibc = ["namada_tx_prelude"] tx_init_account = ["namada_tx_prelude"] tx_init_proposal = ["namada_tx_prelude"] @@ -21,12 +21,12 @@ tx_init_validator = ["namada_tx_prelude"] tx_reveal_pk = ["namada_tx_prelude"] tx_transfer = ["namada_tx_prelude"] tx_unbond = ["namada_tx_prelude"] +tx_unjail_validator = ["namada_tx_prelude"] tx_update_vp = ["namada_tx_prelude"] tx_vote_proposal = ["namada_tx_prelude"] tx_withdraw = ["namada_tx_prelude"] -tx_change_validator_commission = ["namada_tx_prelude"] -vp_masp = ["namada_vp_prelude", "masp_proofs", "masp_primitives"] vp_implicit = ["namada_vp_prelude", "once_cell", "rust_decimal"] +vp_masp = ["namada_vp_prelude", "masp_proofs", "masp_primitives"] vp_testnet_faucet = ["namada_vp_prelude", "once_cell"] vp_token = ["namada_vp_prelude"] vp_user = ["namada_vp_prelude", "once_cell", "rust_decimal"] diff --git a/wasm/wasm_source/Makefile b/wasm/wasm_source/Makefile index aee4f3df8f..1c9d586d88 100644 --- a/wasm/wasm_source/Makefile +++ b/wasm/wasm_source/Makefile @@ -6,19 +6,20 @@ nightly := $(shell cat ../../rust-nightly-version) # All the wasms that can be built from this source, switched via Cargo features # Wasms can be added via the Cargo.toml `[features]` list. wasms := tx_bond +wasms += tx_change_validator_commission wasms += tx_ibc wasms += tx_init_account -wasms += tx_init_validator wasms += tx_init_proposal +wasms += tx_init_validator wasms += tx_reveal_pk -wasms += tx_vote_proposal wasms += tx_transfer wasms += tx_unbond +wasms += tx_unjail_validator wasms += tx_update_vp +wasms += tx_vote_proposal wasms += tx_withdraw -wasms += tx_change_validator_commission -wasms += vp_masp wasms += vp_implicit +wasms += vp_masp wasms += vp_testnet_faucet wasms += vp_token wasms += vp_user From 365fbdbec7d16e1108baf600c3253c4c04c920a3 Mon Sep 17 00:00:00 2001 From: brentstone Date: Fri, 26 May 2023 13:48:38 +0200 Subject: [PATCH 03/31] slashing: unit and e2e tests --- .../lib/node/ledger/shell/finalize_block.rs | 1154 ++++++++++++++++- core/src/types/time.rs | 7 +- genesis/dev.toml | 3 + genesis/e2e-tests-single-node.toml | 3 + proof_of_stake/src/tests.rs | 146 ++- 5 files changed, 1264 insertions(+), 49 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 04462b8d52..fe8f1789dd 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -894,16 +894,25 @@ mod test_finalize_block { use namada::ledger::parameters::EpochDuration; use namada::ledger::storage_api; use namada::proof_of_stake::btree_set::BTreeSetShims; - use namada::proof_of_stake::types::WeightedValidator; + use namada::proof_of_stake::parameters::PosParams; + use namada::proof_of_stake::storage::{ + is_validator_slashes_key, slashes_prefix, + }; + use namada::proof_of_stake::types::{ + decimal_mult_amount, SlashType, ValidatorState, WeightedValidator, + }; use namada::proof_of_stake::{ + enqueued_slashes_handle, get_num_consensus_validators, read_consensus_validator_set_addresses_with_stake, - rewards_accumulator_handle, validator_consensus_key_handle, - validator_rewards_products_handle, + rewards_accumulator_handle, unjail_validator, + validator_consensus_key_handle, validator_rewards_products_handle, + validator_slashes_handle, validator_state_handle, write_pos_params, }; use namada::types::governance::ProposalVote; use namada::types::key::tm_consensus_key_raw_hash; use namada::types::storage::Epoch; use namada::types::time::DurationSecs; + use namada::types::token::Amount; use namada::types::transaction::governance::{ InitProposalData, ProposalType, VoteProposalData, }; @@ -913,7 +922,9 @@ mod test_finalize_block { use test_log::test; use super::*; - use crate::facade::tendermint_proto::abci::{Validator, VoteInfo}; + use crate::facade::tendermint_proto::abci::{ + Misbehavior, Validator, VoteInfo, + }; use crate::node::ledger::shell::test_utils::*; use crate::node::ledger::shims::abcipp_shim_types::shim::request::{ FinalizeBlock, ProcessedTx, @@ -1509,7 +1520,7 @@ mod test_finalize_block { // FINALIZE BLOCK 1. Tell Namada that val1 is the block proposer. We // won't receive votes from TM since we receive votes at a 1-block // delay, so votes will be empty here - next_block_for_inflation(&mut shell, pkh1.clone(), vec![]); + next_block_for_inflation(&mut shell, pkh1.clone(), vec![], None); assert!( rewards_accumulator_handle() .is_empty(&shell.wl_storage) @@ -1519,7 +1530,7 @@ mod test_finalize_block { // FINALIZE BLOCK 2. Tell Namada that val1 is the block proposer. // Include votes that correspond to block 1. Make val2 the next block's // proposer. - next_block_for_inflation(&mut shell, pkh2.clone(), votes.clone()); + next_block_for_inflation(&mut shell, pkh2.clone(), votes.clone(), None); assert!(rewards_prod_1.is_empty(&shell.wl_storage).unwrap()); assert!(rewards_prod_2.is_empty(&shell.wl_storage).unwrap()); assert!(rewards_prod_3.is_empty(&shell.wl_storage).unwrap()); @@ -1542,7 +1553,7 @@ mod test_finalize_block { ); // FINALIZE BLOCK 3, with val1 as proposer for the next block. - next_block_for_inflation(&mut shell, pkh1.clone(), votes); + next_block_for_inflation(&mut shell, pkh1.clone(), votes, None); assert!(rewards_prod_1.is_empty(&shell.wl_storage).unwrap()); assert!(rewards_prod_2.is_empty(&shell.wl_storage).unwrap()); assert!(rewards_prod_3.is_empty(&shell.wl_storage).unwrap()); @@ -1594,7 +1605,7 @@ mod test_finalize_block { // FINALIZE BLOCK 4. The next block proposer will be val1. Only val1, // val2, and val3 vote on this block. - next_block_for_inflation(&mut shell, pkh1.clone(), votes.clone()); + next_block_for_inflation(&mut shell, pkh1.clone(), votes.clone(), None); assert!(rewards_prod_1.is_empty(&shell.wl_storage).unwrap()); assert!(rewards_prod_2.is_empty(&shell.wl_storage).unwrap()); assert!(rewards_prod_3.is_empty(&shell.wl_storage).unwrap()); @@ -1627,7 +1638,12 @@ mod test_finalize_block { get_rewards_acc(&shell.wl_storage), get_rewards_sum(&shell.wl_storage), ); - next_block_for_inflation(&mut shell, pkh1.clone(), votes.clone()); + next_block_for_inflation( + &mut shell, + pkh1.clone(), + votes.clone(), + None, + ); } assert!( rewards_accumulator_handle() @@ -1682,12 +1698,26 @@ mod test_finalize_block { shell: &mut TestShell, proposer_address: Vec, votes: Vec, + byzantine_validators: Option>, ) { - let req = FinalizeBlock { + // Let the header time be always ahead of the next epoch min start time + let header = Header { + time: shell + .wl_storage + .storage + .next_epoch_min_start_time + .next_second(), + ..Default::default() + }; + let mut req = FinalizeBlock { + header, proposer_address, votes, ..Default::default() }; + if let Some(byz_vals) = byzantine_validators { + req.byzantine_validators = byz_vals; + } shell.finalize_block(req).unwrap(); shell.commit(); } @@ -1768,4 +1798,1108 @@ mod test_finalize_block { // .0 // ) } + + #[test] + fn test_ledger_slashing() -> storage_api::Result<()> { + let num_validators = 7_u64; + let (mut shell, _) = setup(num_validators); + let mut params = read_pos_params(&shell.wl_storage).unwrap(); + params.unbonding_len = 4; + write_pos_params(&mut shell.wl_storage, params.clone())?; + + let validator_set: Vec = + read_consensus_validator_set_addresses_with_stake( + &shell.wl_storage, + Epoch::default(), + ) + .unwrap() + .into_iter() + .collect(); + + let val1 = validator_set[0].clone(); + let val2 = validator_set[1].clone(); + + let initial_stake = val1.bonded_stake; + let total_initial_stake = num_validators * initial_stake; + + let get_pkh = |address, epoch| { + let ck = validator_consensus_key_handle(&address) + .get(&shell.wl_storage, epoch, ¶ms) + .unwrap() + .unwrap(); + let hash_string = tm_consensus_key_raw_hash(&ck); + HEXUPPER.decode(hash_string.as_bytes()).unwrap() + }; + + let mut all_pkhs: Vec> = Vec::new(); + let mut behaving_pkhs: Vec> = Vec::new(); + for (idx, validator) in validator_set.iter().enumerate() { + // Every validator should be in the consensus set + assert_eq!( + validator_state_handle(&validator.address) + .get(&shell.wl_storage, Epoch::default(), ¶ms) + .unwrap(), + Some(ValidatorState::Consensus) + ); + all_pkhs.push(get_pkh(validator.address.clone(), Epoch::default())); + if idx > 1_usize { + behaving_pkhs + .push(get_pkh(validator.address.clone(), Epoch::default())); + } + } + + let pkh1 = all_pkhs[0].clone(); + let pkh2 = all_pkhs[1].clone(); + + // Finalize block 1 (no votes since this is the first block) + next_block_for_inflation(&mut shell, pkh1.clone(), vec![], None); + + let votes = get_default_true_votes( + &shell.wl_storage, + shell.wl_storage.storage.block.epoch, + ); + assert!(!votes.is_empty()); + assert_eq!(votes.len(), 7_usize); + + // For block 2, include the evidences found for block 1. + // NOTE: Only the type, height, and validator address fields from the + // Misbehavior struct are used in Namada + let byzantine_validators = vec![ + Misbehavior { + r#type: 1, + validator: Some(Validator { + address: pkh1.clone(), + power: Default::default(), + }), + height: 1, + time: Default::default(), + total_voting_power: Default::default(), + }, + Misbehavior { + r#type: 2, + validator: Some(Validator { + address: pkh2, + power: Default::default(), + }), + height: 1, + time: Default::default(), + total_voting_power: Default::default(), + }, + ]; + next_block_for_inflation( + &mut shell, + pkh1.clone(), + votes, + Some(byzantine_validators), + ); + + let processing_epoch = shell.wl_storage.storage.block.epoch + + params.unbonding_len + + 1_u64 + + params.cubic_slashing_window_length; + + // Check that the ValidatorState, enqueued slashes, and validator sets + // are properly updated + assert_eq!( + validator_state_handle(&val1.address) + .get(&shell.wl_storage, Epoch::default(), ¶ms) + .unwrap(), + Some(ValidatorState::Consensus) + ); + assert_eq!( + validator_state_handle(&val2.address) + .get(&shell.wl_storage, Epoch::default(), ¶ms) + .unwrap(), + Some(ValidatorState::Consensus) + ); + assert!( + enqueued_slashes_handle() + .at(&Epoch::default()) + .is_empty(&shell.wl_storage)? + ); + assert_eq!( + get_num_consensus_validators(&shell.wl_storage, Epoch::default()) + .unwrap(), + 7_u64 + ); + for epoch in Epoch::default().next().iter_range(params.pipeline_len) { + assert_eq!( + validator_state_handle(&val1.address) + .get(&shell.wl_storage, epoch, ¶ms) + .unwrap(), + Some(ValidatorState::Jailed) + ); + assert_eq!( + validator_state_handle(&val2.address) + .get(&shell.wl_storage, epoch, ¶ms) + .unwrap(), + Some(ValidatorState::Jailed) + ); + assert!( + enqueued_slashes_handle() + .at(&epoch) + .is_empty(&shell.wl_storage)? + ); + assert_eq!( + get_num_consensus_validators(&shell.wl_storage, epoch).unwrap(), + 5_u64 + ); + } + assert!( + !enqueued_slashes_handle() + .at(&processing_epoch) + .is_empty(&shell.wl_storage)? + ); + + // Advance to the processing epoch + let votes = get_default_true_votes(&shell.wl_storage, Epoch::default()); + loop { + next_block_for_inflation( + &mut shell, + pkh1.clone(), + votes.clone(), + None, + ); + // println!( + // "Block {} epoch {}", + // shell.wl_storage.storage.block.height, + // shell.wl_storage.storage.block.epoch + // ); + if shell.wl_storage.storage.block.epoch == processing_epoch { + // println!("Reached processing epoch"); + break; + } else { + assert!( + enqueued_slashes_handle() + .at(&shell.wl_storage.storage.block.epoch) + .is_empty(&shell.wl_storage)? + ); + let stake1 = read_validator_stake( + &shell.wl_storage, + ¶ms, + &val1.address, + shell.wl_storage.storage.block.epoch, + )? + .unwrap(); + let stake2 = read_validator_stake( + &shell.wl_storage, + ¶ms, + &val2.address, + shell.wl_storage.storage.block.epoch, + )? + .unwrap(); + let total_stake = read_total_stake( + &shell.wl_storage, + ¶ms, + shell.wl_storage.storage.block.epoch, + )?; + assert_eq!(stake1, initial_stake); + assert_eq!(stake2, initial_stake); + assert_eq!(total_stake, total_initial_stake); + } + } + + let num_slashes = storage_api::iter_prefix_bytes( + &shell.wl_storage, + &slashes_prefix(), + )? + .filter(|kv_res| { + let (k, _v) = kv_res.as_ref().unwrap(); + is_validator_slashes_key(k).is_some() + }) + .count(); + + assert_eq!(num_slashes, 2); + assert_eq!( + validator_slashes_handle(&val1.address) + .len(&shell.wl_storage) + .unwrap(), + 1_u64 + ); + assert_eq!( + validator_slashes_handle(&val2.address) + .len(&shell.wl_storage) + .unwrap(), + 1_u64 + ); + + let slash1 = validator_slashes_handle(&val1.address) + .get(&shell.wl_storage, 0)? + .unwrap(); + let slash2 = validator_slashes_handle(&val2.address) + .get(&shell.wl_storage, 0)? + .unwrap(); + + assert_eq!(slash1.r#type, SlashType::DuplicateVote); + assert_eq!(slash2.r#type, SlashType::LightClientAttack); + assert_eq!(slash1.epoch, Epoch::default()); + assert_eq!(slash2.epoch, Epoch::default()); + + // Each validator has equal weight in this test, and two have been + // slashed + let frac = dec!(2) / dec!(7); + let cubic_rate = dec!(9) * frac * frac; + + assert_eq!(slash1.rate, cubic_rate); + assert_eq!(slash2.rate, cubic_rate); + + // Check that there are still 5 consensus validators and the 2 + // misbehaving ones are still jailed + for epoch in shell + .wl_storage + .storage + .block + .epoch + .iter_range(params.pipeline_len + 1) + { + assert_eq!( + validator_state_handle(&val1.address) + .get(&shell.wl_storage, epoch, ¶ms) + .unwrap(), + Some(ValidatorState::Jailed) + ); + assert_eq!( + validator_state_handle(&val2.address) + .get(&shell.wl_storage, epoch, ¶ms) + .unwrap(), + Some(ValidatorState::Jailed) + ); + assert_eq!( + get_num_consensus_validators(&shell.wl_storage, epoch).unwrap(), + 5_u64 + ); + } + + // Check that the deltas at the pipeline epoch are slashed + let pipeline_epoch = + shell.wl_storage.storage.block.epoch + params.pipeline_len; + let stake1 = read_validator_stake( + &shell.wl_storage, + ¶ms, + &val1.address, + pipeline_epoch, + )? + .unwrap(); + let stake2 = read_validator_stake( + &shell.wl_storage, + ¶ms, + &val2.address, + pipeline_epoch, + )? + .unwrap(); + let total_stake = + read_total_stake(&shell.wl_storage, ¶ms, pipeline_epoch)?; + + let expected_slashed = decimal_mult_amount(cubic_rate, initial_stake); + assert_eq!(stake1, initial_stake - expected_slashed); + assert_eq!(stake2, initial_stake - expected_slashed); + assert_eq!(total_stake, total_initial_stake - 2 * expected_slashed); + + // Unjail one of the validators + let current_epoch = shell.wl_storage.storage.block.epoch; + unjail_validator(&mut shell.wl_storage, &val1.address, current_epoch)?; + let pipeline_epoch = current_epoch + params.pipeline_len; + + // Check that the state is the same until the pipeline epoch, at which + // point one validator is unjailed + for epoch in shell + .wl_storage + .storage + .block + .epoch + .iter_range(params.pipeline_len) + { + assert_eq!( + validator_state_handle(&val1.address) + .get(&shell.wl_storage, epoch, ¶ms) + .unwrap(), + Some(ValidatorState::Jailed) + ); + assert_eq!( + validator_state_handle(&val2.address) + .get(&shell.wl_storage, epoch, ¶ms) + .unwrap(), + Some(ValidatorState::Jailed) + ); + assert_eq!( + get_num_consensus_validators(&shell.wl_storage, epoch).unwrap(), + 5_u64 + ); + } + assert_eq!( + validator_state_handle(&val1.address) + .get(&shell.wl_storage, pipeline_epoch, ¶ms) + .unwrap(), + Some(ValidatorState::Consensus) + ); + assert_eq!( + validator_state_handle(&val2.address) + .get(&shell.wl_storage, pipeline_epoch, ¶ms) + .unwrap(), + Some(ValidatorState::Jailed) + ); + assert_eq!( + get_num_consensus_validators(&shell.wl_storage, pipeline_epoch) + .unwrap(), + 6_u64 + ); + + Ok(()) + } + + /// NOTE: must call `get_default_true_votes` before every call to + /// `next_block_for_inflation` + #[test] + fn test_multiple_misbehaviors() -> storage_api::Result<()> { + for num_validators in 4u64..10u64 { + println!("NUM VALIDATORS = {}", num_validators); + test_multiple_misbehaviors_by_num_vals(num_validators)?; + } + Ok(()) + } + + /// Current test procedure (prefixed by epoch in which the event occurs): + /// 0) Validator initial stake of 200_000 + /// 1) Delegate 67_231 to validator + /// 1) Self-unbond 154_654 + /// 2) Unbond delegation of 18_000 + /// 3) Self-bond 9_123 + /// 4) Self-unbond 15_000 + /// 5) Delegate 8_144 to validator + /// 6) Discover misbehavior in epoch 3 + /// 7) Discover misbehavior in epoch 3 + /// 7) Discover misbehavior in epoch 4 + fn test_multiple_misbehaviors_by_num_vals( + num_validators: u64, + ) -> storage_api::Result<()> { + // Setup the network with pipeline_len = 2, unbonding_len = 4 + // let num_validators = 8_u64; + let (mut shell, _) = setup(num_validators); + let mut params = read_pos_params(&shell.wl_storage).unwrap(); + params.unbonding_len = 4; + params.max_validator_slots = 4; + write_pos_params(&mut shell.wl_storage, params.clone())?; + + // Slash pool balance + let nam_address = shell.wl_storage.storage.native_token.clone(); + let slash_balance_key = token::balance_key( + &nam_address, + &namada_proof_of_stake::SLASH_POOL_ADDRESS, + ); + let slash_pool_balance_init: token::Amount = shell + .wl_storage + .read(&slash_balance_key) + .expect("must be able to read") + .unwrap_or_default(); + debug_assert_eq!(slash_pool_balance_init, token::Amount::default()); + + let consensus_set: Vec = + read_consensus_validator_set_addresses_with_stake( + &shell.wl_storage, + Epoch::default(), + ) + .unwrap() + .into_iter() + .collect(); + + let val1 = consensus_set[0].clone(); + let pkh1 = get_pkh_from_address( + &shell.wl_storage, + ¶ms, + val1.address.clone(), + Epoch::default(), + ); + + let initial_stake = val1.bonded_stake; + let total_initial_stake = num_validators * initial_stake; + + // Finalize block 1 + next_block_for_inflation(&mut shell, pkh1.clone(), vec![], None); + + let votes = get_default_true_votes(&shell.wl_storage, Epoch::default()); + assert!(!votes.is_empty()); + + // Advance to epoch 1 and + // 1. Delegate 67231 NAM to validator + // 2. Validator self-unbond 154654 NAM + let current_epoch = advance_epoch(&mut shell, &pkh1, &votes, None); + assert_eq!(shell.wl_storage.storage.block.epoch.0, 1_u64); + + // Make an account with balance and delegate some tokens + let delegator = address::testing::gen_implicit_address(); + let del_1_amount = token::Amount::whole(67_231); + let staking_token = shell.wl_storage.storage.native_token.clone(); + credit_tokens( + &mut shell.wl_storage, + &staking_token, + &delegator, + token::Amount::whole(200_000), + ) + .unwrap(); + namada_proof_of_stake::bond_tokens( + &mut shell.wl_storage, + Some(&delegator), + &val1.address, + del_1_amount, + current_epoch, + ) + .unwrap(); + + // Self-unbond + let self_unbond_1_amount = token::Amount::whole(154_654); + namada_proof_of_stake::unbond_tokens( + &mut shell.wl_storage, + None, + &val1.address, + self_unbond_1_amount, + current_epoch, + ) + .unwrap(); + + let val_stake = namada_proof_of_stake::read_validator_stake( + &shell.wl_storage, + ¶ms, + &val1.address, + current_epoch + params.pipeline_len, + ) + .unwrap() + .unwrap_or_default(); + + let total_stake = namada_proof_of_stake::read_total_stake( + &shell.wl_storage, + ¶ms, + current_epoch + params.pipeline_len, + ) + .unwrap(); + + assert_eq!( + val_stake, + initial_stake + del_1_amount - self_unbond_1_amount + ); + assert_eq!( + total_stake, + total_initial_stake + del_1_amount - self_unbond_1_amount + ); + + // Advance to epoch 2 and + // 1. Unbond 18000 NAM from delegation + let votes = get_default_true_votes( + &shell.wl_storage, + shell.wl_storage.storage.block.epoch, + ); + let current_epoch = advance_epoch(&mut shell, &pkh1, &votes, None); + println!("\nUnbonding in epoch 2"); + let del_unbond_1_amount = token::Amount::whole(18_000); + namada_proof_of_stake::unbond_tokens( + &mut shell.wl_storage, + Some(&delegator), + &val1.address, + del_unbond_1_amount, + current_epoch, + ) + .unwrap(); + + let val_stake = namada_proof_of_stake::read_validator_stake( + &shell.wl_storage, + ¶ms, + &val1.address, + current_epoch + params.pipeline_len, + ) + .unwrap() + .unwrap_or_default(); + let total_stake = namada_proof_of_stake::read_total_stake( + &shell.wl_storage, + ¶ms, + current_epoch + params.pipeline_len, + ) + .unwrap(); + assert_eq!( + val_stake, + initial_stake + del_1_amount + - self_unbond_1_amount + - del_unbond_1_amount + ); + assert_eq!( + total_stake, + total_initial_stake + del_1_amount + - self_unbond_1_amount + - del_unbond_1_amount + ); + + // Advance to epoch 3 and + // 1. Validator self-bond 9123 NAM + let votes = get_default_true_votes( + &shell.wl_storage, + shell.wl_storage.storage.block.epoch, + ); + let current_epoch = advance_epoch(&mut shell, &pkh1, &votes, None); + println!("\nBonding in epoch 3"); + + let self_bond_1_amount = token::Amount::whole(9_123); + namada_proof_of_stake::bond_tokens( + &mut shell.wl_storage, + None, + &val1.address, + self_bond_1_amount, + current_epoch, + ) + .unwrap(); + + // Advance to epoch 4 + // 1. Validator self-unbond 15000 NAM + let votes = get_default_true_votes( + &shell.wl_storage, + shell.wl_storage.storage.block.epoch, + ); + let current_epoch = advance_epoch(&mut shell, &pkh1, &votes, None); + assert_eq!(current_epoch.0, 4_u64); + + let self_unbond_2_amount = token::Amount::whole(15_000); + namada_proof_of_stake::unbond_tokens( + &mut shell.wl_storage, + None, + &val1.address, + self_unbond_2_amount, + current_epoch, + ) + .unwrap(); + + // Advance to epoch 5 and + // Delegate 8144 NAM to validator + let votes = get_default_true_votes( + &shell.wl_storage, + shell.wl_storage.storage.block.epoch, + ); + let current_epoch = advance_epoch(&mut shell, &pkh1, &votes, None); + assert_eq!(current_epoch.0, 5_u64); + println!("Delegating in epoch 5"); + + // Delegate + let del_2_amount = token::Amount::whole(8_144); + namada_proof_of_stake::bond_tokens( + &mut shell.wl_storage, + Some(&delegator), + &val1.address, + del_2_amount, + current_epoch, + ) + .unwrap(); + + println!("Advancing to epoch 6"); + + // Advance to epoch 6 + let votes = get_default_true_votes( + &shell.wl_storage, + shell.wl_storage.storage.block.epoch, + ); + let current_epoch = advance_epoch(&mut shell, &pkh1, &votes, None); + assert_eq!(current_epoch.0, 6_u64); + + // Discover a misbehavior committed in epoch 3 + // NOTE: Only the type, height, and validator address fields from the + // Misbehavior struct are used in Namada + let misbehavior_epoch = Epoch(3_u64); + let height = shell + .wl_storage + .storage + .block + .pred_epochs + .first_block_heights[misbehavior_epoch.0 as usize]; + let misbehaviors = vec![Misbehavior { + r#type: 1, + validator: Some(Validator { + address: pkh1.clone(), + power: Default::default(), + }), + height: height.0 as i64, + time: Default::default(), + total_voting_power: Default::default(), + }]; + let votes = get_default_true_votes( + &shell.wl_storage, + shell.wl_storage.storage.block.epoch, + ); + next_block_for_inflation( + &mut shell, + pkh1.clone(), + votes.clone(), + Some(misbehaviors), + ); + + // Assertions + assert_eq!(current_epoch.0, 6_u64); + let processing_epoch = misbehavior_epoch + + params.unbonding_len + + 1_u64 + + params.cubic_slashing_window_length; + let enqueued_slash = enqueued_slashes_handle() + .at(&processing_epoch) + .at(&val1.address) + .front(&shell.wl_storage) + .unwrap() + .unwrap(); + assert_eq!(enqueued_slash.epoch, misbehavior_epoch); + assert_eq!(enqueued_slash.r#type, SlashType::DuplicateVote); + assert_eq!(enqueued_slash.rate, Decimal::ZERO); + let last_slash = + namada_proof_of_stake::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() + ); + + println!("Advancing to epoch 7"); + + // Advance to epoch 7 + let current_epoch = advance_epoch(&mut shell, &pkh1, &votes, None); + + // Discover two more misbehaviors, one committed in epoch 3, one in + // epoch 4 + let height4 = shell + .wl_storage + .storage + .block + .pred_epochs + .first_block_heights[4]; + let misbehaviors = vec![ + Misbehavior { + r#type: 1, + validator: Some(Validator { + address: pkh1.clone(), + power: Default::default(), + }), + height: height.0 as i64, + time: Default::default(), + total_voting_power: Default::default(), + }, + Misbehavior { + r#type: 2, + validator: Some(Validator { + address: pkh1.clone(), + power: Default::default(), + }), + height: height4.0 as i64, + time: Default::default(), + total_voting_power: Default::default(), + }, + ]; + next_block_for_inflation( + &mut shell, + pkh1.clone(), + votes.clone(), + Some(misbehaviors), + ); + assert_eq!(current_epoch.0, 7_u64); + let enqueued_slashes_8 = enqueued_slashes_handle() + .at(&processing_epoch) + .at(&val1.address); + let enqueued_slashes_9 = enqueued_slashes_handle() + .at(&processing_epoch.next()) + .at(&val1.address); + + 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( + &shell.wl_storage, + &val1.address, + ) + .unwrap(); + assert_eq!(last_slash, Some(Epoch(4))); + assert!( + namada_proof_of_stake::is_validator_frozen( + &shell.wl_storage, + &val1.address, + current_epoch, + ¶ms + ) + .unwrap() + ); + assert!( + namada_proof_of_stake::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() + .unwrap_or_default(); + assert_eq!( + pre_stake_10, + initial_stake + del_1_amount + - self_unbond_1_amount + - del_unbond_1_amount + + self_bond_1_amount + - self_unbond_2_amount + + del_2_amount + ); + + println!("\nNow processing the infractions\n"); + + // Advance to epoch 9, where the infractions committed in epoch 3 will + // be processed + let votes = get_default_true_votes( + &shell.wl_storage, + shell.wl_storage.storage.block.epoch, + ); + let _ = advance_epoch(&mut shell, &pkh1, &votes, None); + let votes = get_default_true_votes( + &shell.wl_storage, + shell.wl_storage.storage.block.epoch, + ); + 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( + &shell.wl_storage, + ¶ms, + &val1.address, + Epoch(3), + ) + .unwrap() + .unwrap_or_default(); + let val_stake_4 = namada_proof_of_stake::read_validator_stake( + &shell.wl_storage, + ¶ms, + &val1.address, + Epoch(4), + ) + .unwrap() + .unwrap_or_default(); + + let tot_stake_3 = namada_proof_of_stake::read_total_stake( + &shell.wl_storage, + ¶ms, + Epoch(3), + ) + .unwrap(); + let tot_stake_4 = namada_proof_of_stake::read_total_stake( + &shell.wl_storage, + ¶ms, + Epoch(4), + ) + .unwrap(); + + let vp_frac_3 = Decimal::from(val_stake_3) / Decimal::from(tot_stake_3); + let vp_frac_4 = Decimal::from(val_stake_4) / Decimal::from(tot_stake_4); + let tot_frac = dec!(2) * vp_frac_3 + vp_frac_4; + let cubic_rate = + std::cmp::min(Decimal::ONE, dec!(9) * tot_frac * tot_frac); + dbg!(&cubic_rate); + + let equal_enough = |rate1: Decimal, rate2: Decimal| -> bool { + let tolerance = dec!(0.000000001); + (rate1 - rate2).abs() < tolerance + }; + + // 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); + assert_eq!(val_slashes.len(&shell.wl_storage).unwrap(), 2u64); + let is_rate_good = val_slashes + .iter(&shell.wl_storage) + .unwrap() + .all(|s| equal_enough(s.unwrap().rate, cubic_rate)); + assert!(is_rate_good); + + // Check the amount of stake deducted from the futuremost epoch while + // processing the slashes + let post_stake_10 = namada_proof_of_stake::read_validator_stake( + &shell.wl_storage, + ¶ms, + &val1.address, + Epoch(10), + ) + .unwrap() + .unwrap_or_default(); + // The amount unbonded after the infraction that affected the deltas + // before processing is `del_unbond_1_amount + self_bond_1_amount - + // self_unbond_2_amount` (since this self-bond was enacted then unbonded + // all after the infraction). Thus, the additional deltas to be + // deducted is the (infraction stake - this) * rate + let slash_rate_3 = std::cmp::min(Decimal::ONE, dec!(2) * cubic_rate); + let exp_slashed_during_processing_9 = decimal_mult_amount( + slash_rate_3, + initial_stake + del_1_amount + - self_unbond_1_amount + - del_unbond_1_amount + + self_bond_1_amount + - self_unbond_2_amount, + ); + assert_eq!( + pre_stake_10 - post_stake_10, + exp_slashed_during_processing_9 + ); + + // Check that we can compute the stake at the pipeline epoch + // NOTE: may be off. by 1 namnam due to rounding; + let exp_pipeline_stake = decimal_mult_amount( + Decimal::ONE - slash_rate_3, + initial_stake + del_1_amount + - self_unbond_1_amount + - del_unbond_1_amount + + self_bond_1_amount + - self_unbond_2_amount, + ) + del_2_amount; + assert!( + (exp_pipeline_stake.change() - post_stake_10.change()).abs() <= 1 + ); + + // Check the balance of the Slash Pool + let slash_pool_balance: token::Amount = shell + .wl_storage + .read(&slash_balance_key) + .expect("must be able to read") + .unwrap_or_default(); + let exp_slashed_3 = decimal_mult_amount( + std::cmp::min(Decimal::TWO * cubic_rate, Decimal::ONE), + val_stake_3 - del_unbond_1_amount + self_bond_1_amount + - self_unbond_2_amount, + ); + assert_eq!(slash_pool_balance, exp_slashed_3); + + let pre_stake_11 = namada_proof_of_stake::read_validator_stake( + &shell.wl_storage, + ¶ms, + &val1.address, + Epoch(10), + ) + .unwrap() + .unwrap_or_default(); + + // Advance to epoch 10, where the infraction committed in epoch 4 will + // be processed + let votes = get_default_true_votes( + &shell.wl_storage, + shell.wl_storage.storage.block.epoch, + ); + let current_epoch = advance_epoch(&mut shell, &pkh1, &votes, None); + assert_eq!(current_epoch.0, 10_u64); + + // Check the balance of the Slash Pool + let slash_pool_balance: token::Amount = shell + .wl_storage + .read(&slash_balance_key) + .expect("must be able to read") + .unwrap_or_default(); + + let exp_slashed_4 = if dec!(2) * cubic_rate >= Decimal::ONE { + token::Amount::default() + } else if dec!(3) * cubic_rate >= Decimal::ONE { + decimal_mult_amount( + Decimal::ONE - dec!(2) * cubic_rate, + val_stake_4 + self_bond_1_amount - self_unbond_2_amount, + ) + } else { + decimal_mult_amount( + std::cmp::min(cubic_rate, Decimal::ONE), + val_stake_4 + self_bond_1_amount - self_unbond_2_amount, + ) + }; + dbg!(slash_pool_balance, exp_slashed_3 + exp_slashed_4); + assert!( + (slash_pool_balance.change() + - (exp_slashed_3 + exp_slashed_4).change()) + .abs() + <= 1 + ); + + let val_stake = read_validator_stake( + &shell.wl_storage, + ¶ms, + &val1.address, + current_epoch + params.pipeline_len, + )? + .unwrap_or_default(); + + let post_stake_11 = namada_proof_of_stake::read_validator_stake( + &shell.wl_storage, + ¶ms, + &val1.address, + Epoch(10), + ) + .unwrap() + .unwrap_or_default(); + + assert_eq!(post_stake_11, val_stake); + // dbg!(&val_stake); + // dbg!(pre_stake_10 - post_stake_10); + + // dbg!(&exp_slashed_during_processing_9); + assert!( + ((pre_stake_11 - post_stake_11).change() - exp_slashed_4.change()) + .abs() + <= 1 + ); + + // dbg!(&val_stake, &exp_stake); + // dbg!(exp_slashed_during_processing_8 + + // exp_slashed_during_processing_9); dbg!( + // val_stake_3 + // - (exp_slashed_during_processing_8 + + // exp_slashed_during_processing_9) + // ); + + let exp_stake = val_stake_3 - del_unbond_1_amount + self_bond_1_amount + - self_unbond_2_amount + + del_2_amount + - exp_slashed_3 + - exp_slashed_4; + + assert!((exp_stake.change() - post_stake_11.change()).abs() <= 1); + + for _ in 0..2 { + let votes = get_default_true_votes( + &shell.wl_storage, + shell.wl_storage.storage.block.epoch, + ); + let _ = advance_epoch(&mut shell, &pkh1, &votes, None); + } + let current_epoch = shell.wl_storage.storage.block.epoch; + assert_eq!(current_epoch.0, 12_u64); + + println!("\nWITHDRAWING DELEGATION UNBOND"); + let slash_pool_balance_pre_withdraw = slash_pool_balance; + // Withdraw the delegation unbonds, which total to 18_000. This should + // only be affected by the slashes in epoch 3 + let del_withdraw = namada_proof_of_stake::withdraw_tokens( + &mut shell.wl_storage, + Some(&delegator), + &val1.address, + current_epoch, + ) + .unwrap(); + + let exp_del_withdraw_slashed_amount = + decimal_mult_amount(slash_rate_3, del_unbond_1_amount); + assert_eq!( + del_withdraw, + del_unbond_1_amount - exp_del_withdraw_slashed_amount + ); + + // Check the balance of the Slash Pool + let slash_pool_balance: token::Amount = shell + .wl_storage + .read(&slash_balance_key) + .expect("must be able to read") + .unwrap_or_default(); + dbg!(del_withdraw, slash_pool_balance); + assert_eq!( + slash_pool_balance - slash_pool_balance_pre_withdraw, + exp_del_withdraw_slashed_amount + ); + + println!("\nWITHDRAWING SELF UNBOND"); + // Withdraw the self unbonds, which total 154_654 + 15_000 - 9_123. Only + // the (15_000 - 9_123) tokens are slashable. + let self_withdraw = namada_proof_of_stake::withdraw_tokens( + &mut shell.wl_storage, + None, + &val1.address, + current_epoch, + ) + .unwrap(); + + let exp_self_withdraw_slashed_amount = decimal_mult_amount( + std::cmp::min(dec!(3) * cubic_rate, Decimal::ONE), + self_unbond_2_amount - self_bond_1_amount, + ); + // Check the balance of the Slash Pool + let slash_pool_balance: token::Amount = shell + .wl_storage + .read(&slash_balance_key) + .expect("must be able to read") + .unwrap_or_default(); + + dbg!(self_withdraw, slash_pool_balance); + dbg!( + decimal_mult_amount(dec!(2) * cubic_rate, val_stake_3) + + decimal_mult_amount(cubic_rate, val_stake_4) + ); + + assert_eq!( + exp_self_withdraw_slashed_amount, + slash_pool_balance + - slash_pool_balance_pre_withdraw + - exp_del_withdraw_slashed_amount + ); + + Ok(()) + } + + fn get_default_true_votes(storage: &S, epoch: Epoch) -> Vec + where + S: StorageRead, + { + let params = read_pos_params(storage).unwrap(); + read_consensus_validator_set_addresses_with_stake(storage, epoch) + .unwrap() + .into_iter() + .map(|val| { + let pkh = get_pkh_from_address( + storage, + ¶ms, + val.address.clone(), + epoch, + ); + VoteInfo { + validator: Some(Validator { + address: pkh, + power: u64::from(val.bonded_stake) as i64, + }), + signed_last_block: true, + } + }) + .collect::>() + } + + fn advance_epoch( + shell: &mut TestShell, + proposer_address: &[u8], + consensus_votes: &[VoteInfo], + misbehaviors: Option>, + ) -> Epoch { + let current_epoch = shell.wl_storage.storage.block.epoch; + loop { + next_block_for_inflation( + shell, + proposer_address.to_owned(), + consensus_votes.to_owned(), + misbehaviors.clone(), + ); + if shell.wl_storage.storage.block.epoch == current_epoch.next() { + break; + } + } + shell.wl_storage.storage.block.epoch + } + + fn get_pkh_from_address( + storage: &S, + params: &PosParams, + address: Address, + epoch: Epoch, + ) -> Vec + where + S: StorageRead, + { + let ck = validator_consensus_key_handle(&address) + .get(storage, epoch, params) + .unwrap() + .unwrap(); + let hash_string = tm_consensus_key_raw_hash(&ck); + HEXUPPER.decode(hash_string.as_bytes()).unwrap() + } } diff --git a/core/src/types/time.rs b/core/src/types/time.rs index af596db545..5917eb84ea 100644 --- a/core/src/types/time.rs +++ b/core/src/types/time.rs @@ -96,7 +96,7 @@ impl From for DurationNanos { pub struct Rfc3339String(pub String); /// A duration in seconds precision. -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct DateTimeUtc(pub DateTime); impl DateTimeUtc { @@ -109,6 +109,11 @@ impl DateTimeUtc { pub fn to_rfc3339(&self) -> String { chrono::DateTime::to_rfc3339(&self.0) } + + /// Returns the DateTimeUtc corresponding to one second in the future + pub fn next_second(&self) -> Self { + *self + DurationSecs(0) + } } impl FromStr for DateTimeUtc { diff --git a/genesis/dev.toml b/genesis/dev.toml index 0aff92206f..80748ec403 100644 --- a/genesis/dev.toml +++ b/genesis/dev.toml @@ -175,6 +175,9 @@ duplicate_vote_min_slash_rate = 0.001 # Portion of a validator's stake that should be slashed on a light # client attack. light_client_attack_min_slash_rate = 0.001 +# Number of epochs above and below (separately) the current epoch to +# consider when doing cubic slashing +cubic_slashing_window_length = 1 # Governance parameters. [gov_params] diff --git a/genesis/e2e-tests-single-node.toml b/genesis/e2e-tests-single-node.toml index 6a1cc634f3..8d6ae86f31 100644 --- a/genesis/e2e-tests-single-node.toml +++ b/genesis/e2e-tests-single-node.toml @@ -194,6 +194,9 @@ duplicate_vote_min_slash_rate = 0.001 # Portion of a validator's stake that should be slashed on a light # client attack. light_client_attack_min_slash_rate = 0.001 +# Number of epochs above and below (separately) the current epoch to +# consider when doing cubic slashing +cubic_slashing_window_length = 1 # Governance parameters. [gov_params] diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index 84bb8ebe53..70dd968a34 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -38,15 +38,15 @@ use crate::{ become_validator, below_capacity_validator_set_handle, bond_handle, bond_tokens, bonds_and_unbonds, consensus_validator_set_handle, copy_validator_sets_and_positions, find_validator_by_raw_hash, - init_genesis, insert_validator_into_validator_set, + get_num_consensus_validators, init_genesis, + insert_validator_into_validator_set, process_slashes, read_below_capacity_validator_set_addresses_with_stake, - read_consensus_validator_set_addresses_with_stake, - read_num_consensus_validators, read_total_stake, - read_validator_delta_value, read_validator_stake, staking_token_address, - total_deltas_handle, unbond_handle, unbond_tokens, update_validator_deltas, - update_validator_set, validator_consensus_key_handle, - validator_set_update_tendermint, validator_state_handle, withdraw_tokens, - write_validator_address_raw_hash, + read_consensus_validator_set_addresses_with_stake, read_total_stake, + read_validator_delta_value, read_validator_stake, slash, + staking_token_address, total_deltas_handle, unbond_handle, unbond_tokens, + unjail_validator, update_validator_deltas, update_validator_set, + validator_consensus_key_handle, validator_set_update_tendermint, + validator_state_handle, withdraw_tokens, write_validator_address_raw_hash, }; proptest! { @@ -346,7 +346,7 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { &s, ¶ms, &validator.address, - pipeline_epoch - 1, + pipeline_epoch.prev(), ) .unwrap() .unwrap_or_default(); @@ -362,7 +362,7 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { let delegation = bond_handle(&delegator, &validator.address); assert_eq!( delegation - .get_sum(&s, pipeline_epoch - 1, ¶ms) + .get_sum(&s, pipeline_epoch.prev(), ¶ms) .unwrap() .unwrap_or_default(), token::Change::default() @@ -478,7 +478,7 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { &s, ¶ms, &validator.address, - pipeline_epoch - 1, + pipeline_epoch.prev(), ) .unwrap(); @@ -498,7 +498,9 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { assert_eq!(val_delta, Some(-amount_self_unbond.change())); assert_eq!( unbond - .at(&(pipeline_epoch + params.unbonding_len)) + .at(&(pipeline_epoch + + params.unbonding_len + + params.cubic_slashing_window_length)) .get(&s, &Epoch::default()) .unwrap(), if unbonded_genesis_self_bond { @@ -509,7 +511,9 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { ); assert_eq!( unbond - .at(&(pipeline_epoch + params.unbonding_len)) + .at(&(pipeline_epoch + + params.unbonding_len + + params.cubic_slashing_window_length)) .get(&s, &(self_bond_epoch + params.pipeline_len)) .unwrap(), Some(amount_self_bond) @@ -568,7 +572,8 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { start: start_epoch, withdraw: self_unbond_epoch + params.pipeline_len - + params.unbonding_len, + + params.unbonding_len + + params.cubic_slashing_window_length, amount: amount_self_unbond - amount_self_bond, slashed_amount: None } @@ -580,7 +585,8 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { start: self_bond_epoch + params.pipeline_len, withdraw: self_unbond_epoch + params.pipeline_len - + params.unbonding_len, + + params.unbonding_len + + params.cubic_slashing_window_length, amount: amount_self_bond, slashed_amount: None } @@ -606,7 +612,7 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { &s, ¶ms, &validator.address, - pipeline_epoch - 1, + pipeline_epoch.prev(), ) .unwrap(); let val_stake_post = @@ -627,7 +633,9 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { ); assert_eq!( unbond - .at(&(pipeline_epoch + params.unbonding_len)) + .at(&(pipeline_epoch + + params.unbonding_len + + params.cubic_slashing_window_length)) .get(&s, &(delegation_epoch + params.pipeline_len)) .unwrap(), Some(amount_undel) @@ -645,7 +653,9 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { ) ); - let withdrawable_offset = params.unbonding_len + params.pipeline_len; + let withdrawable_offset = params.unbonding_len + + params.pipeline_len + + params.cubic_slashing_window_length; // Advance to withdrawable epoch for _ in 0..withdrawable_offset { @@ -742,7 +752,9 @@ fn test_become_validator_aux( // Advance to epoch 1 current_epoch = advance_epoch(&mut s, ¶ms); - let num_consensus_before = read_num_consensus_validators(&s).unwrap(); + let num_consensus_before = + get_num_consensus_validators(&s, current_epoch + params.pipeline_len) + .unwrap(); assert_eq!( min(validators.len() as u64, params.max_validator_slots), num_consensus_before @@ -761,7 +773,9 @@ fn test_become_validator_aux( ) .unwrap(); - let num_consensus_after = read_num_consensus_validators(&s).unwrap(); + let num_consensus_after = + get_num_consensus_validators(&s, current_epoch + params.pipeline_len) + .unwrap(); assert_eq!( if validators.len() as u64 >= params.max_validator_slots { num_consensus_before @@ -900,8 +914,15 @@ fn test_validator_sets() { ) .unwrap(); - update_validator_deltas(s, ¶ms, addr, stake.change(), epoch) - .unwrap(); + update_validator_deltas( + s, + ¶ms, + addr, + stake.change(), + epoch, + params.pipeline_len, + ) + .unwrap(); // Set their consensus key (needed for // `validator_set_update_tendermint` fn) @@ -1164,8 +1185,15 @@ fn test_validator_sets() { // checks update_validator_set(&mut s, ¶ms, &val1, -unbond.change(), epoch) .unwrap(); - update_validator_deltas(&mut s, ¶ms, &val1, -unbond.change(), epoch) - .unwrap(); + update_validator_deltas( + &mut s, + ¶ms, + &val1, + -unbond.change(), + epoch, + params.pipeline_len, + ) + .unwrap(); // Epoch 6 let val1_unbond_epoch = pipeline_epoch; @@ -1356,8 +1384,15 @@ fn test_validator_sets() { let stake6 = stake6 + bond; println!("val6 {val6} new stake {stake6}"); update_validator_set(&mut s, ¶ms, &val6, bond.change(), epoch).unwrap(); - update_validator_deltas(&mut s, ¶ms, &val6, bond.change(), epoch) - .unwrap(); + update_validator_deltas( + &mut s, + ¶ms, + &val6, + bond.change(), + epoch, + params.pipeline_len, + ) + .unwrap(); let val6_bond_epoch = pipeline_epoch; let consensus_vals: Vec<_> = consensus_validator_set_handle() @@ -1512,8 +1547,15 @@ fn test_validator_sets_swap() { ) .unwrap(); - update_validator_deltas(s, ¶ms, addr, stake.change(), epoch) - .unwrap(); + update_validator_deltas( + s, + ¶ms, + addr, + stake.change(), + epoch, + params.pipeline_len, + ) + .unwrap(); // Set their consensus key (needed for // `validator_set_update_tendermint` fn) @@ -1580,13 +1622,27 @@ fn test_validator_sets_swap() { update_validator_set(&mut s, ¶ms, &val2, bond2.change(), epoch) .unwrap(); - update_validator_deltas(&mut s, ¶ms, &val2, bond2.change(), epoch) - .unwrap(); + update_validator_deltas( + &mut s, + ¶ms, + &val2, + bond2.change(), + epoch, + params.pipeline_len, + ) + .unwrap(); update_validator_set(&mut s, ¶ms, &val3, bond3.change(), epoch) .unwrap(); - update_validator_deltas(&mut s, ¶ms, &val3, bond3.change(), epoch) - .unwrap(); + update_validator_deltas( + &mut s, + ¶ms, + &val3, + bond3.change(), + epoch, + params.pipeline_len, + ) + .unwrap(); // Advance to EPOCH 2 let epoch = advance_epoch(&mut s, ¶ms); @@ -1605,13 +1661,27 @@ fn test_validator_sets_swap() { update_validator_set(&mut s, ¶ms, &val2, bonds.change(), epoch) .unwrap(); - update_validator_deltas(&mut s, ¶ms, &val2, bonds.change(), epoch) - .unwrap(); + update_validator_deltas( + &mut s, + ¶ms, + &val2, + bonds.change(), + epoch, + params.pipeline_len, + ) + .unwrap(); update_validator_set(&mut s, ¶ms, &val3, bonds.change(), epoch) .unwrap(); - update_validator_deltas(&mut s, ¶ms, &val3, bonds.change(), epoch) - .unwrap(); + update_validator_deltas( + &mut s, + ¶ms, + &val3, + bonds.change(), + epoch, + params.pipeline_len, + ) + .unwrap(); // Advance to EPOCH 3 let epoch = advance_epoch(&mut s, ¶ms); @@ -1675,7 +1745,7 @@ fn arb_genesis_validators( size: Range, ) -> impl Strategy> { let tokens: Vec<_> = (0..size.end) - .map(|_| (1..=10_u64).prop_map(token::Amount::from)) + .map(|_| (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 @@ -1688,7 +1758,7 @@ fn arb_genesis_validators( let consensus_key = consensus_sk.to_public(); let commission_rate = Decimal::new(5, 2); - let max_commission_rate_change = Decimal::new(1, 3); + let max_commission_rate_change = Decimal::new(1, 2); GenesisValidator { address, tokens, From cdedbd2e21ffc1b3963df39041b5aac17489aa91 Mon Sep 17 00:00:00 2001 From: brentstone Date: Fri, 26 May 2023 13:48:55 +0200 Subject: [PATCH 04/31] basic nested map test --- .../storage_api/collections/lazy_map.rs | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/core/src/ledger/storage_api/collections/lazy_map.rs b/core/src/ledger/storage_api/collections/lazy_map.rs index c1e8ae6dbf..5870fd7b3f 100644 --- a/core/src/ledger/storage_api/collections/lazy_map.rs +++ b/core/src/ledger/storage_api/collections/lazy_map.rs @@ -740,4 +740,119 @@ mod test { Ok(()) } + + #[test] + fn test_nested_map_basics() -> storage_api::Result<()> { + let mut storage = TestWlStorage::default(); + let key = storage::Key::parse("testing").unwrap(); + + // A nested map from u32 -> String -> u32 + let nested_map = NestedMap::>::open(key); + + assert!(nested_map.is_empty(&storage)?); + assert!(nested_map.iter(&storage)?.next().is_none()); + + // Insert a value + nested_map + .at(&0) + .insert(&mut storage, "string1".to_string(), 100)?; + + assert!(!nested_map.is_empty(&storage)?); + assert!(nested_map.iter(&storage)?.next().is_some()); + assert_eq!( + nested_map.at(&0).get(&storage, &"string1".to_string())?, + Some(100) + ); + assert_eq!( + nested_map.at(&0).get(&storage, &"string2".to_string())?, + None + ); + + // Insert more values + nested_map + .at(&1) + .insert(&mut storage, "string1".to_string(), 200)?; + nested_map + .at(&0) + .insert(&mut storage, "string2".to_string(), 300)?; + + let mut it = nested_map.iter(&storage)?; + let ( + NestedSubKey::Data { + key, + nested_sub_key: SubKey::Data(inner_key), + }, + inner_val, + ) = it.next().unwrap()?; + assert_eq!(key, 0); + assert_eq!(inner_key, "string1".to_string()); + assert_eq!(inner_val, 100); + + let ( + NestedSubKey::Data { + key, + nested_sub_key: SubKey::Data(inner_key), + }, + inner_val, + ) = it.next().unwrap()?; + assert_eq!(key, 0); + assert_eq!(inner_key, "string2".to_string()); + assert_eq!(inner_val, 300); + + let ( + NestedSubKey::Data { + key, + nested_sub_key: SubKey::Data(inner_key), + }, + inner_val, + ) = it.next().unwrap()?; + assert_eq!(key, 1); + assert_eq!(inner_key, "string1".to_string()); + assert_eq!(inner_val, 200); + + // Next element should be None + assert!(it.next().is_none()); + drop(it); + + // Start removing elements + let rem = nested_map + .at(&0) + .remove(&mut storage, &"string2".to_string())?; + assert_eq!(rem, Some(300)); + assert_eq!( + nested_map.at(&0).get(&storage, &"string2".to_string())?, + None + ); + assert_eq!(nested_map.at(&0).len(&storage)?, 1_u64); + assert_eq!(nested_map.at(&1).len(&storage)?, 1_u64); + assert_eq!(nested_map.iter(&storage)?.count(), 2); + + // Start removing elements + let rem = nested_map + .at(&0) + .remove(&mut storage, &"string1".to_string())?; + assert_eq!(rem, Some(100)); + assert_eq!( + nested_map.at(&0).get(&storage, &"string1".to_string())?, + None + ); + assert_eq!(nested_map.at(&0).len(&storage)?, 0_u64); + assert_eq!(nested_map.at(&1).len(&storage)?, 1_u64); + assert_eq!(nested_map.iter(&storage)?.count(), 1); + + // Start removing elements + let rem = nested_map + .at(&1) + .remove(&mut storage, &"string1".to_string())?; + assert_eq!(rem, Some(200)); + assert_eq!( + nested_map.at(&1).get(&storage, &"string1".to_string())?, + None + ); + assert_eq!(nested_map.at(&0).len(&storage)?, 0_u64); + assert_eq!(nested_map.at(&1).len(&storage)?, 0_u64); + assert!(nested_map.is_empty(&storage)?); + + Ok(()) + } } From 5fbd90f3fc4fa8208bf19290c21cebcbc9158e6d Mon Sep 17 00:00:00 2001 From: brentstone Date: Fri, 26 May 2023 13:49:28 +0200 Subject: [PATCH 05/31] state machine test: add slashing --- .../tests/state_machine.txt | 6 +- proof_of_stake/src/tests/state_machine.rs | 2021 +++++++++++++++-- 2 files changed, 1851 insertions(+), 176 deletions(-) diff --git a/proof_of_stake/proptest-regressions/tests/state_machine.txt b/proof_of_stake/proptest-regressions/tests/state_machine.txt index 0022fdc698..fd37a6f64a 100644 --- a/proof_of_stake/proptest-regressions/tests/state_machine.txt +++ b/proof_of_stake/proptest-regressions/tests/state_machine.txt @@ -4,8 +4,4 @@ # # It is recommended to check this file in to source control so that # everyone who runs the test benefits from these saved cases. -cc d96c87f575b0ded4d16fb2ccb9496cb70688e80965289b15f4289b27f74936e0 # shrinks to (initial_state, transitions) = (AbstractPosState { epoch: Epoch(0), params: PosParams { max_validator_slots: 2, pipeline_len: 7, unbonding_len: 10, tm_votes_per_token: 0.3869, block_proposer_reward: 0.125, block_vote_reward: 0.1, max_inflation_rate: 0.1, target_staked_ratio: 0.6667, duplicate_vote_min_slash_rate: 0.001, light_client_attack_min_slash_rate: 0.001 }, genesis_validators: [GenesisValidator { address: Established: atest1v4ehgw36x5cngdejg9pyyd3egscnwvzxgsenjvjpxaq5zvpkxccrxsejxv6y2d6xgerrsv3cjrfert, tokens: Amount { micro: 188390939637 }, consensus_key: Ed25519(PublicKey(VerificationKey("ee1aa49a4459dfe813a3cf6eb882041230c7b2558469de81f87c9bf23bf10a03"))), commission_rate: 0.05, max_commission_rate_change: 0.001 }, GenesisValidator { address: Established: atest1v4ehgw368yeyydzpxqmnyv29xfq523p3gve5zse5g9zyy329gfqnzwfcggmnys3sgve52se4v5em0f, tokens: Amount { micro: 465797340965 }, consensus_key: Ed25519(PublicKey(VerificationKey("ff87a0b0a3c7c0ce827e9cada5ff79e75a44a0633bfcb5b50f99307ddb26b337"))), commission_rate: 0.05, max_commission_rate_change: 0.001 }, GenesisValidator { address: Established: atest1v4ehgw36xqe5y3fn8yenzd3egezrgs3cxuurwd3hxgmr2wf4gcmrjv2rg56yy33cxfprz3f5yefak4, tokens: Amount { micro: 954894516994 }, consensus_key: Ed25519(PublicKey(VerificationKey("191fc38f134aaf1b7fdb1f86330b9d03e94bd4ba884f490389de964448e89b3f"))), commission_rate: 0.05, max_commission_rate_change: 0.001 }], bonds: {Epoch(0): {BondId { source: Established: atest1v4ehgw36x5cngdejg9pyyd3egscnwvzxgsenjvjpxaq5zvpkxccrxsejxv6y2d6xgerrsv3cjrfert, validator: Established: atest1v4ehgw36x5cngdejg9pyyd3egscnwvzxgsenjvjpxaq5zvpkxccrxsejxv6y2d6xgerrsv3cjrfert }: 188390939637, BondId { source: Established: atest1v4ehgw368yeyydzpxqmnyv29xfq523p3gve5zse5g9zyy329gfqnzwfcggmnys3sgve52se4v5em0f, validator: Established: atest1v4ehgw368yeyydzpxqmnyv29xfq523p3gve5zse5g9zyy329gfqnzwfcggmnys3sgve52se4v5em0f }: 465797340965, BondId { source: Established: atest1v4ehgw36xqe5y3fn8yenzd3egezrgs3cxuurwd3hxgmr2wf4gcmrjv2rg56yy33cxfprz3f5yefak4, validator: Established: atest1v4ehgw36xqe5y3fn8yenzd3egezrgs3cxuurwd3hxgmr2wf4gcmrjv2rg56yy33cxfprz3f5yefak4 }: 954894516994}}, total_stakes: {Epoch(0): {Established: atest1v4ehgw36x5cngdejg9pyyd3egscnwvzxgsenjvjpxaq5zvpkxccrxsejxv6y2d6xgerrsv3cjrfert: Amount { micro: 188390939637 }, Established: atest1v4ehgw368yeyydzpxqmnyv29xfq523p3gve5zse5g9zyy329gfqnzwfcggmnys3sgve52se4v5em0f: Amount { micro: 465797340965 }, Established: atest1v4ehgw36xqe5y3fn8yenzd3egezrgs3cxuurwd3hxgmr2wf4gcmrjv2rg56yy33cxfprz3f5yefak4: Amount { micro: 954894516994 }}}, consensus_set: {Epoch(0): {Amount { micro: 188390939637 }: [Established: atest1v4ehgw36x5cngdejg9pyyd3egscnwvzxgsenjvjpxaq5zvpkxccrxsejxv6y2d6xgerrsv3cjrfert], Amount { micro: 465797340965 }: [Established: atest1v4ehgw368yeyydzpxqmnyv29xfq523p3gve5zse5g9zyy329gfqnzwfcggmnys3sgve52se4v5em0f]}}, below_capacity_set: {Epoch(0): {ReverseOrdTokenAmount(Amount { micro: 954894516994 }): [Established: atest1v4ehgw36xqe5y3fn8yenzd3egezrgs3cxuurwd3hxgmr2wf4gcmrjv2rg56yy33cxfprz3f5yefak4]}} }, [Bond { id: BondId { source: Established: atest1v4ehgw36g4zrs3fcxfpnw3pjxucrzv6pg3pyx3pex3py23pexscnzwz9xdzngvjxxfry2dzycuunza, validator: Established: atest1v4ehgw36g4zrs3fcxfpnw3pjxucrzv6pg3pyx3pex3py23pexscnzwz9xdzngvjxxfry2dzycuunza }, amount: Amount { micro: 1238 } }]) -cc 4633c576fa7c7e292a1902de35c186e539bd1fe37c4a23e9b3982e91ade7b2ca # shrinks to (initial_state, transitions) = (AbstractPosState { epoch: Epoch(0), params: PosParams { max_validator_slots: 4, pipeline_len: 7, unbonding_len: 9, tm_votes_per_token: 0.6158, block_proposer_reward: 0.125, block_vote_reward: 0.1, max_inflation_rate: 0.1, target_staked_ratio: 0.6667, duplicate_vote_min_slash_rate: 0.001, light_client_attack_min_slash_rate: 0.001 }, genesis_validators: [GenesisValidator { address: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, tokens: Amount { micro: 27248298187 }, consensus_key: Ed25519(PublicKey(VerificationKey("c5bbbb60e412879bbec7bb769804fa8e36e68af10d5477280b63deeaca931bed"))), commission_rate: 0.05, max_commission_rate_change: 0.001 }, GenesisValidator { address: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, tokens: Amount { micro: 372197384649 }, consensus_key: Ed25519(PublicKey(VerificationKey("ee1aa49a4459dfe813a3cf6eb882041230c7b2558469de81f87c9bf23bf10a03"))), commission_rate: 0.05, max_commission_rate_change: 0.001 }, GenesisValidator { address: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, tokens: Amount { micro: 599772865740 }, consensus_key: Ed25519(PublicKey(VerificationKey("ff87a0b0a3c7c0ce827e9cada5ff79e75a44a0633bfcb5b50f99307ddb26b337"))), commission_rate: 0.05, max_commission_rate_change: 0.001 }, GenesisValidator { address: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, tokens: Amount { micro: 695837066404 }, consensus_key: Ed25519(PublicKey(VerificationKey("191fc38f134aaf1b7fdb1f86330b9d03e94bd4ba884f490389de964448e89b3f"))), commission_rate: 0.05, max_commission_rate_change: 0.001 }], bonds: {Epoch(0): {BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }: 599772865740, BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }: 695837066404, BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }: 27248298187, BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }: 372197384649}}, total_stakes: {Epoch(0): {Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: 695837066404, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: 27248298187, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: 372197384649, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: 599772865740}}, consensus_set: {Epoch(0): {Amount { micro: 27248298187 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { micro: 372197384649 }: [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6], Amount { micro: 599772865740 }: [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], Amount { micro: 695837066404 }: [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv]}, Epoch(1): {Amount { micro: 27248298187 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { micro: 372197384649 }: [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6], Amount { micro: 599772865740 }: [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], Amount { micro: 695837066404 }: [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv]}, Epoch(2): {Amount { micro: 27248298187 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { micro: 372197384649 }: [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6], Amount { micro: 599772865740 }: [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], Amount { micro: 695837066404 }: [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv]}, Epoch(3): {Amount { micro: 27248298187 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { micro: 372197384649 }: [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6], Amount { micro: 599772865740 }: [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], Amount { micro: 695837066404 }: [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv]}, Epoch(4): {Amount { micro: 27248298187 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { micro: 372197384649 }: [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6], Amount { micro: 599772865740 }: [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], Amount { micro: 695837066404 }: [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv]}, Epoch(5): {Amount { micro: 27248298187 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { micro: 372197384649 }: [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6], Amount { micro: 599772865740 }: [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], Amount { micro: 695837066404 }: [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv]}, Epoch(6): {Amount { micro: 27248298187 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { micro: 372197384649 }: [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6], Amount { micro: 599772865740 }: [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], Amount { micro: 695837066404 }: [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv]}, Epoch(7): {Amount { micro: 27248298187 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { micro: 372197384649 }: [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6], Amount { micro: 599772865740 }: [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], Amount { micro: 695837066404 }: [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv]}}, below_capacity_set: {Epoch(0): {}, Epoch(1): {}, Epoch(2): {}, Epoch(3): {}, Epoch(4): {}, Epoch(5): {}, Epoch(6): {}, Epoch(7): {}}, validator_states: {Epoch(0): {Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: Consensus, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: Consensus, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus}, Epoch(1): {Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: Consensus, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: Consensus, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus}, Epoch(2): {Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: Consensus, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: Consensus, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus}, Epoch(3): {Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: Consensus, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: Consensus, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus}, Epoch(4): {Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: Consensus, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: Consensus, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus}, Epoch(5): {Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: Consensus, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: Consensus, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus}, Epoch(6): {Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: Consensus, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: Consensus, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus}, Epoch(7): {Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: Consensus, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: Consensus, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus}} }, [NextEpoch, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { micro: 8782278 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { micro: 1190208 } }, NextEpoch, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { micro: 7795726 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { micro: 6827686 } }, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { micro: 8183306 } }, NextEpoch, NextEpoch, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { micro: 9082723 } }, NextEpoch, NextEpoch, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { micro: 162577 } }, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { micro: 5422009 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { micro: 9752213 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { micro: 143033 } }, Bond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { micro: 2918291 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { micro: 3686768 } }, NextEpoch, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { micro: 6956073 } }, NextEpoch, NextEpoch, NextEpoch, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { micro: 6091560 } }, Bond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { micro: 5082475 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { micro: 1116228 } }, Bond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { micro: 2420024 } }, Bond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { micro: 4430691 } }, NextEpoch, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { micro: 1521967 } }]) -cc cbb985b391e16cb35fb2279ed2530c431e894f44fd269fe6cf76a8cdf118f1a0 # shrinks to (initial_state, transitions) = (AbstractPosState { epoch: Epoch(0), params: PosParams { max_validator_slots: 1, pipeline_len: 2, unbonding_len: 3, tm_votes_per_token: 0.0001, block_proposer_reward: 0.125, block_vote_reward: 0.1, max_inflation_rate: 0.1, target_staked_ratio: 0.6667, duplicate_vote_min_slash_rate: 0.001, light_client_attack_min_slash_rate: 0.001 }, genesis_validators: [GenesisValidator { address: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, tokens: Amount { micro: 781732759169 }, consensus_key: Ed25519(PublicKey(VerificationKey("ee1aa49a4459dfe813a3cf6eb882041230c7b2558469de81f87c9bf23bf10a03"))), commission_rate: 0.05, max_commission_rate_change: 0.001 }], bonds: {Epoch(0): {BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }: 781732759169}}, total_stakes: {Epoch(0): {Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: 781732759169}}, consensus_set: {Epoch(0): {Amount { micro: 781732759169 }: [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6]}, Epoch(1): {Amount { micro: 781732759169 }: [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6]}, Epoch(2): {Amount { micro: 781732759169 }: [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6]}}, below_capacity_set: {Epoch(0): {}, Epoch(1): {}, Epoch(2): {}}, validator_states: {Epoch(0): {Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: Consensus}, Epoch(1): {Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: Consensus}, Epoch(2): {Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: Consensus}} }, [NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { micro: 1 } }, NextEpoch, NextEpoch, NextEpoch, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { micro: 1 } }]) -cc cdf16c113fb6313f325503cf9101b8b5c23ff820bd8952d82ffb82c4eebebdbc # shrinks to (initial_state, transitions) = (AbstractPosState { epoch: Epoch(0), params: PosParams { max_validator_slots: 1, pipeline_len: 2, unbonding_len: 3, tm_votes_per_token: 0.0001, block_proposer_reward: 0.125, block_vote_reward: 0.1, max_inflation_rate: 0.1, target_staked_ratio: 0.6667, duplicate_vote_min_slash_rate: 0.001, light_client_attack_min_slash_rate: 0.001 }, genesis_validators: [GenesisValidator { address: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, tokens: Amount { micro: 139124683733 }, consensus_key: Ed25519(PublicKey(VerificationKey("ee1aa49a4459dfe813a3cf6eb882041230c7b2558469de81f87c9bf23bf10a03"))), commission_rate: 0.05, max_commission_rate_change: 0.001 }], bonds: {Epoch(0): {BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }: 139124683733}}, total_stakes: {Epoch(0): {Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: 139124683733}}, consensus_set: {Epoch(0): {Amount { micro: 139124683733 }: [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6]}, Epoch(1): {Amount { micro: 139124683733 }: [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6]}, Epoch(2): {Amount { micro: 139124683733 }: [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6]}}, below_capacity_set: {Epoch(0): {}, Epoch(1): {}, Epoch(2): {}}, validator_states: {Epoch(0): {Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: Consensus}, Epoch(1): {Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: Consensus}, Epoch(2): {Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: Consensus}} }, [NextEpoch, NextEpoch, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { micro: 1 } }, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { micro: 1 } }]) -cc fda96bfcdb63767251702535cfb4fd995d1fdda7d671fd085e2a536f00f2f6dd # shrinks to (initial_state, transitions) = (AbstractPosState { epoch: Epoch(0), params: PosParams { max_validator_slots: 1, pipeline_len: 2, unbonding_len: 3, tm_votes_per_token: 0.0001, block_proposer_reward: 0.125, block_vote_reward: 0.1, max_inflation_rate: 0.1, target_staked_ratio: 0.6667, duplicate_vote_min_slash_rate: 0.001, light_client_attack_min_slash_rate: 0.001 }, genesis_validators: [GenesisValidator { address: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, tokens: Amount { micro: 2 }, consensus_key: Ed25519(PublicKey(VerificationKey("ff87a0b0a3c7c0ce827e9cada5ff79e75a44a0633bfcb5b50f99307ddb26b337"))), commission_rate: 0.05, max_commission_rate_change: 0.001 }, GenesisValidator { address: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, tokens: Amount { micro: 4 }, consensus_key: Ed25519(PublicKey(VerificationKey("191fc38f134aaf1b7fdb1f86330b9d03e94bd4ba884f490389de964448e89b3f"))), commission_rate: 0.05, max_commission_rate_change: 0.001 }, GenesisValidator { address: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, tokens: Amount { micro: 5 }, consensus_key: Ed25519(PublicKey(VerificationKey("ee1aa49a4459dfe813a3cf6eb882041230c7b2558469de81f87c9bf23bf10a03"))), commission_rate: 0.05, max_commission_rate_change: 0.001 }, GenesisValidator { address: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, tokens: Amount { micro: 6 }, consensus_key: Ed25519(PublicKey(VerificationKey("c5bbbb60e412879bbec7bb769804fa8e36e68af10d5477280b63deeaca931bed"))), commission_rate: 0.05, max_commission_rate_change: 0.001 }, GenesisValidator { address: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, tokens: Amount { micro: 7 }, consensus_key: Ed25519(PublicKey(VerificationKey("4f44e6c7bdfed3d9f48d86149ee3d29382cae8c83ca253e06a70be54a301828b"))), commission_rate: 0.05, max_commission_rate_change: 0.001 }], bonds: {Epoch(0): {BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }: 2, BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }: 5, BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }: 7, BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }: 4, BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }: 6}}, total_stakes: {Epoch(0): {Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: 4, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: 6, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: 7, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: 2, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: 5}}, consensus_set: {Epoch(0): {Amount { micro: 2 }: [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk]}, Epoch(1): {Amount { micro: 2 }: [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk]}, Epoch(2): {Amount { micro: 2 }: [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk]}}, below_capacity_set: {Epoch(0): {ReverseOrdTokenAmount(Amount { micro: 4 }): [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv], ReverseOrdTokenAmount(Amount { micro: 5 }): [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6], ReverseOrdTokenAmount(Amount { micro: 6 }): [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], ReverseOrdTokenAmount(Amount { micro: 7 }): [Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd]}, Epoch(1): {ReverseOrdTokenAmount(Amount { micro: 4 }): [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv], ReverseOrdTokenAmount(Amount { micro: 5 }): [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6], ReverseOrdTokenAmount(Amount { micro: 6 }): [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], ReverseOrdTokenAmount(Amount { micro: 7 }): [Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd]}, Epoch(2): {ReverseOrdTokenAmount(Amount { micro: 4 }): [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv], ReverseOrdTokenAmount(Amount { micro: 5 }): [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6], ReverseOrdTokenAmount(Amount { micro: 6 }): [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], ReverseOrdTokenAmount(Amount { micro: 7 }): [Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd]}}, validator_states: {Epoch(0): {Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: BelowCapacity, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: BelowCapacity, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: BelowCapacity, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: BelowCapacity, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: Consensus}, Epoch(1): {Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: BelowCapacity, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: BelowCapacity, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: BelowCapacity, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: BelowCapacity, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: Consensus}, Epoch(2): {Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: BelowCapacity, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: BelowCapacity, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: BelowCapacity, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: BelowCapacity, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: Consensus}}, unbonds: {} }, [InitValidator { address: Established: atest1v4ehgw36xgunxvj9xqmny3jyxycnzdzxxqeng33ngvunqsfsx5mnwdfjgvenvwfk89prwdpjd0cjrk, consensus_key: Ed25519(PublicKey(VerificationKey("b9c6ee1630ef3e711144a648db06bbb2284f7274cfbee53ffcee503cc1a49200"))), commission_rate: 0, max_commission_rate_change: 0 }, Bond { id: BondId { source: Established: atest1v4ehgw36xgunxvj9xqmny3jyxycnzdzxxqeng33ngvunqsfsx5mnwdfjgvenvwfk89prwdpjd0cjrk, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { micro: 1 } }]) +cc 3076c8509d56c546d5915febcf429f218ab79a7bac34c75c288f531b88110bc3 # shrinks to (initial_state, transitions) = (AbstractPosState { epoch: Epoch(0), params: PosParams { max_validator_slots: 4, pipeline_len: 2, unbonding_len: 4, tm_votes_per_token: 0.0614, block_proposer_reward: 0.125, block_vote_reward: 0.1, max_inflation_rate: 0.1, target_staked_ratio: 0.6667, duplicate_vote_min_slash_rate: 0.001, light_client_attack_min_slash_rate: 0.001, cubic_slashing_window_length: 1 }, genesis_validators: [GenesisValidator { address: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, tokens: Amount { micro: 9185807 }, consensus_key: Ed25519(PublicKey(VerificationKey("ee1aa49a4459dfe813a3cf6eb882041230c7b2558469de81f87c9bf23bf10a03"))), commission_rate: 0.05, max_commission_rate_change: 0.01 }, GenesisValidator { address: Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6, tokens: Amount { micro: 5025206 }, consensus_key: Ed25519(PublicKey(VerificationKey("17888c2ca502371245e5e35d5bcf35246c3bc36878e859938c9ead3c54db174f"))), commission_rate: 0.05, max_commission_rate_change: 0.01 }, GenesisValidator { address: Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc, tokens: Amount { micro: 4424807 }, consensus_key: Ed25519(PublicKey(VerificationKey("478243aed376da313d7cf3a60637c264cb36acc936efb341ff8d3d712092d244"))), commission_rate: 0.05, max_commission_rate_change: 0.01 }, GenesisValidator { address: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, tokens: Amount { micro: 4119410 }, consensus_key: Ed25519(PublicKey(VerificationKey("c5bbbb60e412879bbec7bb769804fa8e36e68af10d5477280b63deeaca931bed"))), commission_rate: 0.05, max_commission_rate_change: 0.01 }, GenesisValidator { address: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, tokens: Amount { micro: 3619078 }, consensus_key: Ed25519(PublicKey(VerificationKey("4f44e6c7bdfed3d9f48d86149ee3d29382cae8c83ca253e06a70be54a301828b"))), commission_rate: 0.05, max_commission_rate_change: 0.01 }, GenesisValidator { address: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, tokens: Amount { micro: 2691447 }, consensus_key: Ed25519(PublicKey(VerificationKey("ff87a0b0a3c7c0ce827e9cada5ff79e75a44a0633bfcb5b50f99307ddb26b337"))), commission_rate: 0.05, max_commission_rate_change: 0.01 }, GenesisValidator { address: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, tokens: Amount { micro: 224944 }, consensus_key: Ed25519(PublicKey(VerificationKey("191fc38f134aaf1b7fdb1f86330b9d03e94bd4ba884f490389de964448e89b3f"))), commission_rate: 0.05, max_commission_rate_change: 0.01 }, GenesisValidator { address: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, tokens: Amount { micro: 142614 }, consensus_key: Ed25519(PublicKey(VerificationKey("e2e8aa145e1ec5cb01ebfaa40e10e12f0230c832fd8135470c001cb86d77de00"))), commission_rate: 0.05, max_commission_rate_change: 0.01 }], bonds: {BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }: {Epoch(0): 142614}, BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }: {Epoch(0): 4119410}, BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }: {Epoch(0): 9185807}, BondId { source: Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6, validator: Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6 }: {Epoch(0): 5025206}, BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }: {Epoch(0): 2691447}, BondId { source: Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc, validator: Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc }: {Epoch(0): 4424807}, BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }: {Epoch(0): 224944}, BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }: {Epoch(0): 3619078}}, validator_stakes: {Epoch(0): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: 142614, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: 4119410, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: 9185807, Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6: 5025206, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: 2691447, Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc: 4424807, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: 224944, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: 3619078}, Epoch(1): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: 142614, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: 4119410, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: 9185807, Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6: 5025206, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: 2691447, Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc: 4424807, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: 224944, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: 3619078}, Epoch(2): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: 142614, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: 4119410, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: 9185807, Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6: 5025206, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: 2691447, Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc: 4424807, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: 224944, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: 3619078}}, consensus_set: {Epoch(0): {Amount { micro: 4119410 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { micro: 4424807 }: [Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc], Amount { micro: 5025206 }: [Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6], Amount { micro: 9185807 }: [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6]}, Epoch(1): {Amount { micro: 4119410 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { micro: 4424807 }: [Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc], Amount { micro: 5025206 }: [Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6], Amount { micro: 9185807 }: [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6]}, Epoch(2): {Amount { micro: 4119410 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { micro: 4424807 }: [Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc], Amount { micro: 5025206 }: [Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6], Amount { micro: 9185807 }: [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6]}}, below_capacity_set: {Epoch(0): {ReverseOrdTokenAmount(Amount { micro: 142614 }): [Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6], ReverseOrdTokenAmount(Amount { micro: 224944 }): [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv], ReverseOrdTokenAmount(Amount { micro: 2691447 }): [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], ReverseOrdTokenAmount(Amount { micro: 3619078 }): [Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd]}, Epoch(1): {ReverseOrdTokenAmount(Amount { micro: 142614 }): [Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6], ReverseOrdTokenAmount(Amount { micro: 224944 }): [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv], ReverseOrdTokenAmount(Amount { micro: 2691447 }): [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], ReverseOrdTokenAmount(Amount { micro: 3619078 }): [Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd]}, Epoch(2): {ReverseOrdTokenAmount(Amount { micro: 142614 }): [Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6], ReverseOrdTokenAmount(Amount { micro: 224944 }): [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv], ReverseOrdTokenAmount(Amount { micro: 2691447 }): [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], ReverseOrdTokenAmount(Amount { micro: 3619078 }): [Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd]}}, validator_states: {Epoch(0): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: BelowCapacity, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: Consensus, Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6: Consensus, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: BelowCapacity, Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc: Consensus, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: BelowCapacity, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: BelowCapacity}, Epoch(1): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: BelowCapacity, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: Consensus, Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6: Consensus, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: BelowCapacity, Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc: Consensus, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: BelowCapacity, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: BelowCapacity}, Epoch(2): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: BelowCapacity, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: Consensus, Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6: Consensus, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: BelowCapacity, Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc: Consensus, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: BelowCapacity, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: BelowCapacity}}, unbonds: {}, validator_slashes: {}, enqueued_slashes: {}, validator_last_slash_epochs: {}, unbond_records: {} }, [InitValidator { address: Established: atest1v4ehgw36xgunxvj9xqmny3jyxycnzdzxxqeng33ngvunqsfsx5mnwdfjgvenvwfk89prwdpjd0cjrk, consensus_key: Ed25519(PublicKey(VerificationKey("bea04de1e5be8ca0ae27be8ad935df8d757e96c1e067e96aedeba0ded0df997d"))), commission_rate: 0.39428, max_commission_rate_change: 0.12485 }]) diff --git a/proof_of_stake/src/tests/state_machine.rs b/proof_of_stake/src/tests/state_machine.rs index 9d55c3004c..594e8dbb31 100644 --- a/proof_of_stake/src/tests/state_machine.rs +++ b/proof_of_stake/src/tests/state_machine.rs @@ -1,9 +1,12 @@ //! Test PoS transitions with a state machine -use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; +use std::cmp; +use std::collections::{BTreeMap, BTreeSet, HashSet, VecDeque}; use itertools::Itertools; use namada_core::ledger::storage::testing::TestWlStorage; +use namada_core::ledger::storage_api::collections::lazy_map::NestedSubKey; +use namada_core::ledger::storage_api::token::read_balance; use namada_core::ledger::storage_api::{token, StorageRead}; use namada_core::types::address::{self, Address}; use namada_core::types::key; @@ -14,6 +17,7 @@ use proptest::prop_state_machine; use proptest::state_machine::{ReferenceStateMachine, StateMachineTest}; use proptest::test_runner::Config; use rust_decimal::Decimal; +use rust_decimal_macros::dec; // Use `RUST_LOG=info` (or another tracing level) and `--nocapture` to see // `tracing` logs from tests use test_log::test; @@ -22,19 +26,25 @@ use super::arb_genesis_validators; use crate::parameters::testing::{arb_pos_params, arb_rate}; use crate::parameters::PosParams; use crate::types::{ - BondId, GenesisValidator, ReverseOrdTokenAmount, ValidatorState, + decimal_mult_amount, decimal_mult_i128, BondId, GenesisValidator, + ReverseOrdTokenAmount, Slash, SlashType, SlashedAmount, ValidatorState, WeightedValidator, }; +use crate::{ + below_capacity_validator_set_handle, consensus_validator_set_handle, + enqueued_slashes_handle, read_pos_params, validator_deltas_handle, + validator_slashes_handle, validator_state_handle, +}; prop_state_machine! { #![proptest_config(Config { - cases: 5, + cases: 2, verbose: 1, .. Config::default() })] #[test] /// A `StateMachineTest` implemented on `PosState` - fn pos_state_machine_test(sequential 1..200 => ConcretePosState); + fn pos_state_machine_test(sequential 500 => ConcretePosState); } /// Abstract representation of a state of PoS system @@ -44,24 +54,35 @@ struct AbstractPosState { epoch: Epoch, /// Parameters params: PosParams, - /// Genesis validator + /// Genesis validators genesis_validators: Vec, /// Bonds delta values. The outer key for Epoch is pipeline offset from /// epoch in which the bond is applied - bonds: BTreeMap>, - /// Validator stakes delta values (sum of all their bonds deltas). + bonds: BTreeMap>, + /// Validator stakes. These are NOT deltas. /// Pipelined. - total_stakes: BTreeMap>, + validator_stakes: BTreeMap>, /// Consensus validator set. Pipelined. consensus_set: BTreeMap>>, /// Below-capacity validator set. Pipelined. below_capacity_set: BTreeMap>>, /// Validator states. Pipelined. - validator_states: BTreeMap>, + validator_states: BTreeMap>, /// Unbonded bonds. The outer key for Epoch is pipeline + unbonding offset /// from epoch in which the unbond is applied. - unbonds: BTreeMap>, + unbonds: BTreeMap>, + /// Validator slashes post-processing + validator_slashes: BTreeMap>, + /// Enqueued slashes pre-processing + enqueued_slashes: BTreeMap>>, + /// The last epoch in which a validator committed an infraction + validator_last_slash_epochs: BTreeMap, + /// Unbond records required for slashing. + /// Inner `Epoch` is the epoch in which the unbond became active. + /// Outer `Epoch` is the epoch in which the underlying bond became active. + unbond_records: + BTreeMap>>, } /// The PoS system under test @@ -73,8 +94,6 @@ struct ConcretePosState { /// State machine transitions #[allow(clippy::large_enum_variant)] -// TODO: remove once all the transitions are being covered -#[allow(dead_code)] #[derive(Clone, Debug)] enum Transition { NextEpoch, @@ -95,6 +114,15 @@ enum Transition { Withdraw { id: BondId, }, + Misbehavior { + address: Address, + slash_type: SlashType, + infraction_epoch: Epoch, + height: u64, + }, + UnjailValidator { + address: Address, + }, } impl StateMachineTest for ConcretePosState { @@ -131,10 +159,23 @@ impl StateMachineTest for ConcretePosState { transition: ::Transition, ) -> Self::SystemUnderTest { let params = crate::read_pos_params(&state.s).unwrap(); + let pos_balance = read_balance( + &state.s, + &state.s.storage.native_token, + &crate::ADDRESS, + ) + .unwrap(); + println!("PoS balance: {}", pos_balance); match transition { Transition::NextEpoch => { + println!("\nCONCRETE Next epoch"); super::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(); + + let params = read_pos_params(&state.s).unwrap(); state.check_next_epoch_post_conditions(¶ms); } Transition::InitValidator { @@ -143,32 +184,37 @@ impl StateMachineTest for ConcretePosState { commission_rate, max_commission_rate_change, } => { - let epoch = state.current_epoch(); + println!("\nCONCRETE Init validator"); + let current_epoch = state.current_epoch(); super::become_validator( &mut state.s, ¶ms, &address, &consensus_key, - epoch, + current_epoch, commission_rate, max_commission_rate_change, ) .unwrap(); + let params = read_pos_params(&state.s).unwrap(); state.check_init_validator_post_conditions( - epoch, ¶ms, &address, + current_epoch, + ¶ms, + &address, ) } Transition::Bond { id, amount } => { - let epoch = state.current_epoch(); - let pipeline = epoch + params.pipeline_len; + println!("\nCONCRETE Bond"); + let current_epoch = state.current_epoch(); + let pipeline = current_epoch + params.pipeline_len; let validator_stake_before_bond_cur = crate::read_validator_stake( &state.s, ¶ms, &id.validator, - epoch, + current_epoch, ) .unwrap() .unwrap_or_default(); @@ -219,12 +265,13 @@ impl StateMachineTest for ConcretePosState { Some(&id.source), &id.validator, amount, - epoch, + current_epoch, ) .unwrap(); + let params = read_pos_params(&state.s).unwrap(); state.check_bond_post_conditions( - epoch, + current_epoch, ¶ms, id.clone(), amount, @@ -248,8 +295,9 @@ impl StateMachineTest for ConcretePosState { ); } Transition::Unbond { id, amount } => { - let epoch = state.current_epoch(); - let pipeline = epoch + params.pipeline_len; + println!("\nCONCRETE Unbond"); + let current_epoch = state.current_epoch(); + let pipeline = current_epoch + params.pipeline_len; let native_token = state.s.get_native_token().unwrap(); let pos = address::POS; let src_balance_pre = @@ -258,16 +306,16 @@ impl StateMachineTest for ConcretePosState { let pos_balance_pre = token::read_balance(&state.s, &native_token, &pos).unwrap(); - let validator_stake_before_bond_cur = + let validator_stake_before_unbond_cur = crate::read_validator_stake( &state.s, ¶ms, &id.validator, - epoch, + current_epoch, ) .unwrap() .unwrap_or_default(); - let validator_stake_before_bond_pipeline = + let validator_stake_before_unbond_pipeline = crate::read_validator_stake( &state.s, ¶ms, @@ -277,23 +325,30 @@ impl StateMachineTest for ConcretePosState { .unwrap() .unwrap_or_default(); + println!( + "BEFORE: cur_stake = {}, pipeline_stake = {}", + u64::from(validator_stake_before_unbond_cur), + u64::from(validator_stake_before_unbond_pipeline) + ); + // Apply the unbond super::unbond_tokens( &mut state.s, Some(&id.source), &id.validator, amount, - epoch, + current_epoch, ) .unwrap(); + let params = read_pos_params(&state.s).unwrap(); state.check_unbond_post_conditions( - epoch, + current_epoch, ¶ms, id.clone(), amount, - validator_stake_before_bond_cur, - validator_stake_before_bond_pipeline, + validator_stake_before_unbond_cur, + validator_stake_before_unbond_pipeline, ); let src_balance_post = @@ -310,21 +365,26 @@ impl StateMachineTest for ConcretePosState { Transition::Withdraw { id: BondId { source, validator }, } => { - let epoch = state.current_epoch(); + println!("\nCONCRETE Withdraw"); + let current_epoch = state.current_epoch(); let native_token = state.s.get_native_token().unwrap(); let pos = address::POS; + let slash_pool = address::POS_SLASH_POOL; let src_balance_pre = token::read_balance(&state.s, &native_token, &source) .unwrap(); let pos_balance_pre = token::read_balance(&state.s, &native_token, &pos).unwrap(); + let slash_balance_pre = + token::read_balance(&state.s, &native_token, &slash_pool) + .unwrap(); // Apply the withdrawal let withdrawn = super::withdraw_tokens( &mut state.s, Some(&source), &validator, - epoch, + current_epoch, ) .unwrap(); @@ -333,28 +393,80 @@ impl StateMachineTest for ConcretePosState { .unwrap(); let pos_balance_post = token::read_balance(&state.s, &native_token, &pos).unwrap(); + let slash_balance_post = + token::read_balance(&state.s, &native_token, &slash_pool) + .unwrap(); // Post-condition: PoS balance should decrease or not change if // nothing was withdrawn assert!(pos_balance_pre >= pos_balance_post); - // Post-condition: The difference in PoS balance should be the - // same as in the source + // Post-condition: The difference in PoS balance should be equal + // to the sum of the difference in the source and the difference + // in the slash pool assert_eq!( pos_balance_pre - pos_balance_post, - src_balance_post - src_balance_pre + src_balance_post - src_balance_pre + slash_balance_post + - slash_balance_pre ); // Post-condition: The increment in source balance should be // equal to the withdrawn amount - assert_eq!(src_balance_post - src_balance_pre, withdrawn,); + assert_eq!(src_balance_post - src_balance_pre, withdrawn); + } + Transition::Misbehavior { + address, + slash_type, + infraction_epoch, + height, + } => { + println!("\nCONCRETE Misbehavior"); + let current_epoch = state.current_epoch(); + // Record the slash evidence + super::slash( + &mut state.s, + ¶ms, + current_epoch, + infraction_epoch, + height, + slash_type, + &address, + ) + .unwrap(); + + // Apply some post-conditions + let params = read_pos_params(&state.s).unwrap(); + state.check_misbehavior_post_conditions( + ¶ms, + current_epoch, + infraction_epoch, + slash_type, + &address, + ); + + // TODO: Any others? + } + Transition::UnjailValidator { address } => { + println!("\nCONCRETE UnjailValidator"); + let current_epoch = state.current_epoch(); + + // Unjail the validator + super::unjail_validator(&mut state.s, &address, current_epoch) + .unwrap(); + + // Post-conditions + let params = read_pos_params(&state.s).unwrap(); + state.check_unjail_validator_post_conditions(¶ms, &address); } } state } fn check_invariants( - _state: &Self::SystemUnderTest, - _ref_state: &::State, + state: &Self::SystemUnderTest, + ref_state: &::State, ) { + let current_epoch = state.current_epoch(); + let params = read_pos_params(&state.s).unwrap(); + state.check_global_post_conditions(¶ms, current_epoch, ref_state); } } @@ -365,7 +477,7 @@ impl ConcretePosState { fn check_next_epoch_post_conditions(&self, params: &PosParams) { let pipeline = self.current_epoch() + params.pipeline_len; - let before_pipeline = pipeline - 1; + let before_pipeline = pipeline.prev(); // Post-condition: Consensus validator sets at pipeline offset // must be the same as at the epoch before it. @@ -402,6 +514,38 @@ impl ConcretePosState { below_cap_before_pipeline.into_iter().sorted(), below_cap_at_pipeline.into_iter().sorted(), ); + + // TODO: post-conditions for processing of slashes, just throwing things + // here atm + let slashed_validators = enqueued_slashes_handle() + .at(&self.current_epoch()) + .iter(&self.s) + .unwrap() + .map(|a| { + let ( + NestedSubKey::Data { + key: address, + nested_sub_key: _, + }, + _b, + ) = a.unwrap(); + address + }) + .collect::>(); + + for validator in &slashed_validators { + assert!( + !validator_slashes_handle(validator) + .is_empty(&self.s) + .unwrap() + ); + assert_eq!( + validator_state_handle(validator) + .get(&self.s, self.current_epoch(), params) + .unwrap(), + Some(ValidatorState::Jailed) + ); + } } fn check_bond_post_conditions( @@ -458,8 +602,8 @@ impl ConcretePosState { params: &PosParams, id: BondId, amount: token::Amount, - validator_stake_before_bond_cur: token::Amount, - validator_stake_before_bond_pipeline: token::Amount, + validator_stake_before_unbond_cur: token::Amount, + validator_stake_before_unbond_pipeline: token::Amount, ) { let pipeline = submit_epoch + params.pipeline_len; @@ -474,7 +618,7 @@ impl ConcretePosState { // Post-condition: the validator stake at the current epoch should not // change - assert_eq!(cur_stake, validator_stake_before_bond_cur); + assert_eq!(cur_stake, validator_stake_before_unbond_cur); let stake_at_pipeline = super::read_validator_stake( &self.s, @@ -484,13 +628,21 @@ impl ConcretePosState { ) .unwrap() .unwrap_or_default(); + println!("AFTER: pipeline stake = {}", u64::from(stake_at_pipeline)); // Post-condition: the validator stake at the pipeline should be - // decremented by the bond amount - assert_eq!( - stake_at_pipeline, - validator_stake_before_bond_pipeline - amount + // decremented at most by the bond amount (because slashing can reduce + // the actual amount unbonded) + // + // TODO: is this a weak assertion here? Seems cumbersome to calculate + // the exact amount considering the slashing applied can be complicated + assert!( + stake_at_pipeline + >= validator_stake_before_unbond_pipeline + .checked_sub(amount) + .unwrap_or_default() ); + println!("Check bond+unbond post-conds"); self.check_bond_and_unbond_post_conditions( submit_epoch, @@ -530,10 +682,19 @@ impl ConcretePosState { .iter() .filter(|(_keys, addr)| addr == &id.validator) .count(); - - // Post-condition: There must only be one instance of this validator - // with some stake across all validator sets - assert!(num_occurrences == 1); + let validator_is_jailed = crate::validator_state_handle(&id.validator) + .get(&self.s, pipeline, params) + .unwrap() + == Some(ValidatorState::Jailed); + + // Post-condition: There must only be one instance of this validator in + // the consensus + below-cap sets with some stake across all + // validator sets, OR there are no instances and this validator is + // jailed + assert!( + num_occurrences == 1 + || (num_occurrences == 0 && validator_is_jailed) + ); let consensus_set = crate::read_consensus_validator_set_addresses_with_stake( @@ -554,7 +715,13 @@ impl ConcretePosState { // Post-condition: The validator should be updated in exactly once in // the validator sets - assert!(consensus_val.is_some() ^ below_cap_val.is_some()); + let jailed_condition = validator_is_jailed + && consensus_val.is_none() + && below_cap_val.is_none(); + assert!( + (consensus_val.is_some() ^ below_cap_val.is_some()) + || jailed_condition + ); // Post-condition: The stake of the validators in the consensus set is // greater than or equal to below-capacity validators @@ -589,6 +756,18 @@ impl ConcretePosState { // Post-condition: the validator should not be in the validator set // until the pipeline epoch for epoch in submit_epoch.iter_range(params.pipeline_len) { + assert!( + !crate::read_consensus_validator_set_addresses(&self.s, epoch) + .unwrap() + .contains(address) + ); + assert!( + !crate::read_below_capacity_validator_set_addresses( + &self.s, epoch + ) + .unwrap() + .contains(address) + ); assert!( !crate::read_all_validator_addresses(&self.s, epoch) .unwrap() @@ -613,6 +792,317 @@ impl ConcretePosState { .contains(&weighted); assert!(in_consensus ^ in_bc); } + + fn check_misbehavior_post_conditions( + &self, + params: &PosParams, + current_epoch: Epoch, + infraction_epoch: Epoch, + slash_type: SlashType, + validator: &Address, + ) { + println!( + "\nChecking misbehavior post conditions for validator: \n{}", + validator + ); + + // Validator state jailed and validator removed from the consensus set + // starting at the next epoch + for offset in 1..=params.pipeline_len { + // dbg!( + // crate::read_consensus_validator_set_addresses_with_stake( + // &self.s, + // current_epoch + offset + // ) + // .unwrap() + // ); + assert_eq!( + validator_state_handle(validator) + .get(&self.s, current_epoch + offset, params) + .unwrap(), + Some(ValidatorState::Jailed) + ); + let in_consensus = consensus_validator_set_handle() + .at(&(current_epoch + offset)) + .iter(&self.s) + .unwrap() + .any(|res| { + let (_, val_address) = res.unwrap(); + // dbg!(&val_address); + val_address == validator.clone() + }); + assert!(!in_consensus); + } + + // `enqueued_slashes` contains the slash element just added + let processing_epoch = infraction_epoch + + params.unbonding_len + + 1_u64 + + params.cubic_slashing_window_length; + let slash = enqueued_slashes_handle() + .at(&processing_epoch) + .at(validator) + .back(&self.s) + .unwrap(); + if let Some(slash) = slash { + assert_eq!(slash.epoch, infraction_epoch); + assert_eq!(slash.r#type, slash_type); + assert_eq!(slash.rate, Decimal::ZERO); + } else { + panic!("Could not find the slash enqueued"); + } + println!("Finished misbehavior post-conditions\n") + + // TODO: Any others? + } + + fn check_unjail_validator_post_conditions( + &self, + params: &PosParams, + validator: &Address, + ) { + let current_epoch = self.s.storage.block.epoch; + + // Make sure the validator is not in either set until the pipeline epoch + for epoch in current_epoch.iter_range(params.pipeline_len) { + let in_consensus = consensus_validator_set_handle() + .at(&epoch) + .iter(&self.s) + .unwrap() + .any(|res| { + let (_, val_address) = res.unwrap(); + val_address == validator.clone() + }); + + let in_bc = below_capacity_validator_set_handle() + .at(&epoch) + .iter(&self.s) + .unwrap() + .any(|res| { + let (_, val_address) = res.unwrap(); + val_address == validator.clone() + }); + assert!(!in_consensus && !in_bc); + + let val_state = validator_state_handle(validator) + .get(&self.s, epoch, params) + .unwrap(); + assert_eq!(val_state, Some(ValidatorState::Jailed)); + } + let in_consensus = consensus_validator_set_handle() + .at(&(current_epoch + params.pipeline_len)) + .iter(&self.s) + .unwrap() + .any(|res| { + let (_, val_address) = res.unwrap(); + val_address == validator.clone() + }); + + let in_bc = below_capacity_validator_set_handle() + .at(&(current_epoch + params.pipeline_len)) + .iter(&self.s) + .unwrap() + .any(|res| { + let (_, val_address) = res.unwrap(); + val_address == validator.clone() + }); + assert!(in_consensus ^ in_bc); + + let val_state = validator_state_handle(validator) + .get(&self.s, current_epoch + params.pipeline_len, params) + .unwrap(); + assert!( + val_state == Some(ValidatorState::Consensus) + || val_state == Some(ValidatorState::BelowCapacity) + ); + } + + fn check_global_post_conditions( + &self, + params: &PosParams, + current_epoch: Epoch, + ref_state: &AbstractPosState, + ) { + // Ensure that every validator in each set has the proper state + for epoch in Epoch::iter_bounds_inclusive( + current_epoch, + current_epoch + params.pipeline_len, + ) { + println!("Epoch {epoch}"); + let mut vals = HashSet::
::new(); + for WeightedValidator { + bonded_stake, + address: validator, + } in crate::read_consensus_validator_set_addresses_with_stake( + &self.s, epoch, + ) + .unwrap() + { + let deltas_stake = validator_deltas_handle(&validator) + .get_sum(&self.s, epoch, params) + .unwrap() + .unwrap_or_default(); + println!( + "Consensus val {}, stake: {} ({})", + &validator, + u64::from(bonded_stake), + deltas_stake + ); + assert!(deltas_stake >= 0); + assert_eq!( + bonded_stake, + token::Amount::from_change(deltas_stake) + ); + assert_eq!( + bonded_stake.change(), + ref_state + .validator_stakes + .get(&epoch) + .unwrap() + .get(&validator) + .cloned() + .unwrap() + ); + + let state = crate::validator_state_handle(&validator) + .get(&self.s, epoch, params) + .unwrap(); + + assert_eq!(state, Some(ValidatorState::Consensus)); + assert_eq!( + state.unwrap(), + ref_state + .validator_states + .get(&epoch) + .unwrap() + .get(&validator) + .cloned() + .unwrap() + ); + assert!(!vals.contains(&validator)); + vals.insert(validator); + } + for WeightedValidator { + bonded_stake, + address: validator, + } in + crate::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) + .unwrap() + .unwrap_or_default(); + println!( + "Below-cap val {}, stake: {} ({})", + &validator, + u64::from(bonded_stake), + deltas_stake + ); + assert_eq!( + bonded_stake, + token::Amount::from_change(deltas_stake) + ); + assert_eq!( + bonded_stake.change(), + ref_state + .validator_stakes + .get(&epoch) + .unwrap() + .get(&validator) + .cloned() + .unwrap() + ); + + let state = crate::validator_state_handle(&validator) + .get(&self.s, epoch, params) + .unwrap(); + if state.is_none() { + dbg!( + crate::validator_state_handle(&validator) + .get(&self.s, current_epoch, params) + .unwrap() + ); + dbg!( + crate::validator_state_handle(&validator) + .get(&self.s, current_epoch.next(), params) + .unwrap() + ); + dbg!( + crate::validator_state_handle(&validator) + .get(&self.s, current_epoch.next(), params) + .unwrap() + ); + } + assert_eq!(state, Some(ValidatorState::BelowCapacity)); + assert_eq!( + state.unwrap(), + ref_state + .validator_states + .get(&epoch) + .unwrap() + .get(&validator) + .cloned() + .unwrap() + ); + assert!(!vals.contains(&validator)); + vals.insert(validator); + } + // Jailed validators not in a set + let all_validators = + crate::read_all_validator_addresses(&self.s, epoch).unwrap(); + + for val in all_validators { + let state = validator_state_handle(&val) + .get(&self.s, epoch, params) + .unwrap() + .unwrap(); + + if state == ValidatorState::Jailed { + assert_eq!( + state, + ref_state + .validator_states + .get(&epoch) + .unwrap() + .get(&val) + .cloned() + .unwrap() + ); + let stake = validator_deltas_handle(&val) + .get_sum(&self.s, epoch, params) + .unwrap() + .unwrap_or_default(); + println!("Jailed val {}, stake {}", &val, stake); + + assert_eq!( + state, + ref_state + .validator_states + .get(&epoch) + .unwrap() + .get(&val) + .cloned() + .unwrap() + ); + assert_eq!( + stake, + ref_state + .validator_stakes + .get(&epoch) + .unwrap() + .get(&val) + .cloned() + .unwrap() + ); + assert!(!vals.contains(&val)); + } + } + } + // TODO: expand this to include jailed validators + } } impl ReferenceStateMachine for AbstractPosState { @@ -620,7 +1110,8 @@ impl ReferenceStateMachine for AbstractPosState { type Transition = Transition; fn init_state() -> BoxedStrategy { - (arb_pos_params(Some(5)), arb_genesis_validators(1..10)) + println!("\nInitializing abstract state machine"); + (arb_pos_params(Some(5)), arb_genesis_validators(5..10)) .prop_map(|(params, genesis_validators)| { let epoch = Epoch::default(); let mut state = Self { @@ -630,13 +1121,18 @@ impl ReferenceStateMachine for AbstractPosState { .into_iter() // Sorted by stake to fill in the consensus set first .sorted_by(|a, b| Ord::cmp(&a.tokens, &b.tokens)) + .rev() .collect(), bonds: Default::default(), unbonds: Default::default(), - total_stakes: Default::default(), + validator_stakes: Default::default(), consensus_set: Default::default(), below_capacity_set: Default::default(), validator_states: Default::default(), + validator_slashes: Default::default(), + enqueued_slashes: Default::default(), + validator_last_slash_epochs: Default::default(), + unbond_records: Default::default(), }; for GenesisValidator { @@ -647,17 +1143,17 @@ impl ReferenceStateMachine for AbstractPosState { max_commission_rate_change: _, } in state.genesis_validators.clone() { - let bonds = state.bonds.entry(epoch).or_default(); - bonds.insert( - BondId { + let bonds = state + .bonds + .entry(BondId { source: address.clone(), validator: address.clone(), - }, - token::Change::from(tokens), - ); + }) + .or_default(); + bonds.insert(epoch, token::Change::from(tokens)); let total_stakes = - state.total_stakes.entry(epoch).or_default(); + state.validator_stakes.entry(epoch).or_default(); total_stakes .insert(address.clone(), token::Change::from(tokens)); @@ -701,23 +1197,50 @@ impl ReferenceStateMachine for AbstractPosState { { state.copy_discrete_epoched_data(epoch) } - + // dbg!(&state); state }) .boxed() } + // TODO: allow bonding to jailed val fn transitions(state: &Self::State) -> BoxedStrategy { + // Let preconditions filter out what unbonds are not allowed let unbondable = state.bond_sums().into_iter().collect::>(); + let withdrawable = state.withdrawable_unbonds().into_iter().collect::>(); + let eligible_for_unjail = state + .validator_states + .get(&state.pipeline()) + .unwrap() + .iter() + .filter_map(|(addr, &val_state)| { + let last_slash_epoch = + state.validator_last_slash_epochs.get(addr); + + if let Some(last_slash_epoch) = last_slash_epoch { + if val_state == ValidatorState::Jailed + // `last_slash_epoch` must be unbonding_len + window_width or more epochs + // before the current + && state.epoch.0 - last_slash_epoch.0 + > state.params.unbonding_len + state.params.cubic_slashing_window_length + { + return Some(addr.clone()); + } + } + None + }) + .collect::>(); + // Transitions that can be applied if there are no bonds and unbonds let basic = prop_oneof![ - Just(Transition::NextEpoch), - add_arb_bond_amount(state), - arb_delegation(state), - ( + 4 => Just(Transition::NextEpoch), + 6 => add_arb_bond_amount(state), + 5 => arb_delegation(state), + 3 => arb_self_bond(state), + 1 => ( address::testing::arb_established_address(), key::testing::arb_common_keypair(), arb_rate(), @@ -738,10 +1261,25 @@ impl ReferenceStateMachine for AbstractPosState { } }, ), + 2 => arb_slash(state), ]; - if unbondable.is_empty() { + // Add unjailing, if any eligible + let transitions = if eligible_for_unjail.is_empty() { basic.boxed() + } else { + prop_oneof![ + basic, + prop::sample::select(eligible_for_unjail).prop_map(|address| { + Transition::UnjailValidator { address } + }) + ] + .boxed() + }; + + // Add unbonds, if any + let transitions = if unbondable.is_empty() { + transitions } else { let arb_unbondable = prop::sample::select(unbondable); let arb_unbond = @@ -754,16 +1292,18 @@ impl ReferenceStateMachine for AbstractPosState { Transition::Unbond { id, amount } }) }); + prop_oneof![transitions, arb_unbond].boxed() + }; - if withdrawable.is_empty() { - prop_oneof![basic, arb_unbond].boxed() - } else { - let arb_withdrawable = prop::sample::select(withdrawable); - let arb_withdrawal = arb_withdrawable - .prop_map(|(id, _)| Transition::Withdraw { id }); + // Add withdrawals, if any + if withdrawable.is_empty() { + transitions + } else { + let arb_withdrawable = prop::sample::select(withdrawable); + let arb_withdrawal = arb_withdrawable + .prop_map(|(id, _)| Transition::Withdraw { id }); - prop_oneof![basic, arb_unbond, arb_withdrawal].boxed() - } + prop_oneof![transitions, arb_withdrawal].boxed() } } @@ -773,12 +1313,18 @@ impl ReferenceStateMachine for AbstractPosState { ) -> Self::State { match transition { Transition::NextEpoch => { + println!("\nABSTRACT Next Epoch"); + state.epoch = state.epoch.next(); // Copy the non-delta data into pipeline epoch from its pred. - state.copy_discrete_epoched_data( - state.epoch + state.params.pipeline_len, - ); + state.copy_discrete_epoched_data(state.pipeline()); + + // Process slashes enqueued for the new epoch + state.process_enqueued_slashes(); + + // print-out the state + state.debug_validators(); } Transition::InitValidator { address, @@ -786,8 +1332,20 @@ impl ReferenceStateMachine for AbstractPosState { commission_rate: _, max_commission_rate_change: _, } => { + println!( + "\nABSTRACT Init Validator {} in epoch {}", + address, state.epoch + ); + let pipeline: Epoch = state.pipeline(); + + // Initialize the stake at pipeline + state + .validator_stakes + .entry(pipeline) + .or_default() + .insert(address.clone(), 0_i128); + // Insert into validator set at pipeline - let pipeline = state.pipeline(); let consensus_set = state.consensus_set.entry(pipeline).or_default(); @@ -818,29 +1376,56 @@ impl ReferenceStateMachine for AbstractPosState { .or_default() }; deque.push_back(address.clone()); + + state.debug_validators(); } Transition::Bond { id, amount } => { - let change = token::Change::from(*amount); - state.update_bond(id, change); - state.update_validator_total_stake(&id.validator, change); - state.update_validator_sets(&id.validator, change); + println!("\nABSTRACT Bond {} tokens, id = {}", amount, id); + + if *amount != token::Amount::default() { + let change = token::Change::from(*amount); + let pipeline_state = state + .validator_states + .get(&state.pipeline()) + .unwrap() + .get(&id.validator) + .unwrap(); + + // Validator sets need to be updated first!! + if *pipeline_state != ValidatorState::Jailed { + state.update_validator_sets(&id.validator, change); + } + state.update_bond(id, change); + state.update_validator_total_stake(&id.validator, change); + } + state.debug_validators(); } Transition::Unbond { id, amount } => { - let change = -token::Change::from(*amount); - state.update_bond(id, change); - state.update_validator_total_stake(&id.validator, change); - state.update_validator_sets(&id.validator, change); - - let withdrawal_epoch = state.epoch - + state.params.pipeline_len - + state.params.unbonding_len - + 1_u64; - let unbonds = - state.unbonds.entry(withdrawal_epoch).or_default(); - let unbond = unbonds.entry(id.clone()).or_default(); - *unbond += *amount; + println!("\nABSTRACT Unbond {} tokens, id = {}", amount, id); + + if *amount != token::Amount::default() { + let change = token::Change::from(*amount); + state.update_state_with_unbond(id, change); + + // Validator sets need to be updated first!! + // state.update_validator_sets(&id.validator, change); + // state.update_bond(id, change); + // state.update_validator_total_stake(&id.validator, + // change); + + // let withdrawal_epoch = + // state.pipeline() + state.params.unbonding_len; + // // + 1_u64; + // let unbonds = + // state.unbonds.entry(withdrawal_epoch).or_default(); + // let unbond = unbonds.entry(id.clone()).or_default(); + // *unbond += *amount; + } + state.debug_validators(); } Transition::Withdraw { id } => { + println!("\nABSTRACT Withdraw, id = {}", id); + // Remove all withdrawable unbonds with this bond ID for (epoch, unbonds) in state.unbonds.iter_mut() { if *epoch <= state.epoch { @@ -849,6 +1434,269 @@ impl ReferenceStateMachine for AbstractPosState { } // Remove any epochs that have no unbonds left state.unbonds.retain(|_epoch, unbonds| !unbonds.is_empty()); + + // TODO: should we do anything here for slashing? + } + Transition::Misbehavior { + address, + slash_type, + infraction_epoch, + height, + } => { + let current_epoch = state.epoch; + println!( + "\nABSTRACT Misbehavior in epoch {} by validator {}, \ + found in epoch {}", + infraction_epoch, address, current_epoch + ); + + let processing_epoch = *infraction_epoch + + state.params.unbonding_len + + 1_u64 + + state.params.cubic_slashing_window_length; + let slash = Slash { + epoch: *infraction_epoch, + block_height: *height, + r#type: *slash_type, + rate: Decimal::ZERO, + }; + + // Enqueue the slash for future processing + state + .enqueued_slashes + .entry(processing_epoch) + .or_default() + .entry(address.clone()) + .or_default() + .push(slash); + + // Remove the validator from either the consensus or + // below-capacity set and place it into the jailed validator set + + // Remove from the validator set starting at the next epoch and + // up thru the pipeline + for offset in 1..=state.params.pipeline_len { + let real_stake = token::Amount::from_change( + state + .validator_stakes + .get(&(current_epoch + offset)) + .unwrap() + .get(address) + .cloned() + .unwrap_or_default(), + ); + + if let Some((index, stake)) = state + .is_in_consensus_w_info(address, current_epoch + offset) + { + debug_assert_eq!(stake, real_stake); + + let vals = state + .consensus_set + .entry(current_epoch + offset) + .or_default() + .entry(stake) + .or_default(); + let removed = vals.remove(index); + debug_assert_eq!(removed, Some(address.clone())); + if vals.is_empty() { + state + .consensus_set + .entry(current_epoch + offset) + .or_default() + .remove(&stake); + } + + // At pipeline epoch, if was consensus, replace it with + // a below-capacity validator + if offset == state.params.pipeline_len { + let below_cap_pipeline = state + .below_capacity_set + .entry(current_epoch + offset) + .or_default(); + + if let Some(mut max_below_cap) = + below_cap_pipeline.last_entry() + { + let max_bc_stake = *max_below_cap.key(); + let vals = max_below_cap.get_mut(); + let first_val = vals.pop_front().unwrap(); + if vals.is_empty() { + below_cap_pipeline.remove(&max_bc_stake); + } + state + .consensus_set + .entry(current_epoch + offset) + .or_default() + .entry(max_bc_stake.into()) + .or_default() + .push_back(first_val.clone()); + state + .validator_states + .entry(current_epoch + offset) + .or_default() + .insert( + first_val.clone(), + ValidatorState::Consensus, + ); + } + } + } else if let Some((index, stake)) = state + .is_in_below_capacity_w_info( + address, + current_epoch + offset, + ) + { + debug_assert_eq!(stake, real_stake); + + let vals = state + .below_capacity_set + .entry(current_epoch + offset) + .or_default() + .entry(stake.into()) + .or_default(); + + let removed = vals.remove(index); + debug_assert_eq!(removed, Some(address.clone())); + if vals.is_empty() { + state + .below_capacity_set + .entry(current_epoch + offset) + .or_default() + .remove(&stake.into()); + } + } else { + // Just make sure the validator is already jailed + debug_assert_eq!( + state + .validator_states + .get(&(current_epoch + offset)) + .unwrap() + .get(address) + .cloned() + .unwrap(), + ValidatorState::Jailed + ); + } + + state + .validator_states + .entry(current_epoch + offset) + .or_default() + .insert(address.clone(), ValidatorState::Jailed); + } + + // Update the most recent infraction epoch for the validator + if let Some(last_epoch) = + state.validator_last_slash_epochs.get(address) + { + if infraction_epoch > last_epoch { + state + .validator_last_slash_epochs + .insert(address.clone(), *infraction_epoch); + } + } else { + state + .validator_last_slash_epochs + .insert(address.clone(), *infraction_epoch); + } + + state.debug_validators(); + } + Transition::UnjailValidator { address } => { + let pipeline_epoch = state.pipeline(); + + println!( + "\nABSTRACT Unjail validator {} starting in epoch {}", + address.clone(), + pipeline_epoch + ); + + let consensus_set_pipeline = + state.consensus_set.entry(pipeline_epoch).or_default(); + let pipeline_stake = state + .validator_stakes + .get(&pipeline_epoch) + .unwrap() + .get(address) + .cloned() + .unwrap_or_default(); + let validator_states_pipeline = + state.validator_states.entry(pipeline_epoch).or_default(); + + // Insert the validator back into the appropriate validator set + // and update its state + let num_consensus = consensus_set_pipeline + .iter() + .fold(0, |sum, (_, validators)| { + sum + validators.len() as u64 + }); + + if num_consensus < state.params.max_validator_slots { + // Place directly into the consensus set + debug_assert!( + state + .below_capacity_set + .get(&pipeline_epoch) + .unwrap() + .is_empty() + ); + consensus_set_pipeline + .entry(token::Amount::from_change(pipeline_stake)) + .or_default() + .push_back(address.clone()); + validator_states_pipeline + .insert(address.clone(), ValidatorState::Consensus); + } else if let Some(mut min_consensus) = + consensus_set_pipeline.first_entry() + { + let below_capacity_set_pipeline = state + .below_capacity_set + .entry(pipeline_epoch) + .or_default(); + + let min_consensus_stake = *min_consensus.key(); + if pipeline_stake > min_consensus_stake.change() { + // Place into the consensus set and demote the last + // min_consensus validator + let min_validators = min_consensus.get_mut(); + let last_val = min_validators.pop_back().unwrap(); + // Remove the key if there's nothing left + if min_validators.is_empty() { + consensus_set_pipeline.remove(&min_consensus_stake); + } + // Do the swap + below_capacity_set_pipeline + .entry(min_consensus_stake.into()) + .or_default() + .push_back(last_val.clone()); + validator_states_pipeline + .insert(last_val, ValidatorState::BelowCapacity); + + consensus_set_pipeline + .entry(token::Amount::from_change(pipeline_stake)) + .or_default() + .push_back(address.clone()); + validator_states_pipeline + .insert(address.clone(), ValidatorState::Consensus); + } else { + // Just place into the below-capacity set + below_capacity_set_pipeline + .entry( + token::Amount::from_change(pipeline_stake) + .into(), + ) + .or_default() + .push_back(address.clone()); + validator_states_pipeline.insert( + address.clone(), + ValidatorState::BelowCapacity, + ); + } + } else { + panic!("Should not reach here I don't think") + } + state.debug_validators(); } } state @@ -859,6 +1707,7 @@ impl ReferenceStateMachine for AbstractPosState { transition: &Self::Transition, ) -> bool { match transition { + // TODO: should there be any slashing preconditions for `NextEpoch`? Transition::NextEpoch => true, Transition::InitValidator { address, @@ -866,23 +1715,26 @@ impl ReferenceStateMachine for AbstractPosState { commission_rate: _, max_commission_rate_change: _, } => { - let pipeline = state.epoch + state.params.pipeline_len; + let pipeline = state.pipeline(); // The address must not belong to an existing validator !state.is_validator(address, pipeline) && // There must be no delegations from this address !state.bond_sums().into_iter().any(|(id, _sum)| - &id.source != address) + &id.source == address) } Transition::Bond { id, amount: _ } => { - let pipeline = state.epoch + state.params.pipeline_len; + let pipeline = state.pipeline(); // The validator must be known - state.is_validator(&id.validator, pipeline) - && (id.validator == id.source + if !state.is_validator(&id.validator, pipeline) { + return false; + } + + id.validator == id.source // If it's not a self-bond, the source must not be a validator - || !state.is_validator(&id.source, pipeline)) + || !state.is_validator(&id.source, pipeline) } Transition::Unbond { id, amount } => { - let pipeline = state.epoch + state.params.pipeline_len; + let pipeline = state.pipeline(); let is_unbondable = state .bond_sums() @@ -890,13 +1742,33 @@ impl ReferenceStateMachine for AbstractPosState { .map(|sum| *sum >= token::Change::from(*amount)) .unwrap_or_default(); + // The validator must not be frozen currently + let is_frozen = if let Some(last_epoch) = + state.validator_last_slash_epochs.get(&id.validator) + { + *last_epoch + + state.params.unbonding_len + + 1u64 + + state.params.cubic_slashing_window_length + > state.epoch + } else { + false + }; + + if is_frozen { + println!( + "\nVALIDATOR {} IS FROZEN - CANNOT UNBOND\n", + &id.validator + ); + } + // The validator must be known state.is_validator(&id.validator, pipeline) - // The amount must be available to unbond - && is_unbondable + // The amount must be available to unbond and the validator not jailed + && is_unbondable && !is_frozen } Transition::Withdraw { id } => { - let pipeline = state.epoch + state.params.pipeline_len; + let pipeline = state.pipeline(); let is_withdrawable = state .withdrawable_unbonds() @@ -904,10 +1776,123 @@ impl ReferenceStateMachine for AbstractPosState { .map(|amount| *amount >= token::Amount::default()) .unwrap_or_default(); + // The validator must not be jailed currently + let is_jailed = state + .validator_states + .get(&state.epoch) + .unwrap() + .get(&id.validator) + .cloned() + == Some(ValidatorState::Jailed); + // The validator must be known state.is_validator(&id.validator, pipeline) // The amount must be available to unbond - && is_withdrawable + && is_withdrawable && !is_jailed + } + Transition::Misbehavior { + address, + slash_type: _, + infraction_epoch, + height: _, + } => { + let is_validator = + state.is_validator(address, *infraction_epoch); + + // The infraction epoch cannot be in the future or more than + // unbonding_len epochs in the past + let current_epoch = state.epoch; + let valid_epoch = *infraction_epoch <= current_epoch + && current_epoch.0 - infraction_epoch.0 + <= state.params.unbonding_len; + + // Only misbehave when there is more than 3 validators that's + // not jailed, so there's always at least one honest left + let enough_honest_validators = || { + state + .validator_states + .get(&state.pipeline()) + .unwrap() + .iter() + .filter(|(_addr, val_state)| match val_state { + ValidatorState::Consensus + | ValidatorState::BelowCapacity => true, + ValidatorState::Inactive + | ValidatorState::Jailed => false, + }) + .count() + > 3 + }; + + // Ensure that the validator is in consensus when it misbehaves + // TODO: possibly also test allowing below-capacity validators + println!("\nVal to possibly misbehave: {}", &address); + let state_at_infraction = state + .validator_states + .get(infraction_epoch) + .unwrap() + .get(address); + if state_at_infraction.is_none() { + // Figure out why this happening + println!( + "State is None at Infraction epoch {}", + infraction_epoch + ); + for epoch in Epoch::iter_bounds_inclusive( + infraction_epoch.next(), + state.epoch, + ) { + let state_ep = state + .validator_states + .get(infraction_epoch) + .unwrap() + .get(address) + .cloned(); + println!("State at epoch {} is {:?}", epoch, state_ep); + } + } + + let can_misbehave = state_at_infraction.cloned() + == Some(ValidatorState::Consensus); + + is_validator + && valid_epoch + && enough_honest_validators() + && can_misbehave + + // TODO: any others conditions? + } + Transition::UnjailValidator { address } => { + // Validator address must be jailed thru the pipeline epoch + for epoch in + Epoch::iter_bounds_inclusive(state.epoch, state.pipeline()) + { + if state + .validator_states + .get(&epoch) + .unwrap() + .get(address) + .cloned() + .unwrap() + != ValidatorState::Jailed + { + return false; + } + } + // Most recent misbehavior is >= unbonding_len epochs away from + // current epoch + if let Some(last_slash_epoch) = + state.validator_last_slash_epochs.get(address) + { + if state.epoch.0 - last_slash_epoch.0 + < state.params.unbonding_len + { + return false; + } + } + + true + // TODO: any others? } } } @@ -917,7 +1902,7 @@ impl AbstractPosState { /// Copy validator sets and validator states at the given epoch from its /// predecessor fn copy_discrete_epoched_data(&mut self, epoch: Epoch) { - let prev_epoch = Epoch(epoch.0 - 1); + let prev_epoch = epoch.prev(); // Copy the non-delta data from the last epoch into the new one self.consensus_set.insert( epoch, @@ -931,27 +1916,130 @@ impl AbstractPosState { epoch, self.validator_states.get(&prev_epoch).unwrap().clone(), ); + self.validator_stakes.insert( + epoch, + self.validator_stakes.get(&prev_epoch).unwrap().clone(), + ); } - /// Update a bond with bonded or unbonded change + /// Update a bond with bonded or unbonded change at the pipeline epoch fn update_bond(&mut self, id: &BondId, change: token::Change) { - let bonds = self.bonds.entry(self.pipeline()).or_default(); - let bond = bonds.entry(id.clone()).or_default(); + let pipeline_epoch = self.pipeline(); + let bonds = self.bonds.entry(id.clone()).or_default(); + let bond = bonds.entry(pipeline_epoch).or_default(); *bond += change; // Remove fully unbonded entries if *bond == 0 { - bonds.remove(id); + bonds.remove(&pipeline_epoch); + } + } + + fn update_state_with_unbond(&mut self, id: &BondId, change: token::Change) { + let pipeline_epoch = self.pipeline(); + let withdraw_epoch = pipeline_epoch + + self.params.unbonding_len + + self.params.cubic_slashing_window_length; + let bonds = self.bonds.entry(id.clone()).or_default(); + let unbond_records = self + .unbond_records + .entry(id.validator.clone()) + .or_default() + .entry(pipeline_epoch) + .or_default(); + let unbonds = self + .unbonds + .entry(withdraw_epoch) + .or_default() + .entry(id.clone()) + .or_default(); + let validator_slashes = self + .validator_slashes + .get(&id.validator) + .cloned() + .unwrap_or_default(); + + let mut remaining = change; + let mut amount_after_slashing = token::Change::default(); + + println!("Bonds before decrementing"); + for (start, amnt) in bonds.iter() { + println!("Bond epoch {} - amnt {}", start, amnt); + } + + for (bond_epoch, bond_amnt) in bonds.iter_mut().rev() { + println!("remaining {}", remaining); + println!("Bond epoch {} - amnt {}", bond_epoch, bond_amnt); + let to_unbond = cmp::min(*bond_amnt, remaining); + println!("to_unbond (init) = {}", to_unbond); + *bond_amnt -= to_unbond; + *unbonds += token::Amount::from_change(to_unbond); + + let slashes_for_this_bond: BTreeMap = + validator_slashes + .iter() + .cloned() + .filter(|s| *bond_epoch <= s.epoch) + .fold(BTreeMap::new(), |mut acc, s| { + let cur = acc.entry(s.epoch).or_default(); + *cur += s.rate; + acc + }); + println!( + "Slashes for this bond{:?}", + slashes_for_this_bond.clone() + ); + amount_after_slashing += compute_amount_after_slashing( + &slashes_for_this_bond, + token::Amount::from_change(to_unbond), + self.params.unbonding_len, + ) + .change(); + println!("Cur amnt after slashing = {}", &amount_after_slashing); + + let amt = unbond_records.entry(*bond_epoch).or_default(); + *amt += token::Amount::from_change(to_unbond); + + remaining -= to_unbond; + if remaining == 0 { + break; + } + } + + println!("Bonds after decrementing"); + for (start, amnt) in bonds.iter() { + println!("Bond epoch {} - amnt {}", start, amnt); + } + + let pipeline_state = self + .validator_states + .get(&self.pipeline()) + .unwrap() + .get(&id.validator) + .unwrap(); + let pipeline_stake = self + .validator_stakes + .get(&self.pipeline()) + .unwrap() + .get(&id.validator) + .unwrap(); + println!("pipeline stake = {}", pipeline_stake); + let token_change = cmp::min(*pipeline_stake, amount_after_slashing); + + if *pipeline_state != ValidatorState::Jailed { + self.update_validator_sets(&id.validator, -token_change); } + self.update_validator_total_stake(&id.validator, -token_change); } - /// Update validator's total stake with bonded or unbonded change + /// Update validator's total stake with bonded or unbonded change at the + /// pipeline epoch fn update_validator_total_stake( &mut self, validator: &Address, change: token::Change, ) { let total_stakes = self - .total_stakes + .validator_stakes .entry(self.pipeline()) .or_default() .entry(validator.clone()) @@ -969,25 +2057,28 @@ impl AbstractPosState { let consensus_set = self.consensus_set.entry(pipeline).or_default(); let below_cap_set = self.below_capacity_set.entry(pipeline).or_default(); - let total_stakes = self.total_stakes.get(&pipeline).unwrap(); - let state = self - .validator_states - .get(&pipeline) - .unwrap() - .get(validator) - .unwrap(); + let validator_stakes = self.validator_stakes.get(&pipeline).unwrap(); + let validator_states = + self.validator_states.get_mut(&pipeline).unwrap(); + + let state = validator_states.get(validator).unwrap(); - let this_val_stake_pre = *total_stakes.get(validator).unwrap(); + let this_val_stake_pre = *validator_stakes.get(validator).unwrap(); let this_val_stake_post = token::Amount::from_change(this_val_stake_pre + change); - let this_val_stake_pre = - token::Amount::from_change(*total_stakes.get(validator).unwrap()); + let this_val_stake_pre = token::Amount::from_change( + *validator_stakes.get(validator).unwrap(), + ); match state { ValidatorState::Consensus => { + println!("Validator initially in consensus"); // Remove from the prior stake let vals = consensus_set.entry(this_val_stake_pre).or_default(); + // dbg!(&vals); vals.retain(|addr| addr != validator); + // dbg!(&vals); + if vals.is_empty() { consensus_set.remove(&this_val_stake_pre); } @@ -1002,19 +2093,28 @@ impl AbstractPosState { // Swap this validator with the max below-cap let vals = max_below_cap.get_mut(); let first_val = vals.pop_front().unwrap(); - // Remove the key is there's nothing left + // Remove the key if there's nothing left if vals.is_empty() { below_cap_set.remove(&max_below_cap_stake); } - // Do the swap + // Do the swap in the validator sets consensus_set .entry(max_below_cap_stake.0) .or_default() - .push_back(first_val); + .push_back(first_val.clone()); below_cap_set .entry(this_val_stake_post.into()) .or_default() .push_back(validator.clone()); + + // Change the validator states + validator_states + .insert(first_val, ValidatorState::Consensus); + validator_states.insert( + validator.clone(), + ValidatorState::BelowCapacity, + ); + // And we're done here return; } @@ -1028,6 +2128,8 @@ impl AbstractPosState { .push_back(validator.clone()); } ValidatorState::BelowCapacity => { + println!("Validator initially in below-cap"); + // Remove from the prior stake let vals = below_cap_set.entry(this_val_stake_pre.into()).or_default(); @@ -1039,26 +2141,39 @@ impl AbstractPosState { // If bonding, check the min consensus validator's state if we // need to do a swap if change >= token::Change::default() { - if let Some(mut min_below_cap) = consensus_set.last_entry() + // dbg!(&consensus_set); + if let Some(mut min_consensus) = consensus_set.first_entry() { - let min_consensus_stake = *min_below_cap.key(); - if min_consensus_stake > this_val_stake_post { + // dbg!(&min_consensus); + let min_consensus_stake = *min_consensus.key(); + if this_val_stake_post > min_consensus_stake { // Swap this validator with the max consensus - let vals = min_below_cap.get_mut(); + let vals = min_consensus.get_mut(); let last_val = vals.pop_back().unwrap(); - // Remove the key is there's nothing left + // Remove the key if there's nothing left if vals.is_empty() { consensus_set.remove(&min_consensus_stake); } - // Do the swap + // Do the swap in the validator sets below_cap_set .entry(min_consensus_stake.into()) .or_default() - .push_back(last_val); + .push_back(last_val.clone()); consensus_set .entry(this_val_stake_post) .or_default() .push_back(validator.clone()); + + // Change the validator states + validator_states.insert( + validator.clone(), + ValidatorState::Consensus, + ); + validator_states.insert( + last_val, + ValidatorState::BelowCapacity, + ); + // And we're done here return; } @@ -1074,6 +2189,261 @@ impl AbstractPosState { ValidatorState::Inactive => { panic!("unexpected state") } + ValidatorState::Jailed => { + panic!("unexpected state (jailed)") + } + } + } + + fn process_enqueued_slashes(&mut self) { + let slashes_this_epoch = self + .enqueued_slashes + .get(&self.epoch) + .cloned() + .unwrap_or_default(); + if !slashes_this_epoch.is_empty() { + let infraction_epoch = self.epoch + - self.params.unbonding_len + - 1 + - self.params.cubic_slashing_window_length; + // Now need to basically do the end_of_epoch() procedure + // from the Informal Systems model + let cubic_rate = self.cubic_slash_rate(); + for (validator, slashes) in slashes_this_epoch { + let stake_at_infraction = self + .validator_stakes + .get(&infraction_epoch) + .unwrap() + .get(&validator) + .cloned() + .unwrap_or_default(); + println!( + "Val {} stake at infraction {}", + validator, stake_at_infraction + ); + + let mut total_rate = Decimal::ZERO; + + for slash in slashes { + debug_assert_eq!(slash.epoch, infraction_epoch); + let rate = cmp::max( + slash.r#type.get_slash_rate(&self.params), + cubic_rate, + ); + let processed_slash = Slash { + epoch: slash.epoch, + block_height: slash.block_height, + r#type: slash.r#type, + rate, + }; + let cur_slashes = self + .validator_slashes + .entry(validator.clone()) + .or_default(); + cur_slashes.push(processed_slash.clone()); + + total_rate += rate; + } + total_rate = cmp::min(total_rate, Decimal::ONE); + println!("Total rate: {}", total_rate); + + let mut total_unbonded = token::Amount::default(); + for epoch in (infraction_epoch.0 + 1)..self.epoch.0 { + println!("\nEpoch {}", epoch); + let unbond_records = self + .unbond_records + .entry(validator.clone()) + .or_default() + .get(&Epoch(epoch)) + .cloned() + .unwrap_or_default(); + for (start, unbond_amount) in unbond_records { + println!( + "UnbondRecord: amount = {}, start_epoch {}", + &unbond_amount, &start + ); + if start > infraction_epoch { + continue; + } + let slashes_for_this_unbond = self + .validator_slashes + .get(&validator) + .cloned() + .unwrap_or_default() + .iter() + .filter(|&s| { + start <= s.epoch + && s.epoch + self.params.unbonding_len + < infraction_epoch + }) + .cloned() + .fold( + BTreeMap::::new(), + |mut acc, s| { + let cur = acc.entry(s.epoch).or_default(); + *cur += s.rate; + acc + }, + ); + println!( + "Slashes for this unbond: {:?}", + slashes_for_this_unbond + ); + total_unbonded += compute_amount_after_slashing( + &slashes_for_this_unbond, + unbond_amount, + self.params.unbonding_len, + ); + + println!( + "Total unbonded (epoch {}) w slashing = {}", + epoch, total_unbonded + ); + } + } + println!("Computing adjusted amounts now"); + + let mut last_slash = token::Change::default(); + for offset in 0..self.params.pipeline_len { + println!( + "Epoch {}\nLast slash = {}", + self.epoch + offset, + last_slash + ); + let unbond_records = self + .unbond_records + .get(&validator) + .unwrap() + .get(&(self.epoch + offset)) + .cloned() + .unwrap_or_default(); + for (start, unbond_amount) in unbond_records { + println!( + "UnbondRecord: amount = {}, start_epoch {}", + &unbond_amount, &start + ); + if start > infraction_epoch { + continue; + } + + let slashes_for_this_unbond = self + .validator_slashes + .get(&validator) + .cloned() + .unwrap_or_default() + .iter() + .filter(|&s| { + start <= s.epoch + && s.epoch + self.params.unbonding_len + < infraction_epoch + }) + .cloned() + .fold( + BTreeMap::::new(), + |mut acc, s| { + let cur = acc.entry(s.epoch).or_default(); + *cur += s.rate; + acc + }, + ); + println!( + "Slashes for this unbond: {:?}", + slashes_for_this_unbond + ); + + total_unbonded += compute_amount_after_slashing( + &slashes_for_this_unbond, + unbond_amount, + self.params.unbonding_len, + ); + println!( + "Total unbonded (offset {}) w slashing = {}", + offset, total_unbonded + ); + } + println!("stake at infraction {}", stake_at_infraction); + println!("total unbonded {}", total_unbonded); + let this_slash = decimal_mult_i128( + total_rate, + stake_at_infraction - total_unbonded.change(), + ); + let diff_slashed_amount = this_slash - last_slash; + println!( + "Offset {} diff_slashed_amount {}", + offset, diff_slashed_amount + ); + last_slash = this_slash; + // total_unbonded = token::Amount::default(); + + // Update the voting powers (consider that the stake is + // discrete) let validator_stake = self + // .validator_stakes + // .entry(self.epoch + offset) + // .or_default() + // .entry(validator.clone()) + // .or_default(); + // *validator_stake -= diff_slashed_amount; + + println!("Updating ABSTRACT voting powers"); + let sum_post_bonds = self.get_validator_bond_sums( + &validator, + infraction_epoch.next(), + self.epoch + offset, + ); + println!("\nUnslashable bonds = {}", sum_post_bonds); + let validator_stake_at_offset = self + .validator_stakes + .entry(self.epoch + offset) + .or_default() + .entry(validator.clone()) + .or_default(); + + let slashable_stake_at_offset = + *validator_stake_at_offset - sum_post_bonds.change(); + println!( + "Val stake pre (epoch {}) = {}", + self.epoch + offset, + validator_stake_at_offset + ); + println!( + "Slashable stake at offset = {}", + slashable_stake_at_offset + ); + let change = if slashable_stake_at_offset + - diff_slashed_amount + < 0i128 + { + slashable_stake_at_offset + } else { + diff_slashed_amount + }; + println!("Change = {}", change); + *validator_stake_at_offset -= change; + + for os in (offset + 1)..=self.params.pipeline_len { + println!("Adjust epoch {}", self.epoch + os); + let offset_stake = self + .validator_stakes + .entry(self.epoch + os) + .or_default() + .entry(validator.clone()) + .or_default(); + *offset_stake -= change; + // let mut new_stake = + // *validator_stake - diff_slashed_amount; + // if new_stake < 0_i128 { + // new_stake = 0_i128; + // } + + // *validator_stake = new_stake; + println!( + "New stake at epoch {} = {}", + self.epoch + os, + offset_stake + ); + } + } + } } } @@ -1084,34 +2454,84 @@ impl AbstractPosState { /// Check if the given address is of a known validator fn is_validator(&self, validator: &Address, epoch: Epoch) -> bool { - let is_in_consensus = self - .consensus_set + // let is_in_consensus = self + // .consensus_set + // .get(&epoch) + // .unwrap() + // .iter() + // .any(|(_stake, vals)| vals.iter().any(|val| val == validator)); + // if is_in_consensus { + // return true; + // } + // self.below_capacity_set + // .get(&epoch) + // .unwrap() + // .iter() + // .any(|(_stake, vals)| vals.iter().any(|val| val == validator)) + + self.validator_states .get(&epoch) .unwrap() - .iter() - .any(|(_stake, vals)| vals.iter().any(|val| val == validator)); - if is_in_consensus { - return true; + .keys() + .any(|val| val == validator) + } + + fn is_in_consensus_w_info( + &self, + validator: &Address, + epoch: Epoch, + ) -> Option<(usize, token::Amount)> { + for (stake, vals) in self.consensus_set.get(&epoch).unwrap() { + if let Some(index) = vals.iter().position(|val| val == validator) { + return Some((index, *stake)); + } } - self.below_capacity_set - .get(&epoch) - .unwrap() - .iter() - .any(|(_stake, vals)| vals.iter().any(|val| val == validator)) + None + } + + fn is_in_below_capacity_w_info( + &self, + validator: &Address, + epoch: Epoch, + ) -> Option<(usize, token::Amount)> { + for (stake, vals) in self.below_capacity_set.get(&epoch).unwrap() { + if let Some(index) = vals.iter().position(|val| val == validator) { + return Some((index, (*stake).into())); + } + } + None + } + + fn get_validator_bond_sums( + &self, + validator: &Address, + start_epoch: Epoch, + end_epoch: Epoch, + ) -> token::Amount { + let bonds = self.bonds.iter().filter_map(|(bond_id, bonds)| { + if bond_id.validator != validator.clone() { + return None; + } + let desired_bonds = bonds.iter().filter_map(|(start, amount)| { + if *start < start_epoch || *start > end_epoch { + return None; + } + Some(*amount) + }); + let sum: token::Change = desired_bonds.sum(); + Some(token::Amount::from_change(sum)) + }); + bonds.sum() } /// Find the sums of the bonds across all epochs - fn bond_sums(&self) -> HashMap { + fn bond_sums(&self) -> BTreeMap { self.bonds.iter().fold( - HashMap::::new(), - |mut acc, (_epoch, bonds)| { - for (id, delta) in bonds { + BTreeMap::::new(), + |mut acc, (id, bonds)| { + for delta in bonds.values() { let entry = acc.entry(id.clone()).or_default(); - *entry += delta; - // Remove entries that are fully unbonded - if *entry == 0 { - acc.remove(id); - } + *entry += *delta; } acc }, @@ -1119,9 +2539,9 @@ impl AbstractPosState { } /// Find the sums of withdrawable unbonds - fn withdrawable_unbonds(&self) -> HashMap { + fn withdrawable_unbonds(&self) -> BTreeMap { self.unbonds.iter().fold( - HashMap::::new(), + BTreeMap::::new(), |mut acc, (epoch, unbonds)| { if *epoch <= self.epoch { for (id, amount) in unbonds { @@ -1134,6 +2554,175 @@ impl AbstractPosState { }, ) } + + /// Compute the cubic slashing rate for the current epoch + fn cubic_slash_rate(&self) -> Decimal { + println!("Computing ABSTRACT slash rate"); + let infraction_epoch = self.epoch + - self.params.unbonding_len + - 1_u64 + - self.params.cubic_slashing_window_length; + println!("Infraction epoch: {}", infraction_epoch); + let window_width = self.params.cubic_slashing_window_length; + let epoch_start = Epoch::from( + infraction_epoch + .0 + .checked_sub(window_width) + .unwrap_or_default(), + ); + let epoch_end = infraction_epoch + window_width; + + // Calculate cubic slashing rate with the abstract state + let mut vp_frac_sum = Decimal::default(); + for epoch in Epoch::iter_bounds_inclusive(epoch_start, epoch_end) { + let consensus_stake = + self.consensus_set.get(&epoch).unwrap().iter().fold( + token::Amount::default(), + |sum, (val_stake, validators)| { + sum + *val_stake * validators.len() as u64 + }, + ); + println!("Consensus stake in epoch {}: {}", epoch, consensus_stake); + + let processing_epoch = epoch + + self.params.unbonding_len + + 1_u64 + + self.params.cubic_slashing_window_length; + let enqueued_slashes = self.enqueued_slashes.get(&processing_epoch); + if let Some(enqueued_slashes) = enqueued_slashes { + for (validator, slashes) in enqueued_slashes.iter() { + let val_stake = token::Amount::from_change( + self.validator_stakes + .get(&epoch) + .unwrap() + .get(validator) + .cloned() + .unwrap_or_default(), + ); + println!( + "Val {} stake epoch {}: {}", + &validator, epoch, val_stake + ); + vp_frac_sum += Decimal::from(slashes.len()) + * Decimal::from(val_stake) + / Decimal::from(consensus_stake); + } + } + } + let vp_frac_sum = cmp::min(Decimal::ONE, vp_frac_sum); + println!("vp_frac_sum: {}", vp_frac_sum); + + cmp::min(dec!(9) * vp_frac_sum * vp_frac_sum, Decimal::ONE) + } + + fn debug_validators(&self) { + println!("DEBUG ABSTRACT VALIDATOR"); + let current_epoch = self.epoch; + for epoch in + Epoch::iter_bounds_inclusive(current_epoch, self.pipeline()) + { + println!("Epoch {}", epoch); + let mut min_consensus = token::Amount::from(u64::MAX); + let consensus = self.consensus_set.get(&epoch).unwrap(); + for (amount, vals) in consensus { + if *amount < min_consensus { + min_consensus = *amount; + } + for val in vals { + let deltas_stake = self + .validator_stakes + .get(&epoch) + .unwrap() + .get(val) + .unwrap(); + let val_state = self + .validator_states + .get(&epoch) + .unwrap() + .get(val) + .unwrap(); + println!( + "Consensus val {}, stake {} ({}) - ({:?})", + val, + u64::from(*amount), + deltas_stake, + val_state + ); + debug_assert_eq!( + *amount, + token::Amount::from_change(*deltas_stake) + ); + debug_assert_eq!(*val_state, ValidatorState::Consensus); + } + } + let mut max_bc = token::Amount::default(); + let bc = self.below_capacity_set.get(&epoch).unwrap(); + for (amount, vals) in bc { + if token::Amount::from(*amount) > max_bc { + max_bc = token::Amount::from(*amount); + } + for val in vals { + let deltas_stake = self + .validator_stakes + .get(&epoch) + .unwrap() + .get(val) + .cloned() + .unwrap_or_default(); + let val_state = self + .validator_states + .get(&epoch) + .unwrap() + .get(val) + .unwrap(); + println!( + "Below-cap val {}, stake {} ({}) - ({:?})", + val, + u64::from(token::Amount::from(*amount)), + deltas_stake, + val_state + ); + debug_assert_eq!( + token::Amount::from(*amount), + token::Amount::from_change(deltas_stake) + ); + debug_assert_eq!(*val_state, ValidatorState::BelowCapacity); + } + } + assert!(min_consensus >= max_bc); + + for addr in self + .validator_states + .get(&epoch) + .unwrap() + .keys() + .cloned() + .collect::>() + { + if let (None, None) = ( + self.is_in_consensus_w_info(&addr, epoch), + self.is_in_below_capacity_w_info(&addr, epoch), + ) { + assert_eq!( + self.validator_states + .get(&epoch) + .unwrap() + .get(&addr) + .cloned(), + Some(ValidatorState::Jailed) + ); + let stake = self + .validator_stakes + .get(&epoch) + .unwrap() + .get(&addr) + .cloned() + .unwrap_or_default(); + println!("Jailed val {}, stake {}", &addr, &stake); + } + } + } + } } /// Arbitrary bond transition that adds tokens to an existing bond @@ -1142,11 +2731,9 @@ fn add_arb_bond_amount( ) -> impl Strategy { let bond_ids = state .bonds - .iter() - .flat_map(|(_epoch, bonds)| { - bonds.keys().cloned().collect::>() - }) - .collect::>() + .keys() + .cloned() + .collect::>() .into_iter() .collect::>(); let arb_bond_id = prop::sample::select(bond_ids); @@ -1158,17 +2745,14 @@ fn add_arb_bond_amount( fn arb_delegation( state: &AbstractPosState, ) -> impl Strategy { - let validators = state.consensus_set.iter().fold( - HashSet::new(), - |mut acc, (_epoch, vals)| { - for vals in vals.values() { - for validator in vals { - acc.insert(validator.clone()); - } - } - acc - }, - ); + // Bond is allowed to any validator in any set - including jailed validators + let validators = state + .validator_states + .get(&state.pipeline()) + .unwrap() + .keys() + .cloned() + .collect::>(); let validator_vec = validators.clone().into_iter().collect::>(); let arb_source = address::testing::arb_non_internal_address() .prop_filter("Must be a non-validator address", move |addr| { @@ -1183,7 +2767,102 @@ fn arb_delegation( ) } +/// Arbitrary validator self-bond +fn arb_self_bond( + state: &AbstractPosState, +) -> impl Strategy { + // Bond is allowed to any validator in any set - including jailed validators + let validator_vec = state + .validator_states + .get(&state.pipeline()) + .unwrap() + .keys() + .cloned() + .collect::>(); + let arb_validator = prop::sample::select(validator_vec); + (arb_validator, arb_bond_amount()).prop_map(|(validator, amount)| { + Transition::Bond { + id: BondId { + source: validator.clone(), + validator, + }, + amount, + } + }) +} + // Bond up to 10 tokens (10M micro units) to avoid overflows pub fn arb_bond_amount() -> impl Strategy { - (1_u64..10).prop_map(token::Amount::from) + (1_u64..10_000_000).prop_map(token::Amount::from) +} + +/// Arbitrary validator misbehavior +fn arb_slash(state: &AbstractPosState) -> impl Strategy { + let validators = state.consensus_set.iter().fold( + Vec::new(), + |mut acc, (_epoch, vals)| { + for vals in vals.values() { + for validator in vals { + acc.push(validator.clone()); + } + } + acc + }, + ); + let current_epoch = state.epoch.0; + + let arb_validator = prop::sample::select(validators); + let slash_types = + vec![SlashType::LightClientAttack, SlashType::DuplicateVote]; + let arb_type = prop::sample::select(slash_types); + let arb_epoch = (current_epoch + .checked_sub(state.params.unbonding_len) + .unwrap_or_default()..=current_epoch) + .prop_map(Epoch::from); + (arb_validator, arb_type, arb_epoch).prop_map( + |(validator, slash_type, infraction_epoch)| Transition::Misbehavior { + address: validator, + slash_type, + infraction_epoch, + height: 0, + }, + ) +} + +fn compute_amount_after_slashing( + slashes: &BTreeMap, + amount: token::Amount, + unbonding_len: u64, +) -> token::Amount { + let mut computed_amounts = Vec::::new(); + let mut updated_amount = amount; + + for (infraction_epoch, slash_rate) in slashes { + let mut indices_to_remove = BTreeSet::::new(); + + for (idx, slashed_amount) in computed_amounts.iter().enumerate() { + if slashed_amount.epoch + unbonding_len < *infraction_epoch { + updated_amount = updated_amount + .checked_sub(slashed_amount.amount) + .unwrap_or_default(); + indices_to_remove.insert(idx); + } + } + for idx in indices_to_remove.into_iter().rev() { + computed_amounts.remove(idx); + } + computed_amounts.push(SlashedAmount { + amount: decimal_mult_amount(*slash_rate, updated_amount), + epoch: *infraction_epoch, + }); + } + updated_amount + .checked_sub( + computed_amounts + .iter() + .fold(token::Amount::default(), |sum, computed| { + sum + computed.amount + }), + ) + .unwrap_or_default() } From 5d007bee4a18dde8753c06940d5bd8be8a9c39de Mon Sep 17 00:00:00 2001 From: brentstone Date: Thu, 11 May 2023 13:14:10 -0400 Subject: [PATCH 06/31] fix `bond_amount` --- proof_of_stake/src/lib.rs | 55 +++++++++---------- .../src/ledger/native_vp/governance/utils.rs | 3 +- shared/src/ledger/queries/vp/pos.rs | 3 +- 3 files changed, 29 insertions(+), 32 deletions(-) diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 6cab766aa9..41e550a14e 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -2093,53 +2093,52 @@ where handle.contains(storage, consensus_key) } -/// Get the total bond amount, including slashes, for a given bond ID and epoch +/// Get the total bond amount, including slashes, for a given bond ID and epoch. +/// Returns a two-element tuple of the raw bond amount and the post-slashed bond +/// amount, respectively. +/// +/// TODO: does epoch of discovery need to be considered for precise accuracy? pub fn bond_amount( storage: &S, - params: &PosParams, bond_id: &BondId, epoch: Epoch, ) -> storage_api::Result<(token::Amount, token::Amount)> where S: StorageRead, { - // TODO: review this logic carefully, do cubic slashing, apply rewards + // TODO: review this logic carefully, apply rewards let slashes = find_validator_slashes(storage, &bond_id.validator)?; + let slash_rates = slashes.into_iter().fold( + BTreeMap::::new(), + |mut map, slash| { + let tot_rate = map.entry(slash.epoch).or_default(); + *tot_rate = cmp::min(Decimal::ONE, *tot_rate + slash.rate); + map + }, + ); + let bonds = bond_handle(&bond_id.source, &bond_id.validator).get_data_handler(); let mut total = token::Amount::default(); let mut total_active = token::Amount::default(); for next in bonds.iter(storage)? { let (bond_epoch, delta) = next?; - // if bond_epoch > epoch { - // break; - // } + if bond_epoch > epoch { + continue; + } - // TODO: do we need to consider the adjusted amounts of previous bonds - // and their slashes when iterating? - for slash in &slashes { - let Slash { - epoch: slash_epoch, - block_height: _, - r#type: slash_type, - rate: _, - } = slash; + total += token::Amount::from_change(delta); + total_active += token::Amount::from_change(delta); + + for (slash_epoch, rate) in &slash_rates { if *slash_epoch < bond_epoch { continue; } - // TODO: consider edge cases with the cubic slashing window - let cubic_rate = get_final_cubic_slash_rate( - storage, - params, - *slash_epoch, - *slash_type, - )?; - let current_slashed = decimal_mult_i128(cubic_rate, delta); - let delta = token::Amount::from_change(delta - current_slashed); - total += delta; - if bond_epoch <= epoch { - total_active += delta; - } + // TODO: think about truncation + let current_slashed = decimal_mult_i128(*rate, delta); + total_active + .checked_sub(token::Amount::from_change(current_slashed)) + .unwrap_or_default(); } } Ok((total, total_active)) diff --git a/shared/src/ledger/native_vp/governance/utils.rs b/shared/src/ledger/native_vp/governance/utils.rs index 2511db46c9..fe87319ff2 100644 --- a/shared/src/ledger/native_vp/governance/utils.rs +++ b/shared/src/ledger/native_vp/governance/utils.rs @@ -418,8 +418,7 @@ where validator: validator.clone(), }; let amount = - bond_amount(storage, ¶ms, &bond_id, epoch)? - .1; + bond_amount(storage, &bond_id, epoch)?.1; if amount != token::Amount::default() { let entry = delegators diff --git a/shared/src/ledger/queries/vp/pos.rs b/shared/src/ledger/queries/vp/pos.rs index 8836dba0a9..5988ea5bbd 100644 --- a/shared/src/ledger/queries/vp/pos.rs +++ b/shared/src/ledger/queries/vp/pos.rs @@ -305,10 +305,9 @@ where H: 'static + StorageHasher + Sync, { let epoch = epoch.unwrap_or(ctx.wl_storage.storage.last_epoch); - let params = read_pos_params(ctx.wl_storage)?; let bond_id = BondId { source, validator }; - bond_amount(ctx.wl_storage, ¶ms, &bond_id, epoch) + bond_amount(ctx.wl_storage, &bond_id, epoch) } fn unbond( From 27290f112fcce00580fef61f556734172ad8be7a Mon Sep 17 00:00:00 2001 From: brentstone Date: Thu, 11 May 2023 13:56:01 -0400 Subject: [PATCH 07/31] fix PoS client query related functions --- proof_of_stake/src/lib.rs | 55 ++++++++++++--------------------------- 1 file changed, 17 insertions(+), 38 deletions(-) diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 41e550a14e..ab5ea0044b 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -2583,8 +2583,6 @@ where let validator = bond_id.validator.clone(); let (bonds, _unbonds) = bonds_and_unbonds.entry(bond_id).or_default(); bonds.push(make_bond_details( - storage, - params, &validator, change, start, @@ -2605,7 +2603,6 @@ where let validator = bond_id.validator.clone(); let (_bonds, unbonds) = bonds_and_unbonds.entry(bond_id).or_default(); unbonds.push(make_unbond_details( - storage, params, &validator, amount, @@ -2649,8 +2646,6 @@ where .filter(|(_start, change)| *change > token::Change::default()) .map(|(start, change)| { make_bond_details( - storage, - params, &validator, change, start, @@ -2664,7 +2659,6 @@ where .into_iter() .map(|(epoch_range, change)| { make_unbond_details( - storage, params, &validator, change, @@ -2684,32 +2678,20 @@ where Ok(HashMap::from_iter([(bond_id, details)])) } -// TODO: update for cubic slashing -fn make_bond_details( - storage: &S, - params: &PosParams, +// TODO: check carefully for validity +fn make_bond_details( validator: &Address, change: token::Change, start: Epoch, slashes: &[Slash], applied_slashes: &mut HashMap>, -) -> BondDetails -where - S: StorageRead, -{ +) -> BondDetails { let amount = token::Amount::from_change(change); let slashed_amount = slashes .iter() .fold(None, |acc: Option, slash| { if slash.epoch >= start { - let slash_rate = get_final_cubic_slash_rate( - storage, - params, - slash.epoch, - slash.r#type, - ) - .unwrap(); let validator_slashes = applied_slashes.entry(validator.clone()).or_default(); if !validator_slashes.contains(slash) { @@ -2717,11 +2699,13 @@ where } return Some( acc.unwrap_or_default() - + mult_change_to_amount(slash_rate, change), + + mult_change_to_amount(slash.rate, change), ); } None }); + let slashed_amount = + slashed_amount.map(|slashed| cmp::min(amount, slashed)); BondDetails { start, amount, @@ -2729,19 +2713,16 @@ where } } -// TODO: update for cubic slashing -fn make_unbond_details( - storage: &S, +// TODO: check carefully for validity +fn make_unbond_details( params: &PosParams, validator: &Address, amount: token::Amount, (start, withdraw): (Epoch, Epoch), slashes: &[Slash], applied_slashes: &mut HashMap>, -) -> UnbondDetails -where - S: StorageRead, -{ +) -> UnbondDetails { + // TODO: checks bounds for considering valid unbond with slash! let slashed_amount = slashes .iter() @@ -2749,16 +2730,12 @@ where if slash.epoch >= start && slash.epoch < withdraw - .checked_sub(Epoch(params.unbonding_len)) + .checked_sub(Epoch( + params.unbonding_len + + params.cubic_slashing_window_length, + )) .unwrap_or_default() { - let slash_rate = get_final_cubic_slash_rate( - storage, - params, - slash.epoch, - slash.r#type, - ) - .unwrap(); let validator_slashes = applied_slashes.entry(validator.clone()).or_default(); if !validator_slashes.contains(slash) { @@ -2766,11 +2743,13 @@ where } return Some( acc.unwrap_or_default() - + decimal_mult_amount(slash_rate, amount), + + decimal_mult_amount(slash.rate, amount), ); } None }); + let slashed_amount = + slashed_amount.map(|slashed| cmp::min(amount, slashed)); UnbondDetails { start, withdraw, From 3dde190fedfe5f9f5a72600c6a2a03192093a85d Mon Sep 17 00:00:00 2001 From: brentstone Date: Mon, 15 May 2023 23:40:17 -0400 Subject: [PATCH 08/31] pos/lib.rs: WIP fix things inside of `bonds_and_unbonds` --- .../lib/node/ledger/shell/finalize_block.rs | 116 +++++++++++++++++- proof_of_stake/src/lib.rs | 36 ++++-- proof_of_stake/src/types.rs | 4 +- 3 files changed, 143 insertions(+), 13 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index fe8f1789dd..c83315b958 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -899,7 +899,8 @@ mod test_finalize_block { is_validator_slashes_key, slashes_prefix, }; use namada::proof_of_stake::types::{ - decimal_mult_amount, SlashType, ValidatorState, WeightedValidator, + decimal_mult_amount, BondId, SlashType, ValidatorState, + WeightedValidator, }; use namada::proof_of_stake::{ enqueued_slashes_handle, get_num_consensus_validators, @@ -2770,6 +2771,119 @@ mod test_finalize_block { let current_epoch = shell.wl_storage.storage.block.epoch; assert_eq!(current_epoch.0, 12_u64); + println!("\nCHECK BOND AND UNBOND DETAILS"); + let details = namada_proof_of_stake::bonds_and_unbonds( + &shell.wl_storage, + None, + None, + ) + .unwrap(); + + let del_id = BondId { + source: delegator.clone(), + validator: val1.address.clone(), + }; + let self_id = BondId { + source: val1.address.clone(), + validator: val1.address.clone(), + }; + + let del_details = details.get(&del_id).unwrap(); + let self_details = details.get(&self_id).unwrap(); + dbg!(del_details, self_details); + + // Check slashes + assert_eq!(del_details.slashes, self_details.slashes); + assert_eq!(del_details.slashes.len(), 3); + assert_eq!(del_details.slashes[0].epoch, Epoch(3)); + assert!(equal_enough(del_details.slashes[0].rate, cubic_rate)); + assert_eq!(del_details.slashes[1].epoch, Epoch(3)); + assert!(equal_enough(del_details.slashes[1].rate, cubic_rate)); + assert_eq!(del_details.slashes[2].epoch, Epoch(4)); + assert!(equal_enough(del_details.slashes[2].rate, cubic_rate)); + + // Check delegations + assert_eq!(del_details.bonds.len(), 2); + assert_eq!(del_details.bonds[0].start, Epoch(3)); + assert_eq!( + del_details.bonds[0].amount, + del_1_amount - del_unbond_1_amount + ); + // TODO: decimal mult issues should be resolved with PR 1282 + assert!( + (del_details.bonds[0].slashed_amount.unwrap().change() + - decimal_mult_amount( + std::cmp::min(Decimal::ONE, dec!(3) * cubic_rate), + del_1_amount - del_unbond_1_amount + ) + .change()) + .abs() + <= 2 + ); + assert_eq!(del_details.bonds[1].start, Epoch(7)); + assert_eq!(del_details.bonds[1].amount, del_2_amount); + assert_eq!(del_details.bonds[1].slashed_amount, None); + + // Check self-bonds + assert_eq!(self_details.bonds.len(), 1); + assert_eq!(self_details.bonds[0].start, Epoch(0)); + assert_eq!( + self_details.bonds[0].amount, + initial_stake - self_unbond_1_amount + self_bond_1_amount + - self_unbond_2_amount + ); + // TODO: not sure why this is correct??? (with + self_bond_1_amount - + // self_unbond_2_amount) + // TODO: Make sure this is sound and what we expect + assert_eq!( + self_details.bonds[0].slashed_amount, + Some(decimal_mult_amount( + std::cmp::min(Decimal::ONE, dec!(3) * cubic_rate), + initial_stake - self_unbond_1_amount + self_bond_1_amount + - self_unbond_2_amount + )) + ); + + // Check delegation unbonds + assert_eq!(del_details.unbonds.len(), 1); + assert_eq!(del_details.unbonds[0].start, Epoch(3)); + assert_eq!(del_details.unbonds[0].withdraw, Epoch(9)); + assert_eq!(del_details.unbonds[0].amount, del_unbond_1_amount); + assert!( + (del_details.unbonds[0].slashed_amount.unwrap().change() + - decimal_mult_amount( + std::cmp::min(Decimal::ONE, dec!(2) * cubic_rate), + del_unbond_1_amount + ) + .change()) + .abs() + <= 1 + ); + + // Check self-unbonds + assert_eq!(self_details.unbonds.len(), 3); + assert_eq!(self_details.unbonds[0].start, Epoch(0)); + assert_eq!(self_details.unbonds[0].withdraw, Epoch(8)); + assert_eq!(self_details.unbonds[1].start, Epoch(0)); + assert_eq!(self_details.unbonds[1].withdraw, Epoch(11)); + assert_eq!(self_details.unbonds[2].start, Epoch(5)); + assert_eq!(self_details.unbonds[2].withdraw, Epoch(11)); + assert_eq!(self_details.unbonds[0].amount, self_unbond_1_amount); + assert_eq!(self_details.unbonds[0].slashed_amount, None); + assert_eq!( + self_details.unbonds[1].amount, + self_unbond_2_amount - self_bond_1_amount + ); + assert_eq!( + self_details.unbonds[1].slashed_amount, + Some(decimal_mult_amount( + std::cmp::min(Decimal::ONE, dec!(3) * cubic_rate), + self_unbond_2_amount - self_bond_1_amount + )) + ); + assert_eq!(self_details.unbonds[2].amount, self_bond_1_amount); + assert_eq!(self_details.unbonds[2].slashed_amount, None); + println!("\nWITHDRAWING DELEGATION UNBOND"); let slash_pool_balance_pre_withdraw = slash_pool_balance; // Withdraw the delegation unbonds, which total to 18_000. This should diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index ab5ea0044b..7559fc45f5 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -2494,7 +2494,7 @@ where ); let mut slashes_cache = HashMap::>::new(); // Applied slashes grouped by validator address - let mut applied_slashes = HashMap::>::new(); + let mut applied_slashes = HashMap::>::new(); // TODO: if validator is `Some`, look-up all its bond owners (including // self-bond, if any) first @@ -2639,7 +2639,7 @@ where S: StorageRead, { let slashes = find_validator_slashes(storage, &validator)?; - let mut applied_slashes = HashMap::>::new(); + let mut applied_slashes = HashMap::>::new(); let bonds = find_bonds(storage, &source, &validator)? .into_iter() @@ -2684,8 +2684,13 @@ fn make_bond_details( change: token::Change, start: Epoch, slashes: &[Slash], - applied_slashes: &mut HashMap>, + applied_slashes: &mut HashMap>, ) -> BondDetails { + let prev_applied_slashes = applied_slashes + .clone() + .get(validator) + .cloned() + .unwrap_or_default(); let amount = token::Amount::from_change(change); let slashed_amount = slashes @@ -2694,15 +2699,18 @@ fn make_bond_details( if slash.epoch >= start { let validator_slashes = applied_slashes.entry(validator.clone()).or_default(); - if !validator_slashes.contains(slash) { - validator_slashes.insert(slash.clone()); + if !prev_applied_slashes + .iter() + .any(|s| s.clone() == slash.clone()) + { + validator_slashes.push(slash.clone()); } return Some( acc.unwrap_or_default() + mult_change_to_amount(slash.rate, change), ); } - None + acc }); let slashed_amount = slashed_amount.map(|slashed| cmp::min(amount, slashed)); @@ -2720,8 +2728,13 @@ fn make_unbond_details( amount: token::Amount, (start, withdraw): (Epoch, Epoch), slashes: &[Slash], - applied_slashes: &mut HashMap>, + applied_slashes: &mut HashMap>, ) -> UnbondDetails { + let prev_applied_slashes = applied_slashes + .clone() + .get(validator) + .cloned() + .unwrap_or_default(); // TODO: checks bounds for considering valid unbond with slash! let slashed_amount = slashes @@ -2738,15 +2751,18 @@ fn make_unbond_details( { let validator_slashes = applied_slashes.entry(validator.clone()).or_default(); - if !validator_slashes.contains(slash) { - validator_slashes.insert(slash.clone()); + if !prev_applied_slashes + .iter() + .any(|s| s.clone() == slash.clone()) + { + validator_slashes.push(slash.clone()); } return Some( acc.unwrap_or_default() + decimal_mult_amount(slash.rate, amount), ); } - None + acc }); let slashed_amount = slashed_amount.map(|slashed| cmp::min(amount, slashed)); diff --git a/proof_of_stake/src/types.rs b/proof_of_stake/src/types.rs index 9233b280af..bf81ad2286 100644 --- a/proof_of_stake/src/types.rs +++ b/proof_of_stake/src/types.rs @@ -3,7 +3,7 @@ mod rev_order; use core::fmt::Debug; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::convert::TryFrom; use std::fmt::Display; use std::hash::Hash; @@ -442,7 +442,7 @@ pub struct BondsAndUnbondsDetail { /// Unbonds pub unbonds: Vec, /// Slashes applied to any of the bonds and/or unbonds - pub slashes: HashSet, + pub slashes: Vec, } /// Bond with all its details From 7dcd4b06f5b67933e120cc0fc7e1bedb7e6aadee Mon Sep 17 00:00:00 2001 From: brentstone Date: Tue, 23 May 2023 21:26:50 +0200 Subject: [PATCH 09/31] store total bond sums of each validator for efficient computation --- proof_of_stake/src/lib.rs | 122 ++++++++++++++++++++++------------ proof_of_stake/src/storage.rs | 20 ++++++ 2 files changed, 100 insertions(+), 42 deletions(-) diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 7559fc45f5..e389b536da 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -292,8 +292,6 @@ pub fn validator_commission_rate_handle( } /// Get the storage handle to a bond -/// TODO: remove `get_remaining` and the unused storage (maybe just call it -/// `storage::bond_key`) pub fn bond_handle(source: &Address, validator: &Address) -> Bonds { let bond_id = BondId { source: source.clone(), @@ -303,6 +301,12 @@ pub fn bond_handle(source: &Address, validator: &Address) -> Bonds { Bonds::open(key) } +/// Get the storage handle to a validator's global bonds +pub fn global_bond_handle(validator: &Address) -> Bonds { + let key = storage::global_bonds_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 { @@ -433,6 +437,11 @@ where delta, current_epoch, )?; + global_bond_handle(&address).init_at_genesis( + storage, + delta, + current_epoch, + )?; validator_commission_rate_handle(&address).init_at_genesis( storage, commission_rate, @@ -876,6 +885,7 @@ where let source = source.unwrap_or(validator); let bond_handle = bond_handle(source, validator); + let global_bond_handle = global_bond_handle(validator); // Check that validator is not inactive at anywhere between the current // epoch and pipeline offset @@ -903,6 +913,15 @@ where .get_delta_val(storage, current_epoch + offset, ¶ms)? .unwrap_or_default(); bond_handle.set(storage, cur_remain + amount, current_epoch, offset)?; + let cur_remain_global = global_bond_handle + .get_delta_val(storage, current_epoch + offset, ¶ms)? + .unwrap_or_default(); + global_bond_handle.set( + storage, + cur_remain_global + amount, + current_epoch, + offset, + )?; println!("\nBonds after incrementing:"); for ep in Epoch::default().iter_range(current_epoch.0 + 3) { @@ -1487,6 +1506,15 @@ where 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, + new_global_bond_value: token::Change, + unbond_value: token::Change, +} + /// Unbond tokens that are bonded between a validator and a source (self or /// delegator) pub fn unbond_tokens( @@ -1543,6 +1571,7 @@ where let source = source.unwrap_or(validator); let bonds_handle = bond_handle(source, validator); + let global_bonds_handle = global_bond_handle(validator); println!("\nBonds before decrementing:"); for ep in Epoch::default().iter_range(current_epoch.0 + 3) { @@ -1573,7 +1602,7 @@ where + params.unbonding_len + params.cubic_slashing_window_length; - let mut remaining = token::Amount::from_change(amount); + let mut remaining = amount; let mut amount_after_slashing = token::Change::default(); // Iterate thru bonds, find non-zero delta entries starting from @@ -1585,25 +1614,29 @@ where bonds_handle.get_data_handler().iter(storage)?.collect(); let mut bond_iter = bonds.into_iter().rev(); + let mut new_bond_values = HashSet::::new(); - // Map: { bond start epoch, (new bond value, unbond value) } - let mut new_bond_values_map = - HashMap::::new(); - - while remaining > token::Amount::default() { + while remaining > token::Change::default() { let bond = bond_iter.next().transpose()?; if bond.is_none() { continue; } - let (bond_epoch, bond_amnt) = bond.unwrap(); - println!("\nBond (epoch, amnt) = ({}, {})", bond_epoch, bond_amnt); + let (bond_epoch, bond_amount) = bond.unwrap(); + println!("\nBond (epoch, amnt) = ({}, {})", bond_epoch, bond_amount); println!("remaining = {}", remaining); - let bond_amount = token::Amount::from_change(bond_amnt); + let global_bond_amount = + global_bonds_handle.get_delta_val(storage, bond_epoch, ¶ms)?; + debug_assert!(global_bond_amount.is_some()); + let global_bond_amount = global_bond_amount.unwrap_or_default(); let to_unbond = cmp::min(bond_amount, remaining); - let new_bond_amount = bond_amount - to_unbond; - new_bond_values_map.insert(bond_epoch, (new_bond_amount, to_unbond)); + new_bond_values.insert(BondAndUnbondUpdates { + bond_start: bond_epoch, + new_bond_value: bond_amount - to_unbond, + new_global_bond_value: global_bond_amount - to_unbond, + unbond_value: to_unbond, + }); println!("to_unbond (init) = {}", to_unbond); let mut slashes_for_this_bond = BTreeMap::::new(); @@ -1620,8 +1653,11 @@ where } } - amount_after_slashing += - get_slashed_amount(¶ms, to_unbond, &slashes_for_this_bond)?; + amount_after_slashing += get_slashed_amount( + ¶ms, + token::Amount::from_change(to_unbond), + &slashes_for_this_bond, + )?; println!("Cur amnt after slashing = {}", &amount_after_slashing); // Update the unbond records @@ -1631,23 +1667,37 @@ where .unwrap_or_default(); unbond_records_handle(validator) .at(&pipeline_epoch) - .insert(storage, bond_epoch, cur_amnt + to_unbond)?; + .insert( + storage, + bond_epoch, + cur_amnt + token::Amount::from_change(to_unbond), + )?; remaining -= to_unbond; } drop(bond_iter); // Write the in-memory bond and unbond values back to storage - for (bond_epoch, (new_bond_amount, unbond_amount)) in - new_bond_values_map.into_iter() + for BondAndUnbondUpdates { + bond_start, + new_bond_value, + new_global_bond_value, + unbond_value, + } in new_bond_values.into_iter() { - bonds_handle.set(storage, new_bond_amount.into(), bond_epoch, 0)?; + bonds_handle.set(storage, new_bond_value, bond_start, 0)?; + global_bonds_handle.set( + storage, + new_global_bond_value, + bond_start, + 0, + )?; update_unbond( &unbonds, storage, &withdrawable_epoch, - &bond_epoch, - unbond_amount, + &bond_start, + token::Amount::from_change(unbond_value), )?; } @@ -3678,29 +3728,17 @@ fn get_validator_bond_sums( where S: StorageRead, { - let prefix = bonds_prefix(); - // We have to iterate raw bytes, cause the epoched data `last_update` field - // gets matched here too - let 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 start < start_epoch || start > end_epoch { - return None; - } - if validator.clone() != bond_id.validator { - return None; - } - - let change: token::Change = - BorshDeserialize::try_from_slice(&val_bytes).ok()?; - println!("Bond start, amnt = ({}, {})", start, change); - return Some(change); + let bond_iter = global_bond_handle(validator) + .get_data_handler() + .iter(storage)? + .filter_map(|bond| { + if let Ok((epoch, delta)) = bond { + if epoch < start_epoch || epoch > end_epoch { + return None; } + return Some(token::Amount::from_change(delta)); } None }); - Ok(raw_bonds.fold(token::Amount::default(), |acc, delta| { - acc + token::Amount::from_change(delta) - })) + Ok(bond_iter.sum::()) } diff --git a/proof_of_stake/src/storage.rs b/proof_of_stake/src/storage.rs index 5b17bcdcd1..ff6068e151 100644 --- a/proof_of_stake/src/storage.rs +++ b/proof_of_stake/src/storage.rs @@ -29,6 +29,8 @@ 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 GLOBAL_BOND_STORAGE_KEY: &str = "global_bonds"; +const GLOBAL_UNBOND_STORAGE_KEY: &str = "global_unbonds"; 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"; @@ -426,6 +428,15 @@ pub fn is_bond_key(key: &Key) -> Option<(BondId, Epoch)> { } } +/// Storage key for global bonds for a given validator. +pub fn global_bonds_key(validator: &Address) -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&GLOBAL_BOND_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()) @@ -484,6 +495,15 @@ pub fn is_unbond_key(key: &Key) -> Option<(BondId, Epoch, Epoch)> { } } +/// Storage key for global unbonds for a given validator. +pub fn global_unbonds_key(validator: &Address) -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&GLOBAL_UNBOND_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") + .push(&validator.to_db_key()) + .expect("Cannot obtain a storage key") +} + /// Storage key for validator's total-unbonded amount to track for slashing pub fn validator_total_unbonded_key(validator: &Address) -> Key { validator_prefix(validator) From 3591284fb397ace49f8ce1786aeb44dab7b40fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 23 May 2023 21:48:19 +0200 Subject: [PATCH 10/31] refactor epoch offsets with params methods --- proof_of_stake/src/lib.rs | 53 ++++++++++---------------------- proof_of_stake/src/parameters.rs | 24 +++++++++++++++ 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index e389b536da..1c42635dc3 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -1597,10 +1597,7 @@ where let unbonds = unbond_handle(source, validator); // TODO: think if this should be +1 or not!!! - let withdrawable_epoch = current_epoch - + params.pipeline_len - + params.unbonding_len - + params.cubic_slashing_window_length; + let withdrawable_epoch = current_epoch + params.withdrawable_epoch_offset(); let mut remaining = amount; let mut amount_after_slashing = token::Change::default(); @@ -1950,9 +1947,8 @@ where let slash = slash?; if start_epoch <= slash.epoch && slash.epoch - < withdraw_epoch - - params.unbonding_len - - params.cubic_slashing_window_length + < withdraw_epoch + params.pipeline_len + - params.withdrawable_epoch_offset() { println!( "Slash (epoch, rate) = ({}, {})", @@ -2792,10 +2788,9 @@ fn make_unbond_details( .fold(None, |acc: Option, slash| { if slash.epoch >= start && slash.epoch - < withdraw + < (withdraw + params.pipeline_len) .checked_sub(Epoch( - params.unbonding_len - + params.cubic_slashing_window_length, + params.withdrawable_epoch_offset(), )) .unwrap_or_default() { @@ -2983,19 +2978,15 @@ where { println!("COMPUTING CUBIC SLASH RATE"); let mut sum_vp_fraction = Decimal::ZERO; - let start_epoch = infraction_epoch - .sub_or_default(Epoch(params.cubic_slashing_window_length)); - let end_epoch = infraction_epoch + params.cubic_slashing_window_length; + 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 = Decimal::from(get_total_consensus_stake(storage, epoch)?); println!("Consensus stake in epoch {}: {}", epoch, consensus_stake); - let processing_epoch = epoch - + params.unbonding_len - + 1_u64 - + params.cubic_slashing_window_length; + let processing_epoch = epoch + params.slash_processing_epoch_offset(); let slashes = enqueued_slashes_handle().at(&processing_epoch); let infracting_stake = slashes .iter(storage)? @@ -3079,10 +3070,8 @@ where rate: Decimal::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.unbonding_len - + params.cubic_slashing_window_length - + 1_u64; + let processing_epoch = + evidence_epoch + params.slash_processing_epoch_offset(); let pipeline_epoch = current_epoch + params.pipeline_len; // Add the slash to the list of enqueued slashes to be processed at a later @@ -3264,15 +3253,11 @@ where { let params = read_pos_params(storage)?; - if current_epoch.0 - < params.unbonding_len + 1 + params.cubic_slashing_window_length - { + if current_epoch.0 < params.slash_processing_epoch_offset() { return Ok(()); } - let infraction_epoch = current_epoch - - params.unbonding_len - - params.cubic_slashing_window_length - - 1; + 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); @@ -3641,9 +3626,8 @@ where // and the most recent infraction epoch let last_slash_epoch = read_validator_last_slash_epoch(storage, validator)? .unwrap_or_default(); - let eligible_epoch = last_slash_epoch - + params.unbonding_len - + params.cubic_slashing_window_length; // TODO: check this is the correct epoch to submit this tx + let eligible_epoch = + last_slash_epoch + params.slash_processing_epoch_offset(); if current_epoch < eligible_epoch { return Err(UnjailValidatorError::NotEligible( validator.clone(), @@ -3687,11 +3671,8 @@ where let last_infraction_epoch = read_validator_last_slash_epoch(storage, validator)?; if let Some(last_epoch) = last_infraction_epoch { - let is_frozen = current_epoch - < last_epoch - + params.unbonding_len - + 1_u64 - + params.cubic_slashing_window_length; + let is_frozen = + current_epoch < last_epoch + params.slash_processing_epoch_offset(); Ok(is_frozen) } else { Ok(false) diff --git a/proof_of_stake/src/parameters.rs b/proof_of_stake/src/parameters.rs index 351ea11368..0e54e0f7f2 100644 --- a/proof_of_stake/src/parameters.rs +++ b/proof_of_stake/src/parameters.rs @@ -1,6 +1,7 @@ //! Proof-of-Stake system parameters use borsh::{BorshDeserialize, BorshSerialize}; +use namada_core::types::storage::Epoch; use rust_decimal::prelude::ToPrimitive; use rust_decimal::Decimal; use rust_decimal_macros::dec; @@ -146,6 +147,29 @@ impl PosParams { errors } + + /// Get the epoch offset from which an unbonded bond can withdrawn + pub fn withdrawable_epoch_offset(&self) -> u64 { + self.pipeline_len + + self.unbonding_len + + self.cubic_slashing_window_length + } + + /// Get the epoch offset for processing slashes + pub fn slash_processing_epoch_offset(&self) -> u64 { + self.unbonding_len + self.cubic_slashing_window_length + 1 + } + + /// Get the first and the last epoch of a cubic slash window. + pub fn cubic_slash_epoch_window( + &self, + infraction_epoch: Epoch, + ) -> (Epoch, Epoch) { + let start = infraction_epoch + .sub_or_default(Epoch(self.cubic_slashing_window_length)); + let end = infraction_epoch + self.cubic_slashing_window_length; + (start, end) + } } #[cfg(test)] From e3782ad1f50c6b23b6cd00308c2978d0978d7eae Mon Sep 17 00:00:00 2001 From: brentstone Date: Wed, 24 May 2023 16:11:07 +0200 Subject: [PATCH 11/31] remove unused cubic slash function --- proof_of_stake/src/lib.rs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 1c42635dc3..0c732e9499 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -3019,25 +3019,6 @@ where Ok(dec!(9) * sum_vp_fraction * sum_vp_fraction) } -/// Get final cubic slashing rate that is bound from below by some minimum value -/// and capped at 100% -pub fn get_final_cubic_slash_rate( - storage: &S, - params: &PosParams, - infraction_epoch: Epoch, - current_slash_type: SlashType, -) -> storage_api::Result -where - S: StorageRead, -{ - let cubic_rate = - compute_cubic_slash_rate(storage, params, infraction_epoch)?; - // Need some truncation right now to max the rate at 100% - let rate = cubic_rate - .clamp(current_slash_type.get_slash_rate(params), Decimal::ONE); - Ok(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. From ffead5fc3ee83e084a78ee76398a30c5c33e0338 Mon Sep 17 00:00:00 2001 From: brentstone Date: Wed, 24 May 2023 16:11:50 +0200 Subject: [PATCH 12/31] fixup! add cubic_slash_window_length to bounds (maybe still needs change) --- proof_of_stake/src/lib.rs | 14 +++++++++++--- proof_of_stake/src/tests/state_machine.rs | 20 +++++++++++++++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 0c732e9499..e75a4e250d 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -1776,7 +1776,11 @@ fn get_slashed_amount( // epochs before this current slash // TODO: understand this better (from Informal) // TODO: do bounds of this need to be changed with a +/- 1?? - if slashed_amount.epoch + params.unbonding_len < *infraction_epoch { + if slashed_amount.epoch + + params.unbonding_len + + params.cubic_slashing_window_length + < *infraction_epoch + { updated_amount = updated_amount .checked_sub(slashed_amount.amount) .unwrap_or_default(); @@ -3354,7 +3358,9 @@ where ); // TODO: is the 2nd condition correct?? if start <= val_slash.epoch - && val_slash.epoch + params.unbonding_len + && val_slash.epoch + + params.unbonding_len + + params.cubic_slashing_window_length < infraction_epoch // TODO: this `<` should maybe be a `<=` { @@ -3451,7 +3457,9 @@ where val_slash.epoch, val_slash.rate ); if start <= val_slash.epoch - && val_slash.epoch + params.unbonding_len + && val_slash.epoch + + params.unbonding_len + + params.cubic_slashing_window_length < infraction_epoch // TODO: this `<` should maybe be a `<=` { diff --git a/proof_of_stake/src/tests/state_machine.rs b/proof_of_stake/src/tests/state_machine.rs index 594e8dbb31..9c88cfbfc0 100644 --- a/proof_of_stake/src/tests/state_machine.rs +++ b/proof_of_stake/src/tests/state_machine.rs @@ -1992,6 +1992,7 @@ impl AbstractPosState { &slashes_for_this_bond, token::Amount::from_change(to_unbond), self.params.unbonding_len, + self.params.cubic_slashing_window_length, ) .change(); println!("Cur amnt after slashing = {}", &amount_after_slashing); @@ -2273,7 +2274,11 @@ impl AbstractPosState { .iter() .filter(|&s| { start <= s.epoch - && s.epoch + self.params.unbonding_len + && s.epoch + + self.params.unbonding_len + + self + .params + .cubic_slashing_window_length < infraction_epoch }) .cloned() @@ -2293,6 +2298,7 @@ impl AbstractPosState { &slashes_for_this_unbond, unbond_amount, self.params.unbonding_len, + self.params.cubic_slashing_window_length, ); println!( @@ -2334,7 +2340,11 @@ impl AbstractPosState { .iter() .filter(|&s| { start <= s.epoch - && s.epoch + self.params.unbonding_len + && s.epoch + + self.params.unbonding_len + + self + .params + .cubic_slashing_window_length < infraction_epoch }) .cloned() @@ -2355,6 +2365,7 @@ impl AbstractPosState { &slashes_for_this_unbond, unbond_amount, self.params.unbonding_len, + self.params.cubic_slashing_window_length, ); println!( "Total unbonded (offset {}) w slashing = {}", @@ -2833,6 +2844,7 @@ fn compute_amount_after_slashing( slashes: &BTreeMap, amount: token::Amount, unbonding_len: u64, + cubic_slash_window_len: u64, ) -> token::Amount { let mut computed_amounts = Vec::::new(); let mut updated_amount = amount; @@ -2841,7 +2853,9 @@ fn compute_amount_after_slashing( let mut indices_to_remove = BTreeSet::::new(); for (idx, slashed_amount) in computed_amounts.iter().enumerate() { - if slashed_amount.epoch + unbonding_len < *infraction_epoch { + if slashed_amount.epoch + unbonding_len + cubic_slash_window_len + < *infraction_epoch + { updated_amount = updated_amount .checked_sub(slashed_amount.amount) .unwrap_or_default(); From b2fb2dc08de916d6fca5107842c4e1b931e999d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Wed, 24 May 2023 20:13:19 +0200 Subject: [PATCH 13/31] refactor slash lookup --- proof_of_stake/src/lib.rs | 144 +++++++++++++++++--------------------- 1 file changed, 64 insertions(+), 80 deletions(-) diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index e75a4e250d..0d3ca646f6 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -1636,19 +1636,8 @@ where }); println!("to_unbond (init) = {}", to_unbond); - let mut slashes_for_this_bond = BTreeMap::::new(); - for slash in validator_slashes_handle(validator).iter(storage)? { - let slash = slash?; - if bond_epoch <= slash.epoch { - println!( - "Slash (epoch, rate) = ({}, {})", - &slash.epoch, &slash.rate - ); - let cur_rate = - slashes_for_this_bond.entry(slash.epoch).or_default(); - *cur_rate = cmp::min(*cur_rate + slash.rate, Decimal::ONE); - } - } + let slashes_for_this_bond = + find_slashes_in_range(storage, bond_epoch, None, validator)?; amount_after_slashing += get_slashed_amount( ¶ms, @@ -1776,9 +1765,7 @@ fn get_slashed_amount( // epochs before this current slash // TODO: understand this better (from Informal) // TODO: do bounds of this need to be changed with a +/- 1?? - if slashed_amount.epoch - + params.unbonding_len - + params.cubic_slashing_window_length + if slashed_amount.epoch + params.slash_processing_epoch_offset() < *infraction_epoch { updated_amount = updated_amount @@ -1946,23 +1933,15 @@ where tracing::debug!("Not yet withdrawable"); continue; } - let mut slashes_for_this_unbond = BTreeMap::::new(); - for slash in validator_slashes_handle(validator).iter(storage)? { - let slash = slash?; - if start_epoch <= slash.epoch - && slash.epoch - < withdraw_epoch + params.pipeline_len - - params.withdrawable_epoch_offset() - { - println!( - "Slash (epoch, rate) = ({}, {})", - &slash.epoch, &slash.rate - ); - let cur_rate = - slashes_for_this_unbond.entry(slash.epoch).or_default(); - *cur_rate = cmp::min(*cur_rate + slash.rate, Decimal::ONE); - } - } + let slashes_for_this_unbond = find_slashes_in_range( + storage, + start_epoch, + Some( + withdraw_epoch + params.pipeline_len + - params.withdrawable_epoch_offset(), + ), + validator, + )?; // let mut slashes_for_this_unbond = Vec::::new(); // for slash in validator_slashes_handle(validator).iter(storage)? { @@ -3347,30 +3326,18 @@ where continue; } - let mut prev_slashes = BTreeMap::::new(); - for val_slash in - validator_slashes_handle(&validator).iter(storage)? - { - let val_slash = val_slash?; - println!( - "Past slash at epoch {} with rate {}", - val_slash.epoch, val_slash.rate - ); - // TODO: is the 2nd condition correct?? - if start <= val_slash.epoch - && val_slash.epoch - + params.unbonding_len - + params.cubic_slashing_window_length - < infraction_epoch - // TODO: this `<` should maybe be a `<=` - { - println!("Collecting this slash"); - let cur_rate = - prev_slashes.entry(val_slash.epoch).or_default(); - *cur_rate = - cmp::min(*cur_rate + val_slash.rate, Decimal::ONE); - } - } + let prev_slashes = find_slashes_in_range( + storage, + start, + Some( + infraction_epoch + .checked_sub(Epoch( + params.slash_processing_epoch_offset(), + )) + .unwrap_or_default(), + ), + &validator, + )?; println!("Slashes for this unbond: {:?}", prev_slashes); total_unbonded += token::Amount::from_change( @@ -3447,29 +3414,18 @@ where continue; } - let mut prev_slashes = BTreeMap::::new(); - for val_slash in - validator_slashes_handle(&validator).iter(storage)? - { - let val_slash = val_slash?; - println!( - "Past slash at epoch {} with rate {}", - val_slash.epoch, val_slash.rate - ); - if start <= val_slash.epoch - && val_slash.epoch - + params.unbonding_len - + params.cubic_slashing_window_length - < infraction_epoch - // TODO: this `<` should maybe be a `<=` - { - println!("Collecting this slash"); - let cur_rate = - prev_slashes.entry(val_slash.epoch).or_default(); - *cur_rate = - cmp::min(*cur_rate + val_slash.rate, Decimal::ONE); - } - } + let prev_slashes = find_slashes_in_range( + storage, + start, + Some( + infraction_epoch + .checked_sub(Epoch( + params.slash_processing_epoch_offset(), + )) + .unwrap_or_default(), + ), + &validator, + )?; println!("Slashes for this unbond: {:?}", prev_slashes); total_unbonded += token::Amount::from_change( @@ -3712,3 +3668,31 @@ where }); Ok(bond_iter.sum::()) } + +/// Find slashes applicable to a validator with inclusive `start` and exclusive +/// `end` epoch. +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) + { + println!( + "Slash (epoch, rate) = ({}, {})", + &slash.epoch, &slash.rate + ); + let cur_rate = slashes.entry(slash.epoch).or_default(); + *cur_rate = cmp::min(*cur_rate + slash.rate, Decimal::ONE); + } + } + Ok(slashes) +} From d8f7a5078dae4d1eba6f3400da72aea998b4ad3b Mon Sep 17 00:00:00 2001 From: brentstone Date: Thu, 25 May 2023 09:07:15 +0200 Subject: [PATCH 14/31] revert bound cleaning for readability --- proof_of_stake/src/lib.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 0d3ca646f6..0ac4b13b88 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -1937,8 +1937,9 @@ where storage, start_epoch, Some( - withdraw_epoch + params.pipeline_len - - params.withdrawable_epoch_offset(), + withdraw_epoch + - params.unbonding_len + - params.cubic_slashing_window_length, ), validator, )?; @@ -2771,9 +2772,10 @@ fn make_unbond_details( .fold(None, |acc: Option, slash| { if slash.epoch >= start && slash.epoch - < (withdraw + params.pipeline_len) + < withdraw .checked_sub(Epoch( - params.withdrawable_epoch_offset(), + params.unbonding_len + + params.cubic_slashing_window_length, )) .unwrap_or_default() { From 8da284935500bc41794f4b89a8cbbb1f92b2edd2 Mon Sep 17 00:00:00 2001 From: brentstone Date: Thu, 25 May 2023 09:07:45 +0200 Subject: [PATCH 15/31] aesthetic cleaning --- proof_of_stake/src/lib.rs | 98 +---------------------- proof_of_stake/src/tests/state_machine.rs | 4 +- 2 files changed, 5 insertions(+), 97 deletions(-) diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 0ac4b13b88..72b19190cc 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -1745,8 +1745,6 @@ where /// 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. -/// -/// TODO: consider if we want to optimize this fn get_slashed_amount( params: &PosParams, amount: token::Amount, @@ -1914,7 +1912,6 @@ where let mut unbonds_to_remove: Vec<(Epoch, Epoch)> = Vec::new(); for unbond in unbond_handle.iter(storage)? { - // println!("\nUNBOND ITER\n"); let ( NestedSubKey::Data { key: withdraw_epoch, @@ -1944,15 +1941,6 @@ where validator, )?; - // let mut slashes_for_this_unbond = Vec::::new(); - // for slash in validator_slashes_handle(validator).iter(storage)? { - // let slash = slash?; - // if start_epoch <= slash.epoch - // && slash.epoch < withdraw_epoch - params.unbonding_len - // { - // slashes_for_this_unbond.push(slash); - // } - // } let amount_after_slashing = get_slashed_amount(¶ms, amount, &slashes_for_this_unbond)?; @@ -2952,7 +2940,8 @@ where } /// Calculate the cubic slashing rate using all slashes within a window around -/// the given infraction epoch +/// 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, @@ -2998,9 +2987,6 @@ where sum_vp_fraction += infracting_stake / consensus_stake; } println!("sum_vp_fraction: {}", sum_vp_fraction); - - // TODO: make sure `sum_vp_fraction` does not exceed 1/3 or handle with care - // another way Ok(dec!(9) * sum_vp_fraction * sum_vp_fraction) } @@ -3162,43 +3148,6 @@ where )?; } - // Debugging - // println!("POST Validator Set"); - - // for offset in 0..=params.pipeline_len { - // println!("Epoch {}", current_epoch.0 + offset); - // for wv in read_consensus_validator_set_addresses_with_stake( - // storage, - // current_epoch + offset, - // )? { - // println!( - // "Consensus val {}, stake {}, state {:?}", - // &wv.address, - // u64::from(wv.bonded_stake), - // validator_state_handle(&wv.address).get( - // storage, - // current_epoch + offset, - // params - // ) - // ); - // } - // for wv in read_below_capacity_validator_set_addresses_with_stake( - // storage, - // current_epoch + offset, - // )? { - // println!( - // "Below-cap val {}, stake {}, state {:?}", - // &wv.address, - // u64::from(wv.bonded_stake), - // validator_state_handle(&wv.address).get( - // storage, - // current_epoch + offset, - // params - // ) - // ); - // } - // } - // No other actions are performed here until the epoch in which the slash is // processed. @@ -3312,8 +3261,7 @@ where // processing for epoch in Epoch::iter_bounds_inclusive( infraction_epoch.next(), - current_epoch.prev(), /* TODO: should this have a prev() or - * should it even go to pipeline ??? */ + current_epoch.prev(), ) { println!("\nEpoch {}", epoch); let unbonds = unbond_records_handle(&validator).at(&epoch); @@ -3354,46 +3302,6 @@ where } println!("Computing adjusted amounts now"); - // How to handle if there is are slashes from earlier epochs that were - // not processed by this current infraction epoch (so were recently - // processed before this current epoch). - // - // Bonds that became - - // TODO: optimize this, maybe make the validator slashes a map from - // epoch to slash - // let prev_slash_epoch = validator_slashes_handle(&validator) - // .iter(storage)? - // .fold(None, |acc, s| { - // let slash_epoch = s.as_ref().unwrap().epoch; - // if slash_epoch > infraction_epoch { - // acc - // } else if acc.is_none() { - // Some(slash_epoch) - // } else if acc.unwrap() < slash_epoch { - // Some(slash_epoch) - // } else { - // acc - // } - // }); - // let prev_total_rate = if let Some(epoch) = prev_slash_epoch { - // validator_slashes_handle(&validator).iter(storage)?.fold( - // Decimal::ZERO, - // |acc, s| { - // let slash_epoch = s.as_ref().unwrap().epoch; - // if acc > Decimal::ONE { - // Decimal::ONE - // } else if slash_epoch == epoch { - // acc + s.as_ref().unwrap().rate - // } else { - // acc - // } - // }, - // ) - // } else { - // Decimal::ZERO - // }; - // Compute the adjusted validator deltas and slashed amounts from the // current up until the pipeline epoch let mut last_slash = token::Change::default(); diff --git a/proof_of_stake/src/tests/state_machine.rs b/proof_of_stake/src/tests/state_machine.rs index 9c88cfbfc0..8a1fea9997 100644 --- a/proof_of_stake/src/tests/state_machine.rs +++ b/proof_of_stake/src/tests/state_machine.rs @@ -2205,8 +2205,8 @@ impl AbstractPosState { if !slashes_this_epoch.is_empty() { let infraction_epoch = self.epoch - self.params.unbonding_len - - 1 - - self.params.cubic_slashing_window_length; + - self.params.cubic_slashing_window_length + - 1; // Now need to basically do the end_of_epoch() procedure // from the Informal Systems model let cubic_rate = self.cubic_slash_rate(); From 0ae4157a20ab2e6ab29f2e7b3a42dda35a23c98a Mon Sep 17 00:00:00 2001 From: brentstone Date: Thu, 25 May 2023 14:56:32 +0200 Subject: [PATCH 16/31] withdraw: fix bounds for collecting slashes for an unbond --- proof_of_stake/src/lib.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 72b19190cc..2f286eba65 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -1933,11 +1933,7 @@ where let slashes_for_this_unbond = find_slashes_in_range( storage, start_epoch, - Some( - withdraw_epoch - - params.unbonding_len - - params.cubic_slashing_window_length, - ), + Some(withdraw_epoch - params.slash_processing_epoch_offset()), validator, )?; From c4eb55e611fbb5e2e9daf56ca66963307e98a51f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 25 May 2023 16:38:00 +0200 Subject: [PATCH 17/31] make find_slashes_in_ranges inclusive on end epoch --- proof_of_stake/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 2f286eba65..5c03e41b52 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -3575,8 +3575,8 @@ where Ok(bond_iter.sum::()) } -/// Find slashes applicable to a validator with inclusive `start` and exclusive -/// `end` epoch. +/// Find slashes applicable to a validator with inclusive `start` and `end` +/// epoch. fn find_slashes_in_range( storage: &S, start: Epoch, @@ -3590,7 +3590,7 @@ where 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) + && end.map(|end| slash.epoch <= end).unwrap_or(true) { println!( "Slash (epoch, rate) = ({}, {})", From 69e52c814e51e7de0453f4c16744297c0a212715 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 25 May 2023 17:00:14 +0200 Subject: [PATCH 18/31] add cli to sdk impl for tx unjail --- apps/src/lib/cli.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index d4087a3027..0342e17235 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -3028,6 +3028,22 @@ pub mod args { } } + impl CliToSdk> for TxUnjailValidator { + fn to_sdk(self, ctx: &mut Context) -> TxUnjailValidator { + TxUnjailValidator { + tx: self.tx.to_sdk(ctx), + validator: ctx.get(&self.validator), + tx_code_path: self + .tx_code_path + .as_path() + .to_str() + .unwrap() + .to_string() + .into_bytes(), + } + } + } + impl Args for TxUnjailValidator { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); From 0f41f7ac0f38f24b00bfc6826ccc0861ef194932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 25 May 2023 17:05:18 +0200 Subject: [PATCH 19/31] get_slashed_amount: inclusive on infraction epoch --- proof_of_stake/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 5c03e41b52..241145e6b5 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -1764,7 +1764,7 @@ fn get_slashed_amount( // TODO: understand this better (from Informal) // TODO: do bounds of this need to be changed with a +/- 1?? if slashed_amount.epoch + params.slash_processing_epoch_offset() - < *infraction_epoch + <= *infraction_epoch { updated_amount = updated_amount .checked_sub(slashed_amount.amount) From 9543ff437230307e529233d8e3264148e153da07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 25 May 2023 21:59:36 +0200 Subject: [PATCH 20/31] rip slash pool --- .../lib/node/ledger/shell/finalize_block.rs | 121 +++++++------ proof_of_stake/src/lib.rs | 35 ++-- proof_of_stake/src/tests.rs | 168 +++++++++++++++++- 3 files changed, 243 insertions(+), 81 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index c83315b958..3e6338716d 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -2659,17 +2659,18 @@ mod test_finalize_block { ); // Check the balance of the Slash Pool - let slash_pool_balance: token::Amount = shell - .wl_storage - .read(&slash_balance_key) - .expect("must be able to read") - .unwrap_or_default(); - let exp_slashed_3 = decimal_mult_amount( - std::cmp::min(Decimal::TWO * cubic_rate, Decimal::ONE), - val_stake_3 - del_unbond_1_amount + self_bond_1_amount - - self_unbond_2_amount, - ); - assert_eq!(slash_pool_balance, exp_slashed_3); + // TODO: finish once implemented + // let slash_pool_balance: token::Amount = shell + // .wl_storage + // .read(&slash_balance_key) + // .expect("must be able to read") + // .unwrap_or_default(); + // let exp_slashed_3 = decimal_mult_amount( + // std::cmp::min(Decimal::TWO * cubic_rate, Decimal::ONE), + // val_stake_3 - del_unbond_1_amount + self_bond_1_amount + // - self_unbond_2_amount, + // ); + // assert_eq!(slash_pool_balance, exp_slashed_3); let pre_stake_11 = namada_proof_of_stake::read_validator_stake( &shell.wl_storage, @@ -2690,32 +2691,33 @@ mod test_finalize_block { assert_eq!(current_epoch.0, 10_u64); // Check the balance of the Slash Pool - let slash_pool_balance: token::Amount = shell - .wl_storage - .read(&slash_balance_key) - .expect("must be able to read") - .unwrap_or_default(); - - let exp_slashed_4 = if dec!(2) * cubic_rate >= Decimal::ONE { - token::Amount::default() - } else if dec!(3) * cubic_rate >= Decimal::ONE { - decimal_mult_amount( - Decimal::ONE - dec!(2) * cubic_rate, - val_stake_4 + self_bond_1_amount - self_unbond_2_amount, - ) - } else { - decimal_mult_amount( - std::cmp::min(cubic_rate, Decimal::ONE), - val_stake_4 + self_bond_1_amount - self_unbond_2_amount, - ) - }; - dbg!(slash_pool_balance, exp_slashed_3 + exp_slashed_4); - assert!( - (slash_pool_balance.change() - - (exp_slashed_3 + exp_slashed_4).change()) - .abs() - <= 1 - ); + // TODO: finish once implemented + // let slash_pool_balance: token::Amount = shell + // .wl_storage + // .read(&slash_balance_key) + // .expect("must be able to read") + // .unwrap_or_default(); + + // let exp_slashed_4 = if dec!(2) * cubic_rate >= Decimal::ONE { + // token::Amount::default() + // } else if dec!(3) * cubic_rate >= Decimal::ONE { + // decimal_mult_amount( + // Decimal::ONE - dec!(2) * cubic_rate, + // val_stake_4 + self_bond_1_amount - self_unbond_2_amount, + // ) + // } else { + // decimal_mult_amount( + // std::cmp::min(cubic_rate, Decimal::ONE), + // val_stake_4 + self_bond_1_amount - self_unbond_2_amount, + // ) + // }; + // dbg!(slash_pool_balance, exp_slashed_3 + exp_slashed_4); + // assert!( + // (slash_pool_balance.change() + // - (exp_slashed_3 + exp_slashed_4).change()) + // .abs() + // <= 1 + // ); let val_stake = read_validator_stake( &shell.wl_storage, @@ -2739,11 +2741,12 @@ mod test_finalize_block { // dbg!(pre_stake_10 - post_stake_10); // dbg!(&exp_slashed_during_processing_9); - assert!( - ((pre_stake_11 - post_stake_11).change() - exp_slashed_4.change()) - .abs() - <= 1 - ); + // TODO: finish once implemented + // assert!( + // ((pre_stake_11 - post_stake_11).change() - + // exp_slashed_4.change()) .abs() + // <= 1 + // ); // dbg!(&val_stake, &exp_stake); // dbg!(exp_slashed_during_processing_8 + @@ -2753,13 +2756,14 @@ mod test_finalize_block { // exp_slashed_during_processing_9) // ); - let exp_stake = val_stake_3 - del_unbond_1_amount + self_bond_1_amount - - self_unbond_2_amount - + del_2_amount - - exp_slashed_3 - - exp_slashed_4; + // let exp_stake = val_stake_3 - del_unbond_1_amount + + // self_bond_1_amount + // - self_unbond_2_amount + // + del_2_amount + // - exp_slashed_3 + // - exp_slashed_4; - assert!((exp_stake.change() - post_stake_11.change()).abs() <= 1); + // assert!((exp_stake.change() - post_stake_11.change()).abs() <= 1); for _ in 0..2 { let votes = get_default_true_votes( @@ -2903,17 +2907,18 @@ mod test_finalize_block { del_unbond_1_amount - exp_del_withdraw_slashed_amount ); + // TODO: finish once implemented // Check the balance of the Slash Pool - let slash_pool_balance: token::Amount = shell - .wl_storage - .read(&slash_balance_key) - .expect("must be able to read") - .unwrap_or_default(); - dbg!(del_withdraw, slash_pool_balance); - assert_eq!( - slash_pool_balance - slash_pool_balance_pre_withdraw, - exp_del_withdraw_slashed_amount - ); + // let slash_pool_balance: token::Amount = shell + // .wl_storage + // .read(&slash_balance_key) + // .expect("must be able to read") + // .unwrap_or_default(); + // dbg!(del_withdraw, slash_pool_balance); + // assert_eq!( + // slash_pool_balance - slash_pool_balance_pre_withdraw, + // exp_del_withdraw_slashed_amount + // ); println!("\nWITHDRAWING SELF UNBOND"); // Withdraw the self unbonds, which total 154_654 + 15_000 - 9_123. Only diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 241145e6b5..a0b4280792 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -1967,15 +1967,16 @@ where &ADDRESS, source, )?; - // Transfer the slashed tokens from the PoS address to the Slash Pool + + // TODO: Transfer the slashed tokens from the PoS address to the Slash Pool // address - transfer_tokens( - storage, - &staking_token, - total_slashed, - &ADDRESS, - &SLASH_POOL_ADDRESS, - )?; + // transfer_tokens( + // storage, + // &staking_token, + // total_slashed, + // &ADDRESS, + // &SLASH_POOL_ADDRESS, + // )?; Ok(withdrawable_amount) } @@ -3427,15 +3428,15 @@ where println!("Total slashed = {}", total_slashed); debug_assert!(total_slashed >= token::Change::default()); - // Transfer all slashed tokens from PoS account to Slash Pool address - let staking_token = staking_token_address(storage); - transfer_tokens( - storage, - &staking_token, - token::Amount::from_change(total_slashed), - &ADDRESS, - &SLASH_POOL_ADDRESS, - )?; + // TODO: Transfer all slashed tokens from PoS account to Slash Pool address + // let staking_token = staking_token_address(storage); + // transfer_tokens( + // storage, + // &staking_token, + // token::Amount::from_change(total_slashed), + // &ADDRESS, + // &SLASH_POOL_ADDRESS, + // )?; Ok(()) } diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index 70dd968a34..82688608c6 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -7,7 +7,7 @@ use std::ops::Range; 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::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, @@ -17,7 +17,7 @@ use namada_core::types::key::common::{PublicKey, SecretKey}; use namada_core::types::key::testing::{ arb_common_keypair, common_sk_from_simple_seed, }; -use namada_core::types::storage::Epoch; +use namada_core::types::storage::{BlockHeight, Epoch}; use namada_core::types::{address, key, token}; use proptest::prelude::*; use proptest::test_runner::Config; @@ -30,9 +30,10 @@ use test_log::test; use crate::parameters::testing::arb_pos_params; use crate::parameters::PosParams; use crate::types::{ - into_tm_voting_power, BondDetails, BondId, BondsAndUnbondsDetails, - ConsensusValidator, GenesisValidator, Position, ReverseOrdTokenAmount, - UnbondDetails, ValidatorSetUpdate, ValidatorState, WeightedValidator, + decimal_mult_amount, into_tm_voting_power, BondDetails, BondId, + BondsAndUnbondsDetails, ConsensusValidator, GenesisValidator, Position, + ReverseOrdTokenAmount, SlashType, UnbondDetails, ValidatorSetUpdate, + ValidatorState, WeightedValidator, }; use crate::{ become_validator, below_capacity_validator_set_handle, bond_handle, @@ -46,7 +47,8 @@ use crate::{ staking_token_address, total_deltas_handle, unbond_handle, unbond_tokens, unjail_validator, update_validator_deltas, update_validator_set, validator_consensus_key_handle, validator_set_update_tendermint, - validator_state_handle, withdraw_tokens, write_validator_address_raw_hash, + validator_slashes_handle, validator_state_handle, withdraw_tokens, + write_validator_address_raw_hash, }; proptest! { @@ -104,6 +106,24 @@ proptest! { } } +proptest! { + // Generate arb valid input for `test_slashes_with_unbonding_aux` + #![proptest_config(Config { + cases: 1, + .. Config::default() + })] + #[test] + fn test_slashes_with_unbonding( + pos_params in arb_pos_params(Some(5)), + // Must have at least 4 validators so we can slash one and the cubic + // slash rate will be less than 100% + genesis_validators in arb_genesis_validators(4..10), + + ) { + test_slashes_with_unbonding_aux(pos_params, genesis_validators) + } +} + /// Test genesis initialization fn test_init_genesis_aux( params: PosParams, @@ -857,6 +877,140 @@ fn test_become_validator_aux( withdraw_tokens(&mut s, None, &new_validator, current_epoch).unwrap(); } +fn test_slashes_with_unbonding_aux( + mut params: PosParams, + 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(); + + // 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}" + ); + + // Genesis + // let start_epoch = s.storage.block.epoch; + let mut current_epoch = s.storage.block.epoch; + init_genesis( + &mut s, + ¶ms, + validators.clone().into_iter(), + current_epoch, + ) + .unwrap(); + s.commit_block().unwrap(); + + current_epoch = advance_epoch(&mut s, ¶ms); + + // 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, + ) + .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); + } + + // Unbond half of the tokens + let unbond_amount = decimal_mult_amount(dec!(0.5), val_tokens); + println!("Going to unbond {unbond_amount}"); + let unbond_epoch = current_epoch; + unbond_tokens(&mut s, None, &val_addr, unbond_amount, unbond_epoch) + .unwrap(); + + // current_epoch = advance_epoch(&mut s, ¶ms); + + // 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, + ) + .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); + } + let token = staking_token_address(&s); + let val_balance_pre = read_balance(&s, &token, &val_addr).unwrap(); + + 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; + + 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 {slash_rate_1}"); + + let expected_withdrawn_amount = decimal_mult_amount( + dec!(1) - slash_rate_1, + decimal_mult_amount(dec!(1) - slash_rate_0, unbond_amount), + ); + // Allow some rounding error, 1 NAMNAM per each slash + let rounding_error_tolerance = 2; + assert!( + dbg!( + (expected_withdrawn_amount.change() - withdrawn_tokens.change()) + .abs() + ) <= 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(); @@ -1738,6 +1892,8 @@ fn advance_epoch(s: &mut TestWlStorage, params: &PosParams) -> Epoch { &below_capacity_validator_set_handle(), ) .unwrap(); + process_slashes(s, current_epoch).unwrap(); + dbg!(current_epoch); current_epoch } From ade6b002730f0100f8738baf63db84f1f3d27c1d Mon Sep 17 00:00:00 2001 From: brentstone Date: Thu, 25 May 2023 22:33:44 +0200 Subject: [PATCH 21/31] remove test code until slash pool transfers are solved --- .../lib/node/ledger/shell/finalize_block.rs | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 3e6338716d..16a25e65bc 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -2672,7 +2672,7 @@ mod test_finalize_block { // ); // assert_eq!(slash_pool_balance, exp_slashed_3); - let pre_stake_11 = namada_proof_of_stake::read_validator_stake( + let _pre_stake_11 = namada_proof_of_stake::read_validator_stake( &shell.wl_storage, ¶ms, &val1.address, @@ -2889,7 +2889,7 @@ mod test_finalize_block { assert_eq!(self_details.unbonds[2].slashed_amount, None); println!("\nWITHDRAWING DELEGATION UNBOND"); - let slash_pool_balance_pre_withdraw = slash_pool_balance; + // let slash_pool_balance_pre_withdraw = slash_pool_balance; // Withdraw the delegation unbonds, which total to 18_000. This should // only be affected by the slashes in epoch 3 let del_withdraw = namada_proof_of_stake::withdraw_tokens( @@ -2920,40 +2920,40 @@ mod test_finalize_block { // exp_del_withdraw_slashed_amount // ); - println!("\nWITHDRAWING SELF UNBOND"); + // println!("\nWITHDRAWING SELF UNBOND"); // Withdraw the self unbonds, which total 154_654 + 15_000 - 9_123. Only // the (15_000 - 9_123) tokens are slashable. - let self_withdraw = namada_proof_of_stake::withdraw_tokens( - &mut shell.wl_storage, - None, - &val1.address, - current_epoch, - ) - .unwrap(); + // let self_withdraw = namada_proof_of_stake::withdraw_tokens( + // &mut shell.wl_storage, + // None, + // &val1.address, + // current_epoch, + // ) + // .unwrap(); - let exp_self_withdraw_slashed_amount = decimal_mult_amount( - std::cmp::min(dec!(3) * cubic_rate, Decimal::ONE), - self_unbond_2_amount - self_bond_1_amount, - ); + // let exp_self_withdraw_slashed_amount = decimal_mult_amount( + // std::cmp::min(dec!(3) * cubic_rate, Decimal::ONE), + // self_unbond_2_amount - self_bond_1_amount, + // ); // Check the balance of the Slash Pool - let slash_pool_balance: token::Amount = shell - .wl_storage - .read(&slash_balance_key) - .expect("must be able to read") - .unwrap_or_default(); + // let slash_pool_balance: token::Amount = shell + // .wl_storage + // .read(&slash_balance_key) + // .expect("must be able to read") + // .unwrap_or_default(); - dbg!(self_withdraw, slash_pool_balance); - dbg!( - decimal_mult_amount(dec!(2) * cubic_rate, val_stake_3) - + decimal_mult_amount(cubic_rate, val_stake_4) - ); + // dbg!(self_withdraw, slash_pool_balance); + // dbg!( + // decimal_mult_amount(dec!(2) * cubic_rate, val_stake_3) + // + decimal_mult_amount(cubic_rate, val_stake_4) + // ); - assert_eq!( - exp_self_withdraw_slashed_amount, - slash_pool_balance - - slash_pool_balance_pre_withdraw - - exp_del_withdraw_slashed_amount - ); + // assert_eq!( + // exp_self_withdraw_slashed_amount, + // slash_pool_balance + // - slash_pool_balance_pre_withdraw + // - exp_del_withdraw_slashed_amount + // ); Ok(()) } From e1e150161a601033c7bf1896d5171690f3193a70 Mon Sep 17 00:00:00 2001 From: brentstone Date: Thu, 25 May 2023 23:08:59 +0200 Subject: [PATCH 22/31] fix clippy --- proof_of_stake/src/tests.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index 82688608c6..432958deb4 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -926,7 +926,7 @@ fn test_slashes_with_unbonding_aux( slash_0_evidence_epoch, evidence_block_height, slash_0_type, - &val_addr, + val_addr, ) .unwrap(); @@ -941,15 +941,14 @@ fn test_slashes_with_unbonding_aux( let unbond_amount = decimal_mult_amount(dec!(0.5), val_tokens); println!("Going to unbond {unbond_amount}"); let unbond_epoch = current_epoch; - unbond_tokens(&mut s, None, &val_addr, unbond_amount, unbond_epoch) - .unwrap(); + unbond_tokens(&mut s, None, val_addr, unbond_amount, unbond_epoch).unwrap(); // current_epoch = advance_epoch(&mut s, ¶ms); // Discover second slash let slash_1_evidence_epoch = current_epoch; // Ensure that both slashes happen before `unbond_epoch + pipeline` - let slash_1_processing_epoch = + 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; @@ -960,7 +959,7 @@ fn test_slashes_with_unbonding_aux( slash_1_evidence_epoch, evidence_block_height, slash_1_type, - &val_addr, + val_addr, ) .unwrap(); @@ -970,11 +969,11 @@ fn test_slashes_with_unbonding_aux( current_epoch = advance_epoch(&mut s, ¶ms); } let token = staking_token_address(&s); - let val_balance_pre = read_balance(&s, &token, &val_addr).unwrap(); + let val_balance_pre = read_balance(&s, &token, val_addr).unwrap(); - withdraw_tokens(&mut s, None, &val_addr, current_epoch).unwrap(); + withdraw_tokens(&mut s, None, val_addr, current_epoch).unwrap(); - let val_balance_post = read_balance(&s, &token, &val_addr).unwrap(); + let val_balance_post = read_balance(&s, &token, val_addr).unwrap(); let withdrawn_tokens = val_balance_post - val_balance_pre; let slash_rate_0 = validator_slashes_handle(val_addr) From fdec51e01ee000042d660c4fd610126b1770d476 Mon Sep 17 00:00:00 2001 From: brentstone Date: Thu, 25 May 2023 23:09:45 +0200 Subject: [PATCH 23/31] clean up logging --- .../lib/node/ledger/shell/finalize_block.rs | 2 +- apps/src/lib/node/ledger/shell/mod.rs | 1 - proof_of_stake/src/lib.rs | 178 +++++++++--------- 3 files changed, 95 insertions(+), 86 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 16a25e65bc..6c26d90687 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -2794,7 +2794,7 @@ mod test_finalize_block { let del_details = details.get(&del_id).unwrap(); let self_details = details.get(&self_id).unwrap(); - dbg!(del_details, self_details); + // dbg!(del_details, self_details); // Check slashes assert_eq!(del_details.slashes, self_details.slashes); diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index 772239cea4..a24a807b9f 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -493,7 +493,6 @@ where /// Apply PoS slashes from the evidence fn record_slashes_from_evidence(&mut self) { if !self.byzantine_validators.is_empty() { - println!("BYZANTINE VALIDATORS NOT EMPTY"); let byzantine_validators = mem::take(&mut self.byzantine_validators); // TODO: resolve this unwrap() better diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index a0b4280792..7d8b1ef110 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -622,7 +622,7 @@ pub fn read_validator_stake( where S: StorageRead, { - tracing::debug!("Read validator stake at epoch {}", epoch); + // tracing::debug!("Read validator stake at epoch {}", epoch); let handle = validator_deltas_handle(validator); let amount = handle .get_sum(storage, epoch, params)? @@ -865,7 +865,7 @@ where S: StorageRead + StorageWrite, { let amount = amount.change(); - tracing::debug!("Bonding token amount {amount} at epoch {current_epoch}"); + tracing::debug!("Bonding token amount {amount} at epoch {current_epoch}."); let params = read_pos_params(storage)?; let pipeline_epoch = current_epoch + params.pipeline_len; if let Some(source) = source { @@ -884,6 +884,7 @@ where } let source = source.unwrap_or(validator); + tracing::debug!("Source {} --> Validator {}", source, validator); let bond_handle = bond_handle(source, validator); let global_bond_handle = global_bond_handle(validator); @@ -897,13 +898,13 @@ where } } - println!("\nBonds before incrementing:"); + tracing::debug!("\nBonds before incrementing:"); for ep in Epoch::default().iter_range(current_epoch.0 + 3) { let delta = bond_handle .get_delta_val(storage, ep, ¶ms)? .unwrap_or_default(); if delta != 0 { - println!("bond ∆ at epoch {}: {}", ep, delta); + tracing::debug!("bond ∆ at epoch {}: {}", ep, delta); } } @@ -923,13 +924,13 @@ where offset, )?; - println!("\nBonds after incrementing:"); + tracing::debug!("\nBonds after incrementing:"); for ep in Epoch::default().iter_range(current_epoch.0 + 3) { let delta = bond_handle .get_delta_val(storage, ep, ¶ms)? .unwrap_or_default(); if delta != 0 { - println!("bond ∆ at epoch {}: {}", ep, delta); + tracing::debug!("bond ∆ at epoch {}: {}", ep, delta); } } @@ -1531,11 +1532,6 @@ where tracing::debug!("Unbonding token amount {amount} at epoch {current_epoch}"); let params = read_pos_params(storage)?; let pipeline_epoch = current_epoch + params.pipeline_len; - tracing::debug!( - "Current validator stake at pipeline: {}", - read_validator_stake(storage, ¶ms, validator, pipeline_epoch)? - .unwrap_or_default() - ); // Make sure source is not some other validator if let Some(source) = source { @@ -1573,13 +1569,13 @@ where let bonds_handle = bond_handle(source, validator); let global_bonds_handle = global_bond_handle(validator); - println!("\nBonds before decrementing:"); + tracing::debug!("\nBonds before decrementing:"); for ep in Epoch::default().iter_range(current_epoch.0 + 3) { let delta = bonds_handle .get_delta_val(storage, ep, ¶ms)? .unwrap_or_default(); if delta != 0 { - println!("bond ∆ at epoch {}: {}", ep, delta); + tracing::debug!("bond ∆ at epoch {}: {}", ep, delta); } } @@ -1619,8 +1615,8 @@ where continue; } let (bond_epoch, bond_amount) = bond.unwrap(); - println!("\nBond (epoch, amnt) = ({}, {})", bond_epoch, bond_amount); - println!("remaining = {}", remaining); + // println!("\nBond (epoch, amnt) = ({}, {})", bond_epoch, bond_amount); + // println!("remaining = {}", remaining); let global_bond_amount = global_bonds_handle.get_delta_val(storage, bond_epoch, ¶ms)?; @@ -1634,7 +1630,7 @@ where new_global_bond_value: global_bond_amount - to_unbond, unbond_value: to_unbond, }); - println!("to_unbond (init) = {}", to_unbond); + // println!("to_unbond (init) = {}", to_unbond); let slashes_for_this_bond = find_slashes_in_range(storage, bond_epoch, None, validator)?; @@ -1644,7 +1640,7 @@ where token::Amount::from_change(to_unbond), &slashes_for_this_bond, )?; - println!("Cur amnt after slashing = {}", &amount_after_slashing); + // println!("Cur amnt after slashing = {}", &amount_after_slashing); // Update the unbond records let cur_amnt = unbond_records_handle(validator) @@ -1687,13 +1683,13 @@ where )?; } - println!("Bonds after decrementing:"); + tracing::debug!("Bonds after decrementing:"); for ep in Epoch::default().iter_range(current_epoch.0 + 3) { let delta = bonds_handle .get_delta_val(storage, ep, ¶ms)? .unwrap_or_default(); if delta != 0 { - println!("bond ∆ at epoch {}: {}", ep, delta); + tracing::debug!("bond ∆ at epoch {}: {}", ep, delta); } } let stake_at_pipeline = @@ -1701,6 +1697,10 @@ where .unwrap_or_default() .change(); let token_change = cmp::min(amount_after_slashing, stake_at_pipeline); + tracing::debug!( + "Token change including slashes on unbond = {}", + token_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 @@ -1712,7 +1712,6 @@ where ValidatorState::Jailed ); if !is_jailed_at_pipeline { - tracing::debug!("Updating validator set for unbonding"); update_validator_set( storage, ¶ms, @@ -1750,13 +1749,13 @@ fn get_slashed_amount( amount: token::Amount, slashes: &BTreeMap, ) -> storage_api::Result { - println!("FN `get_slashed_amount`"); + // println!("FN `get_slashed_amount`"); let mut updated_amount = amount; let mut computed_amounts = Vec::::new(); for (infraction_epoch, slash_rate) in slashes { - println!("Slash epoch: {}, rate: {}", infraction_epoch, slash_rate); + // println!("Slash epoch: {}, rate: {}", infraction_epoch, slash_rate); 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 @@ -1783,9 +1782,9 @@ fn get_slashed_amount( epoch: *infraction_epoch, }); } - println!("Finished loop over slashes in `get_slashed_amount`"); - println!("Updated amount: {:?}", &updated_amount); - println!("Computed amounts: {:?}", &computed_amounts); + // println!("Finished loop over slashes in `get_slashed_amount`"); + // println!("Updated amount: {:?}", &updated_amount); + // println!("Computed amounts: {:?}", &computed_amounts); let total_computed_amounts = computed_amounts .into_iter() @@ -1893,9 +1892,10 @@ pub fn withdraw_tokens( where S: StorageRead + StorageWrite, { - println!("Withdrawing tokens in epoch {current_epoch}"); + tracing::debug!("Withdrawing tokens in epoch {current_epoch}"); let params = read_pos_params(storage)?; let source = source.unwrap_or(validator); + tracing::debug!("Source {} --> Validator {}", source, validator); let unbond_handle = unbond_handle(source, validator); if unbond_handle.is_empty(storage)? { @@ -1927,7 +1927,9 @@ where // TODO: adding slash rates in same epoch, applying cumulatively in dif // epochs if withdraw_epoch > current_epoch { - tracing::debug!("Not yet withdrawable"); + tracing::debug!( + "Not yet withdrawable until epoch {withdraw_epoch}" + ); continue; } let slashes_for_this_unbond = find_slashes_in_range( @@ -2947,7 +2949,7 @@ pub fn compute_cubic_slash_rate( where S: StorageRead, { - println!("COMPUTING CUBIC SLASH RATE"); + // println!("COMPUTING CUBIC SLASH RATE"); let mut sum_vp_fraction = Decimal::ZERO; let (start_epoch, end_epoch) = params.cubic_slash_epoch_window(infraction_epoch); @@ -2955,7 +2957,7 @@ where for epoch in Epoch::iter_bounds_inclusive(start_epoch, end_epoch) { let consensus_stake = Decimal::from(get_total_consensus_stake(storage, epoch)?); - println!("Consensus stake in epoch {}: {}", epoch, consensus_stake); + // println!("Consensus stake in epoch {}: {}", epoch, consensus_stake); let processing_epoch = epoch + params.slash_processing_epoch_offset(); let slashes = enqueued_slashes_handle().at(&processing_epoch); @@ -2973,7 +2975,7 @@ where let validator_stake = read_validator_stake(storage, params, &validator, epoch)? .unwrap_or_default(); - println!("Val {} stake: {}", &validator, validator_stake); + // println!("Val {} stake: {}", &validator, validator_stake); Ok(Decimal::from(validator_stake)) // TODO: does something more complex need to be done @@ -2983,7 +2985,7 @@ where .sum::>()?; sum_vp_fraction += infracting_stake / consensus_stake; } - println!("sum_vp_fraction: {}", sum_vp_fraction); + // println!("sum_vp_fraction: {}", sum_vp_fraction); Ok(dec!(9) * sum_vp_fraction * sum_vp_fraction) } @@ -3002,15 +3004,13 @@ pub fn slash( where S: StorageRead + StorageWrite, { - println!("SLASHING ON NEW EVIDENCE FROM {}", validator); - println!( - "Current state = {:?}", - validator_state_handle(validator) - .get(storage, current_epoch, params) - .unwrap() - .unwrap() + tracing::debug!( + "Slashing validator {} on new evidence from epoch {} (current epoch = \ + {})", + validator, + evidence_epoch, + current_epoch ); - let evidence_block_height: u64 = evidence_block_height.into(); let slash = Slash { epoch: evidence_epoch, @@ -3120,21 +3120,17 @@ where ValidatorState::Inactive => { println!("INACTIVE"); panic!( - "SHouldn't be here - haven't implemented inactive vals yet" + "Shouldn't be here - haven't implemented inactive vals yet" ) } ValidatorState::Jailed => { - println!("Validator already jailed"); + tracing::debug!( + "Found evidence for a validator who is already jailed" + ); // return Ok(()); } } } - - println!( - "\nWRITING VALIDATOR {} STATE AS JAILED STARTING IN EPOCH {}\n", - validator, - current_epoch.next() - ); // Set the validator state as `Jailed` thru the pipeline epoch for offset in 1..=params.pipeline_len { validator_state_handle(validator).set( @@ -3174,10 +3170,14 @@ where // Slashes to be processed in the current epoch let enqueued_slashes = enqueued_slashes_handle().at(¤t_epoch); if enqueued_slashes.is_empty(storage)? { - println!("No slashes found"); return Ok(()); } - println!("Found slashes"); + 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 = @@ -3209,9 +3209,11 @@ where r#type: enqueued_slash.r#type, rate: slash_rate, }; - println!( - "Processing slash for val {} committed in epoch {}, with rate {}", - &validator, enqueued_slash.epoch, slash_rate + tracing::debug!( + "Slash for validator {} committed in epoch {} has rate {}", + &validator, + enqueued_slash.epoch, + slash_rate ); let cur_slashes = validators_and_slashes.entry(validator).or_default(); @@ -3232,11 +3234,11 @@ where )? .unwrap_or_default(); - println!( - "Val {} stake at infraction epoch {} = {}", + tracing::debug!( + "Validator {} stake at infraction epoch {} = {}", &validator, infraction_epoch, - u64::from(validator_stake_at_infraction) + validator_stake_at_infraction ); let mut total_rate = Decimal::ZERO; @@ -3256,17 +3258,18 @@ where // Start from after the infraction epoch up thru last epoch before // processing + tracing::debug!("Iterating over unbonds after the infraction epoch"); for epoch in Epoch::iter_bounds_inclusive( infraction_epoch.next(), current_epoch.prev(), ) { - println!("\nEpoch {}", epoch); + tracing::debug!("Epoch {}", epoch); let unbonds = unbond_records_handle(&validator).at(&epoch); for unbond in unbonds.iter(storage)? { let (start, unbond_amount) = unbond?; - println!( + tracing::debug!( "UnbondRecord: amount = {}, start_epoch {}", - &u64::from(unbond_amount), + &unbond_amount, &start ); if start > infraction_epoch { @@ -3285,25 +3288,25 @@ where ), &validator, )?; - println!("Slashes for this unbond: {:?}", prev_slashes); + tracing::debug!("Slashes for this unbond: {:?}", prev_slashes); total_unbonded += token::Amount::from_change( get_slashed_amount(¶ms, unbond_amount, &prev_slashes)?, ); - println!( + tracing::debug!( "Total unbonded (epoch {}) w slashing = {}", - epoch, total_unbonded + epoch, + total_unbonded ); } } - println!("Computing adjusted amounts now"); // Compute the adjusted validator deltas and slashed amounts from the // current up until the pipeline epoch let mut last_slash = token::Change::default(); for offset in 0..params.pipeline_len { - println!( + tracing::debug!( "Epoch {}\nLast slash = {}", current_epoch + offset, last_slash @@ -3313,9 +3316,10 @@ where for unbond in unbonds.iter(storage)? { let (start, unbond_amount) = unbond?; - println!( + tracing::debug!( "UnbondRecord: amount = {}, start_epoch {}", - &unbond_amount, &start + &unbond_amount, + &start ); if start > infraction_epoch { continue; @@ -3333,14 +3337,15 @@ where ), &validator, )?; - println!("Slashes for this unbond: {:?}", prev_slashes); + tracing::debug!("Slashes for this unbond: {:?}", prev_slashes); total_unbonded += token::Amount::from_change( get_slashed_amount(¶ms, unbond_amount, &prev_slashes)?, ); - println!( + tracing::debug!( "Total unbonded (offset {}) w slashing = {}", - offset, total_unbonded + offset, + total_unbonded ); } @@ -3349,10 +3354,10 @@ where validator_stake_at_infraction - total_unbonded, ) .change(); - println!("This slash = {}", this_slash); + // println!("This slash = {}", this_slash); let diff_slashed_amount = last_slash - this_slash; - println!("Diff slashed amount = {}", diff_slashed_amount); + // println!("Diff slashed amount = {}", diff_slashed_amount); let val_updates = deltas_for_update.entry(validator.clone()).or_default(); @@ -3363,12 +3368,13 @@ where // total_unbonded = token::Amount::default(); } } - println!("\nUpdating deltas"); + // println!("\nUpdating deltas"); // Update the deltas in storage let mut total_slashed = token::Change::default(); for (validator, updates) in deltas_for_update { for (offset, delta) in updates { - println!("Val {}, offset {}, delta {}", &validator, offset, delta); + // println!("Val {}, offset {}, delta {}", &validator, offset, + // delta); let validator_stake_at_offset = read_validator_stake( storage, ¶ms, @@ -3386,17 +3392,17 @@ where infraction_epoch.next(), current_epoch + offset, )?; - println!("\nUnslashable bonds = {}", sum_post_bonds); + // println!("\nUnslashable bonds = {}", sum_post_bonds); let slashable_stake_at_offset = validator_stake_at_offset - sum_post_bonds.change(); // let slashable_stake_at_offset = validator_stake_at_offset; assert!(slashable_stake_at_offset >= token::Change::default()); - println!("Stake at offset = {}", validator_stake_at_offset); - println!( - "Slashable stake at offset = {}", - slashable_stake_at_offset - ); + // println!("Stake at offset = {}", validator_stake_at_offset); + // println!( + // "Slashable stake at offset = {}", + // slashable_stake_at_offset + // ); let change = if slashable_stake_at_offset + delta < token::Change::default() { @@ -3404,7 +3410,12 @@ where } else { delta }; - println!("Change = {}", change); + tracing::debug!( + "Deltas change = {} at offset {} for validator {}", + change, + offset, + &validator + ); total_slashed -= change; update_validator_deltas( @@ -3425,7 +3436,6 @@ where } } - println!("Total slashed = {}", total_slashed); debug_assert!(total_slashed >= token::Change::default()); // TODO: Transfer all slashed tokens from PoS account to Slash Pool address @@ -3593,10 +3603,10 @@ where if start <= slash.epoch && end.map(|end| slash.epoch <= end).unwrap_or(true) { - println!( - "Slash (epoch, rate) = ({}, {})", - &slash.epoch, &slash.rate - ); + // println!( + // "Slash (epoch, rate) = ({}, {})", + // &slash.epoch, &slash.rate + // ); let cur_rate = slashes.entry(slash.epoch).or_default(); *cur_rate = cmp::min(*cur_rate + slash.rate, Decimal::ONE); } From ecd85174c659ac728f400492e3bb31ee55641b6b Mon Sep 17 00:00:00 2001 From: brentstone Date: Fri, 26 May 2023 02:54:35 +0200 Subject: [PATCH 24/31] fixup!: don't call `process_slashes` within `advance_epoch` --- proof_of_stake/src/tests.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index 432958deb4..6cde6373ae 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -912,6 +912,7 @@ fn test_slashes_with_unbonding_aux( 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; @@ -935,6 +936,7 @@ fn test_slashes_with_unbonding_aux( 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(); } // Unbond half of the tokens @@ -967,6 +969,7 @@ fn test_slashes_with_unbonding_aux( 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(); @@ -1891,8 +1894,8 @@ fn advance_epoch(s: &mut TestWlStorage, params: &PosParams) -> Epoch { &below_capacity_validator_set_handle(), ) .unwrap(); - process_slashes(s, current_epoch).unwrap(); - dbg!(current_epoch); + // process_slashes(s, current_epoch).unwrap(); + // dbg!(current_epoch); current_epoch } From 1361f65194e9f336680bdfa3f1a8303d89b23e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 26 May 2023 17:36:34 +0200 Subject: [PATCH 25/31] fixup! rip slash pool --- proof_of_stake/src/tests.rs | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index 6cde6373ae..edc01d3c35 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -109,21 +109,31 @@ proptest! { proptest! { // Generate arb valid input for `test_slashes_with_unbonding_aux` #![proptest_config(Config { - cases: 1, + cases: 5, .. Config::default() })] #[test] fn test_slashes_with_unbonding( - pos_params in arb_pos_params(Some(5)), - // Must have at least 4 validators so we can slash one and the cubic - // slash rate will be less than 100% - genesis_validators in arb_genesis_validators(4..10), - + (params, genesis_validators, unbond_delay) + in test_slashes_with_unbonding_params() ) { - test_slashes_with_unbonding_aux(pos_params, genesis_validators) + test_slashes_with_unbonding_aux( + params, genesis_validators, unbond_delay) } } +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); + (Just(params), validators, unbond_delay) + }) +} + /// Test genesis initialization fn test_init_genesis_aux( params: PosParams, @@ -880,6 +890,7 @@ fn test_become_validator_aux( fn test_slashes_with_unbonding_aux( mut params: PosParams, validators: Vec, + unbond_delay: u64, ) { // This can be useful for debugging: params.pipeline_len = 2; @@ -939,14 +950,17 @@ fn test_slashes_with_unbonding_aux( 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 = decimal_mult_amount(dec!(0.5), val_tokens); println!("Going to unbond {unbond_amount}"); let unbond_epoch = current_epoch; unbond_tokens(&mut s, None, val_addr, unbond_amount, unbond_epoch).unwrap(); - // current_epoch = advance_epoch(&mut s, ¶ms); - // Discover second slash let slash_1_evidence_epoch = current_epoch; // Ensure that both slashes happen before `unbond_epoch + pipeline` From c09ff8ba2478a332fcf496498ff8c0597496ef24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 26 May 2023 17:51:09 +0200 Subject: [PATCH 26/31] fixup! cubic and general slashing algorithms and transactions --- apps/src/lib/node/ledger/shell/mod.rs | 5 ++++- proof_of_stake/src/lib.rs | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index a24a807b9f..9831ff04b5 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -529,7 +529,10 @@ where }; // Disregard evidences that should have already been processed // at this time - if evidence_epoch + pos_params.unbonding_len < current_epoch { + if evidence_epoch + pos_params.slash_processing_epoch_offset() + - pos_params.cubic_slashing_window_length + <= current_epoch + { tracing::info!( "Skipping outdated evidence from epoch \ {evidence_epoch}" diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 7d8b1ef110..cfc1477054 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -3505,7 +3505,6 @@ where let stake = read_validator_stake(storage, ¶ms, validator, pipeline_epoch)? .unwrap_or_default(); - dbg!(&stake); insert_validator_into_validator_set( storage, From 2bc8ec3081c21d0eb02b981d093b66734f79f448 Mon Sep 17 00:00:00 2001 From: brentstone Date: Fri, 26 May 2023 15:16:39 +0200 Subject: [PATCH 27/31] changelog: #892 --- .changelog/unreleased/features/892-cubic-slashing.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changelog/unreleased/features/892-cubic-slashing.md diff --git a/.changelog/unreleased/features/892-cubic-slashing.md b/.changelog/unreleased/features/892-cubic-slashing.md new file mode 100644 index 0000000000..cdd079bfc3 --- /dev/null +++ b/.changelog/unreleased/features/892-cubic-slashing.md @@ -0,0 +1,5 @@ +- The implementation of the cubic slashing system that touches virtually all + parts of the proof-of-stake system. Slashes tokens are currently kept in the + PoS address rather than being transferred to the Slash Pool address. This PR + also includes significant testing infrastructure, highlighted by the PoS state + machine test with slashing. ([#892](https://github.com/anoma/namada/pull/892)) \ No newline at end of file From 293690945ffa9a929f5c057e49c93bff58b043a1 Mon Sep 17 00:00:00 2001 From: brentstone Date: Tue, 30 May 2023 11:09:56 -0400 Subject: [PATCH 28/31] pos sm test: ease load on the CI --- proof_of_stake/src/tests/state_machine.rs | 156 ++++++++++++---------- 1 file changed, 84 insertions(+), 72 deletions(-) diff --git a/proof_of_stake/src/tests/state_machine.rs b/proof_of_stake/src/tests/state_machine.rs index 8a1fea9997..48723248cd 100644 --- a/proof_of_stake/src/tests/state_machine.rs +++ b/proof_of_stake/src/tests/state_machine.rs @@ -44,7 +44,7 @@ prop_state_machine! { })] #[test] /// A `StateMachineTest` implemented on `PosState` - fn pos_state_machine_test(sequential 500 => ConcretePosState); + fn pos_state_machine_test(sequential 200 => ConcretePosState); } /// Abstract representation of a state of PoS system @@ -325,12 +325,6 @@ impl StateMachineTest for ConcretePosState { .unwrap() .unwrap_or_default(); - println!( - "BEFORE: cur_stake = {}, pipeline_stake = {}", - u64::from(validator_stake_before_unbond_cur), - u64::from(validator_stake_before_unbond_pipeline) - ); - // Apply the unbond super::unbond_tokens( &mut state.s, @@ -628,7 +622,6 @@ impl ConcretePosState { ) .unwrap() .unwrap_or_default(); - println!("AFTER: pipeline stake = {}", u64::from(stake_at_pipeline)); // Post-condition: the validator stake at the pipeline should be // decremented at most by the bond amount (because slashing can reduce @@ -642,7 +635,6 @@ impl ConcretePosState { .checked_sub(amount) .unwrap_or_default() ); - println!("Check bond+unbond post-conds"); self.check_bond_and_unbond_post_conditions( submit_epoch, @@ -851,7 +843,6 @@ impl ConcretePosState { } else { panic!("Could not find the slash enqueued"); } - println!("Finished misbehavior post-conditions\n") // TODO: Any others? } @@ -928,7 +919,7 @@ impl ConcretePosState { current_epoch, current_epoch + params.pipeline_len, ) { - println!("Epoch {epoch}"); + // println!("Epoch {epoch}"); let mut vals = HashSet::
::new(); for WeightedValidator { bonded_stake, @@ -942,7 +933,7 @@ impl ConcretePosState { .get_sum(&self.s, epoch, params) .unwrap() .unwrap_or_default(); - println!( + tracing::debug!( "Consensus val {}, stake: {} ({})", &validator, u64::from(bonded_stake), @@ -995,7 +986,7 @@ impl ConcretePosState { .get_sum(&self.s, epoch, params) .unwrap() .unwrap_or_default(); - println!( + tracing::debug!( "Below-cap val {}, stake: {} ({})", &validator, u64::from(bonded_stake), @@ -1075,7 +1066,7 @@ impl ConcretePosState { .get_sum(&self.s, epoch, params) .unwrap() .unwrap_or_default(); - println!("Jailed val {}, stake {}", &val, stake); + tracing::debug!("Jailed val {}, stake {}", &val, stake); assert_eq!( state, @@ -1755,12 +1746,12 @@ impl ReferenceStateMachine for AbstractPosState { false }; - if is_frozen { - println!( - "\nVALIDATOR {} IS FROZEN - CANNOT UNBOND\n", - &id.validator - ); - } + // if is_frozen { + // println!( + // "\nVALIDATOR {} IS FROZEN - CANNOT UNBOND\n", + // &id.validator + // ); + // } // The validator must be known state.is_validator(&id.validator, pipeline) @@ -1826,7 +1817,7 @@ impl ReferenceStateMachine for AbstractPosState { // Ensure that the validator is in consensus when it misbehaves // TODO: possibly also test allowing below-capacity validators - println!("\nVal to possibly misbehave: {}", &address); + // println!("\nVal to possibly misbehave: {}", &address); let state_at_infraction = state .validator_states .get(infraction_epoch) @@ -1834,7 +1825,7 @@ impl ReferenceStateMachine for AbstractPosState { .get(address); if state_at_infraction.is_none() { // Figure out why this happening - println!( + tracing::debug!( "State is None at Infraction epoch {}", infraction_epoch ); @@ -1848,7 +1839,11 @@ impl ReferenceStateMachine for AbstractPosState { .unwrap() .get(address) .cloned(); - println!("State at epoch {} is {:?}", epoch, state_ep); + tracing::debug!( + "State at epoch {} is {:?}", + epoch, + state_ep + ); } } @@ -1961,16 +1956,16 @@ impl AbstractPosState { let mut remaining = change; let mut amount_after_slashing = token::Change::default(); - println!("Bonds before decrementing"); + tracing::debug!("Bonds before decrementing"); for (start, amnt) in bonds.iter() { - println!("Bond epoch {} - amnt {}", start, amnt); + tracing::debug!("Bond epoch {} - amnt {}", start, amnt); } for (bond_epoch, bond_amnt) in bonds.iter_mut().rev() { - println!("remaining {}", remaining); - println!("Bond epoch {} - amnt {}", bond_epoch, bond_amnt); + tracing::debug!("remaining {}", remaining); + tracing::debug!("Bond epoch {} - amnt {}", bond_epoch, bond_amnt); let to_unbond = cmp::min(*bond_amnt, remaining); - println!("to_unbond (init) = {}", to_unbond); + tracing::debug!("to_unbond (init) = {}", to_unbond); *bond_amnt -= to_unbond; *unbonds += token::Amount::from_change(to_unbond); @@ -1984,7 +1979,7 @@ impl AbstractPosState { *cur += s.rate; acc }); - println!( + tracing::debug!( "Slashes for this bond{:?}", slashes_for_this_bond.clone() ); @@ -1995,7 +1990,10 @@ impl AbstractPosState { self.params.cubic_slashing_window_length, ) .change(); - println!("Cur amnt after slashing = {}", &amount_after_slashing); + tracing::debug!( + "Cur amnt after slashing = {}", + &amount_after_slashing + ); let amt = unbond_records.entry(*bond_epoch).or_default(); *amt += token::Amount::from_change(to_unbond); @@ -2006,9 +2004,9 @@ impl AbstractPosState { } } - println!("Bonds after decrementing"); + tracing::debug!("Bonds after decrementing"); for (start, amnt) in bonds.iter() { - println!("Bond epoch {} - amnt {}", start, amnt); + tracing::debug!("Bond epoch {} - amnt {}", start, amnt); } let pipeline_state = self @@ -2073,7 +2071,7 @@ impl AbstractPosState { match state { ValidatorState::Consensus => { - println!("Validator initially in consensus"); + // println!("Validator initially in consensus"); // Remove from the prior stake let vals = consensus_set.entry(this_val_stake_pre).or_default(); // dbg!(&vals); @@ -2129,7 +2127,7 @@ impl AbstractPosState { .push_back(validator.clone()); } ValidatorState::BelowCapacity => { - println!("Validator initially in below-cap"); + // println!("Validator initially in below-cap"); // Remove from the prior stake let vals = @@ -2218,9 +2216,10 @@ impl AbstractPosState { .get(&validator) .cloned() .unwrap_or_default(); - println!( + tracing::debug!( "Val {} stake at infraction {}", - validator, stake_at_infraction + validator, + stake_at_infraction ); let mut total_rate = Decimal::ZERO; @@ -2246,11 +2245,11 @@ impl AbstractPosState { total_rate += rate; } total_rate = cmp::min(total_rate, Decimal::ONE); - println!("Total rate: {}", total_rate); + tracing::debug!("Total rate: {}", total_rate); let mut total_unbonded = token::Amount::default(); for epoch in (infraction_epoch.0 + 1)..self.epoch.0 { - println!("\nEpoch {}", epoch); + tracing::debug!("\nEpoch {}", epoch); let unbond_records = self .unbond_records .entry(validator.clone()) @@ -2259,9 +2258,10 @@ impl AbstractPosState { .cloned() .unwrap_or_default(); for (start, unbond_amount) in unbond_records { - println!( + tracing::debug!( "UnbondRecord: amount = {}, start_epoch {}", - &unbond_amount, &start + &unbond_amount, + &start ); if start > infraction_epoch { continue; @@ -2290,7 +2290,7 @@ impl AbstractPosState { acc }, ); - println!( + tracing::debug!( "Slashes for this unbond: {:?}", slashes_for_this_unbond ); @@ -2301,17 +2301,18 @@ impl AbstractPosState { self.params.cubic_slashing_window_length, ); - println!( + tracing::debug!( "Total unbonded (epoch {}) w slashing = {}", - epoch, total_unbonded + epoch, + total_unbonded ); } } - println!("Computing adjusted amounts now"); + tracing::debug!("Computing adjusted amounts now"); let mut last_slash = token::Change::default(); for offset in 0..self.params.pipeline_len { - println!( + tracing::debug!( "Epoch {}\nLast slash = {}", self.epoch + offset, last_slash @@ -2324,9 +2325,10 @@ impl AbstractPosState { .cloned() .unwrap_or_default(); for (start, unbond_amount) in unbond_records { - println!( + tracing::debug!( "UnbondRecord: amount = {}, start_epoch {}", - &unbond_amount, &start + &unbond_amount, + &start ); if start > infraction_epoch { continue; @@ -2356,7 +2358,7 @@ impl AbstractPosState { acc }, ); - println!( + tracing::debug!( "Slashes for this unbond: {:?}", slashes_for_this_unbond ); @@ -2367,21 +2369,26 @@ impl AbstractPosState { self.params.unbonding_len, self.params.cubic_slashing_window_length, ); - println!( + tracing::debug!( "Total unbonded (offset {}) w slashing = {}", - offset, total_unbonded + offset, + total_unbonded ); } - println!("stake at infraction {}", stake_at_infraction); - println!("total unbonded {}", total_unbonded); + tracing::debug!( + "stake at infraction {}", + stake_at_infraction + ); + tracing::debug!("total unbonded {}", total_unbonded); let this_slash = decimal_mult_i128( total_rate, stake_at_infraction - total_unbonded.change(), ); let diff_slashed_amount = this_slash - last_slash; - println!( + tracing::debug!( "Offset {} diff_slashed_amount {}", - offset, diff_slashed_amount + offset, + diff_slashed_amount ); last_slash = this_slash; // total_unbonded = token::Amount::default(); @@ -2395,13 +2402,13 @@ impl AbstractPosState { // .or_default(); // *validator_stake -= diff_slashed_amount; - println!("Updating ABSTRACT voting powers"); + tracing::debug!("Updating ABSTRACT voting powers"); let sum_post_bonds = self.get_validator_bond_sums( &validator, infraction_epoch.next(), self.epoch + offset, ); - println!("\nUnslashable bonds = {}", sum_post_bonds); + tracing::debug!("\nUnslashable bonds = {}", sum_post_bonds); let validator_stake_at_offset = self .validator_stakes .entry(self.epoch + offset) @@ -2411,12 +2418,12 @@ impl AbstractPosState { let slashable_stake_at_offset = *validator_stake_at_offset - sum_post_bonds.change(); - println!( + tracing::debug!( "Val stake pre (epoch {}) = {}", self.epoch + offset, validator_stake_at_offset ); - println!( + tracing::debug!( "Slashable stake at offset = {}", slashable_stake_at_offset ); @@ -2428,11 +2435,11 @@ impl AbstractPosState { } else { diff_slashed_amount }; - println!("Change = {}", change); + tracing::debug!("Change = {}", change); *validator_stake_at_offset -= change; for os in (offset + 1)..=self.params.pipeline_len { - println!("Adjust epoch {}", self.epoch + os); + tracing::debug!("Adjust epoch {}", self.epoch + os); let offset_stake = self .validator_stakes .entry(self.epoch + os) @@ -2447,7 +2454,7 @@ impl AbstractPosState { // } // *validator_stake = new_stake; - println!( + tracing::debug!( "New stake at epoch {} = {}", self.epoch + os, offset_stake @@ -2568,12 +2575,11 @@ impl AbstractPosState { /// Compute the cubic slashing rate for the current epoch fn cubic_slash_rate(&self) -> Decimal { - println!("Computing ABSTRACT slash rate"); let infraction_epoch = self.epoch - self.params.unbonding_len - 1_u64 - self.params.cubic_slashing_window_length; - println!("Infraction epoch: {}", infraction_epoch); + tracing::debug!("Infraction epoch: {}", infraction_epoch); let window_width = self.params.cubic_slashing_window_length; let epoch_start = Epoch::from( infraction_epoch @@ -2593,7 +2599,11 @@ impl AbstractPosState { sum + *val_stake * validators.len() as u64 }, ); - println!("Consensus stake in epoch {}: {}", epoch, consensus_stake); + tracing::debug!( + "Consensus stake in epoch {}: {}", + epoch, + consensus_stake + ); let processing_epoch = epoch + self.params.unbonding_len @@ -2610,9 +2620,11 @@ impl AbstractPosState { .cloned() .unwrap_or_default(), ); - println!( + tracing::debug!( "Val {} stake epoch {}: {}", - &validator, epoch, val_stake + &validator, + epoch, + val_stake ); vp_frac_sum += Decimal::from(slashes.len()) * Decimal::from(val_stake) @@ -2621,18 +2633,18 @@ impl AbstractPosState { } } let vp_frac_sum = cmp::min(Decimal::ONE, vp_frac_sum); - println!("vp_frac_sum: {}", vp_frac_sum); + tracing::debug!("vp_frac_sum: {}", vp_frac_sum); cmp::min(dec!(9) * vp_frac_sum * vp_frac_sum, Decimal::ONE) } fn debug_validators(&self) { - println!("DEBUG ABSTRACT VALIDATOR"); + tracing::debug!("DEBUG ABSTRACT VALIDATOR"); let current_epoch = self.epoch; for epoch in Epoch::iter_bounds_inclusive(current_epoch, self.pipeline()) { - println!("Epoch {}", epoch); + tracing::debug!("Epoch {}", epoch); let mut min_consensus = token::Amount::from(u64::MAX); let consensus = self.consensus_set.get(&epoch).unwrap(); for (amount, vals) in consensus { @@ -2652,7 +2664,7 @@ impl AbstractPosState { .unwrap() .get(val) .unwrap(); - println!( + tracing::debug!( "Consensus val {}, stake {} ({}) - ({:?})", val, u64::from(*amount), @@ -2686,7 +2698,7 @@ impl AbstractPosState { .unwrap() .get(val) .unwrap(); - println!( + tracing::debug!( "Below-cap val {}, stake {} ({}) - ({:?})", val, u64::from(token::Amount::from(*amount)), @@ -2729,7 +2741,7 @@ impl AbstractPosState { .get(&addr) .cloned() .unwrap_or_default(); - println!("Jailed val {}, stake {}", &addr, &stake); + tracing::debug!("Jailed val {}, stake {}", &addr, &stake); } } } From 97020799024b8f8b611d6dc26086d3120188ec57 Mon Sep 17 00:00:00 2001 From: brentstone Date: Tue, 30 May 2023 16:48:01 -0400 Subject: [PATCH 29/31] fixing `find_slashes_in_range` --- proof_of_stake/src/lib.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index cfc1477054..56ab8203dc 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -1935,7 +1935,11 @@ where let slashes_for_this_unbond = find_slashes_in_range( storage, start_epoch, - Some(withdraw_epoch - params.slash_processing_epoch_offset()), + Some( + withdraw_epoch + - params.unbonding_len + - params.cubic_slashing_window_length, + ), validator, )?; @@ -3282,7 +3286,8 @@ where Some( infraction_epoch .checked_sub(Epoch( - params.slash_processing_epoch_offset(), + params.unbonding_len + + params.cubic_slashing_window_length, )) .unwrap_or_default(), ), @@ -3331,7 +3336,8 @@ where Some( infraction_epoch .checked_sub(Epoch( - params.slash_processing_epoch_offset(), + params.unbonding_len + + params.cubic_slashing_window_length, )) .unwrap_or_default(), ), @@ -3585,8 +3591,8 @@ where Ok(bond_iter.sum::()) } -/// Find slashes applicable to a validator with inclusive `start` and `end` -/// epoch. +/// Find slashes applicable to a validator with inclusive `start` and exclusive +/// `end` epoch. fn find_slashes_in_range( storage: &S, start: Epoch, @@ -3600,7 +3606,7 @@ where 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) + && end.map(|end| slash.epoch < end).unwrap_or(true) { // println!( // "Slash (epoch, rate) = ({}, {})", From 40f601993de525a972dafe0883aa6725e4b13c53 Mon Sep 17 00:00:00 2001 From: brentstone Date: Wed, 31 May 2023 19:49:26 -0400 Subject: [PATCH 30/31] WIP changes from Manu and logging --- proof_of_stake/src/lib.rs | 13 ++++-------- proof_of_stake/src/tests/state_machine.rs | 24 ++++++++++++----------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 56ab8203dc..a985ee48dc 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -1692,14 +1692,9 @@ where tracing::debug!("bond ∆ at epoch {}: {}", ep, delta); } } - let stake_at_pipeline = - read_validator_stake(storage, ¶ms, validator, pipeline_epoch)? - .unwrap_or_default() - .change(); - let token_change = cmp::min(amount_after_slashing, stake_at_pipeline); tracing::debug!( "Token change including slashes on unbond = {}", - token_change + -amount_after_slashing ); // Update the validator set at the pipeline offset. Since unbonding from a @@ -1716,7 +1711,7 @@ where storage, ¶ms, validator, - -token_change, + -amount_after_slashing, current_epoch, )?; } @@ -1726,14 +1721,14 @@ where storage, ¶ms, validator, - -token_change, + -amount_after_slashing, current_epoch, params.pipeline_len, )?; update_total_deltas( storage, ¶ms, - -token_change, + -amount_after_slashing, current_epoch, params.pipeline_len, )?; diff --git a/proof_of_stake/src/tests/state_machine.rs b/proof_of_stake/src/tests/state_machine.rs index 48723248cd..be940f62bc 100644 --- a/proof_of_stake/src/tests/state_machine.rs +++ b/proof_of_stake/src/tests/state_machine.rs @@ -919,7 +919,7 @@ impl ConcretePosState { current_epoch, current_epoch + params.pipeline_len, ) { - // println!("Epoch {epoch}"); + tracing::debug!("Epoch {epoch}"); let mut vals = HashSet::
::new(); for WeightedValidator { bonded_stake, @@ -2015,19 +2015,21 @@ impl AbstractPosState { .unwrap() .get(&id.validator) .unwrap(); - let pipeline_stake = self - .validator_stakes - .get(&self.pipeline()) - .unwrap() - .get(&id.validator) - .unwrap(); - println!("pipeline stake = {}", pipeline_stake); - let token_change = cmp::min(*pipeline_stake, amount_after_slashing); + // let pipeline_stake = self + // .validator_stakes + // .get(&self.pipeline()) + // .unwrap() + // .get(&id.validator) + // .unwrap(); + // let token_change = cmp::min(*pipeline_stake, amount_after_slashing); if *pipeline_state != ValidatorState::Jailed { - self.update_validator_sets(&id.validator, -token_change); + self.update_validator_sets(&id.validator, -amount_after_slashing); } - self.update_validator_total_stake(&id.validator, -token_change); + self.update_validator_total_stake( + &id.validator, + -amount_after_slashing, + ); } /// Update validator's total stake with bonded or unbonded change at the From 424abc444c4408f4ad733d96d9f44bcd51944ebe Mon Sep 17 00:00:00 2001 From: brentstone Date: Thu, 1 Jun 2023 16:40:37 -0400 Subject: [PATCH 31/31] fix wasm tx tests --- wasm/wasm_source/src/tx_unbond.rs | 12 ++++++++---- wasm/wasm_source/src/tx_withdraw.rs | 23 +++++++++++++++++------ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/wasm/wasm_source/src/tx_unbond.rs b/wasm/wasm_source/src/tx_unbond.rs index 33cf9f56ea..46107b4225 100644 --- a/wasm/wasm_source/src/tx_unbond.rs +++ b/wasm/wasm_source/src/tx_unbond.rs @@ -343,10 +343,12 @@ mod tests { // }; // Ensure that the unbond is structured as expected, withdrawable at - // pipeline + unbonding offsets + // pipeline + unbonding + cubic_slash_window offsets let actual_unbond_amount = unbond_handle .at(&Epoch::from( - pos_params.pipeline_len + pos_params.unbonding_len, + pos_params.pipeline_len + + pos_params.unbonding_len + + pos_params.cubic_slashing_window_length, )) .get(ctx(), &start_epoch)?; assert_eq!( @@ -356,8 +358,10 @@ mod tests { unbonded amount" ); - for epoch in - start_epoch.0..(pos_params.pipeline_len + pos_params.unbonding_len) + for epoch in start_epoch.0 + ..(pos_params.pipeline_len + + pos_params.unbonding_len + + pos_params.cubic_slashing_window_length) { let bond_amount = bond_handle.get_sum(ctx(), Epoch(epoch), &pos_params)?; diff --git a/wasm/wasm_source/src/tx_withdraw.rs b/wasm/wasm_source/src/tx_withdraw.rs index b00661261e..68f13c151f 100644 --- a/wasm/wasm_source/src/tx_withdraw.rs +++ b/wasm/wasm_source/src/tx_withdraw.rs @@ -132,10 +132,14 @@ mod tests { tx_host_env::commit_tx_and_block(); - // Fast forward to pipeline + unbonding offset epoch so that it's - // possible to withdraw the unbonded tokens + // Fast forward to pipeline + unbonding + cubic_slashing_window_length + // offset epoch so that it's possible to withdraw the unbonded + // tokens tx_host_env::with(|env| { - for _ in 0..(pos_params.pipeline_len + pos_params.unbonding_len) { + for _ in 0..(pos_params.pipeline_len + + pos_params.unbonding_len + + pos_params.cubic_slashing_window_length) + { env.wl_storage.storage.block.epoch = env.wl_storage.storage.block.epoch.next(); } @@ -145,12 +149,19 @@ mod tests { } else { Epoch::default() }; - let withdraw_epoch = - Epoch(pos_params.pipeline_len + pos_params.unbonding_len); + let withdraw_epoch = Epoch( + pos_params.pipeline_len + + pos_params.unbonding_len + + pos_params.cubic_slashing_window_length, + ); assert_eq!( tx_host_env::with(|env| env.wl_storage.storage.block.epoch), - Epoch(pos_params.pipeline_len + pos_params.unbonding_len) + Epoch( + pos_params.pipeline_len + + pos_params.unbonding_len + + pos_params.cubic_slashing_window_length + ) ); let tx_code = vec![];