diff --git a/Cargo.lock b/Cargo.lock index 96a3b66044..d84b439dad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,15 +83,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anstream" version = "0.3.2" @@ -1583,16 +1574,6 @@ dependencies = [ "sct 0.6.1", ] -[[package]] -name = "ctor" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" -dependencies = [ - "quote", - "syn 1.0.109", -] - [[package]] name = "ctr" version = "0.9.2" @@ -4239,18 +4220,21 @@ dependencies = [ name = "namada_proof_of_stake" version = "0.23.0" dependencies = [ + "assert_matches", "borsh 1.0.0-alpha.4", "data-encoding", "derivative", "itertools 0.10.5", "namada_core", "once_cell", + "pretty_assertions", "proptest", "proptest-state-machine", "test-log", "thiserror", "tracing 0.1.37", "tracing-subscriber", + "yansi", ] [[package]] @@ -4790,15 +4774,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "output_vt100" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" -dependencies = [ - "winapi", -] - [[package]] name = "overload" version = "0.1.1" @@ -5145,14 +5120,12 @@ dependencies = [ [[package]] name = "pretty_assertions" -version = "0.7.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cab0e7c02cf376875e9335e0ba1da535775beb5450d21e1dffca068818ed98b" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" dependencies = [ - "ansi_term", - "ctor", "diff", - "output_vt100", + "yansi", ] [[package]] @@ -8324,6 +8297,12 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "zcash_encoding" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 141006744b..074be6bdab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -102,7 +102,7 @@ num-traits = "0.2.14" once_cell = "1.8.0" orion = "0.16.0" paste = "1.0.9" -pretty_assertions = "0.7.2" +pretty_assertions = "1.4.0" primitive-types = "0.12.1" proptest = "1.2.0" proptest-state-machine = "0.1.0" @@ -147,7 +147,8 @@ tracing-log = "0.1.2" tracing-subscriber = {version = "0.3.7", default-features = false, features = ["env-filter", "fmt"]} wasmparser = "0.107.0" winapi = "0.3.9" -zeroize = {version = "1.5.5", features = ["zeroize_derive"]} +yansi = "0.5.1" +zeroize = { version = "1.5.5", features = ["zeroize_derive"] } [profile.release] lto = true diff --git a/Makefile b/Makefile index e487847277..23669c8d42 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,9 @@ NAMADA_E2E_DEBUG ?= true RUST_BACKTRACE ?= 1 NAMADA_MASP_TEST_SEED ?= 0 PROPTEST_CASES ?= 100 +# Disable shrinking in `make test-pos-sm` for CI runs. If the test fail in CI, +# we only want to get the seed. +PROPTEST_MAX_SHRINK_ITERS ?= 0 cargo := $(env) cargo rustup := $(env) rustup @@ -211,11 +214,13 @@ test-debug: test-benches: $(cargo) +$(nightly) test --package namada_benchmarks --benches -# Run PoS state machine tests +# Run PoS state machine tests with shrinking disabled by default (can be +# overriden with `PROPTEST_MAX_SHRINK_ITERS`) test-pos-sm: cd proof_of_stake && \ - RUST_BACKTRACE=1 \ + RUST_BACKTRACE=1 \ PROPTEST_CASES=$(PROPTEST_CASES) \ + PROPTEST_MAX_SHRINK_ITERS=$(PROPTEST_MAX_SHRINK_ITERS) \ RUSTFLAGS='-C debuginfo=2 -C debug-assertions=true -C overflow-checks=true' \ cargo test pos_state_machine_test --release diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 421ada0e69..960555e385 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -228,6 +228,7 @@ pub mod cmds { .subcommand(Bond::def().display_order(2)) .subcommand(Unbond::def().display_order(2)) .subcommand(Withdraw::def().display_order(2)) + .subcommand(Redelegate::def().display_order(2)) .subcommand(TxCommissionRateChange::def().display_order(2)) // Ethereum bridge transactions .subcommand(AddToEthBridgePool::def().display_order(3)) @@ -285,6 +286,7 @@ pub mod cmds { let bond = Self::parse_with_ctx(matches, Bond); let unbond = Self::parse_with_ctx(matches, Unbond); let withdraw = Self::parse_with_ctx(matches, Withdraw); + let redelegate = Self::parse_with_ctx(matches, Redelegate); let query_epoch = Self::parse_with_ctx(matches, QueryEpoch); let query_account = Self::parse_with_ctx(matches, QueryAccount); let query_transfers = Self::parse_with_ctx(matches, QueryTransfers); @@ -328,6 +330,7 @@ pub mod cmds { .or(bond) .or(unbond) .or(withdraw) + .or(redelegate) .or(add_to_eth_bridge_pool) .or(tx_update_steward_commission) .or(tx_resign_steward) @@ -402,6 +405,7 @@ pub mod cmds { Bond(Bond), Unbond(Unbond), Withdraw(Withdraw), + Redelegate(Redelegate), AddToEthBridgePool(AddToEthBridgePool), TxUpdateStewardCommission(TxUpdateStewardCommission), TxResignSteward(TxResignSteward), @@ -1425,6 +1429,27 @@ pub mod cmds { } } + #[derive(Clone, Debug)] + pub struct Redelegate(pub args::Redelegate); + + impl SubCmd for Redelegate { + const CMD: &'static str = "redelegate"; + + fn parse(matches: &ArgMatches) -> Option { + matches + .subcommand_matches(Self::CMD) + .map(|matches| Redelegate(args::Redelegate::parse(matches))) + } + + fn def() -> App { + App::new(Self::CMD) + .about( + "Redelegate bonded tokens from one validator to another.", + ) + .add_args::>() + } + } + #[derive(Clone, Debug)] pub struct QueryEpoch(pub args::Query); @@ -2552,6 +2577,7 @@ pub mod args { pub const TX_TRANSFER_WASM: &str = "tx_transfer.wasm"; pub const TX_UNBOND_WASM: &str = "tx_unbond.wasm"; pub const TX_UNJAIL_VALIDATOR_WASM: &str = "tx_unjail_validator.wasm"; + pub const TX_REDELEGATE_WASM: &str = "tx_redelegate.wasm"; pub const TX_UPDATE_VP_WASM: &str = "tx_update_vp.wasm"; pub const TX_UPDATE_STEWARD_COMMISSION: &str = "tx_update_steward_commission.wasm"; @@ -2582,7 +2608,7 @@ pub mod args { arg_default( "pool-gas-amount", DefaultFn(|| token::DenominatedAmount { - amount: token::Amount::default(), + amount: token::Amount::zero(), denom: NATIVE_MAX_DECIMAL_PLACES.into(), }), ); @@ -2615,6 +2641,8 @@ pub mod args { pub const DATA_PATH: Arg = arg("data-path"); pub const DECRYPT: ArgFlag = flag("decrypt"); pub const DISPOSABLE_SIGNING_KEY: ArgFlag = flag("disposable-gas-payer"); + pub const DESTINATION_VALIDATOR: Arg = + arg("destination-validator"); pub const DONT_ARCHIVE: ArgFlag = flag("dont-archive"); pub const DONT_PREFETCH_WASM: ArgFlag = flag("dont-prefetch-wasm"); pub const DRY_RUN_TX: ArgFlag = flag("dry-run"); @@ -2719,6 +2747,7 @@ pub mod args { pub const SOURCE: Arg = arg("source"); pub const SOURCE_OPT: ArgOpt = SOURCE.opt(); pub const STEWARD: Arg = arg("steward"); + pub const SOURCE_VALIDATOR: Arg = arg("source-validator"); pub const STORAGE_KEY: Arg = arg("storage-key"); pub const SUSPEND_ACTION: ArgFlag = flag("suspend"); pub const TIMEOUT_HEIGHT: ArgOpt = arg_opt("timeout-height"); @@ -4019,6 +4048,63 @@ pub mod args { } } + impl CliToSdk> for Redelegate { + fn to_sdk(self, ctx: &mut Context) -> Redelegate { + Redelegate:: { + tx: self.tx.to_sdk(ctx), + src_validator: ctx.get(&self.src_validator), + dest_validator: ctx.get(&self.dest_validator), + owner: ctx.get(&self.owner), + amount: self.amount, + tx_code_path: self.tx_code_path.to_path_buf(), + } + } + } + + impl Args for Redelegate { + fn parse(matches: &ArgMatches) -> Self { + let tx = Tx::parse(matches); + let src_validator = SOURCE_VALIDATOR.parse(matches); + let dest_validator = DESTINATION_VALIDATOR.parse(matches); + let owner = OWNER.parse(matches); + let amount = AMOUNT.parse(matches); + let amount = amount + .canonical() + .increase_precision(NATIVE_MAX_DECIMAL_PLACES.into()) + .unwrap_or_else(|e| { + println!("Could not parse bond amount: {:?}", e); + safe_exit(1); + }) + .amount; + let tx_code_path = PathBuf::from(TX_REDELEGATE_WASM); + Self { + tx, + src_validator, + dest_validator, + owner, + amount, + tx_code_path, + } + } + + fn def(app: App) -> App { + app.add_args::>() + .arg( + SOURCE_VALIDATOR + .def() + .help("Source validator address for the redelegation."), + ) + .arg(DESTINATION_VALIDATOR.def().help( + "Destination validator address for the redelegation.", + )) + .arg(OWNER.def().help( + "Delegator (owner) address of the bonds that are being \ + redelegated.", + )) + .arg(AMOUNT.def().help("Amount of tokens to redelegate.")) + } + } + impl CliToSdk> for InitProposal { fn to_sdk(self, ctx: &mut Context) -> InitProposal { InitProposal:: { diff --git a/apps/src/lib/cli/client.rs b/apps/src/lib/cli/client.rs index 5d76a90cf5..2e6685ea5f 100644 --- a/apps/src/lib/cli/client.rs +++ b/apps/src/lib/cli/client.rs @@ -194,6 +194,17 @@ impl CliApi { let namada = ctx.to_sdk(&client, io); tx::submit_withdraw(&namada, args).await?; } + Sub::Redelegate(Redelegate(mut args)) => { + let client = client.unwrap_or_else(|| { + C::from_tendermint_address( + &mut args.tx.ledger_address, + ) + }); + client.wait_until_node_is_synced(io).await?; + let args = args.to_sdk(&mut ctx); + let namada = ctx.to_sdk(&client, io); + tx::submit_redelegate(&namada, args).await?; + } Sub::TxCommissionRateChange(TxCommissionRateChange( mut args, )) => { diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index d727a56f0b..acf2a7a2cd 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -31,7 +31,8 @@ use namada::core::ledger::pgf::parameters::PgfParameters; use namada::core::ledger::pgf::storage::steward::StewardDetail; use namada::ledger::events::Event; use namada::ledger::parameters::{storage as param_storage, EpochDuration}; -use namada::ledger::pos::{CommissionPair, PosParams, Slash}; +use namada::ledger::pos::types::{CommissionPair, Slash}; +use namada::ledger::pos::PosParams; use namada::ledger::queries::RPC; use namada::ledger::storage::ConversionState; use namada::proof_of_stake::types::{ValidatorState, WeightedValidator}; @@ -1450,7 +1451,7 @@ pub async fn query_and_print_unbonds<'a>( query_unbond_with_slashing(context.client(), source, validator).await; let current_epoch = query_epoch(context.client()).await.unwrap(); - let mut total_withdrawable = token::Amount::default(); + let mut total_withdrawable = token::Amount::zero(); let mut not_yet_withdrawable = HashMap::::new(); for ((_start_epoch, withdraw_epoch), amount) in unbonds.into_iter() { if withdraw_epoch <= current_epoch { @@ -1461,7 +1462,7 @@ pub async fn query_and_print_unbonds<'a>( *withdrawable_amount += amount; } } - if total_withdrawable != token::Amount::default() { + if !total_withdrawable.is_zero() { display_line!( context.io(), "Total withdrawable now: {}.", @@ -1537,7 +1538,7 @@ pub async fn query_bonds<'a>( bond.amount.to_string_native() )?; } - if details.bonds_total != token::Amount::zero() { + if !details.bonds_total.is_zero() { display_line!( context.io(), &mut w; @@ -2331,13 +2332,12 @@ pub async fn get_bond_amount_at( validator: &Address, epoch: Epoch, ) -> Option { - let (_total, total_active) = - unwrap_client_response::( - RPC.vp() - .pos() - .bond_with_slashing(client, delegator, validator, &Some(epoch)) - .await, - ); + let total_active = unwrap_client_response::( + RPC.vp() + .pos() + .bond_with_slashing(client, delegator, validator, &Some(epoch)) + .await, + ); Some(total_active) } diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 8215eaa078..7ec1c49d59 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -955,6 +955,27 @@ where Ok(()) } +pub async fn submit_redelegate<'a, N: Namada<'a>>( + namada: &N, + args: args::Redelegate, +) -> Result<(), error::Error> +where + ::Error: std::fmt::Display, +{ + let (mut tx, signing_data) = args.build(namada).await?; + signing::generate_test_vector(namada, &tx).await?; + + if args.tx.dump_tx { + tx::dump_tx(namada.io(), &args.tx, tx); + } else { + namada.sign(&mut tx, &args.tx, signing_data).await?; + + namada.submit(tx, &args.tx).await?; + } + + Ok(()) +} + pub async fn submit_validator_commission_change<'a, N: Namada<'a>>( namada: &N, args: args::CommissionRateChange, diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index c6d03a7f47..fd34935692 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -723,14 +723,12 @@ where let reward = fractional_claim * inflation; // Get validator data at the last epoch - let stake = read_validator_stake( + let stake = Dec::from(read_validator_stake( &self.wl_storage, ¶ms, &address, last_epoch, - )? - .map(Dec::from) - .unwrap_or_default(); + )?); let last_rewards_product = validator_rewards_products_handle(&address) .get(&self.wl_storage, &last_epoch)? @@ -1056,7 +1054,6 @@ mod test_finalize_block { use namada::ledger::pos::PosQueries; use namada::ledger::storage_api; use namada::ledger::storage_api::StorageWrite; - use namada::proof_of_stake::btree_set::BTreeSetShims; use namada::proof_of_stake::storage::{ is_validator_slashes_key, slashes_prefix, }; @@ -1900,7 +1897,6 @@ mod test_finalize_block { validator, Epoch::default(), ) - .unwrap() .unwrap(); let votes = vec![VoteInfo { @@ -1984,10 +1980,10 @@ mod test_finalize_block { let params = read_pos_params(&shell.wl_storage).unwrap(); - let val1 = validator_set.pop_first_shim().unwrap(); - let val2 = validator_set.pop_first_shim().unwrap(); - let val3 = validator_set.pop_first_shim().unwrap(); - let val4 = validator_set.pop_first_shim().unwrap(); + let val1 = validator_set.pop_first().unwrap(); + let val2 = validator_set.pop_first().unwrap(); + let val3 = validator_set.pop_first().unwrap(); + let val4 = validator_set.pop_first().unwrap(); let get_pkh = |address, epoch| { let ck = validator_consensus_key_handle(&address) @@ -2821,15 +2817,13 @@ mod test_finalize_block { ¶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, @@ -2920,21 +2914,35 @@ mod test_finalize_block { ¶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 = cubic_rate * initial_stake; - assert_eq!(stake1, initial_stake - expected_slashed); - assert_eq!(stake2, initial_stake - expected_slashed); + let expected_slashed = initial_stake.mul_ceil(cubic_rate); + + println!( + "Initial stake = {}\nCubic rate = {}\nExpected slashed = {}\n", + initial_stake.to_string_native(), + cubic_rate, + expected_slashed.to_string_native() + ); + + assert!( + (stake1.change() - (initial_stake - expected_slashed).change()) + .abs() + <= 1.into() + ); + assert!( + (stake2.change() - (initial_stake - expected_slashed).change()) + .abs() + <= 1.into() + ); assert_eq!(total_stake, total_initial_stake - 2u64 * expected_slashed); // Unjail one of the validators @@ -3009,7 +3017,6 @@ mod test_finalize_block { /// 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, @@ -3037,7 +3044,7 @@ mod test_finalize_block { .read(&slash_balance_key) .expect("must be able to read") .unwrap_or_default(); - debug_assert_eq!(slash_pool_balance_init, token::Amount::default()); + debug_assert_eq!(slash_pool_balance_init, token::Amount::zero()); let consensus_set: Vec = read_consensus_validator_set_addresses_with_stake( @@ -3099,6 +3106,7 @@ mod test_finalize_block { &val1.address, self_unbond_1_amount, current_epoch, + false, ) .unwrap(); @@ -3108,8 +3116,7 @@ mod test_finalize_block { &val1.address, current_epoch + params.pipeline_len, ) - .unwrap() - .unwrap_or_default(); + .unwrap(); let total_stake = namada_proof_of_stake::read_total_stake( &shell.wl_storage, @@ -3142,6 +3149,7 @@ mod test_finalize_block { &val1.address, del_unbond_1_amount, current_epoch, + false, ) .unwrap(); @@ -3151,8 +3159,7 @@ mod test_finalize_block { &val1.address, current_epoch + params.pipeline_len, ) - .unwrap() - .unwrap_or_default(); + .unwrap(); let total_stake = namada_proof_of_stake::read_total_stake( &shell.wl_storage, ¶ms, @@ -3207,6 +3214,7 @@ mod test_finalize_block { &val1.address, self_unbond_2_amount, current_epoch, + false, ) .unwrap(); @@ -3383,8 +3391,7 @@ mod test_finalize_block { &val1.address, Epoch(10), ) - .unwrap() - .unwrap_or_default(); + .unwrap(); assert_eq!( pre_stake_10, initial_stake + del_1_amount @@ -3417,16 +3424,14 @@ mod test_finalize_block { &val1.address, Epoch(3), ) - .unwrap() - .unwrap_or_default(); + .unwrap(); let val_stake_4 = namada_proof_of_stake::read_validator_stake( &shell.wl_storage, ¶ms, &val1.address, Epoch(4), ) - .unwrap() - .unwrap_or_default(); + .unwrap(); let tot_stake_3 = namada_proof_of_stake::read_total_stake( &shell.wl_storage, @@ -3468,31 +3473,33 @@ mod test_finalize_block { // 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( + let post_stake_10 = read_validator_stake( &shell.wl_storage, ¶ms, &val1.address, Epoch(10), ) - .unwrap() - .unwrap_or_default(); + .unwrap(); // 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(Dec::one(), Dec::two() * cubic_rate); - let exp_slashed_during_processing_9 = slash_rate_3 - * (initial_stake + del_1_amount - - self_unbond_1_amount - - del_unbond_1_amount - + self_bond_1_amount - - self_unbond_2_amount); + let exp_slashed_during_processing_9 = (initial_stake + del_1_amount + - self_unbond_1_amount + - del_unbond_1_amount + + self_bond_1_amount + - self_unbond_2_amount) + .mul_ceil(slash_rate_3); assert!( ((pre_stake_10 - post_stake_10).change() - exp_slashed_during_processing_9.change()) .abs() - < Uint::from(1000) + < Uint::from(1000), + "Expected {}, got {} (with less than 1000 err)", + exp_slashed_during_processing_9.to_string_native(), + (pre_stake_10 - post_stake_10).to_string_native(), ); // Check that we can compute the stake at the pipeline epoch @@ -3509,7 +3516,11 @@ mod test_finalize_block { assert!( exp_pipeline_stake.abs_diff(&Dec::from(post_stake_10)) - <= Dec::new(1, NATIVE_MAX_DECIMAL_PLACES).unwrap() + <= Dec::new(2, NATIVE_MAX_DECIMAL_PLACES).unwrap(), + "Expected {}, got {} (with less than 2 err), diff {}", + exp_pipeline_stake, + post_stake_10.to_string_native(), + exp_pipeline_stake.abs_diff(&Dec::from(post_stake_10)), ); // Check the balance of the Slash Pool @@ -3526,15 +3537,6 @@ mod test_finalize_block { // ); // 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( @@ -3553,7 +3555,7 @@ mod test_finalize_block { // .unwrap_or_default(); // let exp_slashed_4 = if dec!(2) * cubic_rate >= Decimal::ONE { - // token::Amount::default() + // token::Amount::zero() // } else if dec!(3) * cubic_rate >= Decimal::ONE { // decimal_mult_amount( // Decimal::ONE - dec!(2) * cubic_rate, @@ -3578,19 +3580,27 @@ mod test_finalize_block { ¶ms, &val1.address, current_epoch + params.pipeline_len, - )? - .unwrap_or_default(); + )?; - let post_stake_11 = namada_proof_of_stake::read_validator_stake( + let post_stake_10 = read_validator_stake( &shell.wl_storage, ¶ms, &val1.address, Epoch(10), ) - .unwrap() - .unwrap_or_default(); + .unwrap(); + + // Stake at current epoch should be equal to stake at pipeline + assert_eq!( + post_stake_10, + val_stake, + "Stake at pipeline in epoch {} ({}) expected to be equal to stake \ + in epoch 10 ({}).", + current_epoch + params.pipeline_len, + val_stake.to_string_native(), + post_stake_10.to_string_native() + ); - assert_eq!(post_stake_11, val_stake); // dbg!(&val_stake); // dbg!(pre_stake_10 - post_stake_10); @@ -3734,12 +3744,16 @@ mod test_finalize_block { self_details.unbonds[1].amount, self_unbond_2_amount - self_bond_1_amount ); - assert_eq!( - self_details.unbonds[1].slashed_amount, - Some( - std::cmp::min(Dec::one(), Dec::new(3, 0).unwrap() * cubic_rate) - * (self_unbond_2_amount - self_bond_1_amount) - ) + let rate = + std::cmp::min(Dec::one(), Dec::new(3, 0).unwrap() * cubic_rate); + assert!( + // at most off by 1 + (self_details.unbonds[1].slashed_amount.unwrap().change() + - (self_unbond_2_amount - self_bond_1_amount) + .mul_ceil(rate) + .change()) + .abs() + <= Uint::from(1) ); assert_eq!(self_details.unbonds[2].amount, self_bond_1_amount); assert_eq!(self_details.unbonds[2].slashed_amount, None); @@ -3757,10 +3771,12 @@ mod test_finalize_block { .unwrap(); let exp_del_withdraw_slashed_amount = - slash_rate_3 * del_unbond_1_amount; - assert_eq!( - del_withdraw, - del_unbond_1_amount - exp_del_withdraw_slashed_amount + del_unbond_1_amount.mul_ceil(slash_rate_3); + assert!( + (del_withdraw + - (del_unbond_1_amount - exp_del_withdraw_slashed_amount)) + .raw_amount() + <= Uint::one() ); // TODO: finish once implemented diff --git a/apps/src/lib/node/ledger/shell/governance.rs b/apps/src/lib/node/ledger/shell/governance.rs index cc79c9a9f0..1bcad1daaa 100644 --- a/apps/src/lib/node/ledger/shell/governance.rs +++ b/apps/src/lib/node/ledger/shell/governance.rs @@ -233,7 +233,7 @@ where source: delegator.clone(), validator: validator.clone(), }; - let (_, delegator_stake) = + let delegator_stake = bond_amount(storage, &bond_id, epoch).unwrap_or_default(); delegators_vote.insert(delegator.clone(), vote_data.into()); diff --git a/apps/src/lib/node/ledger/shell/prepare_proposal.rs b/apps/src/lib/node/ledger/shell/prepare_proposal.rs index 84bc468ed9..d8f7d21de7 100644 --- a/apps/src/lib/node/ledger/shell/prepare_proposal.rs +++ b/apps/src/lib/node/ledger/shell/prepare_proposal.rs @@ -500,7 +500,6 @@ mod test_prepare_proposal { use namada::ledger::gas::Gas; use namada::ledger::pos::PosQueries; use namada::ledger::replay_protection; - use namada::proof_of_stake::btree_set::BTreeSetShims; use namada::proof_of_stake::types::WeightedValidator; use namada::proof_of_stake::{ consensus_validator_set_handle, @@ -916,8 +915,8 @@ mod test_prepare_proposal { .unwrap() .into_iter() .collect(); - let val1 = consensus_set.pop_first_shim().unwrap(); - let val2 = consensus_set.pop_first_shim().unwrap(); + let val1 = consensus_set.pop_first().unwrap(); + let val2 = consensus_set.pop_first().unwrap(); let pkh1 = get_pkh_from_address( &shell.wl_storage, ¶ms, diff --git a/benches/lib.rs b/benches/lib.rs index 1d07f2e250..b4b285f6db 100644 --- a/benches/lib.rs +++ b/benches/lib.rs @@ -114,12 +114,17 @@ pub const TX_TRANSFER_WASM: &str = "tx_transfer.wasm"; pub const TX_UPDATE_ACCOUNT_WASM: &str = "tx_update_account.wasm"; pub const TX_VOTE_PROPOSAL_WASM: &str = "tx_vote_proposal.wasm"; pub const TX_UNBOND_WASM: &str = "tx_unbond.wasm"; +pub const TX_REDELEGATE_WASM: &str = "tx_redelegate.wasm"; pub const TX_INIT_PROPOSAL_WASM: &str = "tx_init_proposal.wasm"; pub const TX_REVEAL_PK_WASM: &str = "tx_reveal_pk.wasm"; pub const TX_CHANGE_VALIDATOR_COMMISSION_WASM: &str = "tx_change_validator_commission.wasm"; pub const TX_IBC_WASM: &str = "tx_ibc.wasm"; pub const TX_UNJAIL_VALIDATOR_WASM: &str = "tx_unjail_validator.wasm"; +pub const TX_WITHDRAW_WASM: &str = "tx_withdraw.wasm"; +pub const TX_INIT_ACCOUNT_WASM: &str = "tx_init_account.wasm"; +pub const TX_INIT_VALIDATOR_WASM: &str = "tx_init_validator.wasm"; + pub const VP_VALIDATOR_WASM: &str = "vp_validator.wasm"; pub const ALBERT_PAYMENT_ADDRESS: &str = "albert_payment"; diff --git a/benches/txs.rs b/benches/txs.rs index a1373c7931..d5d60f8eae 100644 --- a/benches/txs.rs +++ b/benches/txs.rs @@ -20,24 +20,24 @@ use namada::types::storage::Key; use namada::types::transaction::governance::{ InitProposalData, VoteProposalData, }; -use namada::types::transaction::pos::{Bond, CommissionChange, Withdraw}; +use namada::types::transaction::pos::{ + Bond, CommissionChange, Redelegation, Withdraw, +}; use namada::types::transaction::EllipticCurve; use namada_apps::wallet::defaults; use namada_benches::{ generate_ibc_transfer_tx, generate_tx, BenchShell, BenchShieldedCtx, ALBERT_PAYMENT_ADDRESS, ALBERT_SPENDING_KEY, BERTHA_PAYMENT_ADDRESS, - TX_BOND_WASM, TX_CHANGE_VALIDATOR_COMMISSION_WASM, TX_INIT_PROPOSAL_WASM, + TX_BOND_WASM, TX_CHANGE_VALIDATOR_COMMISSION_WASM, TX_INIT_ACCOUNT_WASM, + TX_INIT_PROPOSAL_WASM, TX_INIT_VALIDATOR_WASM, TX_REDELEGATE_WASM, TX_REVEAL_PK_WASM, TX_UNBOND_WASM, TX_UNJAIL_VALIDATOR_WASM, - TX_UPDATE_ACCOUNT_WASM, TX_VOTE_PROPOSAL_WASM, VP_VALIDATOR_WASM, + TX_UPDATE_ACCOUNT_WASM, TX_VOTE_PROPOSAL_WASM, TX_WITHDRAW_WASM, + VP_VALIDATOR_WASM, }; use rand::rngs::StdRng; use rand::SeedableRng; use sha2::Digest; -const TX_WITHDRAW_WASM: &str = "tx_withdraw.wasm"; -const TX_INIT_ACCOUNT_WASM: &str = "tx_init_account.wasm"; -const TX_INIT_VALIDATOR_WASM: &str = "tx_init_validator.wasm"; - // TODO: need to benchmark tx_bridge_pool.wasm fn transfer(c: &mut Criterion) { let mut group = c.benchmark_group("transfer"); @@ -286,6 +286,43 @@ fn withdraw(c: &mut Criterion) { group.finish(); } +fn redelegate(c: &mut Criterion) { + let mut group = c.benchmark_group("redelegate"); + + let redelegation = |dest_validator| { + generate_tx( + TX_REDELEGATE_WASM, + Redelegation { + src_validator: defaults::validator_address(), + dest_validator, + owner: defaults::albert_address(), + amount: Amount::from(1), + }, + None, + None, + Some(&defaults::albert_keypair()), + ) + }; + + group.bench_function("redelegate", |b| { + b.iter_batched_ref( + || { + let shell = BenchShell::default(); + // Find the other genesis validator + let current_epoch = shell.wl_storage.get_block_epoch().unwrap(); + let validators = namada::proof_of_stake::read_consensus_validator_set_addresses(&shell.inner.wl_storage, current_epoch).unwrap(); + let validator_2 = validators.into_iter().find(|addr| addr != &defaults::validator_address()).expect("There must be another validator to redelegate to"); + // Prepare the redelegation tx + (shell, redelegation(validator_2)) + }, + |(shell, tx)| shell.execute_tx(tx), + criterion::BatchSize::LargeInput, + ) + }); + + group.finish(); +} + fn reveal_pk(c: &mut Criterion) { let mut csprng = rand::rngs::OsRng {}; let new_implicit_account: common::SecretKey = @@ -687,6 +724,7 @@ criterion_group!( bond, unbond, withdraw, + redelegate, reveal_pk, update_vp, init_account, diff --git a/core/src/ledger/storage_api/collections/lazy_map.rs b/core/src/ledger/storage_api/collections/lazy_map.rs index 4f9aeb426d..8d30e2052c 100644 --- a/core/src/ledger/storage_api/collections/lazy_map.rs +++ b/core/src/ledger/storage_api/collections/lazy_map.rs @@ -1,6 +1,6 @@ //! Lazy map. -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::fmt::Debug; use std::hash::Hash; use std::marker::PhantomData; @@ -101,6 +101,107 @@ pub enum ValidationError { InvalidNestedSubKey(storage::Key), } +// pub trait EagerMapFromIter { +// fn from_iter(iter: I) -> Self +// where +// I: IntoIterator; +// } + +// impl EagerMapFromIter for HashMap { +// fn from_iter(iter: I) -> Self +// where +// I: IntoIterator, +// { +// iter.into_iter().collect() +// } +// } + +// impl EagerMapFromIter for BTreeMap { +// fn from_iter(iter: I) -> Self +// where +// K: Eq + Hash + Ord, +// I: IntoIterator, +// { +// iter.into_iter().collect() +// } +// } + +/// Trait used to facilitate collection of lazy maps into eager maps +pub trait Collectable { + /// The type of the value of the lazy map + type Collected; + + /// Collect the lazy map into an eager map + fn collect_map( + &self, + storage: &S, + ) -> storage_api::Result; +} + +impl Collectable for LazyMap +where + K: Hash + Eq + Clone + Debug + storage::KeySeg + Ord, + V: Collectable + LazyCollection + Debug, +{ + type Collected = BTreeMap; + + fn collect_map( + &self, + storage: &S, + ) -> storage_api::Result + where + S: StorageRead, + { + let mut map = BTreeMap::::new(); + for res in self.iter(storage)? { + let ( + NestedSubKey::Data { + key, + nested_sub_key: _, + }, + _, + ) = res?; + let next_layer = self.at(&key).collect_map(storage)?; + map.insert(key, next_layer); + } + Ok(map) + } +} + +impl Collectable for LazyMap +where + K: Hash + Eq + Clone + Debug + storage::KeySeg + Ord, + V: BorshSerialize + BorshDeserialize + Clone + Debug + 'static, +{ + type Collected = BTreeMap; + + fn collect_map( + &self, + storage: &S, + ) -> storage_api::Result + where + S: StorageRead, + { + let mut map = BTreeMap::::new(); + for res in self.iter(storage)? { + let (key, value) = res?; + map.insert(key, value); + } + Ok(map) + } +} + +// impl Collectable for V { +// type Collected = Self; + +// fn collect_map( +// &self, +// _storage: &S, +// ) -> storage_api::Result { +// Ok(self.clone()) +// } +// } + /// [`LazyMap`] validation result pub type ValidationResult = std::result::Result; @@ -359,14 +460,6 @@ impl LazyMap where K: storage::KeySeg, { - /// Returns whether the set contains a value. - pub fn contains(&self, storage: &S, key: &K) -> Result - where - S: StorageRead, - { - storage.has_key(&self.get_data_key(key)) - } - /// Get the prefix of set's elements storage fn get_data_prefix(&self) -> storage::Key { self.key.push(&DATA_SUBKEY.to_owned()).unwrap() @@ -392,6 +485,16 @@ where V::open(self.get_data_key(key)) } + /// Returns whether the nested map contains a certain key with data inside. + pub fn contains(&self, storage: &S, key: &K) -> Result + where + S: StorageRead, + { + let prefix = self.get_data_key(key); + let mut iter = storage_api::iter_prefix_bytes(storage, &prefix)?; + Ok(iter.next().is_some()) + } + /// Remove all map entries at a given key prefix pub fn remove_all(&self, storage: &mut S, key: &K) -> Result where @@ -505,6 +608,28 @@ where Self::read_key_val(storage, &data_key) } + /// Update a value at the given key with the given function. If no existing + /// value exists, the closure's argument will be `None`. + pub fn update(&self, storage: &mut S, key: K, f: F) -> Result<()> + where + S: StorageWrite + StorageRead, + F: FnOnce(Option) -> V, + { + let data_key = self.get_data_key(&key); + let current = Self::read_key_val(storage, &data_key)?; + let new = f(current); + Self::write_key_val(storage, &data_key, new)?; + Ok(()) + } + + /// Returns whether the map contains a key with a value. + pub fn contains(&self, storage: &S, key: &K) -> Result + where + S: StorageRead, + { + storage.has_key(&self.get_data_key(key)) + } + /// Returns whether the map contains no elements. pub fn is_empty(&self, storage: &S) -> Result where @@ -553,6 +678,19 @@ where })) } + // /// Collect the lazy map into an eager map + // pub fn collect(&self, storage: &S) -> Result + // where + // S: StorageRead, + // M: EagerMapFromIter, + // K: Eq + Hash + Ord, + // { + // let it = self + // .iter(storage)? + // .map(|res| res.expect("Failed to unwrap a lazy map element")); + // Ok(M::from_iter(it)) + // } + /// Reads a value from storage fn read_key_val( storage: &S, @@ -619,6 +757,14 @@ mod test { assert_eq!(lazy_map.get(&storage, &key)?.unwrap(), val); assert_eq!(lazy_map.get(&storage, &key2)?.unwrap(), val2); + let eager_map: BTreeMap<_, _> = lazy_map.collect_map(&storage)?; + assert_eq!( + eager_map, + vec![(123, "Test".to_string()), (456, "Test2".to_string())] + .into_iter() + .collect::>() + ); + // Remove the values and check the map contents let removed = lazy_map.remove(&mut storage, &key)?.unwrap(); assert_eq!(removed, val); @@ -650,6 +796,20 @@ mod test { Some(SubKey::Data(key2)) ); + // Try to update a key that doesn't yet exist. + let updated_val = "updated"; + lazy_map.update(&mut storage, key, |current| { + assert!(current.is_none()); + updated_val.to_string() + })?; + // Try to update a key that exists. + let updated_val_2 = "updated again"; + lazy_map.update(&mut storage, key, |current| { + assert_eq!(¤t.unwrap_or_default(), updated_val); + updated_val_2.to_string() + })?; + assert_eq!(&lazy_map.get(&storage, &key)?.unwrap(), updated_val_2); + Ok(()) } @@ -780,6 +940,7 @@ mod test { nested_map.at(&0).get(&storage, &"string2".to_string())?, None ); + assert!(nested_map.contains(&storage, &0)?); // Insert more values nested_map @@ -789,6 +950,9 @@ mod test { .at(&0) .insert(&mut storage, "string2".to_string(), 300)?; + assert!(nested_map.contains(&storage, &0)?); + assert!(nested_map.contains(&storage, &1)?); + let mut it = nested_map.iter(&storage)?; let ( NestedSubKey::Data { @@ -852,6 +1016,8 @@ mod test { 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); + assert!(!nested_map.contains(&storage, &0)?); + assert!(nested_map.contains(&storage, &1)?); // Start removing elements let rem = nested_map @@ -899,4 +1065,56 @@ mod test { assert!(!nested_map.contains(&storage, &1).unwrap()); assert!(nested_map.is_empty(&storage).unwrap()); } + + #[test] + fn test_lazy_map_collection() { + let mut storage = TestWlStorage::default(); + let key_s = storage::Key::parse("testing_simple").unwrap(); + let key_n = storage::Key::parse("testing_nested").unwrap(); + + let simple = LazyMap::::open(key_s); + simple + .insert(&mut storage, "bartle".to_string(), 5) + .unwrap(); + simple.insert(&mut storage, "doo".to_string(), 4).unwrap(); + + let nested_map = NestedMap::>::open(key_n); + nested_map + .at(&0) + .insert(&mut storage, "dingus".to_string(), 5) + .unwrap(); + nested_map + .at(&0) + .insert(&mut storage, "zingus".to_string(), 3) + .unwrap(); + nested_map + .at(&1) + .insert(&mut storage, "dingus".to_string(), 4) + .unwrap(); + + let exp_simple = + vec![("bartle".to_string(), 5), ("doo".to_string(), 4)] + .into_iter() + .collect::>(); + let mut exp_nested: BTreeMap> = + BTreeMap::new(); + exp_nested + .entry(0) + .or_default() + .insert("dingus".to_string(), 5); + exp_nested + .entry(0) + .or_default() + .insert("zingus".to_string(), 3); + exp_nested + .entry(1) + .or_default() + .insert("dingus".to_string(), 4); + + let simple_eager = simple.collect_map(&storage).unwrap(); + let nested_eager = nested_map.collect_map(&storage).unwrap(); + + assert_eq!(exp_simple, simple_eager); + assert_eq!(exp_nested, nested_eager); + } } diff --git a/core/src/ledger/storage_api/error.rs b/core/src/ledger/storage_api/error.rs index f99539bc87..5644bc0a1a 100644 --- a/core/src/ledger/storage_api/error.rs +++ b/core/src/ledger/storage_api/error.rs @@ -63,6 +63,33 @@ impl Error { { Self::CustomWithMessage(msg, CustomError(error.into())) } + + /// Attempt to downgrade the inner error to `E` if any. + /// + /// If this [`enum@Error`] was constructed via [`new`] or [`wrap`] then this + /// function will attempt to perform downgrade on it, otherwise it will + /// return [`Err`]. + /// + /// [`new`]: Error::new + /// [`wrap`]: Error::wrap + /// + /// To match on the inner error type when the downcast is successful, you'll + /// typically want to [`std::ops::Deref::deref`] it out of the [`Box`]. + pub fn downcast(self) -> std::result::Result, Self> + where + E: std::error::Error + Send + Sync + 'static, + { + match self { + Self::Custom(CustomError(b)) + | Self::CustomWithMessage(_, CustomError(b)) + if b.is::() => + { + let res = b.downcast::(); + Ok(res.unwrap()) + } + _ => Err(self), + } + } } /// A custom error diff --git a/core/src/ledger/storage_api/token.rs b/core/src/ledger/storage_api/token.rs index 02adcc32be..2281dc5706 100644 --- a/core/src/ledger/storage_api/token.rs +++ b/core/src/ledger/storage_api/token.rs @@ -171,7 +171,7 @@ where amount } None => { - storage.write(&key, token::Amount::default())?; + storage.write(&key, token::Amount::zero())?; balance } }; diff --git a/core/src/types/dec.rs b/core/src/types/dec.rs index abc80c618c..40494ffad0 100644 --- a/core/src/types/dec.rs +++ b/core/src/types/dec.rs @@ -4,7 +4,8 @@ //! precision. use std::fmt::{Debug, Display, Formatter}; -use std::ops::{Add, AddAssign, Div, Mul, Sub}; +use std::iter::Sum; +use std::ops::{Add, AddAssign, Div, Mul, Neg, Sub}; use std::str::FromStr; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; @@ -330,6 +331,12 @@ impl AddAssign for Dec { } } +impl Sum for Dec { + fn sum>(iter: I) -> Self { + iter.fold(Dec::default(), |acc, next| acc + next) + } +} + impl Sub for Dec { type Output = Self; @@ -409,6 +416,14 @@ impl Div for Dec { } } +impl Neg for Dec { + type Output = Self; + + fn neg(self) -> Self::Output { + Self(self.0.neg()) + } +} + impl Display for Dec { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let is_neg = self.is_negative(); diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 289602ae23..9df9671dbc 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -148,6 +148,7 @@ impl Amount { } /// Checked subtraction. Returns `None` on underflow. + #[must_use] pub fn checked_sub(&self, amount: Amount) -> Option { self.raw .checked_sub(amount.raw) @@ -248,6 +249,26 @@ impl Amount { pub fn from_string_precise(string: &str) -> Result { DenominatedAmount::from_str(string).map(|den| den.amount) } + + /// Multiply by a decimal [`Dec`] with the result rounded up. + /// + /// # Panics + /// Panics when the `dec` is negative. + #[must_use] + pub fn mul_ceil(&self, dec: Dec) -> Self { + assert!(!dec.is_negative()); + let tot = self.raw * dec.abs(); + let denom = Uint::from(10u64.pow(POS_DECIMAL_PRECISION as u32)); + let floor_div = tot / denom; + let rem = tot % denom; + // dbg!(tot, denom, floor_div, rem); + let raw = if !rem.is_zero() { + floor_div + Self::from(1_u64) + } else { + floor_div + }; + Self { raw } + } } /// Given a number represented as `M*B^D`, then @@ -1160,6 +1181,17 @@ mod tests { let non_zero = Amount::from_uint(1, 0).expect("Test failed"); assert!(!non_zero.is_zero()); } + + #[test] + fn test_token_amount_mul_ceil() { + let one = Amount::from(1); + let two = Amount::from(2); + let three = Amount::from(3); + let dec = Dec::from_str("0.34").unwrap(); + assert_eq!(one.mul_ceil(dec), one); + assert_eq!(two.mul_ceil(dec), one); + assert_eq!(three.mul_ceil(dec), two); + } } /// Helpers for testing with addresses. diff --git a/core/src/types/transaction/pos.rs b/core/src/types/transaction/pos.rs index fa0e3d0891..e3ea9d3a21 100644 --- a/core/src/types/transaction/pos.rs +++ b/core/src/types/transaction/pos.rs @@ -95,6 +95,30 @@ pub struct Withdraw { pub source: Option
, } +/// A redelegation of bonded tokens from one validator to another. +#[derive( + Debug, + Clone, + PartialEq, + BorshSerialize, + BorshDeserialize, + BorshSchema, + Hash, + Eq, + Serialize, + Deserialize, +)] +pub struct Redelegation { + /// Source validator address + pub src_validator: Address, + /// Destination validator address + pub dest_validator: Address, + /// Owner (delegator) of the bonds to be redelegate + pub owner: Address, + /// The amount of tokens + pub amount: token::Amount, +} + /// A change to the validator commission rate. #[derive( Debug, diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index ee14e67ad1..db4e664c8b 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -11,6 +11,7 @@ use num_integer::Integer; use num_traits::CheckedMul; use uint::construct_uint; +use super::dec::{Dec, POS_DECIMAL_PRECISION}; use crate::types::token; use crate::types::token::{Amount, AmountParseError, MaspDenom}; @@ -337,6 +338,22 @@ impl I256 { Err(AmountParseError::InvalidRange) } } + + /// Multiply by a decimal [`Dec`] with the result rounded up. + #[must_use] + pub fn mul_ceil(&self, dec: Dec) -> Self { + let is_res_negative = self.is_negative() ^ dec.is_negative(); + let tot = self.abs() * dec.0.abs(); + let denom = Uint::from(10u64.pow(POS_DECIMAL_PRECISION as u32)); + let floor_div = tot / denom; + let rem = tot % denom; + let abs_res = Self(if !rem.is_zero() && !is_res_negative { + floor_div + Uint::from(1_u64) + } else { + floor_div + }); + if is_res_negative { -abs_res } else { abs_res } + } } impl From for I256 { @@ -554,6 +571,8 @@ impl TryFrom for i128 { #[cfg(test)] mod test_uint { + use std::str::FromStr; + use super::*; /// Test that dividing two [`Uint`]s with the specified precision @@ -710,4 +729,20 @@ mod test_uint { let amount: Result = serde_json::from_str(r#""1000000000.2""#); assert!(amount.is_err()); } + + #[test] + fn test_i256_mul_ceil() { + let one = I256::from(1); + let two = I256::from(2); + let dec = Dec::from_str("0.25").unwrap(); + assert_eq!(one.mul_ceil(dec), one); + assert_eq!(two.mul_ceil(dec), one); + assert_eq!(I256::from(4).mul_ceil(dec), one); + assert_eq!(I256::from(5).mul_ceil(dec), two); + + assert_eq!((-one).mul_ceil(-dec), one); + + assert_eq!((-one).mul_ceil(dec), I256::zero()); + assert_eq!(one.mul_ceil(-dec), I256::zero()); + } } diff --git a/ethereum_bridge/src/test_utils.rs b/ethereum_bridge/src/test_utils.rs index 4d2283ad8e..96b5abf240 100644 --- a/ethereum_bridge/src/test_utils.rs +++ b/ethereum_bridge/src/test_utils.rs @@ -8,6 +8,7 @@ use namada_core::ledger::eth_bridge::storage::bridge_pool::get_key_from_hash; use namada_core::ledger::eth_bridge::storage::whitelist; use namada_core::ledger::storage::mockdb::MockDBWriteBatch; use namada_core::ledger::storage::testing::{TestStorage, TestWlStorage}; +use namada_core::ledger::storage_api::token::credit_tokens; use namada_core::ledger::storage_api::{StorageRead, StorageWrite}; use namada_core::types::address::{self, wnam, Address}; use namada_core::types::dec::Dec; @@ -20,7 +21,8 @@ use namada_proof_of_stake::parameters::PosParams; use namada_proof_of_stake::pos_queries::PosQueries; use namada_proof_of_stake::types::GenesisValidator; use namada_proof_of_stake::{ - become_validator, bond_tokens, store_total_consensus_stake, BecomeValidator, + become_validator, bond_tokens, staking_token_address, + store_total_consensus_stake, BecomeValidator, }; use crate::parameters::{ @@ -263,6 +265,8 @@ pub fn append_validators_to_storage( let mut all_keys = HashMap::new(); let params = wl_storage.pos_queries().get_pos_params(); + let staking_token = staking_token_address(wl_storage); + for (validator, stake) in consensus_validators { let keys = TestValidatorKeys::generate(); @@ -282,6 +286,8 @@ pub fn append_validators_to_storage( max_commission_rate_change: Dec::new(1, 2).unwrap(), }) .expect("Test failed"); + credit_tokens(wl_storage, &staking_token, &validator, stake) + .expect("Test failed"); bond_tokens(wl_storage, None, &validator, stake, current_epoch) .expect("Test failed"); diff --git a/proof_of_stake/Cargo.toml b/proof_of_stake/Cargo.toml index a5b407df36..5506ec5174 100644 --- a/proof_of_stake/Cargo.toml +++ b/proof_of_stake/Cargo.toml @@ -33,8 +33,12 @@ tracing.workspace = true [dev-dependencies] namada_core = {path = "../core", features = ["testing"]} +assert_matches.workspace = true itertools.workspace = true proptest.workspace = true proptest-state-machine.workspace = true test-log.workspace = true tracing-subscriber.workspace = true +pretty_assertions.workspace = true +derivative.workspace = true +yansi.workspace = true diff --git a/proof_of_stake/proptest-regressions/tests/state_machine.txt b/proof_of_stake/proptest-regressions/tests/state_machine.txt index 4c02bc0ede..341ba3ff3d 100644 --- a/proof_of_stake/proptest-regressions/tests/state_machine.txt +++ b/proof_of_stake/proptest-regressions/tests/state_machine.txt @@ -4,5 +4,3 @@ # # It is recommended to check this file in to source control so that # everyone who runs the test benefits from these saved cases. -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 }]) -cc c0ffe7b368967ea0c456da20046f7d8a78c232c066ea116d3a123c945b7882fb # shrinks to (initial_state, transitions) = (AbstractPosState { epoch: Epoch(0), params: PosParams { max_validator_slots: 4, pipeline_len: 2, unbonding_len: 7, tm_votes_per_token: Dec(900700.000000), block_proposer_reward: Dec(125000.000000), block_vote_reward: Dec(100000.000000), max_inflation_rate: Dec(100000.000000), target_staked_ratio: Dec(666700.000000), duplicate_vote_min_slash_rate: Dec(1000.000000), light_client_attack_min_slash_rate: Dec(1000.000000), cubic_slashing_window_length: 1 }, genesis_validators: [GenesisValidator { address: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, tokens: Amount { raw: 8937727 }, consensus_key: Ed25519(PublicKey(VerificationKey("e2e8aa145e1ec5cb01ebfaa40e10e12f0230c832fd8135470c001cb86d77de00"))), commission_rate: Dec(50000.000000), max_commission_rate_change: Dec(10000.000000) }, GenesisValidator { address: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, tokens: Amount { raw: 8738693 }, consensus_key: Ed25519(PublicKey(VerificationKey("ff87a0b0a3c7c0ce827e9cada5ff79e75a44a0633bfcb5b50f99307ddb26b337"))), commission_rate: Dec(50000.000000), max_commission_rate_change: Dec(10000.000000) }, GenesisValidator { address: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, tokens: Amount { raw: 8373784 }, consensus_key: Ed25519(PublicKey(VerificationKey("c5bbbb60e412879bbec7bb769804fa8e36e68af10d5477280b63deeaca931bed"))), commission_rate: Dec(50000.000000), max_commission_rate_change: Dec(10000.000000) }, GenesisValidator { address: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, tokens: Amount { raw: 3584214 }, consensus_key: Ed25519(PublicKey(VerificationKey("4f44e6c7bdfed3d9f48d86149ee3d29382cae8c83ca253e06a70be54a301828b"))), commission_rate: Dec(50000.000000), max_commission_rate_change: Dec(10000.000000) }, GenesisValidator { address: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, tokens: Amount { raw: 553863 }, consensus_key: Ed25519(PublicKey(VerificationKey("ee1aa49a4459dfe813a3cf6eb882041230c7b2558469de81f87c9bf23bf10a03"))), commission_rate: Dec(50000.000000), max_commission_rate_change: Dec(10000.000000) }, GenesisValidator { address: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, tokens: Amount { raw: 218044 }, consensus_key: Ed25519(PublicKey(VerificationKey("191fc38f134aaf1b7fdb1f86330b9d03e94bd4ba884f490389de964448e89b3f"))), commission_rate: Dec(50000.000000), max_commission_rate_change: Dec(10000.000000) }], bonds: {BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }: {Epoch(0): 8.937727}, BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }: {Epoch(0): 8.373784}, BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }: {Epoch(0): 0.553863}, BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }: {Epoch(0): 8.738693}, BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }: {Epoch(0): 0.218044}, BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }: {Epoch(0): 3.584214}}, validator_stakes: {Epoch(0): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: 8.937727, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: 8.373784, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: 0.553863, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: 8.738693, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: 0.218044, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: 3.584214}, Epoch(1): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: 8.937727, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: 8.373784, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: 0.553863, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: 8.738693, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: 0.218044, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: 3.584214}, Epoch(2): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: 8.937727, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: 8.373784, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: 0.553863, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: 8.738693, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: 0.218044, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: 3.584214}}, consensus_set: {Epoch(0): {Amount { raw: 3584214 }: [Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd], Amount { raw: 8373784 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { raw: 8738693 }: [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], Amount { raw: 8937727 }: [Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6]}, Epoch(1): {Amount { raw: 3584214 }: [Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd], Amount { raw: 8373784 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { raw: 8738693 }: [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], Amount { raw: 8937727 }: [Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6]}, Epoch(2): {Amount { raw: 3584214 }: [Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd], Amount { raw: 8373784 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { raw: 8738693 }: [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], Amount { raw: 8937727 }: [Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6]}}, below_capacity_set: {Epoch(0): {ReverseOrdTokenAmount(Amount { raw: 218044 }): [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv], ReverseOrdTokenAmount(Amount { raw: 553863 }): [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6]}, Epoch(1): {ReverseOrdTokenAmount(Amount { raw: 218044 }): [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv], ReverseOrdTokenAmount(Amount { raw: 553863 }): [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6]}, Epoch(2): {ReverseOrdTokenAmount(Amount { raw: 218044 }): [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv], ReverseOrdTokenAmount(Amount { raw: 553863 }): [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6]}}, validator_states: {Epoch(0): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: Consensus, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: BelowCapacity, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: Consensus, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: BelowCapacity, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: Consensus}, Epoch(1): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: Consensus, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: BelowCapacity, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: Consensus, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: BelowCapacity, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: Consensus}, Epoch(2): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: Consensus, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: BelowCapacity, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: Consensus, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: BelowCapacity, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: Consensus}}, unbonds: {}, validator_slashes: {}, enqueued_slashes: {}, validator_last_slash_epochs: {}, unbond_records: {} }, [Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 267 } }, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 7610143 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 9863718 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 7102818 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 63132 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 9663084 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 2694963 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 7453740 } }, NextEpoch, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 14974324 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 2628172 } }, NextEpoch, NextEpoch, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 282055 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 11228090 } }, Bond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 2027105 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 2034080 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { raw: 3329590 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 854661 } }, Misbehavior { address: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, slash_type: DuplicateVote, infraction_epoch: Epoch(1), height: 0 }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 227931 } }, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 2701887 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 1776100 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { raw: 3717491 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { raw: 5281559 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { raw: 2426117 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 2005749 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 7883312 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 7300122 } }, Bond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 3388459 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { raw: 195542 } }, NextEpoch, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 2251455 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 1237777 } }, NextEpoch, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 691613 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 1244599 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 2645543 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 8384136 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 590662 } }, NextEpoch, InitValidator { address: Established: atest1v4ehgw368qcrqd2ygvmyyvf4g9qnvv3kxucrwv3hxg6ryve4x56r233cxucnysjrxsmygdj9yer4pz, consensus_key: Ed25519(PublicKey(VerificationKey("afa2335747c0249f66eca84e88fba1a0e3ccec6a8f6f97f3177a42ffbb216492"))), commission_rate: Dec(195450.000000), max_commission_rate_change: Dec(954460.000000) }, Bond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 1687952 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 12754717 } }, Misbehavior { address: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, slash_type: LightClientAttack, infraction_epoch: Epoch(4), height: 0 }, Bond { id: BondId { source: Implicit: atest1d9khqw36xqunjdeegge5xdpcg5mnqwzp8yerzde58pq5g3pcxu6yvvphg3zr23z9gg6yvs3cmzdz9u, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 8952712 } }, NextEpoch, Withdraw { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv } }, Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 519835 } }, UnjailValidator { address: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, Unbond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 2207493 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 236124 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 71122 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw36xqunjdeegge5xdpcg5mnqwzp8yerzde58pq5g3pcxu6yvvphg3zr23z9gg6yvs3cmzdz9u, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 1158688 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 267618 } }, InitValidator { address: Established: atest1v4ehgw36xucy2dfcxdzrxvpjx5uygwzrxpzrjs3jx4p5vvjrxdq5yvpjx5e5zs3jxdqng3pcplv2ch, consensus_key: Ed25519(PublicKey(VerificationKey("822cfec1ec829a50306424ac3d11115e880b952f5f54ac9a624277898991ee70"))), commission_rate: Dec(614520.000000), max_commission_rate_change: Dec(369920.000000) }, Bond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 8634884 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 8660668 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g9rryv3sx5c5v33sgsmrsd3egerrgdenx3zy2sfex4prvsehxcurydjx8qu5zdz9f2npes, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 8436873 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 515615 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 46481 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 4153966 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 2272563 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g5eyzwf3xqc5gwzxg3pnq3jpgsenxwp3x56rjvz9x5crwsf3gerrgwphxqen2sjz4hscvd, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 7491749 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 1921487 } }, Bond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 8316111 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 11873152 } }, NextEpoch, Withdraw { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv } }, Withdraw { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk } }, Withdraw { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 } }, Bond { id: BondId { source: Implicit: atest1d9khqw368yenjvpjxcu5vv33x3zrqw2zgg6nsvzrx9prxd2pgsmyxwfjxgunvs3exerrydp3csdkvr, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 4728535 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g5eyzwf3xqc5gwzxg3pnq3jpgsenxwp3x56rjvz9x5crwsf3gerrgwphxqen2sjz4hscvd, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 2828807 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g5eyzwf3xqc5gwzxg3pnq3jpgsenxwp3x56rjvz9x5crwsf3gerrgwphxqen2sjz4hscvd, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 655500 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 234416 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 330322 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 222600 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw36xqunjdeegge5xdpcg5mnqwzp8yerzde58pq5g3pcxu6yvvphg3zr23z9gg6yvs3cmzdz9u, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 2538059 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 168498 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw368yenjvpjxcu5vv33x3zrqw2zgg6nsvzrx9prxd2pgsmyxwfjxgunvs3exerrydp3csdkvr, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 510701 } }, Misbehavior { address: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, slash_type: DuplicateVote, infraction_epoch: Epoch(8), height: 0 }, InitValidator { address: Established: atest1v4ehgw36ggcrz3zygyunqsfjggmnq33h8ycnsdphxepnsve4gerrss2pgfp5z3psgccrj33klenl5r, consensus_key: Ed25519(PublicKey(VerificationKey("afc853489cf37abedeb6a97d036f3dc60934194af7169a2cc15fb3f85e4e287c"))), commission_rate: Dec(52690.000000), max_commission_rate_change: Dec(56470.000000) }, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 7098849 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw36xqunjdeegge5xdpcg5mnqwzp8yerzde58pq5g3pcxu6yvvphg3zr23z9gg6yvs3cmzdz9u, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 2180088 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 243441 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw36xqunjdeegge5xdpcg5mnqwzp8yerzde58pq5g3pcxu6yvvphg3zr23z9gg6yvs3cmzdz9u, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 1621261 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 7650954 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 1201023 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36xqunjdeegge5xdpcg5mnqwzp8yerzde58pq5g3pcxu6yvvphg3zr23z9gg6yvs3cmzdz9u, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 9702706 } }, InitValidator { address: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu, consensus_key: Ed25519(PublicKey(VerificationKey("f8506f129faaf3bac1397ad0ab3bfa6d1a00d5c1064c4fafe740f2844be8fb04"))), commission_rate: Dec(575190.000000), max_commission_rate_change: Dec(602710.000000) }, Unbond { id: BondId { source: Implicit: atest1d9khqw368yenjvpjxcu5vv33x3zrqw2zgg6nsvzrx9prxd2pgsmyxwfjxgunvs3exerrydp3csdkvr, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 347187 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 5536481 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36xc6nvvf4g9znxvf3xdrrgvfexuen2dek8qmnqse58q6ygdpkxeznz3j9xyeyydfht747xe, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 1859243 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 1907757 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw368yenjvpjxcu5vv33x3zrqw2zgg6nsvzrx9prxd2pgsmyxwfjxgunvs3exerrydp3csdkvr, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 3007741 } }, Misbehavior { address: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, slash_type: DuplicateVote, infraction_epoch: Epoch(9), height: 0 }, Bond { id: BondId { source: Established: atest1v4ehgw36g9rryv3sx5c5v33sgsmrsd3egerrgdenx3zy2sfex4prvsehxcurydjx8qu5zdz9f2npes, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 8226972 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw368yenjvpjxcu5vv33x3zrqw2zgg6nsvzrx9prxd2pgsmyxwfjxgunvs3exerrydp3csdkvr, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 602759 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw36xqunjdeegge5xdpcg5mnqwzp8yerzde58pq5g3pcxu6yvvphg3zr23z9gg6yvs3cmzdz9u, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 8350223 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 3787232 } }, InitValidator { address: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595, consensus_key: Ed25519(PublicKey(VerificationKey("0b88c50c1b9b5b1e83c89110e388908dc3cc18ce0551494ab1c82bece24b2714"))), commission_rate: Dec(674000.000000), max_commission_rate_change: Dec(247230.000000) }, Bond { id: BondId { source: Established: atest1v4ehgw36gdp52wp4xv6yyd3nx9pnysfn89znjsen8quyvwfkgycnjs29x9ryxveh8prygsfecye5dj, validator: Established: atest1v4ehgw36ggcrz3zygyunqsfjggmnq33h8ycnsdphxepnsve4gerrss2pgfp5z3psgccrj33klenl5r }, amount: Amount { raw: 1391049 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36gve5zdf4gccygv6zxgcnxwzrgv65x32rg4zrxv34g9prvs2pxqmnzve5xvuns33czq9awp, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 4008194 } }, Bond { id: BondId { source: Implicit: atest1d9khqw368pq5g3f3gceygvpjxuenyveexary2wzx8ycnw3zpg9zrvvp4xger2dzyxuunwvjz4n93ww, validator: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595 }, amount: Amount { raw: 9368360 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { raw: 9140634 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 600383 } }, Misbehavior { address: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, slash_type: DuplicateVote, infraction_epoch: Epoch(7), height: 0 }, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 8599835 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 345454 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g9rryv3sx5c5v33sgsmrsd3egerrgdenx3zy2sfex4prvsehxcurydjx8qu5zdz9f2npes, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 12448069 } }, NextEpoch, Withdraw { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 5151682 } }, Bond { id: BondId { source: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 1862578 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 10904134 } }, Bond { id: BondId { source: Implicit: atest1d9khqw368pq5g3f3gceygvpjxuenyveexary2wzx8ycnw3zpg9zrvvp4xger2dzyxuunwvjz4n93ww, validator: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595 }, amount: Amount { raw: 773655 } }, Bond { id: BondId { source: Implicit: atest1d9khqw3689rrqdp58pznydecgyu5xs3cxdznvd6xxsmng32zxumrxvpj8qenydejgfzygwzxlu6r7s, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 8927299 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 1288039 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 2861830 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 445593 } }, Bond { id: BondId { source: Implicit: atest1d9khqw368pq5g3f3gceygvpjxuenyveexary2wzx8ycnw3zpg9zrvvp4xger2dzyxuunwvjz4n93ww, validator: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595 }, amount: Amount { raw: 8204875 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 602527 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 5812026 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw3689rrqdp58pznydecgyu5xs3cxdznvd6xxsmng32zxumrxvpj8qenydejgfzygwzxlu6r7s, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 211165 } }, NextEpoch, Bond { id: BondId { source: Implicit: atest1d9khqw36xsun2decx9p52v2xg5cr2vphxym5vve58yerqve5x5c5yve3gepyzs3ngycy233eufckzz, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 350302 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 4560437 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36xqunjdeegge5xdpcg5mnqwzp8yerzde58pq5g3pcxu6yvvphg3zr23z9gg6yvs3cmzdz9u, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 3515009 } }, Bond { id: BondId { source: Established: atest1v4ehgw36xucy2dfcxdzrxvpjx5uygwzrxpzrjs3jx4p5vvjrxdq5yvpjx5e5zs3jxdqng3pcplv2ch, validator: Established: atest1v4ehgw36xucy2dfcxdzrxvpjx5uygwzrxpzrjs3jx4p5vvjrxdq5yvpjx5e5zs3jxdqng3pcplv2ch }, amount: Amount { raw: 4956849 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw36xsun2decx9p52v2xg5cr2vphxym5vve58yerqve5x5c5yve3gepyzs3ngycy233eufckzz, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 290427 } }, NextEpoch, Unbond { id: BondId { source: Implicit: atest1d9khqw36gve5zdf4gccygv6zxgcnxwzrgv65x32rg4zrxv34g9prvs2pxqmnzve5xvuns33czq9awp, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 3261985 } }, Bond { id: BondId { source: Established: atest1v4ehgw36xucy2dfcxdzrxvpjx5uygwzrxpzrjs3jx4p5vvjrxdq5yvpjx5e5zs3jxdqng3pcplv2ch, validator: Established: atest1v4ehgw36xucy2dfcxdzrxvpjx5uygwzrxpzrjs3jx4p5vvjrxdq5yvpjx5e5zs3jxdqng3pcplv2ch }, amount: Amount { raw: 8946479 } }, Withdraw { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 } }, NextEpoch, InitValidator { address: Established: atest1v4ehgw36gvcrgdeex5ensvfkgccyxve3x3pnys6xxpzr2s6rxuurv3j9g4pyysjzxq6ygdzyt2wxa3, consensus_key: Ed25519(PublicKey(VerificationKey("a856fc650a2404e2d0c152d89c1c221bd9056a6103980e1d821b0cbae213ff44"))), commission_rate: Dec(324920.000000), max_commission_rate_change: Dec(512260.000000) }, Withdraw { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 82795 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 128956 } }, Bond { id: BondId { source: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 2043203 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 6764953 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g5eyzwf3xqc5gwzxg3pnq3jpgsenxwp3x56rjvz9x5crwsf3gerrgwphxqen2sjz4hscvd, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 6413168 } }, Bond { id: BondId { source: Implicit: atest1d9khqw368pq5g3f3gceygvpjxuenyveexary2wzx8ycnw3zpg9zrvvp4xger2dzyxuunwvjz4n93ww, validator: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595 }, amount: Amount { raw: 6384185 } }, Misbehavior { address: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595, slash_type: LightClientAttack, infraction_epoch: Epoch(13), height: 0 }, Bond { id: BondId { source: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595, validator: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595 }, amount: Amount { raw: 8314982 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36xscrsve3geqnwd2x8qmrzwpe89z5zsekgvenqwp5x4p5ydzp8qmrz3zpgcmnydjptyfc40, validator: Established: atest1v4ehgw36gvcrgdeex5ensvfkgccyxve3x3pnys6xxpzr2s6rxuurv3j9g4pyysjzxq6ygdzyt2wxa3 }, amount: Amount { raw: 9139532 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 34693 } }, Bond { id: BondId { source: Implicit: atest1d9khqw3689rrqdp58pznydecgyu5xs3cxdznvd6xxsmng32zxumrxvpj8qenydejgfzygwzxlu6r7s, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 9487215 } }, NextEpoch, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 799953 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw36xscrsve3geqnwd2x8qmrzwpe89z5zsekgvenqwp5x4p5ydzp8qmrz3zpgcmnydjptyfc40, validator: Established: atest1v4ehgw36gvcrgdeex5ensvfkgccyxve3x3pnys6xxpzr2s6rxuurv3j9g4pyysjzxq6ygdzyt2wxa3 }, amount: Amount { raw: 3334636 } }, NextEpoch, Withdraw { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xucy2dfcxdzrxvpjx5uygwzrxpzrjs3jx4p5vvjrxdq5yvpjx5e5zs3jxdqng3pcplv2ch, validator: Established: atest1v4ehgw36xucy2dfcxdzrxvpjx5uygwzrxpzrjs3jx4p5vvjrxdq5yvpjx5e5zs3jxdqng3pcplv2ch }, amount: Amount { raw: 7942329 } }, NextEpoch, Unbond { id: BondId { source: Established: atest1v4ehgw36gdp52wp4xv6yyd3nx9pnysfn89znjsen8quyvwfkgycnjs29x9ryxveh8prygsfecye5dj, validator: Established: atest1v4ehgw36ggcrz3zygyunqsfjggmnq33h8ycnsdphxepnsve4gerrss2pgfp5z3psgccrj33klenl5r }, amount: Amount { raw: 878389 } }, Withdraw { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 } }, UnjailValidator { address: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, UnjailValidator { address: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, Bond { id: BondId { source: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 5376602 } }, UnjailValidator { address: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, Unbond { id: BondId { source: Implicit: atest1d9khqw36xc6nvvf4g9znxvf3xdrrgvfexuen2dek8qmnqse58q6ygdpkxeznz3j9xyeyydfht747xe, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 1118174 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 286221 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 73579 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g9rryv3sx5c5v33sgsmrsd3egerrgdenx3zy2sfex4prvsehxcurydjx8qu5zdz9f2npes, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 2010212 } }, Bond { id: BondId { source: Implicit: atest1d9khqw3689rrqdp58pznydecgyu5xs3cxdznvd6xxsmng32zxumrxvpj8qenydejgfzygwzxlu6r7s, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 4276553 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw368yenjvpjxcu5vv33x3zrqw2zgg6nsvzrx9prxd2pgsmyxwfjxgunvs3exerrydp3csdkvr, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 54860 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gdp52wp4xv6yyd3nx9pnysfn89znjsen8quyvwfkgycnjs29x9ryxveh8prygsfecye5dj, validator: Established: atest1v4ehgw36ggcrz3zygyunqsfjggmnq33h8ycnsdphxepnsve4gerrss2pgfp5z3psgccrj33klenl5r }, amount: Amount { raw: 145154 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { raw: 1941194 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 93 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw3689rrqdp58pznydecgyu5xs3cxdznvd6xxsmng32zxumrxvpj8qenydejgfzygwzxlu6r7s, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 9992596 } }, Bond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 504024 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 5640962 } }, InitValidator { address: Established: atest1v4ehgw368qmnzsfeg5urqw2p8pq5gsf4ggcnqdz9xvc5vsfjxc6nvsekgsmyv3jp8ym52wph0hm33r, consensus_key: Ed25519(PublicKey(VerificationKey("2bccbdf7490f98b2e258a399b75c74bd1b71e9f6f4cc2160edbe3186e23d30e4"))), commission_rate: Dec(427420.000000), max_commission_rate_change: Dec(574220.000000) }, Misbehavior { address: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, slash_type: DuplicateVote, infraction_epoch: Epoch(12), height: 0 }, Bond { id: BondId { source: Implicit: atest1d9khqw368pq5g3f3gceygvpjxuenyveexary2wzx8ycnw3zpg9zrvvp4xger2dzyxuunwvjz4n93ww, validator: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595 }, amount: Amount { raw: 4019468 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36xscrsve3geqnwd2x8qmrzwpe89z5zsekgvenqwp5x4p5ydzp8qmrz3zpgcmnydjptyfc40, validator: Established: atest1v4ehgw36gvcrgdeex5ensvfkgccyxve3x3pnys6xxpzr2s6rxuurv3j9g4pyysjzxq6ygdzyt2wxa3 }, amount: Amount { raw: 5683219 } }, Bond { id: BondId { source: Implicit: atest1d9khqw368pz5zd3sgeqnxve4g9ryv3zzggerqdf3xqmrywfng4zrs3pkx5enydesg5mr2v6p4v8rst, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 6886837 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g9rryv3sx5c5v33sgsmrsd3egerrgdenx3zy2sfex4prvsehxcurydjx8qu5zdz9f2npes, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 7852494 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 749047 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gdp52wp4xv6yyd3nx9pnysfn89znjsen8quyvwfkgycnjs29x9ryxveh8prygsfecye5dj, validator: Established: atest1v4ehgw36ggcrz3zygyunqsfjggmnq33h8ycnsdphxepnsve4gerrss2pgfp5z3psgccrj33klenl5r }, amount: Amount { raw: 9097957 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g9rryv3sx5c5v33sgsmrsd3egerrgdenx3zy2sfex4prvsehxcurydjx8qu5zdz9f2npes, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 6781624 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw36gve5zdf4gccygv6zxgcnxwzrgv65x32rg4zrxv34g9prvs2pxqmnzve5xvuns33czq9awp, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 123577 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gvmrzsf58yurxsjxgfqnqv6yg56nwv69xv6yv3zpx9znv3jpg4p5zdpnxpznzv3hq7q2az, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 1515359 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 9136180 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw368yenjvpjxcu5vv33x3zrqw2zgg6nsvzrx9prxd2pgsmyxwfjxgunvs3exerrydp3csdkvr, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 190090 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw368pz5zd3sgeqnxve4g9ryv3zzggerqdf3xqmrywfng4zrs3pkx5enydesg5mr2v6p4v8rst, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 2817512 } }, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 5207922 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36x5uyvv2px4pr2d3cgdpry3zzxq6nsd6yg5mnwsjzgcervdpegsunqd3kgy6ygvpjyvyhzj, validator: Established: atest1v4ehgw368qcrqd2ygvmyyvf4g9qnvv3kxucrwv3hxg6ryve4x56r233cxucnysjrxsmygdj9yer4pz }, amount: Amount { raw: 70961 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gdzns33sgsmr2wz9x4rrxdenx3zyysfcxcmry32pgeznjw2zx4zrysjxgeryxsfc2etu33, validator: Established: atest1v4ehgw36ggcrz3zygyunqsfjggmnq33h8ycnsdphxepnsve4gerrss2pgfp5z3psgccrj33klenl5r }, amount: Amount { raw: 9056961 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gvmrzsf58yurxsjxgfqnqv6yg56nwv69xv6yv3zpx9znv3jpg4p5zdpnxpznzv3hq7q2az, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 1451932 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36gcunwdzyxpz5xs2rxuuyxvfcgfznzd3hg9zrzdfnx5crwv69ggcnvsjpgc65gd33uuymj8, validator: Established: atest1v4ehgw36xucy2dfcxdzrxvpjx5uygwzrxpzrjs3jx4p5vvjrxdq5yvpjx5e5zs3jxdqng3pcplv2ch }, amount: Amount { raw: 1463719 } }, Withdraw { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36x5uyvv2px4pr2d3cgdpry3zzxq6nsd6yg5mnwsjzgcervdpegsunqd3kgy6ygvpjyvyhzj, validator: Established: atest1v4ehgw368qcrqd2ygvmyyvf4g9qnvv3kxucrwv3hxg6ryve4x56r233cxucnysjrxsmygdj9yer4pz }, amount: Amount { raw: 792907 } }, InitValidator { address: Established: atest1v4ehgw36xy65xd3cgvcyxsesgsunys3hgg6nyvekxgerz3fjxaprqvfhxser2wphg5mnjdzpf7edt5, consensus_key: Ed25519(PublicKey(VerificationKey("8f6eeade76a7ce1ccf1d3138807774696d51fcf2c8879e53aa2b082e34eec42b"))), commission_rate: Dec(592790.000000), max_commission_rate_change: Dec(854710.000000) }]) diff --git a/proof_of_stake/src/btree_set.rs b/proof_of_stake/src/btree_set.rs deleted file mode 100644 index 48460b2f0b..0000000000 --- a/proof_of_stake/src/btree_set.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! This module adds shims for BTreeSet methods that not yet stable. - -use std::collections::BTreeSet; - -/// This trait adds shims for BTreeSet methods that not yet stable. They have -/// the same behavior as their nightly counterparts, but additionally require -/// `Clone` bound on the element type (for `pop_first` and `pop_last`). -pub trait BTreeSetShims { - /// Returns a reference to the first value in the set, if any. This value is - /// always the minimum of all values in the set. - fn first_shim(&self) -> Option<&T>; - - /// Returns a reference to the last value in the set, if any. This value is - /// always the maximum of all values in the set. - fn last_shim(&self) -> Option<&T>; - - /// Removes the first value from the set and returns it, if any. The first - /// value is always the minimum value in the set. - fn pop_first_shim(&mut self) -> Option; - - /// Removes the last value from the set and returns it, if any. The last - /// value is always the maximum value in the set. - fn pop_last_shim(&mut self) -> Option; -} - -impl BTreeSetShims for BTreeSet { - fn first_shim(&self) -> Option<&T> { - let mut iter = self.iter(); - iter.next() - } - - fn last_shim(&self) -> Option<&T> { - let iter = self.iter(); - iter.last() - } - - fn pop_first_shim(&mut self) -> Option { - let mut iter = self.iter(); - let first = iter.next().cloned(); - if let Some(first) = first { - return self.take(&first); - } - None - } - - fn pop_last_shim(&mut self) -> Option { - let iter = self.iter(); - let last = iter.last().cloned(); - if let Some(last) = last { - return self.take(&last); - } - None - } -} diff --git a/proof_of_stake/src/epoched.rs b/proof_of_stake/src/epoched.rs index d5a567fc94..c06a6efd8e 100644 --- a/proof_of_stake/src/epoched.rs +++ b/proof_of_stake/src/epoched.rs @@ -434,7 +434,6 @@ where &self, storage: &S, epoch: Epoch, - _params: &PosParams, ) -> storage_api::Result> where S: StorageRead, @@ -482,6 +481,26 @@ where } } + /// Initialize or add a value to the current delta value at the given epoch + /// offset. + pub fn add( + &self, + storage: &mut S, + value: Data, + current_epoch: Epoch, + offset: u64, + ) -> storage_api::Result<()> + where + S: StorageWrite + StorageRead, + Data: Default, + { + self.update_data(storage, current_epoch)?; + let cur_value = self + .get_delta_val(storage, current_epoch + offset)? + .unwrap_or_default(); + self.set_at_epoch(storage, cur_value + value, current_epoch, offset) + } + /// Initialize or set the value at the given epoch offset. pub fn set( &self, @@ -1074,6 +1093,20 @@ mod test { assert_eq!(data_handler.get(&s, &Epoch(9))?, None); assert_eq!(data_handler.get(&s, &Epoch(10))?, Some(6)); + epoched.add(&mut s, 15, Epoch(10), 0)?; + assert_eq!(epoched.get_last_update(&s)?, Some(Epoch(10))); + assert_eq!(epoched.get_oldest_epoch(&s)?, Some(Epoch(0))); + assert_eq!(data_handler.get(&s, &Epoch(0))?, Some(1)); + assert_eq!(data_handler.get(&s, &Epoch(1))?, Some(2)); + assert_eq!(data_handler.get(&s, &Epoch(2))?, Some(3)); + assert_eq!(data_handler.get(&s, &Epoch(3))?, Some(4)); + assert_eq!(data_handler.get(&s, &Epoch(5))?, Some(5)); + assert_eq!(data_handler.get(&s, &Epoch(6))?, None); + assert_eq!(data_handler.get(&s, &Epoch(7))?, None); + assert_eq!(data_handler.get(&s, &Epoch(8))?, None); + assert_eq!(data_handler.get(&s, &Epoch(9))?, None); + assert_eq!(data_handler.get(&s, &Epoch(10))?, Some(21)); + Ok(()) } diff --git a/proof_of_stake/src/error.rs b/proof_of_stake/src/error.rs new file mode 100644 index 0000000000..96123d6feb --- /dev/null +++ b/proof_of_stake/src/error.rs @@ -0,0 +1,185 @@ +/// Custom error types +use std::num::TryFromIntError; + +use namada_core::ledger::storage_api; +use namada_core::types::address::Address; +use namada_core::types::dec::Dec; +use namada_core::types::storage::Epoch; +use thiserror::Error; + +use crate::rewards; +use crate::types::{BondId, ValidatorState}; + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum GenesisError { + #[error("Voting power overflow: {0}")] + VotingPowerOverflow(TryFromIntError), +} + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum InflationError { + #[error("Error in calculating rewards: {0}")] + Rewards(rewards::RewardsError), + #[error("Expected validator {0} to be in consensus set but got: {1:?}")] + ExpectedValidatorInConsensus(Address, Option), +} + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum BecomeValidatorError { + #[error("The given address {0} is already a validator")] + AlreadyValidator(Address), +} + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum BondError { + #[error("The given address {0} is not a validator address")] + NotAValidator(Address), + #[error( + "The given source address {0} is a validator address. Validators may \ + not delegate." + )] + SourceMustNotBeAValidator(Address), + #[error("The given validator address {0} is inactive")] + InactiveValidator(Address), + #[error("Voting power overflow: {0}")] + VotingPowerOverflow(TryFromIntError), +} + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum UnbondError { + #[error("No bond could be found")] + NoBondFound, + #[error( + "Trying to withdraw more tokens ({0}) than the amount bonded ({0})" + )] + UnbondAmountGreaterThanBond(String, String), + #[error("No bonds found for the validator {0}")] + ValidatorHasNoBonds(Address), + #[error("Voting power not found for the validator {0}")] + ValidatorHasNoVotingPower(Address), + #[error("Voting power overflow: {0}")] + VotingPowerOverflow(TryFromIntError), + #[error("Trying to unbond from a frozen validator: {0}")] + ValidatorIsFrozen(Address), +} + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum WithdrawError { + #[error("No unbond could be found for {0}")] + NoUnbondFound(BondId), + #[error("No unbond may be withdrawn yet for {0}")] + NoWithdrawableUnbond(BondId), +} + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum SlashError { + #[error("The validator {0} has no total deltas value")] + ValidatorHasNoTotalDeltas(Address), + #[error("The validator {0} has no voting power")] + ValidatorHasNoVotingPower(Address), + #[error("Unexpected slash token change")] + InvalidSlashChange(i128), + #[error("Voting power overflow: {0}")] + VotingPowerOverflow(TryFromIntError), + #[error("Unexpected negative stake {0} for validator {1}")] + NegativeStake(i128, Address), +} + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum CommissionRateChangeError { + #[error("Unexpected negative commission rate {0} for validator {1}")] + NegativeRate(Dec, Address), + #[error("Rate change of {0} is too large for validator {1}")] + RateChangeTooLarge(Dec, Address), + #[error( + "There is no maximum rate change written in storage for validator {0}" + )] + NoMaxSetInStorage(Address), + #[error("Cannot write to storage for validator {0}")] + CannotWrite(Address), + #[error("Cannot read storage for validator {0}")] + 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), +} + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum RedelegationError { + #[error("The redelegation is chained")] + IsChainedRedelegation, + #[error("The source and destination validator must be different")] + RedelegationSrcEqDest, + #[error("The delegator must not be a validator")] + DelegatorIsValidator, + #[error("The address {0} must be a validator")] + NotAValidator(Address), +} + +impl From for storage_api::Error { + fn from(err: BecomeValidatorError) -> Self { + Self::new(err) + } +} + +impl From for storage_api::Error { + fn from(err: BondError) -> Self { + Self::new(err) + } +} + +impl From for storage_api::Error { + fn from(err: UnbondError) -> Self { + Self::new(err) + } +} + +impl From for storage_api::Error { + fn from(err: WithdrawError) -> Self { + Self::new(err) + } +} + +impl From for storage_api::Error { + fn from(err: CommissionRateChangeError) -> Self { + Self::new(err) + } +} + +impl From for storage_api::Error { + fn from(err: InflationError) -> Self { + Self::new(err) + } +} + +impl From for storage_api::Error { + fn from(err: UnjailValidatorError) -> Self { + Self::new(err) + } +} + +impl From for storage_api::Error { + fn from(err: RedelegationError) -> Self { + Self::new(err) + } +} diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 0fbbf2231b..0fadd6c728 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -12,7 +12,6 @@ #![deny(rustdoc::broken_intra_doc_links)] #![deny(rustdoc::private_intra_doc_links)] -pub mod btree_set; pub mod epoched; pub mod parameters; pub mod pos_queries; @@ -21,22 +20,22 @@ pub mod storage; pub mod types; // pub mod validation; +mod error; #[cfg(test)] mod tests; use core::fmt::Debug; use std::cmp::{self, Reverse}; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; -use std::num::TryFromIntError; use borsh::BorshDeserialize; +pub use error::*; use namada_core::ledger::storage_api::collections::lazy_map::{ - NestedSubKey, SubKey, + Collectable, LazyMap, NestedMap, NestedSubKey, SubKey, }; use namada_core::ledger::storage_api::collections::{LazyCollection, LazySet}; -use namada_core::ledger::storage_api::token::credit_tokens; use namada_core::ledger::storage_api::{ - self, ResultExt, StorageRead, StorageWrite, + self, token, ResultExt, StorageRead, StorageWrite, }; use namada_core::types::address::{Address, InternalAddress}; use namada_core::types::dec::Dec; @@ -44,31 +43,33 @@ use namada_core::types::key::{ common, tm_consensus_key_raw_hash, PublicKeyTmRawHash, }; pub use namada_core::types::storage::{Epoch, Key, KeySeg}; -use namada_core::types::token; use once_cell::unsync::Lazy; -use parameters::PosParams; +pub use parameters::PosParams; use rewards::PosRewardsCalculator; 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, - params_key, slashes_prefix, unbonds_for_source_prefix, unbonds_prefix, + get_validator_address_from_bond, is_bond_key, is_unbond_key, + is_validator_slashes_key, last_block_proposer_key, 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, EpochedSlashes, - ReverseOrdTokenAmount, RewardsAccumulator, SlashedAmount, - TotalConsensusStakes, UnbondDetails, ValidatorAddresses, - ValidatorUnbondRecords, + validator_max_commission_rate_change_key, }; -use thiserror::Error; use types::{ - BelowCapacityValidatorSet, BelowCapacityValidatorSets, BondId, Bonds, - CommissionRates, ConsensusValidator, ConsensusValidatorSet, - ConsensusValidatorSets, GenesisValidator, Position, RewardsProducts, Slash, - SlashType, Slashes, TotalDeltas, Unbonds, ValidatorConsensusKeys, - ValidatorDeltas, ValidatorEthColdKeys, ValidatorEthHotKeys, - ValidatorPositionAddresses, ValidatorSetPositions, ValidatorSetUpdate, - ValidatorState, ValidatorStates, VoteInfo, WeightedValidator, + into_tm_voting_power, BelowCapacityValidatorSet, + BelowCapacityValidatorSets, BondDetails, BondId, Bonds, + BondsAndUnbondsDetail, BondsAndUnbondsDetails, CommissionRates, + ConsensusValidator, ConsensusValidatorSet, ConsensusValidatorSets, + DelegatorRedelegatedBonded, DelegatorRedelegatedUnbonded, + EagerRedelegatedBondsMap, EpochedSlashes, GenesisValidator, + IncomingRedelegations, OutgoingRedelegations, Position, + RedelegatedBondsOrUnbonds, RedelegatedTokens, ReverseOrdTokenAmount, + RewardsAccumulator, RewardsProducts, Slash, SlashType, SlashedAmount, + Slashes, TotalConsensusStakes, TotalDeltas, TotalRedelegatedBonded, + TotalRedelegatedUnbonded, UnbondDetails, Unbonds, ValidatorAddresses, + ValidatorConsensusKeys, ValidatorDeltas, ValidatorEthColdKeys, + ValidatorEthHotKeys, ValidatorPositionAddresses, ValidatorSetPositions, + ValidatorSetUpdate, ValidatorState, ValidatorStates, + ValidatorTotalUnbonded, VoteInfo, WeightedValidator, }; /// Address of the PoS account implemented as a native VP @@ -89,160 +90,7 @@ pub fn staking_token_address(storage: &impl StorageRead) -> Address { /// stored const STORE_VALIDATOR_SETS_LEN: u64 = 2; -#[allow(missing_docs)] -#[derive(Error, Debug)] -pub enum GenesisError { - #[error("Voting power overflow: {0}")] - VotingPowerOverflow(TryFromIntError), -} - -#[allow(missing_docs)] -#[derive(Error, Debug)] -pub enum InflationError { - #[error("Error in calculating rewards: {0}")] - Rewards(rewards::RewardsError), - #[error("Expected validator {0} to be in consensus set but got: {1:?}")] - ExpectedValidatorInConsensus(Address, Option), -} - -#[allow(missing_docs)] -#[derive(Error, Debug)] -pub enum BecomeValidatorError { - #[error("The given address {0} is already a validator")] - AlreadyValidator(Address), -} - -#[allow(missing_docs)] -#[derive(Error, Debug)] -pub enum BondError { - #[error("The given address {0} is not a validator address")] - NotAValidator(Address), - #[error( - "The given source address {0} is a validator address. Validators may \ - not delegate." - )] - SourceMustNotBeAValidator(Address), - #[error("The given validator address {0} is inactive")] - InactiveValidator(Address), - #[error("Voting power overflow: {0}")] - VotingPowerOverflow(TryFromIntError), -} - -#[allow(missing_docs)] -#[derive(Error, Debug)] -pub enum UnbondError { - #[error("No bond could be found")] - NoBondFound, - #[error( - "Trying to withdraw more tokens ({0}) than the amount bonded ({0})" - )] - UnbondAmountGreaterThanBond(String, String), - #[error("No bonds found for the validator {0}")] - ValidatorHasNoBonds(Address), - #[error("Voting power not found for the validator {0}")] - ValidatorHasNoVotingPower(Address), - #[error("Voting power overflow: {0}")] - VotingPowerOverflow(TryFromIntError), - #[error("Trying to unbond from a frozen validator: {0}")] - ValidatorIsFrozen(Address), -} - -#[allow(missing_docs)] -#[derive(Error, Debug)] -pub enum WithdrawError { - #[error("No unbond could be found for {0}")] - NoUnbondFound(BondId), - #[error("No unbond may be withdrawn yet for {0}")] - NoWithdrawableUnbond(BondId), -} - -#[allow(missing_docs)] -#[derive(Error, Debug)] -pub enum SlashError { - #[error("The validator {0} has no total deltas value")] - ValidatorHasNoTotalDeltas(Address), - #[error("The validator {0} has no voting power")] - ValidatorHasNoVotingPower(Address), - #[error("Unexpected slash token change")] - InvalidSlashChange(i128), - #[error("Voting power overflow: {0}")] - VotingPowerOverflow(TryFromIntError), - #[error("Unexpected negative stake {0} for validator {1}")] - NegativeStake(i128, Address), -} - -#[allow(missing_docs)] -#[derive(Error, Debug)] -pub enum CommissionRateChangeError { - #[error("Unexpected negative commission rate {0} for validator {1}")] - NegativeRate(Dec, Address), - #[error("Rate change of {0} is too large for validator {1}")] - RateChangeTooLarge(Dec, Address), - #[error( - "There is no maximum rate change written in storage for validator {0}" - )] - NoMaxSetInStorage(Address), - #[error("Cannot write to storage for validator {0}")] - CannotWrite(Address), - #[error("Cannot read storage for validator {0}")] - 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 { - Self::new(err) - } -} - -impl From for storage_api::Error { - fn from(err: BondError) -> Self { - Self::new(err) - } -} - -impl From for storage_api::Error { - fn from(err: UnbondError) -> Self { - Self::new(err) - } -} - -impl From for storage_api::Error { - fn from(err: WithdrawError) -> Self { - Self::new(err) - } -} - -impl From for storage_api::Error { - fn from(err: CommissionRateChangeError) -> Self { - Self::new(err) - } -} - -impl From for storage_api::Error { - fn from(err: InflationError) -> Self { - Self::new(err) - } -} - -impl From for storage_api::Error { - fn from(err: UnjailValidatorError) -> Self { - Self::new(err) - } -} +// ---- Storage handles ---- /// Get the storage handle to the epoched consensus validator set pub fn consensus_validator_set_handle() -> ConsensusValidatorSets { @@ -319,7 +167,8 @@ pub fn validator_commission_rate_handle( CommissionRates::open(key) } -/// Get the storage handle to a bond +/// Get the storage handle to a bond, which is dynamically updated with when +/// unbonding pub fn bond_handle(source: &Address, validator: &Address) -> Bonds { let bond_id = BondId { source: source.clone(), @@ -329,7 +178,8 @@ pub fn bond_handle(source: &Address, validator: &Address) -> Bonds { Bonds::open(key) } -/// Get the storage handle to a validator's total bonds +/// Get the storage handle to a validator's total bonds, which are not updated +/// due to unbonding pub fn total_bonded_handle(validator: &Address) -> Bonds { let key = storage::validator_total_bonded_key(validator); Bonds::open(key) @@ -346,9 +196,9 @@ pub fn unbond_handle(source: &Address, validator: &Address) -> Unbonds { } /// Get the storage handle to a validator's total-unbonded map -pub fn unbond_records_handle(validator: &Address) -> ValidatorUnbondRecords { +pub fn total_unbonded_handle(validator: &Address) -> ValidatorTotalUnbonded { let key = storage::validator_total_unbonded_key(validator); - ValidatorUnbondRecords::open(key) + ValidatorTotalUnbonded::open(key) } /// Get the storage handle to a PoS validator's deltas @@ -394,6 +244,54 @@ pub fn delegator_rewards_products_handle( RewardsProducts::open(key) } +/// Get the storage handle to a validator's incoming redelegations +pub fn validator_incoming_redelegations_handle( + validator: &Address, +) -> IncomingRedelegations { + let key = storage::validator_incoming_redelegations_key(validator); + IncomingRedelegations::open(key) +} + +/// Get the storage handle to a validator's outgoing redelegations +pub fn validator_outgoing_redelegations_handle( + validator: &Address, +) -> OutgoingRedelegations { + let key: Key = storage::validator_outgoing_redelegations_key(validator); + OutgoingRedelegations::open(key) +} + +/// Get the storage handle to a validator's total redelegated bonds +pub fn validator_total_redelegated_bonded_handle( + validator: &Address, +) -> TotalRedelegatedBonded { + let key: Key = storage::validator_total_redelegated_bonded_key(validator); + TotalRedelegatedBonded::open(key) +} + +/// Get the storage handle to a validator's outgoing redelegations +pub fn validator_total_redelegated_unbonded_handle( + validator: &Address, +) -> TotalRedelegatedUnbonded { + let key: Key = storage::validator_total_redelegated_unbonded_key(validator); + TotalRedelegatedUnbonded::open(key) +} + +/// Get the storage handle to a delegator's redelegated bonds information +pub fn delegator_redelegated_bonds_handle( + delegator: &Address, +) -> DelegatorRedelegatedBonded { + let key: Key = storage::delegator_redelegated_bonds_key(delegator); + DelegatorRedelegatedBonded::open(key) +} + +/// Get the storage handle to a delegator's redelegated unbonds information +pub fn delegator_redelegated_unbonds_handle( + delegator: &Address, +) -> DelegatorRedelegatedUnbonded { + let key: Key = storage::delegator_redelegated_unbonds_key(delegator); + DelegatorRedelegatedUnbonded::open(key) +} + /// Init genesis pub fn init_genesis( storage: &mut S, @@ -407,7 +305,7 @@ where tracing::debug!("Initializing PoS genesis"); write_pos_params(storage, params.clone())?; - let mut total_bonded = token::Amount::default(); + let mut total_bonded = token::Amount::zero(); 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)?; @@ -466,20 +364,19 @@ where eth_cold_key, current_epoch, )?; - let delta = token::Change::from(tokens); validator_deltas_handle(&address).init_at_genesis( storage, - delta, + tokens.change(), current_epoch, )?; bond_handle(&address, &address).init_at_genesis( storage, - delta, + tokens, current_epoch, )?; total_bonded_handle(&address).init_at_genesis( storage, - delta, + tokens, current_epoch, )?; validator_commission_rate_handle(&address).init_at_genesis( @@ -501,7 +398,7 @@ where // Credit bonded token amount to the PoS account let staking_token = staking_token_address(storage); - credit_tokens(storage, &staking_token, &ADDRESS, total_bonded)?; + token::credit_tokens(storage, &staking_token, &ADDRESS, total_bonded)?; // Copy the genesis validator set into the pipeline epoch as well for epoch in (current_epoch.next()).iter_range(params.pipeline_len) { copy_validator_sets_and_positions(storage, current_epoch, epoch)?; @@ -634,42 +531,44 @@ where } /// Read PoS validator's delta value. -pub fn read_validator_delta_value( +pub fn read_validator_deltas_value( storage: &S, - params: &PosParams, validator: &Address, - epoch: namada_core::types::storage::Epoch, + epoch: &namada_core::types::storage::Epoch, ) -> storage_api::Result> where S: StorageRead, { let handle = validator_deltas_handle(validator); - handle.get_delta_val(storage, epoch, params) + handle.get_delta_val(storage, *epoch) } /// Read PoS validator's stake (sum of deltas). -/// Returns `None` when the given address is not a validator address. For a -/// validator with `0` stake, this returns `Ok(token::Amount::default())`. +/// For non-validators and validators with `0` stake, this returns the default - +/// `token::Amount::zero()`. pub fn read_validator_stake( storage: &S, params: &PosParams, validator: &Address, epoch: namada_core::types::storage::Epoch, -) -> storage_api::Result> +) -> storage_api::Result where S: StorageRead, { let handle = validator_deltas_handle(validator); let amount = handle .get_sum(storage, epoch, params)? - .map(token::Amount::from_change); + .map(|change| { + debug_assert!(change.non_negative()); + token::Amount::from_change(change) + }) + .unwrap_or_default(); Ok(amount) } /// Add or remove PoS validator's stake delta value pub fn update_validator_deltas( storage: &mut S, - params: &PosParams, validator: &Address, delta: token::Change, current_epoch: namada_core::types::storage::Epoch, @@ -680,7 +579,7 @@ where { let handle = validator_deltas_handle(validator); let val = handle - .get_delta_val(storage, current_epoch + offset, params)? + .get_delta_val(storage, current_epoch + offset)? .unwrap_or_default(); handle.set(storage, val + delta, current_epoch, offset) } @@ -697,7 +596,10 @@ where let handle = total_deltas_handle(); let amnt = handle .get_sum(storage, epoch, params)? - .map(token::Amount::from_change) + .map(|change| { + debug_assert!(change.non_negative()); + token::Amount::from_change(change) + }) .unwrap_or_default(); Ok(amnt) } @@ -848,7 +750,6 @@ where /// Note: for EpochedDelta, write the value to change storage by pub fn update_total_deltas( storage: &mut S, - params: &PosParams, delta: token::Change, current_epoch: namada_core::types::storage::Epoch, offset: u64, @@ -858,7 +759,7 @@ where { let handle = total_deltas_handle(); let val = handle - .get_delta_val(storage, current_epoch + offset, params)? + .get_delta_val(storage, current_epoch + offset)? .unwrap_or_default(); handle.set(storage, val + delta, current_epoch, offset) } @@ -920,13 +821,18 @@ pub fn bond_tokens( where S: StorageRead + StorageWrite, { - let amount = amount.change(); tracing::debug!( "Bonding token amount {} at epoch {current_epoch}", amount.to_string_native() ); + if amount.is_zero() { + return Ok(()); + } + let params = read_pos_params(storage)?; let pipeline_epoch = current_epoch + params.pipeline_len; + + // Check that the source is not a validator if let Some(source) = source { if source != validator && is_validator(storage, source)? { return Err( @@ -934,6 +840,8 @@ where ); } } + + // Check that the validator is actually a validator let validator_state_handle = validator_state_handle(validator); let state = validator_state_handle.get(storage, pipeline_epoch, ¶ms)?; if state.is_none() { @@ -942,6 +850,7 @@ where let source = source.unwrap_or(validator); tracing::debug!("Source {} --> Validator {}", source, validator); + let bond_handle = bond_handle(source, validator); let total_bonded_handle = total_bonded_handle(validator); @@ -955,52 +864,27 @@ where } } - 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.is_zero() { - tracing::debug!( - "bond ∆ at epoch {}: {}", - ep, - delta.to_string_native() - ); - } + if tracing::level_enabled!(tracing::Level::DEBUG) { + let bonds = find_bonds(storage, source, validator)?; + tracing::debug!("\nBonds before incrementing: {bonds:#?}"); } // Initialize or update the bond at the pipeline offset - let offset = params.pipeline_len; - let cur_remain = bond_handle - .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 = total_bonded_handle - .get_delta_val(storage, current_epoch + offset, ¶ms)? - .unwrap_or_default(); - total_bonded_handle.set( + bond_handle.add(storage, amount, current_epoch, params.pipeline_len)?; + total_bonded_handle.add( storage, - cur_remain_global + amount, + amount, current_epoch, - offset, + params.pipeline_len, )?; - 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.is_zero() { - tracing::debug!( - "bond ∆ at epoch {}: {}", - ep, - delta.to_string_native() - ); - } + if tracing::level_enabled!(tracing::Level::DEBUG) { + let bonds = find_bonds(storage, source, validator)?; + tracing::debug!("\nBonds after incrementing: {bonds:#?}"); } // Update the validator set - // We allow bonding if the validator is jailed, however if jailed, there + // Allow bonding even if the validator is jailed. However, if jailed, there // must be no changes to the validator set. Check at the pipeline epoch. let is_jailed_at_pipeline = matches!( validator_state_handle @@ -1013,32 +897,30 @@ where storage, ¶ms, validator, - amount, - current_epoch, + amount.change(), + pipeline_epoch, )?; } // Update the validator and total deltas update_validator_deltas( storage, - ¶ms, validator, - amount, + amount.change(), current_epoch, - offset, + params.pipeline_len, )?; - update_total_deltas(storage, ¶ms, amount, current_epoch, offset)?; + update_total_deltas( + storage, + amount.change(), + current_epoch, + params.pipeline_len, + )?; // Transfer the bonded tokens from the source to PoS let staking_token = staking_token_address(storage); - transfer_tokens( - storage, - &staking_token, - token::Amount::from_change(amount), - source, - &ADDRESS, - )?; + token::transfer(storage, &staking_token, source, &ADDRESS, amount)?; Ok(()) } @@ -1155,7 +1037,7 @@ fn update_validator_set( params: &PosParams, validator: &Address, token_change: token::Change, - current_epoch: Epoch, + epoch: Epoch, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, @@ -1163,26 +1045,23 @@ where if token_change.is_zero() { return Ok(()); } - let pipeline_epoch = current_epoch + params.pipeline_len; + // let pipeline_epoch = current_epoch + params.pipeline_len; tracing::debug!( - "Update epoch for validator set: {pipeline_epoch}, validator: \ - {validator}" + "Update epoch for validator set: {epoch}, validator: {validator}" ); let consensus_validator_set = consensus_validator_set_handle(); let below_capacity_validator_set = below_capacity_validator_set_handle(); // Validator sets at the pipeline offset - let consensus_val_handle = consensus_validator_set.at(&pipeline_epoch); - let below_capacity_val_handle = - below_capacity_validator_set.at(&pipeline_epoch); + let consensus_val_handle = consensus_validator_set.at(&epoch); + let below_capacity_val_handle = below_capacity_validator_set.at(&epoch); - let tokens_pre = - read_validator_stake(storage, params, validator, pipeline_epoch)? - .unwrap_or_default(); + let tokens_pre = read_validator_stake(storage, params, validator, epoch)?; // tracing::debug!("VALIDATOR STAKE BEFORE UPDATE: {}", tokens_pre); let tokens_post = tokens_pre.change() + token_change; + debug_assert!(tokens_post.non_negative()); let tokens_post = token::Amount::from_change(tokens_post); // If token amounts both before and after the action are below the threshold @@ -1195,12 +1074,8 @@ where // The position is only set when the validator is in consensus or // below_capacity set (not in below_threshold set) - let position = read_validator_set_position( - storage, - validator, - pipeline_epoch, - params, - )?; + let position = + read_validator_set_position(storage, validator, epoch, params)?; if let Some(position) = position { let consensus_vals_pre = consensus_val_handle.at(&tokens_pre); @@ -1234,13 +1109,13 @@ where validator_state_handle(validator).set( storage, ValidatorState::BelowThreshold, - current_epoch, - params.pipeline_len, + epoch, + 0, )?; // Remove the validator's position from storage validator_set_positions_handle() - .at(&pipeline_epoch) + .at(&epoch) .remove(storage, validator)?; // Promote the next below-cap validator if there is one @@ -1265,14 +1140,14 @@ where insert_validator_into_set( &consensus_val_handle.at(&max_bc_amount), storage, - &pipeline_epoch, + &epoch, &removed_max_below_capacity, )?; validator_state_handle(&removed_max_below_capacity).set( storage, ValidatorState::Consensus, - current_epoch, - params.pipeline_len, + epoch, + 0, )?; } } else if tokens_post < max_below_capacity_validator_amount { @@ -1300,28 +1175,28 @@ where &consensus_val_handle .at(&max_below_capacity_validator_amount), storage, - &pipeline_epoch, + &epoch, &removed_max_below_capacity, )?; validator_state_handle(&removed_max_below_capacity).set( storage, ValidatorState::Consensus, - current_epoch, - params.pipeline_len, + epoch, + 0, )?; // Insert the current validator into the below-capacity set insert_validator_into_set( &below_capacity_val_handle.at(&tokens_post.into()), storage, - &pipeline_epoch, + &epoch, validator, )?; validator_state_handle(validator).set( storage, ValidatorState::BelowCapacity, - current_epoch, - params.pipeline_len, + epoch, + 0, )?; } else { tracing::debug!("Validator remains in consensus set"); @@ -1330,7 +1205,7 @@ where insert_validator_into_set( &consensus_val_handle.at(&tokens_post), storage, - &pipeline_epoch, + &epoch, validator, )?; } @@ -1361,11 +1236,10 @@ where insert_into_consensus_and_demote_to_below_cap( storage, - params, validator, tokens_post, min_consensus_validator_amount, - current_epoch, + epoch, &consensus_val_handle, &below_capacity_val_handle, )?; @@ -1375,14 +1249,14 @@ where insert_validator_into_set( &below_capacity_val_handle.at(&tokens_post.into()), storage, - &pipeline_epoch, + &epoch, validator, )?; validator_state_handle(validator).set( storage, ValidatorState::BelowCapacity, - current_epoch, - params.pipeline_len, + epoch, + 0, )?; } else { // The current validator is demoted to the below-threshold set @@ -1393,13 +1267,13 @@ where validator_state_handle(validator).set( storage, ValidatorState::BelowThreshold, - current_epoch, - params.pipeline_len, + epoch, + 0, )?; // Remove the validator's position from storage validator_set_positions_handle() - .at(&pipeline_epoch) + .at(&epoch) .remove(storage, validator)?; } } @@ -1411,7 +1285,7 @@ where // Move the validator into the appropriate set let num_consensus_validators = - get_num_consensus_validators(storage, pipeline_epoch)?; + get_num_consensus_validators(storage, epoch)?; if num_consensus_validators < params.max_validator_slots { // Just insert into the consensus set tracing::debug!("Inserting validator into the consensus set"); @@ -1419,14 +1293,14 @@ where insert_validator_into_set( &consensus_val_handle.at(&tokens_post), storage, - &pipeline_epoch, + &epoch, validator, )?; validator_state_handle(validator).set( storage, ValidatorState::Consensus, - current_epoch, - params.pipeline_len, + epoch, + 0, )?; } else { let min_consensus_validator_amount = @@ -1444,11 +1318,10 @@ where insert_into_consensus_and_demote_to_below_cap( storage, - params, validator, tokens_post, min_consensus_validator_amount, - current_epoch, + epoch, &consensus_val_handle, &below_capacity_val_handle, )?; @@ -1461,14 +1334,14 @@ where insert_validator_into_set( &below_capacity_val_handle.at(&tokens_post.into()), storage, - &pipeline_epoch, + &epoch, validator, )?; validator_state_handle(validator).set( storage, ValidatorState::BelowCapacity, - current_epoch, - params.pipeline_len, + epoch, + 0, )?; } } @@ -1480,11 +1353,10 @@ where #[allow(clippy::too_many_arguments)] fn insert_into_consensus_and_demote_to_below_cap( storage: &mut S, - params: &PosParams, validator: &Address, tokens_post: token::Amount, min_consensus_amount: token::Amount, - current_epoch: Epoch, + epoch: Epoch, consensus_set: &ConsensusValidatorSet, below_capacity_set: &BelowCapacityValidatorSet, ) -> storage_api::Result<()> @@ -1500,35 +1372,35 @@ where .remove(storage, &last_position_of_min_consensus_vals)? .expect("There must be always be at least 1 consensus validator"); - let pipeline_epoch = current_epoch + params.pipeline_len; + // let pipeline_epoch = current_epoch + params.pipeline_len; // Insert the min consensus validator into the below-capacity // set insert_validator_into_set( &below_capacity_set.at(&min_consensus_amount.into()), storage, - &pipeline_epoch, + &epoch, &removed_min_consensus, )?; validator_state_handle(&removed_min_consensus).set( storage, ValidatorState::BelowCapacity, - current_epoch, - params.pipeline_len, + epoch, + 0, )?; // Insert the current validator into the consensus set insert_validator_into_set( &consensus_set.at(&tokens_post), storage, - &pipeline_epoch, + &epoch, validator, )?; validator_state_handle(validator).set( storage, ValidatorState::Consensus, - current_epoch, - params.pipeline_len, + epoch, + 0, )?; Ok(()) } @@ -1583,8 +1455,6 @@ where below_cap_in_mem.insert((stake, position), address); } - tracing::debug!("{consensus_in_mem:?}"); - for ((val_stake, val_position), val_address) in consensus_in_mem.into_iter() { consensus_validator_set @@ -1592,11 +1462,6 @@ where .at(&val_stake) .insert(storage, val_position, val_address)?; } - tracing::debug!("New validator set should be inserted:"); - tracing::debug!( - "{:?}", - read_consensus_validator_set_addresses(storage, target_epoch)? - ); for ((val_stake, val_position), val_address) in below_cap_in_mem.into_iter() { @@ -1842,23 +1707,42 @@ struct BondAndUnbondUpdates { unbond_value: token::Change, } +/// Temp: In quint this is from `ResultUnbondTx` field `resultSlashing: {sum: +/// int, epochMap: Epoch -> int}` +#[derive(Debug, Default)] +pub struct ResultSlashing { + /// The token amount unbonded from the validator stake after accounting for + /// slashes + pub sum: token::Amount, + /// Map from bond start epoch to token amount after slashing + pub epoch_map: BTreeMap, +} + /// Unbond tokens that are bonded between a validator and a source (self or -/// delegator) +/// delegator). +/// +/// This fn is also called during redelegation for a source validator, in +/// which case the `is_redelegation` param must be true. pub fn unbond_tokens( storage: &mut S, source: Option<&Address>, validator: &Address, amount: token::Amount, current_epoch: Epoch, -) -> storage_api::Result<()> + is_redelegation: bool, +) -> storage_api::Result where S: StorageRead + StorageWrite, { - let amount = amount.change(); tracing::debug!( - "Unbonding token amount {} at epoch {current_epoch}", - amount.to_string_native() + "Unbonding token amount {} at epoch {}", + amount.to_string_native(), + current_epoch ); + if amount.is_zero() { + return Ok(ResultSlashing::default()); + } + let params = read_pos_params(storage)?; let pipeline_epoch = current_epoch + params.pipeline_len; @@ -1879,146 +1763,252 @@ where return Err(UnbondError::ValidatorIsFrozen(validator.clone()).into()); } - // Should be able to unbond inactive validators - - // 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) = - // validator_state_handle.get(storage, epoch, ¶ms)? - // { - // return - // Err(BondError::InactiveValidator(validator.clone()).into()); } - // } + // TODO: check that validator is not inactive (when implemented)! let source = source.unwrap_or(validator); let bonds_handle = bond_handle(source, validator); - 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.is_zero() { - tracing::debug!( - "bond ∆ at epoch {}: {}", - ep, - delta.to_string_native() - ); - } - } - // Make sure there are enough tokens left in the bond at the pipeline offset let remaining_at_pipeline = bonds_handle .get_sum(storage, pipeline_epoch, ¶ms)? .unwrap_or_default(); if amount > remaining_at_pipeline { return Err(UnbondError::UnbondAmountGreaterThanBond( - token::Amount::from_change(amount).to_string_native(), - token::Amount::from_change(remaining_at_pipeline) - .to_string_native(), + amount.to_string_native(), + remaining_at_pipeline.to_string_native(), ) .into()); } + if tracing::level_enabled!(tracing::Level::DEBUG) { + let bonds = find_bonds(storage, source, validator)?; + tracing::debug!("\nBonds before decrementing: {bonds:#?}"); + } + let unbonds = unbond_handle(source, validator); - // TODO: think if this should be +1 or not!!! let withdrawable_epoch = current_epoch + params.withdrawable_epoch_offset(); - let mut remaining = amount; - let mut amount_after_slashing = token::Change::default(); + let redelegated_bonds = + delegator_redelegated_bonds_handle(source).at(validator); - // 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> = - bonds_handle.get_data_handler().iter(storage)?.collect(); + #[cfg(debug_assertions)] + let redel_bonds_pre = redelegated_bonds.collect_map(storage)?; - let mut bond_iter = bonds.into_iter().rev(); - let mut new_bond_values = HashSet::::new(); + // `resultUnbonding` + // Find the bonds to fully unbond (remove) and one to partially unbond, if + // necessary + let bonds_to_unbond = find_bonds_to_remove( + storage, + &bonds_handle.get_data_handler(), + amount, + )?; - while remaining > token::Change::default() { - let bond = bond_iter.next().transpose()?; - if bond.is_none() { - continue; + // `modifiedRedelegation` + // A bond may have both redelegated and non-redelegated tokens in it. If + // this is the case, compute the modified state of the redelegation. + let modified_redelegation = match bonds_to_unbond.new_entry { + Some((bond_epoch, new_bond_amount)) => { + if redelegated_bonds.contains(storage, &bond_epoch)? { + let cur_bond_amount = bonds_handle + .get_delta_val(storage, bond_epoch)? + .unwrap_or_default(); + compute_modified_redelegation( + storage, + &redelegated_bonds.at(&bond_epoch), + bond_epoch, + cur_bond_amount - new_bond_amount, + )? + } else { + ModifiedRedelegation::default() + } } - let (bond_epoch, bond_amount) = bond.unwrap(); - // println!("\nBond (epoch, amnt) = ({}, {})", bond_epoch, bond_amount); - // println!("remaining = {}", remaining); + None => ModifiedRedelegation::default(), + }; - let to_unbond = cmp::min(bond_amount, remaining); - new_bond_values.insert(BondAndUnbondUpdates { - bond_start: bond_epoch, - new_bond_value: bond_amount - to_unbond, - unbond_value: to_unbond, - }); - // println!("to_unbond (init) = {}", to_unbond); + // Compute the new unbonds eagerly + // `keysUnbonds` + // Get a set of epochs from which we're unbonding (fully and partially). + let bond_epochs_to_unbond = + if let Some((start_epoch, _)) = bonds_to_unbond.new_entry { + let mut to_remove = bonds_to_unbond.epochs.clone(); + to_remove.insert(start_epoch); + to_remove + } else { + bonds_to_unbond.epochs.clone() + }; - let slashes_for_this_bond = - find_slashes_in_range(storage, bond_epoch, None, validator)?; + // `newUnbonds` + // For each epoch we're unbonding, find the amount that's being unbonded. + // For full unbonds, this is the current bond value. For partial unbonds + // it is a difference between the current and new bond amount. + let new_unbonds_map = bond_epochs_to_unbond + .into_iter() + .map(|epoch| { + let cur_bond_value = bonds_handle + .get_delta_val(storage, epoch) + .unwrap() + .unwrap_or_default(); + let value = if let Some((start_epoch, new_bond_amount)) = + bonds_to_unbond.new_entry + { + if start_epoch == epoch { + cur_bond_value - new_bond_amount + } else { + cur_bond_value + } + } else { + cur_bond_value + }; + (epoch, value) + }) + .collect::>(); - 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); + // `updatedBonded` + // Remove bonds for all the full unbonds. + for epoch in &bonds_to_unbond.epochs { + bonds_handle.get_data_handler().remove(storage, epoch)?; + } + // Replace bond amount for partial unbond, if any. + if let Some((bond_epoch, new_bond_amount)) = bonds_to_unbond.new_entry { + bonds_handle.set(storage, new_bond_amount, bond_epoch, 0)?; + } - // 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( + // `updatedUnbonded` + // Update the unbonds in storage using the eager map computed above + if !is_redelegation { + for (start_epoch, &unbond_amount) in new_unbonds_map.iter() { + unbonds.at(start_epoch).update( storage, - bond_epoch, - cur_amnt + token::Amount::from_change(to_unbond), + withdrawable_epoch, + |cur_val| cur_val.unwrap_or_default() + unbond_amount, )?; + } + } - remaining -= to_unbond; + // `newRedelegatedUnbonds` + // This is what the delegator's redelegated unbonds would look like if this + // was the only unbond in the PoS system. We need to add these redelegated + // unbonds to the existing redelegated unbonds + let new_redelegated_unbonds = compute_new_redelegated_unbonds( + storage, + &redelegated_bonds, + &bonds_to_unbond.epochs, + &modified_redelegation, + )?; + + // `updatedRedelegatedBonded` + // NOTE: for now put this here after redelegated unbonds calc bc that one + // uses the pre-modified redelegated bonds from storage! + // First remove redelegation entries in epochs with full unbonds. + for epoch_to_remove in &bonds_to_unbond.epochs { + redelegated_bonds.remove_all(storage, epoch_to_remove)?; + } + if let Some(epoch) = modified_redelegation.epoch { + tracing::debug!("\nIs modified redelegation"); + if modified_redelegation.validators_to_remove.is_empty() { + redelegated_bonds.remove_all(storage, &epoch)?; + } else { + // Then update the redelegated bonds at this epoch + let rbonds = redelegated_bonds.at(&epoch); + update_redelegated_bonds(storage, &rbonds, &modified_redelegation)?; + } } - drop(bond_iter); - // Write the in-memory bond and unbond values back to storage - for BondAndUnbondUpdates { - bond_start, - new_bond_value, - unbond_value, - } in new_bond_values.into_iter() - { - bonds_handle.set(storage, new_bond_value, bond_start, 0)?; - update_unbond( - &unbonds, - storage, - &withdrawable_epoch, - &bond_start, - token::Amount::from_change(unbond_value), - )?; + if !is_redelegation { + // `val updatedRedelegatedUnbonded` with updates applied below + // Delegator's redelegated unbonds to this validator. + let delegator_redelegated_unbonded = + delegator_redelegated_unbonds_handle(source).at(validator); + + // Quint `def updateRedelegatedUnbonded` with `val + // updatedRedelegatedUnbonded` together with last statement + // in `updatedDelegator.with("redelegatedUnbonded", ...` updated + // directly in storage + for (start, unbonds) in &new_redelegated_unbonds { + let this_redelegated_unbonded = delegator_redelegated_unbonded + .at(start) + .at(&withdrawable_epoch); + + // Update the delegator's redelegated unbonds with the change + for (src_validator, redelegated_unbonds) in unbonds { + let redelegated_unbonded = + this_redelegated_unbonded.at(src_validator); + for (&redelegation_epoch, &change) in redelegated_unbonds { + redelegated_unbonded.update( + storage, + redelegation_epoch, + |current| current.unwrap_or_default() + change, + )?; + } + } + } + } + // all `val updatedDelegator` changes are applied at this point + + // `val updatedTotalBonded` and `val updatedTotalUnbonded` with updates + // Update the validator's total bonded and unbonded amounts + let total_bonded = total_bonded_handle(validator).get_data_handler(); + let total_unbonded = total_unbonded_handle(validator).at(&pipeline_epoch); + for (&start_epoch, &amount) in &new_unbonds_map { + total_bonded.update(storage, start_epoch, |current| { + current.unwrap_or_default() - amount + })?; + total_unbonded.update(storage, start_epoch, |current| { + current.unwrap_or_default() + amount + })?; } - 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.is_zero() { - tracing::debug!( - "bond ∆ at epoch {}: {}", - ep, - delta.to_string_native() - ); + let total_redelegated_bonded = + validator_total_redelegated_bonded_handle(validator); + let total_redelegated_unbonded = + validator_total_redelegated_unbonded_handle(validator); + for (redelegation_start_epoch, unbonds) in &new_redelegated_unbonds { + for (src_validator, changes) in unbonds { + for (bond_start_epoch, change) in changes { + // total redelegated bonded + let bonded_sub_map = total_redelegated_bonded + .at(redelegation_start_epoch) + .at(src_validator); + bonded_sub_map.update( + storage, + *bond_start_epoch, + |current| current.unwrap_or_default() - *change, + )?; + + // total redelegated unbonded + let unbonded_sub_map = total_redelegated_unbonded + .at(&pipeline_epoch) + .at(redelegation_start_epoch) + .at(src_validator); + unbonded_sub_map.update( + storage, + *bond_start_epoch, + |current| current.unwrap_or_default() + *change, + )?; + } } } - tracing::debug!( - "Token change including slashes on unbond = {}", - (-amount_after_slashing).to_string_native() + + let slashes = find_validator_slashes(storage, validator)?; + // `val resultSlashing` + let result_slashing = compute_amount_after_slashing_unbond( + storage, + ¶ms, + &new_unbonds_map, + &new_redelegated_unbonds, + slashes, + )?; + #[cfg(debug_assertions)] + let redel_bonds_post = redelegated_bonds.collect_map(storage)?; + debug_assert!( + result_slashing.sum <= amount, + "Amount after slashing ({}) must be <= requested amount to unbond \ + ({}).", + result_slashing.sum.to_string_native(), + amount.to_string_native(), ); + let change_after_slashing = -result_slashing.sum.change(); // Update the validator set at the pipeline offset. Since unbonding from a // jailed validator who is no longer frozen is allowed, only update the // validator set if the validator is not jailed @@ -2033,54 +2023,548 @@ where storage, ¶ms, validator, - -amount_after_slashing, - current_epoch, + change_after_slashing, + pipeline_epoch, )?; } // Update the validator and total deltas at the pipeline offset update_validator_deltas( storage, - ¶ms, validator, - -amount_after_slashing, + change_after_slashing, current_epoch, params.pipeline_len, )?; update_total_deltas( storage, - ¶ms, - -amount_after_slashing, + change_after_slashing, current_epoch, params.pipeline_len, )?; - Ok(()) + if tracing::level_enabled!(tracing::Level::DEBUG) { + let bonds = find_bonds(storage, source, validator)?; + tracing::debug!("\nBonds after decrementing: {bonds:#?}"); + } + + // Invariant: in the affected epochs, the delta of bonds must be >= delta of + // redelegated bonds deltas sum + #[cfg(debug_assertions)] + { + let mut epochs = bonds_to_unbond.epochs.clone(); + if let Some((epoch, _)) = bonds_to_unbond.new_entry { + epochs.insert(epoch); + } + for epoch in epochs { + let cur_bond = bonds_handle + .get_delta_val(storage, epoch)? + .unwrap_or_default(); + let redelegated_deltas = redelegated_bonds + .at(&epoch) + // Sum of redelegations from any src validator + .collect_map(storage)? + .into_values() + .map(|redeleg| redeleg.into_values().sum()) + .sum(); + debug_assert!( + cur_bond >= redelegated_deltas, + "After unbonding, in epoch {epoch} the bond amount {} must be \ + >= redelegated deltas at pipeline {}.\n\nredelegated_bonds \ + pre: {redel_bonds_pre:#?}\nredelegated_bonds post: \ + {redel_bonds_post:#?},\nmodified_redelegation: \ + {modified_redelegation:#?},\nbonds_to_unbond: \ + {bonds_to_unbond:#?}", + cur_bond.to_string_native(), + redelegated_deltas.to_string_native() + ); + } + } + + Ok(result_slashing) } -/// 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. -fn get_slashed_amount( +#[derive(Debug, Default, Eq, PartialEq)] +struct FoldRedelegatedBondsResult { + total_redelegated: token::Amount, + total_after_slashing: token::Amount, +} + +/// Iterates over a `redelegated_unbonds` and computes the both the sum of all +/// redelegated tokens and how much is left after applying all relevant slashes. +// `def foldAndSlashRedelegatedBondsMap` +fn fold_and_slash_redelegated_bonds( + storage: &S, params: &PosParams, - amount: token::Amount, - slashes: &BTreeMap, -) -> storage_api::Result { - // println!("FN `get_slashed_amount`"); + redelegated_unbonds: &EagerRedelegatedBondsMap, + start_epoch: Epoch, + list_slashes: &[Slash], + slash_epoch_filter: impl Fn(Epoch) -> bool, +) -> FoldRedelegatedBondsResult +where + S: StorageRead, +{ + let mut result = FoldRedelegatedBondsResult::default(); + for (src_validator, bonds_map) in redelegated_unbonds { + for (bond_start, &change) in bonds_map { + // Merge the two lists of slashes + let mut merged: Vec = + // Look-up slashes for this validator ... + validator_slashes_handle(src_validator) + .iter(storage) + .unwrap() + .map(Result::unwrap) + .filter(|slash| { + params.in_redelegation_slashing_window( + slash.epoch, + params.redelegation_start_epoch_from_end( + start_epoch, + ), + start_epoch, + ) && *bond_start <= slash.epoch + && slash_epoch_filter(slash.epoch) + }) + // ... and add `list_slashes` + .chain(list_slashes.iter().cloned()) + .collect(); + + // Sort slashes by epoch + merged.sort_by(|s1, s2| s1.epoch.partial_cmp(&s2.epoch).unwrap()); + + result.total_redelegated += change; + result.total_after_slashing += + apply_list_slashes(params, &merged, change); + } + } + result +} - let mut updated_amount = amount; - let mut computed_amounts = Vec::::new(); +/// Computes how much remains from an amount of tokens after applying a list of +/// slashes. +/// +/// - `slashes` - a list of slashes ordered by misbehaving epoch. +/// - `amount` - the amount of slashable tokens. +// `def applyListSlashes` +fn apply_list_slashes( + params: &PosParams, + slashes: &[Slash], + amount: token::Amount, +) -> token::Amount { + let mut final_amount = amount; + let mut computed_slashes = BTreeMap::::new(); + for slash in slashes { + let slashed_amount = + compute_slashable_amount(params, slash, amount, &computed_slashes); + final_amount = + final_amount.checked_sub(slashed_amount).unwrap_or_default(); + computed_slashes.insert(slash.epoch, slashed_amount); + } + final_amount +} + +/// Computes how much is left from a bond or unbond after applying a slash given +/// that a set of slashes may have been previously applied. +// `def computeSlashableAmount` +fn compute_slashable_amount( + params: &PosParams, + slash: &Slash, + amount: token::Amount, + computed_slashes: &BTreeMap, +) -> token::Amount { + let updated_amount = computed_slashes + .iter() + .filter(|(&epoch, _)| { + // Keep slashes that have been applied and processed before the + // current slash occurred. We use `<=` because slashes processed at + // `slash.epoch` (at the start of the epoch) are also processed + // before this slash occurred. + epoch + params.slash_processing_epoch_offset() <= slash.epoch + }) + .fold(amount, |acc, (_, &amnt)| { + acc.checked_sub(amnt).unwrap_or_default() + }); + updated_amount.mul_ceil(slash.rate) +} + +/// Epochs for full and partial unbonds. +#[derive(Debug, Default)] +struct BondsForRemovalRes { + /// Full unbond epochs + pub epochs: BTreeSet, + /// Partial unbond epoch associated with the new bond amount + pub new_entry: Option<(Epoch, token::Amount)>, +} + +/// In decreasing epoch order, decrement the non-zero bond amount entries until +/// the full `amount` has been removed. Returns a `BondsForRemovalRes` object +/// that contains the epochs for which the full bond amount is removed and +/// additionally information for the one epoch whose bond amount is partially +/// removed, if any. +fn find_bonds_to_remove( + storage: &S, + bonds_handle: &LazyMap, + amount: token::Amount, +) -> storage_api::Result +where + S: StorageRead, +{ + #[allow(clippy::needless_collect)] + let bonds: Vec> = bonds_handle.iter(storage)?.collect(); + + let mut bonds_for_removal = BondsForRemovalRes::default(); + let mut remaining = amount; + + for bond in bonds.into_iter().rev() { + let (bond_epoch, bond_amount) = bond?; + let to_unbond = cmp::min(bond_amount, remaining); + if to_unbond == bond_amount { + bonds_for_removal.epochs.insert(bond_epoch); + } else { + bonds_for_removal.new_entry = + Some((bond_epoch, bond_amount - to_unbond)); + } + remaining -= to_unbond; + if remaining.is_zero() { + break; + } + } + Ok(bonds_for_removal) +} + +#[derive(Debug, Default, PartialEq, Eq)] +struct ModifiedRedelegation { + epoch: Option, + validators_to_remove: BTreeSet
, + validator_to_modify: Option
, + epochs_to_remove: BTreeSet, + epoch_to_modify: Option, + new_amount: Option, +} + +/// Used in `fn unbond_tokens` to compute the modified state of a redelegation +/// if redelegated tokens are being unbonded. +fn compute_modified_redelegation( + storage: &S, + redelegated_bonds: &RedelegatedTokens, + start_epoch: Epoch, + amount_to_unbond: token::Amount, +) -> storage_api::Result +where + S: StorageRead, +{ + let mut modified_redelegation = ModifiedRedelegation::default(); + + let mut src_validators = BTreeSet::
::new(); + let mut total_redelegated = token::Amount::zero(); + for rb in redelegated_bonds.iter(storage)? { + let ( + NestedSubKey::Data { + key: src_validator, + nested_sub_key: _, + }, + amount, + ) = rb?; + total_redelegated += amount; + src_validators.insert(src_validator); + } + + modified_redelegation.epoch = Some(start_epoch); + + // If the total amount of redelegated bonds is less than the target amount, + // then all redelegated bonds must be unbonded. + if total_redelegated <= amount_to_unbond { + return Ok(modified_redelegation); + } + + let mut remaining = amount_to_unbond; + for src_validator in src_validators.into_iter() { + if remaining.is_zero() { + break; + } + let rbonds = redelegated_bonds.at(&src_validator); + let total_src_val_amount = rbonds + .iter(storage)? + .map(|res| { + let (_, amount) = res?; + Ok(amount) + }) + .sum::>()?; + + // TODO: move this into the `if total_redelegated <= remaining` branch + // below, then we don't have to remove it in `fn + // update_redelegated_bonds` when `validator_to_modify` is Some (and + // avoid `modified_redelegation.validators_to_remove.clone()`). + // It affects assumption 2. in `fn compute_new_redelegated_unbonds`, but + // that looks trivial to change. + // NOTE: not sure if this TODO is still relevant... + modified_redelegation + .validators_to_remove + .insert(src_validator.clone()); + if total_src_val_amount <= remaining { + remaining -= total_src_val_amount; + } else { + let bonds_to_remove = + find_bonds_to_remove(storage, &rbonds, remaining)?; + + remaining = token::Amount::zero(); + + // NOTE: When there are multiple `src_validators` from which we're + // unbonding, `validator_to_modify` cannot get overriden, because + // only one of them can be a partial unbond (`new_entry` + // is partial unbond) + if let Some((bond_epoch, new_bond_amount)) = + bonds_to_remove.new_entry + { + modified_redelegation.validator_to_modify = Some(src_validator); + modified_redelegation.epochs_to_remove = { + let mut epochs = bonds_to_remove.epochs; + // TODO: remove this insertion then we don't have to remove + // it again in `fn update_redelegated_bonds` + // when `epoch_to_modify` is Some (and avoid + // `modified_redelegation.epochs_to_remove.clone`) + // It affects assumption 3. in `fn + // compute_new_redelegated_unbonds`, but that also looks + // trivial to change. + epochs.insert(bond_epoch); + epochs + }; + modified_redelegation.epoch_to_modify = Some(bond_epoch); + modified_redelegation.new_amount = Some(new_bond_amount); + } else { + modified_redelegation.validator_to_modify = Some(src_validator); + modified_redelegation.epochs_to_remove = bonds_to_remove.epochs; + } + } + } + Ok(modified_redelegation) +} + +fn update_redelegated_bonds( + storage: &mut S, + redelegated_bonds: &RedelegatedTokens, + modified_redelegation: &ModifiedRedelegation, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + if let Some(val_to_modify) = &modified_redelegation.validator_to_modify { + let mut updated_vals_to_remove = + modified_redelegation.validators_to_remove.clone(); + updated_vals_to_remove.remove(val_to_modify); + + // Remove the updated_vals_to_remove keys from the + // redelegated_bonds map + for val in &updated_vals_to_remove { + redelegated_bonds.remove_all(storage, val)?; + } + + if let Some(epoch_to_modify) = modified_redelegation.epoch_to_modify { + let mut updated_epochs_to_remove = + modified_redelegation.epochs_to_remove.clone(); + updated_epochs_to_remove.remove(&epoch_to_modify); + let val_bonds_to_modify = redelegated_bonds.at(val_to_modify); + for epoch in updated_epochs_to_remove { + val_bonds_to_modify.remove(storage, &epoch)?; + } + val_bonds_to_modify.insert( + storage, + epoch_to_modify, + modified_redelegation.new_amount.unwrap(), + )?; + } else { + // Then remove to epochs_to_remove from the redelegated bonds of the + // val_to_modify + let val_bonds_to_modify = redelegated_bonds.at(val_to_modify); + for epoch in &modified_redelegation.epochs_to_remove { + val_bonds_to_modify.remove(storage, epoch)?; + } + } + } else { + // Remove all validators in modified_redelegation.validators_to_remove + // from redelegated_bonds + for val in &modified_redelegation.validators_to_remove { + redelegated_bonds.remove_all(storage, val)?; + } + } + Ok(()) +} + +/// Temp helper type to match quint model. +/// Result of `compute_new_redelegated_unbonds` that contains a map of +/// redelegated unbonds. +/// The map keys from outside in are: +/// +/// - redelegation end epoch where redeleg stops contributing to src validator +/// - src validator address +/// - src bond start epoch where it started contributing to src validator +// TODO: refactor out +type EagerRedelegatedUnbonds = BTreeMap; + +/// Computes a map of redelegated unbonds from a set of redelegated bonds. +/// +/// - `redelegated_bonds` - a map of redelegated bonds from epoch to +/// `RedelegatedTokens`. +/// - `epochs_to_remove` - a set of epochs that indicate the set of epochs +/// unbonded. +/// - `modified` record that represents a redelegated bond that it is only +/// partially unbonded. +/// +/// The function assumes that: +/// +/// 1. `modified.epoch` is not in the `epochs_to_remove` set. +/// 2. `modified.validator_to_modify` is in `modified.vals_to_remove`. +/// 3. `modified.epoch_to_modify` is in in `modified.epochs_to_remove`. +// TODO: try to optimize this by only writing to storage via Lazy! +// `def computeNewRedelegatedUnbonds` from Quint +fn compute_new_redelegated_unbonds( + storage: &S, + redelegated_bonds: &RedelegatedBondsOrUnbonds, + epochs_to_remove: &BTreeSet, + modified: &ModifiedRedelegation, +) -> storage_api::Result +where + S: StorageRead + StorageWrite, +{ + let unbonded_epochs = if let Some(epoch) = modified.epoch { + debug_assert!( + !epochs_to_remove.contains(&epoch), + "1. assumption in `fn compute_new_redelegated_unbonds` doesn't \ + hold" + ); + let mut epochs = epochs_to_remove.clone(); + epochs.insert(epoch); + epochs + .iter() + .cloned() + .filter(|e| redelegated_bonds.contains(storage, e).unwrap()) + .collect::>() + } else { + epochs_to_remove + .iter() + .cloned() + .filter(|e| redelegated_bonds.contains(storage, e).unwrap()) + .collect::>() + }; + debug_assert!( + modified + .validator_to_modify + .as_ref() + .map(|validator| modified.validators_to_remove.contains(validator)) + .unwrap_or(true), + "2. assumption in `fn compute_new_redelegated_unbonds` doesn't hold" + ); + debug_assert!( + modified + .epoch_to_modify + .as_ref() + .map(|epoch| modified.epochs_to_remove.contains(epoch)) + .unwrap_or(true), + "3. assumption in `fn compute_new_redelegated_unbonds` doesn't hold" + ); - for (infraction_epoch, slash_rate) in slashes { - // println!("Slash epoch: {}, rate: {}", infraction_epoch, slash_rate); + // quint `newRedelegatedUnbonds` returned from + // `computeNewRedelegatedUnbonds` + let new_redelegated_unbonds: EagerRedelegatedUnbonds = unbonded_epochs + .into_iter() + .map(|start| { + let mut rbonds = EagerRedelegatedBondsMap::default(); + if modified + .epoch + .map(|redelegation_epoch| start != redelegation_epoch) + .unwrap_or(true) + || modified.validators_to_remove.is_empty() + { + for res in redelegated_bonds.at(&start).iter(storage).unwrap() { + let ( + NestedSubKey::Data { + key: validator, + nested_sub_key: SubKey::Data(epoch), + }, + amount, + ) = res.unwrap(); + rbonds + .entry(validator.clone()) + .or_default() + .insert(epoch, amount); + } + (start, rbonds) + } else { + for src_validator in &modified.validators_to_remove { + if modified + .validator_to_modify + .as_ref() + .map(|validator| src_validator != validator) + .unwrap_or(true) + { + let raw_bonds = + redelegated_bonds.at(&start).at(src_validator); + for res in raw_bonds.iter(storage).unwrap() { + let (bond_epoch, bond_amount) = res.unwrap(); + rbonds + .entry(src_validator.clone()) + .or_default() + .insert(bond_epoch, bond_amount); + } + } else { + for bond_start in &modified.epochs_to_remove { + let cur_redel_bond_amount = redelegated_bonds + .at(&start) + .at(src_validator) + .get(storage, bond_start) + .unwrap() + .unwrap_or_default(); + let raw_bonds = rbonds + .entry(src_validator.clone()) + .or_default(); + if modified + .epoch_to_modify + .as_ref() + .map(|epoch| bond_start != epoch) + .unwrap_or(true) + { + raw_bonds + .insert(*bond_start, cur_redel_bond_amount); + } else { + raw_bonds.insert( + *bond_start, + cur_redel_bond_amount + - modified + .new_amount + // Safe unwrap - it shouldn't + // get to + // this if it's None + .unwrap(), + ); + } + } + } + } + (start, rbonds) + } + }) + .collect(); + + Ok(new_redelegated_unbonds) +} + +/// Compute a token amount after slashing, given the initial amount and a set of +/// slashes. It is assumed that the input `slashes` are those commited while the +/// `amount` was contributing to voting power. +fn get_slashed_amount( + params: &PosParams, + amount: token::Amount, + slashes: &BTreeMap, +) -> storage_api::Result { + let mut updated_amount = amount; + let mut computed_amounts = Vec::::new(); + + for (&infraction_epoch, &slash_rate) in slashes { let mut computed_to_remove = BTreeSet::>::new(); for (ix, slashed_amount) in computed_amounts.iter().enumerate() { // Update amount with slashes that happened more than unbonding_len // epochs before this current slash - // 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) @@ -2095,13 +2579,10 @@ fn get_slashed_amount( computed_amounts.remove(item.0); } computed_amounts.push(SlashedAmount { - amount: *slash_rate * updated_amount, - epoch: *infraction_epoch, + amount: updated_amount.mul_ceil(slash_rate), + 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() @@ -2112,29 +2593,126 @@ fn get_slashed_amount( .checked_sub(total_computed_amounts) .unwrap_or_default(); - Ok(final_amount.change()) + Ok(final_amount) } -fn update_unbond( - handle: &Unbonds, - storage: &mut S, - withdraw_epoch: &Epoch, - start_epoch: &Epoch, - amount: token::Amount, -) -> storage_api::Result<()> +// `def computeAmountAfterSlashingUnbond` +fn compute_amount_after_slashing_unbond( + storage: &S, + params: &PosParams, + unbonds: &BTreeMap, + redelegated_unbonds: &EagerRedelegatedUnbonds, + slashes: Vec, +) -> storage_api::Result where - S: StorageRead + StorageWrite, + S: StorageRead, { - let current = handle - .at(withdraw_epoch) - .get(storage, start_epoch)? - .unwrap_or_default(); - handle.at(withdraw_epoch).insert( - storage, - *start_epoch, - current + amount, - )?; - Ok(()) + let mut result_slashing = ResultSlashing::default(); + for (&start_epoch, amount) in unbonds { + // `val listSlashes` + let list_slashes: Vec = slashes + .iter() + .filter(|slash| slash.epoch >= start_epoch) + .cloned() + .collect(); + // `val resultFold` + let result_fold = if let Some(redelegated_unbonds) = + redelegated_unbonds.get(&start_epoch) + { + fold_and_slash_redelegated_bonds( + storage, + params, + redelegated_unbonds, + start_epoch, + &list_slashes, + |_| true, + ) + } else { + FoldRedelegatedBondsResult::default() + }; + // `val totalNoRedelegated` + let total_not_redelegated = amount + .checked_sub(result_fold.total_redelegated) + .unwrap_or_default(); + // `val afterNoRedelegated` + let after_not_redelegated = + apply_list_slashes(params, &list_slashes, total_not_redelegated); + // `val amountAfterSlashing` + let amount_after_slashing = + after_not_redelegated + result_fold.total_after_slashing; + // Accumulation step + result_slashing.sum += amount_after_slashing; + result_slashing + .epoch_map + .insert(start_epoch, amount_after_slashing); + } + Ok(result_slashing) +} + +/// Compute from a set of unbonds (both redelegated and not) how much is left +/// after applying all relevant slashes. +// `def computeAmountAfterSlashingWithdraw` +fn compute_amount_after_slashing_withdraw( + storage: &S, + params: &PosParams, + unbonds_and_redelegated_unbonds: &BTreeMap< + (Epoch, Epoch), + (token::Amount, EagerRedelegatedBondsMap), + >, + slashes: Vec, +) -> storage_api::Result +where + S: StorageRead, +{ + let mut result_slashing = ResultSlashing::default(); + + for ((start_epoch, withdraw_epoch), (amount, redelegated_unbonds)) in + unbonds_and_redelegated_unbonds.iter() + { + // TODO: check if slashes in the same epoch can be + // folded into one effective slash + let end_epoch = *withdraw_epoch + - params.unbonding_len + - params.cubic_slashing_window_length; + // Find slashes that apply to `start_epoch..end_epoch` + let list_slashes = slashes + .iter() + .filter(|slash| { + // Started before the slash occurred + start_epoch <= &slash.epoch + // Ends after the slash + && end_epoch > slash.epoch + }) + .cloned() + .collect::>(); + + // Find the sum and the sum after slashing of the redelegated unbonds + let result_fold = fold_and_slash_redelegated_bonds( + storage, + params, + redelegated_unbonds, + *start_epoch, + &list_slashes, + |_| true, + ); + + // Unbond amount that didn't come from a redelegation + let total_not_redelegated = *amount - result_fold.total_redelegated; + // Find how much remains after slashing non-redelegated amount + let after_not_redelegated = + apply_list_slashes(params, &list_slashes, total_not_redelegated); + + // Add back the unbond and redelegated unbond amount after slashing + let amount_after_slashing = + after_not_redelegated + result_fold.total_after_slashing; + + result_slashing.sum += amount_after_slashing; + result_slashing + .epoch_map + .insert(*start_epoch, amount_after_slashing); + } + + Ok(result_slashing) } /// Arguments to [`become_validator`]. @@ -2221,7 +2799,7 @@ where )?; validator_deltas_handle(address).set( storage, - token::Change::default(), + token::Change::zero(), current_epoch, params.pipeline_len, )?; @@ -2248,12 +2826,17 @@ pub fn withdraw_tokens( where S: StorageRead + StorageWrite, { - tracing::debug!("Withdrawing tokens in epoch {current_epoch}"); let params = read_pos_params(storage)?; let source = source.unwrap_or(validator); + + tracing::debug!("Withdrawing tokens in epoch {current_epoch}"); tracing::debug!("Source {} --> Validator {}", source, validator); - let unbond_handle = unbond_handle(source, validator); + let unbond_handle: Unbonds = unbond_handle(source, validator); + let redelegated_unbonds = + delegator_redelegated_unbonds_handle(source).at(validator); + + // Check that there are unbonded tokens available for withdrawal if unbond_handle.is_empty(storage)? { return Err(WithdrawError::NoUnbondFound(BondId { source: source.clone(), @@ -2262,84 +2845,109 @@ where .into()); } - // 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(); + let mut unbonds_and_redelegated_unbonds: BTreeMap< + (Epoch, Epoch), + (token::Amount, EagerRedelegatedBondsMap), + > = BTreeMap::new(); for unbond in unbond_handle.iter(storage)? { let ( NestedSubKey::Data { - key: withdraw_epoch, - nested_sub_key: SubKey::Data(start_epoch), + key: start_epoch, + nested_sub_key: SubKey::Data(withdraw_epoch), }, amount, ) = unbond?; + // Logging tracing::debug!( "Unbond delta ({start_epoch}..{withdraw_epoch}), amount {}", amount.to_string_native() ); - - // TODO: adding slash rates in same epoch, applying cumulatively in dif - // epochs if withdraw_epoch > current_epoch { tracing::debug!( "Not yet withdrawable until epoch {withdraw_epoch}" ); continue; } - let slashes_for_this_unbond = find_slashes_in_range( - storage, - start_epoch, - Some( - withdraw_epoch - - params.unbonding_len - - params.cubic_slashing_window_length, - ), - validator, - )?; - let amount_after_slashing = - get_slashed_amount(¶ms, amount, &slashes_for_this_unbond)?; + let mut eager_redelegated_unbonds = EagerRedelegatedBondsMap::default(); + let matching_redelegated_unbonds = + redelegated_unbonds.at(&start_epoch).at(&withdraw_epoch); + for ub in matching_redelegated_unbonds.iter(storage)? { + let ( + NestedSubKey::Data { + key: address, + nested_sub_key: SubKey::Data(epoch), + }, + amount, + ) = ub?; + eager_redelegated_unbonds + .entry(address) + .or_default() + .entry(epoch) + .or_insert(amount); + } - // total_slashed += amount - token::Amount::from(amount_after_slashing); - withdrawable_amount += token::Amount::from(amount_after_slashing); - unbonds_to_remove.push((withdraw_epoch, start_epoch)); + unbonds_and_redelegated_unbonds.insert( + (start_epoch, withdraw_epoch), + (amount, eager_redelegated_unbonds), + ); } + let slashes = find_validator_slashes(storage, validator)?; + + // `val resultSlashing` + let result_slashing = compute_amount_after_slashing_withdraw( + storage, + ¶ms, + &unbonds_and_redelegated_unbonds, + slashes, + )?; + + let withdrawable_amount = result_slashing.sum; tracing::debug!( "Withdrawing total {}", withdrawable_amount.to_string_native() ); - // Remove the unbond data from storage - for (withdraw_epoch, start_epoch) in unbonds_to_remove { + // `updateDelegator` with `unbonded` and `redelegeatedUnbonded` + for ((start_epoch, withdraw_epoch), _unbond_and_redelegations) in + unbonds_and_redelegated_unbonds + { tracing::debug!("Remove ({start_epoch}..{withdraw_epoch}) from unbond"); unbond_handle - .at(&withdraw_epoch) - .remove(storage, &start_epoch)?; - // TODO: check if the `end_epoch` layer is now empty and remove it if - // so, may need to implement remove/delete for nested map + .at(&start_epoch) + .remove(storage, &withdraw_epoch)?; + redelegated_unbonds + .at(&start_epoch) + .remove_all(storage, &withdraw_epoch)?; + + if unbond_handle.at(&start_epoch).is_empty(storage)? { + unbond_handle.remove_all(storage, &start_epoch)?; + } + if redelegated_unbonds.at(&start_epoch).is_empty(storage)? { + redelegated_unbonds.remove_all(storage, &start_epoch)?; + } } // Transfer the withdrawable tokens from the PoS address back to the source let staking_token = staking_token_address(storage); - transfer_tokens( + token::transfer( storage, &staking_token, - withdrawable_amount, &ADDRESS, source, + withdrawable_amount, )?; // TODO: Transfer the slashed tokens from the PoS address to the Slash Pool // address - // transfer_tokens( + // token::transfer( // storage, // &staking_token, - // total_slashed, // &ADDRESS, // &SLASH_POOL_ADDRESS, + // total_slashed, // )?; Ok(withdrawable_amount) @@ -2405,53 +3013,6 @@ where commission_handle.set(storage, new_rate, current_epoch, params.pipeline_len) } -/// Transfer tokens between accounts -/// TODO: may want to move this into core crate -pub fn transfer_tokens( - storage: &mut S, - token: &Address, - amount: token::Amount, - src: &Address, - dest: &Address, -) -> storage_api::Result<()> -where - S: StorageRead + StorageWrite, -{ - let src_key = token::balance_key(token, src); - let dest_key = token::balance_key(token, dest); - if let Some(mut src_balance) = storage.read::(&src_key)? { - // let mut src_balance: token::Amount = - // decode(src_balance).unwrap_or_default(); - if src_balance < amount { - tracing::error!( - "PoS system transfer error, the source doesn't have \ - sufficient balance. It has {}, but {} is required", - src_balance.to_string_native(), - amount.to_string_native(), - ); - } - src_balance.spend(&amount); - let mut dest_balance = storage - .read::(&dest_key)? - .unwrap_or_default(); - - // let dest_balance = storage.read_bytes(&dest_key).unwrap_or_default(); - // let mut dest_balance: token::Amount = dest_balance - // .and_then(|b| decode(b).ok()) - // .unwrap_or_default(); - dest_balance.receive(&amount); - storage - .write(&src_key, src_balance) - .expect("Unable to write token balance for PoS system"); - storage - .write(&dest_key, dest_balance) - .expect("Unable to write token balance for PoS system"); - } else { - tracing::error!("PoS system transfer error, the source has no balance"); - } - Ok(()) -} - /// Check if the given consensus key is already being used to ensure uniqueness. /// /// If it's not being used, it will be inserted into the set that's being used @@ -2481,54 +3042,273 @@ where } /// 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? +/// Returns the bond amount after slashing. For future epochs the value is +/// subject to change. pub fn bond_amount( storage: &S, bond_id: &BondId, epoch: Epoch, -) -> storage_api::Result<(token::Amount, token::Amount)> +) -> storage_api::Result where S: StorageRead, { - // TODO: review this logic carefully, apply rewards + // TODO: our method of applying slashes is not correct! This needs review + + println!("FN BOND AMOUNT"); + let params = read_pos_params(storage)?; + + // TODO: 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(Dec::one(), *tot_rate + slash.rate); - map - }, - ); + dbg!(&slashes); + let slash_rates = + slashes + .iter() + .fold(BTreeMap::::new(), |mut map, slash| { + let tot_rate = map.entry(slash.epoch).or_default(); + *tot_rate = cmp::min(Dec::one(), *tot_rate + slash.rate); + map + }); + dbg!(&slash_rates); + + // Accumulate incoming redelegations slashes from source validator, if any. + // This ensures that if there're slashes on both src validator and dest + // validator, they're combined correctly. + let mut redelegation_slashes = BTreeMap::::new(); + for res in delegator_redelegated_bonds_handle(&bond_id.source) + .at(&bond_id.validator) + .iter(storage)? + { + let ( + NestedSubKey::Data { + key: redelegation_end, + nested_sub_key: + NestedSubKey::Data { + key: src_validator, + nested_sub_key: SubKey::Data(start), + }, + }, + delta, + ) = res?; + + let list_slashes = validator_slashes_handle(&src_validator) + .iter(storage)? + .map(Result::unwrap) + .filter(|slash| { + let slash_processing_epoch = + slash.epoch + params.slash_processing_epoch_offset(); + start <= slash.epoch + && redelegation_end > slash.epoch + && slash_processing_epoch + > redelegation_end - params.pipeline_len + }) + .collect::>(); + + let slashed_delta = apply_list_slashes(¶ms, &list_slashes, delta); + + // let mut slashed_delta = delta; + // let slashes = find_slashes_in_range( + // storage, + // start, + // Some(redelegation_end), + // &src_validator, + // )?; + // for (slash_epoch, rate) in slashes { + // let slash_processing_epoch = + // slash_epoch + params.slash_processing_epoch_offset(); + // // If the slash was processed after redelegation was submitted + // // it has to be slashed now + // if slash_processing_epoch > redelegation_end - + // params.pipeline_len { let slashed = + // slashed_delta.mul_ceil(rate); slashed_delta -= + // slashed; } + // } + *redelegation_slashes.entry(redelegation_end).or_default() += + delta - slashed_delta; + } + dbg!(&redelegation_slashes); 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(); + let mut total_active = token::Amount::zero(); for next in bonds.iter(storage)? { - let (bond_epoch, delta) = next?; + let (bond_epoch, delta) = dbg!(next?); if bond_epoch > epoch { continue; } - total += token::Amount::from(delta); - total_active += token::Amount::from(delta); + let list_slashes = slashes + .iter() + .filter(|slash| bond_epoch <= slash.epoch) + .cloned() + .collect::>(); + + let mut slashed_delta = + apply_list_slashes(¶ms, &list_slashes, delta); + + // Deduct redelegation src validator slash, if any + if let Some(&redelegation_slash) = redelegation_slashes.get(&bond_epoch) + { + slashed_delta -= redelegation_slash; + } + + // let list_slashes = slashes + // .iter() + // .map(Result::unwrap) + // .filter(|slash| bond_epoch <= slash.epoch) + // .collect::>(); + + // for (&slash_epoch, &rate) in &slash_rates { + // if slash_epoch < bond_epoch { + // continue; + // } + // // TODO: think about truncation + // let current_slash = slashed_delta.mul_ceil(rate); + // slashed_delta -= current_slash; + // } + total_active += slashed_delta; + } + dbg!(&total_active); + + // Add unbonds that are still contributing to stake + let unbonds = unbond_handle(&bond_id.source, &bond_id.validator); + for next in unbonds.iter(storage)? { + let ( + NestedSubKey::Data { + key: start, + nested_sub_key: SubKey::Data(withdrawable_epoch), + }, + delta, + ) = next?; + let end = withdrawable_epoch - params.withdrawable_epoch_offset() + + params.pipeline_len; + + if start <= epoch && end > epoch { + let list_slashes = slashes + .iter() + .filter(|slash| start <= slash.epoch && end > slash.epoch) + .cloned() + .collect::>(); + + let slashed_delta = + apply_list_slashes(¶ms, &list_slashes, delta); + + // let mut slashed_delta = delta; + // for (&slash_epoch, &rate) in &slash_rates { + // if start <= slash_epoch && end > slash_epoch { + // // TODO: think about truncation + // let current_slash = slashed_delta.mul_ceil(rate); + // slashed_delta -= current_slash; + // } + // } + total_active += slashed_delta; + } + } + dbg!(&total_active); + + if bond_id.validator != bond_id.source { + // Add outgoing redelegations that are still contributing to the source + // validator's stake + let redelegated_bonds = + delegator_redelegated_bonds_handle(&bond_id.source); + for res in redelegated_bonds.iter(storage)? { + let ( + NestedSubKey::Data { + key: _dest_validator, + nested_sub_key: + NestedSubKey::Data { + key: end, + nested_sub_key: + NestedSubKey::Data { + key: src_validator, + nested_sub_key: SubKey::Data(start), + }, + }, + }, + delta, + ) = res?; + if src_validator == bond_id.validator + && start <= epoch + && end > epoch + { + let list_slashes = slashes + .iter() + .filter(|slash| start <= slash.epoch && end > slash.epoch) + .cloned() + .collect::>(); + + let slashed_delta = + apply_list_slashes(¶ms, &list_slashes, delta); + + // let mut slashed_delta = delta; + // for (&slash_epoch, &rate) in &slash_rates { + // if start <= slash_epoch && end > slash_epoch { + // // TODO: think about truncation + // let current_slash = delta.mul_ceil(rate); + // slashed_delta -= current_slash; + // } + // } + total_active += slashed_delta; + } + } + dbg!(&total_active); - for (slash_epoch, rate) in &slash_rates { - if *slash_epoch < bond_epoch { - continue; + // Add outgoing redelegation unbonds that are still contributing to + // the source validator's stake + let redelegated_unbonds = + delegator_redelegated_unbonds_handle(&bond_id.source); + for res in redelegated_unbonds.iter(storage)? { + let ( + NestedSubKey::Data { + key: _dest_validator, + nested_sub_key: + NestedSubKey::Data { + key: redelegation_epoch, + nested_sub_key: + NestedSubKey::Data { + key: withdraw_epoch, + nested_sub_key: + NestedSubKey::Data { + key: src_validator, + nested_sub_key: SubKey::Data(start), + }, + }, + }, + }, + delta, + ) = res?; + let end = withdraw_epoch - params.withdrawable_epoch_offset() + + params.pipeline_len; + if src_validator == bond_id.validator + // If the unbonded bond was redelegated after this epoch ... + && redelegation_epoch > epoch + // ... the start was before or at this epoch ... + && start <= epoch + // ... and the end after this epoch + && end > epoch + { + let list_slashes = slashes + .iter() + .filter(|slash| start <= slash.epoch && end > slash.epoch) + .cloned() + .collect::>(); + + let slashed_delta = + apply_list_slashes(¶ms, &list_slashes, delta); + + // let mut slashed_delta = delta; + // for (&slash_epoch, &rate) in &slash_rates { + // if start <= slash_epoch && end > slash_epoch { + // let current_slash = delta.mul_ceil(rate); + // slashed_delta -= current_slash; + // } + // } + total_active += slashed_delta; } - // TODO: think about truncation - let current_slashed = *rate * delta; - total_active - .checked_sub(token::Amount::from(current_slashed)) - .unwrap_or_default(); } } - Ok((total, total_active)) + dbg!(&total_active); + + Ok(total_active) } /// Get the genesis consensus validators stake and consensus key for Tendermint, @@ -2618,8 +3398,7 @@ where &address, current_epoch, ) - .unwrap() - .unwrap_or_default(); + .unwrap(); into_tm_voting_power( params.tm_votes_per_token, prev_validator_stake, @@ -2642,7 +3421,7 @@ where } // If both previous and current voting powers are 0, and the // validator_stake_threshold is 0, skip update - if params.validator_stake_threshold == token::Amount::default() + if params.validator_stake_threshold.is_zero() && *prev_tm_voting_power == 0 && *new_tm_voting_power == 0 { @@ -2690,8 +3469,7 @@ where &address, current_epoch, ) - .unwrap() - .unwrap_or_default(); + .unwrap(); into_tm_voting_power( params.tm_votes_per_token, prev_validator_stake, @@ -2702,8 +3480,7 @@ where // it in the `new_consensus_validators` iterator above if matches!(new_state, Some(ValidatorState::Consensus)) { return None; - } else if params.validator_stake_threshold - == token::Amount::default() + } else if params.validator_stake_threshold.is_zero() && *prev_tm_voting_power == 0 { // If the new state is not Consensus but its prev voting power @@ -2782,11 +3559,10 @@ where "Delegation key should contain validator address.", ) })?; - let amount = bond_handle(owner, &validator_address) + let deltas_sum = bond_handle(owner, &validator_address) .get_sum(storage, *epoch, ¶ms)? .unwrap_or_default(); - delegations - .insert(validator_address, token::Amount::from_change(amount)); + delegations.insert(validator_address, deltas_sum); } Ok(delegations) } @@ -2807,7 +3583,7 @@ pub fn find_bonds( storage: &S, source: &Address, validator: &Address, -) -> storage_api::Result> +) -> storage_api::Result> where S: StorageRead, { @@ -2831,8 +3607,8 @@ where .map(|next_result| { let ( NestedSubKey::Data { - key: withdraw_epoch, - nested_sub_key: SubKey::Data(start_epoch), + key: start_epoch, + nested_sub_key: SubKey::Data(withdraw_epoch), }, amount, ) = next_result?; @@ -2976,7 +3752,7 @@ where { return None; } - let change: token::Change = + let change: token::Amount = BorshDeserialize::try_from_slice(&val_bytes).ok()?; if change.is_zero() { return None; @@ -3101,12 +3877,12 @@ where let bonds = find_bonds(storage, &source, &validator)? .into_iter() - .filter(|(_start, change)| *change > token::Change::default()) - .map(|(start, change)| { + .filter(|(_start, amount)| *amount > token::Amount::zero()) + .map(|(start, amount)| { make_bond_details( params, &validator, - change, + amount, start, &slashes, &mut applied_slashes, @@ -3140,7 +3916,7 @@ where fn make_bond_details( params: &PosParams, validator: &Address, - change: token::Change, + deltas_sum: token::Amount, start: Epoch, slashes: &[Slash], applied_slashes: &mut HashMap>, @@ -3150,7 +3926,7 @@ fn make_bond_details( .get(validator) .cloned() .unwrap_or_default(); - let amount = token::Amount::from_change(change); + let mut slash_rates_by_epoch = BTreeMap::::new(); let validator_slashes = @@ -3169,15 +3945,15 @@ fn make_bond_details( let slashed_amount = if slash_rates_by_epoch.is_empty() { None } else { - let amount_after_slashing = token::Amount::from_change( - get_slashed_amount(params, amount, &slash_rates_by_epoch).unwrap(), - ); - Some(amount - amount_after_slashing) + let amount_after_slashing = + get_slashed_amount(params, deltas_sum, &slash_rates_by_epoch) + .unwrap(); + Some(deltas_sum - amount_after_slashing) }; BondDetails { start, - amount, + amount: deltas_sum, slashed_amount, } } @@ -3221,9 +3997,8 @@ fn make_unbond_details( let slashed_amount = if slash_rates_by_epoch.is_empty() { None } else { - let amount_after_slashing = token::Amount::from_change( - get_slashed_amount(params, amount, &slash_rates_by_epoch).unwrap(), - ); + let amount_after_slashing = + get_slashed_amount(params, amount, &slash_rates_by_epoch).unwrap(); Some(amount - amount_after_slashing) }; @@ -3255,7 +4030,7 @@ where let consensus_validators = consensus_validator_set_handle().at(&epoch); // Get total stake of the consensus validator set - let mut total_consensus_stake = token::Amount::default(); + let mut total_consensus_stake = token::Amount::zero(); for validator in consensus_validators.iter(storage)? { let ( NestedSubKey::Data { @@ -3270,7 +4045,7 @@ where // Get set of signing validator addresses and the combined stake of // these signers let mut signer_set: HashSet
= HashSet::new(); - let mut total_signing_stake = token::Amount::default(); + let mut total_signing_stake = token::Amount::zero(); for VoteInfo { validator_address, validator_vp, @@ -3291,8 +4066,7 @@ where } let stake_from_deltas = - read_validator_stake(storage, ¶ms, &validator_address, epoch)? - .unwrap_or_default(); + read_validator_stake(storage, ¶ms, &validator_address, epoch)?; // Ensure TM stake updates properly with a debug_assert if cfg!(debug_assertions) { @@ -3325,7 +4099,7 @@ where "PoS rewards coefficients {coeffs:?}, inputs: {rewards_calculator:?}." ); - // println!( + // tracing::debug!( // "TOTAL SIGNING STAKE (LOGGING BLOCK REWARDS) = {}", // signing_stake // ); @@ -3348,13 +4122,13 @@ where // When below-threshold validator set is added, this shouldn't be needed // anymore since some minimal stake will be required to be in at least // the consensus set - if stake == token::Amount::default() { + if stake.is_zero() { continue; } let mut rewards_frac = Dec::zero(); let stake_unscaled: Dec = stake.into(); - // println!( + // tracing::debug!( // "NAMADA VALIDATOR STAKE (LOGGING BLOCK REWARDS) OF EPOCH {} = // {}", epoch, stake // ); @@ -3396,7 +4170,7 @@ pub fn compute_cubic_slash_rate( where S: StorageRead, { - // println!("COMPUTING CUBIC SLASH RATE"); + // tracing::debug!("COMPUTING CUBIC SLASH RATE"); let mut sum_vp_fraction = Dec::zero(); let (start_epoch, end_epoch) = params.cubic_slash_epoch_window(infraction_epoch); @@ -3424,9 +4198,9 @@ where ) = res?; let validator_stake = - read_validator_stake(storage, params, &validator, epoch)? - .unwrap_or_default(); - // println!("Val {} stake: {}", &validator, validator_stake); + read_validator_stake(storage, params, &validator, epoch)?; + // tracing::debug!("Val {} stake: {}", &validator, + // validator_stake); Ok(acc + Dec::from(validator_stake)) // TODO: does something more complex need to be done @@ -3436,7 +4210,7 @@ where )?; sum_vp_fraction += infracting_stake / consensus_stake; } - // println!("sum_vp_fraction: {}", sum_vp_fraction); + // tracing::debug!("sum_vp_fraction: {}", sum_vp_fraction); Ok(Dec::new(9, 0).unwrap() * sum_vp_fraction * sum_vp_fraction) } @@ -3495,16 +4269,15 @@ where .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 amount_pre = + read_validator_stake(storage, params, validator, epoch)?; let val_position = validator_set_positions_handle() .at(&epoch) .get(storage, validator)? .expect("Could not find validator's position in storage."); let _ = consensus_validator_set_handle() .at(&epoch) - .at(&token::Amount::from_change(amount_pre)) + .at(&amount_pre) .remove(storage, &val_position)?; validator_set_positions_handle() .at(&epoch) @@ -3557,6 +4330,7 @@ where let amount_pre = validator_deltas_handle(validator) .get_sum(storage, epoch, params)? .unwrap_or_default(); + debug_assert!(amount_pre.non_negative()); let val_position = validator_set_positions_handle() .at(&epoch) .get(storage, validator)? @@ -3570,10 +4344,10 @@ where .remove(storage, validator)?; } ValidatorState::BelowThreshold => { - println!("Below-threshold"); + tracing::debug!("Below-threshold"); } ValidatorState::Inactive => { - println!("INACTIVE"); + tracing::debug!("INACTIVE"); panic!( "Shouldn't be here - haven't implemented inactive vals yet" ) @@ -3604,11 +4378,7 @@ where 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. +/// Process slashes NEW pub fn process_slashes( storage: &mut S, current_epoch: Epoch, @@ -3641,8 +4411,11 @@ where compute_cubic_slash_rate(storage, ¶ms, infraction_epoch)?; // Collect the enqueued slashes and update their rates - let mut validators_and_slashes: HashMap> = - HashMap::new(); + let mut eager_validator_slashes: BTreeMap> = + BTreeMap::new(); // TODO: will need to update this in storage later + let mut eager_validator_slash_rates: HashMap = HashMap::new(); + + // `slashPerValidator` and `slashesMap` while also updating in storage for enqueued_slash in enqueued_slashes.iter(storage)? { let ( NestedSubKey::Data { @@ -3666,254 +4439,585 @@ where r#type: enqueued_slash.r#type, rate: 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(); + let cur_slashes = eager_validator_slashes + .entry(validator.clone()) + .or_default(); cur_slashes.push(updated_slash); + let cur_rate = + eager_validator_slash_rates.entry(validator).or_default(); + *cur_rate = cmp::min(Dec::one(), *cur_rate + slash_rate); } - 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( + // `resultSlashing` + let mut map_validator_slash: EagerRedelegatedBondsMap = BTreeMap::new(); + for (validator, slash_rate) in eager_validator_slash_rates { + process_validator_slash( storage, ¶ms, &validator, - infraction_epoch, - )? - .unwrap_or_default(); - - tracing::debug!( - "Validator {} stake at infraction epoch {} = {}", - &validator, - infraction_epoch, - validator_stake_at_infraction.to_string_native() - ); - - let mut total_rate = Dec::zero(); + slash_rate, + current_epoch, + &mut map_validator_slash, + )?; + } + tracing::debug!("Slashed amounts for validators: {map_validator_slash:#?}"); - 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())?; + // Now update the remaining parts of storage - total_rate += enqueued_slash.rate; + // Write slashes themselves into storage + for (validator, slashes) in eager_validator_slashes { + let validator_slashes = validator_slashes_handle(&validator); + for slash in slashes { + validator_slashes.push(storage, slash)?; } - total_rate = cmp::min(Dec::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(); - - let total_bonded_handle = total_bonded_handle(&validator); - let mut sum_post_bonds = token::Change::default(); - - // 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(), - ) { - tracing::debug!("Epoch {}", epoch); - let mut recent_unbonds = token::Change::default(); - let unbonds = unbond_records_handle(&validator).at(&epoch); - for unbond in unbonds.iter(storage)? { - let (start, unbond_amount) = unbond?; - tracing::debug!( - "UnbondRecord: amount = {}, start_epoch {}", - unbond_amount.to_string_native(), - &start - ); - if start <= infraction_epoch { - let prev_slashes = find_slashes_in_range( - storage, - start, - Some( - infraction_epoch - .checked_sub(Epoch( - params.unbonding_len - + params.cubic_slashing_window_length, - )) - .unwrap_or_default(), - ), - &validator, - )?; - tracing::debug!( - "Slashes for this unbond: {:?}", - prev_slashes - ); + } - total_unbonded += - token::Amount::from_change(get_slashed_amount( - ¶ms, - unbond_amount, - &prev_slashes, - )?); - } else { - recent_unbonds += unbond_amount.change(); - } + // Update the validator stakes + for (validator, slash_amounts) in map_validator_slash { + let mut slash_acc = token::Amount::zero(); - tracing::debug!( - "Total unbonded (epoch {}) w slashing = {}", + // Update validator sets first because it needs to be able to read + // validator stake before we make any changes to it + for (&epoch, &slash_amount) in &slash_amounts { + let state = validator_state_handle(&validator) + .get(storage, epoch, ¶ms)? + .unwrap(); + if state != ValidatorState::Jailed { + update_validator_set( + storage, + ¶ms, + &validator, + -slash_amount.change(), epoch, - total_unbonded.to_string_native() - ); + )?; } + } + // Then update validator and total deltas + for (epoch, slash_amount) in slash_amounts { + let slash_delta = slash_amount - slash_acc; + slash_acc += slash_delta; - sum_post_bonds += total_bonded_handle - .get_delta_val(storage, epoch, ¶ms)? - .unwrap_or_default() - - recent_unbonds; + update_validator_deltas( + storage, + &validator, + -slash_delta.change(), + epoch, + 0, + )?; + update_total_deltas(storage, -slash_delta.change(), epoch, 0)?; } - // 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 { - tracing::debug!( - "Epoch {}\nLast slash = {}", - current_epoch + offset, - last_slash.to_string_native() - ); - let mut recent_unbonds = token::Change::default(); - let unbonds = - unbond_records_handle(&validator).at(&(current_epoch + offset)); + // TODO: should we clear some storage here as is done in Quint?? + // Possibly make the `unbonded` LazyMaps epoched so that it is done + // automatically? + } - for unbond in unbonds.iter(storage)? { - let (start, unbond_amount) = unbond?; - tracing::debug!( - "UnbondRecord: amount = {}, start_epoch {}", - unbond_amount.to_string_native(), - &start - ); - if start <= infraction_epoch { - let prev_slashes = find_slashes_in_range( - storage, - start, - Some( - infraction_epoch - .checked_sub(Epoch( - params.unbonding_len - + params.cubic_slashing_window_length, - )) - .unwrap_or_default(), - ), - &validator, - )?; - tracing::debug!( - "Slashes for this unbond: {:?}", - prev_slashes - ); + Ok(()) +} - total_unbonded += - token::Amount::from_change(get_slashed_amount( - ¶ms, - unbond_amount, - &prev_slashes, - )?); - } else { - recent_unbonds += unbond_amount.change(); - } +/// Process a slash by (i) slashing the misbehaving validator; and (ii) any +/// validator to which it has redelegated some tokens and the slash misbehaving +/// epoch is wihtin the redelegation slashing window. +/// +/// `validator` - the misbehaving validator. +/// `slash_rate` - the slash rate. +/// `slashed_amounts_map` - a map from validator address to a map from epoch to +/// already processed slash amounts. +/// +/// Adds any newly processed slash amount of any involved validator to +/// `slashed_amounts_map`. +// Quint `processSlash` +fn process_validator_slash( + storage: &mut S, + params: &PosParams, + validator: &Address, + slash_rate: Dec, + current_epoch: Epoch, + slashed_amount_map: &mut EagerRedelegatedBondsMap, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + // `resultSlashValidator + let result_slash = slash_validator( + storage, + params, + validator, + slash_rate, + current_epoch, + &slashed_amount_map + .get(validator) + .cloned() + .unwrap_or_default(), + )?; - tracing::debug!( - "Total unbonded (offset {}) w slashing = {}", - offset, - total_unbonded.to_string_native() - ); - } + // `updatedSlashedAmountMap` + let validator_slashes = + slashed_amount_map.entry(validator.clone()).or_default(); + for (epoch, slash) in result_slash { + *validator_slashes.entry(epoch).or_default() += slash; + } - let this_slash = total_rate - * (validator_stake_at_infraction - total_unbonded).change(); - let diff_slashed_amount = last_slash - this_slash; - last_slash = this_slash; - // println!("This slash = {}", this_slash); - // println!("Diff slashed amount = {}", diff_slashed_amount); - // total_slashed -= diff_slashed_amount; - // total_unbonded = token::Amount::default(); - - sum_post_bonds += total_bonded_handle - .get_delta_val(storage, current_epoch + offset, ¶ms)? - .unwrap_or_default() - - recent_unbonds; - - let validator_stake_at_offset = read_validator_stake( - storage, - ¶ms, - &validator, - current_epoch + offset, - )? - .unwrap_or_default() - .change(); - let slashable_stake_at_offset = - validator_stake_at_offset - sum_post_bonds; - assert!(slashable_stake_at_offset >= token::Change::default()); - - let change = - cmp::max(-slashable_stake_at_offset, diff_slashed_amount); - - let val_updates = - deltas_for_update.entry(validator.clone()).or_default(); - val_updates.push((offset, change)); - } + // `outgoingRedelegation` + let outgoing_redelegations = + validator_outgoing_redelegations_handle(validator); + + // Final loop in `processSlash` + let dest_validators = outgoing_redelegations + .iter(storage)? + .map(|res| { + let ( + NestedSubKey::Data { + key: dest_validator, + nested_sub_key: _, + }, + _redelegation, + ) = res?; + Ok(dest_validator) + }) + .collect::>>()?; + + for dest_validator in dest_validators { + let to_modify = slashed_amount_map + .entry(dest_validator.clone()) + .or_default(); + + tracing::debug!( + "Slashing {} redelegation to {}", + validator, + &dest_validator + ); + + // `slashValidatorRedelegation` + slash_validator_redelegation( + storage, + params, + validator, + current_epoch, + &outgoing_redelegations.at(&dest_validator), + &validator_slashes_handle(validator), + &validator_total_redelegated_unbonded_handle(&dest_validator), + slash_rate, + to_modify, + )?; } - // 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); - tracing::debug!( - "Deltas change = {} at offset {} for validator {}", - delta.to_string_native(), - offset, - &validator - ); - // total_slashed -= change; + Ok(()) +} - update_validator_deltas( - storage, - ¶ms, - &validator, - delta, - current_epoch, - offset, - )?; - update_total_deltas( +/// In the context of a redelegation, the function computes how much a validator +/// (the destination validator of the redelegation) should be slashed due to the +/// misbehaving of a second validator (the source validator of the +/// redelegation). The function computes how much the validator whould be +/// slashed at all epochs between the current epoch (curEpoch) + 1 and the +/// current epoch + 1 + PIPELINE_OFFSET, accounting for any tokens of the +/// redelegation already unbonded. +/// +/// - `src_validator` - the source validator +/// - `outgoing_redelegations` - a map from pair of epochs to int that includes +/// all the redelegations from the source validator to the destination +/// validator. +/// - The outer key is epoch at which the bond started at the source +/// validator. +/// - The inner key is epoch at which the redelegation started (the epoch at +/// which was issued). +/// - `slashes` a list of slashes of the source validator. +/// - `dest_total_redelegated_unbonded` - a map of unbonded redelegated tokens +/// at the destination validator. +/// - `slash_rate` - the rate of the slash being processed. +/// - `dest_slashed_amounts` - a map from epoch to already processed slash +/// amounts. +/// +/// Adds any newly processed slash amount to `dest_slashed_amounts`. +#[allow(clippy::too_many_arguments)] +fn slash_validator_redelegation( + storage: &S, + params: &PosParams, + src_validator: &Address, + current_epoch: Epoch, + outgoing_redelegations: &NestedMap>, + slashes: &Slashes, + dest_total_redelegated_unbonded: &TotalRedelegatedUnbonded, + slash_rate: Dec, + dest_slashed_amounts: &mut BTreeMap, +) -> storage_api::Result<()> +where + S: StorageRead, +{ + let infraction_epoch = + current_epoch - params.slash_processing_epoch_offset(); + + for res in outgoing_redelegations.iter(storage)? { + let ( + NestedSubKey::Data { + key: bond_start, + nested_sub_key: SubKey::Data(redel_start), + }, + amount, + ) = res?; + + if params.in_redelegation_slashing_window( + infraction_epoch, + redel_start, + params.redelegation_end_epoch_from_start(redel_start), + ) && bond_start <= infraction_epoch + { + slash_redelegation( storage, - ¶ms, - delta, + params, + amount, + bond_start, + params.redelegation_end_epoch_from_start(redel_start), + src_validator, current_epoch, - offset, + slashes, + dest_total_redelegated_unbonded, + slash_rate, + dest_slashed_amounts, )?; } } - // debug_assert!(total_slashed >= token::Change::default()); + Ok(()) +} - // 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, - // )?; +#[allow(clippy::too_many_arguments)] +fn slash_redelegation( + storage: &S, + params: &PosParams, + amount: token::Amount, + bond_start: Epoch, + redel_bond_start: Epoch, + src_validator: &Address, + current_epoch: Epoch, + slashes: &Slashes, + total_redelegated_unbonded: &TotalRedelegatedUnbonded, + slash_rate: Dec, + slashed_amounts: &mut BTreeMap, +) -> storage_api::Result<()> +where + S: StorageRead, +{ + tracing::debug!( + "\nSlashing redelegation amount {} - bond start {} and \ + redel_bond_start {} - at rate {}\n", + amount.to_string_native(), + bond_start, + redel_bond_start, + slash_rate + ); + + let infraction_epoch = + current_epoch - params.slash_processing_epoch_offset(); + + // Slash redelegation destination validator from the next epoch only + // as they won't be jailed + let set_update_epoch = current_epoch.next(); + + let mut init_tot_unbonded = + Epoch::iter_bounds_inclusive(infraction_epoch.next(), set_update_epoch) + .map(|epoch| { + let redelegated_unbonded = total_redelegated_unbonded + .at(&epoch) + .at(&redel_bond_start) + .at(src_validator) + .get(storage, &bond_start)? + .unwrap_or_default(); + Ok(redelegated_unbonded) + }) + .sum::>()?; + + for epoch in Epoch::iter_range(set_update_epoch, params.pipeline_len) { + let updated_total_unbonded = { + let redelegated_unbonded = total_redelegated_unbonded + .at(&epoch) + .at(&redel_bond_start) + .at(src_validator) + .get(storage, &bond_start)? + .unwrap_or_default(); + init_tot_unbonded + redelegated_unbonded + }; + + let list_slashes = slashes + .iter(storage)? + .map(Result::unwrap) + .filter(|slash| { + params.in_redelegation_slashing_window( + slash.epoch, + params.redelegation_start_epoch_from_end(redel_bond_start), + redel_bond_start, + ) && bond_start <= slash.epoch + && slash.epoch + params.slash_processing_epoch_offset() + // TODO this may need to be `<=` as in `fn compute_total_unbonded` + // + // NOTE(Tomas): Agreed and changed to `<=`. We're looking + // for slashes that were processed before or in the epoch + // in which slashes that are currently being processed + // occurred. Because we're slashing in the beginning of an + // epoch, we're also taking slashes that were processed in + // the infraction epoch as they would still be processed + // before any infraction occurred. + <= infraction_epoch + }) + .collect::>(); + + let slashable_amount = amount + .checked_sub(updated_total_unbonded) + .unwrap_or_default(); + + let slashed = + apply_list_slashes(params, &list_slashes, slashable_amount) + .mul_ceil(slash_rate); + + let list_slashes = slashes + .iter(storage)? + .map(Result::unwrap) + .filter(|slash| { + params.in_redelegation_slashing_window( + slash.epoch, + params.redelegation_start_epoch_from_end(redel_bond_start), + redel_bond_start, + ) && bond_start <= slash.epoch + }) + .collect::>(); + + let slashable_stake = + apply_list_slashes(params, &list_slashes, slashable_amount) + .mul_ceil(slash_rate); + + init_tot_unbonded = updated_total_unbonded; + let to_slash = cmp::min(slashed, slashable_stake); + if !to_slash.is_zero() { + let map_value = slashed_amounts.entry(epoch).or_default(); + *map_value += to_slash; + } + } Ok(()) } +/// Computes for a given validator and a slash how much should be slashed at all +/// epochs between the currentÃ¥ epoch (curEpoch) + 1 and the current epoch + 1 + +/// PIPELINE_OFFSET, accounting for any tokens already unbonded. +/// +/// - `validator` - the misbehaving validator. +/// - `slash_rate` - the rate of the slash being processed. +/// - `slashed_amounts_map` - a map from epoch to already processed slash +/// amounts. +/// +/// Returns a map that adds any newly processed slash amount to +/// `slashed_amounts_map`. +// `def slashValidator` +fn slash_validator( + storage: &S, + params: &PosParams, + validator: &Address, + slash_rate: Dec, + current_epoch: Epoch, + slashed_amounts_map: &BTreeMap, +) -> storage_api::Result> +where + S: StorageRead, +{ + tracing::debug!("Slashing validator {} at rate {}", validator, slash_rate); + let infraction_epoch = + current_epoch - params.slash_processing_epoch_offset(); + + let total_unbonded = total_unbonded_handle(validator); + let total_redelegated_unbonded = + validator_total_redelegated_unbonded_handle(validator); + let total_bonded = total_bonded_handle(validator); + let total_redelegated_bonded = + validator_total_redelegated_bonded_handle(validator); + + let mut slashed_amounts = slashed_amounts_map.clone(); + + let mut tot_bonds = total_bonded + .get_data_handler() + .iter(storage)? + .map(Result::unwrap) + .filter(|&(epoch, bonded)| { + epoch <= infraction_epoch && bonded > 0.into() + }) + .collect::>(); + + let mut redelegated_bonds = tot_bonds + .keys() + .filter(|&epoch| { + !total_redelegated_bonded + .at(epoch) + .is_empty(storage) + .unwrap() + }) + .map(|epoch| { + let tot_redel_bonded = total_redelegated_bonded + .at(epoch) + .collect_map(storage) + .unwrap(); + (*epoch, tot_redel_bonded) + }) + .collect::>(); + + let mut sum = token::Amount::zero(); + + let eps = current_epoch + .iter_range(params.pipeline_len) + .collect::>(); + for epoch in eps.into_iter().rev() { + let amount = tot_bonds.iter().fold( + token::Amount::zero(), + |acc, (bond_start, bond_amount)| { + acc + compute_slash_bond_at_epoch( + storage, + params, + validator, + epoch, + infraction_epoch, + *bond_start, + *bond_amount, + redelegated_bonds.get(bond_start), + slash_rate, + ) + .unwrap() + }, + ); + + let new_bonds = total_unbonded.at(&epoch); + tot_bonds = new_bonds + .collect_map(storage) + .unwrap() + .into_iter() + .filter(|(ep, _)| *ep <= infraction_epoch) + .collect::>(); + + let new_redelegated_bonds = tot_bonds + .keys() + .filter(|&ep| { + !total_redelegated_unbonded.at(ep).is_empty(storage).unwrap() + }) + .map(|ep| { + ( + *ep, + total_redelegated_unbonded + .at(&epoch) + .at(ep) + .collect_map(storage) + .unwrap(), + ) + }) + .collect::>(); + + redelegated_bonds = new_redelegated_bonds; + + // `newSum` + sum += amount; + + // `newSlashesMap` + let cur = slashed_amounts.entry(epoch).or_default(); + *cur += sum; + } + // Hack - should this be done differently? (think this is safe) + let pipeline_epoch = current_epoch + params.pipeline_len; + let last_amt = slashed_amounts + .get(&pipeline_epoch.prev()) + .cloned() + .unwrap(); + slashed_amounts.insert(pipeline_epoch, last_amt); + + Ok(slashed_amounts) +} + +/// Get the remaining token amount in a bond after applying a set of slashes. +/// +/// - `validator` - the bond's validator +/// - `epoch` - the latest slash epoch to consider. +/// - `start` - the start epoch of the bond +/// - `redelegated_bonds` +fn compute_bond_at_epoch( + storage: &S, + params: &PosParams, + validator: &Address, + epoch: Epoch, + start: Epoch, + amount: token::Amount, + redelegated_bonds: Option<&EagerRedelegatedBondsMap>, +) -> storage_api::Result +where + S: StorageRead, +{ + let list_slashes = validator_slashes_handle(validator) + .iter(storage)? + .map(Result::unwrap) + .filter(|slash| { + // TODO: check bounds on second arg + start <= slash.epoch + && slash.epoch + params.slash_processing_epoch_offset() <= epoch + }) + .collect::>(); + + let slash_epoch_filter = + |e: Epoch| e + params.slash_processing_epoch_offset() <= epoch; + + let result_fold = redelegated_bonds + .map(|redelegated_bonds| { + fold_and_slash_redelegated_bonds( + storage, + params, + redelegated_bonds, + start, + &list_slashes, + slash_epoch_filter, + ) + }) + .unwrap_or_default(); + + let total_not_redelegated = amount - result_fold.total_redelegated; + let after_not_redelegated = + apply_list_slashes(params, &list_slashes, total_not_redelegated); + + Ok(after_not_redelegated + result_fold.total_after_slashing) +} + +/// Uses `fn compute_bond_at_epoch` to compute the token amount to slash in +/// order to prevent overslashing. +#[allow(clippy::too_many_arguments)] +fn compute_slash_bond_at_epoch( + storage: &S, + params: &PosParams, + validator: &Address, + epoch: Epoch, + infraction_epoch: Epoch, + bond_start: Epoch, + bond_amount: token::Amount, + redelegated_bonds: Option<&EagerRedelegatedBondsMap>, + slash_rate: Dec, +) -> storage_api::Result +where + S: StorageRead, +{ + let amount_due = compute_bond_at_epoch( + storage, + params, + validator, + infraction_epoch, + bond_start, + bond_amount, + redelegated_bonds, + )? + .mul_ceil(slash_rate); + let slashable_amount = compute_bond_at_epoch( + storage, + params, + validator, + epoch, + bond_start, + bond_amount, + redelegated_bonds, + )?; + Ok(cmp::min(amount_due, slashable_amount)) +} + /// Unjail a validator that is currently jailed pub fn unjail_validator( storage: &mut S, @@ -3963,8 +5067,7 @@ where // 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(); + read_validator_stake(storage, ¶ms, validator, pipeline_epoch)?; insert_validator_into_validator_set( storage, @@ -4017,6 +5120,7 @@ where /// Find slashes applicable to a validator with inclusive `start` and exclusive /// `end` epoch. +#[allow(dead_code)] fn find_slashes_in_range( storage: &S, start: Epoch, @@ -4032,13 +5136,215 @@ where 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, Dec::one()); } } Ok(slashes) } + +/// Redelegate bonded tokens from a source validator to a destination validator +pub fn redelegate_tokens( + storage: &mut S, + delegator: &Address, + src_validator: &Address, + dest_validator: &Address, + current_epoch: Epoch, + amount: token::Amount, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + tracing::debug!( + "Delegator {} redelegating {} tokens from {} to {}", + delegator, + amount.to_string_native(), + src_validator, + dest_validator + ); + if amount.is_zero() { + return Ok(()); + } + + // The src and dest validators must be different + if src_validator == dest_validator { + return Err(RedelegationError::RedelegationSrcEqDest.into()); + } + + // The delegator must not be a validator + if is_validator(storage, delegator)? { + return Err(RedelegationError::DelegatorIsValidator.into()); + } + + // The src and dest validators must actually be validators + if !is_validator(storage, src_validator)? { + return Err( + RedelegationError::NotAValidator(src_validator.clone()).into() + ); + } + if !is_validator(storage, dest_validator)? { + return Err( + RedelegationError::NotAValidator(dest_validator.clone()).into() + ); + } + + let params = read_pos_params(storage)?; + let pipeline_epoch = current_epoch + params.pipeline_len; + let src_redel_end_epoch = + validator_incoming_redelegations_handle(src_validator) + .get(storage, delegator)?; + + // Forbid chained redelegations. A redelegation is "chained" if: + // 1. the source validator holds bonded tokens that themselves were + // redelegated to the src validator + // 2. given the latest epoch at which the most recently redelegated tokens + // started contributing to the src validator's voting power, these tokens + // cannot be slashed anymore + let is_not_chained = if let Some(end_epoch) = src_redel_end_epoch { + // TODO: check bounds for correctness (> and presence of cubic offset) + let last_contrib_epoch = end_epoch.prev(); + // If the source validator's slashes that would cause slash on + // redelegation are now outdated (would have to be processed before or + // on start of the current epoch), the redelegation can be redelegated + // again + last_contrib_epoch + params.slash_processing_epoch_offset() + <= current_epoch + } else { + true + }; + if !is_not_chained { + return Err(RedelegationError::IsChainedRedelegation.into()); + } + + // Unbond the redelegated tokens from the src validator. + // `resultUnbond` in quint + let result_unbond = unbond_tokens( + storage, + Some(delegator), + src_validator, + amount, + current_epoch, + true, + )?; + + // The unbonded amount after slashing is what is going to be redelegated. + // `amountAfterSlashing` + let amount_after_slashing = result_unbond.sum; + tracing::debug!( + "Redelegated amount after slashing: {}", + amount_after_slashing.to_string_native() + ); + + // Add incoming redelegated bonds to the dest validator. + // `updatedRedelegatedBonds` with updates to delegatorState + // `redelegatedBonded` + let redelegated_bonds = delegator_redelegated_bonds_handle(delegator) + .at(dest_validator) + .at(&pipeline_epoch) + .at(src_validator); + for (&epoch, &unbonded_amount) in result_unbond.epoch_map.iter() { + redelegated_bonds.update(storage, epoch, |current| { + current.unwrap_or_default() + unbonded_amount + })?; + } + + if tracing::level_enabled!(tracing::Level::DEBUG) { + let bonds = find_bonds(storage, delegator, dest_validator)?; + tracing::debug!("\nRedeleg dest bonds before incrementing: {bonds:#?}"); + } + + // Add a bond delta to the destination. + if !amount_after_slashing.is_zero() { + // `updatedDelegator` with updates to `bonded` + let bond_handle = bond_handle(delegator, dest_validator); + bond_handle.add( + storage, + amount_after_slashing, + current_epoch, + params.pipeline_len, + )?; + // `updatedDestValidator` --> `with("totalVBonded")` + // Add the amount to the dest validator total bonded + let dest_total_bonded = total_bonded_handle(dest_validator); + dest_total_bonded.add( + storage, + amount_after_slashing, + current_epoch, + params.pipeline_len, + )?; + } + + if tracing::level_enabled!(tracing::Level::DEBUG) { + let bonds = find_bonds(storage, delegator, dest_validator)?; + tracing::debug!("\nRedeleg dest bonds after incrementing: {bonds:#?}"); + } + + // Add outgoing redelegation to the src validator. + // `updateOutgoingRedelegations` with `updatedSrcValidator` + let outgoing_redelegations = + validator_outgoing_redelegations_handle(src_validator) + .at(dest_validator); + for (start, &unbonded_amount) in result_unbond.epoch_map.iter() { + outgoing_redelegations.at(start).update( + storage, + current_epoch, + |current| current.unwrap_or_default() + unbonded_amount, + )?; + } + + // Add the amount to the dest validator total redelegated bonds. + let dest_total_redelegated_bonded = + validator_total_redelegated_bonded_handle(dest_validator) + .at(&pipeline_epoch) + .at(src_validator); + for (&epoch, &amount) in &result_unbond.epoch_map { + dest_total_redelegated_bonded.update(storage, epoch, |current| { + current.unwrap_or_default() + amount + })?; + } + + // Set the epoch of the validator incoming redelegation from this delegator + let dest_incoming_redelegations = + validator_incoming_redelegations_handle(dest_validator); + dest_incoming_redelegations.insert( + storage, + delegator.clone(), + pipeline_epoch, + )?; + + // Update validator set for dest validator + let is_jailed_at_pipeline = matches!( + validator_state_handle(dest_validator).get( + storage, + pipeline_epoch, + ¶ms + )?, + Some(ValidatorState::Jailed) + ); + if !is_jailed_at_pipeline { + update_validator_set( + storage, + ¶ms, + dest_validator, + amount_after_slashing.change(), + pipeline_epoch, + )?; + } + + // Update deltas + update_validator_deltas( + storage, + dest_validator, + amount_after_slashing.change(), + current_epoch, + params.pipeline_len, + )?; + update_total_deltas( + storage, + amount_after_slashing.change(), + current_epoch, + params.pipeline_len, + )?; + + Ok(()) +} diff --git a/proof_of_stake/src/parameters.rs b/proof_of_stake/src/parameters.rs index 8501aff379..1fe0b33ed3 100644 --- a/proof_of_stake/src/parameters.rs +++ b/proof_of_stake/src/parameters.rs @@ -173,6 +173,37 @@ impl PosParams { let end = infraction_epoch + self.cubic_slashing_window_length; (start, end) } + + /// Get the redelegation end epoch from the start epoch + pub fn redelegation_end_epoch_from_start(&self, end: Epoch) -> Epoch { + end + self.pipeline_len + } + + /// Get the redelegation start epoch from the end epoch + pub fn redelegation_start_epoch_from_end(&self, end: Epoch) -> Epoch { + end - self.pipeline_len + } + + /// Determine if the infraction is in the lazy slashing window for a + /// redelegation source validator. Any source validator slashes that + /// were processed before redelegation was applied will be applied + /// eagerly on the redelegation amount, so this function will only return + /// `true` for applicable infractions that were processed after + /// the redelegation was applied. + /// + /// The `redel_start` is the epoch in which the redelegation was applied and + /// `redel_end` the epoch in which it no longer contributed to source + /// validator's stake. + pub fn in_redelegation_slashing_window( + &self, + infraction_epoch: Epoch, + redel_start: Epoch, + redel_end: Epoch, + ) -> bool { + let processing_epoch = + infraction_epoch + self.slash_processing_epoch_offset(); + redel_start < processing_epoch && infraction_epoch < redel_end + } } #[cfg(test)] diff --git a/proof_of_stake/src/storage.rs b/proof_of_stake/src/storage.rs index 54bd7cfe6b..fe7e6c8d7e 100644 --- a/proof_of_stake/src/storage.rs +++ b/proof_of_stake/src/storage.rs @@ -6,7 +6,7 @@ use namada_core::types::storage::{DbKeySeg, Epoch, Key, KeySeg}; use super::ADDRESS; use crate::epoched::LAZY_MAP_SUB_KEY; -pub use crate::types::*; // TODO: not sure why this needs to be public +use crate::types::BondId; const PARAMS_STORAGE_KEY: &str = "params"; const VALIDATOR_ADDRESSES_KEY: &str = "validator_addresses"; @@ -43,6 +43,13 @@ const CONSENSUS_KEYS: &str = "consensus_keys"; const LAST_BLOCK_PROPOSER_STORAGE_KEY: &str = "last_block_proposer"; const CONSENSUS_VALIDATOR_SET_ACCUMULATOR_STORAGE_KEY: &str = "validator_rewards_accumulator"; +const VALIDATOR_INCOMING_REDELEGATIONS_KEY: &str = "incoming_redelegations"; +const VALIDATOR_OUTGOING_REDELEGATIONS_KEY: &str = "outgoing_redelegations"; +const VALIDATOR_TOTAL_REDELEGATED_BONDED_KEY: &str = "total_redelegated_bonded"; +const VALIDATOR_TOTAL_REDELEGATED_UNBONDED_KEY: &str = + "total_redelegated_unbonded"; +const DELEGATOR_REDELEGATED_BONDS_KEY: &str = "delegator_redelegated_bonds"; +const DELEGATOR_REDELEGATED_UNBONDS_KEY: &str = "delegator_redelegated_unbonds"; /// Is the given key a PoS storage key? pub fn is_pos_key(key: &Key) -> bool { @@ -257,6 +264,66 @@ pub fn validator_delegation_rewards_product_key(validator: &Address) -> Key { .expect("Cannot obtain a storage key") } +/// Storage key for a validator's incoming redelegations, where the prefixed +/// validator is the destination validator. +pub fn validator_incoming_redelegations_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_INCOMING_REDELEGATIONS_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for a validator's outgoing redelegations, where the prefixed +/// validator is the source validator. +pub fn validator_outgoing_redelegations_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_OUTGOING_REDELEGATIONS_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for validator's total-redelegated-bonded amount to track for +/// slashing +pub fn validator_total_redelegated_bonded_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_TOTAL_REDELEGATED_BONDED_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for validator's total-redelegated-unbonded amount to track for +/// slashing +pub fn validator_total_redelegated_unbonded_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_TOTAL_REDELEGATED_UNBONDED_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key prefix for all delegators' redelegated bonds. +pub fn delegator_redelegated_bonds_prefix() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&DELEGATOR_REDELEGATED_BONDS_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for a particular delegator's redelegated bond information. +pub fn delegator_redelegated_bonds_key(delegator: &Address) -> Key { + delegator_redelegated_bonds_prefix() + .push(&delegator.to_db_key()) + .expect("Cannot obtain a storage key") +} + +/// Storage key prefix for all delegators' redelegated unbonds. +pub fn delegator_redelegated_unbonds_prefix() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&DELEGATOR_REDELEGATED_UNBONDS_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for a particular delegator's redelegated unbond information. +pub fn delegator_redelegated_unbonds_key(delegator: &Address) -> Key { + delegator_redelegated_unbonds_prefix() + .push(&delegator.to_db_key()) + .expect("Cannot obtain a storage key") +} + /// Is storage key for validator's delegation rewards products? pub fn is_validator_delegation_rewards_product_key( key: &Key, @@ -521,9 +588,9 @@ pub fn is_unbond_key(key: &Key) -> Option<(BondId, Epoch, Epoch)> { DbKeySeg::AddressSeg(source), DbKeySeg::AddressSeg(validator), DbKeySeg::StringSeg(data_1), - DbKeySeg::StringSeg(withdraw_epoch_str), - DbKeySeg::StringSeg(data_2), DbKeySeg::StringSeg(start_epoch_str), + DbKeySeg::StringSeg(data_2), + DbKeySeg::StringSeg(withdraw_epoch_str), ] if addr == &ADDRESS && prefix == UNBOND_STORAGE_KEY && data_1 == lazy_map::DATA_SUBKEY diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index b7463c8ea5..8c2ce7fbc4 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -1,16 +1,25 @@ //! PoS system tests mod state_machine; +mod state_machine_v2; +mod utils; -use std::cmp::min; -use std::ops::Range; +use std::cmp::{max, min}; +use std::collections::{BTreeMap, BTreeSet}; +use std::ops::{Deref, Range}; +use std::str::FromStr; +use assert_matches::assert_matches; use namada_core::ledger::storage::testing::TestWlStorage; -use namada_core::ledger::storage_api::collections::lazy_map; +use namada_core::ledger::storage_api::collections::lazy_map::{ + self, Collectable, NestedMap, +}; +use namada_core::ledger::storage_api::collections::LazyCollection; use namada_core::ledger::storage_api::token::{credit_tokens, read_balance}; use namada_core::ledger::storage_api::StorageRead; use namada_core::types::address::testing::{ - address_from_simple_seed, arb_established_address, + address_from_simple_seed, arb_established_address, established_address_1, + established_address_2, established_address_3, }; use namada_core::types::address::{Address, EstablishedAddressGen}; use namada_core::types::dec::Dec; @@ -19,9 +28,9 @@ use namada_core::types::key::testing::{ arb_common_keypair, common_sk_from_simple_seed, }; use namada_core::types::key::RefTo; -use namada_core::types::storage::{BlockHeight, Epoch}; +use namada_core::types::storage::{BlockHeight, Epoch, Key}; +use namada_core::types::token::testing::arb_amount_non_zero_ceiled; use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; -use namada_core::types::uint::Uint; use namada_core::types::{address, key, token}; use proptest::prelude::*; use proptest::test_runner::Config; @@ -33,34 +42,45 @@ 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, - SlashType, UnbondDetails, ValidatorSetUpdate, ValidatorState, - WeightedValidator, + ConsensusValidator, EagerRedelegatedBondsMap, GenesisValidator, Position, + RedelegatedTokens, ReverseOrdTokenAmount, Slash, SlashType, UnbondDetails, + ValidatorSetUpdate, ValidatorState, WeightedValidator, }; 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, - get_num_consensus_validators, init_genesis, - insert_validator_into_validator_set, is_validator, process_slashes, - purge_validator_sets_for_old_epoch, + apply_list_slashes, become_validator, below_capacity_validator_set_handle, + bond_handle, bond_tokens, bonds_and_unbonds, + compute_amount_after_slashing_unbond, + compute_amount_after_slashing_withdraw, compute_bond_at_epoch, + compute_modified_redelegation, compute_new_redelegated_unbonds, + compute_slash_bond_at_epoch, compute_slashable_amount, + consensus_validator_set_handle, copy_validator_sets_and_positions, + delegator_redelegated_bonds_handle, delegator_redelegated_unbonds_handle, + find_bonds_to_remove, find_validator_by_raw_hash, + fold_and_slash_redelegated_bonds, get_num_consensus_validators, + init_genesis, insert_validator_into_validator_set, is_validator, + process_slashes, purge_validator_sets_for_old_epoch, read_below_capacity_validator_set_addresses_with_stake, read_below_threshold_validator_set_addresses, read_consensus_validator_set_addresses_with_stake, read_total_stake, - read_validator_delta_value, read_validator_stake, slash, - staking_token_address, store_total_consensus_stake, total_deltas_handle, - unbond_handle, unbond_tokens, unjail_validator, update_validator_deltas, - update_validator_set, validator_consensus_key_handle, - validator_set_positions_handle, validator_set_update_tendermint, - validator_slashes_handle, validator_state_handle, withdraw_tokens, - write_validator_address_raw_hash, BecomeValidator, + read_validator_deltas_value, read_validator_stake, slash, + slash_redelegation, slash_validator, slash_validator_redelegation, + staking_token_address, store_total_consensus_stake, total_bonded_handle, + total_deltas_handle, total_unbonded_handle, unbond_handle, unbond_tokens, + unjail_validator, update_validator_deltas, update_validator_set, + validator_consensus_key_handle, validator_incoming_redelegations_handle, + validator_outgoing_redelegations_handle, validator_set_positions_handle, + validator_set_update_tendermint, validator_slashes_handle, + validator_state_handle, validator_total_redelegated_bonded_handle, + validator_total_redelegated_unbonded_handle, withdraw_tokens, + write_validator_address_raw_hash, BecomeValidator, EagerRedelegatedUnbonds, + FoldRedelegatedBondsResult, ModifiedRedelegation, RedelegationError, STORE_VALIDATOR_SETS_LEN, }; proptest! { // Generate arb valid input for `test_init_genesis_aux` #![proptest_config(Config { - cases: 1, + cases: 100, .. Config::default() })] #[test] @@ -77,7 +97,7 @@ proptest! { proptest! { // Generate arb valid input for `test_bonds_aux` #![proptest_config(Config { - cases: 1, + cases: 100, .. Config::default() })] #[test] @@ -93,7 +113,7 @@ proptest! { proptest! { // Generate arb valid input for `test_become_validator_aux` #![proptest_config(Config { - cases: 1, + cases: 100, .. Config::default() })] #[test] @@ -112,7 +132,7 @@ proptest! { proptest! { // Generate arb valid input for `test_slashes_with_unbonding_aux` #![proptest_config(Config { - cases: 5, + cases: 100, .. Config::default() })] #[test] @@ -128,7 +148,7 @@ proptest! { proptest! { // Generate arb valid input for `test_unjail_validator_aux` #![proptest_config(Config { - cases: 5, + cases: 100, .. Config::default() })] #[test] @@ -141,6 +161,72 @@ proptest! { } } +proptest! { + // Generate arb valid input for `test_simple_redelegation_aux` + #![proptest_config(Config { + cases: 100, + .. Config::default() + })] + #[test] + fn test_simple_redelegation( + + genesis_validators in arb_genesis_validators(2..4, None), + (amount_delegate, amount_redelegate, amount_unbond) in arb_redelegation_amounts(20) + + ) { + test_simple_redelegation_aux(genesis_validators, amount_delegate, amount_redelegate, amount_unbond) + } +} + +proptest! { + // Generate arb valid input for `test_simple_redelegation_aux` + #![proptest_config(Config { + cases: 100, + .. Config::default() + })] + #[test] + fn test_redelegation_with_slashing( + + genesis_validators in arb_genesis_validators(2..4, None), + (amount_delegate, amount_redelegate, amount_unbond) in arb_redelegation_amounts(20) + + ) { + test_redelegation_with_slashing_aux(genesis_validators, amount_delegate, amount_redelegate, amount_unbond) + } +} + +proptest! { + // Generate arb valid input for `test_chain_redelegations_aux` + #![proptest_config(Config { + cases: 100, + .. Config::default() + })] + #[test] + fn test_chain_redelegations( + + genesis_validators in arb_genesis_validators(3..4, None), + + ) { + test_chain_redelegations_aux(genesis_validators) + } +} + +proptest! { + // Generate arb valid input for `test_overslashing_aux` + #![proptest_config(Config { + cases: 1, + .. Config::default() + })] + #[test] + fn test_overslashing( + + genesis_validators in arb_genesis_validators(4..5, None), + + ) { + test_overslashing_aux(genesis_validators) + } +} + fn arb_params_and_genesis_validators( num_max_validator_slots: Option, val_size: Range, @@ -303,10 +389,8 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { // Check the bond delta let self_bond = bond_handle(&validator.address, &validator.address); - let delta = self_bond - .get_delta_val(&s, pipeline_epoch, ¶ms) - .unwrap(); - assert_eq!(delta, Some(amount_self_bond.change())); + let delta = self_bond.get_delta_val(&s, pipeline_epoch).unwrap(); + assert_eq!(delta, Some(amount_self_bond)); // Check the validator in the validator set let set = @@ -322,13 +406,9 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { } )); - let val_deltas = read_validator_delta_value( - &s, - ¶ms, - &validator.address, - pipeline_epoch, - ) - .unwrap(); + let val_deltas = + read_validator_deltas_value(&s, &validator.address, &pipeline_epoch) + .unwrap(); assert_eq!(val_deltas, Some(amount_self_bond.change())); let total_deltas_handle = total_deltas_handle(); @@ -423,12 +503,10 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { &validator.address, pipeline_epoch.prev(), ) - .unwrap() - .unwrap_or_default(); + .unwrap(); let val_stake_post = read_validator_stake(&s, ¶ms, &validator.address, pipeline_epoch) - .unwrap() - .unwrap_or_default(); + .unwrap(); assert_eq!(validator.tokens + amount_self_bond, val_stake_pre); assert_eq!( validator.tokens + amount_self_bond + amount_del, @@ -440,14 +518,14 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { .get_sum(&s, pipeline_epoch.prev(), ¶ms) .unwrap() .unwrap_or_default(), - token::Change::default() + token::Amount::zero() ); assert_eq!( delegation .get_sum(&s, pipeline_epoch, ¶ms) .unwrap() .unwrap_or_default(), - amount_del.change() + amount_del ); // Check delegation bonds details after delegation @@ -532,7 +610,7 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { amount_self_bond + (validator.tokens / 2); // When the difference is 0, only the non-genesis self-bond is unbonded let unbonded_genesis_self_bond = - amount_self_unbond - amount_self_bond != token::Amount::default(); + amount_self_unbond - amount_self_bond != token::Amount::zero(); dbg!( amount_self_unbond, amount_self_bond, @@ -546,6 +624,7 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { &validator.address, amount_self_unbond, current_epoch, + false, ) .unwrap(); @@ -561,22 +640,21 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { read_validator_stake(&s, ¶ms, &validator.address, pipeline_epoch) .unwrap(); - let val_delta = read_validator_delta_value( - &s, - ¶ms, - &validator.address, - pipeline_epoch, - ) - .unwrap(); + let val_delta = + read_validator_deltas_value(&s, &validator.address, &pipeline_epoch) + .unwrap(); let unbond = unbond_handle(&validator.address, &validator.address); assert_eq!(val_delta, Some(-amount_self_unbond.change())); assert_eq!( unbond - .at(&(pipeline_epoch - + params.unbonding_len - + params.cubic_slashing_window_length)) - .get(&s, &Epoch::default()) + .at(&Epoch::default()) + .get( + &s, + &(pipeline_epoch + + params.unbonding_len + + params.cubic_slashing_window_length) + ) .unwrap(), if unbonded_genesis_self_bond { Some(amount_self_unbond - amount_self_bond) @@ -586,23 +664,23 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { ); assert_eq!( unbond - .at(&(pipeline_epoch - + params.unbonding_len - + params.cubic_slashing_window_length)) - .get(&s, &(self_bond_epoch + params.pipeline_len)) + .at(&(self_bond_epoch + params.pipeline_len)) + .get( + &s, + &(pipeline_epoch + + params.unbonding_len + + params.cubic_slashing_window_length) + ) .unwrap(), Some(amount_self_bond) ); assert_eq!( val_stake_pre, - Some(validator.tokens + amount_self_bond + amount_del) + validator.tokens + amount_self_bond + amount_del ); assert_eq!( val_stake_post, - Some( - validator.tokens + amount_self_bond + amount_del - - amount_self_unbond - ) + validator.tokens + amount_self_bond + amount_del - amount_self_unbond ); // Check all bond and unbond details (self-bonds and delegation) @@ -680,6 +758,7 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { &validator.address, amount_undel, current_epoch, + false, ) .unwrap(); @@ -693,13 +772,9 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { let val_stake_post = read_validator_stake(&s, ¶ms, &validator.address, pipeline_epoch) .unwrap(); - let val_delta = read_validator_delta_value( - &s, - ¶ms, - &validator.address, - pipeline_epoch, - ) - .unwrap(); + let val_delta = + read_validator_deltas_value(&s, &validator.address, &pipeline_epoch) + .unwrap(); let unbond = unbond_handle(&delegator, &validator.address); assert_eq!( @@ -708,24 +783,24 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { ); assert_eq!( unbond - .at(&(pipeline_epoch - + params.unbonding_len - + params.cubic_slashing_window_length)) - .get(&s, &(delegation_epoch + params.pipeline_len)) + .at(&(delegation_epoch + params.pipeline_len)) + .get( + &s, + &(pipeline_epoch + + params.unbonding_len + + params.cubic_slashing_window_length) + ) .unwrap(), Some(amount_undel) ); assert_eq!( val_stake_pre, - Some(validator.tokens + amount_self_bond + amount_del) + validator.tokens + amount_self_bond + amount_del ); assert_eq!( val_stake_post, - Some( - validator.tokens + amount_self_bond - amount_self_unbond - + amount_del - - amount_undel - ) + validator.tokens + amount_self_bond - amount_self_unbond + amount_del + - amount_undel ); let withdrawable_offset = params.unbonding_len @@ -888,10 +963,8 @@ fn test_become_validator_aux( // Check the bond delta let bond_handle = bond_handle(&new_validator, &new_validator); let pipeline_epoch = current_epoch + params.pipeline_len; - let delta = bond_handle - .get_delta_val(&s, pipeline_epoch, ¶ms) - .unwrap(); - assert_eq!(delta, Some(amount.change())); + let delta = bond_handle.get_delta_val(&s, pipeline_epoch).unwrap(); + assert_eq!(delta, Some(amount)); // Check the validator in the validator set - // If the consensus validator slots are full and all the genesis validators @@ -935,7 +1008,8 @@ fn test_become_validator_aux( current_epoch = advance_epoch(&mut s, ¶ms); // Unbond the self-bond - unbond_tokens(&mut s, None, &new_validator, amount, current_epoch).unwrap(); + unbond_tokens(&mut s, None, &new_validator, amount, current_epoch, false) + .unwrap(); let withdrawable_offset = params.unbonding_len + params.pipeline_len; @@ -1022,7 +1096,8 @@ fn test_slashes_with_unbonding_aux( let unbond_amount = Dec::new(5, 1).unwrap() * val_tokens; println!("Going to unbond {}", unbond_amount.to_string_native()); let unbond_epoch = current_epoch; - unbond_tokens(&mut s, None, val_addr, unbond_amount, unbond_epoch).unwrap(); + unbond_tokens(&mut s, None, val_addr, unbond_amount, unbond_epoch, false) + .unwrap(); // Discover second slash let slash_1_evidence_epoch = current_epoch; @@ -1163,7 +1238,6 @@ fn test_validator_sets() { update_validator_deltas( s, - ¶ms, addr, stake.change(), epoch, @@ -1459,13 +1533,18 @@ fn test_validator_sets() { // Because `update_validator_set` and `update_validator_deltas` are // effective from pipeline offset, we use pipeline epoch for the rest of the // checks - update_validator_set(&mut s, ¶ms, &val1, -unbond.change(), epoch) - .unwrap(); - update_validator_deltas( + update_validator_set( &mut s, ¶ms, &val1, -unbond.change(), + pipeline_epoch, + ) + .unwrap(); + update_validator_deltas( + &mut s, + &val1, + -unbond.change(), epoch, params.pipeline_len, ) @@ -1655,10 +1734,10 @@ fn test_validator_sets() { let bond = token::Amount::from_uint(500_000, 0).unwrap(); let stake6 = stake6 + bond; println!("val6 {val6} new stake {}", stake6.to_string_native()); - update_validator_set(&mut s, ¶ms, &val6, bond.change(), epoch).unwrap(); + update_validator_set(&mut s, ¶ms, &val6, bond.change(), pipeline_epoch) + .unwrap(); update_validator_deltas( &mut s, - ¶ms, &val6, bond.change(), epoch, @@ -1808,7 +1887,7 @@ fn test_validator_sets_swap() { max_validator_slots: 2, // Set the stake threshold to 0 so no validators are in the // below-threshold set - validator_stake_threshold: token::Amount::default(), + validator_stake_threshold: token::Amount::zero(), // Set 0.1 votes per token tm_votes_per_token: Dec::new(1, 1).expect("Dec creation failed"), ..Default::default() @@ -1844,7 +1923,6 @@ fn test_validator_sets_swap() { update_validator_deltas( s, - ¶ms, addr, stake.change(), epoch, @@ -1936,25 +2014,35 @@ fn test_validator_sets_swap() { assert_eq!(into_tm_voting_power(params.tm_votes_per_token, stake2), 0); assert_eq!(into_tm_voting_power(params.tm_votes_per_token, stake3), 0); - update_validator_set(&mut s, ¶ms, &val2, bond2.change(), epoch) - .unwrap(); - update_validator_deltas( + update_validator_set( &mut s, ¶ms, &val2, bond2.change(), + pipeline_epoch, + ) + .unwrap(); + update_validator_deltas( + &mut s, + &val2, + bond2.change(), epoch, params.pipeline_len, ) .unwrap(); - update_validator_set(&mut s, ¶ms, &val3, bond3.change(), epoch) - .unwrap(); - update_validator_deltas( + update_validator_set( &mut s, ¶ms, &val3, bond3.change(), + pipeline_epoch, + ) + .unwrap(); + update_validator_deltas( + &mut s, + &val3, + bond3.change(), epoch, params.pipeline_len, ) @@ -1975,25 +2063,35 @@ fn test_validator_sets_swap() { into_tm_voting_power(params.tm_votes_per_token, stake3) ); - update_validator_set(&mut s, ¶ms, &val2, bonds.change(), epoch) - .unwrap(); - update_validator_deltas( + update_validator_set( &mut s, ¶ms, &val2, bonds.change(), + pipeline_epoch, + ) + .unwrap(); + update_validator_deltas( + &mut s, + &val2, + bonds.change(), epoch, params.pipeline_len, ) .unwrap(); - update_validator_set(&mut s, ¶ms, &val3, bonds.change(), epoch) - .unwrap(); - update_validator_deltas( + update_validator_set( &mut s, ¶ms, &val3, bonds.change(), + pipeline_epoch, + ) + .unwrap(); + update_validator_deltas( + &mut s, + &val3, + bonds.change(), epoch, params.pipeline_len, ) @@ -2063,16 +2161,15 @@ fn arb_genesis_validators( size: Range, threshold: Option, ) -> impl Strategy> { + let threshold = threshold + .unwrap_or_else(|| PosParams::default().validator_stake_threshold); let tokens: Vec<_> = (0..size.end) .map(|ix| { if ix == 0 { - // If there's a threshold, make sure that at least one validator - // has at least a stake greater or equal to the threshold to - // avoid having an empty consensus set. - threshold - .map(|token| token.raw_amount()) - .unwrap_or(Uint::one()) - .as_u64()..=10_000_000_u64 + // Make sure that at least one validator has at least a stake + // greater or equal to the threshold to avoid having an empty + // consensus set. + threshold.raw_amount().as_u64()..=10_000_000_u64 } else { 1..=10_000_000_u64 } @@ -2121,11 +2218,7 @@ fn arb_genesis_validators( "Must have at least one genesis validator with stake above the \ provided threshold, if any.", move |gen_vals: &Vec| { - if let Some(thresh) = threshold { - gen_vals.iter().any(|val| val.tokens >= thresh) - } else { - true - } + gen_vals.iter().any(|val| val.tokens >= threshold) }, ) } @@ -2253,3 +2346,3384 @@ fn test_unjail_validator_aux( let second_att = unjail_validator(&mut s, val_addr, current_epoch); assert!(second_att.is_err()); } + +/// `iterateBondsUpToAmountTest` +#[test] +fn test_find_bonds_to_remove() { + let mut storage = TestWlStorage::default(); + let source = established_address_1(); + let validator = established_address_2(); + let bond_handle = bond_handle(&source, &validator); + + let (e1, e2, e6) = (Epoch(1), Epoch(2), Epoch(6)); + + bond_handle + .set(&mut storage, token::Amount::from(5), e1, 0) + .unwrap(); + bond_handle + .set(&mut storage, token::Amount::from(3), e2, 0) + .unwrap(); + bond_handle + .set(&mut storage, token::Amount::from(8), e6, 0) + .unwrap(); + + // Test 1 + let bonds_for_removal = find_bonds_to_remove( + &storage, + &bond_handle.get_data_handler(), + token::Amount::from(8), + ) + .unwrap(); + assert_eq!( + bonds_for_removal.epochs, + vec![e6].into_iter().collect::>() + ); + assert!(bonds_for_removal.new_entry.is_none()); + + // Test 2 + let bonds_for_removal = find_bonds_to_remove( + &storage, + &bond_handle.get_data_handler(), + token::Amount::from(10), + ) + .unwrap(); + assert_eq!( + bonds_for_removal.epochs, + vec![e6].into_iter().collect::>() + ); + assert_eq!( + bonds_for_removal.new_entry, + Some((Epoch(2), token::Amount::from(1))) + ); + + // Test 3 + let bonds_for_removal = find_bonds_to_remove( + &storage, + &bond_handle.get_data_handler(), + token::Amount::from(11), + ) + .unwrap(); + assert_eq!( + bonds_for_removal.epochs, + vec![e6, e2].into_iter().collect::>() + ); + assert!(bonds_for_removal.new_entry.is_none()); + + // Test 4 + let bonds_for_removal = find_bonds_to_remove( + &storage, + &bond_handle.get_data_handler(), + token::Amount::from(12), + ) + .unwrap(); + assert_eq!( + bonds_for_removal.epochs, + vec![e6, e2].into_iter().collect::>() + ); + assert_eq!( + bonds_for_removal.new_entry, + Some((Epoch(1), token::Amount::from(4))) + ); +} + +/// `computeModifiedRedelegationTest` +#[test] +fn test_compute_modified_redelegation() { + let mut storage = TestWlStorage::default(); + let validator1 = established_address_1(); + let validator2 = established_address_2(); + let owner = established_address_3(); + let outer_epoch = Epoch(0); + + let mut alice = validator1.clone(); + let mut bob = validator2.clone(); + + // Ensure a ranking order of alice > bob + // TODO: check why this needs to be > (am I just confusing myself?) + if bob > alice { + alice = validator2; + bob = validator1; + } + println!("\n\nalice = {}\nbob = {}\n", &alice, &bob); + + // Fill redelegated bonds in storage + let redelegated_bonds_map = delegator_redelegated_bonds_handle(&owner) + .at(&alice) + .at(&outer_epoch); + redelegated_bonds_map + .at(&alice) + .insert(&mut storage, Epoch(2), token::Amount::from(6)) + .unwrap(); + redelegated_bonds_map + .at(&alice) + .insert(&mut storage, Epoch(4), token::Amount::from(7)) + .unwrap(); + redelegated_bonds_map + .at(&bob) + .insert(&mut storage, Epoch(1), token::Amount::from(5)) + .unwrap(); + redelegated_bonds_map + .at(&bob) + .insert(&mut storage, Epoch(4), token::Amount::from(7)) + .unwrap(); + + // Test cases 1 and 2 + let mr1 = compute_modified_redelegation( + &storage, + &redelegated_bonds_map, + Epoch(5), + token::Amount::from(25), + ) + .unwrap(); + let mr2 = compute_modified_redelegation( + &storage, + &redelegated_bonds_map, + Epoch(5), + token::Amount::from(30), + ) + .unwrap(); + + let exp_mr = ModifiedRedelegation { + epoch: Some(Epoch(5)), + ..Default::default() + }; + + assert_eq!(mr1, exp_mr); + assert_eq!(mr2, exp_mr); + + // Test case 3 + let mr3 = compute_modified_redelegation( + &storage, + &redelegated_bonds_map, + Epoch(5), + token::Amount::from(7), + ) + .unwrap(); + + let exp_mr = ModifiedRedelegation { + epoch: Some(Epoch(5)), + validators_to_remove: BTreeSet::from_iter([bob.clone()]), + validator_to_modify: Some(bob.clone()), + epochs_to_remove: BTreeSet::from_iter([Epoch(4)]), + ..Default::default() + }; + assert_eq!(mr3, exp_mr); + + // Test case 4 + let mr4 = compute_modified_redelegation( + &storage, + &redelegated_bonds_map, + Epoch(5), + token::Amount::from(8), + ) + .unwrap(); + + let exp_mr = ModifiedRedelegation { + epoch: Some(Epoch(5)), + validators_to_remove: BTreeSet::from_iter([bob.clone()]), + validator_to_modify: Some(bob.clone()), + epochs_to_remove: BTreeSet::from_iter([Epoch(1), Epoch(4)]), + epoch_to_modify: Some(Epoch(1)), + new_amount: Some(4.into()), + }; + assert_eq!(mr4, exp_mr); + + // Test case 5 + let mr5 = compute_modified_redelegation( + &storage, + &redelegated_bonds_map, + Epoch(5), + 12.into(), + ) + .unwrap(); + + let exp_mr = ModifiedRedelegation { + epoch: Some(Epoch(5)), + validators_to_remove: BTreeSet::from_iter([bob.clone()]), + ..Default::default() + }; + assert_eq!(mr5, exp_mr); + + // Test case 6 + let mr6 = compute_modified_redelegation( + &storage, + &redelegated_bonds_map, + Epoch(5), + 14.into(), + ) + .unwrap(); + + let exp_mr = ModifiedRedelegation { + epoch: Some(Epoch(5)), + validators_to_remove: BTreeSet::from_iter([alice.clone(), bob.clone()]), + validator_to_modify: Some(alice.clone()), + epochs_to_remove: BTreeSet::from_iter([Epoch(4)]), + epoch_to_modify: Some(Epoch(4)), + new_amount: Some(5.into()), + }; + assert_eq!(mr6, exp_mr); + + // Test case 7 + let mr7 = compute_modified_redelegation( + &storage, + &redelegated_bonds_map, + Epoch(5), + 19.into(), + ) + .unwrap(); + + let exp_mr = ModifiedRedelegation { + epoch: Some(Epoch(5)), + validators_to_remove: BTreeSet::from_iter([alice.clone(), bob.clone()]), + validator_to_modify: Some(alice.clone()), + epochs_to_remove: BTreeSet::from_iter([Epoch(4)]), + ..Default::default() + }; + assert_eq!(mr7, exp_mr); + + // Test case 8 + let mr8 = compute_modified_redelegation( + &storage, + &redelegated_bonds_map, + Epoch(5), + 21.into(), + ) + .unwrap(); + + let exp_mr = ModifiedRedelegation { + epoch: Some(Epoch(5)), + validators_to_remove: BTreeSet::from_iter([alice.clone(), bob]), + validator_to_modify: Some(alice), + epochs_to_remove: BTreeSet::from_iter([Epoch(2), Epoch(4)]), + epoch_to_modify: Some(Epoch(2)), + new_amount: Some(4.into()), + }; + assert_eq!(mr8, exp_mr); +} + +/// `computeBondAtEpochTest` +#[test] +fn test_compute_bond_at_epoch() { + let mut storage = TestWlStorage::default(); + let params = PosParams { + pipeline_len: 2, + unbonding_len: 4, + cubic_slashing_window_length: 1, + ..Default::default() + }; + let alice = established_address_1(); + let bob = established_address_2(); + + // Test 1 + let res = compute_bond_at_epoch( + &storage, + ¶ms, + &bob, + 12.into(), + 3.into(), + 23.into(), + Some(&Default::default()), + ) + .unwrap(); + + pretty_assertions::assert_eq!(res, 23.into()); + + // Test 2 + validator_slashes_handle(&bob) + .push( + &mut storage, + Slash { + epoch: 4.into(), + block_height: 0, + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }, + ) + .unwrap(); + let res = compute_bond_at_epoch( + &storage, + ¶ms, + &bob, + 12.into(), + 3.into(), + 23.into(), + Some(&Default::default()), + ) + .unwrap(); + + pretty_assertions::assert_eq!(res, 0.into()); + + // Test 3 + validator_slashes_handle(&bob).pop(&mut storage).unwrap(); + let mut redel_bonds = EagerRedelegatedBondsMap::default(); + redel_bonds.insert( + alice.clone(), + BTreeMap::from_iter([(Epoch(1), token::Amount::from(5))]), + ); + let res = compute_bond_at_epoch( + &storage, + ¶ms, + &bob, + 12.into(), + 3.into(), + 23.into(), + Some(&redel_bonds), + ) + .unwrap(); + + pretty_assertions::assert_eq!(res, 23.into()); + + // Test 4 + validator_slashes_handle(&bob) + .push( + &mut storage, + Slash { + epoch: 4.into(), + block_height: 0, + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }, + ) + .unwrap(); + let res = compute_bond_at_epoch( + &storage, + ¶ms, + &bob, + 12.into(), + 3.into(), + 23.into(), + Some(&redel_bonds), + ) + .unwrap(); + + pretty_assertions::assert_eq!(res, 0.into()); + + // Test 5 + validator_slashes_handle(&bob).pop(&mut storage).unwrap(); + validator_slashes_handle(&alice) + .push( + &mut storage, + Slash { + epoch: 6.into(), + block_height: 0, + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }, + ) + .unwrap(); + let res = compute_bond_at_epoch( + &storage, + ¶ms, + &bob, + 12.into(), + 3.into(), + 23.into(), + Some(&redel_bonds), + ) + .unwrap(); + + pretty_assertions::assert_eq!(res, 23.into()); + + // Test 6 + validator_slashes_handle(&alice).pop(&mut storage).unwrap(); + validator_slashes_handle(&alice) + .push( + &mut storage, + Slash { + epoch: 4.into(), + block_height: 0, + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }, + ) + .unwrap(); + let res = compute_bond_at_epoch( + &storage, + ¶ms, + &bob, + 18.into(), + 9.into(), + 23.into(), + Some(&redel_bonds), + ) + .unwrap(); + + pretty_assertions::assert_eq!(res, 18.into()); +} + +/// `computeSlashBondAtEpochTest` +#[test] +fn test_compute_slash_bond_at_epoch() { + let mut storage = TestWlStorage::default(); + let params = PosParams { + pipeline_len: 2, + unbonding_len: 4, + cubic_slashing_window_length: 1, + ..Default::default() + }; + let alice = established_address_1(); + let bob = established_address_2(); + + let current_epoch = Epoch(20); + let infraction_epoch = + current_epoch - params.slash_processing_epoch_offset(); + + let redelegated_bond = BTreeMap::from_iter([( + alice, + BTreeMap::from_iter([(infraction_epoch - 4, token::Amount::from(10))]), + )]); + + // Test 1 + let res = compute_slash_bond_at_epoch( + &storage, + ¶ms, + &bob, + current_epoch.next(), + infraction_epoch, + infraction_epoch - 2, + 30.into(), + Some(&Default::default()), + Dec::one(), + ) + .unwrap(); + + pretty_assertions::assert_eq!(res, 30.into()); + + // Test 2 + let res = compute_slash_bond_at_epoch( + &storage, + ¶ms, + &bob, + current_epoch.next(), + infraction_epoch, + infraction_epoch - 2, + 30.into(), + Some(&redelegated_bond), + Dec::one(), + ) + .unwrap(); + + pretty_assertions::assert_eq!(res, 30.into()); + + // Test 3 + validator_slashes_handle(&bob) + .push( + &mut storage, + Slash { + epoch: infraction_epoch.prev(), + block_height: 0, + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }, + ) + .unwrap(); + let res = compute_slash_bond_at_epoch( + &storage, + ¶ms, + &bob, + current_epoch.next(), + infraction_epoch, + infraction_epoch - 2, + 30.into(), + Some(&Default::default()), + Dec::one(), + ) + .unwrap(); + + pretty_assertions::assert_eq!(res, 0.into()); + + // Test 4 + let res = compute_slash_bond_at_epoch( + &storage, + ¶ms, + &bob, + current_epoch.next(), + infraction_epoch, + infraction_epoch - 2, + 30.into(), + Some(&redelegated_bond), + Dec::one(), + ) + .unwrap(); + + pretty_assertions::assert_eq!(res, 0.into()); +} + +/// `computeNewRedelegatedUnbondsTest` +#[test] +fn test_compute_new_redelegated_unbonds() { + let mut storage = TestWlStorage::default(); + let alice = established_address_1(); + let bob = established_address_2(); + + let key = Key::parse("testing").unwrap(); + let redelegated_bonds = NestedMap::::open(key); + + // Populate the lazy and eager maps + let (ep1, ep2, ep4, ep5, ep6, ep7) = + (Epoch(1), Epoch(2), Epoch(4), Epoch(5), Epoch(6), Epoch(7)); + let keys_and_values = vec![ + (ep5, alice.clone(), ep2, 1), + (ep5, alice.clone(), ep4, 1), + (ep7, alice.clone(), ep2, 1), + (ep7, alice.clone(), ep4, 1), + (ep5, bob.clone(), ep1, 1), + (ep5, bob.clone(), ep4, 2), + (ep7, bob.clone(), ep1, 1), + (ep7, bob.clone(), ep4, 2), + ]; + let mut eager_map = BTreeMap::::new(); + for (outer_ep, address, inner_ep, amount) in keys_and_values { + redelegated_bonds + .at(&outer_ep) + .at(&address) + .insert(&mut storage, inner_ep, token::Amount::from(amount)) + .unwrap(); + eager_map + .entry(outer_ep) + .or_default() + .entry(address.clone()) + .or_default() + .insert(inner_ep, token::Amount::from(amount)); + } + + // Different ModifiedRedelegation objects for testing + let empty_mr = ModifiedRedelegation::default(); + let all_mr = ModifiedRedelegation { + epoch: Some(ep7), + validators_to_remove: BTreeSet::from_iter([alice.clone(), bob.clone()]), + validator_to_modify: None, + epochs_to_remove: Default::default(), + epoch_to_modify: None, + new_amount: None, + }; + let mod_val_mr = ModifiedRedelegation { + epoch: Some(ep7), + validators_to_remove: BTreeSet::from_iter([alice.clone()]), + validator_to_modify: None, + epochs_to_remove: Default::default(), + epoch_to_modify: None, + new_amount: None, + }; + let mod_val_partial_mr = ModifiedRedelegation { + epoch: Some(ep7), + validators_to_remove: BTreeSet::from_iter([alice.clone(), bob.clone()]), + validator_to_modify: Some(bob.clone()), + epochs_to_remove: BTreeSet::from_iter([ep1]), + epoch_to_modify: None, + new_amount: None, + }; + let mod_epoch_partial_mr = ModifiedRedelegation { + epoch: Some(ep7), + validators_to_remove: BTreeSet::from_iter([alice, bob.clone()]), + validator_to_modify: Some(bob.clone()), + epochs_to_remove: BTreeSet::from_iter([ep1, ep4]), + epoch_to_modify: Some(ep4), + new_amount: Some(token::Amount::from(1)), + }; + + // Test case 1 + let res = compute_new_redelegated_unbonds( + &storage, + &redelegated_bonds, + &Default::default(), + &empty_mr, + ) + .unwrap(); + assert_eq!(res, Default::default()); + + let set5 = BTreeSet::::from_iter([ep5]); + let set56 = BTreeSet::::from_iter([ep5, ep6]); + + // Test case 2 + let res = compute_new_redelegated_unbonds( + &storage, + &redelegated_bonds, + &set5, + &empty_mr, + ) + .unwrap(); + let mut exp_res = eager_map.clone(); + exp_res.remove(&ep7); + assert_eq!(res, exp_res); + + // Test case 3 + let res = compute_new_redelegated_unbonds( + &storage, + &redelegated_bonds, + &set56, + &empty_mr, + ) + .unwrap(); + assert_eq!(res, exp_res); + + // Test case 4 + println!("\nTEST CASE 4\n"); + let res = compute_new_redelegated_unbonds( + &storage, + &redelegated_bonds, + &set56, + &all_mr, + ) + .unwrap(); + assert_eq!(res, eager_map); + + // Test case 5 + let res = compute_new_redelegated_unbonds( + &storage, + &redelegated_bonds, + &set56, + &mod_val_mr, + ) + .unwrap(); + exp_res = eager_map.clone(); + exp_res.entry(ep7).or_default().remove(&bob); + assert_eq!(res, exp_res); + + // Test case 6 + let res = compute_new_redelegated_unbonds( + &storage, + &redelegated_bonds, + &set56, + &mod_val_partial_mr, + ) + .unwrap(); + exp_res = eager_map.clone(); + exp_res + .entry(ep7) + .or_default() + .entry(bob.clone()) + .or_default() + .remove(&ep4); + assert_eq!(res, exp_res); + + // Test case 7 + let res = compute_new_redelegated_unbonds( + &storage, + &redelegated_bonds, + &set56, + &mod_epoch_partial_mr, + ) + .unwrap(); + exp_res + .entry(ep7) + .or_default() + .entry(bob) + .or_default() + .insert(ep4, token::Amount::from(1)); + assert_eq!(res, exp_res); +} + +/// `applyListSlashesTest` +#[test] +fn test_apply_list_slashes() { + let init_epoch = Epoch(2); + let params = PosParams { + unbonding_len: 4, + ..Default::default() + }; + // let unbonding_len = 4u64; + // let cubic_offset = 1u64; + + let slash1 = Slash { + epoch: init_epoch, + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }; + let slash2 = Slash { + epoch: init_epoch + + params.unbonding_len + + params.cubic_slashing_window_length + + 1u64, + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }; + + let list1 = vec![slash1.clone()]; + let list2 = vec![slash1.clone(), slash2.clone()]; + let list3 = vec![slash1.clone(), slash1.clone()]; + let list4 = vec![slash1.clone(), slash1, slash2]; + + let res = apply_list_slashes(¶ms, &[], token::Amount::from(100)); + assert_eq!(res, token::Amount::from(100)); + + let res = apply_list_slashes(¶ms, &list1, token::Amount::from(100)); + assert_eq!(res, token::Amount::zero()); + + let res = apply_list_slashes(¶ms, &list2, token::Amount::from(100)); + assert_eq!(res, token::Amount::zero()); + + let res = apply_list_slashes(¶ms, &list3, token::Amount::from(100)); + assert_eq!(res, token::Amount::zero()); + + let res = apply_list_slashes(¶ms, &list4, token::Amount::from(100)); + assert_eq!(res, token::Amount::zero()); +} + +/// `computeSlashableAmountTest` +#[test] +fn test_compute_slashable_amount() { + let init_epoch = Epoch(2); + let params = PosParams { + unbonding_len: 4, + ..Default::default() + }; + + let slash1 = Slash { + epoch: init_epoch + + params.unbonding_len + + params.cubic_slashing_window_length, + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }; + + let slash2 = Slash { + epoch: init_epoch + + params.unbonding_len + + params.cubic_slashing_window_length + + 1u64, + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }; + + let test_map = vec![(init_epoch, token::Amount::from(50))] + .into_iter() + .collect::>(); + + let res = compute_slashable_amount( + ¶ms, + &slash1, + token::Amount::from(100), + &BTreeMap::new(), + ); + assert_eq!(res, token::Amount::from(100)); + + let res = compute_slashable_amount( + ¶ms, + &slash2, + token::Amount::from(100), + &test_map, + ); + assert_eq!(res, token::Amount::from(50)); + + let res = compute_slashable_amount( + ¶ms, + &slash1, + token::Amount::from(100), + &test_map, + ); + assert_eq!(res, token::Amount::from(100)); +} + +/// `foldAndSlashRedelegatedBondsMapTest` +#[test] +fn test_fold_and_slash_redelegated_bonds() { + let mut storage = TestWlStorage::default(); + let params = PosParams { + unbonding_len: 4, + ..Default::default() + }; + let start_epoch = Epoch(7); + + let alice = established_address_1(); + let bob = established_address_2(); + + println!("\n\nAlice: {}", alice); + println!("Bob: {}\n", bob); + + let test_slash = Slash { + epoch: Default::default(), + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }; + + let test_data = vec![ + (alice.clone(), vec![(2, 1), (4, 1)]), + (bob, vec![(1, 1), (4, 2)]), + ]; + let mut eager_redel_bonds = EagerRedelegatedBondsMap::default(); + for (address, pair) in test_data { + for (epoch, amount) in pair { + eager_redel_bonds + .entry(address.clone()) + .or_default() + .insert(Epoch(epoch), token::Amount::from(amount)); + } + } + + // Test case 1 + let res = fold_and_slash_redelegated_bonds( + &storage, + ¶ms, + &eager_redel_bonds, + start_epoch, + &[], + |_| true, + ); + assert_eq!( + res, + FoldRedelegatedBondsResult { + total_redelegated: token::Amount::from(5), + total_after_slashing: token::Amount::from(5), + } + ); + + // Test case 2 + let res = fold_and_slash_redelegated_bonds( + &storage, + ¶ms, + &eager_redel_bonds, + start_epoch, + &[test_slash], + |_| true, + ); + assert_eq!( + res, + FoldRedelegatedBondsResult { + total_redelegated: token::Amount::from(5), + total_after_slashing: token::Amount::zero(), + } + ); + + // Test case 3 + let alice_slash = Slash { + epoch: Epoch(6), + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }; + validator_slashes_handle(&alice) + .push(&mut storage, alice_slash) + .unwrap(); + + let res = fold_and_slash_redelegated_bonds( + &storage, + ¶ms, + &eager_redel_bonds, + start_epoch, + &[], + |_| true, + ); + assert_eq!( + res, + FoldRedelegatedBondsResult { + total_redelegated: token::Amount::from(5), + total_after_slashing: token::Amount::from(3), + } + ); +} + +/// `slashRedelegationTest` +#[test] +fn test_slash_redelegation() { + let mut storage = TestWlStorage::default(); + let params = PosParams { + unbonding_len: 4, + ..Default::default() + }; + let alice = established_address_1(); + + let total_redelegated_unbonded = + validator_total_redelegated_unbonded_handle(&alice); + total_redelegated_unbonded + .at(&Epoch(13)) + .at(&Epoch(10)) + .at(&alice) + .insert(&mut storage, Epoch(7), token::Amount::from(2)) + .unwrap(); + + let slashes = validator_slashes_handle(&alice); + + let mut slashed_amounts_map = BTreeMap::from_iter([ + (Epoch(15), token::Amount::zero()), + (Epoch(16), token::Amount::zero()), + ]); + let empty_slash_amounts = slashed_amounts_map.clone(); + + // Test case 1 + slash_redelegation( + &storage, + ¶ms, + token::Amount::from(7), + Epoch(7), + Epoch(10), + &alice, + Epoch(14), + &slashes, + &total_redelegated_unbonded, + Dec::one(), + &mut slashed_amounts_map, + ) + .unwrap(); + assert_eq!( + slashed_amounts_map, + BTreeMap::from_iter([ + (Epoch(15), token::Amount::from(5)), + (Epoch(16), token::Amount::from(5)), + ]) + ); + + // Test case 2 + slashed_amounts_map = empty_slash_amounts.clone(); + slash_redelegation( + &storage, + ¶ms, + token::Amount::from(7), + Epoch(7), + Epoch(11), + &alice, + Epoch(14), + &slashes, + &total_redelegated_unbonded, + Dec::one(), + &mut slashed_amounts_map, + ) + .unwrap(); + assert_eq!( + slashed_amounts_map, + BTreeMap::from_iter([ + (Epoch(15), token::Amount::from(7)), + (Epoch(16), token::Amount::from(7)), + ]) + ); + + // Test case 3 + slashed_amounts_map = BTreeMap::from_iter([ + (Epoch(15), token::Amount::from(2)), + (Epoch(16), token::Amount::from(3)), + ]); + slash_redelegation( + &storage, + ¶ms, + token::Amount::from(7), + Epoch(7), + Epoch(10), + &alice, + Epoch(14), + &slashes, + &total_redelegated_unbonded, + Dec::one(), + &mut slashed_amounts_map, + ) + .unwrap(); + assert_eq!( + slashed_amounts_map, + BTreeMap::from_iter([ + (Epoch(15), token::Amount::from(7)), + (Epoch(16), token::Amount::from(8)), + ]) + ); + + // Test case 4 + slashes + .push( + &mut storage, + Slash { + epoch: Epoch(8), + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }, + ) + .unwrap(); + slashed_amounts_map = empty_slash_amounts.clone(); + slash_redelegation( + &storage, + ¶ms, + token::Amount::from(7), + Epoch(7), + Epoch(10), + &alice, + Epoch(14), + &slashes, + &total_redelegated_unbonded, + Dec::one(), + &mut slashed_amounts_map, + ) + .unwrap(); + assert_eq!(slashed_amounts_map, empty_slash_amounts); + + // Test case 5 + slashes.pop(&mut storage).unwrap(); + slashes + .push( + &mut storage, + Slash { + epoch: Epoch(9), + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }, + ) + .unwrap(); + slash_redelegation( + &storage, + ¶ms, + token::Amount::from(7), + Epoch(7), + Epoch(10), + &alice, + Epoch(14), + &slashes, + &total_redelegated_unbonded, + Dec::one(), + &mut slashed_amounts_map, + ) + .unwrap(); + assert_eq!(slashed_amounts_map, empty_slash_amounts); + + // Test case 6 + slashes + .push( + &mut storage, + Slash { + epoch: Epoch(8), + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }, + ) + .unwrap(); + slash_redelegation( + &storage, + ¶ms, + token::Amount::from(7), + Epoch(7), + Epoch(10), + &alice, + Epoch(14), + &slashes, + &total_redelegated_unbonded, + Dec::one(), + &mut slashed_amounts_map, + ) + .unwrap(); + assert_eq!(slashed_amounts_map, empty_slash_amounts); +} + +/// `slashValidatorRedelegationTest` +#[test] +fn test_slash_validator_redelegation() { + let mut storage = TestWlStorage::default(); + let params = PosParams { + unbonding_len: 4, + ..Default::default() + }; + let alice = established_address_1(); + let bob = established_address_2(); + + let total_redelegated_unbonded = + validator_total_redelegated_unbonded_handle(&alice); + total_redelegated_unbonded + .at(&Epoch(13)) + .at(&Epoch(10)) + .at(&alice) + .insert(&mut storage, Epoch(7), token::Amount::from(2)) + .unwrap(); + + let outgoing_redelegations = + validator_outgoing_redelegations_handle(&alice).at(&bob); + + let slashes = validator_slashes_handle(&alice); + + let mut slashed_amounts_map = BTreeMap::from_iter([ + (Epoch(15), token::Amount::zero()), + (Epoch(16), token::Amount::zero()), + ]); + let empty_slash_amounts = slashed_amounts_map.clone(); + + // Test case 1 + slash_validator_redelegation( + &storage, + ¶ms, + &alice, + Epoch(14), + &outgoing_redelegations, + &slashes, + &total_redelegated_unbonded, + Dec::one(), + &mut slashed_amounts_map, + ) + .unwrap(); + assert_eq!(slashed_amounts_map, empty_slash_amounts); + + // Test case 2 + total_redelegated_unbonded + .remove_all(&mut storage, &Epoch(13)) + .unwrap(); + slash_validator_redelegation( + &storage, + ¶ms, + &alice, + Epoch(14), + &outgoing_redelegations, + &slashes, + &total_redelegated_unbonded, + Dec::one(), + &mut slashed_amounts_map, + ) + .unwrap(); + assert_eq!(slashed_amounts_map, empty_slash_amounts); + + // Test case 3 + total_redelegated_unbonded + .at(&Epoch(13)) + .at(&Epoch(10)) + .at(&alice) + .insert(&mut storage, Epoch(7), token::Amount::from(2)) + .unwrap(); + outgoing_redelegations + .at(&Epoch(6)) + .insert(&mut storage, Epoch(8), token::Amount::from(7)) + .unwrap(); + slash_validator_redelegation( + &storage, + ¶ms, + &alice, + Epoch(14), + &outgoing_redelegations, + &slashes, + &total_redelegated_unbonded, + Dec::one(), + &mut slashed_amounts_map, + ) + .unwrap(); + assert_eq!( + slashed_amounts_map, + BTreeMap::from_iter([ + (Epoch(15), token::Amount::from(7)), + (Epoch(16), token::Amount::from(7)), + ]) + ); + + // Test case 4 + slashed_amounts_map = empty_slash_amounts.clone(); + outgoing_redelegations + .remove_all(&mut storage, &Epoch(6)) + .unwrap(); + outgoing_redelegations + .at(&Epoch(7)) + .insert(&mut storage, Epoch(8), token::Amount::from(7)) + .unwrap(); + slash_validator_redelegation( + &storage, + ¶ms, + &alice, + Epoch(14), + &outgoing_redelegations, + &slashes, + &total_redelegated_unbonded, + Dec::one(), + &mut slashed_amounts_map, + ) + .unwrap(); + assert_eq!( + slashed_amounts_map, + BTreeMap::from_iter([ + (Epoch(15), token::Amount::from(5)), + (Epoch(16), token::Amount::from(5)), + ]) + ); + + // Test case 5 + slashed_amounts_map = BTreeMap::from_iter([ + (Epoch(15), token::Amount::from(2)), + (Epoch(16), token::Amount::from(3)), + ]); + slash_validator_redelegation( + &storage, + ¶ms, + &alice, + Epoch(14), + &outgoing_redelegations, + &slashes, + &total_redelegated_unbonded, + Dec::one(), + &mut slashed_amounts_map, + ) + .unwrap(); + assert_eq!( + slashed_amounts_map, + BTreeMap::from_iter([ + (Epoch(15), token::Amount::from(7)), + (Epoch(16), token::Amount::from(8)), + ]) + ); + + // Test case 6 + slashed_amounts_map = empty_slash_amounts.clone(); + slashes + .push( + &mut storage, + Slash { + epoch: Epoch(8), + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }, + ) + .unwrap(); + slash_validator_redelegation( + &storage, + ¶ms, + &alice, + Epoch(14), + &outgoing_redelegations, + &slashes, + &total_redelegated_unbonded, + Dec::one(), + &mut slashed_amounts_map, + ) + .unwrap(); + assert_eq!(slashed_amounts_map, empty_slash_amounts); +} + +/// `slashValidatorTest` +#[test] +fn test_slash_validator() { + let mut storage = TestWlStorage::default(); + let params = PosParams { + unbonding_len: 4, + ..Default::default() + }; + let alice = established_address_1(); + let bob = established_address_2(); + + let total_bonded = total_bonded_handle(&bob); + let total_unbonded = total_unbonded_handle(&bob); + let total_redelegated_bonded = + validator_total_redelegated_bonded_handle(&bob); + let total_redelegated_unbonded = + validator_total_redelegated_unbonded_handle(&bob); + + let infraction_stake = token::Amount::from(23); + + let initial_stakes = BTreeMap::from_iter([ + (Epoch(11), infraction_stake), + (Epoch(12), infraction_stake), + (Epoch(13), infraction_stake), + ]); + let mut exp_res = initial_stakes.clone(); + + let current_epoch = Epoch(10); + let infraction_epoch = + current_epoch - params.slash_processing_epoch_offset(); + let processing_epoch = current_epoch.next(); + let slash_rate = Dec::one(); + + // Test case 1 + println!("\nTEST 1:"); + + total_bonded + .set(&mut storage, 23.into(), infraction_epoch - 2, 0) + .unwrap(); + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + assert_eq!(res, exp_res); + + // Test case 2 + println!("\nTEST 2:"); + total_bonded + .set(&mut storage, 17.into(), infraction_epoch - 2, 0) + .unwrap(); + total_unbonded + .at(&(current_epoch + params.pipeline_len)) + .insert(&mut storage, infraction_epoch - 2, 6.into()) + .unwrap(); + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + exp_res.insert(Epoch(12), 17.into()); + exp_res.insert(Epoch(13), 17.into()); + assert_eq!(res, exp_res); + + // Test case 3 + println!("\nTEST 3:"); + total_redelegated_bonded + .at(&infraction_epoch.prev()) + .at(&alice) + .insert(&mut storage, Epoch(2), 5.into()) + .unwrap(); + total_redelegated_bonded + .at(&infraction_epoch.prev()) + .at(&alice) + .insert(&mut storage, Epoch(3), 1.into()) + .unwrap(); + + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + assert_eq!(res, exp_res); + + // Test case 4 + println!("\nTEST 4:"); + total_unbonded_handle(&bob) + .at(&(current_epoch + params.pipeline_len)) + .remove(&mut storage, &(infraction_epoch - 2)) + .unwrap(); + total_unbonded_handle(&bob) + .at(&(current_epoch + params.pipeline_len)) + .insert(&mut storage, infraction_epoch - 1, 6.into()) + .unwrap(); + total_redelegated_unbonded + .at(&(current_epoch + params.pipeline_len)) + .at(&infraction_epoch.prev()) + .at(&alice) + .insert(&mut storage, Epoch(2), 5.into()) + .unwrap(); + total_redelegated_unbonded + .at(&(current_epoch + params.pipeline_len)) + .at(&infraction_epoch.prev()) + .at(&alice) + .insert(&mut storage, Epoch(3), 1.into()) + .unwrap(); + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + assert_eq!(res, exp_res); + + // Test case 5 + println!("\nTEST 5:"); + total_bonded_handle(&bob) + .set(&mut storage, 19.into(), infraction_epoch - 2, 0) + .unwrap(); + total_unbonded_handle(&bob) + .at(&(current_epoch + params.pipeline_len)) + .insert(&mut storage, infraction_epoch - 1, 4.into()) + .unwrap(); + total_redelegated_bonded + .at(¤t_epoch) + .at(&alice) + .insert(&mut storage, Epoch(2), token::Amount::from(1)) + .unwrap(); + total_redelegated_unbonded + .at(&(current_epoch + params.pipeline_len)) + .at(&infraction_epoch.prev()) + .at(&alice) + .remove(&mut storage, &Epoch(3)) + .unwrap(); + total_redelegated_unbonded + .at(&(current_epoch + params.pipeline_len)) + .at(&infraction_epoch.prev()) + .at(&alice) + .insert(&mut storage, Epoch(2), 4.into()) + .unwrap(); + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + exp_res.insert(Epoch(12), 19.into()); + exp_res.insert(Epoch(13), 19.into()); + assert_eq!(res, exp_res); + + // Test case 6 + println!("\nTEST 6:"); + total_unbonded_handle(&bob) + .remove_all(&mut storage, &(current_epoch + params.pipeline_len)) + .unwrap(); + total_redelegated_unbonded + .remove_all(&mut storage, &(current_epoch + params.pipeline_len)) + .unwrap(); + total_redelegated_bonded + .remove_all(&mut storage, ¤t_epoch) + .unwrap(); + total_bonded_handle(&bob) + .set(&mut storage, 23.into(), infraction_epoch - 2, 0) + .unwrap(); + total_bonded_handle(&bob) + .set(&mut storage, 6.into(), current_epoch, 0) + .unwrap(); + + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + exp_res = initial_stakes; + assert_eq!(res, exp_res); + + // Test case 7 + println!("\nTEST 7:"); + total_bonded + .get_data_handler() + .remove(&mut storage, ¤t_epoch) + .unwrap(); + total_unbonded + .at(¤t_epoch.next()) + .insert(&mut storage, current_epoch, 6.into()) + .unwrap(); + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + assert_eq!(res, exp_res); + + // Test case 8 + println!("\nTEST 8:"); + total_bonded + .get_data_handler() + .insert(&mut storage, current_epoch, 3.into()) + .unwrap(); + total_unbonded + .at(¤t_epoch.next()) + .insert(&mut storage, current_epoch, 3.into()) + .unwrap(); + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + assert_eq!(res, exp_res); + + // Test case 9 + println!("\nTEST 9:"); + total_unbonded + .remove_all(&mut storage, ¤t_epoch.next()) + .unwrap(); + total_bonded + .set(&mut storage, 6.into(), current_epoch, 0) + .unwrap(); + total_redelegated_bonded + .at(¤t_epoch) + .at(&alice) + .insert(&mut storage, 2.into(), 5.into()) + .unwrap(); + total_redelegated_bonded + .at(¤t_epoch) + .at(&alice) + .insert(&mut storage, 3.into(), 1.into()) + .unwrap(); + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + assert_eq!(res, exp_res); + + // Test case 10 + println!("\nTEST 10:"); + total_redelegated_bonded + .remove_all(&mut storage, ¤t_epoch) + .unwrap(); + total_bonded + .get_data_handler() + .remove(&mut storage, ¤t_epoch) + .unwrap(); + total_redelegated_unbonded + .at(¤t_epoch.next()) + .at(¤t_epoch) + .at(&alice) + .insert(&mut storage, 2.into(), 5.into()) + .unwrap(); + total_redelegated_unbonded + .at(¤t_epoch.next()) + .at(¤t_epoch) + .at(&alice) + .insert(&mut storage, 3.into(), 1.into()) + .unwrap(); + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + assert_eq!(res, exp_res); + + // Test case 11 + println!("\nTEST 11:"); + total_bonded + .set(&mut storage, 2.into(), current_epoch, 0) + .unwrap(); + total_redelegated_unbonded + .at(¤t_epoch.next()) + .at(¤t_epoch) + .at(&alice) + .insert(&mut storage, 2.into(), 4.into()) + .unwrap(); + total_redelegated_unbonded + .at(¤t_epoch.next()) + .at(¤t_epoch) + .at(&alice) + .remove(&mut storage, &3.into()) + .unwrap(); + total_redelegated_bonded + .at(¤t_epoch) + .at(&alice) + .insert(&mut storage, 2.into(), 1.into()) + .unwrap(); + total_redelegated_bonded + .at(¤t_epoch) + .at(&alice) + .insert(&mut storage, 3.into(), 1.into()) + .unwrap(); + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + assert_eq!(res, exp_res); + + // Test case 12 + println!("\nTEST 12:"); + total_bonded + .set(&mut storage, 6.into(), current_epoch, 0) + .unwrap(); + total_bonded + .set(&mut storage, 2.into(), current_epoch.next(), 0) + .unwrap(); + total_redelegated_bonded + .remove_all(&mut storage, ¤t_epoch) + .unwrap(); + total_redelegated_bonded + .at(¤t_epoch.next()) + .at(&alice) + .insert(&mut storage, 2.into(), 1.into()) + .unwrap(); + total_redelegated_bonded + .at(¤t_epoch.next()) + .at(&alice) + .insert(&mut storage, 3.into(), 1.into()) + .unwrap(); + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + assert_eq!(res, exp_res); + + // Test case 13 + println!("\nTEST 13:"); + validator_slashes_handle(&bob) + .push( + &mut storage, + Slash { + epoch: infraction_epoch.prev(), + block_height: 0, + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }, + ) + .unwrap(); + total_redelegated_unbonded + .remove_all(&mut storage, ¤t_epoch.next()) + .unwrap(); + total_bonded + .get_data_handler() + .remove(&mut storage, ¤t_epoch.next()) + .unwrap(); + total_redelegated_bonded + .remove_all(&mut storage, ¤t_epoch.next()) + .unwrap(); + let res = slash_validator( + &storage, + ¶ms, + &bob, + slash_rate, + processing_epoch, + &Default::default(), + ) + .unwrap(); + exp_res.insert(Epoch(11), 0.into()); + exp_res.insert(Epoch(12), 0.into()); + exp_res.insert(Epoch(13), 0.into()); + assert_eq!(res, exp_res); +} + +/// `computeAmountAfterSlashingUnbondTest` +#[test] +fn compute_amount_after_slashing_unbond_test() { + let mut storage = TestWlStorage::default(); + let params = PosParams { + unbonding_len: 4, + ..Default::default() + }; + + // Test data + let alice = established_address_1(); + let bob = established_address_2(); + let unbonds: BTreeMap = BTreeMap::from_iter([ + ((Epoch(2)), token::Amount::from(5)), + ((Epoch(4)), token::Amount::from(6)), + ]); + let redelegated_unbonds: EagerRedelegatedUnbonds = BTreeMap::from_iter([( + Epoch(2), + BTreeMap::from_iter([( + alice.clone(), + BTreeMap::from_iter([(Epoch(1), token::Amount::from(1))]), + )]), + )]); + + // Test case 1 + let slashes = vec![]; + let result = compute_amount_after_slashing_unbond( + &storage, + ¶ms, + &unbonds, + &redelegated_unbonds, + slashes, + ) + .unwrap(); + assert_eq!(result.sum, 11.into()); + itertools::assert_equal( + result.epoch_map, + [(2.into(), 5.into()), (4.into(), 6.into())], + ); + + // Test case 2 + let bob_slash = Slash { + epoch: Epoch(5), + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }; + let slashes = vec![bob_slash.clone()]; + validator_slashes_handle(&bob) + .push(&mut storage, bob_slash) + .unwrap(); + let result = compute_amount_after_slashing_unbond( + &storage, + ¶ms, + &unbonds, + &redelegated_unbonds, + slashes, + ) + .unwrap(); + assert_eq!(result.sum, 0.into()); + itertools::assert_equal( + result.epoch_map, + [(2.into(), 0.into()), (4.into(), 0.into())], + ); + + // Test case 3 + let alice_slash = Slash { + epoch: Epoch(0), + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }; + let slashes = vec![alice_slash.clone()]; + validator_slashes_handle(&alice) + .push(&mut storage, alice_slash) + .unwrap(); + validator_slashes_handle(&bob).pop(&mut storage).unwrap(); + let result = compute_amount_after_slashing_unbond( + &storage, + ¶ms, + &unbonds, + &redelegated_unbonds, + slashes, + ) + .unwrap(); + assert_eq!(result.sum, 11.into()); + itertools::assert_equal( + result.epoch_map, + [(2.into(), 5.into()), (4.into(), 6.into())], + ); + + // Test case 4 + let alice_slash = Slash { + epoch: Epoch(1), + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }; + let slashes = vec![alice_slash.clone()]; + validator_slashes_handle(&alice).pop(&mut storage).unwrap(); + validator_slashes_handle(&alice) + .push(&mut storage, alice_slash) + .unwrap(); + let result = compute_amount_after_slashing_unbond( + &storage, + ¶ms, + &unbonds, + &redelegated_unbonds, + slashes, + ) + .unwrap(); + assert_eq!(result.sum, 10.into()); + itertools::assert_equal( + result.epoch_map, + [(2.into(), 4.into()), (4.into(), 6.into())], + ); +} + +/// `computeAmountAfterSlashingWithdrawTest` +#[test] +fn compute_amount_after_slashing_withdraw_test() { + let mut storage = TestWlStorage::default(); + let params = PosParams { + unbonding_len: 4, + ..Default::default() + }; + + // Test data + let alice = established_address_1(); + let bob = established_address_2(); + let unbonds_and_redelegated_unbonds: BTreeMap< + (Epoch, Epoch), + (token::Amount, EagerRedelegatedBondsMap), + > = BTreeMap::from_iter([ + ( + (Epoch(2), Epoch(20)), + ( + // unbond + token::Amount::from(5), + // redelegations + BTreeMap::from_iter([( + alice.clone(), + BTreeMap::from_iter([(Epoch(1), token::Amount::from(1))]), + )]), + ), + ), + ( + (Epoch(4), Epoch(20)), + ( + // unbond + token::Amount::from(6), + // redelegations + BTreeMap::default(), + ), + ), + ]); + + // Test case 1 + let slashes = vec![]; + let result = compute_amount_after_slashing_withdraw( + &storage, + ¶ms, + &unbonds_and_redelegated_unbonds, + slashes, + ) + .unwrap(); + assert_eq!(result.sum, 11.into()); + itertools::assert_equal( + result.epoch_map, + [(2.into(), 5.into()), (4.into(), 6.into())], + ); + + // Test case 2 + let bob_slash = Slash { + epoch: Epoch(5), + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }; + let slashes = vec![bob_slash.clone()]; + validator_slashes_handle(&bob) + .push(&mut storage, bob_slash) + .unwrap(); + let result = compute_amount_after_slashing_withdraw( + &storage, + ¶ms, + &unbonds_and_redelegated_unbonds, + slashes, + ) + .unwrap(); + assert_eq!(result.sum, 0.into()); + itertools::assert_equal( + result.epoch_map, + [(2.into(), 0.into()), (4.into(), 0.into())], + ); + + // Test case 3 + let alice_slash = Slash { + epoch: Epoch(0), + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }; + let slashes = vec![alice_slash.clone()]; + validator_slashes_handle(&alice) + .push(&mut storage, alice_slash) + .unwrap(); + validator_slashes_handle(&bob).pop(&mut storage).unwrap(); + let result = compute_amount_after_slashing_withdraw( + &storage, + ¶ms, + &unbonds_and_redelegated_unbonds, + slashes, + ) + .unwrap(); + assert_eq!(result.sum, 11.into()); + itertools::assert_equal( + result.epoch_map, + [(2.into(), 5.into()), (4.into(), 6.into())], + ); + + // Test case 4 + let alice_slash = Slash { + epoch: Epoch(1), + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate: Dec::one(), + }; + let slashes = vec![alice_slash.clone()]; + validator_slashes_handle(&alice).pop(&mut storage).unwrap(); + validator_slashes_handle(&alice) + .push(&mut storage, alice_slash) + .unwrap(); + let result = compute_amount_after_slashing_withdraw( + &storage, + ¶ms, + &unbonds_and_redelegated_unbonds, + slashes, + ) + .unwrap(); + assert_eq!(result.sum, 10.into()); + itertools::assert_equal( + result.epoch_map, + [(2.into(), 4.into()), (4.into(), 6.into())], + ); +} + +fn arb_redelegation_amounts( + max_delegation: u64, +) -> impl Strategy { + let arb_delegation = arb_amount_non_zero_ceiled(max_delegation); + let amounts = arb_delegation.prop_flat_map(move |amount_delegate| { + let amount_redelegate = arb_amount_non_zero_ceiled(max( + 1, + u64::try_from(amount_delegate.raw_amount()).unwrap() - 1, + )); + (Just(amount_delegate), amount_redelegate) + }); + amounts.prop_flat_map(move |(amount_delegate, amount_redelegate)| { + let amount_unbond = arb_amount_non_zero_ceiled(max( + 1, + u64::try_from(amount_redelegate.raw_amount()).unwrap() - 1, + )); + ( + Just(amount_delegate), + Just(amount_redelegate), + amount_unbond, + ) + }) +} + +fn test_simple_redelegation_aux( + mut validators: Vec, + amount_delegate: token::Amount, + amount_redelegate: token::Amount, + amount_unbond: token::Amount, +) { + validators.sort_by(|a, b| b.tokens.cmp(&a.tokens)); + + let src_validator = validators[0].address.clone(); + let dest_validator = validators[1].address.clone(); + + let mut storage = TestWlStorage::default(); + let params = PosParams { + unbonding_len: 4, + ..Default::default() + }; + + // Genesis + let mut current_epoch = storage.storage.block.epoch; + init_genesis( + &mut storage, + ¶ms, + validators.clone().into_iter(), + current_epoch, + ) + .unwrap(); + storage.commit_block().unwrap(); + + // Get a delegator with some tokens + let staking_token = staking_token_address(&storage); + let delegator = address::testing::gen_implicit_address(); + let del_balance = token::Amount::from_uint(1_000_000, 0).unwrap(); + credit_tokens(&mut storage, &staking_token, &delegator, del_balance) + .unwrap(); + + // Ensure that we cannot redelegate with the same src and dest validator + let err = super::redelegate_tokens( + &mut storage, + &delegator, + &src_validator, + &src_validator, + current_epoch, + amount_redelegate, + ) + .unwrap_err(); + let err_str = err.to_string(); + assert_matches!( + err.downcast::().unwrap().deref(), + RedelegationError::RedelegationSrcEqDest, + "Redelegation with the same src and dest validator must be rejected, \ + got {err_str}", + ); + + for _ in 0..5 { + current_epoch = advance_epoch(&mut storage, ¶ms); + super::process_slashes(&mut storage, current_epoch).unwrap(); + } + + let init_epoch = current_epoch; + + // Delegate in epoch 1 to src_validator + println!( + "\nBONDING {} TOKENS TO {}\n", + amount_delegate.to_string_native(), + &src_validator + ); + super::bond_tokens( + &mut storage, + Some(&delegator), + &src_validator, + amount_delegate, + current_epoch, + ) + .unwrap(); + + println!("\nAFTER DELEGATION\n"); + let bonds = bond_handle(&delegator, &src_validator) + .get_data_handler() + .collect_map(&storage) + .unwrap(); + let bonds_dest = bond_handle(&delegator, &dest_validator) + .get_data_handler() + .collect_map(&storage) + .unwrap(); + let unbonds = unbond_handle(&delegator, &src_validator) + .collect_map(&storage) + .unwrap(); + let tot_bonds = total_bonded_handle(&src_validator) + .get_data_handler() + .collect_map(&storage) + .unwrap(); + let tot_unbonds = total_unbonded_handle(&src_validator) + .collect_map(&storage) + .unwrap(); + dbg!(&bonds, &bonds_dest, &unbonds, &tot_bonds, &tot_unbonds); + + // Advance three epochs + current_epoch = advance_epoch(&mut storage, ¶ms); + super::process_slashes(&mut storage, current_epoch).unwrap(); + current_epoch = advance_epoch(&mut storage, ¶ms); + super::process_slashes(&mut storage, current_epoch).unwrap(); + current_epoch = advance_epoch(&mut storage, ¶ms); + super::process_slashes(&mut storage, current_epoch).unwrap(); + + // Redelegate in epoch 3 + println!( + "\nREDELEGATING {} TOKENS TO {}\n", + amount_redelegate.to_string_native(), + &dest_validator + ); + + super::redelegate_tokens( + &mut storage, + &delegator, + &src_validator, + &dest_validator, + current_epoch, + amount_redelegate, + ) + .unwrap(); + + println!("\nAFTER REDELEGATION\n"); + println!("\nDELEGATOR\n"); + let bonds_src = bond_handle(&delegator, &src_validator) + .get_data_handler() + .collect_map(&storage) + .unwrap(); + let bonds_dest = bond_handle(&delegator, &dest_validator) + .get_data_handler() + .collect_map(&storage) + .unwrap(); + let unbonds_src = unbond_handle(&delegator, &src_validator) + .collect_map(&storage) + .unwrap(); + let unbonds_dest = unbond_handle(&delegator, &dest_validator) + .collect_map(&storage) + .unwrap(); + let redel_bonds = delegator_redelegated_bonds_handle(&delegator) + .collect_map(&storage) + .unwrap(); + let redel_unbonds = delegator_redelegated_unbonds_handle(&delegator) + .collect_map(&storage) + .unwrap(); + + dbg!( + &bonds_src, + &bonds_dest, + &unbonds_src, + &unbonds_dest, + &redel_bonds, + &redel_unbonds + ); + + // Dest val + println!("\nDEST VALIDATOR\n"); + + let incoming_redels_dest = + validator_incoming_redelegations_handle(&dest_validator) + .collect_map(&storage) + .unwrap(); + let outgoing_redels_dest = + validator_outgoing_redelegations_handle(&dest_validator) + .collect_map(&storage) + .unwrap(); + let tot_bonds_dest = total_bonded_handle(&dest_validator) + .get_data_handler() + .collect_map(&storage) + .unwrap(); + let tot_unbonds_dest = total_unbonded_handle(&dest_validator) + .collect_map(&storage) + .unwrap(); + let tot_redel_bonds_dest = + validator_total_redelegated_bonded_handle(&dest_validator) + .collect_map(&storage) + .unwrap(); + let tot_redel_unbonds_dest = + validator_total_redelegated_unbonded_handle(&dest_validator) + .collect_map(&storage) + .unwrap(); + dbg!( + &incoming_redels_dest, + &outgoing_redels_dest, + &tot_bonds_dest, + &tot_unbonds_dest, + &tot_redel_bonds_dest, + &tot_redel_unbonds_dest + ); + + // Src val + println!("\nSRC VALIDATOR\n"); + + let incoming_redels_src = + validator_incoming_redelegations_handle(&src_validator) + .collect_map(&storage) + .unwrap(); + let outgoing_redels_src = + validator_outgoing_redelegations_handle(&src_validator) + .collect_map(&storage) + .unwrap(); + let tot_bonds_src = total_bonded_handle(&src_validator) + .get_data_handler() + .collect_map(&storage) + .unwrap(); + let tot_unbonds_src = total_unbonded_handle(&src_validator) + .collect_map(&storage) + .unwrap(); + let tot_redel_bonds_src = + validator_total_redelegated_bonded_handle(&src_validator) + .collect_map(&storage) + .unwrap(); + let tot_redel_unbonds_src = + validator_total_redelegated_unbonded_handle(&src_validator) + .collect_map(&storage) + .unwrap(); + dbg!( + &incoming_redels_src, + &outgoing_redels_src, + &tot_bonds_src, + &tot_unbonds_src, + &tot_redel_bonds_src, + &tot_redel_unbonds_src + ); + + // Checks + let redelegated = delegator_redelegated_bonds_handle(&delegator) + .at(&dest_validator) + .at(&(current_epoch + params.pipeline_len)) + .at(&src_validator) + .get(&storage, &(init_epoch + params.pipeline_len)) + .unwrap() + .unwrap(); + assert_eq!(redelegated, amount_redelegate); + + let redel_start_epoch = + validator_incoming_redelegations_handle(&dest_validator) + .get(&storage, &delegator) + .unwrap() + .unwrap(); + assert_eq!(redel_start_epoch, current_epoch + params.pipeline_len); + + let redelegated = validator_outgoing_redelegations_handle(&src_validator) + .at(&dest_validator) + .at(¤t_epoch.prev()) + .get(&storage, ¤t_epoch) + .unwrap() + .unwrap(); + assert_eq!(redelegated, amount_redelegate); + + // Advance three epochs + current_epoch = advance_epoch(&mut storage, ¶ms); + super::process_slashes(&mut storage, current_epoch).unwrap(); + current_epoch = advance_epoch(&mut storage, ¶ms); + super::process_slashes(&mut storage, current_epoch).unwrap(); + current_epoch = advance_epoch(&mut storage, ¶ms); + super::process_slashes(&mut storage, current_epoch).unwrap(); + + // Unbond in epoch 5 from dest_validator + println!( + "\nUNBONDING {} TOKENS FROM {}\n", + amount_unbond.to_string_native(), + &dest_validator + ); + let _ = unbond_tokens( + &mut storage, + Some(&delegator), + &dest_validator, + amount_unbond, + current_epoch, + false, + ) + .unwrap(); + + println!("\nAFTER UNBONDING\n"); + println!("\nDELEGATOR\n"); + + let bonds_src = bond_handle(&delegator, &src_validator) + .get_data_handler() + .collect_map(&storage) + .unwrap(); + let bonds_dest = bond_handle(&delegator, &dest_validator) + .get_data_handler() + .collect_map(&storage) + .unwrap(); + let unbonds_src = unbond_handle(&delegator, &src_validator) + .collect_map(&storage) + .unwrap(); + let unbonds_dest = unbond_handle(&delegator, &dest_validator) + .collect_map(&storage) + .unwrap(); + let redel_bonds = delegator_redelegated_bonds_handle(&delegator) + .collect_map(&storage) + .unwrap(); + let redel_unbonds = delegator_redelegated_unbonds_handle(&delegator) + .collect_map(&storage) + .unwrap(); + + dbg!( + &bonds_src, + &bonds_dest, + &unbonds_src, + &unbonds_dest, + &redel_bonds, + &redel_unbonds + ); + + println!("\nDEST VALIDATOR\n"); + + let incoming_redels_dest = + validator_incoming_redelegations_handle(&dest_validator) + .collect_map(&storage) + .unwrap(); + let outgoing_redels_dest = + validator_outgoing_redelegations_handle(&dest_validator) + .collect_map(&storage) + .unwrap(); + let tot_bonds_dest = total_bonded_handle(&dest_validator) + .get_data_handler() + .collect_map(&storage) + .unwrap(); + let tot_unbonds_dest = total_unbonded_handle(&dest_validator) + .collect_map(&storage) + .unwrap(); + let tot_redel_bonds_dest = + validator_total_redelegated_bonded_handle(&dest_validator) + .collect_map(&storage) + .unwrap(); + let tot_redel_unbonds_dest = + validator_total_redelegated_unbonded_handle(&dest_validator) + .collect_map(&storage) + .unwrap(); + dbg!( + &incoming_redels_dest, + &outgoing_redels_dest, + &tot_bonds_dest, + &tot_unbonds_dest, + &tot_redel_bonds_dest, + &tot_redel_unbonds_dest + ); + + let bond_start = init_epoch + params.pipeline_len; + let redelegation_end = bond_start + params.pipeline_len + 1u64; + let unbond_end = + redelegation_end + params.withdrawable_epoch_offset() + 1u64; + let unbond_materialized = redelegation_end + params.pipeline_len + 1u64; + + // Checks + let redelegated_remaining = delegator_redelegated_bonds_handle(&delegator) + .at(&dest_validator) + .at(&redelegation_end) + .at(&src_validator) + .get(&storage, &bond_start) + .unwrap() + .unwrap_or_default(); + assert_eq!(redelegated_remaining, amount_redelegate - amount_unbond); + + let redel_unbonded = delegator_redelegated_unbonds_handle(&delegator) + .at(&dest_validator) + .at(&redelegation_end) + .at(&unbond_end) + .at(&src_validator) + .get(&storage, &bond_start) + .unwrap() + .unwrap(); + assert_eq!(redel_unbonded, amount_unbond); + + dbg!(unbond_materialized, redelegation_end, bond_start); + let total_redel_unbonded = + validator_total_redelegated_unbonded_handle(&dest_validator) + .at(&unbond_materialized) + .at(&redelegation_end) + .at(&src_validator) + .get(&storage, &bond_start) + .unwrap() + .unwrap(); + assert_eq!(total_redel_unbonded, amount_unbond); + + // Advance to withdrawal epoch + loop { + current_epoch = advance_epoch(&mut storage, ¶ms); + super::process_slashes(&mut storage, current_epoch).unwrap(); + if current_epoch == unbond_end { + break; + } + } + + // Withdraw + withdraw_tokens( + &mut storage, + Some(&delegator), + &dest_validator, + current_epoch, + ) + .unwrap(); + + assert!( + delegator_redelegated_unbonds_handle(&delegator) + .at(&dest_validator) + .is_empty(&storage) + .unwrap() + ); + + let delegator_balance = storage + .read::(&token::balance_key(&staking_token, &delegator)) + .unwrap() + .unwrap_or_default(); + assert_eq!( + delegator_balance, + del_balance - amount_delegate + amount_unbond + ); +} + +fn test_redelegation_with_slashing_aux( + mut validators: Vec, + amount_delegate: token::Amount, + amount_redelegate: token::Amount, + amount_unbond: token::Amount, +) { + validators.sort_by(|a, b| b.tokens.cmp(&a.tokens)); + + let src_validator = validators[0].address.clone(); + let dest_validator = validators[1].address.clone(); + + let mut storage = TestWlStorage::default(); + let params = PosParams { + unbonding_len: 4, + // Avoid empty consensus set by removing the threshold + validator_stake_threshold: token::Amount::zero(), + ..Default::default() + }; + + // Genesis + let mut current_epoch = storage.storage.block.epoch; + init_genesis( + &mut storage, + ¶ms, + validators.clone().into_iter(), + current_epoch, + ) + .unwrap(); + storage.commit_block().unwrap(); + + // Get a delegator with some tokens + let staking_token = staking_token_address(&storage); + let delegator = address::testing::gen_implicit_address(); + let del_balance = token::Amount::from_uint(1_000_000, 0).unwrap(); + credit_tokens(&mut storage, &staking_token, &delegator, del_balance) + .unwrap(); + + for _ in 0..5 { + current_epoch = advance_epoch(&mut storage, ¶ms); + super::process_slashes(&mut storage, current_epoch).unwrap(); + } + + let init_epoch = current_epoch; + + // Delegate in epoch 5 to src_validator + println!( + "\nBONDING {} TOKENS TO {}\n", + amount_delegate.to_string_native(), + &src_validator + ); + super::bond_tokens( + &mut storage, + Some(&delegator), + &src_validator, + amount_delegate, + current_epoch, + ) + .unwrap(); + + println!("\nAFTER DELEGATION\n"); + let bonds = bond_handle(&delegator, &src_validator) + .get_data_handler() + .collect_map(&storage) + .unwrap(); + let bonds_dest = bond_handle(&delegator, &dest_validator) + .get_data_handler() + .collect_map(&storage) + .unwrap(); + let unbonds = unbond_handle(&delegator, &src_validator) + .collect_map(&storage) + .unwrap(); + let tot_bonds = total_bonded_handle(&src_validator) + .get_data_handler() + .collect_map(&storage) + .unwrap(); + let tot_unbonds = total_unbonded_handle(&src_validator) + .collect_map(&storage) + .unwrap(); + dbg!(&bonds, &bonds_dest, &unbonds, &tot_bonds, &tot_unbonds); + + // Advance three epochs + current_epoch = advance_epoch(&mut storage, ¶ms); + super::process_slashes(&mut storage, current_epoch).unwrap(); + current_epoch = advance_epoch(&mut storage, ¶ms); + super::process_slashes(&mut storage, current_epoch).unwrap(); + current_epoch = advance_epoch(&mut storage, ¶ms); + super::process_slashes(&mut storage, current_epoch).unwrap(); + + // Redelegate in epoch 8 + println!( + "\nREDELEGATING {} TOKENS TO {}\n", + amount_redelegate.to_string_native(), + &dest_validator + ); + + super::redelegate_tokens( + &mut storage, + &delegator, + &src_validator, + &dest_validator, + current_epoch, + amount_redelegate, + ) + .unwrap(); + + println!("\nAFTER REDELEGATION\n"); + println!("\nDELEGATOR\n"); + let bonds_src = bond_handle(&delegator, &src_validator) + .get_data_handler() + .collect_map(&storage) + .unwrap(); + let bonds_dest = bond_handle(&delegator, &dest_validator) + .get_data_handler() + .collect_map(&storage) + .unwrap(); + let unbonds_src = unbond_handle(&delegator, &src_validator) + .collect_map(&storage) + .unwrap(); + let unbonds_dest = unbond_handle(&delegator, &dest_validator) + .collect_map(&storage) + .unwrap(); + let redel_bonds = delegator_redelegated_bonds_handle(&delegator) + .collect_map(&storage) + .unwrap(); + let redel_unbonds = delegator_redelegated_unbonds_handle(&delegator) + .collect_map(&storage) + .unwrap(); + + dbg!( + &bonds_src, + &bonds_dest, + &unbonds_src, + &unbonds_dest, + &redel_bonds, + &redel_unbonds + ); + + // Dest val + println!("\nDEST VALIDATOR\n"); + + let incoming_redels_dest = + validator_incoming_redelegations_handle(&dest_validator) + .collect_map(&storage) + .unwrap(); + let outgoing_redels_dest = + validator_outgoing_redelegations_handle(&dest_validator) + .collect_map(&storage) + .unwrap(); + let tot_bonds_dest = total_bonded_handle(&dest_validator) + .get_data_handler() + .collect_map(&storage) + .unwrap(); + let tot_unbonds_dest = total_unbonded_handle(&dest_validator) + .collect_map(&storage) + .unwrap(); + let tot_redel_bonds_dest = + validator_total_redelegated_bonded_handle(&dest_validator) + .collect_map(&storage) + .unwrap(); + let tot_redel_unbonds_dest = + validator_total_redelegated_unbonded_handle(&dest_validator) + .collect_map(&storage) + .unwrap(); + dbg!( + &incoming_redels_dest, + &outgoing_redels_dest, + &tot_bonds_dest, + &tot_unbonds_dest, + &tot_redel_bonds_dest, + &tot_redel_unbonds_dest + ); + + // Src val + println!("\nSRC VALIDATOR\n"); + + let incoming_redels_src = + validator_incoming_redelegations_handle(&src_validator) + .collect_map(&storage) + .unwrap(); + let outgoing_redels_src = + validator_outgoing_redelegations_handle(&src_validator) + .collect_map(&storage) + .unwrap(); + let tot_bonds_src = total_bonded_handle(&src_validator) + .get_data_handler() + .collect_map(&storage) + .unwrap(); + let tot_unbonds_src = total_unbonded_handle(&src_validator) + .collect_map(&storage) + .unwrap(); + let tot_redel_bonds_src = + validator_total_redelegated_bonded_handle(&src_validator) + .collect_map(&storage) + .unwrap(); + let tot_redel_unbonds_src = + validator_total_redelegated_unbonded_handle(&src_validator) + .collect_map(&storage) + .unwrap(); + dbg!( + &incoming_redels_src, + &outgoing_redels_src, + &tot_bonds_src, + &tot_unbonds_src, + &tot_redel_bonds_src, + &tot_redel_unbonds_src + ); + + // Checks + let redelegated = delegator_redelegated_bonds_handle(&delegator) + .at(&dest_validator) + .at(&(current_epoch + params.pipeline_len)) + .at(&src_validator) + .get(&storage, &(init_epoch + params.pipeline_len)) + .unwrap() + .unwrap(); + assert_eq!(redelegated, amount_redelegate); + + let redel_start_epoch = + validator_incoming_redelegations_handle(&dest_validator) + .get(&storage, &delegator) + .unwrap() + .unwrap(); + assert_eq!(redel_start_epoch, current_epoch + params.pipeline_len); + + let redelegated = validator_outgoing_redelegations_handle(&src_validator) + .at(&dest_validator) + .at(¤t_epoch.prev()) + .get(&storage, ¤t_epoch) + .unwrap() + .unwrap(); + assert_eq!(redelegated, amount_redelegate); + + // Advance three epochs + current_epoch = advance_epoch(&mut storage, ¶ms); + super::process_slashes(&mut storage, current_epoch).unwrap(); + current_epoch = advance_epoch(&mut storage, ¶ms); + super::process_slashes(&mut storage, current_epoch).unwrap(); + current_epoch = advance_epoch(&mut storage, ¶ms); + super::process_slashes(&mut storage, current_epoch).unwrap(); + + // Unbond in epoch 11 from dest_validator + println!( + "\nUNBONDING {} TOKENS FROM {}\n", + amount_unbond.to_string_native(), + &dest_validator + ); + let _ = unbond_tokens( + &mut storage, + Some(&delegator), + &dest_validator, + amount_unbond, + current_epoch, + false, + ) + .unwrap(); + + println!("\nAFTER UNBONDING\n"); + println!("\nDELEGATOR\n"); + + let bonds_src = bond_handle(&delegator, &src_validator) + .get_data_handler() + .collect_map(&storage) + .unwrap(); + let bonds_dest = bond_handle(&delegator, &dest_validator) + .get_data_handler() + .collect_map(&storage) + .unwrap(); + let unbonds_src = unbond_handle(&delegator, &src_validator) + .collect_map(&storage) + .unwrap(); + let unbonds_dest = unbond_handle(&delegator, &dest_validator) + .collect_map(&storage) + .unwrap(); + let redel_bonds = delegator_redelegated_bonds_handle(&delegator) + .collect_map(&storage) + .unwrap(); + let redel_unbonds = delegator_redelegated_unbonds_handle(&delegator) + .collect_map(&storage) + .unwrap(); + + dbg!( + &bonds_src, + &bonds_dest, + &unbonds_src, + &unbonds_dest, + &redel_bonds, + &redel_unbonds + ); + + println!("\nDEST VALIDATOR\n"); + + let incoming_redels_dest = + validator_incoming_redelegations_handle(&dest_validator) + .collect_map(&storage) + .unwrap(); + let outgoing_redels_dest = + validator_outgoing_redelegations_handle(&dest_validator) + .collect_map(&storage) + .unwrap(); + let tot_bonds_dest = total_bonded_handle(&dest_validator) + .get_data_handler() + .collect_map(&storage) + .unwrap(); + let tot_unbonds_dest = total_unbonded_handle(&dest_validator) + .collect_map(&storage) + .unwrap(); + let tot_redel_bonds_dest = + validator_total_redelegated_bonded_handle(&dest_validator) + .collect_map(&storage) + .unwrap(); + let tot_redel_unbonds_dest = + validator_total_redelegated_unbonded_handle(&dest_validator) + .collect_map(&storage) + .unwrap(); + dbg!( + &incoming_redels_dest, + &outgoing_redels_dest, + &tot_bonds_dest, + &tot_unbonds_dest, + &tot_redel_bonds_dest, + &tot_redel_unbonds_dest + ); + + // Advance one epoch + current_epoch = advance_epoch(&mut storage, ¶ms); + super::process_slashes(&mut storage, current_epoch).unwrap(); + + // Discover evidence + slash( + &mut storage, + ¶ms, + current_epoch, + init_epoch + 2 * params.pipeline_len, + 0u64, + SlashType::DuplicateVote, + &src_validator, + current_epoch.next(), + ) + .unwrap(); + + let bond_start = init_epoch + params.pipeline_len; + let redelegation_end = bond_start + params.pipeline_len + 1u64; + let unbond_end = + redelegation_end + params.withdrawable_epoch_offset() + 1u64; + let unbond_materialized = redelegation_end + params.pipeline_len + 1u64; + + // Checks + let redelegated_remaining = delegator_redelegated_bonds_handle(&delegator) + .at(&dest_validator) + .at(&redelegation_end) + .at(&src_validator) + .get(&storage, &bond_start) + .unwrap() + .unwrap_or_default(); + assert_eq!(redelegated_remaining, amount_redelegate - amount_unbond); + + let redel_unbonded = delegator_redelegated_unbonds_handle(&delegator) + .at(&dest_validator) + .at(&redelegation_end) + .at(&unbond_end) + .at(&src_validator) + .get(&storage, &bond_start) + .unwrap() + .unwrap(); + assert_eq!(redel_unbonded, amount_unbond); + + dbg!(unbond_materialized, redelegation_end, bond_start); + let total_redel_unbonded = + validator_total_redelegated_unbonded_handle(&dest_validator) + .at(&unbond_materialized) + .at(&redelegation_end) + .at(&src_validator) + .get(&storage, &bond_start) + .unwrap() + .unwrap(); + assert_eq!(total_redel_unbonded, amount_unbond); + + // Advance to withdrawal epoch + loop { + current_epoch = advance_epoch(&mut storage, ¶ms); + super::process_slashes(&mut storage, current_epoch).unwrap(); + if current_epoch == unbond_end { + break; + } + } + + // Withdraw + withdraw_tokens( + &mut storage, + Some(&delegator), + &dest_validator, + current_epoch, + ) + .unwrap(); + + assert!( + delegator_redelegated_unbonds_handle(&delegator) + .at(&dest_validator) + .is_empty(&storage) + .unwrap() + ); + + let delegator_balance = storage + .read::(&token::balance_key(&staking_token, &delegator)) + .unwrap() + .unwrap_or_default(); + assert_eq!(delegator_balance, del_balance - amount_delegate); +} + +fn test_chain_redelegations_aux(mut validators: Vec) { + validators.sort_by(|a, b| b.tokens.cmp(&a.tokens)); + + let src_validator = validators[0].address.clone(); + let _init_stake_src = validators[0].tokens; + let dest_validator = validators[1].address.clone(); + let _init_stake_dest = validators[1].tokens; + let dest_validator_2 = validators[2].address.clone(); + let _init_stake_dest_2 = validators[2].tokens; + + let mut storage = TestWlStorage::default(); + let params = PosParams { + unbonding_len: 4, + ..Default::default() + }; + + // Genesis + let mut current_epoch = storage.storage.block.epoch; + init_genesis( + &mut storage, + ¶ms, + validators.clone().into_iter(), + current_epoch, + ) + .unwrap(); + storage.commit_block().unwrap(); + + // Get a delegator with some tokens + let staking_token = staking_token_address(&storage); + let delegator = address::testing::gen_implicit_address(); + let del_balance = token::Amount::from_uint(1_000_000, 0).unwrap(); + credit_tokens(&mut storage, &staking_token, &delegator, del_balance) + .unwrap(); + + // Delegate in epoch 0 to src_validator + let bond_amount: token::Amount = 100.into(); + super::bond_tokens( + &mut storage, + Some(&delegator), + &src_validator, + bond_amount, + current_epoch, + ) + .unwrap(); + + let bond_start = current_epoch + params.pipeline_len; + + // Advance one epoch + current_epoch = advance_epoch(&mut storage, ¶ms); + super::process_slashes(&mut storage, current_epoch).unwrap(); + + // Redelegate in epoch 1 to dest_validator + let redel_amount_1: token::Amount = 58.into(); + super::redelegate_tokens( + &mut storage, + &delegator, + &src_validator, + &dest_validator, + current_epoch, + redel_amount_1, + ) + .unwrap(); + + let redel_start = current_epoch; + let redel_end = current_epoch + params.pipeline_len; + + // Checks ---------------- + + // Dest validator should have an incoming redelegation + let incoming_redelegation = + validator_incoming_redelegations_handle(&dest_validator) + .get(&storage, &delegator) + .unwrap(); + assert_eq!(incoming_redelegation, Some(redel_end)); + + // Src validator should have an outoging redelegation + let outgoing_redelegation = + validator_outgoing_redelegations_handle(&src_validator) + .at(&dest_validator) + .at(&bond_start) + .get(&storage, &redel_start) + .unwrap(); + assert_eq!(outgoing_redelegation, Some(redel_amount_1)); + + // Delegator should have redelegated bonds + let del_total_redelegated_bonded = + delegator_redelegated_bonds_handle(&delegator) + .at(&dest_validator) + .at(&redel_end) + .at(&src_validator) + .get(&storage, &bond_start) + .unwrap() + .unwrap_or_default(); + assert_eq!(del_total_redelegated_bonded, redel_amount_1); + + // There should be delegator bonds for both src and dest validators + let bonded_src = bond_handle(&delegator, &src_validator); + let bonded_dest = bond_handle(&delegator, &dest_validator); + assert_eq!( + bonded_src + .get_delta_val(&storage, bond_start) + .unwrap() + .unwrap_or_default(), + bond_amount - redel_amount_1 + ); + assert_eq!( + bonded_dest + .get_delta_val(&storage, redel_end) + .unwrap() + .unwrap_or_default(), + redel_amount_1 + ); + + // The dest validator should have total redelegated bonded tokens + let dest_total_redelegated_bonded = + validator_total_redelegated_bonded_handle(&dest_validator) + .at(&redel_end) + .at(&src_validator) + .get(&storage, &bond_start) + .unwrap() + .unwrap_or_default(); + assert_eq!(dest_total_redelegated_bonded, redel_amount_1); + + // The dest validator's total bonded should have an entry for the genesis + // bond and the redelegation + let dest_total_bonded = total_bonded_handle(&dest_validator) + .get_data_handler() + .collect_map(&storage) + .unwrap(); + assert!( + dest_total_bonded.len() == 2 + && dest_total_bonded.contains_key(&Epoch::default()) + ); + assert_eq!( + dest_total_bonded + .get(&redel_end) + .cloned() + .unwrap_or_default(), + redel_amount_1 + ); + + // The src validator should have a total bonded entry for the original bond + // accounting for the redelegation + assert_eq!( + total_bonded_handle(&src_validator) + .get_delta_val(&storage, bond_start) + .unwrap() + .unwrap_or_default(), + bond_amount - redel_amount_1 + ); + + // The src validator should have a total unbonded entry due to the + // redelegation + let src_total_unbonded = total_unbonded_handle(&src_validator) + .at(&redel_end) + .get(&storage, &bond_start) + .unwrap() + .unwrap_or_default(); + assert_eq!(src_total_unbonded, redel_amount_1); + + // Attempt to redelegate in epoch 3 to dest_validator + current_epoch = advance_epoch(&mut storage, ¶ms); + super::process_slashes(&mut storage, current_epoch).unwrap(); + current_epoch = advance_epoch(&mut storage, ¶ms); + super::process_slashes(&mut storage, current_epoch).unwrap(); + + let redel_amount_2: token::Amount = 23.into(); + let redel_att = super::redelegate_tokens( + &mut storage, + &delegator, + &dest_validator, + &dest_validator_2, + current_epoch, + redel_amount_2, + ); + assert!(redel_att.is_err()); + + // Advance to right before the redelegation can be redelegated again + assert_eq!(redel_end, current_epoch); + let epoch_can_redel = + redel_end.prev() + params.slash_processing_epoch_offset(); + loop { + current_epoch = advance_epoch(&mut storage, ¶ms); + super::process_slashes(&mut storage, current_epoch).unwrap(); + if current_epoch == epoch_can_redel.prev() { + break; + } + } + + // Attempt to redelegate in epoch before we actually are able to + let redel_att = super::redelegate_tokens( + &mut storage, + &delegator, + &dest_validator, + &dest_validator_2, + current_epoch, + redel_amount_2, + ); + assert!(redel_att.is_err()); + + // Advance one more epoch + current_epoch = advance_epoch(&mut storage, ¶ms); + super::process_slashes(&mut storage, current_epoch).unwrap(); + + // Redelegate from dest_validator to dest_validator_2 now + super::redelegate_tokens( + &mut storage, + &delegator, + &dest_validator, + &dest_validator_2, + current_epoch, + redel_amount_2, + ) + .unwrap(); + + let redel_2_start = current_epoch; + let redel_2_end = current_epoch + params.pipeline_len; + + // Checks ----------------------------------- + + // Both the dest validator and dest validator 2 should have incoming + // redelegations + let incoming_redelegation_1 = + validator_incoming_redelegations_handle(&dest_validator) + .get(&storage, &delegator) + .unwrap(); + assert_eq!(incoming_redelegation_1, Some(redel_end)); + let incoming_redelegation_2 = + validator_incoming_redelegations_handle(&dest_validator_2) + .get(&storage, &delegator) + .unwrap(); + assert_eq!(incoming_redelegation_2, Some(redel_2_end)); + + // Both the src validator and dest validator should have outgoing + // redelegations + let outgoing_redelegation_1 = + validator_outgoing_redelegations_handle(&src_validator) + .at(&dest_validator) + .at(&bond_start) + .get(&storage, &redel_start) + .unwrap(); + assert_eq!(outgoing_redelegation_1, Some(redel_amount_1)); + + let outgoing_redelegation_2 = + validator_outgoing_redelegations_handle(&dest_validator) + .at(&dest_validator_2) + .at(&redel_end) + .get(&storage, &redel_2_start) + .unwrap(); + assert_eq!(outgoing_redelegation_2, Some(redel_amount_2)); + + // All three validators should have bonds + let bonded_dest2 = bond_handle(&delegator, &dest_validator_2); + assert_eq!( + bonded_src + .get_delta_val(&storage, bond_start) + .unwrap() + .unwrap_or_default(), + bond_amount - redel_amount_1 + ); + assert_eq!( + bonded_dest + .get_delta_val(&storage, redel_end) + .unwrap() + .unwrap_or_default(), + redel_amount_1 - redel_amount_2 + ); + assert_eq!( + bonded_dest2 + .get_delta_val(&storage, redel_2_end) + .unwrap() + .unwrap_or_default(), + redel_amount_2 + ); + + // There should be no unbond entries + let unbond_src = unbond_handle(&delegator, &src_validator); + let unbond_dest = unbond_handle(&delegator, &dest_validator); + assert!(unbond_src.is_empty(&storage).unwrap()); + assert!(unbond_dest.is_empty(&storage).unwrap()); + + // The dest validator should have some total unbonded due to the second + // redelegation + let dest_total_unbonded = total_unbonded_handle(&dest_validator) + .at(&redel_2_end) + .get(&storage, &redel_end) + .unwrap(); + assert_eq!(dest_total_unbonded, Some(redel_amount_2)); + + // Delegator should have redelegated bonds due to both redelegations + let del_redelegated_bonds = delegator_redelegated_bonds_handle(&delegator); + assert_eq!( + Some(redel_amount_1 - redel_amount_2), + del_redelegated_bonds + .at(&dest_validator) + .at(&redel_end) + .at(&src_validator) + .get(&storage, &bond_start) + .unwrap() + ); + assert_eq!( + Some(redel_amount_2), + del_redelegated_bonds + .at(&dest_validator_2) + .at(&redel_2_end) + .at(&dest_validator) + .get(&storage, &redel_end) + .unwrap() + ); + + // Delegator redelegated unbonds should be empty + assert!( + delegator_redelegated_unbonds_handle(&delegator) + .is_empty(&storage) + .unwrap() + ); + + // Both the dest validator and dest validator 2 should have total + // redelegated bonds + let dest_redelegated_bonded = + validator_total_redelegated_bonded_handle(&dest_validator) + .at(&redel_end) + .at(&src_validator) + .get(&storage, &bond_start) + .unwrap() + .unwrap_or_default(); + let dest2_redelegated_bonded = + validator_total_redelegated_bonded_handle(&dest_validator_2) + .at(&redel_2_end) + .at(&dest_validator) + .get(&storage, &redel_end) + .unwrap() + .unwrap_or_default(); + assert_eq!(dest_redelegated_bonded, redel_amount_1 - redel_amount_2); + assert_eq!(dest2_redelegated_bonded, redel_amount_2); + + // Total redelegated unbonded should be empty for src_validator and + // dest_validator_2 + assert!( + validator_total_redelegated_unbonded_handle(&dest_validator_2) + .is_empty(&storage) + .unwrap() + ); + assert!( + validator_total_redelegated_unbonded_handle(&src_validator) + .is_empty(&storage) + .unwrap() + ); + + // The dest_validator should have total_redelegated unbonded + let tot_redel_unbonded = + validator_total_redelegated_unbonded_handle(&dest_validator) + .at(&redel_2_end) + .at(&redel_end) + .at(&src_validator) + .get(&storage, &bond_start) + .unwrap() + .unwrap_or_default(); + assert_eq!(tot_redel_unbonded, redel_amount_2); +} + +/// SM test case 1 from Brent +#[test] +fn test_from_sm_case_1() { + use namada_core::types::address::testing::established_address_4; + + let mut storage = TestWlStorage::default(); + let validator = established_address_1(); + let redeleg_src_1 = established_address_2(); + let redeleg_src_2 = established_address_3(); + let owner = established_address_4(); + let unbond_amount = token::Amount::from(3130688); + println!( + "Owner: {owner}\nValidator: {validator}\nRedeleg src 1: \ + {redeleg_src_1}\nRedeleg src 2: {redeleg_src_2}" + ); + + // Validator's incoming redelegations + let outer_epoch_1 = Epoch(27); + // from redeleg_src_1 + let epoch_1_redeleg_1 = token::Amount::from(8516); + // from redeleg_src_2 + let epoch_1_redeleg_2 = token::Amount::from(5704386); + let outer_epoch_2 = Epoch(30); + // from redeleg_src_2 + let epoch_2_redeleg_2 = token::Amount::from(1035191); + + // Insert the data - bonds and redelegated bonds + let bonds_handle = bond_handle(&owner, &validator); + bonds_handle + .add( + &mut storage, + epoch_1_redeleg_1 + epoch_1_redeleg_2, + outer_epoch_1, + 0, + ) + .unwrap(); + bonds_handle + .add(&mut storage, epoch_2_redeleg_2, outer_epoch_2, 0) + .unwrap(); + + let redelegated_bonds_map_1 = delegator_redelegated_bonds_handle(&owner) + .at(&validator) + .at(&outer_epoch_1); + redelegated_bonds_map_1 + .at(&redeleg_src_1) + .insert(&mut storage, Epoch(14), epoch_1_redeleg_1) + .unwrap(); + redelegated_bonds_map_1 + .at(&redeleg_src_2) + .insert(&mut storage, Epoch(18), epoch_1_redeleg_2) + .unwrap(); + let redelegated_bonds_map_1 = delegator_redelegated_bonds_handle(&owner) + .at(&validator) + .at(&outer_epoch_1); + + let redelegated_bonds_map_2 = delegator_redelegated_bonds_handle(&owner) + .at(&validator) + .at(&outer_epoch_2); + redelegated_bonds_map_2 + .at(&redeleg_src_2) + .insert(&mut storage, Epoch(18), epoch_2_redeleg_2) + .unwrap(); + + // Find the modified redelegation the same way as `unbond_tokens` + let bonds_to_unbond = find_bonds_to_remove( + &storage, + &bonds_handle.get_data_handler(), + unbond_amount, + ) + .unwrap(); + dbg!(&bonds_to_unbond); + + let (new_entry_epoch, new_bond_amount) = bonds_to_unbond.new_entry.unwrap(); + assert_eq!(outer_epoch_1, new_entry_epoch); + // The modified bond should be sum of all redelegations less the unbonded + // amouunt + assert_eq!( + epoch_1_redeleg_1 + epoch_1_redeleg_2 + epoch_2_redeleg_2 + - unbond_amount, + new_bond_amount + ); + // The current bond should be sum of redelegations fom the modified epoch + let cur_bond_amount = bonds_handle + .get_delta_val(&storage, new_entry_epoch) + .unwrap() + .unwrap_or_default(); + assert_eq!(epoch_1_redeleg_1 + epoch_1_redeleg_2, cur_bond_amount); + + let mr = compute_modified_redelegation( + &storage, + &redelegated_bonds_map_1, + new_entry_epoch, + cur_bond_amount - new_bond_amount, + ) + .unwrap(); + + let exp_mr = ModifiedRedelegation { + epoch: Some(Epoch(27)), + validators_to_remove: BTreeSet::from_iter([redeleg_src_2.clone()]), + validator_to_modify: Some(redeleg_src_2), + epochs_to_remove: BTreeSet::from_iter([Epoch(18)]), + epoch_to_modify: Some(Epoch(18)), + new_amount: Some(token::Amount::from(3608889)), + }; + + pretty_assertions::assert_eq!(mr, exp_mr); +} + +/// Test precisely that we are not overslashing, as originally discovered by Tomas in this issue: https://github.com/informalsystems/partnership-heliax/issues/74 +fn test_overslashing_aux(mut validators: Vec) { + assert_eq!(validators.len(), 4); + + let params = PosParams { + unbonding_len: 4, + ..Default::default() + }; + + let offending_stake = token::Amount::native_whole(110); + let other_stake = token::Amount::native_whole(100); + + // Set stakes so we know we will get a slashing rate between 0.5 -1.0 + validators[0].tokens = offending_stake; + validators[1].tokens = other_stake; + validators[2].tokens = other_stake; + validators[3].tokens = other_stake; + + // Get the offending validator + let validator = validators[0].address.clone(); + + println!("\nTest inputs: {params:?}, genesis validators: {validators:#?}"); + let mut storage = TestWlStorage::default(); + + // Genesis + let mut current_epoch = storage.storage.block.epoch; + init_genesis( + &mut storage, + ¶ms, + validators.clone().into_iter(), + current_epoch, + ) + .unwrap(); + storage.commit_block().unwrap(); + + // Get a delegator with some tokens + let staking_token = storage.storage.native_token.clone(); + let delegator = address::testing::gen_implicit_address(); + let amount_del = token::Amount::native_whole(5); + credit_tokens(&mut storage, &staking_token, &delegator, amount_del) + .unwrap(); + + // Delegate tokens in epoch 0 to validator + bond_tokens( + &mut storage, + Some(&delegator), + &validator, + amount_del, + current_epoch, + ) + .unwrap(); + + let self_bond_epoch = current_epoch; + let delegation_epoch = current_epoch + params.pipeline_len; + + // Advance to pipeline epoch + for _ in 0..params.pipeline_len { + current_epoch = advance_epoch(&mut storage, ¶ms); + } + assert_eq!(delegation_epoch, current_epoch); + + // Find a misbehavior committed in epoch 0 + slash( + &mut storage, + ¶ms, + current_epoch, + self_bond_epoch, + 0_u64, + SlashType::DuplicateVote, + &validator, + current_epoch.next(), + ) + .unwrap(); + + // Find a misbehavior committed in current epoch + slash( + &mut storage, + ¶ms, + current_epoch, + delegation_epoch, + 0_u64, + SlashType::DuplicateVote, + &validator, + current_epoch.next(), + ) + .unwrap(); + + let processing_epoch_1 = + self_bond_epoch + params.slash_processing_epoch_offset(); + let processing_epoch_2 = + delegation_epoch + params.slash_processing_epoch_offset(); + + // Advance to processing epoch 1 + loop { + current_epoch = advance_epoch(&mut storage, ¶ms); + super::process_slashes(&mut storage, current_epoch).unwrap(); + if current_epoch == processing_epoch_1 { + break; + } + } + + let total_stake_1 = offending_stake + 3 * other_stake; + let stake_frac = Dec::from(offending_stake) / Dec::from(total_stake_1); + let slash_rate_1 = Dec::from_str("9.0").unwrap() * stake_frac * stake_frac; + dbg!(&slash_rate_1); + + let exp_slashed_1 = offending_stake.mul_ceil(slash_rate_1); + + // Check that the proper amount was slashed + let epoch = current_epoch.next(); + let validator_stake = + read_validator_stake(&storage, ¶ms, &validator, epoch).unwrap(); + let exp_validator_stake = offending_stake - exp_slashed_1 + amount_del; + assert_eq!(validator_stake, exp_validator_stake); + + let total_stake = read_total_stake(&storage, ¶ms, epoch).unwrap(); + let exp_total_stake = + offending_stake - exp_slashed_1 + amount_del + 3 * other_stake; + assert_eq!(total_stake, exp_total_stake); + + let self_bond_id = BondId { + source: validator.clone(), + validator: validator.clone(), + }; + let bond_amount = + crate::bond_amount(&storage, &self_bond_id, epoch).unwrap(); + let exp_bond_amount = offending_stake - exp_slashed_1; + assert_eq!(bond_amount, exp_bond_amount); + + // Advance to processing epoch 2 + loop { + current_epoch = advance_epoch(&mut storage, ¶ms); + super::process_slashes(&mut storage, current_epoch).unwrap(); + if current_epoch == processing_epoch_2 { + break; + } + } + + let total_stake_2 = offending_stake + amount_del + 3 * other_stake; + let stake_frac = + Dec::from(offending_stake + amount_del) / Dec::from(total_stake_2); + let slash_rate_2 = Dec::from_str("9.0").unwrap() * stake_frac * stake_frac; + dbg!(&slash_rate_2); + + let exp_slashed_from_delegation = amount_del.mul_ceil(slash_rate_2); + + // Check that the proper amount was slashed. We expect that all of the + // validator self-bond has been slashed and some of the delegation has been + // slashed due to the second infraction. + let epoch = current_epoch.next(); + + let validator_stake = + read_validator_stake(&storage, ¶ms, &validator, epoch).unwrap(); + let exp_validator_stake = amount_del - exp_slashed_from_delegation; + assert_eq!(validator_stake, exp_validator_stake); + + let total_stake = read_total_stake(&storage, ¶ms, epoch).unwrap(); + let exp_total_stake = + amount_del - exp_slashed_from_delegation + 3 * other_stake; + assert_eq!(total_stake, exp_total_stake); + + let delegation_id = BondId { + source: delegator.clone(), + validator: validator.clone(), + }; + let delegation_amount = + crate::bond_amount(&storage, &delegation_id, epoch).unwrap(); + let exp_del_amount = amount_del - exp_slashed_from_delegation; + assert_eq!(delegation_amount, exp_del_amount); + + let self_bond_amount = + crate::bond_amount(&storage, &self_bond_id, epoch).unwrap(); + let exp_bond_amount = token::Amount::zero(); + assert_eq!(self_bond_amount, exp_bond_amount); +} diff --git a/proof_of_stake/src/tests/state_machine.rs b/proof_of_stake/src/tests/state_machine.rs index 6c9968c519..e9c4db1b3a 100644 --- a/proof_of_stake/src/tests/state_machine.rs +++ b/proof_of_stake/src/tests/state_machine.rs @@ -2,10 +2,14 @@ use std::cmp; use std::collections::{BTreeMap, BTreeSet, HashSet, VecDeque}; +use std::ops::Deref; +use assert_matches::assert_matches; 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::collections::lazy_map::{ + Collectable, NestedSubKey, SubKey, +}; use namada_core::ledger::storage_api::token::read_balance; use namada_core::ledger::storage_api::{token, StorageRead}; use namada_core::types::address::{self, Address}; @@ -27,27 +31,72 @@ use crate::parameters::testing::arb_rate; use crate::parameters::PosParams; use crate::tests::arb_params_and_genesis_validators; use crate::types::{ - BondId, GenesisValidator, ReverseOrdTokenAmount, Slash, SlashType, - SlashedAmount, ValidatorState, WeightedValidator, + BondId, EagerRedelegatedBondsMap, GenesisValidator, ReverseOrdTokenAmount, + Slash, SlashType, ValidatorState, WeightedValidator, }; use crate::{ below_capacity_validator_set_handle, consensus_validator_set_handle, enqueued_slashes_handle, read_below_threshold_validator_set_addresses, - read_pos_params, validator_deltas_handle, validator_slashes_handle, - validator_state_handle, + read_pos_params, redelegate_tokens, validator_deltas_handle, + validator_slashes_handle, validator_state_handle, BondsForRemovalRes, + EagerRedelegatedUnbonds, FoldRedelegatedBondsResult, ModifiedRedelegation, + RedelegationError, ResultSlashing, }; prop_state_machine! { #![proptest_config(Config { cases: 2, - verbose: 1, .. Config::default() })] #[test] /// A `StateMachineTest` implemented on `PosState` - fn pos_state_machine_test(sequential 200 => ConcretePosState); + fn pos_state_machine_test(sequential 500 => ConcretePosState); } +type AbstractDelegatorRedelegatedBonded = BTreeMap< + Address, + BTreeMap< + Address, + BTreeMap>>, + >, +>; + +type AbstractDelegatorRedelegatedUnbonded = BTreeMap< + Address, + BTreeMap< + Address, + BTreeMap< + (Epoch, Epoch), + BTreeMap>, + >, + >, +>; + +type AbstractValidatorTotalRedelegatedBonded = BTreeMap< + Address, + BTreeMap>>, +>; + +type AbstractTotalRedelegatedUnbonded = BTreeMap< + Epoch, + BTreeMap>>, +>; + +type AbstractValidatorTotalRedelegatedUnbonded = BTreeMap< + Address, + BTreeMap< + Epoch, + BTreeMap>>, + >, +>; + +type AbstractIncomingRedelegations = + BTreeMap>; +type AbstractOutgoingRedelegations = BTreeMap< + Address, + BTreeMap>, +>; + /// Abstract representation of a state of PoS system #[derive(Clone, Debug)] struct AbstractPosState { @@ -59,13 +108,13 @@ struct AbstractPosState { genesis_validators: Vec, /// Bonds delta values. The outer key for Epoch is pipeline offset from /// epoch in which the bond is applied - bonds: BTreeMap>, + bonds: BTreeMap>, /// Total bonded tokens to a validator in each epoch. This is never /// decremented and used for slashing computations. - total_bonded: BTreeMap>, + total_bonded: BTreeMap>, /// Validator stakes. These are NOT deltas. /// Pipelined. - validator_stakes: BTreeMap>, + validator_stakes: BTreeMap>, /// Consensus validator set. Pipelined. consensus_set: BTreeMap>>, /// Below-capacity validator set. Pipelined. @@ -75,20 +124,30 @@ struct AbstractPosState { below_threshold_set: BTreeMap>, /// Validator states. Pipelined. validator_states: BTreeMap>, - /// Unbonded bonds. The outer key for Epoch is pipeline + unbonding offset - /// from epoch in which the unbond is applied. - unbonds: BTreeMap>, + /// Unbonded bonds. The outer key for Epoch is pipeline + unbonding + + /// cubic_window offset from epoch in which the unbond transition + /// occurs. + unbonds: BTreeMap<(Epoch, Epoch), 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. + /// Validator's total unbonded 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: + total_unbonded: BTreeMap>>, + /// The outer key is the epoch in which redelegation became active + /// (pipeline offset). The next key is the address of the delegator. + delegator_redelegated_bonded: AbstractDelegatorRedelegatedBonded, + delegator_redelegated_unbonded: AbstractDelegatorRedelegatedUnbonded, + validator_total_redelegated_bonded: AbstractValidatorTotalRedelegatedBonded, + validator_total_redelegated_unbonded: + AbstractValidatorTotalRedelegatedUnbonded, + incoming_redelegations: AbstractIncomingRedelegations, + outgoing_redelegations: AbstractOutgoingRedelegations, } /// The PoS system under test @@ -122,6 +181,13 @@ enum Transition { Withdraw { id: BondId, }, + Redelegate { + /// A chained redelegation must fail + is_chained: bool, + id: BondId, + new_validator: Address, + amount: token::Amount, + }, Misbehavior { address: Address, slash_type: SlashType, @@ -140,9 +206,8 @@ impl StateMachineTest for ConcretePosState { fn init_test( initial_state: &::State, ) -> Self::SystemUnderTest { - println!(); - println!("New test case"); - println!( + tracing::debug!("New test case"); + tracing::debug!( "Genesis validators: {:#?}", initial_state .genesis_validators @@ -163,7 +228,7 @@ impl StateMachineTest for ConcretePosState { fn apply( mut state: Self::SystemUnderTest, - _ref_state: &::State, + ref_state: &::State, transition: ::Transition, ) -> Self::SystemUnderTest { let params = crate::read_pos_params(&state.s).unwrap(); @@ -173,10 +238,10 @@ impl StateMachineTest for ConcretePosState { &crate::ADDRESS, ) .unwrap(); - println!("PoS balance: {}", pos_balance.to_string_native()); + tracing::debug!("PoS balance: {}", pos_balance.to_string_native()); match transition { Transition::NextEpoch => { - println!("\nCONCRETE Next epoch"); + tracing::debug!("\nCONCRETE Next epoch"); super::advance_epoch(&mut state.s, ¶ms); // Need to apply some slashing @@ -194,7 +259,7 @@ impl StateMachineTest for ConcretePosState { commission_rate, max_commission_rate_change, } => { - println!("\nCONCRETE Init validator"); + tracing::debug!("\nCONCRETE Init validator"); let current_epoch = state.current_epoch(); super::become_validator(super::BecomeValidator { @@ -218,7 +283,7 @@ impl StateMachineTest for ConcretePosState { ) } Transition::Bond { id, amount } => { - println!("\nCONCRETE Bond"); + tracing::debug!("\nCONCRETE Bond"); let current_epoch = state.current_epoch(); let pipeline = current_epoch + params.pipeline_len; let validator_stake_before_bond_cur = @@ -228,8 +293,7 @@ impl StateMachineTest for ConcretePosState { &id.validator, current_epoch, ) - .unwrap() - .unwrap_or_default(); + .unwrap(); let validator_stake_before_bond_pipeline = crate::read_validator_stake( &state.s, @@ -237,8 +301,7 @@ impl StateMachineTest for ConcretePosState { &id.validator, pipeline, ) - .unwrap() - .unwrap_or_default(); + .unwrap(); // Credit tokens to ensure we can apply the bond let native_token = state.s.get_native_token().unwrap(); @@ -299,9 +362,11 @@ impl StateMachineTest for ConcretePosState { pos_balance_post - pos_balance_pre, src_balance_pre - src_balance_post ); + + state.check_multistate_bond_post_conditions(ref_state, &id); } Transition::Unbond { id, amount } => { - println!("\nCONCRETE Unbond"); + tracing::debug!("\nCONCRETE Unbond"); let current_epoch = state.current_epoch(); let pipeline = current_epoch + params.pipeline_len; let native_token = state.s.get_native_token().unwrap(); @@ -319,8 +384,7 @@ impl StateMachineTest for ConcretePosState { &id.validator, current_epoch, ) - .unwrap() - .unwrap_or_default(); + .unwrap(); let validator_stake_before_unbond_pipeline = crate::read_validator_stake( &state.s, @@ -328,8 +392,7 @@ impl StateMachineTest for ConcretePosState { &id.validator, pipeline, ) - .unwrap() - .unwrap_or_default(); + .unwrap(); // Apply the unbond super::unbond_tokens( @@ -338,6 +401,7 @@ impl StateMachineTest for ConcretePosState { &id.validator, amount, current_epoch, + false, ) .unwrap(); @@ -361,11 +425,13 @@ impl StateMachineTest for ConcretePosState { assert_eq!(pos_balance_pre, pos_balance_post); // Post-condition: Source balance should not change assert_eq!(src_balance_post, src_balance_pre); + + state.check_multistate_unbond_post_conditions(ref_state, &id); } Transition::Withdraw { id: BondId { source, validator }, } => { - println!("\nCONCRETE Withdraw"); + tracing::debug!("\nCONCRETE Withdraw"); let current_epoch = state.current_epoch(); let native_token = state.s.get_native_token().unwrap(); let pos = address::POS; @@ -411,6 +477,218 @@ impl StateMachineTest for ConcretePosState { // Post-condition: The increment in source balance should be // equal to the withdrawn amount assert_eq!(src_balance_post - src_balance_pre, withdrawn); + + state.check_multistate_withdraw_post_conditions( + ref_state, + &BondId { source, validator }, + ); + } + Transition::Redelegate { + is_chained, + id, + new_validator, + amount, + } => { + tracing::debug!("\nCONCRETE Redelegate"); + + let current_epoch = state.current_epoch(); + let pipeline = current_epoch + params.pipeline_len; + + // Read data prior to applying the transition + let native_token = state.s.get_native_token().unwrap(); + let pos = address::POS; + let pos_balance_pre = + token::read_balance(&state.s, &native_token, &pos).unwrap(); + let slash_pool = address::POS_SLASH_POOL; + let slash_balance_pre = + token::read_balance(&state.s, &native_token, &slash_pool) + .unwrap(); + + // Read src validator stakes + let src_validator_stake_cur_pre = crate::read_validator_stake( + &state.s, + ¶ms, + &id.validator, + current_epoch, + ) + .unwrap(); + let _src_validator_stake_pipeline_pre = + crate::read_validator_stake( + &state.s, + ¶ms, + &id.validator, + pipeline, + ) + .unwrap(); + + // Read dest validator stakes + let dest_validator_stake_cur_pre = crate::read_validator_stake( + &state.s, + ¶ms, + &new_validator, + current_epoch, + ) + .unwrap(); + let _dest_validator_stake_pipeline_pre = + crate::read_validator_stake( + &state.s, + ¶ms, + &new_validator, + pipeline, + ) + .unwrap(); + + // Find delegations + let delegations_pre = + crate::find_delegations(&state.s, &id.source, &pipeline) + .unwrap(); + + // Apply redelegation + let result = redelegate_tokens( + &mut state.s, + &id.source, + &id.validator, + &new_validator, + current_epoch, + amount, + ); + + state.check_multistate_redelegation_post_conditions( + ref_state, + &id.source, + &id.validator, + &new_validator, + ); + + if is_chained && !amount.is_zero() { + assert!(result.is_err()); + let err = result.unwrap_err(); + let err_str = err.to_string(); + assert_matches!( + err.downcast::().unwrap().deref(), + RedelegationError::IsChainedRedelegation, + "A chained redelegation must be rejected, got \ + {err_str}", + ); + } else { + result.unwrap(); + + // Post-condition: PoS balance is unchanged + let pos_balance_post = + token::read_balance(&state.s, &native_token, &pos) + .unwrap(); + assert_eq!(pos_balance_pre, pos_balance_post); + + // Find slash pool balance difference + let slash_balance_post = token::read_balance( + &state.s, + &native_token, + &slash_pool, + ) + .unwrap(); + let slashed = slash_balance_post - slash_balance_pre; + + // Post-condition: Source validator stake at current epoch + // is unchanged + let src_validator_stake_cur_post = + crate::read_validator_stake( + &state.s, + ¶ms, + &id.validator, + current_epoch, + ) + .unwrap(); + assert_eq!( + src_validator_stake_cur_pre, + src_validator_stake_cur_post + ); + + // Post-condition: Source validator stake at pipeline epoch + // is reduced by the redelegation amount + + // TODO: shouldn't this be reduced by the redelegation + // amount post-slashing tho? + // NOTE: We changed it to reduce it, check again later + let _amount_after_slash = amount - slashed; + let _src_validator_stake_pipeline_post = + crate::read_validator_stake( + &state.s, + ¶ms, + &id.validator, + pipeline, + ) + .unwrap(); + // assert_eq!( + // src_validator_stake_pipeline_pre - + // amount_after_slash, + // src_validator_stake_pipeline_post + // ); + + // Post-condition: Destination validator stake at current + // epoch is unchanged + let dest_validator_stake_cur_post = + crate::read_validator_stake( + &state.s, + ¶ms, + &new_validator, + current_epoch, + ) + .unwrap(); + assert_eq!( + dest_validator_stake_cur_pre, + dest_validator_stake_cur_post + ); + + // Post-condition: Destination validator stake at pipeline + // epoch is increased by the redelegation amount, less any + // slashes + let _dest_validator_stake_pipeline_post = + crate::read_validator_stake( + &state.s, + ¶ms, + &new_validator, + pipeline, + ) + .unwrap(); + // assert_eq!( + // dest_validator_stake_pipeline_pre + + // amount_after_slash, + // dest_validator_stake_pipeline_post + // ); + + // Post-condition: The delegator's delegations should be + // updated with redelegation. For the source reduced by the + // redelegation amount and for the destination increased by + // the redelegation amount, less any slashes. + let delegations_post = crate::find_delegations( + &state.s, &id.source, &pipeline, + ) + .unwrap(); + let src_delegation_pre = delegations_pre + .get(&id.validator) + .cloned() + .unwrap_or_default(); + let src_delegation_post = delegations_post + .get(&id.validator) + .cloned() + .unwrap_or_default(); + assert_eq!( + src_delegation_pre - src_delegation_post, + amount + ); + let _dest_delegation_pre = delegations_pre + .get(&new_validator) + .cloned() + .unwrap_or_default(); + let _dest_delegation_post = delegations_post + .get(&new_validator) + .cloned() + .unwrap_or_default(); + // assert_eq!( + // dest_delegation_post - dest_delegation_pre, + // amount_after_slash + // ); + } } Transition::Misbehavior { address, @@ -418,7 +696,7 @@ impl StateMachineTest for ConcretePosState { infraction_epoch, height, } => { - println!("\nCONCRETE Misbehavior"); + tracing::debug!("\nCONCRETE Misbehavior"); let current_epoch = state.current_epoch(); // Record the slash evidence super::slash( @@ -443,10 +721,10 @@ impl StateMachineTest for ConcretePosState { &address, ); - // TODO: Any others? + state.check_multistate_misbehavior_post_conditions(ref_state); } Transition::UnjailValidator { address } => { - println!("\nCONCRETE UnjailValidator"); + tracing::debug!("\nCONCRETE UnjailValidator"); let current_epoch = state.current_epoch(); // Unjail the validator @@ -566,8 +844,7 @@ impl ConcretePosState { &id.validator, submit_epoch, ) - .unwrap() - .unwrap_or_default(); + .unwrap(); // Post-condition: the validator stake at the current epoch should not // change @@ -579,8 +856,7 @@ impl ConcretePosState { &id.validator, pipeline, ) - .unwrap() - .unwrap_or_default(); + .unwrap(); // Post-condition: the validator stake at the pipeline should be // incremented by the bond amount @@ -597,6 +873,29 @@ impl ConcretePosState { ); } + fn check_multistate_bond_post_conditions( + &self, + ref_state: &AbstractPosState, + id: &BondId, + ) { + // Check that the bonds are the same + let abs_bonds = ref_state.bonds.get(id).cloned().unwrap(); + let conc_bonds = crate::bond_handle(&id.source, &id.validator) + .get_data_handler() + .collect_map(&self.s) + .unwrap(); + assert_eq!(abs_bonds, conc_bonds); + + // Check that the total bonded is the same + let abs_tot_bonded = + ref_state.total_bonded.get(&id.validator).cloned().unwrap(); + let conc_tot_bonded = crate::total_bonded_handle(&id.validator) + .get_data_handler() + .collect_map(&self.s) + .unwrap(); + assert_eq!(abs_tot_bonded, conc_tot_bonded); + } + fn check_unbond_post_conditions( &self, submit_epoch: Epoch, @@ -614,8 +913,7 @@ impl ConcretePosState { &id.validator, submit_epoch, ) - .unwrap() - .unwrap_or_default(); + .unwrap(); // Post-condition: the validator stake at the current epoch should not // change @@ -627,8 +925,7 @@ impl ConcretePosState { &id.validator, pipeline, ) - .unwrap() - .unwrap_or_default(); + .unwrap(); // Post-condition: the validator stake at the pipeline should be // decremented at most by the bond amount (because slashing can reduce @@ -651,6 +948,172 @@ impl ConcretePosState { ); } + fn check_multistate_unbond_post_conditions( + &self, + ref_state: &AbstractPosState, + id: &BondId, + ) { + // Check that the bonds are the same + let abs_bonds = ref_state.bonds.get(id).cloned().unwrap(); + let conc_bonds = crate::bond_handle(&id.source, &id.validator) + .get_data_handler() + .collect_map(&self.s) + .unwrap(); + assert_eq!(abs_bonds, conc_bonds); + + // Check that the total bonded is the same + let abs_tot_bonded = + ref_state.total_bonded.get(&id.validator).cloned().unwrap(); + let conc_tot_bonded = crate::total_bonded_handle(&id.validator) + .get_data_handler() + .collect_map(&self.s) + .unwrap(); + assert_eq!(abs_tot_bonded, conc_tot_bonded); + + // Check that the unbonds are the same + let mut abs_unbonds: BTreeMap> = + BTreeMap::new(); + ref_state.unbonds.iter().for_each( + |((start_epoch, withdraw_epoch), inner)| { + let amount = inner.get(id).cloned().unwrap_or_default(); + if !amount.is_zero() { + abs_unbonds + .entry(*start_epoch) + .or_default() + .insert(*withdraw_epoch, amount); + } + }, + ); + let conc_unbonds = crate::unbond_handle(&id.source, &id.validator) + .collect_map(&self.s) + .unwrap(); + assert_eq!(abs_unbonds, conc_unbonds); + + // Check that the total_unbonded are the same + // TODO: figure out how we get entries with 0 amount in the + // abstract version (and prevent) + let mut abs_total_unbonded = ref_state + .total_unbonded + .get(&id.validator) + .cloned() + .unwrap(); + abs_total_unbonded.retain(|_, inner_map| { + inner_map.retain(|_, value| !value.is_zero()); + !inner_map.is_empty() + }); + let conc_total_unbonded = crate::total_unbonded_handle(&id.validator) + .collect_map(&self.s) + .unwrap(); + assert_eq!(abs_total_unbonded, conc_total_unbonded); + + // Check that the delegator redelegated bonds are the same + let abs_del_redel_bonds = ref_state + .delegator_redelegated_bonded + .get(&id.source) + .cloned() + .unwrap_or_default() + .get(&id.validator) + .cloned() + .unwrap_or_default(); + let conc_del_redel_bonds = + crate::delegator_redelegated_bonds_handle(&id.source) + .at(&id.validator) + .collect_map(&self.s) + .unwrap(); + assert_eq!(abs_del_redel_bonds, conc_del_redel_bonds); + + // Check that the delegator redelegated unbonds are the same + #[allow(clippy::type_complexity)] + let mut abs_del_redel_unbonds: BTreeMap< + Epoch, + BTreeMap>>, + > = BTreeMap::new(); + ref_state + .delegator_redelegated_unbonded + .get(&id.source) + .cloned() + .unwrap_or_default() + .get(&id.validator) + .cloned() + .unwrap_or_default() + .iter() + .for_each(|((redel_end_epoch, withdraw_epoch), inner)| { + let abs_map = abs_del_redel_unbonds + .entry(*redel_end_epoch) + .or_default() + .entry(*withdraw_epoch) + .or_default(); + for (src, bonds) in inner { + for (start, amount) in bonds { + abs_map + .entry(src.clone()) + .or_default() + .insert(*start, *amount); + } + } + }); + let conc_del_redel_unbonds = + crate::delegator_redelegated_unbonds_handle(&id.source) + .at(&id.validator) + .collect_map(&self.s) + .unwrap(); + assert_eq!(abs_del_redel_unbonds, conc_del_redel_unbonds); + + // Check the validator total redelegated bonded + let abs_total_redel_bonded = ref_state + .validator_total_redelegated_bonded + .get(&id.validator) + .cloned() + .unwrap_or_default(); + let mut conc_total_redel_bonded: BTreeMap< + Epoch, + BTreeMap>, + > = BTreeMap::new(); + crate::validator_total_redelegated_bonded_handle(&id.validator) + .iter(&self.s) + .unwrap() + .for_each(|res| { + let ( + NestedSubKey::Data { + key: redel_end_epoch, + nested_sub_key: + NestedSubKey::Data { + key: src_val, + nested_sub_key: SubKey::Data(bond_start), + }, + }, + amount, + ) = res.unwrap(); + conc_total_redel_bonded + .entry(redel_end_epoch) + .or_default() + .entry(src_val) + .or_default() + .insert(bond_start, amount); + }); + assert_eq!(abs_total_redel_bonded, conc_total_redel_bonded); + + // Check the validator total redelegated unbonded + let mut abs_total_redel_unbonded = ref_state + .validator_total_redelegated_unbonded + .get(&id.validator) + .cloned() + .unwrap_or_default(); + abs_total_redel_unbonded.retain(|_, inner1| { + inner1.retain(|_, inner2| { + inner2.retain(|_, inner3| !inner3.is_empty()); + !inner2.is_empty() + }); + !inner1.is_empty() + }); + + let conc_total_redel_unbonded = + crate::validator_total_redelegated_unbonded_handle(&id.validator) + .collect_map(&self.s) + .unwrap(); + assert_eq!(abs_total_redel_unbonded, conc_total_redel_unbonded); + } + /// These post-conditions apply to bonding and unbonding fn check_bond_and_unbond_post_conditions( &self, @@ -760,6 +1223,68 @@ impl ConcretePosState { } } + fn check_multistate_withdraw_post_conditions( + &self, + ref_state: &AbstractPosState, + id: &BondId, + ) { + // Check that the unbonds are the same + let mut abs_unbonds: BTreeMap> = + BTreeMap::new(); + ref_state.unbonds.iter().for_each( + |((start_epoch, withdraw_epoch), inner)| { + let amount = inner.get(id).cloned().unwrap_or_default(); + if !amount.is_zero() { + abs_unbonds + .entry(*start_epoch) + .or_default() + .insert(*withdraw_epoch, amount); + } + }, + ); + let conc_unbonds = crate::unbond_handle(&id.source, &id.validator) + .collect_map(&self.s) + .unwrap(); + assert_eq!(abs_unbonds, conc_unbonds); + + // Check the delegator redelegated unbonds + #[allow(clippy::type_complexity)] + let mut abs_del_redel_unbonds: BTreeMap< + Epoch, + BTreeMap>>, + > = BTreeMap::new(); + ref_state + .delegator_redelegated_unbonded + .get(&id.source) + .cloned() + .unwrap_or_default() + .get(&id.validator) + .cloned() + .unwrap_or_default() + .iter() + .for_each(|((redel_end_epoch, withdraw_epoch), inner)| { + let abs_map = abs_del_redel_unbonds + .entry(*redel_end_epoch) + .or_default() + .entry(*withdraw_epoch) + .or_default(); + for (src, bonds) in inner { + for (start, amount) in bonds { + abs_map + .entry(src.clone()) + .or_default() + .insert(*start, *amount); + } + } + }); + let conc_del_redel_unbonds = + crate::delegator_redelegated_unbonds_handle(&id.source) + .at(&id.validator) + .collect_map(&self.s) + .unwrap(); + assert_eq!(abs_del_redel_unbonds, conc_del_redel_unbonds); + } + fn check_init_validator_post_conditions( &self, submit_epoch: Epoch, @@ -823,7 +1348,7 @@ impl ConcretePosState { slash_type: SlashType, validator: &Address, ) { - println!( + tracing::debug!( "\nChecking misbehavior post conditions for validator: \n{}", validator ); @@ -831,13 +1356,6 @@ impl ConcretePosState { // 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) @@ -850,7 +1368,6 @@ impl ConcretePosState { .unwrap() .any(|res| { let (_, val_address) = res.unwrap(); - // dbg!(&val_address); val_address == validator.clone() }); assert!(!in_consensus); @@ -877,6 +1394,40 @@ impl ConcretePosState { // TODO: Any others? } + fn check_multistate_misbehavior_post_conditions( + &self, + ref_state: &AbstractPosState, + ) { + // Check the enqueued slashes + let abs_enqueued = ref_state.enqueued_slashes.clone(); + let mut conc_enqueued: BTreeMap>> = + BTreeMap::new(); + crate::enqueued_slashes_handle() + .get_data_handler() + .iter(&self.s) + .unwrap() + .for_each(|res| { + let ( + NestedSubKey::Data { + key: epoch, + nested_sub_key: + NestedSubKey::Data { + key: address, + nested_sub_key: _, + }, + }, + slash, + ) = res.unwrap(); + let slashes = conc_enqueued + .entry(epoch) + .or_default() + .entry(address) + .or_default(); + slashes.push(slash); + }); + assert_eq!(abs_enqueued, conc_enqueued); + } + fn check_unjail_validator_post_conditions( &self, params: &PosParams, @@ -950,58 +1501,299 @@ impl ConcretePosState { ); } - fn check_global_post_conditions( + fn check_multistate_redelegation_post_conditions( &self, - params: &PosParams, - current_epoch: Epoch, ref_state: &AbstractPosState, + delegator: &Address, + src_validator: &Address, + dest_validator: &Address, ) { - // 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, - ) { - tracing::debug!("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(); - tracing::debug!( - "Consensus val {}, stake: {} ({})", - &validator, - bonded_stake.to_string_native(), - deltas_stake.to_string_native(), - ); - assert!(!deltas_stake.is_negative()); - 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 src_id = BondId { + source: delegator.clone(), + validator: src_validator.clone(), + }; + let dest_id = BondId { + source: delegator.clone(), + validator: dest_validator.clone(), + }; - let state = crate::validator_state_handle(&validator) - .get(&self.s, epoch, params) - .unwrap(); + // Check the src bonds + let abs_src_bonds = + ref_state.bonds.get(&src_id).cloned().unwrap_or_default(); + let conc_src_bonds = crate::bond_handle(delegator, src_validator) + .get_data_handler() + .collect_map(&self.s) + .unwrap(); + assert_eq!(abs_src_bonds, conc_src_bonds); + + // Check the dest bonds + let abs_dest_bonds = + ref_state.bonds.get(&dest_id).cloned().unwrap_or_default(); + let conc_dest_bonds = crate::bond_handle(delegator, dest_validator) + .get_data_handler() + .collect_map(&self.s) + .unwrap(); + assert_eq!(abs_dest_bonds, conc_dest_bonds); - assert_eq!(state, Some(ValidatorState::Consensus)); + // Check the src total bonded + let abs_src_tot_bonded = ref_state + .total_bonded + .get(src_validator) + .cloned() + .unwrap_or_default(); + let conc_src_tot_bonded = crate::total_bonded_handle(src_validator) + .get_data_handler() + .collect_map(&self.s) + .unwrap(); + assert_eq!(abs_src_tot_bonded, conc_src_tot_bonded); + + // Check the dest total bonded + let abs_dest_tot_bonded = ref_state + .total_bonded + .get(dest_validator) + .cloned() + .unwrap_or_default(); + let conc_dest_tot_bonded = crate::total_bonded_handle(dest_validator) + .get_data_handler() + .collect_map(&self.s) + .unwrap(); + assert_eq!(abs_dest_tot_bonded, conc_dest_tot_bonded); + + // NOTE: Unbonds are not updated by redelegation + + // Check the src total_unbonded + let mut abs_src_total_unbonded = ref_state + .total_unbonded + .get(src_validator) + .cloned() + .unwrap_or_default(); + abs_src_total_unbonded.retain(|_, inner_map| { + inner_map.retain(|_, value| !value.is_zero()); + !inner_map.is_empty() + }); + let conc_src_total_unbonded = + crate::total_unbonded_handle(src_validator) + .collect_map(&self.s) + .unwrap(); + assert_eq!(abs_src_total_unbonded, conc_src_total_unbonded); + + // Check the delegator redelegated bonds to the src + let abs_del_redel_bonds_src = ref_state + .delegator_redelegated_bonded + .get(delegator) + .cloned() + .unwrap_or_default() + .get(src_validator) + .cloned() + .unwrap_or_default(); + let conc_del_redel_bonds_src = + crate::delegator_redelegated_bonds_handle(delegator) + .at(src_validator) + .collect_map(&self.s) + .unwrap(); + assert_eq!(abs_del_redel_bonds_src, conc_del_redel_bonds_src); + + // Check the delegator redelegated bonds to the dest + let abs_del_redel_bonds_dest = ref_state + .delegator_redelegated_bonded + .get(delegator) + .cloned() + .unwrap_or_default() + .get(dest_validator) + .cloned() + .unwrap_or_default(); + let conc_del_redel_bonds_dest = + crate::delegator_redelegated_bonds_handle(delegator) + .at(dest_validator) + .collect_map(&self.s) + .unwrap(); + assert_eq!(abs_del_redel_bonds_dest, conc_del_redel_bonds_dest); + + // NOTE: Delegator redelegated unbonds are not updated by redelegation + + // Check the src total redelegated bonded + let abs_src_total_redel_bonded = ref_state + .validator_total_redelegated_bonded + .get(src_validator) + .cloned() + .unwrap_or_default(); + let mut conc_src_total_redel_bonded: BTreeMap< + Epoch, + BTreeMap>, + > = BTreeMap::new(); + crate::validator_total_redelegated_bonded_handle(src_validator) + .iter(&self.s) + .unwrap() + .for_each(|res| { + let ( + NestedSubKey::Data { + key: redel_end_epoch, + nested_sub_key: + NestedSubKey::Data { + key: src_val, + nested_sub_key: SubKey::Data(bond_start), + }, + }, + amount, + ) = res.unwrap(); + conc_src_total_redel_bonded + .entry(redel_end_epoch) + .or_default() + .entry(src_val) + .or_default() + .insert(bond_start, amount); + }); + assert_eq!(abs_src_total_redel_bonded, conc_src_total_redel_bonded); + + // Check the dest total redelegated bonded + let abs_dest_total_redel_bonded = ref_state + .validator_total_redelegated_bonded + .get(dest_validator) + .cloned() + .unwrap_or_default(); + let mut conc_dest_total_redel_bonded: BTreeMap< + Epoch, + BTreeMap>, + > = BTreeMap::new(); + crate::validator_total_redelegated_bonded_handle(dest_validator) + .iter(&self.s) + .unwrap() + .for_each(|res| { + let ( + NestedSubKey::Data { + key: redel_end_epoch, + nested_sub_key: + NestedSubKey::Data { + key: src_val, + nested_sub_key: SubKey::Data(bond_start), + }, + }, + amount, + ) = res.unwrap(); + conc_dest_total_redel_bonded + .entry(redel_end_epoch) + .or_default() + .entry(src_val) + .or_default() + .insert(bond_start, amount); + }); + assert_eq!(abs_dest_total_redel_bonded, conc_dest_total_redel_bonded); + + // Check the src validator's total redelegated unbonded + let mut abs_src_total_redel_unbonded = ref_state + .validator_total_redelegated_unbonded + .get(src_validator) + .cloned() + .unwrap_or_default(); + abs_src_total_redel_unbonded.retain(|_, inner1| { + inner1.retain(|_, inner2| { + inner2.retain(|_, inner3| !inner3.is_empty()); + !inner2.is_empty() + }); + !inner1.is_empty() + }); + + let conc_src_total_redel_unbonded = + crate::validator_total_redelegated_unbonded_handle(src_validator) + .collect_map(&self.s) + .unwrap(); + assert_eq!(abs_src_total_redel_unbonded, conc_src_total_redel_unbonded); + + // Check the src validator's outgoing redelegations + let mut abs_src_outgoing: BTreeMap< + Address, + BTreeMap>, + > = BTreeMap::new(); + ref_state + .outgoing_redelegations + .get(src_validator) + .cloned() + .unwrap_or_default() + .iter() + .for_each(|(address, amounts)| { + for ((bond_start, redel_start), amount) in amounts { + abs_src_outgoing + .entry(address.clone()) + .or_default() + .entry(*bond_start) + .or_default() + .insert(*redel_start, *amount); + } + }); + let conc_src_outgoing = + crate::validator_outgoing_redelegations_handle(src_validator) + .collect_map(&self.s) + .unwrap(); + assert_eq!(abs_src_outgoing, conc_src_outgoing); + + // Check the dest validator's incoming redelegations + let abs_dest_incoming = ref_state + .incoming_redelegations + .get(dest_validator) + .cloned() + .unwrap_or_default(); + let conc_dest_incoming = + crate::validator_incoming_redelegations_handle(dest_validator) + .collect_map(&self.s) + .unwrap(); + assert_eq!(abs_dest_incoming, conc_dest_incoming); + } + + fn check_global_post_conditions( + &self, + params: &PosParams, + current_epoch: Epoch, + ref_state: &AbstractPosState, + ) { + for epoch in Epoch::iter_bounds_inclusive( + current_epoch, + current_epoch + params.pipeline_len, + ) { + tracing::debug!("Epoch {epoch}"); + let mut vals = HashSet::
::new(); + + // Consensus validators + 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(); + tracing::debug!( + "Consensus val {}, stake: {} ({})", + &validator, + bonded_stake.to_string_native(), + deltas_stake.to_string_native(), + ); + assert!(!deltas_stake.is_negative()); + + // Checks on stake + assert_eq!( + bonded_stake, + token::Amount::from_change(deltas_stake) + ); + assert_eq!( + bonded_stake, + ref_state + .validator_stakes + .get(&epoch) + .unwrap() + .get(&validator) + .cloned() + .unwrap() + ); + + // Checks on validator state + 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 @@ -1012,9 +1804,12 @@ impl ConcretePosState { .cloned() .unwrap() ); + assert!(!vals.contains(&validator)); vals.insert(validator); } + + // Below-capacity validators for WeightedValidator { bonded_stake, address: validator, @@ -1039,7 +1834,7 @@ impl ConcretePosState { token::Amount::from_change(deltas_stake) ); assert_eq!( - bonded_stake.change(), + bonded_stake, ref_state .validator_stakes .get(&epoch) @@ -1052,23 +1847,7 @@ impl ConcretePosState { 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(), @@ -1080,6 +1859,7 @@ impl ConcretePosState { .cloned() .unwrap() ); + assert!(!vals.contains(&validator)); vals.insert(validator); } @@ -1090,10 +1870,10 @@ impl ConcretePosState { ) .unwrap() { - let stake = validator_deltas_handle(&validator) - .get_sum(&self.s, epoch, params) - .unwrap() - .unwrap_or_default(); + let stake = crate::read_validator_stake( + &self.s, params, &validator, epoch, + ) + .unwrap(); tracing::debug!( "Below-thresh val {}, stake {}", &validator, @@ -1126,6 +1906,7 @@ impl ConcretePosState { .cloned() .unwrap() ); + assert!(!vals.contains(&validator)); vals.insert(validator); } @@ -1134,8 +1915,8 @@ impl ConcretePosState { let all_validators = crate::read_all_validator_addresses(&self.s, epoch).unwrap(); - for val in all_validators { - let state = validator_state_handle(&val) + for validator in all_validators { + let state = validator_state_handle(&validator) .get(&self.s, epoch, params) .unwrap() .unwrap(); @@ -1147,17 +1928,17 @@ impl ConcretePosState { .validator_states .get(&epoch) .unwrap() - .get(&val) + .get(&validator) .cloned() .unwrap() ); - let stake = validator_deltas_handle(&val) - .get_sum(&self.s, epoch, params) - .unwrap() - .unwrap_or_default(); + let stake = crate::read_validator_stake( + &self.s, params, &validator, epoch, + ) + .unwrap(); tracing::debug!( "Jailed val {}, stake {}", - &val, + &validator, stake.to_string_native() ); @@ -1167,7 +1948,7 @@ impl ConcretePosState { .validator_states .get(&epoch) .unwrap() - .get(&val) + .get(&validator) .cloned() .unwrap() ); @@ -1177,11 +1958,12 @@ impl ConcretePosState { .validator_stakes .get(&epoch) .unwrap() - .get(&val) + .get(&validator) .cloned() .unwrap() ); - assert!(!vals.contains(&val)); + + assert!(!vals.contains(&validator)); } } } @@ -1194,7 +1976,7 @@ impl ReferenceStateMachine for AbstractPosState { type Transition = Transition; fn init_state() -> BoxedStrategy { - println!("\nInitializing abstract state machine"); + tracing::debug!("\nInitializing abstract state machine"); arb_params_and_genesis_validators(Some(8), 8..10) .prop_map(|(params, genesis_validators)| { let epoch = Epoch::default(); @@ -1218,7 +2000,13 @@ impl ReferenceStateMachine for AbstractPosState { validator_slashes: Default::default(), enqueued_slashes: Default::default(), validator_last_slash_epochs: Default::default(), - unbond_records: Default::default(), + total_unbonded: Default::default(), + delegator_redelegated_bonded: Default::default(), + delegator_redelegated_unbonded: Default::default(), + validator_total_redelegated_bonded: Default::default(), + validator_total_redelegated_unbonded: Default::default(), + incoming_redelegations: Default::default(), + outgoing_redelegations: Default::default(), }; for GenesisValidator { @@ -1238,12 +2026,15 @@ impl ReferenceStateMachine for AbstractPosState { validator: address.clone(), }) .or_default(); - bonds.insert(epoch, token::Change::from(tokens)); + bonds.insert(epoch, tokens); + + let total_bonded = + state.total_bonded.entry(address.clone()).or_default(); + total_bonded.insert(epoch, tokens); let total_stakes = state.validator_stakes.entry(epoch).or_default(); - total_stakes - .insert(address.clone(), token::Change::from(tokens)); + total_stakes.insert(address.clone(), tokens); let consensus_set = state.consensus_set.entry(epoch).or_default(); @@ -1302,7 +2093,6 @@ impl ReferenceStateMachine for AbstractPosState { { state.copy_discrete_epoched_data(epoch) } - // dbg!(&state); state }) .boxed() @@ -1312,6 +2102,24 @@ impl ReferenceStateMachine for AbstractPosState { fn transitions(state: &Self::State) -> BoxedStrategy { // Let preconditions filter out what unbonds are not allowed let unbondable = state.bond_sums().into_iter().collect::>(); + let redelegatable = unbondable + .iter() + // Self-bonds cannot be redelegated + .filter(|(id, _)| id.source != id.validator) + .cloned() + .collect::>(); + + for (id, amt) in &redelegatable { + if *amt <= 0.into() { + tracing::debug!( + "Source: {}\nValidator: {}\nAmount: {}", + &id.source, + &id.validator, + amt.to_string_native() + ); + panic!("Should have no bonds with 0 amount or less!"); + } + } let withdrawable = state.withdrawable_unbonds().into_iter().collect::>(); @@ -1394,10 +2202,14 @@ impl ReferenceStateMachine for AbstractPosState { } else { let arb_unbondable = prop::sample::select(unbondable); let arb_unbond = - arb_unbondable.prop_flat_map(|(id, deltas_sum)| { - let deltas_sum = i128::try_from(deltas_sum).unwrap(); + arb_unbondable.prop_flat_map(move |(id, deltas_sum)| { + let deltas_sum = + i128::try_from(deltas_sum.change()).unwrap(); // Generate an amount to unbond, up to the sum - assert!(deltas_sum > 0); + assert!( + deltas_sum > 0, + "Bond {id} deltas_sum must be non-zero" + ); (0..deltas_sum).prop_map(move |to_unbond| { let id = id.clone(); let amount = @@ -1409,7 +2221,7 @@ impl ReferenceStateMachine for AbstractPosState { }; // Add withdrawals, if any - if withdrawable.is_empty() { + let transitions = if withdrawable.is_empty() { transitions } else { let arb_withdrawable = prop::sample::select(withdrawable); @@ -1417,6 +2229,63 @@ impl ReferenceStateMachine for AbstractPosState { .prop_map(|(id, _)| Transition::Withdraw { id }); prop_oneof![transitions, arb_withdrawal].boxed() + }; + + // Add redelegations, if any + if redelegatable.is_empty() { + transitions + } else { + let arb_redelegatable = prop::sample::select(redelegatable); + let validators = state + .validator_states + .get(&state.pipeline()) + .unwrap() + .keys() + .cloned() + .collect::>(); + let epoch = state.epoch; + let params = state.params.clone(); + let incoming_redelegations = state.incoming_redelegations.clone(); + let arb_redelegation = + arb_redelegatable.prop_flat_map(move |(id, deltas_sum)| { + let deltas_sum = + i128::try_from(deltas_sum.change()).unwrap(); + // Generate an amount to redelegate, up to the sum + assert!( + deltas_sum > 0, + "Bond {id} deltas_sum must be non-zero" + ); + let arb_amount = (0..deltas_sum).prop_map(|to_unbond| { + token::Amount::from_change(Change::from(to_unbond)) + }); + // Generate a new validator for redelegation + let current_validator = id.validator.clone(); + let new_validators = validators + .iter() + // The validator must be other than the current + .filter(|validator| *validator != ¤t_validator) + .cloned() + .collect::>(); + let arb_new_validator = + prop::sample::select(new_validators); + let params = params.clone(); + let incoming_redelegations = incoming_redelegations.clone(); + (arb_amount, arb_new_validator).prop_map( + move |(amount, new_validator)| Transition::Redelegate { + is_chained: Self::is_chained_redelegation( + epoch, + ¶ms, + &incoming_redelegations, + &id.source, + &id.validator, + ), + id: id.clone(), + new_validator, + amount, + }, + ) + }); + prop_oneof![transitions, arb_redelegation].boxed() } } @@ -1426,7 +2295,7 @@ impl ReferenceStateMachine for AbstractPosState { ) -> Self::State { match transition { Transition::NextEpoch => { - println!("\nABSTRACT Next Epoch"); + tracing::debug!("\nABSTRACT Next Epoch"); state.epoch = state.epoch.next(); @@ -1447,9 +2316,10 @@ impl ReferenceStateMachine for AbstractPosState { commission_rate: _, max_commission_rate_change: _, } => { - println!( + tracing::debug!( "\nABSTRACT Init Validator {} in epoch {}", - address, state.epoch + address, + state.epoch ); let pipeline: Epoch = state.pipeline(); @@ -1458,7 +2328,7 @@ impl ReferenceStateMachine for AbstractPosState { .validator_stakes .entry(pipeline) .or_default() - .insert(address.clone(), 0_i128.into()); + .insert(address.clone(), token::Amount::zero()); // Insert into the below-threshold set at pipeline since the // initial stake is 0 @@ -1476,14 +2346,13 @@ impl ReferenceStateMachine for AbstractPosState { state.debug_validators(); } Transition::Bond { id, amount } => { - println!( + tracing::debug!( "\nABSTRACT Bond {} tokens, id = {}", amount.to_string_native(), id ); - if *amount != token::Amount::default() { - let change = token::Change::from(*amount); + if !amount.is_zero() { let pipeline_state = state .validator_states .get(&state.pipeline()) @@ -1493,54 +2362,95 @@ impl ReferenceStateMachine for AbstractPosState { // Validator sets need to be updated first!! if *pipeline_state != ValidatorState::Jailed { - state.update_validator_sets(&id.validator, change); + state.update_validator_sets( + state.pipeline(), + &id.validator, + amount.change(), + ); } - state.update_bond(id, change); - state.update_validator_total_stake(&id.validator, change); + state.update_bond(id, *amount); + state.update_validator_total_stake( + &id.validator, + amount.change(), + ); } state.debug_validators(); } Transition::Unbond { id, amount } => { - println!( + tracing::debug!( "\nABSTRACT Unbond {} tokens, id = {}", amount.to_string_native(), id ); - if *amount != token::Amount::default() { - let change = token::Change::from(*amount); - state.update_state_with_unbond(id, change); + // `totalBonded` + let sum_bonded = state + .bonds + .get(id) + .map(|a| { + a.iter() + .fold(token::Amount::zero(), |acc, (_, amount)| { + acc + *amount + }) + }) + .unwrap_or_default(); - // 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; + if !amount.is_zero() && *amount <= sum_bonded { + state.update_state_with_unbond(id, *amount); } state.debug_validators(); } Transition::Withdraw { id } => { - println!("\nABSTRACT Withdraw, id = {}", id); + tracing::debug!("\nABSTRACT Withdraw, id = {}", id); + + let redel_unbonds = state + .delegator_redelegated_unbonded + .entry(id.source.clone()) + .or_default() + .entry(id.validator.clone()) + .or_default(); // Remove all withdrawable unbonds with this bond ID - for (epoch, unbonds) in state.unbonds.iter_mut() { - if *epoch <= state.epoch { + for ((start_epoch, withdraw_epoch), unbonds) in + state.unbonds.iter_mut() + { + if *withdraw_epoch <= state.epoch { unbonds.remove(id); + redel_unbonds.remove(&(*start_epoch, *withdraw_epoch)); } } // Remove any epochs that have no unbonds left - state.unbonds.retain(|_epoch, unbonds| !unbonds.is_empty()); + state.unbonds.retain(|_epochs, unbonds| !unbonds.is_empty()); + + // Remove the redel unbonds if empty now + redel_unbonds.retain(|_epochs, unbonds| !unbonds.is_empty()); // TODO: should we do anything here for slashing? } + Transition::Redelegate { + is_chained, + id, + new_validator, + amount, + } => { + tracing::debug!( + "\nABSTRACT Redelegation, id = {id}, new validator = \ + {new_validator}, amount = {}, is_chained = {is_chained}", + amount.to_string_native(), + ); + if *is_chained { + return state; + } + if !amount.is_zero() { + // Remove the amount from source validator + state.update_state_with_redelegation( + id, + new_validator, + *amount, + ); + } + state.debug_validators(); + } Transition::Misbehavior { address, slash_type, @@ -1548,10 +2458,12 @@ impl ReferenceStateMachine for AbstractPosState { height, } => { let current_epoch = state.epoch; - println!( + tracing::debug!( "\nABSTRACT Misbehavior in epoch {} by validator {}, \ found in epoch {}", - infraction_epoch, address, current_epoch + infraction_epoch, + address, + current_epoch ); let processing_epoch = *infraction_epoch @@ -1580,15 +2492,13 @@ impl ReferenceStateMachine for AbstractPosState { // 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(), - ); + let real_stake = 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) @@ -1719,7 +2629,7 @@ impl ReferenceStateMachine for AbstractPosState { Transition::UnjailValidator { address } => { let pipeline_epoch = state.pipeline(); - println!( + tracing::debug!( "\nABSTRACT Unjail validator {} starting in epoch {}", address.clone(), pipeline_epoch @@ -1745,9 +2655,7 @@ impl ReferenceStateMachine for AbstractPosState { sum + validators.len() as u64 }); - if pipeline_stake - < state.params.validator_stake_threshold.change() - { + if pipeline_stake < state.params.validator_stake_threshold { // Place into the below-threshold set let below_threshold_set_pipeline = state .below_threshold_set @@ -1768,7 +2676,7 @@ impl ReferenceStateMachine for AbstractPosState { .is_empty() ); consensus_set_pipeline - .entry(token::Amount::from_change(pipeline_stake)) + .entry(pipeline_stake) .or_default() .push_back(address.clone()); validator_states_pipeline @@ -1782,7 +2690,7 @@ impl ReferenceStateMachine for AbstractPosState { .or_default(); let min_consensus_stake = *min_consensus.key(); - if pipeline_stake > min_consensus_stake.change() { + if pipeline_stake > min_consensus_stake { // Place into the consensus set and demote the last // min_consensus validator let min_validators = min_consensus.get_mut(); @@ -1800,7 +2708,7 @@ impl ReferenceStateMachine for AbstractPosState { .insert(last_val, ValidatorState::BelowCapacity); consensus_set_pipeline - .entry(token::Amount::from_change(pipeline_stake)) + .entry(pipeline_stake) .or_default() .push_back(address.clone()); validator_states_pipeline @@ -1808,10 +2716,7 @@ impl ReferenceStateMachine for AbstractPosState { } else { // Just place into the below-capacity set below_capacity_set_pipeline - .entry( - token::Amount::from_change(pipeline_stake) - .into(), - ) + .entry(pipeline_stake.into()) .or_default() .push_back(address.clone()); validator_states_pipeline.insert( @@ -1867,7 +2772,7 @@ impl ReferenceStateMachine for AbstractPosState { let is_unbondable = state .bond_sums() .get(id) - .map(|sum| *sum >= token::Change::from(*amount)) + .map(|sum| *sum >= *amount) .unwrap_or_default(); // The validator must not be frozen currently @@ -1883,13 +2788,6 @@ impl ReferenceStateMachine for AbstractPosState { 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 and the validator not jailed @@ -1901,7 +2799,7 @@ impl ReferenceStateMachine for AbstractPosState { let is_withdrawable = state .withdrawable_unbonds() .get(id) - .map(|amount| *amount >= token::Amount::default()) + .map(|amount| *amount >= token::Amount::zero()) .unwrap_or_default(); // The validator must not be jailed currently @@ -1918,6 +2816,71 @@ impl ReferenceStateMachine for AbstractPosState { // The amount must be available to unbond && is_withdrawable && !is_jailed } + Transition::Redelegate { + is_chained, + id, + new_validator, + amount, + } => { + let pipeline = state.pipeline(); + + if *is_chained { + Self::is_chained_redelegation( + state.epoch, + &state.params, + &state.incoming_redelegations, + &id.source, + new_validator, + ) + } else { + // The src and dest validator must be known + if !state.is_validator(&id.validator, pipeline) + || !state.is_validator(new_validator, pipeline) + { + return false; + } + + // The amount must be available to redelegate + if !state + .bond_sums() + .get(id) + .map(|sum| *sum >= *amount) + .unwrap_or_default() + { + return false; + } + + // The src validator must not be frozen + if let Some(last_epoch) = + state.validator_last_slash_epochs.get(&id.validator) + { + if *last_epoch + + state.params.unbonding_len + + 1u64 + + state.params.cubic_slashing_window_length + > state.epoch + { + return false; + } + } + + // The dest validator must not be frozen + if let Some(last_epoch) = + state.validator_last_slash_epochs.get(new_validator) + { + if *last_epoch + + state.params.unbonding_len + + 1u64 + + state.params.cubic_slashing_window_length + > state.epoch + { + return false; + } + } + + true + } + } Transition::Misbehavior { address, slash_type: _, @@ -1935,27 +2898,43 @@ impl ReferenceStateMachine for AbstractPosState { <= 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 + // not jailed or about to be slashed, so there's always at least + // one honest left let enough_honest_validators = || { - state + let num_of_honest = state .validator_states .get(&state.pipeline()) .unwrap() .iter() .filter(|(_addr, val_state)| match val_state { ValidatorState::Consensus - | ValidatorState::BelowCapacity - | ValidatorState::BelowThreshold => true, + | ValidatorState::BelowCapacity => true, ValidatorState::Inactive - | ValidatorState::Jailed => false, + | ValidatorState::Jailed + // Below threshold cannot be in consensus + | ValidatorState::BelowThreshold => false, + }) + .count(); + + // Find the number of enqueued slashes to unique validators + let num_of_enquequed_slashes = state + .enqueued_slashes + .iter() + // find all validators with any enqueued slashes + .fold(BTreeSet::new(), |mut acc, (&epoch, slashes)| { + if epoch > current_epoch { + acc.extend(slashes.keys().cloned()); + } + acc }) - .count() - > 3 + .len(); + + num_of_honest - num_of_enquequed_slashes > 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); + // tracing::debug!("\nVal to possibly misbehave: {}", &address); let state_at_infraction = state .validator_states .get(infraction_epoch) @@ -2060,7 +3039,7 @@ impl AbstractPosState { } /// Update a bond with bonded or unbonded change at the pipeline epoch - fn update_bond(&mut self, id: &BondId, change: token::Change) { + fn update_bond(&mut self, id: &BondId, change: token::Amount) { let pipeline_epoch = self.pipeline(); let bonds = self.bonds.entry(id.clone()).or_default(); let bond = bonds.entry(pipeline_epoch).or_default(); @@ -2079,32 +3058,59 @@ impl AbstractPosState { *total_bonded += change; } - fn update_state_with_unbond(&mut self, id: &BondId, change: token::Change) { + fn update_state_with_unbond(&mut self, id: &BondId, change: token::Amount) { + self.unbond_tokens(id, change, false); + } + + fn unbond_tokens( + &mut self, + id: &BondId, + change: token::Amount, + is_redelegation: bool, + ) -> ResultSlashing { + // TODO: check in here too that the amount is less or equal to bond sum + 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 + + let total_bonded = + self.total_bonded.entry(id.validator.clone()).or_default(); + let total_unbonded = self + .total_unbonded .entry(id.validator.clone()) .or_default() .entry(pipeline_epoch) .or_default(); - let unbonds = self - .unbonds - .entry(withdraw_epoch) + + let delegator_redelegated_bonds = self + .delegator_redelegated_bonded + .entry(id.source.clone()) .or_default() - .entry(id.clone()) + .entry(id.validator.clone()) + .or_default(); + let delegator_redelegated_unbonds = self + .delegator_redelegated_unbonded + .entry(id.source.clone()) + .or_default() + .entry(id.validator.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(); + + let validator_total_redelegated_bonded = self + .validator_total_redelegated_bonded + .entry(id.validator.clone()) + .or_default(); + let validator_total_redelegated_unbonded = self + .validator_total_redelegated_unbonded + .entry(id.validator.clone()) + .or_default() + .entry(pipeline_epoch) + .or_default(); + + let validator_slashes = &self.validator_slashes; tracing::debug!("Bonds before decrementing"); for (start, amnt) in bonds.iter() { @@ -2115,52 +3121,79 @@ impl AbstractPosState { ); } - for (bond_epoch, bond_amnt) in bonds.iter_mut().rev() { - tracing::debug!("remaining {}", remaining.to_string_native()); - tracing::debug!( - "Bond epoch {} - amnt {}", - bond_epoch, - bond_amnt.to_string_native() - ); - let to_unbond = cmp::min(*bond_amnt, remaining); - tracing::debug!( - "to_unbond (init) = {}", - to_unbond.to_string_native() - ); - *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 - }); - tracing::debug!( - "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, - self.params.cubic_slashing_window_length, - ) - .change(); - tracing::debug!( - "Cur amnt after slashing = {}", - &amount_after_slashing.to_string_native() - ); + // `resultUnbonding` + // Get the bonds for removal + let bonds_to_remove = Self::find_bonds_to_remove(bonds, change); + + // `modifiedRedelegation` + // Modified redelegation + // The unbond may need to partially unbond redelegated tokens, so + // compute if necessary + let modified_redelegation = match bonds_to_remove.new_entry { + Some((bond_epoch, new_bond_amount)) => { + if delegator_redelegated_bonds.contains_key(&bond_epoch) { + let cur_bond_amount = + bonds.get(&bond_epoch).cloned().unwrap_or_default(); + Self::compute_modified_redelegation( + delegator_redelegated_bonds, + bond_epoch, + cur_bond_amount - new_bond_amount, + ) + } else { + ModifiedRedelegation::default() + } + } + None => ModifiedRedelegation::default(), + }; - let amt = unbond_records.entry(*bond_epoch).or_default(); - *amt += token::Amount::from_change(to_unbond); + // `keysUnbonds` + // New unbonds. This will be needed for a couple things + let unbonded_bond_starts = + if let Some((start_epoch, _)) = bonds_to_remove.new_entry { + let mut to_remove = bonds_to_remove.epochs.clone(); + to_remove.insert(start_epoch); + to_remove + } else { + bonds_to_remove.epochs.clone() + }; + // `newUnbonds` + let new_unbonds = unbonded_bond_starts + .into_iter() + .map(|start| { + let cur_bond_amnt = bonds.get(&start).cloned().unwrap(); + let new_value = if let Some((start_epoch, new_bond_amount)) = + bonds_to_remove.new_entry + { + if start_epoch == start { + cur_bond_amnt - new_bond_amount + } else { + cur_bond_amnt + } + } else { + cur_bond_amnt + }; + ((start, withdraw_epoch), new_value) + }) + .collect::>(); - remaining -= to_unbond; - if remaining.is_zero() { - break; + // Update the bonds and unbonds in the AbstractState + // `updatedBonded` + updates to `updatedDelegator` + for bond_epoch in &bonds_to_remove.epochs { + bonds.remove(bond_epoch); + } + if let Some((bond_epoch, new_bond_amt)) = bonds_to_remove.new_entry { + bonds.insert(bond_epoch, new_bond_amt); + } + // `updatedUnbonded` + updates to `updatedDelegator` + if !is_redelegation { + for (epoch_pair, amount) in &new_unbonds { + let unbonds = self + .unbonds + .entry(*epoch_pair) + .or_default() + .entry(id.clone()) + .or_default(); + *unbonds += *amount; } } @@ -2173,27 +3206,323 @@ impl AbstractPosState { ); } + // `newRedelegatedUnbonds` + // Compute new redelegated unbonds (which requires unmodified + // redelegated bonds) + let new_redelegated_unbonds = Self::compute_new_redelegated_unbonds( + delegator_redelegated_bonds, + &bonds_to_remove.epochs, + &modified_redelegation, + ); + + // `updatedRedelegatedBonded` + // Update the delegator's redelegated bonds in the state + for epoch_to_remove in &bonds_to_remove.epochs { + delegator_redelegated_bonds.remove(epoch_to_remove); + } + if let Some(epoch) = modified_redelegation.epoch { + if modified_redelegation.validators_to_remove.is_empty() { + delegator_redelegated_bonds.remove(&epoch); + } else { + let rbonds = + delegator_redelegated_bonds.entry(epoch).or_default(); + + if let Some(val_to_modify) = + &modified_redelegation.validator_to_modify + { + let mut updated_vals_to_remove = + modified_redelegation.validators_to_remove.clone(); + updated_vals_to_remove.remove(val_to_modify); + + // Remove the updated_vals_to_remove keys from the + // redelegated_bonds map first + for val in &updated_vals_to_remove { + rbonds.remove(val); + } + + if let Some(epoch_to_modify) = + modified_redelegation.epoch_to_modify + { + let mut updated_epochs_to_remove = + modified_redelegation.epochs_to_remove.clone(); + updated_epochs_to_remove.remove(&epoch_to_modify); + let val_bonds_to_modify = + rbonds.entry(val_to_modify.clone()).or_default(); + for epoch in updated_epochs_to_remove { + val_bonds_to_modify.remove(&epoch); + } + val_bonds_to_modify.insert( + epoch_to_modify, + modified_redelegation.new_amount.unwrap(), + ); + } else { + // Then remove to epochs_to_remove from the redelegated + // bonds of the val_to_modify + let val_bonds_to_modify = + rbonds.entry(val_to_modify.clone()).or_default(); + for epoch in &modified_redelegation.epochs_to_remove { + val_bonds_to_modify.remove(epoch); + } + } + } else { + // Remove all validators in + // modified_redelegation.validators_to_remove + // from redelegated_bonds + for val in &modified_redelegation.validators_to_remove { + rbonds.remove(val); + } + } + } + } + + // `updatedRedelegatedUnbonded + if !is_redelegation { + // Get all the epoch pairs that should exist in the state now + let new_unbond_epoch_pairs = new_redelegated_unbonds + .keys() + .map(|start_epoch| (*start_epoch, withdraw_epoch)) + .collect::>(); + + // Update the state for delegator's redelegated unbonds now + // NOTE: can maybe do this by only looking at those inside the new + // epoch pairs? + for unbond_pair in new_unbond_epoch_pairs { + for (src_val, redel_unbonds) in + new_redelegated_unbonds.get(&unbond_pair.0).unwrap() + { + for (src_start, unbonded) in redel_unbonds { + let existing_unbonded = delegator_redelegated_unbonds + .entry(unbond_pair) + .or_default() + .entry(src_val.clone()) + .or_default() + .entry(*src_start) + .or_default(); + *existing_unbonded += *unbonded; + } + } + } + } + + // `updatedTotalBonded` and `updatedTotalUnbonded` + // Update the validator's total bonded and total unbonded + for ((start_epoch, _), unbonded) in &new_unbonds { + let cur_total_bonded = + total_bonded.entry(*start_epoch).or_default(); + *cur_total_bonded -= *unbonded; + let cur_total_unbonded = + total_unbonded.entry(*start_epoch).or_default(); + *cur_total_unbonded += *unbonded; + } + + // `updatedTotalRedelegatedBonded` and `updatedTotalRedelegatedUnbonded` + // Update the validator's total redelegated bonded and unbonded + for (dest_start, r_unbonds) in &new_redelegated_unbonds { + for (src_val, changes) in r_unbonds { + for (bond_start, change) in changes { + let cur_total_bonded = validator_total_redelegated_bonded + .entry(*dest_start) + .or_default() + .entry(src_val.clone()) + .or_default() + .entry(*bond_start) + .or_default(); + *cur_total_bonded -= *change; + + let cur_total_unbonded = + validator_total_redelegated_unbonded + .entry(*dest_start) + .or_default() + .entry(src_val.clone()) + .or_default() + .entry(*bond_start) + .or_default(); + *cur_total_unbonded += *change; + } + } + } + + // `resultSlashing` + // Get the slashed amount of the unbond now + let result_slashing = Self::compute_amount_after_slashing_unbond( + &self.params, + validator_slashes, + &id.validator, + &new_unbonds, + &new_redelegated_unbonds, + ); + // `amountAfterSlashing` + let amount_after_slashing = result_slashing.sum.change(); + 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(); - // let token_change = cmp::min(*pipeline_stake, amount_after_slashing); if *pipeline_state != ValidatorState::Jailed { - self.update_validator_sets(&id.validator, -amount_after_slashing); + self.update_validator_sets( + self.pipeline(), + &id.validator, + -amount_after_slashing, + ); } self.update_validator_total_stake( &id.validator, -amount_after_slashing, ); + + result_slashing + } + + fn update_state_with_redelegation( + &mut self, + id: &BondId, + new_validator: &Address, + change: token::Amount, + ) { + // First need to unbond the redelegated tokens + // NOTE: same logic as unbond transition but with some things left out + let pipeline_epoch = self.pipeline(); + + // `resultUnbond` + let result_unbond = self.unbond_tokens(id, change, true); + + // `amountAfterSlashing` + let amount_after_slashing = result_unbond.sum; + + // `updatedRedelegatedBonds` + // Update the delegator's redelegated bonded + let delegator_redelegated_bonded = self + .delegator_redelegated_bonded + .entry(id.source.clone()) + .or_default() + .entry(new_validator.clone()) + .or_default() + .entry(pipeline_epoch) + .or_default() + .entry(id.validator.clone()) + .or_default(); + for (start_epoch, bonded) in &result_unbond.epoch_map { + *delegator_redelegated_bonded + .entry(*start_epoch) + .or_default() += *bonded; + } + + if tracing::level_enabled!(tracing::Level::DEBUG) { + let bonds = self + .bonds + .get(&BondId { + source: id.source.clone(), + validator: new_validator.clone(), + }) + .cloned() + .unwrap_or_default(); + tracing::debug!( + "\nRedeleg dest bonds before incrementing: {bonds:#?}" + ); + } + + if !amount_after_slashing.is_zero() { + // `updatedDelegator` --> `with("bonded")` + // Update the delegator's bonds + let bonds = self + .bonds + .entry(BondId { + source: id.source.clone(), + validator: new_validator.clone(), + }) + .or_default(); + *bonds.entry(pipeline_epoch).or_default() += amount_after_slashing; + + // `updatedDestValidator` --> `with("totalBonded")` + // Update the dest validator's total bonded + let dest_total_bonded = self + .total_bonded + .entry(new_validator.clone()) + .or_default() + .entry(pipeline_epoch) + .or_default(); + *dest_total_bonded += amount_after_slashing; + } + + if tracing::level_enabled!(tracing::Level::DEBUG) { + let bonds = self + .bonds + .get(&BondId { + source: id.source.clone(), + validator: new_validator.clone(), + }) + .cloned() + .unwrap_or_default(); + tracing::debug!( + "\nRedeleg dest bonds after incrementing: {bonds:#?}" + ); + } + + // `updatedOutgoingRedelegations` and `updatedSrcValidator` + // Update the src validator's outgoing redelegations + let outgoing_redelegations = self + .outgoing_redelegations + .entry(id.validator.clone()) + .or_default() + .entry(new_validator.clone()) + .or_default(); + for (start_epoch, bonded) in &result_unbond.epoch_map { + let cur_outgoing = outgoing_redelegations + .entry((*start_epoch, self.epoch)) + .or_default(); + *cur_outgoing += *bonded; + } + + // `updatedDestValidator` --> `with("totalRedelegatedBonded")` + // Update the dest validator's total redelegated bonded + let dest_total_redelegated_bonded = self + .validator_total_redelegated_bonded + .entry(new_validator.clone()) + .or_default() + .entry(pipeline_epoch) + .or_default() + .entry(id.validator.clone()) + .or_default(); + for (start_epoch, bonded) in &result_unbond.epoch_map { + let cur_tot_bonded = dest_total_redelegated_bonded + .entry(*start_epoch) + .or_default(); + *cur_tot_bonded += *bonded; + } + + // `updatedDestValidator` --> `with("incomingRedelegations")` + // Update the dest validator's incoming redelegations + let incoming_redelegations = self + .incoming_redelegations + .entry(new_validator.clone()) + .or_default(); + incoming_redelegations.insert(id.source.clone(), pipeline_epoch); + + // `updatedDestValidator` --> `with("stake")` + // Update validator set and stake + let pipeline_state = self + .validator_states + .get(&self.pipeline()) + .unwrap() + .get(new_validator) + .unwrap(); + + if !amount_after_slashing.is_zero() { + if *pipeline_state != ValidatorState::Jailed { + self.update_validator_sets( + self.pipeline(), + new_validator, + amount_after_slashing.change(), + ); + } + self.update_validator_total_stake( + new_validator, + amount_after_slashing.change(), + ); + } } /// Update validator's total stake with bonded or unbonded change at the @@ -2209,32 +3538,39 @@ impl AbstractPosState { .or_default() .entry(validator.clone()) .or_default(); - *total_stakes += change; + *total_stakes = token::Amount::from(total_stakes.change() + change); } /// Update validator in sets with bonded or unbonded change fn update_validator_sets( &mut self, + epoch: Epoch, validator: &Address, change: token::Change, ) { - let pipeline = self.pipeline(); - let consensus_set = self.consensus_set.entry(pipeline).or_default(); - let below_cap_set = - self.below_capacity_set.entry(pipeline).or_default(); + tracing::debug!( + "\nUpdating set for validator {} in epoch {} with amount {}\n", + validator, + epoch, + change + ); + if change.is_zero() { + return; + } + // let pipeline = self.pipeline(); + let consensus_set = self.consensus_set.entry(epoch).or_default(); + let below_cap_set = self.below_capacity_set.entry(epoch).or_default(); let below_thresh_set = - self.below_threshold_set.entry(pipeline).or_default(); + self.below_threshold_set.entry(epoch).or_default(); - let validator_stakes = self.validator_stakes.get(&pipeline).unwrap(); - let validator_states = - self.validator_states.get_mut(&pipeline).unwrap(); + let validator_stakes = self.validator_stakes.get(&epoch).unwrap(); + let validator_states = self.validator_states.get_mut(&epoch).unwrap(); let state_pre = validator_states.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(this_val_stake_pre); + token::Amount::from_change(this_val_stake_pre.change() + change); let threshold = self.params.validator_stake_threshold; if this_val_stake_pre < threshold && this_val_stake_post < threshold { @@ -2246,12 +3582,9 @@ impl AbstractPosState { match state_pre { 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); @@ -2290,7 +3623,7 @@ impl AbstractPosState { // If unbonding, check the max below-cap validator's state if we // need to do a swap - if change < token::Change::default() { + if change < token::Change::zero() { if let Some(mut max_below_cap) = below_cap_set.last_entry() { let max_below_cap_stake = *max_below_cap.key(); @@ -2333,7 +3666,7 @@ impl AbstractPosState { .push_back(validator.clone()); } ValidatorState::BelowCapacity => { - // println!("Validator initially in below-cap"); + // tracing::debug!("Validator initially in below-cap"); // Remove from the prior stake let vals = @@ -2356,11 +3689,9 @@ impl AbstractPosState { // If bonding, check the min consensus validator's state if we // need to do a swap - if change >= token::Change::default() { - // dbg!(&consensus_set); + if change >= token::Change::zero() { if let Some(mut min_consensus) = consensus_set.first_entry() { - // 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 @@ -2423,7 +3754,6 @@ impl AbstractPosState { } // Determine which set to place the validator into if let Some(mut min_consensus) = consensus_set.first_entry() { - // 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 @@ -2478,288 +3808,632 @@ impl AbstractPosState { .get(&self.epoch) .cloned() .unwrap_or_default(); - if !slashes_this_epoch.is_empty() { - let infraction_epoch = self.epoch - - self.params.unbonding_len - - 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(); - for (validator, slashes) in slashes_this_epoch { - let stake_at_infraction = self - .validator_stakes - .get(&infraction_epoch) - .unwrap() - .get(&validator) - .cloned() - .unwrap_or_default(); - tracing::debug!( - "Val {} stake at infraction {}", + + if slashes_this_epoch.is_empty() { + return; + } + + let infraction_epoch = + self.epoch - self.params.slash_processing_epoch_offset(); + let cubic_rate = self.cubic_slash_rate(); + + // Get effective slash rate per validator and update the slashes in the + // Abstract state + let slash_rates = slashes_this_epoch.iter().fold( + BTreeMap::::new(), + |mut acc, (validator, slashes)| { + let mut tot_rate = + acc.get(validator).cloned().unwrap_or_default(); + 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, + ); + tot_rate = cmp::min(Dec::one(), tot_rate + rate); + } + acc.insert(validator.clone(), tot_rate); + acc + }, + ); + + let mut map_validator_slash: EagerRedelegatedBondsMap = BTreeMap::new(); + for (validator, rate) in slash_rates { + self.process_validator_slash( + &validator, + rate, + &mut map_validator_slash, + ); + } + tracing::debug!( + "Slashed amounts for validators: {map_validator_slash:#?}" + ); + + for (validator, slash_amounts) in map_validator_slash { + for (update_epoch, delta) in slash_amounts { + let state = self + .validator_states + .get(&update_epoch) + .unwrap() + .get(&validator) + .unwrap(); + if *state != ValidatorState::Jailed { + self.update_validator_sets( + update_epoch, + &validator, + -delta.change(), + ); + } + + let stake = self + .validator_stakes + .entry(update_epoch) + .or_default() + .entry(validator.clone()) + .or_default(); + *stake -= delta; + } + + let next_state = self + .validator_states + .get(&self.epoch.next()) + .unwrap() + .get(&validator) + .cloned() + .unwrap(); + + let pipeline_state = self + .validator_states + .get(&self.pipeline()) + .unwrap() + .get(&validator) + .cloned() + .unwrap(); + + debug_assert_eq!(next_state, pipeline_state); + } + + // Update the slashes in the Abstract state ONLY AFTER processing them + for (validator, slashes) in slashes_this_epoch { + let cur_slashes = + self.validator_slashes.entry(validator.clone()).or_default(); + + for slash in slashes { + let rate = cmp::max( + slash.r#type.get_slash_rate(&self.params), + cubic_rate, + ); + cur_slashes.push(Slash { + epoch: slash.epoch, + block_height: Default::default(), + r#type: SlashType::DuplicateVote, + rate, + }); + } + } + } + + fn process_validator_slash( + &mut self, + validator: &Address, + slash_rate: Dec, + val_slash_amounts: &mut EagerRedelegatedBondsMap, + ) { + let slash_amounts = val_slash_amounts + .get(validator) + .cloned() + .unwrap_or_default(); + let result_slash = + self.slash_validator(validator, slash_rate, &slash_amounts); + + // `updatedSlashedAmountMap` + let validator_slashes = + val_slash_amounts.entry(validator.clone()).or_default(); + for (epoch, slash) in result_slash { + *validator_slashes.entry(epoch).or_default() += slash; + } + + let dest_validators = self + .outgoing_redelegations + .get(validator) + .cloned() + .unwrap_or_default() + .keys() + .cloned() + .collect::>(); + + for dest_val in dest_validators { + let to_modify = + val_slash_amounts.entry(dest_val.clone()).or_default(); + + tracing::debug!( + "Slashing {} redelegation to {}", + validator, + &dest_val + ); + + // `slashValidatorRedelegation` + self.slash_validator_redelegation( + validator, &dest_val, slash_rate, to_modify, + ); + + if to_modify.is_empty() { + val_slash_amounts.remove(&dest_val); + }; + } + } + + fn slash_validator( + &self, + validator: &Address, + slash_rate: Dec, + val_slash_amounts: &BTreeMap, + ) -> BTreeMap { + tracing::debug!( + "Slashing validator {} at rate {}", + validator, + slash_rate + ); + + let infraction_epoch = + self.epoch - self.params.slash_processing_epoch_offset(); + + let total_unbonded = self + .total_unbonded + .get(validator) + .cloned() + .unwrap_or_default(); + let total_redelegated_unbonded = self + .validator_total_redelegated_unbonded + .get(validator) + .cloned() + .unwrap_or_default(); + + // `val bonds` + let mut total_bonded = self + .total_bonded + .get(validator) + .cloned() + .unwrap_or_default() + .into_iter() + .filter(|&(epoch, _amount)| epoch <= infraction_epoch) + .collect::>(); + + // `val redelegatedBonds` + let mut total_redelegated_bonded = total_bonded + .keys() + .filter(|&epoch| { + self.validator_total_redelegated_bonded + .get(validator) + .cloned() + .unwrap_or_default() + .contains_key(epoch) + }) + .map(|epoch| { + ( + *epoch, + self.validator_total_redelegated_bonded + .get(validator) + .unwrap() + .get(epoch) + .cloned() + .unwrap(), + ) + }) + .collect::>(); + + let mut slashed_amounts = val_slash_amounts.clone(); + let mut sum = token::Amount::zero(); + + let eps = self + .epoch + .iter_range(self.params.pipeline_len) + .collect::>(); + for epoch in eps.into_iter().rev() { + let amount = total_bonded.iter().fold( + token::Amount::zero(), + |acc, (bond_start, bond_amount)| { + let redel_bonds = total_redelegated_bonded + .get(bond_start) + .cloned() + .unwrap_or_default(); + acc + self.compute_slash_bond_at_epoch( + epoch, + infraction_epoch, + *bond_start, + *bond_amount, + &redel_bonds, + slash_rate, + validator, + ) + }, + ); + + let new_bonds = total_unbonded + .get(&epoch) + .cloned() + .unwrap_or_default() + .into_iter() + .filter(|(ep, _)| *ep <= infraction_epoch) + .collect::>(); + + let new_redelegated_bonds = new_bonds + .keys() + .filter(|&ep| { + total_redelegated_unbonded + .get(&epoch) + .cloned() + .unwrap_or_default() + .contains_key(ep) + }) + .map(|ep| { + ( + *ep, + total_redelegated_unbonded + .get(&epoch) + .unwrap() + .get(ep) + .cloned() + .unwrap(), + ) + }) + .collect::>(); + + total_bonded = new_bonds; + total_redelegated_bonded = new_redelegated_bonds; + sum += amount; + + let cur = slashed_amounts.entry(epoch).or_default(); + *cur += sum; + } + // Hack - should this be done differently? (think this is safe) + let last_amt = slashed_amounts + .get(&self.pipeline().prev()) + .cloned() + .unwrap(); + slashed_amounts.insert(self.pipeline(), last_amt); + + slashed_amounts + } + + fn fold_and_slash_redelegated_bonds( + &self, + redel_bonds: &BTreeMap>, + start: Epoch, + list_slashes: &[Slash], + slash_epoch_filter: impl Fn(Epoch) -> bool, + ) -> FoldRedelegatedBondsResult { + let mut result = FoldRedelegatedBondsResult::default(); + for (src_validator, bonds) in redel_bonds { + for (bond_start, bonded) in bonds { + let src_slashes = self + .validator_slashes + .get(src_validator) + .cloned() + .unwrap_or_default() + .iter() + .filter(|&s| { + self.params.in_redelegation_slashing_window( + s.epoch, + self.params + .redelegation_start_epoch_from_end(start), + start, + ) && *bond_start <= s.epoch + && slash_epoch_filter(s.epoch) + }) + .cloned() + .collect::>(); + + let mut merged = list_slashes + .iter() + .chain(src_slashes.iter()) + .cloned() + .collect::>(); + merged + .sort_by(|s1, s2| s1.epoch.partial_cmp(&s2.epoch).unwrap()); + + result.total_redelegated += *bonded; + result.total_after_slashing += Self::apply_slashes_to_amount( + &self.params, + &merged, + *bonded, + ); + } + } + result + } + + fn compute_bond_at_epoch( + &self, + epoch: Epoch, + start: Epoch, + amount: token::Amount, + redel_bonds: &BTreeMap>, + validator: &Address, + ) -> token::Amount { + // `val list_slashes` + let list_slashes = self + .validator_slashes + .get(validator) + .cloned() + .unwrap_or_default() + .iter() + .filter(|&slash| { + // TODO: check bounds! + start <= slash.epoch + && slash.epoch + self.params.slash_processing_epoch_offset() + <= epoch + }) + .cloned() + .collect::>(); + + // `val filteredSlashMap` and `val resultFold` + // `fold_and_slash_redelegated_bonds` + let slash_epoch_filter = + |e: Epoch| e + self.params.slash_processing_epoch_offset() <= epoch; + let result_fold = self.fold_and_slash_redelegated_bonds( + redel_bonds, + start, + &list_slashes, + slash_epoch_filter, + ); + + // `val totalNoRedelegated` + let total_not_redelegated = amount - result_fold.total_redelegated; + // `val afterNoRedelegated` + let after_not_redelegated = Self::apply_slashes_to_amount( + &self.params, + &list_slashes, + total_not_redelegated, + ); + + after_not_redelegated + result_fold.total_after_slashing + } + + #[allow(clippy::too_many_arguments)] + fn compute_slash_bond_at_epoch( + &self, + epoch: Epoch, + infraction_epoch: Epoch, + bond_start: Epoch, + bond_amount: token::Amount, + redel_bonds: &BTreeMap>, + slash_rate: Dec, + validator: &Address, + ) -> token::Amount { + let amount_due = self + .compute_bond_at_epoch( + infraction_epoch, + bond_start, + bond_amount, + redel_bonds, + validator, + ) + .mul_ceil(slash_rate); + let slashable_amount = self.compute_bond_at_epoch( + epoch, + bond_start, + bond_amount, + redel_bonds, + validator, + ); + + cmp::min(amount_due, slashable_amount) + } + + fn slash_validator_redelegation( + &self, + validator: &Address, + dest_validator: &Address, + slash_rate: Dec, + slash_amounts: &mut BTreeMap, + ) { + let infraction_epoch = + self.epoch - self.params.slash_processing_epoch_offset(); + + let dest_total_redelegated_unbonded = self + .validator_total_redelegated_unbonded + .get(dest_validator) + .cloned() + .unwrap_or_default(); + let validator_slashes = self + .validator_slashes + .get(validator) + .cloned() + .unwrap_or_default(); + + // Loop over outgoing redelegations of validator -> dest_validator + let outgoing_redelegations = if let Some(outgoing_redels) = + self.outgoing_redelegations.get(validator) + { + outgoing_redels + .get(dest_validator) + .cloned() + .unwrap_or_default() + } else { + BTreeMap::<(Epoch, Epoch), token::Amount>::new() + }; + + for ((src_start_epoch, redel_start), amount) in outgoing_redelegations { + if self.params.in_redelegation_slashing_window( + infraction_epoch, + redel_start, + self.params.redelegation_end_epoch_from_start(redel_start), + ) && src_start_epoch <= infraction_epoch + { + self.slash_redelegation( + amount, + src_start_epoch, + self.params.redelegation_end_epoch_from_start(redel_start), validator, - stake_at_infraction.to_string_native(), + slash_rate, + &validator_slashes, + &dest_total_redelegated_unbonded, + slash_amounts, ); + } + } + } - let mut total_rate = Dec::zero(); + #[allow(clippy::too_many_arguments)] + fn slash_redelegation( + &self, + amount: token::Amount, + bond_start: Epoch, + redel_bond_start: Epoch, + src_validator: &Address, + slash_rate: Dec, + slashes: &[Slash], + dest_total_redelegated_unbonded: &AbstractTotalRedelegatedUnbonded, + slash_amounts: &mut BTreeMap, + ) { + tracing::debug!( + "\nSlashing redelegation amount {} - bond start {} and \ + redel_bond_start {} - at rate {}\n", + amount.to_string_native(), + bond_start, + redel_bond_start, + slash_rate + ); - 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()); + let infraction_epoch = + self.epoch - self.params.slash_processing_epoch_offset(); - total_rate += rate; - } - total_rate = cmp::min(total_rate, Dec::one()); - tracing::debug!("Total rate: {}", total_rate); - - let mut total_unbonded = token::Amount::default(); - let mut sum_post_bonds = token::Change::default(); - - for epoch in (infraction_epoch.0 + 1)..self.epoch.0 { - tracing::debug!("\nEpoch {}", epoch); - let mut recent_unbonds = token::Change::default(); - 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 { - tracing::debug!( - "UnbondRecord: amount = {}, start_epoch {}", - &unbond_amount.to_string_native(), - &start - ); - if start <= infraction_epoch { - 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 - + self - .params - .cubic_slashing_window_length - < infraction_epoch - }) - .cloned() - .fold( - BTreeMap::::new(), - |mut acc, s| { - let cur = - acc.entry(s.epoch).or_default(); - *cur += s.rate; - acc - }, - ); - tracing::debug!( - "Slashes for this unbond: {:?}", - slashes_for_this_unbond - ); - total_unbonded += compute_amount_after_slashing( - &slashes_for_this_unbond, - unbond_amount, - self.params.unbonding_len, - self.params.cubic_slashing_window_length, - ); - } else { - recent_unbonds += unbond_amount.change(); - } + // Slash redelegation destination validator from the next epoch only + // as they won't be jailed + let set_update_epoch = self.epoch.next(); - tracing::debug!( - "Total unbonded (epoch {}) w slashing = {}", - epoch, - total_unbonded.to_string_native() - ); - } - sum_post_bonds += self - .total_bonded - .get(&validator) - .and_then(|bonded| bonded.get(&Epoch(epoch))) + // Do initial computation of total unbonded + let mut tot_unbonded = token::Amount::zero(); + for epoch in Epoch::iter_bounds_inclusive( + infraction_epoch.next(), + set_update_epoch, + ) { + let total_redelegated_unbonded = + dest_total_redelegated_unbonded.get(&epoch); + if let Some(tot_redel_unbonded) = total_redelegated_unbonded { + if Self::has_redelegation( + tot_redel_unbonded, + bond_start, + redel_bond_start, + src_validator, + ) { + tot_unbonded += tot_redel_unbonded + .get(&redel_bond_start) + .unwrap() + .get(src_validator) + .unwrap() + .get(&bond_start) .cloned() - .unwrap_or_default() - - recent_unbonds; + .unwrap(); } - tracing::debug!("Computing adjusted amounts now"); + } + } - let mut last_slash = token::Change::default(); - for offset in 0..self.params.pipeline_len { - tracing::debug!( - "Epoch {}\nLast slash = {}", - self.epoch + offset, - last_slash.to_string_native(), - ); - let mut recent_unbonds = token::Change::default(); - let unbond_records = self - .unbond_records - .get(&validator) + for epoch in + Epoch::iter_range(set_update_epoch, self.params.pipeline_len) + { + let total_redelegated_unbonded = dest_total_redelegated_unbonded + .get(&epoch) + .cloned() + .unwrap_or_default(); + let updated_total_unbonded = if !Self::has_redelegation( + &total_redelegated_unbonded, + bond_start, + redel_bond_start, + src_validator, + ) { + tot_unbonded + } else { + tot_unbonded + + total_redelegated_unbonded + .get(&redel_bond_start) .unwrap() - .get(&(self.epoch + offset)) + .get(src_validator) + .unwrap() + .get(&bond_start) .cloned() - .unwrap_or_default(); - for (start, unbond_amount) in unbond_records { - tracing::debug!( - "UnbondRecord: amount = {}, start_epoch {}", - unbond_amount.to_string_native(), - &start - ); - if start <= infraction_epoch { - 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 - + self - .params - .cubic_slashing_window_length - < infraction_epoch - }) - .cloned() - .fold( - BTreeMap::::new(), - |mut acc, s| { - let cur = - acc.entry(s.epoch).or_default(); - *cur += s.rate; - acc - }, - ); - tracing::debug!( - "Slashes for this unbond: {:?}", - slashes_for_this_unbond - ); + .unwrap() + }; - total_unbonded += compute_amount_after_slashing( - &slashes_for_this_unbond, - unbond_amount, - self.params.unbonding_len, - self.params.cubic_slashing_window_length, - ); - } else { - recent_unbonds += unbond_amount.change(); - } + let list_slashes = slashes + .iter() + .filter(|&slash| { + self.params.in_redelegation_slashing_window( + slash.epoch, + self.params.redelegation_start_epoch_from_end( + redel_bond_start, + ), + redel_bond_start, + ) && bond_start <= slash.epoch + && slash.epoch + + self.params.slash_processing_epoch_offset() + <= infraction_epoch + }) + .cloned() + .collect::>(); - tracing::debug!( - "Total unbonded (offset {}) w slashing = {}", - offset, - total_unbonded.to_string_native() - ); - } - tracing::debug!( - "stake at infraction {}", - stake_at_infraction.to_string_native(), - ); - tracing::debug!( - "total unbonded {}", - total_unbonded.to_string_native() - ); - let this_slash = total_rate - * (stake_at_infraction - total_unbonded.change()); - let diff_slashed_amount = last_slash - this_slash; - tracing::debug!( - "Offset {} diff_slashed_amount {}", - offset, - diff_slashed_amount.to_string_native(), - ); - 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; - - tracing::debug!("Updating ABSTRACT voting powers"); - sum_post_bonds += self - .total_bonded - .get(&validator) - .and_then(|bonded| bonded.get(&(self.epoch + offset))) - .cloned() - .unwrap_or_default() - - recent_unbonds; + let slashable_amount = amount + .checked_sub(updated_total_unbonded) + .unwrap_or_default(); - tracing::debug!( - "\nUnslashable bonds = {}", - sum_post_bonds.to_string_native() - ); - let validator_stake_at_offset = self - .validator_stakes - .entry(self.epoch + offset) - .or_default() - .entry(validator.clone()) - .or_default(); + let slashed = Self::apply_slashes_to_amount( + &self.params, + &list_slashes, + slashable_amount, + ) + .mul_ceil(slash_rate); - let slashable_stake_at_offset = - *validator_stake_at_offset - sum_post_bonds; - tracing::debug!( - "Val stake pre (epoch {}) = {}", - self.epoch + offset, - validator_stake_at_offset.to_string_native(), - ); - tracing::debug!( - "Slashable stake at offset = {}", - slashable_stake_at_offset.to_string_native(), - ); - let change = cmp::max( - -slashable_stake_at_offset, - diff_slashed_amount, - ); + let list_slashes = slashes + .iter() + .filter(|&slash| { + self.params.in_redelegation_slashing_window( + slash.epoch, + self.params.redelegation_start_epoch_from_end( + redel_bond_start, + ), + redel_bond_start, + ) && bond_start <= slash.epoch + }) + .cloned() + .collect::>(); - tracing::debug!("Change = {}", change.to_string_native()); - *validator_stake_at_offset += change; + let slashable_stake = Self::apply_slashes_to_amount( + &self.params, + &list_slashes, + slashable_amount, + ) + .mul_ceil(slash_rate); - for os in (offset + 1)..=self.params.pipeline_len { - tracing::debug!("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; - tracing::debug!( - "New stake at epoch {} = {}", - self.epoch + os, - offset_stake.to_string_native() - ); - } + tot_unbonded = updated_total_unbonded; + + let to_slash = cmp::min(slashed, slashable_stake); + if !to_slash.is_zero() { + let slashed_amt = slash_amounts.entry(epoch).or_default(); + *slashed_amt += to_slash; + } + } + } + + fn has_redelegation( + total_redelegated_unbonded: &BTreeMap< + Epoch, + BTreeMap>, + >, + bond_start: Epoch, + redel_start: Epoch, + src_validator: &Address, + ) -> bool { + if let Some(redel_unbonded) = + total_redelegated_unbonded.get(&redel_start) + { + if let Some(unbonded) = redel_unbonded.get(src_validator) { + if unbonded.contains_key(&bond_start) { + return true; } } } + false } /// Get the pipeline epoch @@ -2826,9 +4500,9 @@ impl AbstractPosState { } /// Find the sums of the bonds across all epochs - fn bond_sums(&self) -> BTreeMap { + fn bond_sums(&self) -> BTreeMap { self.bonds.iter().fold( - BTreeMap::::new(), + BTreeMap::::new(), |mut acc, (id, bonds)| { for delta in bonds.values() { let entry = acc.entry(id.clone()).or_default(); @@ -2843,10 +4517,10 @@ impl AbstractPosState { fn withdrawable_unbonds(&self) -> BTreeMap { self.unbonds.iter().fold( BTreeMap::::new(), - |mut acc, (epoch, unbonds)| { - if *epoch <= self.epoch { + |mut acc, ((_start_epoch, withdraw_epoch), unbonds)| { + if *withdraw_epoch <= self.epoch { for (id, amount) in unbonds { - if *amount > token::Amount::default() { + if *amount > token::Amount::zero() { *acc.entry(id.clone()).or_default() += *amount; } } @@ -2858,11 +4532,13 @@ impl AbstractPosState { /// Compute the cubic slashing rate for the current epoch fn cubic_slash_rate(&self) -> Dec { - let infraction_epoch = self.epoch - - self.params.unbonding_len - - 1_u64 - - self.params.cubic_slashing_window_length; - tracing::debug!("Infraction epoch: {}", infraction_epoch); + let infraction_epoch = + self.epoch - self.params.slash_processing_epoch_offset(); + tracing::debug!( + "Infraction epoch: {}, Current epoch: {}", + infraction_epoch, + self.epoch + ); let window_width = self.params.cubic_slashing_window_length; let epoch_start = Epoch::from( infraction_epoch @@ -2877,7 +4553,7 @@ impl AbstractPosState { 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(), + token::Amount::zero(), |sum, (val_stake, validators)| { sum + *val_stake * validators.len() as u64 }, @@ -2895,14 +4571,13 @@ impl AbstractPosState { 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(), - ); + let val_stake = self + .validator_stakes + .get(&epoch) + .unwrap() + .get(validator) + .cloned() + .unwrap_or_default(); tracing::debug!( "Val {} stake epoch {}: {}", &validator, @@ -2957,14 +4632,11 @@ impl AbstractPosState { deltas_stake.to_string_native(), val_state ); - debug_assert_eq!( - *amount, - token::Amount::from_change(*deltas_stake) - ); + debug_assert_eq!(*amount, *deltas_stake); debug_assert_eq!(*val_state, ValidatorState::Consensus); } } - let mut max_bc = token::Amount::default(); + let mut max_bc = token::Amount::zero(); let bc = self.below_capacity_set.get(&epoch).unwrap(); for (amount, vals) in bc { if token::Amount::from(*amount) > max_bc { @@ -2993,13 +4665,13 @@ impl AbstractPosState { ); debug_assert_eq!( token::Amount::from(*amount), - token::Amount::from_change(deltas_stake) + deltas_stake ); debug_assert_eq!(*val_state, ValidatorState::BelowCapacity); } } if max_bc > min_consensus { - println!( + tracing::debug!( "min_consensus = {}, max_bc = {}", min_consensus.to_string_native(), max_bc.to_string_native() @@ -3069,6 +4741,364 @@ impl AbstractPosState { } } } + + fn is_chained_redelegation( + current_epoch: Epoch, + params: &PosParams, + incoming_redelegations: &AbstractIncomingRedelegations, + delegator: &Address, + src_validator: &Address, + ) -> bool { + let src_incoming_redelegations = + incoming_redelegations.get(src_validator); + if let Some(incoming) = src_incoming_redelegations { + if let Some(redel_end_epoch) = incoming.get(delegator) { + return redel_end_epoch.prev() + + params.slash_processing_epoch_offset() + > current_epoch; + } + } + false + } + + fn find_bonds_to_remove( + bonds: &BTreeMap, + amount: token::Amount, + ) -> BondsForRemovalRes { + let mut bonds_for_removal = BondsForRemovalRes::default(); + let mut remaining = amount; + + for (&bond_epoch, &bond_amount) in bonds.iter().rev() { + let to_unbond = cmp::min(bond_amount, remaining); + if to_unbond == bond_amount { + bonds_for_removal.epochs.insert(bond_epoch); + } else { + bonds_for_removal.new_entry = + Some((bond_epoch, bond_amount - to_unbond)); + } + remaining -= to_unbond; + if remaining.is_zero() { + break; + } + } + bonds_for_removal + } + + fn compute_modified_redelegation( + delegator_redelegated_bonds: &mut BTreeMap< + Epoch, + BTreeMap>, + >, + bond_epoch: Epoch, + amount: token::Amount, + ) -> ModifiedRedelegation { + let mut modified_redelegation = ModifiedRedelegation::default(); + + let redelegated_bonds = + delegator_redelegated_bonds.entry(bond_epoch).or_default(); + let (src_validators, total_redelegated) = + redelegated_bonds.iter().fold( + (BTreeSet::
::new(), token::Amount::zero()), + |mut acc, (src_val, redel_bonds)| { + acc.0.insert(src_val.clone()); + acc.1 += redel_bonds + .values() + .fold(token::Amount::zero(), |sum, val| sum + *val); + acc + }, + ); + + modified_redelegation.epoch = Some(bond_epoch); + + if total_redelegated <= amount { + return modified_redelegation; + } + + let mut remaining = amount; + for src_val in src_validators { + if remaining.is_zero() { + break; + } + let bonds = redelegated_bonds.get(&src_val).unwrap(); + let total_src_amount = + bonds.values().cloned().sum::(); + + modified_redelegation + .validators_to_remove + .insert(src_val.clone()); + + if total_src_amount <= remaining { + remaining -= total_src_amount; + } else { + let src_bonds_to_remove = + Self::find_bonds_to_remove(bonds, remaining); + + remaining = token::Amount::zero(); + + if let Some((bond_epoch, new_bond_amount)) = + src_bonds_to_remove.new_entry + { + modified_redelegation.validator_to_modify = Some(src_val); + modified_redelegation.epochs_to_remove = { + let mut epochs = src_bonds_to_remove.epochs; + epochs.insert(bond_epoch); + epochs + }; + modified_redelegation.epoch_to_modify = Some(bond_epoch); + modified_redelegation.new_amount = Some(new_bond_amount); + } else { + modified_redelegation.validator_to_modify = Some(src_val); + modified_redelegation.epochs_to_remove = + src_bonds_to_remove.epochs; + } + } + } + + modified_redelegation + } + + fn compute_new_redelegated_unbonds( + redelegated_bonds: &mut BTreeMap< + Epoch, + BTreeMap>, + >, + epochs_to_remove: &BTreeSet, + modified_redelegation: &ModifiedRedelegation, + ) -> BTreeMap>> + { + let unbonded_epochs = if let Some(epoch) = modified_redelegation.epoch { + let mut epochs = epochs_to_remove.clone(); + epochs.insert(epoch); + epochs + .iter() + .cloned() + .filter(|e| redelegated_bonds.contains_key(e)) + .collect::>() + } else { + epochs_to_remove + .iter() + .cloned() + .filter(|e| redelegated_bonds.contains_key(e)) + .collect::>() + }; + + let new_redelegated_unbonds: EagerRedelegatedUnbonds = unbonded_epochs + .into_iter() + .map(|start| { + let mut rbonds = EagerRedelegatedBondsMap::default(); + if modified_redelegation + .epoch + .map(|redelegation_epoch| start != redelegation_epoch) + .unwrap_or(true) + || modified_redelegation.validators_to_remove.is_empty() + { + for (src_val, bonds) in + redelegated_bonds.get(&start).unwrap() + { + for (bond_epoch, bond_amount) in bonds { + rbonds + .entry(src_val.clone()) + .or_default() + .insert(*bond_epoch, *bond_amount); + } + } + (start, rbonds) + } else { + for src_validator in + &modified_redelegation.validators_to_remove + { + if modified_redelegation + .validator_to_modify + .as_ref() + .map(|validator| src_validator != validator) + .unwrap_or(true) + { + let raw_bonds = redelegated_bonds + .entry(start) + .or_default() + .entry(src_validator.clone()) + .or_default(); + for (bond_epoch, bond_amount) in raw_bonds { + rbonds + .entry(src_validator.clone()) + .or_default() + .insert(*bond_epoch, *bond_amount); + } + } else { + for bond_start in + &modified_redelegation.epochs_to_remove + { + let cur_redel_bond_amount = redelegated_bonds + .entry(start) + .or_default() + .entry(src_validator.clone()) + .or_default() + .entry(*bond_start) + .or_default(); + + let raw_bonds = rbonds + .entry(src_validator.clone()) + .or_default(); + if modified_redelegation + .epoch_to_modify + .as_ref() + .map(|epoch| bond_start != epoch) + .unwrap_or(true) + { + raw_bonds.insert( + *bond_start, + *cur_redel_bond_amount, + ); + } else { + raw_bonds.insert( + *bond_start, + *cur_redel_bond_amount + - modified_redelegation + .new_amount + // Safe unwrap - it shouldn't + // get to + // this if it's None + .unwrap(), + ); + } + } + } + } + (start, rbonds) + } + }) + .collect(); + new_redelegated_unbonds + } + + fn compute_amount_after_slashing_unbond( + params: &PosParams, + all_slashes: &BTreeMap>, + validator: &Address, + new_unbonds: &BTreeMap<(Epoch, Epoch), token::Amount>, + new_redelegated_unbonded: &BTreeMap< + Epoch, + BTreeMap>, + >, + ) -> ResultSlashing { + let mut result_slashing = ResultSlashing::default(); + let validator_slashes = + all_slashes.get(validator).cloned().unwrap_or_default(); + for ((start_epoch, _withdraw_epoch), to_unbond) in new_unbonds { + let slashes = validator_slashes + .iter() + .filter(|&s| s.epoch >= *start_epoch) + .cloned() + .collect::>(); + + // Begin the logic for `fold_and_slash_redelegated_bonds` + let result_fold = { + let (mut total_redelegated, mut total_after_slashing) = + (token::Amount::zero(), token::Amount::zero()); + + for (src_validator, unbonded_map) in new_redelegated_unbonded + .get(start_epoch) + .cloned() + .unwrap_or_default() + { + for (bond_start, unbonded) in unbonded_map { + let src_slashes = all_slashes + .get(&src_validator) + .cloned() + .unwrap_or_default() + .iter() + .filter(|&s| { + params.in_redelegation_slashing_window( + s.epoch, + params.redelegation_start_epoch_from_end( + *start_epoch, + ), + *start_epoch, + ) && bond_start <= s.epoch + }) + .cloned() + .collect::>(); + + let mut merged = slashes + .iter() + .chain(src_slashes.iter()) + .cloned() + .collect::>(); + merged.sort_by(|s1, s2| { + s1.epoch.partial_cmp(&s2.epoch).unwrap() + }); + + total_redelegated += unbonded; + total_after_slashing += Self::apply_slashes_to_amount( + params, &merged, unbonded, + ); + } + } + + FoldRedelegatedBondsResult { + total_redelegated, + total_after_slashing, + } + }; + + let total_not_redelegated = + *to_unbond - result_fold.total_redelegated; + let after_not_redelegated = Self::apply_slashes_to_amount( + params, + &slashes, + total_not_redelegated, + ); + let amount_after_slashing = + after_not_redelegated + result_fold.total_after_slashing; + result_slashing.sum += amount_after_slashing; + result_slashing + .epoch_map + .insert(*start_epoch, amount_after_slashing); + } + + result_slashing + } + + fn apply_slashes_to_amount( + params: &PosParams, + slashes: &[Slash], + amount: token::Amount, + ) -> token::Amount { + let mut final_amount = amount; + let mut computed_slashes = BTreeMap::::new(); + for slash in slashes { + let slashed_amount = Self::compute_slashable_amount( + params, + slash, + amount, + &computed_slashes, + ); + final_amount = + final_amount.checked_sub(slashed_amount).unwrap_or_default(); + + computed_slashes.insert(slash.epoch, slashed_amount); + } + final_amount + } + + fn compute_slashable_amount( + params: &PosParams, + slash: &Slash, + amount: token::Amount, + computed_slashes: &BTreeMap, + ) -> token::Amount { + let updated_amount = computed_slashes + .iter() + .filter(|(&epoch, _)| { + // TODO: check if bounds correct! + // slashes that have already been applied and processed + epoch + params.slash_processing_epoch_offset() <= slash.epoch + }) + .fold(amount, |acc, (_, amnt)| { + acc.checked_sub(*amnt).unwrap_or_default() + }); + updated_amount.mul_ceil(slash.rate) + } } /// Arbitrary bond transition that adds tokens to an existing bond @@ -3175,44 +5205,3 @@ fn arb_slash(state: &AbstractPosState) -> impl Strategy { }, ) } - -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; - - 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 + cubic_slash_window_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: *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() -} diff --git a/proof_of_stake/src/tests/state_machine_v2.rs b/proof_of_stake/src/tests/state_machine_v2.rs new file mode 100644 index 0000000000..ce2e0e6817 --- /dev/null +++ b/proof_of_stake/src/tests/state_machine_v2.rs @@ -0,0 +1,4584 @@ +//! Test PoS transitions with a state machine + +use std::collections::{BTreeMap, BTreeSet, HashSet, VecDeque}; +use std::ops::{AddAssign, Deref}; +use std::{cmp, mem}; + +use assert_matches::assert_matches; +use derivative::Derivative; +use itertools::Itertools; +use namada_core::ledger::storage::testing::TestWlStorage; +use namada_core::ledger::storage_api::collections::lazy_map::{ + NestedSubKey, SubKey, +}; +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::dec::Dec; +use namada_core::types::key; +use namada_core::types::key::common::PublicKey; +use namada_core::types::storage::Epoch; +use namada_core::types::token::Change; +use proptest::prelude::*; +use proptest::test_runner::Config; +use proptest_state_machine::{ + prop_state_machine, ReferenceStateMachine, StateMachineTest, +}; +// Use `RUST_LOG=info` (or another tracing level) and `--nocapture` to see +// `tracing` logs from tests +use test_log::test; +use yansi::Paint; + +use super::utils::DbgPrintDiff; +use crate::parameters::testing::arb_rate; +use crate::parameters::PosParams; +use crate::tests::arb_params_and_genesis_validators; +use crate::tests::utils::pause_for_enter; +use crate::types::{ + BondId, GenesisValidator, ReverseOrdTokenAmount, Slash, SlashType, + ValidatorState, WeightedValidator, +}; +use crate::{ + below_capacity_validator_set_handle, bond_handle, + consensus_validator_set_handle, delegator_redelegated_bonds_handle, + enqueued_slashes_handle, find_slashes_in_range, + read_below_threshold_validator_set_addresses, read_pos_params, + redelegate_tokens, validator_deltas_handle, validator_slashes_handle, + validator_state_handle, RedelegationError, +}; + +prop_state_machine! { + #![proptest_config(Config { + cases: 2, + .. Config::default() + })] + #[ignore] + #[test] + /// A `StateMachineTest` implemented on `PosState` + fn pos_state_machine_test_v2(sequential 1000 => ConcretePosState); +} + +/// Abstract representation of a state of PoS system +#[derive(Clone, Derivative)] +#[derivative(Debug)] +struct AbstractPosState { + /// Current epoch + epoch: Epoch, + /// Parameters + params: PosParams, + /// Genesis validators + #[derivative(Debug = "ignore")] + genesis_validators: Vec, + /// Records of bonds, unbonds, withdrawal and redelegations with slashes, + /// if any + validator_records: BTreeMap, + /// Validator stakes. These are NOT deltas. + /// Pipelined. + validator_stakes: BTreeMap>, + /// Consensus validator set. Pipelined. + consensus_set: BTreeMap>>, + /// Below-capacity validator set. Pipelined. + below_capacity_set: + BTreeMap>>, + /// Below-threshold validator set. Pipelined. + below_threshold_set: BTreeMap>, + /// Validator states. Pipelined. + validator_states: 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, +} + +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.prev(); + // Copy the non-delta data from the last epoch into the new one + self.consensus_set.insert( + epoch, + self.consensus_set.get(&prev_epoch).unwrap().clone(), + ); + self.below_capacity_set.insert( + epoch, + self.below_capacity_set.get(&prev_epoch).unwrap().clone(), + ); + self.below_threshold_set.insert( + epoch, + self.below_threshold_set.get(&prev_epoch).unwrap().clone(), + ); + self.validator_states.insert( + epoch, + self.validator_states.get(&prev_epoch).unwrap().clone(), + ); + self.validator_stakes.insert( + epoch, + self.validator_stakes.get(&prev_epoch).unwrap().clone(), + ); + } + + /// Add a bond. + fn bond( + &mut self, + BondId { source, validator }: &BondId, + amount: token::Amount, + ) { + let start = self.pipeline(); + + let records = self.records_mut(validator, source); + let bond_at_start = records.bonds.entry(start).or_default(); + bond_at_start.tokens.amount += amount; + + let change = amount.change(); + let pipeline_state = self + .validator_states + .get(&start) + .unwrap() + .get(validator) + .unwrap(); + // Validator sets need to be updated before total stake + if *pipeline_state != ValidatorState::Jailed { + self.update_validator_sets(validator, change, self.pipeline()); + } + self.update_validator_total_stake(validator, change, self.pipeline()); + } + + /// Unbond a bond. + fn unbond( + &mut self, + BondId { source, validator }: &BondId, + amount: token::Amount, + ) { + // Last epoch in which it contributes to stake + let end = self.pipeline().prev(); + let withdrawable_epoch = + self.epoch + self.params.withdrawable_epoch_offset(); + let pipeline_len = self.params.pipeline_len; + + let records = self.records_mut(validator, source); + // The amount requested is before any slashing that may be applicable + let mut to_unbond = amount; + let mut amount_after_slashing = token::Amount::zero(); + + 'bonds_iter: for (&start, bond) in records.bonds.iter_mut().rev() { + // In every loop, try to unbond redelegations first. We have to + // go in reverse order of the start epoch to match the order of + // unbond in the implementation. + for (dest_validator, redelegs) in bond.incoming_redelegs.iter_mut() + { + let _redeleg_epoch = start - pipeline_len; + + for (&src_bond_start, redeleg) in + redelegs.tokens.iter_mut().rev() + { + let amount_before_slashing = + redeleg.amount_before_slashing(); + + let unbonded = if to_unbond >= amount_before_slashing { + // Unbond the whole bond + to_unbond -= amount_before_slashing; + amount_after_slashing += redeleg.amount; + + mem::take(redeleg) + } else { + // We have to divide this bond in case there are slashes + let unbond_slash = + to_unbond.mul_ceil(redeleg.slash_rates_sum()); + let to_unbond_after_slash = to_unbond - unbond_slash; + + to_unbond = token::Amount::zero(); + amount_after_slashing += to_unbond_after_slash; + + redeleg.amount -= to_unbond_after_slash; + let removed_slashes = + redeleg.subtract_slash(unbond_slash); + + TokensWithSlashes { + amount: to_unbond_after_slash, + slashes: removed_slashes, + } + }; + + let unbond = + bond.unbonds.entry(end).or_insert_with(|| Unbond { + withdrawable_epoch, + tokens: Default::default(), + incoming_redelegs: Default::default(), + }); + debug_assert_eq!( + unbond.withdrawable_epoch, + withdrawable_epoch + ); + let redeleg_unbond = unbond + .incoming_redelegs + .entry(dest_validator.clone()) + .or_default(); + let redeleg_unbond_tokens = redeleg_unbond + .tokens + .entry(src_bond_start) + .or_default(); + redeleg_unbond_tokens.amount += unbonded.amount; + redeleg_unbond_tokens.add_slashes(&unbonded.slashes); + + // Stop once all is unbonded + if to_unbond.is_zero() { + break 'bonds_iter; + } + } + } + + // Then try to unbond regular bonds + if !to_unbond.is_zero() { + let amount_before_slashing = + bond.tokens.amount_before_slashing(); + + let unbonded = if to_unbond >= amount_before_slashing { + // Unbond the whole bond + to_unbond -= amount_before_slashing; + amount_after_slashing += bond.tokens.amount; + + mem::take(&mut bond.tokens) + } else { + // We have to divide this bond in case there are slashes + let unbond_slash = + to_unbond.mul_ceil(bond.tokens.slash_rates_sum()); + let to_unbond_after_slash = to_unbond - unbond_slash; + + to_unbond = token::Amount::zero(); + amount_after_slashing += to_unbond_after_slash; + + bond.tokens.amount -= to_unbond_after_slash; + let removed_slashes = + bond.tokens.subtract_slash(unbond_slash); + + TokensWithSlashes { + amount: to_unbond_after_slash, + slashes: removed_slashes, + } + }; + + let unbond = + bond.unbonds.entry(end).or_insert_with(|| Unbond { + withdrawable_epoch, + tokens: Default::default(), + incoming_redelegs: Default::default(), + }); + debug_assert_eq!(unbond.withdrawable_epoch, withdrawable_epoch); + unbond.tokens.amount += unbonded.amount; + unbond.tokens.add_slashes(&unbonded.slashes); + + // Stop once all is unbonded + if to_unbond.is_zero() { + break; + } + } + } + assert!(to_unbond.is_zero()); + + let pipeline_state = self + .validator_states + .get(&self.pipeline()) + .unwrap() + .get(validator) + .unwrap(); + if *pipeline_state != ValidatorState::Jailed { + self.update_validator_sets( + validator, + -amount_after_slashing.change(), + self.pipeline(), + ); + } + self.update_validator_total_stake( + validator, + -amount_after_slashing.change(), + self.pipeline(), + ); + } + + /// Redelegate a bond. + fn redelegate( + &mut self, + BondId { source, validator }: &BondId, + new_validator: &Address, + amount: token::Amount, + ) { + // Last epoch in which it contributes to stake of thhe source validator + let current_epoch = self.epoch; + let pipeline = self.pipeline(); + let src_end = pipeline.prev(); + let withdrawable_epoch_offset = self.params.withdrawable_epoch_offset(); + let pipeline_len = self.params.pipeline_len; + + let records = self.records_mut(validator, source); + + // The amount requested is before any slashing that may be applicable + let mut to_unbond = amount; + let mut amount_after_slashing = token::Amount::zero(); + // Keyed by redelegation src bond start epoch + let mut dest_incoming_redelegs = + BTreeMap::::new(); + + 'bonds_iter: for (&start, bond) in records.bonds.iter_mut().rev() { + // In every loop, try to redelegate redelegations first. We have to + // go in reverse order of the start epoch to match the order of + // redelegation in the implementation. + for (_src_validator, redelegs) in + bond.incoming_redelegs.iter_mut().rev() + { + let _redeleg_epoch = start - pipeline_len; + + for (_src_bond_start, redeleg) in + redelegs.tokens.iter_mut().rev() + { + let amount_before_slashing = + redeleg.amount_before_slashing(); + + // No chained redelegations + if Epoch( + start.0.checked_sub(pipeline_len).unwrap_or_default(), + ) + withdrawable_epoch_offset + <= current_epoch + { + let unbonded = if to_unbond >= amount_before_slashing { + // Unbond the whole bond + to_unbond -= amount_before_slashing; + amount_after_slashing += redeleg.amount; + + mem::take(redeleg) + } else { + // We have to divide this bond in case there are + // slashes + let unbond_slash = + to_unbond.mul_ceil(redeleg.slash_rates_sum()); + let to_unbond_after_slash = + to_unbond - unbond_slash; + + to_unbond = token::Amount::zero(); + amount_after_slashing += to_unbond_after_slash; + + redeleg.amount -= to_unbond_after_slash; + let removed_slashes = + redeleg.subtract_slash(unbond_slash); + + TokensWithSlashes { + amount: to_unbond_after_slash, + slashes: removed_slashes, + } + }; + + let outgoing_redeleg = bond + .outgoing_redelegs + .entry(src_end) + .or_default() + .entry(new_validator.clone()) + .or_default(); + + outgoing_redeleg.amount += unbonded.amount; + outgoing_redeleg.add_slashes(&unbonded.slashes); + + let redeleg = + dest_incoming_redelegs.entry(start).or_default(); + redeleg.amount += unbonded.amount; + redeleg.add_slashes(&unbonded.slashes); + + // Stop once all is unbonded + if to_unbond.is_zero() { + break 'bonds_iter; + } + } + } + } + + // Then try to redelegate regular bonds + if !to_unbond.is_zero() { + let amount_before_slashing = + bond.tokens.amount_before_slashing(); + + let unbonded = if to_unbond >= amount_before_slashing { + // Unbond the whole bond + to_unbond -= amount_before_slashing; + amount_after_slashing += bond.tokens.amount; + + mem::take(&mut bond.tokens) + } else { + // We have to divide this bond in case there are slashes + let unbond_slash = + to_unbond.mul_ceil(bond.tokens.slash_rates_sum()); + let to_unbond_after_slash = to_unbond - unbond_slash; + + to_unbond = token::Amount::zero(); + amount_after_slashing += to_unbond_after_slash; + + bond.tokens.amount -= to_unbond_after_slash; + let removed_slashes = + bond.tokens.subtract_slash(unbond_slash); + + TokensWithSlashes { + amount: to_unbond_after_slash, + slashes: removed_slashes, + } + }; + + let outgoing_redeleg = bond + .outgoing_redelegs + .entry(src_end) + .or_default() + .entry(new_validator.clone()) + .or_default(); + outgoing_redeleg.amount += unbonded.amount; + outgoing_redeleg.add_slashes(&unbonded.slashes); + let dest_incoming_redeleg = + dest_incoming_redelegs.entry(start).or_default(); + dest_incoming_redeleg.amount += unbonded.amount; + dest_incoming_redeleg.add_slashes(&unbonded.slashes); + } + // Stop once all is unbonded + if to_unbond.is_zero() { + break; + } + } + assert!(to_unbond.is_zero()); + + // Record the incoming redelegations on destination validator + let dest_records = self.records_mut(new_validator, source); + let redeleg = dest_records + .bonds + .entry(pipeline) + .or_default() + .incoming_redelegs + .entry(validator.clone()) + .or_default(); + for (start, inc_redeleg) in dest_incoming_redelegs { + let redeleg_tokens = redeleg.tokens.entry(start).or_default(); + redeleg_tokens.amount += inc_redeleg.amount; + redeleg_tokens.add_slashes(&inc_redeleg.slashes); + } + + // Update stake of src validator + let src_pipeline_state = self + .validator_states + .get(&self.pipeline()) + .unwrap() + .get(validator) + .unwrap(); + if *src_pipeline_state != ValidatorState::Jailed { + self.update_validator_sets( + validator, + -amount_after_slashing.change(), + self.pipeline(), + ); + } + self.update_validator_total_stake( + validator, + -amount_after_slashing.change(), + self.pipeline(), + ); + + // Update stake of dest validator + let dest_pipeline_state = self + .validator_states + .get(&self.pipeline()) + .unwrap() + .get(new_validator) + .unwrap(); + if *dest_pipeline_state != ValidatorState::Jailed { + self.update_validator_sets( + new_validator, + amount_after_slashing.change(), + self.pipeline(), + ); + } + self.update_validator_total_stake( + new_validator, + amount_after_slashing.change(), + self.pipeline(), + ); + } + + /// Withdraw all unbonds that can be withdrawn. + fn withdraw(&mut self, BondId { source, validator }: &BondId) { + let epoch = self.epoch; + let records = self.records_mut(validator, source); + let mut to_store = BTreeMap::::new(); + for (_start, bond) in records.bonds.iter_mut() { + bond.unbonds.retain(|_end, unbond| { + let is_withdrawable = unbond.withdrawable_epoch <= epoch; + if is_withdrawable { + let withdrawn = to_store.entry(epoch).or_default(); + withdrawn.amount += unbond.tokens.amount; + withdrawn.add_slashes(&unbond.tokens.slashes); + for redeleg in unbond.incoming_redelegs.values() { + for tokens in redeleg.tokens.values() { + withdrawn.amount += tokens.amount; + withdrawn.add_slashes(&tokens.slashes); + } + } + } + !is_withdrawable + }) + } + records.withdrawn.extend(to_store.into_iter()); + } + + /// Get or insert default mutable records + fn records_mut( + &mut self, + validator: &Address, + source: &Address, + ) -> &mut Records { + self.validator_records + .entry(validator.clone()) + .or_default() + .per_source + .entry(source.clone()) + .or_default() + } + + /// Get records + fn records( + &self, + validator: &Address, + source: &Address, + ) -> Option<&Records> { + self.validator_records + .get(validator) + .and_then(|records| records.per_source.get(source)) + } + + /// 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, + epoch: Epoch, + ) { + let total_stakes = self + .validator_stakes + .entry(epoch) + .or_default() + .entry(validator.clone()) + .or_default(); + tracing::debug!("TOTAL {validator} stakes before {}", total_stakes); + *total_stakes += change; + tracing::debug!("TOTAL {validator} stakes after {}", total_stakes); + } + + /// Update validator in sets with bonded or unbonded change (should be + /// called with epoch at pipeline) or slashes. + fn update_validator_sets( + &mut self, + validator: &Address, + change: token::Change, + epoch: Epoch, + ) { + let consensus_set = self.consensus_set.entry(epoch).or_default(); + let below_cap_set = self.below_capacity_set.entry(epoch).or_default(); + let below_thresh_set = + self.below_threshold_set.entry(epoch).or_default(); + + let validator_stakes = self.validator_stakes.get(&epoch).unwrap(); + let validator_states = self.validator_states.get_mut(&epoch).unwrap(); + + let state_pre = validator_states.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(this_val_stake_pre); + + let threshold = self.params.validator_stake_threshold; + if this_val_stake_pre < threshold && this_val_stake_post < threshold { + // Validator is already below-threshold and will remain there, so do + // nothing + debug_assert!(below_thresh_set.contains(validator)); + return; + } + + match state_pre { + ValidatorState::Consensus => { + // tracing::debug!("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); + } + + // If posterior stake is below threshold, place into the + // below-threshold set + if this_val_stake_post < threshold { + below_thresh_set.insert(validator.clone()); + validator_states.insert( + validator.clone(), + ValidatorState::BelowThreshold, + ); + + // Promote the next below-cap validator if there is one + if let Some(mut max_below_cap) = below_cap_set.last_entry() + { + let max_below_cap_stake = *max_below_cap.key(); + let vals = max_below_cap.get_mut(); + let promoted_val = vals.pop_front().unwrap(); + // Remove the key if there's nothing left + if vals.is_empty() { + below_cap_set.remove(&max_below_cap_stake); + } + + consensus_set + .entry(max_below_cap_stake.0) + .or_default() + .push_back(promoted_val.clone()); + validator_states + .insert(promoted_val, ValidatorState::Consensus); + } + + return; + } + + // If unbonding, check the max below-cap validator's state if we + // need to do a swap + if change < token::Change::zero() { + if let Some(mut max_below_cap) = below_cap_set.last_entry() + { + let max_below_cap_stake = *max_below_cap.key(); + if max_below_cap_stake.0 > this_val_stake_post { + // 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 if there's nothing left + if vals.is_empty() { + below_cap_set.remove(&max_below_cap_stake); + } + // Do the swap in the validator sets + consensus_set + .entry(max_below_cap_stake.0) + .or_default() + .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; + } + } + } + + // Insert with the posterior stake + consensus_set + .entry(this_val_stake_post) + .or_default() + .push_back(validator.clone()); + } + ValidatorState::BelowCapacity => { + // tracing::debug!("Validator initially in below-cap"); + + // Remove from the prior stake + let vals = + below_cap_set.entry(this_val_stake_pre.into()).or_default(); + vals.retain(|addr| addr != validator); + if vals.is_empty() { + below_cap_set.remove(&this_val_stake_pre.into()); + } + + // If posterior stake is below threshold, place into the + // below-threshold set + if this_val_stake_post < threshold { + below_thresh_set.insert(validator.clone()); + validator_states.insert( + validator.clone(), + ValidatorState::BelowThreshold, + ); + return; + } + + // If bonding, check the min consensus validator's state if we + // need to do a swap + if change >= token::Change::zero() { + // dbg!(&consensus_set); + if let Some(mut min_consensus) = consensus_set.first_entry() + { + // 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_consensus.get_mut(); + let last_val = vals.pop_back().unwrap(); + // Remove the key if there's nothing left + if vals.is_empty() { + consensus_set.remove(&min_consensus_stake); + } + // Do the swap in the validator sets + below_cap_set + .entry(min_consensus_stake.into()) + .or_default() + .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; + } + } + } + + // Insert with the posterior stake + below_cap_set + .entry(this_val_stake_post.into()) + .or_default() + .push_back(validator.clone()); + } + ValidatorState::BelowThreshold => { + // We know that this validator will be promoted into one of the + // higher sets, so first remove from the below-threshold set. + below_thresh_set.remove(validator); + + let num_consensus = + consensus_set.iter().fold(0, |sum, (_, validators)| { + sum + validators.len() as u64 + }); + if num_consensus < self.params.max_validator_slots { + // Place the validator directly into the consensus set + consensus_set + .entry(this_val_stake_post) + .or_default() + .push_back(validator.clone()); + validator_states + .insert(validator.clone(), ValidatorState::Consensus); + return; + } + // Determine which set to place the validator into + if let Some(mut min_consensus) = consensus_set.first_entry() { + // 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_consensus.get_mut(); + let last_val = vals.pop_back().unwrap(); + // Remove the key if there's nothing left + if vals.is_empty() { + consensus_set.remove(&min_consensus_stake); + } + // Do the swap in the validator sets + below_cap_set + .entry(min_consensus_stake.into()) + .or_default() + .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); + } else { + // Place the validator into the below-capacity set + below_cap_set + .entry(this_val_stake_post.into()) + .or_default() + .push_back(validator.clone()); + validator_states.insert( + validator.clone(), + ValidatorState::BelowCapacity, + ); + } + } + } + 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 + - self.params.cubic_slashing_window_length + - 1; + + let cubic_rate = self.cubic_slash_rate(); + for (validator, slashes) in slashes_this_epoch { + // Slash this validator on it's full stake at infration + self.slash_a_validator( + &validator, + &slashes, + infraction_epoch, + cubic_rate, + ); + } + } + } + + fn slash_a_validator( + &mut self, + validator: &Address, + slashes: &[Slash], + infraction_epoch: Epoch, + cubic_rate: Dec, + ) { + let current_epoch = self.epoch; + let mut total_rate = Dec::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, Dec::one()); + tracing::debug!("Total rate: {}", total_rate); + + // Find validator stakes before slashing for up to pipeline epoch + let mut validator_stakes_pre = + BTreeMap::>::new(); + for epoch in + Epoch::iter_bounds_inclusive(current_epoch, self.pipeline()) + { + for (validator, records) in &self.validator_records { + let stake = records.stake(epoch); + validator_stakes_pre + .entry(epoch) + .or_default() + .insert(validator.clone(), stake); + } + } + + let mut redelegations_to_slash = BTreeMap::< + Address, + BTreeMap>>, + >::new(); + for (addr, records) in self.validator_records.iter_mut() { + if addr == validator { + for (source, records) in records.per_source.iter_mut() { + // Apply slashes on non-redelegated bonds + records.slash(total_rate, infraction_epoch, current_epoch); + + // Slash tokens in the outgoing redelegation records for + // this validator + for (&start, bond) in records.bonds.iter_mut() { + for (&end, redelegs) in + bond.outgoing_redelegs.iter_mut() + { + if start <= infraction_epoch + && end >= infraction_epoch + { + for (dest, tokens) in redelegs.iter_mut() { + let slashed = tokens.slash( + total_rate, + infraction_epoch, + current_epoch, + ); + // Store the redelegation slashes to apply + // on destination validator + *redelegations_to_slash + .entry(dest.clone()) + .or_default() + .entry(source.clone()) + .or_default() + .entry( + // start epoch of redelegation + end.next(), + ) + .or_default() + // redelegation src bond start epoch + .entry(start) + .or_default() += TokensSlash { + amount: slashed, + rate: total_rate, + }; + } + } + } + } + } + } + } + // Apply redelegation slashes on destination validator + for (dest_validator, redelegations) in redelegations_to_slash { + for (source, tokens) in redelegations { + for (redelegation_start, slashes) in tokens { + for (src_bond_start, slash) in slashes { + let records = self + .validator_records + .get_mut(&dest_validator) + .unwrap() + .per_source + .get_mut(&source) + .unwrap(); + records.subtract_redelegation_slash( + validator, + src_bond_start, + redelegation_start, + slash, + current_epoch, + ); + } + } + } + } + + // Find validator stakes after slashing for up to pipeline epoch + let mut validator_stakes_post = + BTreeMap::>::new(); + for epoch in + Epoch::iter_bounds_inclusive(current_epoch, self.pipeline()) + { + for (validator, records) in &self.validator_records { + let stake = records.stake(epoch); + validator_stakes_post + .entry(epoch) + .or_default() + .insert(validator.clone(), stake); + } + } + + // Apply the difference in stakes to validator_stakes, states and deltas + for epoch in + Epoch::iter_bounds_inclusive(current_epoch, self.pipeline()) + { + for (validator_to_update, &stake_post) in + validator_stakes_post.get(&epoch).unwrap() + { + let stake_pre = validator_stakes_pre + .get(&epoch) + .unwrap() + .get(validator_to_update) + .cloned() + .unwrap_or_default(); + let change = stake_post.change() - stake_pre.change(); + + if !change.is_zero() { + let state = self + .validator_states + .get(&epoch) + .unwrap() + .get(validator_to_update) + .unwrap(); + // Validator sets need to be updated before total + // stake + if *state != ValidatorState::Jailed { + self.update_validator_sets( + validator_to_update, + change, + epoch, + ); + } + self.update_validator_total_stake( + validator_to_update, + change, + epoch, + ); + } + } + } + } + + /// Get the pipeline epoch + fn pipeline(&self) -> Epoch { + self.epoch + self.params.pipeline_len + } + + /// Check if the given address is of a known validator + fn is_validator(&self, validator: &Address, epoch: Epoch) -> bool { + self.validator_states + .get(&epoch) + .unwrap() + .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)); + } + } + 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 is_in_below_threshold(&self, validator: &Address, epoch: Epoch) -> bool { + self.below_threshold_set + .get(&epoch) + .unwrap() + .iter() + .any(|val| val == validator) + } + + /// Find the sum of bonds that can be unbonded. The returned amounts are + /// prior to slashing. + fn unbondable_bonds(&self) -> BTreeMap { + let mut sums = BTreeMap::::new(); + for (validator, records) in &self.validator_records { + for (source, record) in &records.per_source { + let unbondable = sums + .entry(BondId { + source: source.clone(), + validator: validator.clone(), + }) + .or_default(); + // Add bonds and incoming redelegations + for (&start, bond) in &record.bonds { + *unbondable += bond.tokens.amount_before_slashing(); + for redeleg in bond.incoming_redelegs.values() { + let redeleg_epoch = start - self.params.pipeline_len; + *unbondable += redeleg + .amount_before_slashing_after_redeleg( + redeleg_epoch, + ); + } + } + } + } + // Filter out any 0s. + sums.retain(|_id, tokens| !tokens.is_zero()); + sums + } + + /// Find the sum of bonds that can be redelegated. The returned amounts are + /// prior to slashing. + fn redelegatable_bonds(&self) -> BTreeMap { + let mut sums = BTreeMap::::new(); + for (validator, records) in &self.validator_records { + for (source, record) in &records.per_source { + // Self-bonds cannot be redelegated + if validator != source { + let unbondable = sums + .entry(BondId { + source: source.clone(), + validator: validator.clone(), + }) + .or_default(); + // Add bonds + for (&start, bond) in &record.bonds { + *unbondable += bond.tokens.amount_before_slashing(); + // Add redelegations + for redeleg in bond.incoming_redelegs.values() { + // No chained redelegations + if Epoch( + start + .0 + .checked_sub(self.params.pipeline_len) + .unwrap_or_default(), + ) + self.params.withdrawable_epoch_offset() + <= self.epoch + { + *unbondable += redeleg.amount_before_slashing(); + } + } + } + } + } + } + // Filter out any 0s. + sums.retain(|_id, tokens| !tokens.is_zero()); + sums + } + + fn unchainable_redelegations(&self) -> BTreeSet { + let mut unchainable = BTreeSet::new(); + for records in self.validator_records.values() { + for (owner, records) in &records.per_source { + for bond in records.bonds.values() { + for (&end, redelegs) in &bond.outgoing_redelegs { + // If the outgoing redelegation is still slashable for + // source validator ... + if end + self.params.slash_processing_epoch_offset() + > self.epoch + { + // ... it cannot be redelegated for now + for (dest_validator, tokens) in redelegs { + if !tokens.is_zero() { + unchainable.insert(BondId { + source: owner.clone(), + validator: dest_validator.clone(), + }); + } + } + } + } + } + } + } + unchainable + } + + /// Find the sums of withdrawable unbonds + fn withdrawable_unbonds(&self) -> BTreeMap { + let mut withdrawable = BTreeMap::::new(); + for (validator, records) in &self.validator_records { + for (source, records) in &records.per_source { + for bond in records.bonds.values() { + for unbond in bond.unbonds.values() { + if unbond.withdrawable_epoch <= self.epoch { + let entry = withdrawable + .entry(BondId { + source: source.clone(), + validator: validator.clone(), + }) + .or_default(); + // Add withdrawable unbonds including redelegations + *entry += unbond.amount_before_slashing(); + } + } + } + } + } + withdrawable + } + + fn existing_bond_ids(&self) -> Vec { + let mut ids = Vec::new(); + for (validator, records) in &self.validator_records { + for source in records.per_source.keys() { + ids.push(BondId { + source: source.clone(), + validator: validator.clone(), + }); + } + } + ids + } + + /// Compute the cubic slashing rate for the current epoch + fn cubic_slash_rate(&self) -> Dec { + let infraction_epoch = self.epoch + - self.params.unbonding_len + - 1_u64 + - self.params.cubic_slashing_window_length; + tracing::debug!("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 = Dec::zero(); + for epoch in Epoch::iter_bounds_inclusive(epoch_start, epoch_end) { + let consensus_stake = + self.consensus_set.get(&epoch).unwrap().iter().fold( + token::Amount::zero(), + |sum, (val_stake, validators)| { + sum + *val_stake * validators.len() as u64 + }, + ); + tracing::debug!( + "Consensus stake in epoch {}: {}", + epoch, + consensus_stake.to_string_native() + ); + + 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(), + ); + tracing::debug!( + "Val {} stake epoch {}: {}", + &validator, + epoch, + val_stake.to_string_native(), + ); + vp_frac_sum += Dec::from(slashes.len()) + * Dec::from(val_stake) + / Dec::from(consensus_stake); + } + } + } + let vp_frac_sum = cmp::min(Dec::one(), vp_frac_sum); + tracing::debug!("vp_frac_sum: {}", vp_frac_sum); + + cmp::min( + Dec::new(9, 0).unwrap() * vp_frac_sum * vp_frac_sum, + Dec::one(), + ) + } + + fn debug_validators(&self) { + let current_epoch = self.epoch; + for epoch in + Epoch::iter_bounds_inclusive(current_epoch, self.pipeline()) + { + 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(); + debug_assert_eq!( + *amount, + token::Amount::from_change(*deltas_stake) + ); + debug_assert_eq!(*val_state, ValidatorState::Consensus); + } + } + let mut max_bc = token::Amount::zero(); + 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(); + debug_assert_eq!( + token::Amount::from(*amount), + token::Amount::from_change(deltas_stake) + ); + debug_assert_eq!(*val_state, ValidatorState::BelowCapacity); + } + } + if max_bc > min_consensus { + tracing::debug!( + "min_consensus = {}, max_bc = {}", + min_consensus.to_string_native(), + max_bc.to_string_native() + ); + } + assert!(min_consensus >= max_bc); + + for addr in self.below_threshold_set.get(&epoch).unwrap() { + let state = self + .validator_states + .get(&epoch) + .unwrap() + .get(addr) + .unwrap(); + + assert_eq!(*state, ValidatorState::BelowThreshold); + } + + for addr in self + .validator_states + .get(&epoch) + .unwrap() + .keys() + .cloned() + .collect::>() + { + if let (None, None, false) = ( + self.is_in_consensus_w_info(&addr, epoch), + self.is_in_below_capacity_w_info(&addr, epoch), + self.is_in_below_threshold(&addr, epoch), + ) { + assert_eq!( + self.validator_states + .get(&epoch) + .unwrap() + .get(&addr) + .cloned(), + Some(ValidatorState::Jailed) + ); + } + } + } + } + + fn is_chained_redelegation( + unchainable_redelegations: &BTreeSet, + delegator: &Address, + src_validator: &Address, + ) -> bool { + unchainable_redelegations.contains(&BondId { + source: delegator.clone(), + validator: src_validator.clone(), + }) + } +} + +#[derive(Clone, Debug, Default)] +struct ValidatorRecords { + /// All records to a validator that contribute to its + /// [`ValidatorBonds::stake`]. For self-bonds the key is a validator + /// and for delegations a delegator. + per_source: BTreeMap, +} + +impl ValidatorRecords { + /// Validator's stake is a sum of bond amounts with any slashing applied. + fn stake(&self, epoch: Epoch) -> token::Amount { + let mut total = token::Amount::zero(); + for bonds in self.per_source.values() { + total += bonds.amount(epoch); + } + total + } + + /// Find how much slash rounding error at most can be tolerated for slashes + /// that were processed before or at the given epoch on a total validator's + /// stake vs sum of slashes on bond deltas, unbonded, withdrawn or + /// redelegated bonds. + /// + /// We allow `n - 1` slash rounding error for `n` number of slashes in + /// unique epochs for bonds, unbonds and withdrawals. The bond deltas, + /// unbonds and withdrawals are slashed individually and so their total + /// slashed may be more than the slash on a sum of total validator's + /// stake. + fn slash_round_err_tolerance(&self, epoch: Epoch) -> token::Amount { + let mut unique_count = 0_u64; + for record in self.per_source.values() { + unique_count += record.num_of_slashes(epoch); + } + token::Amount::from(unique_count.checked_sub(1).unwrap_or_default()) + } +} + +#[derive(Clone, Debug, Default)] +struct Records { + /// Key is a bond start epoch (when it first contributed to voting power) + /// The value contains the sum of all the bonds started at the same epoch. + bonds: BTreeMap, + /// Withdrawn tokens in the epoch + withdrawn: BTreeMap, +} + +impl Records { + /// Sum of bond amounts with any slashes that were processed before or at + /// the given epoch applied. + fn amount(&self, epoch: Epoch) -> token::Amount { + let Records { + bonds, + withdrawn: _, + } = self; + let mut total = token::Amount::zero(); + for (&start, bond) in bonds { + if start <= epoch { + // Bonds + total += bond.tokens.amount; + // Add back any slashes that were processed after the given + // epoch + total += bond.tokens.slashes_sum_after_epoch(epoch); + + for (&end, unbond) in &bond.unbonds { + if end >= epoch { + // Unbonds + total += unbond.tokens.amount; + total += unbond.tokens.slashes_sum_after_epoch(epoch); + + // Unbonded incoming redelegations + for redelegs in unbond.incoming_redelegs.values() { + for tokens in redelegs.tokens.values() { + total += tokens.amount; + total += tokens.slashes_sum_after_epoch(epoch); + } + } + } + } + + // Outgoing redelegations + for (&end, redelegs) in &bond.outgoing_redelegs { + if end >= epoch { + for tokens in redelegs.values() { + total += tokens.amount; + total += tokens.slashes_sum_after_epoch(epoch); + } + } + } + + // Incoming redelegations + for redelegs in bond.incoming_redelegs.values() { + for tokens in redelegs.tokens.values() { + total += tokens.amount; + total += tokens.slashes_sum_after_epoch(epoch); + } + } + } + } + total + } + + fn slash( + &mut self, + rate: Dec, + infraction_epoch: Epoch, + processing_epoch: Epoch, + ) { + for (&start, bond) in self.bonds.iter_mut() { + if start <= infraction_epoch { + bond.slash(rate, infraction_epoch, processing_epoch); + + for (&end, unbond) in bond.unbonds.iter_mut() { + if end >= infraction_epoch { + unbond.slash(rate, infraction_epoch, processing_epoch); + } + } + } + } + } + + fn subtract_redelegation_slash( + &mut self, + src_validator: &Address, + src_bond_start: Epoch, + redelegation_start: Epoch, + mut to_sub: TokensSlash, + processing_epoch: Epoch, + ) { + // Slash redelegation destination on the next epoch + let slash_epoch = processing_epoch.next(); + let bond = self.bonds.get_mut(&redelegation_start).unwrap(); + for unbond in bond.unbonds.values_mut() { + if let Some(redeleg) = + unbond.incoming_redelegs.get_mut(src_validator) + { + if let Some(tokens) = redeleg.tokens.get_mut(&src_bond_start) { + if tokens.amount >= to_sub.amount { + tokens.amount -= to_sub.amount; + *tokens.slashes.entry(slash_epoch).or_default() += + to_sub; + return; + } else { + to_sub.amount -= tokens.amount; + *tokens.slashes.entry(slash_epoch).or_default() += + TokensSlash { + amount: tokens.amount, + rate: to_sub.rate, + }; + tokens.amount = token::Amount::zero(); + } + } + } + } + let redeleg = bond.incoming_redelegs.get_mut(src_validator).unwrap(); + if let Some(tokens) = redeleg.tokens.get_mut(&src_bond_start) { + tokens.amount -= to_sub.amount; + *tokens.slashes.entry(slash_epoch).or_default() += to_sub; + } else { + debug_assert!(to_sub.amount.is_zero()); + } + } + + /// Find how much slash rounding error at most can be tolerated for slashes + /// that were processed before or at the given epoch on a bond's amount vs + /// sum of slashes on bond deltas, unbonded, withdrawn or redelegated + /// bonds. + /// + /// We allow `n - 1` slash rounding error for `n` number of slashes (`fn + /// num_of_slashes`) in unique epochs for bonds, unbonds and + /// withdrawals. The bond deltas, unbonds and withdrawals are slashed + /// individually and so their total slashed may be more than the slash + /// on a sum of a bond's total amount. + fn slash_round_err_tolerance(&self, epoch: Epoch) -> token::Amount { + token::Amount::from( + self.num_of_slashes(epoch) + .checked_sub(1) + .unwrap_or_default(), + ) + } + + /// Get the number of slashes in unique epochs that were processed before or + /// at the given epoch for all bonds, unbonds, redelegs, unbonded redelegs + /// and withdrawn tokens. + fn num_of_slashes(&self, epoch: Epoch) -> u64 { + let mut unique_count = 0_u64; + for bond in self.bonds.values() { + unique_count += bond.tokens.num_of_slashes(epoch); + for redeleg in bond.incoming_redelegs.values() { + for tokens in redeleg.tokens.values() { + unique_count += tokens.num_of_slashes(epoch); + } + } + for unbond in bond.unbonds.values() { + unique_count += unbond.tokens.num_of_slashes(epoch); + for redeleg in unbond.incoming_redelegs.values() { + for tokens in redeleg.tokens.values() { + unique_count += tokens.num_of_slashes(epoch); + } + } + } + } + for withdrawn in self.withdrawn.values() { + unique_count += withdrawn.num_of_slashes(epoch); + } + unique_count + } +} + +#[derive(Clone, Debug, Default)] +struct Bond { + /// Bonded amount is the amount that's been bonded originally, reduced by + /// unbonding or slashing, if any. Incoming redelegations are recorded + /// separately. + tokens: TokensWithSlashes, + /// Incoming redelegations contribute to the stake of this validator. + /// Their sum is not included in the `tokens` field. + incoming_redelegs: BTreeMap, + /// Key is end epoch in which the unbond last contributed to stake of the + /// validator. + unbonds: BTreeMap, + /// The outer key is an end epoch of the redelegated bond in which the bond + /// last contributed to voting power of this validator (the source). The + /// inner key is the redelegation destination validator. + /// + /// After a redelegation a bond transferred to destination validator is + /// liable for slashes on a source validator (key in the map) from the + /// Bond's `start` to key's `end` epoch. + outgoing_redelegs: BTreeMap>, +} + +impl Bond { + fn slash( + &mut self, + rate: Dec, + infraction_epoch: Epoch, + processing_epoch: Epoch, + ) { + self.tokens.slash(rate, infraction_epoch, processing_epoch); + for (_src, redeleg) in self.incoming_redelegs.iter_mut() { + for tokens in redeleg.tokens.values_mut() { + tokens.slash(rate, infraction_epoch, processing_epoch); + } + } + } +} + +#[derive(Clone, Debug, Default)] +struct IncomingRedeleg { + /// Total amount with all slashes keyed by redelegation source bond start + tokens: BTreeMap, +} +impl IncomingRedeleg { + /// Get the token amount before any slashes that were processed after the + /// redelegation epoch. + fn amount_before_slashing_after_redeleg( + &self, + redeleg_epoch: Epoch, + ) -> token::Amount { + self.tokens + .values() + .map(|tokens| { + tokens.amount_before_slashing_after_redeleg(redeleg_epoch) + }) + .sum() + } + + // Get the token amount before any slashing. + fn amount_before_slashing(&self) -> token::Amount { + self.tokens + .values() + .map(TokensWithSlashes::amount_before_slashing) + .sum() + } +} + +#[derive(Clone, Debug, Default, PartialEq)] +struct TokensWithSlashes { + /// Token amount after any applicable slashing + amount: token::Amount, + /// Total amount that's been slashed associated with the epoch in which the + /// slash was processed. + slashes: BTreeMap, +} + +#[derive(Clone, Debug, Default, PartialEq)] +struct TokensSlash { + amount: token::Amount, + rate: Dec, +} + +impl AddAssign for TokensSlash { + fn add_assign(&mut self, rhs: Self) { + self.amount += rhs.amount; + // Cap the rate at 1 + self.rate = cmp::min(Dec::one(), self.rate + rhs.rate); + } +} + +impl TokensWithSlashes { + /// Slash on original amount before slashes that were processed after the + /// infraction epoch. Returns the slashed amount. + fn slash( + &mut self, + rate: Dec, + infraction_epoch: Epoch, + processing_epoch: Epoch, + ) -> token::Amount { + // Add back slashes to slashable amount that didn't affect this epoch + // (applied after infraction epoch) + let slashable_amount = + self.amount + self.slashes_sum_after_epoch(infraction_epoch); + let amount = cmp::min(slashable_amount.mul_ceil(rate), self.amount); + if !amount.is_zero() { + self.amount -= amount; + let slash = self.slashes.entry(processing_epoch).or_default(); + *slash += TokensSlash { amount, rate }; + } + amount + } + + /// Add the given slashes at their epochs. + fn add_slashes(&mut self, slashes: &BTreeMap) { + for (&epoch, slash) in slashes { + *self.slashes.entry(epoch).or_default() += slash.clone(); + } + } + + /// Subtract the given slash amount in order of the epochs. Returns the + /// removed slashes. + fn subtract_slash( + &mut self, + mut to_slash: token::Amount, + ) -> BTreeMap { + let mut removed = BTreeMap::new(); + self.slashes.retain(|&epoch, slash| { + if to_slash.is_zero() { + return true; + } + if slash.amount > to_slash { + slash.amount -= to_slash; + removed.insert( + epoch, + TokensSlash { + amount: to_slash, + rate: slash.rate, + }, + ); + to_slash = token::Amount::zero(); + true + } else { + to_slash -= slash.amount; + removed.insert(epoch, slash.clone()); + false + } + }); + removed + } + + /// Get the token amount before any slashing. + fn amount_before_slashing(&self) -> token::Amount { + self.amount + self.slashes_sum() + } + + /// Get the token amount before any slashes that were processed after the + /// redelegation epoch. + fn amount_before_slashing_after_redeleg( + &self, + redeleg_epoch: Epoch, + ) -> token::Amount { + let mut amount = self.amount; + for (&processed_epoch, slash) in &self.slashes { + if processed_epoch > redeleg_epoch { + amount += slash.amount; + } + } + amount + } + + /// Get a sum of all slash amounts. + fn slashes_sum(&self) -> token::Amount { + self.slashes + .values() + .map(|TokensSlash { amount, rate: _ }| *amount) + .sum() + } + + /// Get a sum of all slash rates, capped at 1. + fn slash_rates_sum(&self) -> Dec { + cmp::min( + Dec::one(), + self.slashes + .values() + .map(|TokensSlash { amount: _, rate }| *rate) + .sum(), + ) + } + + /// Get a sum of all slashes that were processed after the given epoch. + fn slashes_sum_after_epoch(&self, epoch: Epoch) -> token::Amount { + let mut sum = token::Amount::zero(); + for (&processed_epoch, slash) in &self.slashes { + if processed_epoch > epoch { + sum += slash.amount; + } + } + sum + } + + /// Is the sum of tokens and slashed tokens zero? I.e. Are there no tokens? + fn is_zero(&self) -> bool { + self.amount.is_zero() && self.slashes_sum().is_zero() + } + + /// Get the number of slashes in unique epochs that were processed before or + /// at the given epoch. + fn num_of_slashes(&self, epoch: Epoch) -> u64 { + self.slashes + .keys() + .filter(|&&processed| processed <= epoch) + .count() as u64 + } +} + +#[derive(Clone, Debug, Default)] +struct Unbond { + /// A first epoch from which the unbond is withdrawable. + withdrawable_epoch: Epoch, + /// Bonded amount is the amount that's been bonded originally, reduced by + /// unbonding or slashing, if any. + tokens: TokensWithSlashes, + incoming_redelegs: BTreeMap, +} + +impl Unbond { + /// Get the total unbonded amount before slashing, including any unbonded + /// redelegations. + fn amount_before_slashing(&self) -> token::Amount { + self.tokens.amount_before_slashing() + + self + .incoming_redelegs + .iter() + .fold(token::Amount::zero(), |acc, (_src, redeleg)| { + acc + redeleg.amount_before_slashing() + }) + } + + fn slash( + &mut self, + rate: Dec, + infraction_epoch: Epoch, + processing_epoch: Epoch, + ) { + self.tokens.slash(rate, infraction_epoch, processing_epoch); + for (_src, redeleg) in self.incoming_redelegs.iter_mut() { + for tokens in redeleg.tokens.values_mut() { + tokens.slash(rate, infraction_epoch, processing_epoch); + } + } + } +} + +/// The PoS system under test +#[derive(Derivative)] +#[derivative(Debug)] +struct ConcretePosState { + /// Storage - contains all the PoS state + s: TestWlStorage, + /// Last reference state in debug format to print changes after transitions + #[derivative(Debug = "ignore")] + last_state_diff: DbgPrintDiff, +} + +/// State machine transitions +#[allow(clippy::large_enum_variant)] +#[derive(Clone, Derivative)] +#[derivative(Debug)] +enum Transition { + NextEpoch, + InitValidator { + address: Address, + #[derivative(Debug = "ignore")] + consensus_key: PublicKey, + #[derivative(Debug = "ignore")] + eth_cold_key: PublicKey, + #[derivative(Debug = "ignore")] + eth_hot_key: PublicKey, + commission_rate: Dec, + max_commission_rate_change: Dec, + }, + Bond { + id: BondId, + amount: token::Amount, + }, + Unbond { + id: BondId, + amount: token::Amount, + }, + Withdraw { + id: BondId, + }, + Redelegate { + /// A chained redelegation must fail + is_chained: bool, + id: BondId, + new_validator: Address, + amount: token::Amount, + }, + Misbehavior { + address: Address, + slash_type: SlashType, + infraction_epoch: Epoch, + height: u64, + }, + UnjailValidator { + address: Address, + }, +} + +impl StateMachineTest for ConcretePosState { + type Reference = AbstractPosState; + type SystemUnderTest = Self; + + fn init_test( + initial_state: &::State, + ) -> Self::SystemUnderTest { + tracing::debug!("New test case"); + tracing::debug!( + "Genesis validators: {:#?}", + initial_state + .genesis_validators + .iter() + .map(|val| &val.address) + .collect::>() + ); + let mut s = TestWlStorage::default(); + crate::init_genesis( + &mut s, + &initial_state.params, + initial_state.genesis_validators.clone().into_iter(), + initial_state.epoch, + ) + .unwrap(); + let last_state_diff = DbgPrintDiff::new().store(initial_state); + Self { s, last_state_diff } + } + + fn apply( + mut state: Self::SystemUnderTest, + ref_state: &::State, + transition: ::Transition, + ) -> Self::SystemUnderTest { + tracing::debug!( + "{} {:#?}", + Paint::green("Transition").underline(), + Paint::yellow(&transition) + ); + + if false { + // NOTE: enable to capture and print ref state diff + let new_diff = + state.last_state_diff.print_diff_and_store(ref_state); + state.last_state_diff = new_diff; + } + + pause_for_enter(); + + let params = crate::read_pos_params(&state.s).unwrap(); + let pos_balance = read_balance( + &state.s, + &state.s.storage.native_token, + &crate::ADDRESS, + ) + .unwrap(); + tracing::debug!("PoS balance: {}", pos_balance.to_string_native()); + match transition { + Transition::NextEpoch => { + tracing::debug!("\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 { + address, + consensus_key, + eth_cold_key, + eth_hot_key, + commission_rate, + max_commission_rate_change, + } => { + tracing::debug!("\nCONCRETE Init validator"); + let current_epoch = state.current_epoch(); + + super::become_validator(super::BecomeValidator { + storage: &mut state.s, + params: ¶ms, + address: &address, + consensus_key: &consensus_key, + eth_cold_key: ð_cold_key, + eth_hot_key: ð_hot_key, + current_epoch, + commission_rate, + max_commission_rate_change, + }) + .unwrap(); + + let params = read_pos_params(&state.s).unwrap(); + state.check_init_validator_post_conditions( + current_epoch, + ¶ms, + &address, + ) + } + Transition::Bond { id, amount } => { + tracing::debug!("\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, + current_epoch, + ) + .unwrap(); + let validator_stake_before_bond_pipeline = + crate::read_validator_stake( + &state.s, + ¶ms, + &id.validator, + pipeline, + ) + .unwrap(); + + // Credit tokens to ensure we can apply the bond + let native_token = state.s.get_native_token().unwrap(); + let pos = address::POS; + token::credit_tokens( + &mut state.s, + &native_token, + &id.source, + amount, + ) + .unwrap(); + + let src_balance_pre = + token::read_balance(&state.s, &native_token, &id.source) + .unwrap(); + let pos_balance_pre = + token::read_balance(&state.s, &native_token, &pos).unwrap(); + + // This must be ensured by both transitions generator and + // pre-conditions! + assert!( + crate::is_validator(&state.s, &id.validator).unwrap(), + "{} is not a validator", + id.validator + ); + + // Apply the bond + super::bond_tokens( + &mut state.s, + Some(&id.source), + &id.validator, + amount, + current_epoch, + ) + .unwrap(); + + let params = read_pos_params(&state.s).unwrap(); + state.check_bond_post_conditions( + current_epoch, + ¶ms, + id.clone(), + amount, + validator_stake_before_bond_cur, + validator_stake_before_bond_pipeline, + ); + + let src_balance_post = + token::read_balance(&state.s, &native_token, &id.source) + .unwrap(); + let pos_balance_post = + token::read_balance(&state.s, &native_token, &pos).unwrap(); + + // Post-condition: PoS balance should increase + assert!(pos_balance_pre < pos_balance_post); + // Post-condition: The difference in PoS balance should be the + // same as in the source + assert_eq!( + pos_balance_post - pos_balance_pre, + src_balance_pre - src_balance_post + ); + } + Transition::Unbond { id, amount } => { + tracing::debug!("\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 = + token::read_balance(&state.s, &native_token, &id.source) + .unwrap(); + let pos_balance_pre = + token::read_balance(&state.s, &native_token, &pos).unwrap(); + + let validator_stake_before_unbond_cur = + crate::read_validator_stake( + &state.s, + ¶ms, + &id.validator, + current_epoch, + ) + .unwrap(); + let validator_stake_before_unbond_pipeline = + crate::read_validator_stake( + &state.s, + ¶ms, + &id.validator, + pipeline, + ) + .unwrap(); + + // Apply the unbond + super::unbond_tokens( + &mut state.s, + Some(&id.source), + &id.validator, + amount, + current_epoch, + false, + ) + .unwrap(); + + let params = read_pos_params(&state.s).unwrap(); + state.check_unbond_post_conditions( + current_epoch, + ¶ms, + id.clone(), + amount, + validator_stake_before_unbond_cur, + validator_stake_before_unbond_pipeline, + ); + + let src_balance_post = + token::read_balance(&state.s, &native_token, &id.source) + .unwrap(); + let pos_balance_post = + token::read_balance(&state.s, &native_token, &pos).unwrap(); + + // Post-condition: PoS balance should not change + assert_eq!(pos_balance_pre, pos_balance_post); + // Post-condition: Source balance should not change + assert_eq!(src_balance_post, src_balance_pre); + + // Check that the bonds are the same + // let abs_bonds = ref_state.bonds.get(&id).cloned().unwrap(); + // let conc_bonds = crate::bond_handle(&id.source, + // &id.validator) .get_data_handler() + // .collect_map(&state.s) + // .unwrap(); + // assert_eq!(abs_bonds, conc_bonds); + + // // Check that the unbond records are the same + // // TODO: figure out how we get entries with 0 amount in the + // // abstract version (and prevent) + // let mut abs_unbond_records = ref_state + // .unbond_records + // .get(&id.validator) + // .cloned() + // .unwrap(); + // abs_unbond_records.retain(|_, inner_map| { + // inner_map.retain(|_, value| !value.is_zero()); + // !inner_map.is_empty() + // }); + // let conc_unbond_records = + // crate::total_unbonded_handle(&id.validator) + // .collect_map(&state.s) + // .unwrap(); + // assert_eq!(abs_unbond_records, conc_unbond_records); + } + Transition::Withdraw { + id: BondId { source, validator }, + } => { + tracing::debug!("\nCONCRETE Withdraw"); + let current_epoch = state.current_epoch(); + let native_token = state.s.get_native_token().unwrap(); + let pos = address::POS; + // TODO: add back when slash pool is being used again + // 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, + current_epoch, + ) + .unwrap(); + + let src_balance_post = + token::read_balance(&state.s, &native_token, &source) + .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 equal + // to the sum of the difference in the source and the difference + // in the slash pool + // TODO: needs slash pool + // assert_eq!( + // pos_balance_pre - pos_balance_post, + // 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); + + // Post-condition: The amount withdrawn must match reference + // state withdrawal + let records = ref_state.records(&validator, &source).unwrap(); + let max_slash_round_err = + records.slash_round_err_tolerance(current_epoch); + let ref_withdrawn = + records.withdrawn.get(¤t_epoch).unwrap().amount; + assert!( + ref_withdrawn <= withdrawn + && withdrawn <= ref_withdrawn + max_slash_round_err, + "Expected to withdraw from validator {validator} owner \ + {source} amount {} ({}), but withdrawn {}.", + ref_withdrawn.to_string_native(), + if max_slash_round_err.is_zero() { + "no slashing rounding error expected".to_string() + } else { + format!( + "max slashing rounding error +{}", + max_slash_round_err.to_string_native() + ) + }, + withdrawn.to_string_native(), + ); + } + Transition::Redelegate { + is_chained, + id, + new_validator, + amount, + } => { + tracing::debug!("\nCONCRETE Redelegate"); + + let current_epoch = state.current_epoch(); + let pipeline = current_epoch + params.pipeline_len; + + // Read data prior to applying the transition + let native_token = state.s.get_native_token().unwrap(); + let pos = address::POS; + let pos_balance_pre = + token::read_balance(&state.s, &native_token, &pos).unwrap(); + + // Read validator's redelegations and bonds to find how much of + // them is slashed + let mut amount_after_slash = token::Amount::zero(); + let mut to_redelegate = amount; + + let redelegations_handle = + delegator_redelegated_bonds_handle(&id.source) + .at(&id.validator); + + let bonds: Vec> = + bond_handle(&id.source, &id.validator) + .get_data_handler() + .iter(&state.s) + .unwrap() + .collect(); + 'bonds_loop: for res in bonds.into_iter().rev() { + let (bond_start, bond_delta) = res.unwrap(); + + // Find incoming redelegations at this bond start epoch as a + // redelegation end epoch (the epoch in which it stopped to + // contributing to src) + let redeleg_end = bond_start; + let redeleg_start = + params.redelegation_start_epoch_from_end(redeleg_end); + let redelegations: Vec<_> = redelegations_handle + .at(&redeleg_end) + .iter(&state.s) + .unwrap() + .collect(); + // Iterate incoming redelegations first + for res in redelegations.into_iter().rev() { + let ( + NestedSubKey::Data { + key: src_validator, + nested_sub_key: + SubKey::Data(redeleg_src_bond_start), + }, + delta, + ) = res.unwrap(); + + // Apply slashes on this delta, if any + let mut this_amount_after_slash = delta; + + // Find redelegation source validator's slashes + let slashes = find_slashes_in_range( + &state.s, + redeleg_src_bond_start, + Some(redeleg_end), + &src_validator, + ) + .unwrap(); + for (slash_epoch, rate) in slashes { + // Only apply slashes that weren't processed before + // redelegation as those are applied eagerly + if slash_epoch + + params.slash_processing_epoch_offset() + > redeleg_start + { + let slash = delta.mul_ceil(rate); + this_amount_after_slash = + this_amount_after_slash + .checked_sub(slash) + .unwrap_or_default(); + } + } + // Find redelegation destination validator's slashes + let slashes = find_slashes_in_range( + &state.s, + redeleg_end, + None, + &id.validator, + ) + .unwrap(); + for (_slash_epoch, rate) in slashes { + let slash = delta.mul_ceil(rate); + this_amount_after_slash = this_amount_after_slash + .checked_sub(slash) + .unwrap_or_default(); + } + + if to_redelegate >= delta { + amount_after_slash += this_amount_after_slash; + to_redelegate -= delta; + } else { + // We have to divide this bond in case there are + // slashes + let slash_ratio = + Dec::from(this_amount_after_slash) + / Dec::from(delta); + amount_after_slash += slash_ratio * to_redelegate; + to_redelegate = token::Amount::zero(); + } + + if to_redelegate.is_zero() { + break 'bonds_loop; + } + } + + // Then if there's still something to redelegate, unbond the + // regular bonds + if !to_redelegate.is_zero() { + // Apply slashes on this bond delta, if any + let mut this_amount_after_slash = bond_delta; + + // Find validator's slashes + let slashes = find_slashes_in_range( + &state.s, + bond_start, + None, + &id.validator, + ) + .unwrap(); + for (_slash_epoch, rate) in slashes { + let slash = bond_delta.mul_ceil(rate); + this_amount_after_slash = this_amount_after_slash + .checked_sub(slash) + .unwrap_or_default(); + } + + if to_redelegate >= bond_delta { + amount_after_slash += this_amount_after_slash; + to_redelegate -= bond_delta; + } else { + // We have to divide this bond in case there are + // slashes + let slash_ratio = + Dec::from(this_amount_after_slash) + / Dec::from(bond_delta); + amount_after_slash += slash_ratio * to_redelegate; + to_redelegate = token::Amount::zero(); + } + if to_redelegate.is_zero() { + break; + } + } + } + + // Read src validator stakes + let src_validator_stake_cur_pre = crate::read_validator_stake( + &state.s, + ¶ms, + &id.validator, + current_epoch, + ) + .unwrap(); + let src_validator_stake_pipeline_pre = + crate::read_validator_stake( + &state.s, + ¶ms, + &id.validator, + pipeline, + ) + .unwrap(); + + // Read dest validator stakes + let dest_validator_stake_cur_pre = crate::read_validator_stake( + &state.s, + ¶ms, + &new_validator, + current_epoch, + ) + .unwrap(); + let dest_validator_stake_pipeline_pre = + crate::read_validator_stake( + &state.s, + ¶ms, + &new_validator, + pipeline, + ) + .unwrap(); + + // Find delegations + let delegations_pre = + crate::find_delegations(&state.s, &id.source, &pipeline) + .unwrap(); + + // Apply redelegation + let result = redelegate_tokens( + &mut state.s, + &id.source, + &id.validator, + &new_validator, + current_epoch, + amount, + ); + + if !amount.is_zero() && is_chained { + assert!(result.is_err()); + let err = result.unwrap_err(); + let err_str = err.to_string(); + assert_matches!( + err.downcast::().unwrap().deref(), + RedelegationError::IsChainedRedelegation, + "A chained redelegation must be rejected, got \ + {err_str}", + ); + } else { + result.unwrap(); + + // Post-condition: PoS balance is unchanged + let pos_balance_post = + token::read_balance(&state.s, &native_token, &pos) + .unwrap(); + assert_eq!(pos_balance_pre, pos_balance_post); + + // Post-condition: Source validator stake at current epoch + // is unchanged + let src_validator_stake_cur_post = + crate::read_validator_stake( + &state.s, + ¶ms, + &id.validator, + current_epoch, + ) + .unwrap(); + assert_eq!( + src_validator_stake_cur_pre, + src_validator_stake_cur_post + ); + + // Post-condition: Source validator stake at pipeline epoch + // is reduced by the redelegation amount + + // TODO: shouldn't this be reduced by the redelegation + // amount post-slashing tho? + // NOTE: We changed it to reduce it, check again later + let src_validator_stake_pipeline_post = + crate::read_validator_stake( + &state.s, + ¶ms, + &id.validator, + pipeline, + ) + .unwrap(); + let max_slash_round_err = ref_state + .validator_records + .get(&id.validator) + .map(|r| r.slash_round_err_tolerance(current_epoch)) + .unwrap_or_default(); + let expected_new_stake = src_validator_stake_pipeline_pre + .checked_sub(amount_after_slash) + .unwrap_or_default(); + assert!( + src_validator_stake_pipeline_post + <= expected_new_stake + max_slash_round_err + && expected_new_stake + <= src_validator_stake_pipeline_post + + max_slash_round_err, + "Expected src validator {} stake after redelegation \ + at pipeline to be equal to {} ({}), got {}.", + id.validator, + expected_new_stake.to_string_native(), + if max_slash_round_err.is_zero() { + "no slashing rounding error expected".to_string() + } else { + format!( + "max slashing rounding error +-{}", + max_slash_round_err.to_string_native() + ) + }, + src_validator_stake_pipeline_post.to_string_native() + ); + + // Post-condition: Destination validator stake at current + // epoch is unchanged + let dest_validator_stake_cur_post = + crate::read_validator_stake( + &state.s, + ¶ms, + &new_validator, + current_epoch, + ) + .unwrap(); + assert_eq!( + dest_validator_stake_cur_pre, + dest_validator_stake_cur_post + ); + + // Post-condition: Destination validator stake at pipeline + // epoch is increased by the redelegation amount, less any + // slashes + let expected_new_stake = + dest_validator_stake_pipeline_pre + amount_after_slash; + let dest_validator_stake_pipeline_post = + crate::read_validator_stake( + &state.s, + ¶ms, + &new_validator, + pipeline, + ) + .unwrap(); + assert!( + expected_new_stake + <= dest_validator_stake_pipeline_post + + max_slash_round_err + && dest_validator_stake_pipeline_post + <= expected_new_stake + max_slash_round_err, + "Expected dest validator {} stake after redelegation \ + at pipeline to be equal to {} ({}), got {}.", + new_validator, + expected_new_stake.to_string_native(), + if max_slash_round_err.is_zero() { + "no slashing rounding error expected".to_string() + } else { + format!( + "max slashing rounding error +-{}", + max_slash_round_err.to_string_native() + ) + }, + dest_validator_stake_pipeline_post.to_string_native() + ); + + // Post-condition: The difference at pipeline in src + // validator stake is equal to negative difference in dest + // validator. + assert_eq!( + src_validator_stake_pipeline_pre + - src_validator_stake_pipeline_post, + dest_validator_stake_pipeline_post + - dest_validator_stake_pipeline_pre + ); + + // Post-condition: The delegator's delegations should be + // updated with redelegation. For the source reduced by the + // redelegation amount and for the destination increased by + // the redelegation amount, less any slashes. + let delegations_post = crate::find_delegations( + &state.s, &id.source, &pipeline, + ) + .unwrap(); + let src_delegation_pre = delegations_pre + .get(&id.validator) + .cloned() + .unwrap_or_default(); + let src_delegation_post = delegations_post + .get(&id.validator) + .cloned() + .unwrap_or_default(); + assert_eq!( + src_delegation_pre - src_delegation_post, + amount + ); + let dest_delegation_pre = delegations_pre + .get(&new_validator) + .cloned() + .unwrap_or_default(); + let dest_delegation_post = delegations_post + .get(&new_validator) + .cloned() + .unwrap_or_default(); + let dest_delegation_diff = + dest_delegation_post - dest_delegation_pre; + assert!( + amount_after_slash + <= dest_delegation_diff + max_slash_round_err + && dest_delegation_diff + <= amount_after_slash + max_slash_round_err, + "Expected redelegation by {} to be increased by to {} \ + ({}), but it increased by {}.", + id.source, + amount_after_slash.to_string_native(), + if max_slash_round_err.is_zero() { + "no slashing rounding error expected".to_string() + } else { + format!( + "max slashing rounding error +-{}", + max_slash_round_err.to_string_native() + ) + }, + dest_delegation_diff.to_string_native(), + ); + } + } + Transition::Misbehavior { + address, + slash_type, + infraction_epoch, + height, + } => { + tracing::debug!("\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, + current_epoch.next(), + ) + .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 } => { + tracing::debug!("\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, + ) { + let current_epoch = state.current_epoch(); + let params = read_pos_params(&state.s).unwrap(); + state.check_global_post_conditions(¶ms, current_epoch, ref_state); + } +} + +impl ConcretePosState { + fn current_epoch(&self) -> Epoch { + self.s.storage.block.epoch + } + + fn check_next_epoch_post_conditions(&self, params: &PosParams) { + let pipeline = self.current_epoch() + params.pipeline_len; + let before_pipeline = pipeline.prev(); + + // Post-condition: Consensus validator sets at pipeline offset + // must be the same as at the epoch before it. + let consensus_set_before_pipeline = + crate::read_consensus_validator_set_addresses_with_stake( + &self.s, + before_pipeline, + ) + .unwrap(); + let consensus_set_at_pipeline = + crate::read_consensus_validator_set_addresses_with_stake( + &self.s, pipeline, + ) + .unwrap(); + itertools::assert_equal( + consensus_set_before_pipeline.into_iter().sorted(), + consensus_set_at_pipeline.into_iter().sorted(), + ); + + // Post-condition: Below-capacity validator sets at pipeline + // offset must be the same as at the epoch before it. + let below_cap_before_pipeline = + crate::read_below_capacity_validator_set_addresses_with_stake( + &self.s, + before_pipeline, + ) + .unwrap(); + let below_cap_at_pipeline = + crate::read_below_capacity_validator_set_addresses_with_stake( + &self.s, pipeline, + ) + .unwrap(); + itertools::assert_equal( + 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( + &self, + submit_epoch: Epoch, + params: &PosParams, + id: BondId, + amount: token::Amount, + validator_stake_before_bond_cur: token::Amount, + validator_stake_before_bond_pipeline: token::Amount, + ) { + let pipeline = submit_epoch + params.pipeline_len; + + let cur_stake = super::read_validator_stake( + &self.s, + params, + &id.validator, + submit_epoch, + ) + .unwrap(); + + // Post-condition: the validator stake at the current epoch should not + // change + assert_eq!(cur_stake, validator_stake_before_bond_cur); + + let stake_at_pipeline = super::read_validator_stake( + &self.s, + params, + &id.validator, + pipeline, + ) + .unwrap(); + + // Post-condition: the validator stake at the pipeline should be + // incremented by the bond amount + assert_eq!( + stake_at_pipeline, + validator_stake_before_bond_pipeline + amount + ); + + self.check_bond_and_unbond_post_conditions( + submit_epoch, + params, + id, + stake_at_pipeline, + ); + } + + fn check_unbond_post_conditions( + &self, + submit_epoch: Epoch, + params: &PosParams, + id: BondId, + amount: token::Amount, + validator_stake_before_unbond_cur: token::Amount, + validator_stake_before_unbond_pipeline: token::Amount, + ) { + let pipeline = submit_epoch + params.pipeline_len; + + let cur_stake = super::read_validator_stake( + &self.s, + params, + &id.validator, + submit_epoch, + ) + .unwrap(); + + // Post-condition: the validator stake at the current epoch should not + // change + assert_eq!(cur_stake, validator_stake_before_unbond_cur); + + let stake_at_pipeline = super::read_validator_stake( + &self.s, + params, + &id.validator, + pipeline, + ) + .unwrap(); + + // Post-condition: the validator stake at the pipeline should be + // 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() + ); + + self.check_bond_and_unbond_post_conditions( + submit_epoch, + params, + id, + stake_at_pipeline, + ); + } + + /// These post-conditions apply to bonding and unbonding + fn check_bond_and_unbond_post_conditions( + &self, + submit_epoch: Epoch, + params: &PosParams, + id: BondId, + stake_at_pipeline: token::Amount, + ) { + let pipeline = submit_epoch + params.pipeline_len; + // Read the consensus sets data using iterator + let num_in_consensus = crate::consensus_validator_set_handle() + .at(&pipeline) + .iter(&self.s) + .unwrap() + .map(|res| res.unwrap()) + .filter(|(_keys, addr)| addr == &id.validator) + .count(); + + let num_in_below_cap = crate::below_capacity_validator_set_handle() + .at(&pipeline) + .iter(&self.s) + .unwrap() + .map(|res| res.unwrap()) + .filter(|(_keys, addr)| addr == &id.validator) + .count(); + + let num_in_below_thresh = + read_below_threshold_validator_set_addresses(&self.s, pipeline) + .unwrap() + .into_iter() + .filter(|addr| addr == &id.validator) + .count(); + + let num_occurrences = + num_in_consensus + num_in_below_cap + num_in_below_thresh; + 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( + &self.s, pipeline, + ) + .unwrap(); + let below_cap_set = + crate::read_below_capacity_validator_set_addresses_with_stake( + &self.s, pipeline, + ) + .unwrap(); + let below_thresh_set = + crate::read_below_threshold_validator_set_addresses( + &self.s, pipeline, + ) + .unwrap(); + let weighted = WeightedValidator { + bonded_stake: stake_at_pipeline, + address: id.validator, + }; + let consensus_val = consensus_set.get(&weighted); + let below_cap_val = below_cap_set.get(&weighted); + let below_thresh_val = below_thresh_set.get(&weighted.address); + + // Post-condition: The validator should be updated in exactly once in + // the validator sets + let jailed_condition = validator_is_jailed + && consensus_val.is_none() + && below_cap_val.is_none() + && below_thresh_val.is_none(); + + let mut num_sets = i32::from(consensus_val.is_some()); + num_sets += i32::from(below_cap_val.is_some()); + num_sets += i32::from(below_thresh_val.is_some()); + + assert!(num_sets == 1 || jailed_condition); + + // Post-condition: The stake of the validators in the consensus set is + // greater than or equal to below-capacity validators + for WeightedValidator { + bonded_stake: consensus_stake, + address: consensus_addr, + } in consensus_set.iter() + { + for WeightedValidator { + bonded_stake: below_cap_stake, + address: below_cap_addr, + } in below_cap_set.iter() + { + assert!( + consensus_stake >= below_cap_stake, + "Consensus validator {consensus_addr} with stake {} and \ + below-capacity {below_cap_addr} with stake {} should be \ + swapped.", + consensus_stake.to_string_native(), + below_cap_stake.to_string_native() + ); + } + } + } + + fn check_init_validator_post_conditions( + &self, + submit_epoch: Epoch, + params: &PosParams, + address: &Address, + ) { + let pipeline = submit_epoch + params.pipeline_len; + + // 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_below_threshold_validator_set_addresses( + &self.s, epoch + ) + .unwrap() + .contains(address) + ); + assert!( + !crate::read_all_validator_addresses(&self.s, epoch) + .unwrap() + .contains(address) + ); + } + let in_consensus = + crate::read_consensus_validator_set_addresses(&self.s, pipeline) + .unwrap() + .contains(address); + let in_bc = crate::read_below_capacity_validator_set_addresses( + &self.s, pipeline, + ) + .unwrap() + .contains(address); + let in_below_thresh = + crate::read_below_threshold_validator_set_addresses( + &self.s, pipeline, + ) + .unwrap() + .contains(address); + + assert!(in_below_thresh && !in_consensus && !in_bc); + } + + fn check_misbehavior_post_conditions( + &self, + params: &PosParams, + current_epoch: Epoch, + infraction_epoch: Epoch, + slash_type: SlashType, + validator: &Address, + ) { + tracing::debug!( + "\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, Dec::zero()); + } else { + panic!("Could not find the slash enqueued"); + } + + // 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 pipeline_epoch = current_epoch + params.pipeline_len; + + let num_in_consensus = consensus_validator_set_handle() + .at(&pipeline_epoch) + .iter(&self.s) + .unwrap() + .map(|res| res.unwrap()) + .filter(|(_keys, addr)| addr == validator) + .count(); + + let num_in_bc = below_capacity_validator_set_handle() + .at(&pipeline_epoch) + .iter(&self.s) + .unwrap() + .map(|res| res.unwrap()) + .filter(|(_keys, addr)| addr == validator) + .count(); + + let num_in_bt = read_below_threshold_validator_set_addresses( + &self.s, + pipeline_epoch, + ) + .unwrap() + .into_iter() + .filter(|addr| addr == validator) + .count(); + + let num_occurrences = num_in_consensus + num_in_bc + num_in_bt; + assert_eq!(num_occurrences, 1); + + 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) + || val_state == Some(ValidatorState::BelowThreshold) + ); + } + + 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, + ) { + tracing::debug!("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(); + let max_slash_round_err = ref_state + .validator_records + .get(&validator) + .unwrap() + .slash_round_err_tolerance(epoch); + let ref_stake = ref_state + .validator_stakes + .get(&epoch) + .unwrap() + .get(&validator) + .cloned() + .unwrap(); + let conc_stake = bonded_stake.change(); + let max_err_msg = if max_slash_round_err.is_zero() { + "no error expected".to_string() + } else { + format!( + "max err +-{}", + max_slash_round_err.to_string_native() + ) + }; + tracing::debug!( + "Consensus val {}, set stake: {}, deltas: {}, ref: {}, \ + {max_err_msg}", + &validator, + conc_stake.to_string_native(), + deltas_stake.to_string_native(), + ref_stake.to_string_native(), + ); + assert!(!deltas_stake.is_negative()); + assert_eq!(conc_stake, deltas_stake); + assert!( + ref_stake <= conc_stake + max_slash_round_err.change() + && conc_stake + <= ref_stake + max_slash_round_err.change(), + "Expected {} ({max_err_msg}), got {}.", + ref_stake.to_string_native(), + conc_stake.to_string_native() + ); + + 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(); + let max_slash_round_err = ref_state + .validator_records + .get(&validator) + .unwrap() + .slash_round_err_tolerance(epoch); + let ref_stake = ref_state + .validator_stakes + .get(&epoch) + .unwrap() + .get(&validator) + .cloned() + .unwrap(); + let conc_stake = bonded_stake.change(); + let max_err_msg = if max_slash_round_err.is_zero() { + "no error expected".to_string() + } else { + format!( + "max err +-{}", + max_slash_round_err.to_string_native() + ) + }; + tracing::debug!( + "Below-cap val {}, set stake: {}, deltas: {}, ref: {}, \ + {max_err_msg}", + &validator, + conc_stake.to_string_native(), + deltas_stake.to_string_native(), + ref_stake.to_string_native(), + ); + assert_eq!(conc_stake, deltas_stake); + assert!( + conc_stake <= ref_stake + max_slash_round_err.change() + && ref_stake + <= conc_stake + max_slash_round_err.change(), + "Expected {} ({max_err_msg}), got {}.", + ref_stake.to_string_native(), + bonded_stake.to_string_native() + ); + + 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); + } + + for validator in + crate::read_below_threshold_validator_set_addresses( + &self.s, epoch, + ) + .unwrap() + { + let conc_stake = validator_deltas_handle(&validator) + .get_sum(&self.s, epoch, params) + .unwrap() + .unwrap_or_default(); + + let state = crate::validator_state_handle(&validator) + .get(&self.s, epoch, params) + .unwrap() + .unwrap(); + + assert_eq!(state, ValidatorState::BelowThreshold); + assert_eq!( + state, + ref_state + .validator_states + .get(&epoch) + .unwrap() + .get(&validator) + .cloned() + .unwrap() + ); + let max_slash_round_err = ref_state + .validator_records + .get(&validator) + .map(|r| r.slash_round_err_tolerance(epoch)) + .unwrap_or_default(); + let ref_stake = ref_state + .validator_stakes + .get(&epoch) + .unwrap() + .get(&validator) + .cloned() + .unwrap(); + let max_err_msg = if max_slash_round_err.is_zero() { + "no error expected".to_string() + } else { + format!( + "max err +-{}", + max_slash_round_err.to_string_native() + ) + }; + tracing::debug!( + "Below-thresh val {}, deltas: {}, ref: {}, {max_err_msg})", + &validator, + conc_stake.to_string_native(), + ref_stake.to_string_native(), + ); + assert!( + conc_stake <= ref_stake + max_slash_round_err.change() + && ref_stake + <= conc_stake + max_slash_round_err.change(), + "Expected {} ({max_err_msg}), got {}.", + ref_stake.to_string_native(), + conc_stake.to_string_native() + ); + 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 conc_stake = validator_deltas_handle(&val) + .get_sum(&self.s, epoch, params) + .unwrap() + .unwrap_or_default(); + let max_slash_round_err = ref_state + .validator_records + .get(&val) + .map(|r| r.slash_round_err_tolerance(epoch)) + .unwrap_or_default(); + let max_err_msg = if max_slash_round_err.is_zero() { + "no error expected".to_string() + } else { + format!( + "max err +-{}", + max_slash_round_err.to_string_native() + ) + }; + let ref_stake = ref_state + .validator_stakes + .get(&epoch) + .unwrap() + .get(&val) + .cloned() + .unwrap(); + tracing::debug!( + "Jailed val {}, deltas: {}, ref: {}, {max_err_msg}", + &val, + conc_stake.to_string_native(), + ref_stake.to_string_native(), + ); + + assert_eq!( + state, + ref_state + .validator_states + .get(&epoch) + .unwrap() + .get(&val) + .cloned() + .unwrap() + ); + assert!( + conc_stake <= ref_stake + max_slash_round_err.change() + && ref_stake + <= conc_stake + max_slash_round_err.change(), + "Expected {} ({}), got {}.", + ref_stake.to_string_native(), + max_err_msg, + conc_stake.to_string_native() + ); + assert!(!vals.contains(&val)); + } + } + } + + // Check that validator stakes are matching ref_state + for (validator, records) in &ref_state.validator_records { + // On every epoch from current up to pipeline + for epoch in current_epoch.iter_range(params.pipeline_len) { + let ref_stake = records.stake(epoch); + let conc_stake = crate::read_validator_stake( + &self.s, params, validator, epoch, + ) + .unwrap(); + let max_slash_round_err = + records.slash_round_err_tolerance(epoch); + assert!( + ref_stake <= conc_stake + max_slash_round_err + && conc_stake <= ref_stake + max_slash_round_err, + "Stake for validator {validator} in epoch {epoch} is not \ + matched against reference stake. Expected {} ({}), got \ + {}.", + ref_stake.to_string_native(), + if max_slash_round_err.is_zero() { + "no slashing rounding error expected".to_string() + } else { + format!( + "max slashing rounding error +-{}", + max_slash_round_err.to_string_native() + ) + }, + conc_stake.to_string_native() + ); + } + } + // TODO: expand above to include jailed validators + + for (validator, records) in &ref_state.validator_records { + for (source, records) in &records.per_source { + let bond_id = BondId { + source: source.clone(), + validator: validator.clone(), + }; + for epoch in current_epoch.iter_range(params.pipeline_len) { + let max_slash_round_err = + records.slash_round_err_tolerance(epoch); + let conc_bond_amount = + crate::bond_amount(&self.s, &bond_id, epoch).unwrap(); + let ref_bond_amount = records.amount(epoch); + assert!( + ref_bond_amount + <= conc_bond_amount + max_slash_round_err + && conc_bond_amount + <= ref_bond_amount + max_slash_round_err, + "Slashed `bond_amount` for validator {validator} in \ + epoch {epoch} is not matched against reference \ + state. Expected {} ({}), got {}.", + ref_bond_amount.to_string_native(), + if max_slash_round_err.is_zero() { + "no slashing rounding error expected".to_string() + } else { + format!( + "max slashing rounding error +-{}", + max_slash_round_err.to_string_native() + ) + }, + conc_bond_amount.to_string_native() + ); + } + } + } + } +} + +impl ReferenceStateMachine for AbstractPosState { + type State = Self; + type Transition = Transition; + + fn init_state() -> BoxedStrategy { + tracing::debug!("\nInitializing abstract state machine"); + arb_params_and_genesis_validators(Some(8), 8..10) + .prop_map(|(params, genesis_validators)| { + let epoch = Epoch::default(); + let mut state = Self { + epoch, + params, + genesis_validators: genesis_validators + .into_iter() + // Sorted by stake to fill in the consensus set first + .sorted_by(|a, b| Ord::cmp(&a.tokens, &b.tokens)) + .rev() + .collect(), + validator_records: Default::default(), + validator_stakes: Default::default(), + consensus_set: Default::default(), + below_capacity_set: Default::default(), + below_threshold_set: Default::default(), + validator_states: Default::default(), + validator_slashes: Default::default(), + enqueued_slashes: Default::default(), + validator_last_slash_epochs: Default::default(), + }; + + for GenesisValidator { + address, + tokens, + consensus_key: _, + eth_cold_key: _, + eth_hot_key: _, + commission_rate: _, + max_commission_rate_change: _, + } in state.genesis_validators.clone() + { + let records = state.records_mut(&address, &address); + let bond_at_start = records.bonds.entry(epoch).or_default(); + bond_at_start.tokens.amount = tokens; + + let total_stakes = + state.validator_stakes.entry(epoch).or_default(); + total_stakes + .insert(address.clone(), token::Change::from(tokens)); + + let consensus_set = + state.consensus_set.entry(epoch).or_default(); + let consensus_vals_len = consensus_set + .iter() + .map(|(_stake, validators)| validators.len() as u64) + .sum(); + + if tokens < state.params.validator_stake_threshold { + state + .below_threshold_set + .entry(epoch) + .or_default() + .insert(address.clone()); + state + .validator_states + .entry(epoch) + .or_default() + .insert(address, ValidatorState::BelowThreshold); + } else if state.params.max_validator_slots + > consensus_vals_len + { + state + .validator_states + .entry(epoch) + .or_default() + .insert(address.clone(), ValidatorState::Consensus); + consensus_set + .entry(tokens) + .or_default() + .push_back(address); + } else { + state + .validator_states + .entry(epoch) + .or_default() + .insert( + address.clone(), + ValidatorState::BelowCapacity, + ); + let below_cap_set = + state.below_capacity_set.entry(epoch).or_default(); + below_cap_set + .entry(ReverseOrdTokenAmount(tokens)) + .or_default() + .push_back(address) + }; + } + // Ensure that below-capacity and below-threshold sets are + // initialized even if empty + state.below_capacity_set.entry(epoch).or_default(); + state.below_threshold_set.entry(epoch).or_default(); + + // Copy validator sets up to pipeline epoch + for epoch in epoch.next().iter_range(state.params.pipeline_len) + { + state.copy_discrete_epoched_data(epoch) + } + 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.unbondable_bonds().into_iter().collect::>(); + let redelegatable = + state.redelegatable_bonds().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![ + 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(), + key::testing::arb_common_secp256k1_keypair(), + key::testing::arb_common_secp256k1_keypair(), + arb_rate(), + arb_rate(), + ) + .prop_map( + |( + addr, + consensus_key, + eth_hot_key, + eth_cold_key, + commission_rate, + max_commission_rate_change, + )| { + Transition::InitValidator { + address: Address::Established(addr), + consensus_key: consensus_key.to_public(), + eth_hot_key: eth_hot_key.to_public(), + eth_cold_key: eth_cold_key.to_public(), + commission_rate, + max_commission_rate_change, + } + }, + ), + 1 => arb_slash(state), + ]; + + // Add unjailing, if any eligible + let transitions = if eligible_for_unjail.is_empty() { + basic.boxed() + } else { + prop_oneof![ + // basic 6x more likely as it's got 6 cases + 6 => basic, + 1 => 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 = + arb_unbondable.prop_flat_map(move |(id, bonds_sum)| { + let bonds_sum: i128 = + TryFrom::try_from(bonds_sum.change()).unwrap(); + (0..bonds_sum).prop_map(move |to_unbond| { + let id = id.clone(); + let amount = + token::Amount::from_change(Change::from(to_unbond)); + Transition::Unbond { id, amount } + }) + }); + prop_oneof![ + 7 => transitions, + 1 => arb_unbond, + ] + .boxed() + }; + + // Add withdrawals, if any + let transitions = 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![ + 8 => transitions, + 1 => arb_withdrawal, + ] + .boxed() + }; + + // Add redelegations, if any + if redelegatable.is_empty() { + transitions + } else { + let arb_redelegatable = prop::sample::select(redelegatable); + let validators = state + .validator_states + .get(&state.pipeline()) + .unwrap() + .keys() + .cloned() + .collect::>(); + let unchainable_redelegations = state.unchainable_redelegations(); + let arb_redelegation = + arb_redelegatable.prop_flat_map(move |(id, deltas_sum)| { + let deltas_sum = + i128::try_from(deltas_sum.change()).unwrap(); + // Generate an amount to redelegate, up to the sum + assert!( + deltas_sum > 0, + "Bond {id} deltas_sum must be non-zero" + ); + let arb_amount = (0..deltas_sum).prop_map(|to_unbond| { + token::Amount::from_change(Change::from(to_unbond)) + }); + // Generate a new validator for redelegation + let current_validator = id.validator.clone(); + let new_validators = validators + .iter() + // The validator must be other than the current + .filter(|validator| *validator != ¤t_validator) + .cloned() + .collect::>(); + let arb_new_validator = + prop::sample::select(new_validators); + let unchainable_redelegations = + unchainable_redelegations.clone(); + (arb_amount, arb_new_validator).prop_map( + move |(amount, new_validator)| Transition::Redelegate { + is_chained: Self::is_chained_redelegation( + &unchainable_redelegations, + &id.source, + &id.validator, + ), + id: id.clone(), + new_validator, + amount, + }, + ) + }); + prop_oneof![ + 9 => transitions, + // Cranked up to make redelegations more common + 15 => arb_redelegation, + ] + .boxed() + } + } + + fn apply( + mut state: Self::State, + transition: &Self::Transition, + ) -> Self::State { + match transition { + Transition::NextEpoch => { + state.epoch = state.epoch.next(); + tracing::debug!("Starting epoch {}", state.epoch); + + // Copy the non-delta data into pipeline epoch from its pred. + 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, + consensus_key: _, + eth_cold_key: _, + eth_hot_key: _, + commission_rate: _, + max_commission_rate_change: _, + } => { + let pipeline: Epoch = state.pipeline(); + + // Initialize the stake at pipeline + state + .validator_stakes + .entry(pipeline) + .or_default() + .insert(address.clone(), 0_i128.into()); + + // Insert into the below-threshold set at pipeline since the + // initial stake is 0 + state + .below_threshold_set + .entry(pipeline) + .or_default() + .insert(address.clone()); + state + .validator_states + .entry(pipeline) + .or_default() + .insert(address.clone(), ValidatorState::BelowThreshold); + + state.debug_validators(); + } + Transition::Bond { id, amount } => { + if !amount.is_zero() { + state.bond(id, *amount); + state.debug_validators(); + } + } + Transition::Unbond { id, amount } => { + if !amount.is_zero() { + state.unbond(id, *amount); + state.debug_validators(); + } + } + Transition::Withdraw { id } => { + state.withdraw(id); + } + Transition::Redelegate { + is_chained, + id, + new_validator, + amount, + } => { + if *is_chained { + return state; + } + if !amount.is_zero() { + state.redelegate(id, new_validator, *amount); + state.debug_validators(); + } + } + Transition::Misbehavior { + address, + slash_type, + infraction_epoch, + height, + } => { + let current_epoch = state.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: Dec::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 if state + .is_in_below_threshold(address, current_epoch + offset) + { + let removed = state + .below_threshold_set + .entry(current_epoch + offset) + .or_default() + .remove(address); + debug_assert!(removed); + } 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(); + 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 pipeline_stake + < state.params.validator_stake_threshold.change() + { + // Place into the below-threshold set + let below_threshold_set_pipeline = state + .below_threshold_set + .entry(pipeline_epoch) + .or_default(); + below_threshold_set_pipeline.insert(address.clone()); + validator_states_pipeline.insert( + address.clone(), + ValidatorState::BelowThreshold, + ); + } else 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 + } + + fn preconditions( + state: &Self::State, + transition: &Self::Transition, + ) -> bool { + match transition { + // TODO: should there be any slashing preconditions for `NextEpoch`? + Transition::NextEpoch => true, + Transition::InitValidator { + address, + consensus_key: _, + eth_cold_key: _, + eth_hot_key: _, + commission_rate: _, + max_commission_rate_change: _, + } => { + 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.unbondable_bonds().into_iter().any(|(id, _sum)| + &id.source == address) + } + Transition::Bond { id, amount: _ } => { + let pipeline = state.pipeline(); + // The validator must be known + 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) + } + Transition::Unbond { id, amount } => { + let pipeline = state.pipeline(); + + let is_unbondable = state + .unbondable_bonds() + .get(id) + .map(|sum| sum >= 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 { + // tracing::debug!( + // "\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 and the validator not jailed + && is_unbondable && !is_frozen + } + Transition::Withdraw { id } => { + let pipeline = state.pipeline(); + + let is_withdrawable = state + .withdrawable_unbonds() + .get(id) + .map(|amount| *amount > token::Amount::zero()) + .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_jailed + } + Transition::Redelegate { + is_chained, + id, + new_validator, + amount, + } => { + let pipeline = state.pipeline(); + + if *is_chained { + Self::is_chained_redelegation( + &state.unchainable_redelegations(), + &id.source, + new_validator, + ) + } else { + // The src and dest validator must be known + if !state.is_validator(&id.validator, pipeline) + || !state.is_validator(new_validator, pipeline) + { + return false; + } + + // The amount must be available to redelegate + if !state + .unbondable_bonds() + .get(id) + .map(|sum| sum >= amount) + .unwrap_or_default() + { + return false; + } + + // The src validator must not be frozen + if let Some(last_epoch) = + state.validator_last_slash_epochs.get(&id.validator) + { + if *last_epoch + + state.params.unbonding_len + + 1u64 + + state.params.cubic_slashing_window_length + > state.epoch + { + return false; + } + } + + // The dest validator must not be frozen + if let Some(last_epoch) = + state.validator_last_slash_epochs.get(new_validator) + { + if *last_epoch + + state.params.unbonding_len + + 1u64 + + state.params.cubic_slashing_window_length + > state.epoch + { + return false; + } + } + + true + } + } + 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 = || { + let num_of_honest = state + .validator_states + .get(&state.pipeline()) + .unwrap() + .iter() + .filter(|(_addr, val_state)| match val_state { + ValidatorState::Consensus + | ValidatorState::BelowCapacity => true, + ValidatorState::Inactive + | ValidatorState::Jailed + // Below threshold cannot be in consensus + | ValidatorState::BelowThreshold => false, + }) + .count(); + + // Find the number of enqueued slashes to unique validators + let num_of_enquequed_slashes = state + .enqueued_slashes + .iter() + // find all validators with any enqueued slashes + .fold(BTreeSet::new(), |mut acc, (&epoch, slashes)| { + if epoch > current_epoch { + acc.extend(slashes.keys().cloned()); + } + acc + }) + .len(); + + num_of_honest - num_of_enquequed_slashes > 3 + }; + + // Ensure that the validator is in consensus when it misbehaves + // TODO: possibly also test allowing below-capacity validators + // tracing::debug!("\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 + tracing::debug!( + "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(); + tracing::debug!( + "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? + } + } + } +} + +/// Arbitrary bond transition that adds tokens to an existing bond +fn add_arb_bond_amount( + state: &AbstractPosState, +) -> impl Strategy { + let bond_ids = state.existing_bond_ids(); + let arb_bond_id = prop::sample::select(bond_ids); + (arb_bond_id, arb_bond_amount()) + .prop_map(|(id, amount)| Transition::Bond { id, amount }) +} + +/// Arbitrary delegation to one of the validators +fn arb_delegation( + state: &AbstractPosState, +) -> impl Strategy { + // 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| { + !validators.contains(addr) + }); + let arb_validator = prop::sample::select(validator_vec); + (arb_source, arb_validator, arb_bond_amount()).prop_map( + |(source, validator, amount)| Transition::Bond { + id: BondId { source, validator }, + amount, + }, + ) +} + +/// 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 (in micro units) to avoid overflows +pub fn arb_bond_amount() -> impl Strategy { + (1_u64..10).prop_map(|val| token::Amount::from_uint(val, 0).unwrap()) +} + +/// 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, + }, + ) +} diff --git a/proof_of_stake/src/tests/utils.rs b/proof_of_stake/src/tests/utils.rs new file mode 100644 index 0000000000..1e5f5acf62 --- /dev/null +++ b/proof_of_stake/src/tests/utils.rs @@ -0,0 +1,81 @@ +use std::marker::PhantomData; +use std::str::FromStr; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering::Relaxed; +use std::{env, fmt}; + +// TODO: allow custom fmt fn +#[derive(Clone)] +pub struct DbgPrintDiff +where + T: fmt::Debug, +{ + last: String, + phantom_t: PhantomData, +} +impl DbgPrintDiff +where + T: fmt::Debug, +{ + pub fn new() -> Self { + Self { + last: Default::default(), + phantom_t: PhantomData, + } + } + + /// Store a state in dbg format string + pub fn store(&self, data: &T) -> Self { + Self { + last: Self::fmt_data(data), + phantom_t: PhantomData, + } + } + + /// Diff a state in dbg format string against the stored state + pub fn print_diff_and_store(&self, data: &T) -> Self { + let dbg_str = Self::fmt_data(data); + println!( + "{}", + pretty_assertions::StrComparison::new(&self.last, &dbg_str,) + ); + Self { + last: dbg_str, + phantom_t: PhantomData, + } + } + + fn fmt_data(data: &T) -> String { + format!("{:#?}", data) + } +} + +const ENV_VAR_TEST_PAUSES: &str = "TEST_PAUSES"; + +pub fn pause_for_enter() { + if paused_enabled() { + println!("Press Enter to continue"); + let mut input = String::new(); + std::io::stdin().read_line(&mut input).unwrap(); + } +} + +fn paused_enabled() -> bool { + // Cache the result of reading the environment variable + static ENABLED: AtomicUsize = AtomicUsize::new(0); + match ENABLED.load(Relaxed) { + 0 => {} + 1 => return false, + _ => return true, + } + let enabled: bool = matches!( + env::var(ENV_VAR_TEST_PAUSES).map(|val| { + FromStr::from_str(&val).unwrap_or_else(|_| { + panic!("Expected a bool for {ENV_VAR_TEST_PAUSES} env var.") + }) + }), + Ok(true), + ); + ENABLED.store(enabled as usize + 1, Relaxed); + enabled +} diff --git a/proof_of_stake/src/types.rs b/proof_of_stake/src/types.rs index 736ffe7a46..8477b21cf2 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; +use std::collections::{BTreeMap, HashMap}; use std::convert::TryFrom; use std::fmt::Display; use std::hash::Hash; @@ -149,7 +149,7 @@ pub type CommissionRates = /// Epoched validator's bonds pub type Bonds = crate::epoched::EpochedDelta< - token::Change, + token::Amount, crate::epoched::OffsetPipelineLen, U64_MAX, >; @@ -176,6 +176,10 @@ pub type EpochedSlashes = crate::epoched::NestedEpoched< >; /// Epoched validator's unbonds +/// +/// The map keys from outside in are: +/// - start epoch of the bond in which it started contributing to stake +/// - withdrawable epoch of the unbond pub type Unbonds = NestedMap>; /// Consensus keys set, used to ensure uniqueness @@ -186,17 +190,104 @@ pub type ConsensusKeys = LazySet; /// (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 = +pub type ValidatorTotalUnbonded = NestedMap>; +/// A validator's incoming redelegations, where the key is the bond owner +/// address and the value is the redelegation end epoch +pub type IncomingRedelegations = LazyMap; + +/// A validator's outgoing redelegations, where the validator in question is a +/// source validator. +/// +/// The map keys from outside in are: +/// - destination validator's address +/// - bond start epoch +/// - redelegation epoch in which it started contributing to destination +/// validator +/// +/// The value is the redelegated bond amount. +pub type OutgoingRedelegations = + NestedMap>>; + +/// A validator's total redelegated unbonded tokens for any delegator. +/// The map keys from outside in are: +/// +/// - redelegation epoch in which it started contributing to destination +/// validator +/// - redelegation source validator +/// - start epoch of the bond that's been redelegated +pub type TotalRedelegatedBonded = NestedMap; + +/// A validator's total redelegated unbonded tokens for any delegator. +/// The map keys from outside in are: +/// +/// - unbond epoch +/// - redelegation epoch in which it started contributing to destination +/// validator +/// - redelegation source validator +/// - bond start epoch +pub type TotalRedelegatedUnbonded = NestedMap; + +/// Map of redelegated tokens. +/// The map keys from outside in are: +/// +/// - redelegation source validator +/// - start epoch of the bond that's been redelegated +pub type RedelegatedTokens = NestedMap>; + +/// Map of redelegated bonds or unbonds. +/// The map keys from outside in are: +/// +/// - for bonds redelegation epoch in which the redelegation started +/// contributing to destination validator, for unbonds it's withdrawal epoch +/// - redelegation source validator +/// - start epoch of the bond that's been redelegated +/// +/// TODO: it's a confusing that the outermost epoch is different for bonds vs +/// unbonds, can we swap withdrawal with redelegation epoch for +/// `DelegatorRedelegatedUnbonded`? +pub type RedelegatedBondsOrUnbonds = NestedMap; + +/// A delegator's redelegated bonded token amount. +/// The map keys from outside in are: +/// +/// - redelegation destination validator +/// - redelegation epoch in which the redelegation started contributing to +/// destination validator +/// - redelegation source validator +/// - start epoch of the bond that's been redelegated +pub type DelegatorRedelegatedBonded = + NestedMap; + +/// A delegator's redelegated unbonded token amounts. +/// The map keys from outside in are: +/// +/// - redelegation destination validator +/// - redelegation epoch in which the redelegation started contributing to +/// destination validator +/// - withdrawal epoch of the unbond +/// - redelegation source validator +/// - start epoch of the bond that's been redelegated +pub type DelegatorRedelegatedUnbonded = + NestedMap>; + +/// In-memory map of redelegated bonds. +/// The map keys from outside in are: +/// +/// - src validator address +/// - src bond start epoch where it started contributing to src validator +pub type EagerRedelegatedBondsMap = + BTreeMap>; + #[derive( Debug, Clone, BorshSerialize, BorshDeserialize, Eq, Hash, PartialEq, )] -/// TODO: slashed amount for thing +/// Slashed amount of tokens. pub struct SlashedAmount { - /// Perlangus + /// Amount of tokens that were slashed. pub amount: token::Amount, - /// Churms + /// Infraction epoch from which the tokens were slashed pub epoch: Epoch, } @@ -216,6 +307,20 @@ pub type RewardsProducts = LazyMap; /// rewards owed over the course of an epoch) pub type RewardsAccumulator = LazyMap; +/// Eager data for a generic redelegation +#[derive(Debug)] +pub struct Redelegation { + /// Start epoch of the redelegation is the first epoch in which the + /// redelegated amount no longer contributes to the stake of source + /// validator and starts contributing to destination validator. + pub redel_bond_start: Epoch, + /// Source validator + pub src_validator: Address, + /// Start epoch of the redelgated bond + pub bond_start: Epoch, + /// Redelegation amount + pub amount: token::Amount, +} // -------------------------------------------------------------------------------------------- /// A genesis validator definition. diff --git a/sdk/src/args.rs b/sdk/src/args.rs index de4fd5bb98..36e32df04a 100644 --- a/sdk/src/args.rs +++ b/sdk/src/args.rs @@ -926,6 +926,33 @@ impl Unbond { } } +/// Redelegation arguments +#[derive(Clone, Debug)] +pub struct Redelegate { + /// Common tx arguments + pub tx: Tx, + /// Source validator address + pub src_validator: C::Address, + /// Destination validator address + pub dest_validator: C::Address, + /// Owner of the bonds that are being redelegated + pub owner: C::Address, + /// The amount of tokens to redelegate + pub amount: token::Amount, + /// Path to the TX WASM code file + pub tx_code_path: PathBuf, +} + +impl Redelegate { + /// Build a transaction from this builder + pub async fn build<'a>( + &self, + context: &impl Namada<'a>, + ) -> crate::error::Result<(crate::proto::Tx, SigningTxData)> { + tx::build_redelegation(context, self).await + } +} + /// Reveal public key #[derive(Clone, Debug)] pub struct RevealPk { diff --git a/sdk/src/error.rs b/sdk/src/error.rs index dba1898615..9f84195cc2 100644 --- a/sdk/src/error.rs +++ b/sdk/src/error.rs @@ -172,6 +172,12 @@ pub enum TxError { /// Error retrieving from storage #[error("Error retrieving from storage")] Retrieval, + /// Bond amount is zero + #[error("The requested bond amount is 0.")] + BondIsZero, + /// Unond amount is zero + #[error("The requested unbond amount is 0.")] + UnbondIsZero, /// No unbonded bonds ready to withdraw in the current epoch #[error( "There are no unbonded bonds ready to withdraw in the current epoch \ @@ -282,6 +288,28 @@ pub enum TxError { /// Invalid owner account #[error("The source account {0} is not valid or doesn't exist.")] InvalidAccount(String), + /// The redelegation amount is larger than the remaining bond amount + #[error( + "The redelegation amount is larger than the remaining bond amount. \ + Amount to redelegate is {0} and the remaining bond amount is {1}." + )] + RedelegationAmountTooLarge(String, String), + /// The redelegation amount is 0 + #[error("The amount requested to redelegate is 0 tokens")] + RedelegationIsZero, + /// The src and dest validators are the same + #[error("The source and destination validators are the same")] + RedelegationSrcEqDest, + /// The redelegation owner is a validator + #[error("The redelegation owner {0} is a validator")] + RedelegatorIsValidator(Address), + /// There is an incoming redelegation that is still subject to possible + /// slashing + #[error( + "An incoming redelegation from delegator {0} to validator {1} is \ + still subject to possible slashing" + )] + IncomingRedelIsStillSlashable(Address, Address), /// Other Errors that may show up when using the interface #[error("{0}")] Other(String), diff --git a/sdk/src/queries/vp/pos.rs b/sdk/src/queries/vp/pos.rs index 875779447d..454680f1f8 100644 --- a/sdk/src/queries/vp/pos.rs +++ b/sdk/src/queries/vp/pos.rs @@ -23,13 +23,12 @@ use namada_proof_of_stake::{ read_consensus_validator_set_addresses_with_stake, read_pos_params, read_total_stake, read_validator_max_commission_rate_change, read_validator_stake, unbond_handle, validator_commission_rate_handle, - validator_slashes_handle, validator_state_handle, + validator_incoming_redelegations_handle, validator_slashes_handle, + validator_state_handle, }; use crate::queries::types::RequestCtx; -type AmountPair = (token::Amount, token::Amount); - // PoS validity predicate queries router! {POS, ( "validator" ) = { @@ -49,6 +48,9 @@ router! {POS, ( "state" / [validator: Address] / [epoch: opt Epoch] ) -> Option = validator_state, + + ( "incoming_redelegation" / [src_validator: Address] / [delegator: Address] ) + -> Option = validator_incoming_redelegation, }, ( "validator_set" ) = { @@ -79,7 +81,7 @@ router! {POS, -> token::Amount = bond, ( "bond_with_slashing" / [source: Address] / [validator: Address] / [epoch: opt Epoch] ) - -> AmountPair = bond_with_slashing, + -> token::Amount = bond_with_slashing, ( "unbond" / [source: Address] / [validator: Address] ) -> HashMap<(Epoch, Epoch), token::Amount> = unbond, @@ -264,7 +266,28 @@ where { let epoch = epoch.unwrap_or(ctx.wl_storage.storage.last_epoch); let params = read_pos_params(ctx.wl_storage)?; - read_validator_stake(ctx.wl_storage, ¶ms, &validator, epoch) + if namada_proof_of_stake::is_validator(ctx.wl_storage, &validator)? { + let stake = + read_validator_stake(ctx.wl_storage, ¶ms, &validator, epoch)?; + Ok(Some(stake)) + } else { + Ok(None) + } +} + +/// Get the incoming redelegation epoch for a source validator - delegator pair, +/// if there is any. +fn validator_incoming_redelegation( + ctx: RequestCtx<'_, D, H, V, T>, + src_validator: Address, + delegator: Address, +) -> storage_api::Result> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + let handle = validator_incoming_redelegations_handle(&src_validator); + handle.get(ctx.wl_storage, &delegator) } /// Get all the validator in the consensus set with their bonded stake. @@ -314,7 +337,7 @@ fn bond_deltas( ctx: RequestCtx<'_, D, H, V, T>, source: Address, validator: Address, -) -> storage_api::Result> +) -> storage_api::Result> where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, @@ -341,7 +364,6 @@ where let handle = bond_handle(&source, &validator); handle .get_sum(ctx.wl_storage, epoch, ¶ms)? - .map(token::Amount::from_change) .ok_or_err_msg("Cannot find bond") } @@ -350,7 +372,7 @@ fn bond_with_slashing( source: Address, validator: Address, epoch: Option, -) -> storage_api::Result +) -> storage_api::Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, diff --git a/sdk/src/queries/vp/token.rs b/sdk/src/queries/vp/token.rs index 0a2a5df509..5e2e5aa95b 100644 --- a/sdk/src/queries/vp/token.rs +++ b/sdk/src/queries/vp/token.rs @@ -52,7 +52,7 @@ pub mod client_only_methods { .await?; let balance = if response.data.is_empty() { - token::Amount::default() + token::Amount::zero() } else { token::Amount::try_from_slice(&response.data) .unwrap_or_default() diff --git a/sdk/src/rpc.rs b/sdk/src/rpc.rs index f302574aca..b163aa2723 100644 --- a/sdk/src/rpc.rs +++ b/sdk/src/rpc.rs @@ -732,6 +732,21 @@ pub async fn query_commission_rate( ) } +/// Query and return the incoming redelegation epoch for a given pair of source +/// validator and delegator, if there is any. +pub async fn query_incoming_redelegations( + client: &C, + src_validator: &Address, + delegator: &Address, +) -> Result, Error> { + convert_response::>( + RPC.vp() + .pos() + .validator_incoming_redelegation(client, src_validator, delegator) + .await, + ) +} + /// Query a validator's bonds for a given epoch pub async fn query_bond( client: &C, @@ -788,7 +803,7 @@ pub async fn query_and_print_unbonds<'a>( query_unbond_with_slashing(context.client(), source, validator).await?; let current_epoch = query_epoch(context.client()).await?; - let mut total_withdrawable = token::Amount::default(); + let mut total_withdrawable = token::Amount::zero(); let mut not_yet_withdrawable = HashMap::::new(); for ((_start_epoch, withdraw_epoch), amount) in unbonds.into_iter() { if withdraw_epoch <= current_epoch { @@ -799,7 +814,7 @@ pub async fn query_and_print_unbonds<'a>( *withdrawable_amount += amount; } } - if total_withdrawable != token::Amount::default() { + if !total_withdrawable.is_zero() { display_line!( context.io(), "Total withdrawable now: {}.", @@ -871,14 +886,14 @@ pub async fn get_bond_amount_at( delegator: &Address, validator: &Address, epoch: Epoch, -) -> Result, error::Error> { - let (_total, total_active) = convert_response::( +) -> Result { + let total_active = convert_response::( RPC.vp() .pos() .bond_with_slashing(client, delegator, validator, &Some(epoch)) .await, )?; - Ok(Some(total_active)) + Ok(total_active) } /// Get bonds and unbonds with all details (slashes and rewards, if any) diff --git a/sdk/src/signing.rs b/sdk/src/signing.rs index d9b6d18854..3c8bf51c77 100644 --- a/sdk/src/signing.rs +++ b/sdk/src/signing.rs @@ -355,7 +355,7 @@ pub async fn wrap_tx<'a, N: Namada<'a>>( if !args.force { return Err(e); } else { - token::Amount::default() + token::Amount::zero() } } }; diff --git a/sdk/src/tx.rs b/sdk/src/tx.rs index b3abe954d3..0c6b1d5428 100644 --- a/sdk/src/tx.rs +++ b/sdk/src/tx.rs @@ -806,6 +806,160 @@ pub async fn build_unjail_validator<'a>( .map(|(tx, epoch)| (tx, signing_data, epoch)) } +/// Redelegate bonded tokens from one validator to another +pub async fn build_redelegation<'a>( + context: &impl Namada<'a>, + args::Redelegate { + tx: tx_args, + src_validator, + dest_validator, + owner, + amount: redel_amount, + tx_code_path, + }: &args::Redelegate, +) -> Result<(Tx, SigningTxData)> { + // Require a positive amount of tokens to be redelegated + if redel_amount.is_zero() { + edisplay_line!( + context.io(), + "The requested redelegation amount is 0. A positive amount must \ + be requested." + ); + if !tx_args.force { + return Err(Error::from(TxError::RedelegationIsZero)); + } + } + + // The src and dest validators must actually be validators + let src_validator = + known_validator_or_err(src_validator.clone(), tx_args.force, context) + .await?; + let dest_validator = + known_validator_or_err(dest_validator.clone(), tx_args.force, context) + .await?; + + // The delegator (owner) must exist on-chain and must not be a validator + let owner = + source_exists_or_err(owner.clone(), tx_args.force, context).await?; + if rpc::is_validator(context.client(), &owner).await? { + edisplay_line!( + context.io(), + "The given address {} is a validator. A validator is prohibited \ + from redelegating its own bonds.", + &owner + ); + if !tx_args.force { + return Err(Error::from(TxError::RedelegatorIsValidator( + owner.clone(), + ))); + } + } + + // Prohibit redelegation to the same validator + if src_validator == dest_validator { + edisplay_line!( + context.io(), + "The provided source and destination validators are the same. \ + Redelegation is not allowed to the same validator." + ); + if !tx_args.force { + return Err(Error::from(TxError::RedelegationSrcEqDest)); + } + } + + // Prohibit chained redelegations + let params = rpc::get_pos_params(context.client()).await?; + let incoming_redel_epoch = rpc::query_incoming_redelegations( + context.client(), + &src_validator, + &owner, + ) + .await?; + let current_epoch = rpc::query_epoch(context.client()).await?; + let is_not_chained = if let Some(redel_end_epoch) = incoming_redel_epoch { + let last_contrib_epoch = redel_end_epoch.prev(); + last_contrib_epoch + params.slash_processing_epoch_offset() + <= current_epoch + } else { + true + }; + if !is_not_chained { + edisplay_line!( + context.io(), + "The source validator {} has an incoming redelegation from the \ + delegator {} that may still be subject to future slashing. \ + Redelegation is not allowed until this is no longer the case.", + &src_validator, + &owner + ); + if !tx_args.force { + return Err(Error::from(TxError::IncomingRedelIsStillSlashable( + src_validator.clone(), + owner.clone(), + ))); + } + } + + // There must be at least as many tokens in the bond as the requested + // redelegation amount + let bond_amount = + rpc::query_bond(context.client(), &owner, &src_validator, None).await?; + if *redel_amount > bond_amount { + edisplay_line!( + context.io(), + "There are not enough tokens available for the desired \ + redelegation at the current epoch {}. Requested to redelegate {} \ + tokens but only {} tokens are available.", + current_epoch, + redel_amount.to_string_native(), + bond_amount.to_string_native() + ); + if !tx_args.force { + return Err(Error::from(TxError::RedelegationAmountTooLarge( + redel_amount.to_string_native(), + bond_amount.to_string_native(), + ))); + } + } else { + display_line!( + context.io(), + "{} NAM tokens available for redelegation. Submitting \ + redelegation transaction for {} tokens...", + bond_amount.to_string_native(), + redel_amount.to_string_native() + ); + } + + let default_address = owner.clone(); + let default_signer = Some(default_address.clone()); + let signing_data = signing::aux_signing_data( + context, + tx_args, + Some(default_address), + default_signer, + ) + .await?; + + let data = pos::Redelegation { + src_validator, + dest_validator, + owner, + amount: *redel_amount, + }; + + build( + context, + tx_args, + tx_code_path.clone(), + data, + do_nothing, + &signing_data.fee_payer, + None, + ) + .await + .map(|(tx, _epoch)| (tx, signing_data)) +} + /// Submit transaction to withdraw an unbond pub async fn build_withdraw<'a>( context: &impl Namada<'a>, @@ -898,6 +1052,31 @@ pub async fn build_unbond<'a>( Option, Option<(Epoch, token::Amount)>, )> { + // Require a positive amount of tokens to be bonded + if amount.is_zero() { + edisplay_line!( + context.io(), + "The requested bond amount is 0. A positive amount must be \ + requested." + ); + if !tx_args.force { + return Err(Error::from(TxError::BondIsZero)); + } + } + + // The validator must actually be a validator + let validator = + known_validator_or_err(validator.clone(), tx_args.force, context) + .await?; + + // Check that the source address exists on chain + let source = match source.clone() { + Some(source) => source_exists_or_err(source, tx_args.force, context) + .await + .map(Some), + None => Ok(source.clone()), + }?; + let default_address = source.clone().unwrap_or(validator.clone()); let default_signer = Some(default_address.clone()); let signing_data = signing::aux_signing_data( @@ -908,48 +1087,34 @@ pub async fn build_unbond<'a>( ) .await?; - let source = source.clone(); // Check the source's current bond amount let bond_source = source.clone().unwrap_or_else(|| validator.clone()); - if !tx_args.force { - known_validator_or_err(validator.clone(), tx_args.force, context) + let bond_amount = + rpc::query_bond(context.client(), &bond_source, &validator, None) .await?; + display_line!( + context.io(), + "Bond amount available for unbonding: {} NAM", + bond_amount.to_string_native() + ); - let bond_amount = - rpc::query_bond(context.client(), &bond_source, validator, None) - .await?; - display_line!( + if *amount > bond_amount { + edisplay_line!( context.io(), - "Bond amount available for unbonding: {} NAM", - bond_amount.to_string_native() + "The total bonds of the source {} is lower than the amount to be \ + unbonded. Amount to unbond is {} and the total bonds is {}.", + bond_source, + amount.to_string_native(), + bond_amount.to_string_native(), ); - - if *amount > bond_amount { - edisplay_line!( - context.io(), - "The total bonds of the source {} is lower than the amount to \ - be unbonded. Amount to unbond is {} and the total bonds is \ - {}.", - bond_source, - amount.to_string_native(), - bond_amount.to_string_native() - ); - if !tx_args.force { - return Err(Error::from(TxError::LowerBondThanUnbond( - bond_source, - amount.to_string_native(), - bond_amount.to_string_native(), - ))); - } - } } // Query the unbonds before submitting the tx let unbonds = rpc::query_unbond_with_slashing( context.client(), &bond_source, - validator, + &validator, ) .await?; let mut withdrawable = BTreeMap::::new(); @@ -1061,16 +1226,19 @@ pub async fn build_bond<'a>( tx_code_path, }: &args::Bond, ) -> Result<(Tx, SigningTxData, Option)> { - let default_address = source.clone().unwrap_or(validator.clone()); - let default_signer = Some(default_address.clone()); - let signing_data = signing::aux_signing_data( - context, - tx_args, - Some(default_address.clone()), - default_signer, - ) - .await?; + // Require a positive amount of tokens to be bonded + if amount.is_zero() { + edisplay_line!( + context.io(), + "The requested bond amount is 0. A positive amount must be \ + requested." + ); + if !tx_args.force { + return Err(Error::from(TxError::BondIsZero)); + } + } + // The validator must actually be a validator let validator = known_validator_or_err(validator.clone(), tx_args.force, context) .await?; @@ -1082,6 +1250,17 @@ pub async fn build_bond<'a>( .map(Some), None => Ok(source.clone()), }?; + + let default_address = source.clone().unwrap_or(validator.clone()); + let default_signer = Some(default_address.clone()); + let signing_data = signing::aux_signing_data( + context, + tx_args, + Some(default_address.clone()), + default_signer, + ) + .await?; + // Check bond's source (source for delegation or validator for self-bonds) // balance let bond_source = source.as_ref().unwrap_or(&validator); @@ -1660,7 +1839,7 @@ pub async fn build_transfer<'a, N: Namada<'a>>( // This has no side-effect because transaction is to self. let (_amount, token) = if source == masp_addr && target == masp_addr { // TODO Refactor me, we shouldn't rely on any specific token here. - (token::Amount::default(), args.native_token.clone()) + (token::Amount::zero(), args.native_token.clone()) } else { (validated_amount.amount, token) }; @@ -2083,7 +2262,7 @@ async fn check_balance_too_low_err<'a, N: Namada<'a>>( context.format_amount(token, amount).await, context.format_amount(token, balance).await, ); - Ok(token::Amount::default()) + Ok(token::Amount::zero()) } else { Err(Error::from(TxError::BalanceTooLow( source.clone(), @@ -2104,7 +2283,7 @@ async fn check_balance_too_low_err<'a, N: Namada<'a>>( source, token ); - Ok(token::Amount::default()) + Ok(token::Amount::zero()) } else { Err(Error::from(TxError::NoBalanceForToken( source.clone(), diff --git a/shared/src/sdk/args.rs b/shared/src/sdk/args.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 8d9af9a96e..158c670945 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -1049,13 +1049,16 @@ fn invalid_transactions() -> Result<()> { /// PoS bonding, unbonding and withdrawal tests. In this test we: /// /// 1. Run the ledger node with shorter epochs for faster progression -/// 2. Submit a self-bond for the genesis validator -/// 3. Submit a delegation to the genesis validator -/// 4. Submit an unbond of the self-bond -/// 5. Submit an unbond of the delegation -/// 6. Wait for the unbonding epoch -/// 7. Submit a withdrawal of the self-bond -/// 8. Submit a withdrawal of the delegation +/// 2. Submit a self-bond for the first genesis validator +/// 3. Submit a delegation to the first genesis validator +/// 4. Submit a re-delegation from the first to the second genesis validator +/// 5. Submit an unbond of the self-bond +/// 6. Submit an unbond of the delegation from the first validator +/// 7. Submit an unbond of the re-delegation from the second validator +/// 8. Wait for the unbonding epoch +/// 9. Submit a withdrawal of the self-bond +/// 10. Submit a withdrawal of the delegation +/// 11. Submit an withdrawal of the re-delegation #[test] fn pos_bonds() -> Result<()> { let pipeline_len = 2; @@ -1073,11 +1076,17 @@ fn pos_bonds() -> Result<()> { unbonding_len, ..genesis.pos_params }; - GenesisConfig { + let genesis = GenesisConfig { parameters, pos_params, ..genesis - } + }; + let mut genesis = + setup::set_validators(2, genesis, default_port_offset); + // Remove stake from the 2nd validator so chain can run with a + // single node + genesis.validator.get_mut("validator-1").unwrap().tokens = None; + genesis }, None, )?; @@ -1091,13 +1100,13 @@ fn pos_bonds() -> Result<()> { ); // 1. Run the ledger node - let _bg_ledger = + let _bg_validator_0 = start_namada_ledger_node_wait_wasm(&test, Some(0), Some(40))? .background(); - let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); + let validator_0_rpc = get_actor_rpc(&test, &Who::Validator(0)); - // 2. Submit a self-bond for the genesis validator + // 2. Submit a self-bond for the first genesis validator let tx_args = vec![ "bond", "--validator", @@ -1107,7 +1116,7 @@ fn pos_bonds() -> Result<()> { "--signing-keys", "validator-0-account-key", "--node", - &validator_one_rpc, + &validator_0_rpc, ]; let mut client = run_as!(test, Who::Validator(0), Bin::Client, tx_args, Some(40))?; @@ -1115,7 +1124,7 @@ fn pos_bonds() -> Result<()> { client.exp_string("Transaction is valid.")?; client.assert_success(); - // 3. Submit a delegation to the genesis validator + // 3. Submit a delegation to the first genesis validator let tx_args = vec![ "bond", "--validator", @@ -1127,14 +1136,35 @@ fn pos_bonds() -> Result<()> { "--signing-keys", BERTHA_KEY, "--node", - &validator_one_rpc, + &validator_0_rpc, ]; let mut client = run!(test, Bin::Client, tx_args, Some(40))?; client.exp_string("Transaction applied with result:")?; client.exp_string("Transaction is valid.")?; client.assert_success(); - // 4. Submit an unbond of the self-bond + // 4. Submit a re-delegation from the first to the second genesis validator + let tx_args = vec![ + "redelegate", + "--source-validator", + "validator-0", + "--destination-validator", + "validator-1", + "--owner", + BERTHA, + "--amount", + "2500.0", + "--signing-keys", + BERTHA_KEY, + "--node", + &validator_0_rpc, + ]; + let mut client = run!(test, Bin::Client, tx_args, Some(40))?; + client.exp_string("Transaction applied with result:")?; + client.exp_string("Transaction is valid.")?; + client.assert_success(); + + // 5. Submit an unbond of the self-bond let tx_args = vec![ "unbond", "--validator", @@ -1144,7 +1174,7 @@ fn pos_bonds() -> Result<()> { "--signing-keys", "validator-0-account-key", "--node", - &validator_one_rpc, + &validator_0_rpc, ]; let mut client = run_as!(test, Who::Validator(0), Bin::Client, tx_args, Some(40))?; @@ -1152,7 +1182,7 @@ fn pos_bonds() -> Result<()> { .exp_string("Amount 5100.000000 withdrawable starting from epoch ")?; client.assert_success(); - // 5. Submit an unbond of the delegation + // 6. Submit an unbond of the delegation from the first validator let tx_args = vec![ "unbond", "--validator", @@ -1160,22 +1190,41 @@ fn pos_bonds() -> Result<()> { "--source", BERTHA, "--amount", - "3200.", + "1600.", "--signing-keys", BERTHA_KEY, "--node", - &validator_one_rpc, + &validator_0_rpc, ]; let mut client = run!(test, Bin::Client, tx_args, Some(40))?; - let expected = "Amount 3200.000000 withdrawable starting from epoch "; + let expected = "Amount 1600.000000 withdrawable starting from epoch "; + let _ = client.exp_regex(&format!("{expected}.*\n"))?; + client.assert_success(); + + // 7. Submit an unbond of the re-delegation from the second validator + let tx_args = vec![ + "unbond", + "--validator", + "validator-1", + "--source", + BERTHA, + "--amount", + "1600.", + "--signing-keys", + BERTHA_KEY, + "--node", + &validator_0_rpc, + ]; + let mut client = run!(test, Bin::Client, tx_args, Some(40))?; + let expected = "Amount 1600.000000 withdrawable starting from epoch "; let (_unread, matched) = client.exp_regex(&format!("{expected}.*\n"))?; let epoch_raw = matched.trim().split_once(expected).unwrap().1; let delegation_withdrawable_epoch = Epoch::from_str(epoch_raw).unwrap(); client.assert_success(); - // 6. Wait for the delegation withdrawable epoch (the self-bond was unbonded + // 8. Wait for the delegation withdrawable epoch (the self-bond was unbonded // before it) - let epoch = get_epoch(&test, &validator_one_rpc)?; + let epoch = get_epoch(&test, &validator_0_rpc)?; println!( "Current epoch: {}, earliest epoch for withdrawal: {}", @@ -1190,13 +1239,13 @@ fn pos_bonds() -> Result<()> { delegation_withdrawable_epoch ); } - let epoch = epoch_sleep(&test, &validator_one_rpc, 40)?; + let epoch = epoch_sleep(&test, &validator_0_rpc, 40)?; if epoch >= delegation_withdrawable_epoch { break; } } - // 7. Submit a withdrawal of the self-bond + // 9. Submit a withdrawal of the self-bond let tx_args = vec![ "withdraw", "--validator", @@ -1204,7 +1253,7 @@ fn pos_bonds() -> Result<()> { "--signing-keys", "validator-0-account-key", "--node", - &validator_one_rpc, + &validator_0_rpc, ]; let mut client = run_as!(test, Who::Validator(0), Bin::Client, tx_args, Some(40))?; @@ -1212,7 +1261,7 @@ fn pos_bonds() -> Result<()> { client.exp_string("Transaction is valid.")?; client.assert_success(); - // 8. Submit a withdrawal of the delegation + // 10. Submit a withdrawal of the delegation let tx_args = vec![ "withdraw", "--validator", @@ -1222,12 +1271,30 @@ fn pos_bonds() -> Result<()> { "--signing-keys", BERTHA_KEY, "--node", - &validator_one_rpc, + &validator_0_rpc, + ]; + let mut client = run!(test, Bin::Client, tx_args, Some(40))?; + client.exp_string("Transaction applied with result:")?; + client.exp_string("Transaction is valid.")?; + client.assert_success(); + + // 11. Submit an withdrawal of the re-delegation + let tx_args = vec![ + "withdraw", + "--validator", + "validator-1", + "--source", + BERTHA, + "--signing-keys", + BERTHA_KEY, + "--node", + &validator_0_rpc, ]; let mut client = run!(test, Bin::Client, tx_args, Some(40))?; client.exp_string("Transaction applied with result:")?; client.exp_string("Transaction is valid.")?; client.assert_success(); + Ok(()) } diff --git a/tests/src/native_vp/pos.rs b/tests/src/native_vp/pos.rs index 344a75d4e3..3715804b66 100644 --- a/tests/src/native_vp/pos.rs +++ b/tests/src/native_vp/pos.rs @@ -97,7 +97,7 @@ use namada::ledger::pos::namada_proof_of_stake::init_genesis; use namada::proof_of_stake::parameters::PosParams; -use namada::proof_of_stake::storage::GenesisValidator; +use namada::proof_of_stake::types::GenesisValidator; use namada::types::storage::Epoch; use crate::tx::tx_host_env; @@ -572,8 +572,7 @@ pub mod testing { use namada::proof_of_stake::epoched::DynEpochOffset; use namada::proof_of_stake::parameters::testing::arb_rate; use namada::proof_of_stake::parameters::PosParams; - use namada::proof_of_stake::storage::BondId; - use namada::proof_of_stake::types::ValidatorState; + use namada::proof_of_stake::types::{BondId, ValidatorState}; use namada::proof_of_stake::{ get_num_consensus_validators, read_pos_params, unbond_handle, ADDRESS as POS_ADDRESS, @@ -1033,7 +1032,7 @@ pub mod testing { // .sum() // }) // .unwrap_or_default(); - let token_delta = token::Change::default(); + let token_delta = token::Change::zero(); vec![ PosStorageChange::WithdrawUnbond { owner, validator }, @@ -1150,7 +1149,7 @@ pub mod testing { // last // update, until we unbond the full // amount let mut bond_epoch = // u64::from(bonds.last_update()) + params.unbonding_len; - // 'outer: while to_unbond != token::Amount::default() + // 'outer: while to_unbond != token::Amount::zero() // && bond_epoch >= bonds.last_update().into() // { // if let Some(bond) = bonds.get_delta_at_epoch(bond_epoch) diff --git a/tx_prelude/src/proof_of_stake.rs b/tx_prelude/src/proof_of_stake.rs index cc8bcb7b63..36584a4d19 100644 --- a/tx_prelude/src/proof_of_stake.rs +++ b/tx_prelude/src/proof_of_stake.rs @@ -7,15 +7,15 @@ 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, unjail_validator, withdraw_tokens, - BecomeValidator, + read_pos_params, redelegate_tokens, unbond_tokens, unjail_validator, + withdraw_tokens, BecomeValidator, }; -pub use namada_proof_of_stake::{parameters, types}; +pub use namada_proof_of_stake::{parameters, types, ResultSlashing}; use super::*; impl Ctx { - /// NEW: Self-bond tokens to a validator when `source` is `None` or equal to + /// Self-bond tokens to a validator when `source` is `None` or equal to /// the `validator` address, or delegate tokens from the `source` to the /// `validator`. pub fn bond_tokens( @@ -28,7 +28,7 @@ impl Ctx { bond_tokens(self, source, validator, amount, current_epoch) } - /// NEW: Unbond self-bonded tokens from a validator when `source` is `None` + /// Unbond self-bonded tokens from a validator when `source` is `None` /// or equal to the `validator` address, or unbond delegated tokens from /// the `source` to the `validator`. pub fn unbond_tokens( @@ -36,12 +36,12 @@ impl Ctx { source: Option<&Address>, validator: &Address, amount: token::Amount, - ) -> TxResult { + ) -> EnvResult { let current_epoch = self.get_block_epoch()?; - unbond_tokens(self, source, validator, amount, current_epoch) + unbond_tokens(self, source, validator, amount, current_epoch, false) } - /// NEW: Withdraw unbonded tokens from a self-bond to a validator when + /// Withdraw unbonded tokens from a self-bond to a validator when /// `source` is `None` or equal to the `validator` address, or withdraw /// unbonded tokens delegated to the `validator` to the `source`. pub fn withdraw_tokens( @@ -53,7 +53,7 @@ impl Ctx { withdraw_tokens(self, source, validator, current_epoch) } - /// NEW: Change validator commission rate. + /// Change validator commission rate. pub fn change_validator_commission_rate( &mut self, validator: &Address, @@ -69,7 +69,26 @@ impl Ctx { unjail_validator(self, validator, current_epoch) } - /// NEW: Attempt to initialize a validator account. On success, returns the + /// Redelegate bonded tokens from one validator to another one. + pub fn redelegate_tokens( + &mut self, + owner: &Address, + src_validator: &Address, + dest_validator: &Address, + amount: token::Amount, + ) -> TxResult { + let current_epoch = self.get_block_epoch()?; + redelegate_tokens( + self, + owner, + src_validator, + dest_validator, + current_epoch, + amount, + ) + } + + /// Attempt to initialize a validator account. On success, returns the /// initialized validator account's address. pub fn init_validator( &mut self, diff --git a/wasm/checksums.json b/wasm/checksums.json index 614ab78e6b..629d590c18 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,22 +1,23 @@ { - "tx_bond.wasm": "tx_bond.b322054eef9d45e299384b2a363049ce0b0160a0c4781ca357aa59970904726c.wasm", - "tx_bridge_pool.wasm": "tx_bridge_pool.6f6ad3b95e21072af9e854e374fa0d7f691f0743da8cf52a643ed1bdb0e16611.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.9310e0a0b7c14fc7c2427040da8c91eb4067babfaaea9e3b646edbfdd09c8069.wasm", - "tx_ibc.wasm": "tx_ibc.54313469bcc9bcaabf661177f88cb90ac9008f542edbf686f286a02f8cdbfd41.wasm", - "tx_init_account.wasm": "tx_init_account.10ee01dac5325685360119ba8e4b597d776a018ea4c9ac3534dd876ec377789e.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.04cad5a3a71f833a5867bca3ced54b06d34ad07f3f21877599d38581d362ba10.wasm", - "tx_init_validator.wasm": "tx_init_validator.16d53a09e5df06400849aaa161c35e4e377284692f73a71dcbd4573656da7f64.wasm", - "tx_resign_steward.wasm": "tx_resign_steward.b5d92c1bd196be0d196ef16e2ceed9a9ced7ac61d7b177fdbad208c0e784e172.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.32011ddc5316705ae005059d5916b071288a04fb4dee80854af16d61548b5c27.wasm", - "tx_transfer.wasm": "tx_transfer.963ec4c2705377423ddc46b4ff3de63f9b625351467d89290fa771a485710c41.wasm", - "tx_unbond.wasm": "tx_unbond.7f26336db8e8cfebc04d301dc4790138fdd9bc22878fe7542c3da525a09576be.wasm", - "tx_unjail_validator.wasm": "tx_unjail_validator.15a7a399d8fb79f8df959d0ddf4c193020886d1caab1e094cca10ea3aff44a72.wasm", - "tx_update_account.wasm": "tx_update_account.7b4e225a823449d3d8bffde197c439ad24f4f6c95cf754acf62b6373958c4486.wasm", - "tx_update_steward_commission.wasm": "tx_update_steward_commission.0001b21ef3ef4f9b33afb5a5ef75a6a5427fbe221a8350cfbd81781ac18ded6e.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.727e36112fcd0753f758370dff981cc93430fe7d6f95ceb570a02a37529a7531.wasm", - "tx_withdraw.wasm": "tx_withdraw.e70485a8b79c5bff17d3b6ea96a7546cb709137c8a64606bdd1e77637157de33.wasm", - "vp_implicit.wasm": "vp_implicit.e0958c2ec06863f7bd48cd9abb67cc7557f956ce9fa6c714deba885db721fa50.wasm", - "vp_masp.wasm": "vp_masp.037671b60b3e9f312c1c5fdc53d040ebfad21a646b9b1e2dac6b3e20fc0d01ec.wasm", - "vp_user.wasm": "vp_user.0203fddde57bc31ef411370b628963486928a7c4d34614980d1a52616e0f617b.wasm", - "vp_validator.wasm": "vp_validator.39c685bc1407ef484f963aff9f7576273d56bbf283dcbded9f01944cf7ff9bf0.wasm" + "tx_bond.wasm": "tx_bond.bd9f6fb78273d234c0be9aa99f58a7cb637a97dc2c3424c0a30cc9b395f3b165.wasm", + "tx_bridge_pool.wasm": "tx_bridge_pool.f7057f5ba2d219d96307304af20946a92e8d2a9984c6569288d66573c83f9e09.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.f40a962fba8b4b8d610fdbf05a30b2904322684e8afbebb6e155060953952d0d.wasm", + "tx_ibc.wasm": "tx_ibc.0d2cffa511ab313741735313307f90872319beafd1da04e33d9eac58d1eaa9b2.wasm", + "tx_init_account.wasm": "tx_init_account.6b2d874c80371f843fc4d971de0b589a25db307d80fa74b2c7075114cd6ef654.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.3b031b24c1dc203a3b14885f90c060f2da414c0584dbe495afad0ccf29efae11.wasm", + "tx_init_validator.wasm": "tx_init_validator.0399c9c64e4f77773fd4b5f1413bbc515460559cf9dbe851ca3c38501f688137.wasm", + "tx_redelegate.wasm": "tx_redelegate.6b0b58dba41b6cc1be5db89363893d5c2a31340d94ded7ea1076239a04405ee6.wasm", + "tx_resign_steward.wasm": "tx_resign_steward.955eee167ca6b206a02dc61595fe32449003eebb20621cd2343fce7844c885b5.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.a4109383a11ecc06bc9098d82c0d9ee23d60068b76e88e70b1adadb4e75d8f9b.wasm", + "tx_transfer.wasm": "tx_transfer.a5881e411cc02dffe0db97a336cc617cd624ac771aeeb901f9d91b0ede385d0b.wasm", + "tx_unbond.wasm": "tx_unbond.3a78a23ebfd6e0ed38c0be827ba07f9057d7f60f6fc8f3a033cddd3736ec70de.wasm", + "tx_unjail_validator.wasm": "tx_unjail_validator.76986a378ff219dadef369f4b459bb7542af2486cda4e3fb07e9ee4768d31366.wasm", + "tx_update_account.wasm": "tx_update_account.45ed453aebe68c8c64553918eaf3be8dcc914f32fcb02082c1766bf90b5609a4.wasm", + "tx_update_steward_commission.wasm": "tx_update_steward_commission.88b35049b102f6d6ae867a5d400fbd44e021eddd426eb5b710489a0f8381c09a.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.a9c0a47f1d5afd0338c8f9cfd67c9e781441d7a1f558d3e0cdc305df5fd8993a.wasm", + "tx_withdraw.wasm": "tx_withdraw.7385bd400ee32db3d6d9504593116faa5412ca5ba7088c11fed9aad74e750632.wasm", + "vp_implicit.wasm": "vp_implicit.cc1435713c12ee6de7c1caed76fac09f99ad93c77e78bbed3d1e2eadd324b0fc.wasm", + "vp_masp.wasm": "vp_masp.ac713f88be7d977c37b68c7b76a8f10ee689cda22e79d24a458f30c317163457.wasm", + "vp_user.wasm": "vp_user.1944e4ef7512c2d07c124316952e642eb257daa351dc0af9ea9bfed3d1534ddd.wasm", + "vp_validator.wasm": "vp_validator.345491f65e2ca3553ba2033a4afdacf99c0d6cd8729108362f2e66b642399cbf.wasm" } \ No newline at end of file diff --git a/wasm/wasm_source/Cargo.toml b/wasm/wasm_source/Cargo.toml index 82ccc18a31..c734416dcd 100644 --- a/wasm/wasm_source/Cargo.toml +++ b/wasm/wasm_source/Cargo.toml @@ -20,6 +20,7 @@ tx_ibc = ["namada_tx_prelude"] tx_init_account = ["namada_tx_prelude"] tx_init_proposal = ["namada_tx_prelude"] tx_init_validator = ["namada_tx_prelude"] +tx_redelegate = ["namada_tx_prelude"] tx_reveal_pk = ["namada_tx_prelude"] tx_transfer = ["namada_tx_prelude"] tx_unbond = ["namada_tx_prelude"] diff --git a/wasm/wasm_source/Makefile b/wasm/wasm_source/Makefile index 7b00424baf..e78237c89d 100644 --- a/wasm/wasm_source/Makefile +++ b/wasm/wasm_source/Makefile @@ -12,6 +12,7 @@ wasms += tx_ibc wasms += tx_init_account wasms += tx_init_proposal wasms += tx_init_validator +wasms += tx_redelegate wasms += tx_reveal_pk wasms += tx_transfer wasms += tx_unbond diff --git a/wasm/wasm_source/src/lib.rs b/wasm/wasm_source/src/lib.rs index d376f8ca70..139835fe9f 100644 --- a/wasm/wasm_source/src/lib.rs +++ b/wasm/wasm_source/src/lib.rs @@ -12,6 +12,8 @@ pub mod tx_init_account; pub mod tx_init_proposal; #[cfg(feature = "tx_init_validator")] pub mod tx_init_validator; +#[cfg(feature = "tx_redelegate")] +pub mod tx_redelegate; #[cfg(feature = "tx_resign_steward")] pub mod tx_resign_steward; #[cfg(feature = "tx_reveal_pk")] diff --git a/wasm/wasm_source/src/tx_bond.rs b/wasm/wasm_source/src/tx_bond.rs index 775e7d0e91..bfd088b32a 100644 --- a/wasm/wasm_source/src/tx_bond.rs +++ b/wasm/wasm_source/src/tx_bond.rs @@ -17,7 +17,8 @@ fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { mod tests { use std::collections::BTreeSet; - use namada::ledger::pos::{GenesisValidator, PosParams, PosVP}; + use namada::ledger::pos::{PosParams, PosVP}; + use namada::proof_of_stake::types::{GenesisValidator, WeightedValidator}; use namada::proof_of_stake::{ bond_handle, read_consensus_validator_set_addresses_with_stake, read_total_stake, read_validator_stake, @@ -38,7 +39,6 @@ mod tests { use namada_tx_prelude::key::RefTo; use namada_tx_prelude::proof_of_stake::parameters::testing::arb_pos_params; use namada_tx_prelude::token; - use namada_vp_prelude::proof_of_stake::WeightedValidator; use proptest::prelude::*; use super::*; @@ -69,7 +69,7 @@ mod tests { ) -> TxResult { // Remove the validator stake threshold for simplicity let pos_params = PosParams { - validator_stake_threshold: token::Amount::default(), + validator_stake_threshold: token::Amount::zero(), ..pos_params }; @@ -139,15 +139,12 @@ mod tests { &pos_params, Epoch(epoch), )?); - epoched_validator_stake_pre.push( - read_validator_stake( - ctx(), - &pos_params, - &bond.validator, - Epoch(epoch), - )? - .unwrap(), - ); + epoched_validator_stake_pre.push(read_validator_stake( + ctx(), + &pos_params, + &bond.validator, + Epoch(epoch), + )?); epoched_validator_set_pre.push( read_consensus_validator_set_addresses_with_stake( ctx(), @@ -172,15 +169,12 @@ mod tests { &pos_params, Epoch(epoch), )?); - epoched_validator_stake_post.push( - read_validator_stake( - ctx(), - &pos_params, - &bond.validator, - Epoch(epoch), - )? - .unwrap(), - ); + epoched_validator_stake_post.push(read_validator_stake( + ctx(), + &pos_params, + &bond.validator, + Epoch(epoch), + )?); epoched_validator_set_post.push( read_consensus_validator_set_addresses_with_stake( ctx(), @@ -270,13 +264,6 @@ mod tests { let bonds_post = bond_handle(&bond_src, &bond.validator); // let bonds_post = ctx().read_bond(&bond_id)?.unwrap(); - for epoch in 0..pos_params.unbonding_len { - dbg!( - epoch, - bonds_post.get_delta_val(ctx(), Epoch(epoch), &pos_params)? - ); - } - if is_delegation { // A delegation is applied at pipeline offset // Check that bond is empty before pipeline offset @@ -291,7 +278,7 @@ mod tests { } // Check that bond is updated after the pipeline length for epoch in pos_params.pipeline_len..=pos_params.unbonding_len { - let expected_bond_amount = bond.amount.change(); + let expected_bond_amount = bond.amount; let bond = bonds_post.get_sum(ctx(), Epoch(epoch), &pos_params)?; assert_eq!( @@ -306,7 +293,7 @@ mod tests { // Check that a bond already exists from genesis with initial stake // for the validator for epoch in 0..pos_params.pipeline_len { - let expected_bond_amount = initial_stake.change(); + let expected_bond_amount = initial_stake; let bond = bonds_post .get_sum(ctx(), Epoch(epoch), &pos_params) .expect("Genesis validator should already have self-bond"); @@ -324,7 +311,7 @@ mod tests { bonds_post.get_sum(ctx(), Epoch(epoch), &pos_params)?; assert_eq!( bond, - Some(expected_bond_amount.change()), + Some(expected_bond_amount), "Self-bond at and after pipeline offset should contain \ genesis stake and the bonded amount - checking epoch \ {epoch}" diff --git a/wasm/wasm_source/src/tx_change_validator_commission.rs b/wasm/wasm_source/src/tx_change_validator_commission.rs index 581d9d8dcf..ac7f827ccf 100644 --- a/wasm/wasm_source/src/tx_change_validator_commission.rs +++ b/wasm/wasm_source/src/tx_change_validator_commission.rs @@ -20,6 +20,7 @@ mod tests { use std::cmp; use namada::ledger::pos::{PosParams, PosVP}; + use namada::proof_of_stake::types::GenesisValidator; use namada::proof_of_stake::validator_commission_rate_handle; use namada::types::dec::{Dec, POS_DECIMAL_PRECISION}; use namada::types::storage::Epoch; @@ -34,7 +35,6 @@ mod tests { use namada_tx_prelude::key::RefTo; use namada_tx_prelude::proof_of_stake::parameters::testing::arb_pos_params; use namada_tx_prelude::token; - use namada_vp_prelude::proof_of_stake::GenesisValidator; use proptest::prelude::*; use super::*; diff --git a/wasm/wasm_source/src/tx_redelegate.rs b/wasm/wasm_source/src/tx_redelegate.rs new file mode 100644 index 0000000000..338e0205d7 --- /dev/null +++ b/wasm/wasm_source/src/tx_redelegate.rs @@ -0,0 +1,410 @@ +//! A tx for a delegator (non-validator bond owner) to redelegate bonded tokens +//! from one validator to another. + +use namada_tx_prelude::*; + +#[transaction(gas = 460000)] +fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { + let signed = tx_data; + let data = signed.data().ok_or_err_msg("Missing data")?; + let transaction::pos::Redelegation { + src_validator, + dest_validator, + owner, + amount, + } = transaction::pos::Redelegation::try_from_slice(&data[..]) + .wrap_err("failed to decode a Redelegation")?; + ctx.redelegate_tokens(&owner, &src_validator, &dest_validator, amount) +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + + use namada::ledger::pos::{PosParams, PosVP}; + use namada::proof_of_stake::types::{GenesisValidator, WeightedValidator}; + use namada::proof_of_stake::{ + bond_handle, read_consensus_validator_set_addresses_with_stake, + read_total_stake, read_validator_stake, unbond_handle, + }; + use namada::types::dec::Dec; + use namada::types::storage::Epoch; + use namada_tests::log::test; + use namada_tests::native_vp::pos::init_pos; + use namada_tests::native_vp::TestNativeVpEnv; + use namada_tests::tx::*; + use namada_tx_prelude::address::InternalAddress; + use namada_tx_prelude::borsh_ext::BorshSerializeExt; + use namada_tx_prelude::chain::ChainId; + use namada_tx_prelude::key::testing::arb_common_keypair; + use namada_tx_prelude::key::RefTo; + use namada_tx_prelude::proof_of_stake::parameters::testing::arb_pos_params; + use namada_tx_prelude::token; + use proptest::prelude::*; + + use super::*; + + proptest! { + /// In this test we setup the ledger and PoS system with an arbitrary + /// initial state with 1 genesis validator, a delegation bond if the + /// unbond is for a delegation, arbitrary PoS parameters, and + /// we generate an arbitrary unbond that we'd like to apply. + /// + /// After we apply the unbond, we check that all the storage values + /// in PoS system have been updated as expected and then we also check + /// that this transaction is accepted by the PoS validity predicate. + #[test] + fn test_tx_redelegate( + (initial_stake, redelegation) in arb_initial_stake_and_redelegation(), + // A key to sign the transaction + key in arb_common_keypair(), + pos_params in arb_pos_params(None)) { + test_tx_redelegate_aux(initial_stake, redelegation, key, pos_params).unwrap() + } + } + + // TODO: more assertions needed!! + fn test_tx_redelegate_aux( + initial_stake: token::Amount, + redelegation: transaction::pos::Redelegation, + key: key::common::SecretKey, + pos_params: PosParams, + ) -> TxResult { + // Remove the validator stake threshold for simplicity + let pos_params = PosParams { + validator_stake_threshold: token::Amount::zero(), + ..pos_params + }; + dbg!(&initial_stake, &redelegation); + + let consensus_key_1 = key::testing::keypair_1().ref_to(); + let consensus_key_2 = key::testing::keypair_2().ref_to(); + let eth_cold_key = key::testing::keypair_3().ref_to(); + let eth_hot_key = key::testing::keypair_4().ref_to(); + let commission_rate = Dec::new(5, 2).expect("Cannot fail"); + let max_commission_rate_change = Dec::new(1, 2).expect("Cannot fail"); + + let genesis_validators = [ + GenesisValidator { + address: redelegation.src_validator.clone(), + tokens: token::Amount::zero(), + consensus_key: consensus_key_1, + eth_cold_key: eth_cold_key.clone(), + eth_hot_key: eth_hot_key.clone(), + commission_rate, + max_commission_rate_change, + }, + GenesisValidator { + address: redelegation.dest_validator.clone(), + tokens: token::Amount::zero(), + consensus_key: consensus_key_2, + eth_cold_key, + eth_hot_key, + commission_rate, + max_commission_rate_change, + }, + ]; + + init_pos(&genesis_validators[..], &pos_params, Epoch(0)); + + let native_token = tx_host_env::with(|tx_env| { + let native_token = tx_env.wl_storage.storage.native_token.clone(); + let owner = &redelegation.owner; + tx_env.spawn_accounts([owner]); + + // First, credit the delegator with the initial stake, + // before we initialize the bond below + tx_env.credit_tokens(owner, &native_token, initial_stake); + native_token + }); + + // Create the initial bond. + ctx().bond_tokens( + Some(&redelegation.owner), + &redelegation.src_validator, + initial_stake, + )?; + tx_host_env::commit_tx_and_block(); + + let tx_code = vec![]; + let tx_data = redelegation.serialize_to_vec(); + let mut tx = Tx::new(ChainId::default(), None); + tx.add_code(tx_code) + .add_serialized_data(tx_data) + .sign_wrapper(key); + let signed_tx = tx; + + // Check that PoS balance is the same as the initial validator stake + let pos_balance_key = token::balance_key( + &native_token, + &Address::Internal(InternalAddress::PoS), + ); + let pos_balance_pre: token::Amount = ctx() + .read(&pos_balance_key)? + .expect("PoS must have balance"); + assert_eq!(pos_balance_pre, initial_stake); + + let mut epoched_total_stake_pre: Vec = Vec::new(); + let mut epoched_src_validator_stake_pre: Vec = + Vec::new(); + let mut epoched_dest_validator_stake_pre: Vec = + Vec::new(); + let mut epoched_src_bonds_pre: Vec> = Vec::new(); + let mut epoched_dest_bonds_pre: Vec> = Vec::new(); + let mut epoched_validator_set_pre: Vec> = + Vec::new(); + + for epoch in 0..=pos_params.withdrawable_epoch_offset() { + epoched_total_stake_pre.push(read_total_stake( + ctx(), + &pos_params, + Epoch(epoch), + )?); + epoched_src_validator_stake_pre.push(read_validator_stake( + ctx(), + &pos_params, + &redelegation.src_validator, + Epoch(epoch), + )?); + epoched_dest_validator_stake_pre.push(read_validator_stake( + ctx(), + &pos_params, + &redelegation.dest_validator, + Epoch(epoch), + )?); + epoched_src_bonds_pre.push( + bond_handle(&redelegation.owner, &redelegation.src_validator) + .get_delta_val(ctx(), Epoch(epoch))?, + ); + epoched_dest_bonds_pre.push( + bond_handle(&redelegation.owner, &redelegation.src_validator) + .get_delta_val(ctx(), Epoch(epoch))?, + ); + epoched_validator_set_pre.push( + read_consensus_validator_set_addresses_with_stake( + ctx(), + Epoch(epoch), + )?, + ); + } + + // Apply the redelegation tx + apply_tx(ctx(), signed_tx)?; + + // Read the data after the redelegation tx is executed. + // The following storage keys should be updated: + // - `#{PoS}/validator/#{validator}/deltas` + // - `#{PoS}/total_deltas` + // - `#{PoS}/validator_set` + + let mut epoched_src_bonds_post: Vec> = Vec::new(); + let mut epoched_dest_bonds_post: Vec> = + Vec::new(); + for epoch in 0..=pos_params.unbonding_len { + epoched_src_bonds_post.push( + bond_handle(&redelegation.owner, &redelegation.src_validator) + .get_delta_val(ctx(), Epoch(epoch))?, + ); + epoched_dest_bonds_post.push( + bond_handle(&redelegation.owner, &redelegation.dest_validator) + .get_delta_val(ctx(), Epoch(epoch))?, + ); + } + + // Before pipeline offset, there can only be self-bond for genesis + // validator. In case of a delegation the state is setup so that there + // is no bond until pipeline offset. + for epoch in 0..pos_params.pipeline_len { + assert_eq!( + read_validator_stake( + ctx(), + &pos_params, + &redelegation.src_validator, + Epoch(epoch) + )?, + token::Amount::zero(), + "The validator stake before the pipeline offset must be 0 - \ + checking in epoch: {epoch}" + ); + assert_eq!( + read_validator_stake( + ctx(), + &pos_params, + &redelegation.dest_validator, + Epoch(epoch) + )?, + token::Amount::zero(), + "The validator stake before the pipeline offset must be 0 - \ + checking in epoch: {epoch}" + ); + assert_eq!( + read_total_stake(ctx(), &pos_params, Epoch(epoch))?, + token::Amount::zero(), + "The total stake before the pipeline offset must be 0 - \ + checking in epoch: {epoch}" + ); + assert_eq!( + epoched_validator_set_pre[epoch as usize], + read_consensus_validator_set_addresses_with_stake( + ctx(), + Epoch(epoch), + )?, + "Validator set before pipeline offset must not change - \ + checking epoch {epoch}" + ); + } + + // Check stakes after the pipeline length + for epoch in + pos_params.pipeline_len..=pos_params.withdrawable_epoch_offset() + { + assert_eq!( + read_validator_stake( + ctx(), + &pos_params, + &redelegation.src_validator, + Epoch(epoch) + )?, + initial_stake - redelegation.amount, + "The validator stake at and after the pipeline offset must \ + have changed - checking in epoch: {epoch}" + ); + assert_eq!( + read_validator_stake( + ctx(), + &pos_params, + &redelegation.dest_validator, + Epoch(epoch) + )?, + redelegation.amount, + "The validator stake at and after the pipeline offset must \ + have changed - checking in epoch: {epoch}" + ); + assert_eq!( + read_total_stake(ctx(), &pos_params, Epoch(epoch))?, + initial_stake, + "The total stake at and after the pipeline offset must have \ + changed - checking in epoch: {epoch}" + ); + } + // Check validator sets + assert_eq!( + BTreeSet::from_iter([ + WeightedValidator { + bonded_stake: initial_stake - redelegation.amount, + address: redelegation.src_validator.clone() + }, + WeightedValidator { + bonded_stake: redelegation.amount, + address: redelegation.dest_validator.clone() + } + ]), + read_consensus_validator_set_addresses_with_stake( + ctx(), + Epoch(pos_params.pipeline_len), + )?, + "The validator set at pipeline offset should have changed" + ); + + // Check that PoS account balance is unchanged by the redelegation + let pos_balance_post: token::Amount = + ctx().read(&pos_balance_key)?.unwrap(); + assert_eq!( + pos_balance_pre, pos_balance_post, + "Unbonding should not affect PoS system balance" + ); + + // Check that no unbonds exist + assert!( + unbond_handle(&redelegation.owner, &redelegation.src_validator) + .is_empty(ctx())? + ); + assert!( + unbond_handle(&redelegation.owner, &redelegation.dest_validator) + .is_empty(ctx())? + ); + + // Check bonds + for epoch in 0..pos_params.withdrawable_epoch_offset() { + let (exp_src_bond, exp_dest_bond) = + if epoch == pos_params.pipeline_len { + ( + Some(initial_stake - redelegation.amount), + Some(redelegation.amount), + ) + } else { + (None, None) + }; + + assert_eq!( + bond_handle(&redelegation.owner, &redelegation.src_validator) + .get_delta_val(ctx(), Epoch(epoch))?, + exp_src_bond, + "After the tx is applied, the bond should be changed in \ + place, checking epoch {epoch}" + ); + assert_eq!( + bond_handle(&redelegation.owner, &redelegation.dest_validator) + .get_delta_val(ctx(), Epoch(epoch))?, + exp_dest_bond, + "After the tx is applied, the bond should be changed in \ + place, checking epoch {epoch}" + ); + } + + // Use the tx_env to run PoS VP + let tx_env = tx_host_env::take(); + let vp_env = TestNativeVpEnv::from_tx_env(tx_env, address::POS); + let result = vp_env.validate_tx(PosVP::new); + let result = + result.expect("Validation of valid changes must not fail!"); + assert!( + result, + "PoS Validity predicate must accept this transaction" + ); + Ok(()) + } + + /// Generates an initial validator stake and a redelegation, while making + /// sure that the `initial_stake >= redelegation.amount`. + fn arb_initial_stake_and_redelegation() + -> impl Strategy + { + // Generate initial stake + token::testing::arb_amount_ceiled((i64::MAX / 8) as u64).prop_flat_map( + |initial_stake| { + // Use the initial stake to limit the bond amount + let redelegation = arb_redelegation( + u128::try_from(initial_stake).unwrap() as u64, + ); + // Use the generated initial stake too too + (Just(initial_stake), redelegation) + }, + ) + } + + /// Generates an arbitrary redelegation, with the amount constrained from + /// above. + fn arb_redelegation( + max_amount: u64, + ) -> impl Strategy { + ( + address::testing::arb_established_address(), + address::testing::arb_established_address(), + address::testing::arb_non_internal_address(), + token::testing::arb_amount_non_zero_ceiled(max_amount), + ) + .prop_map( + |(src_validator, dest_validator, owner, amount)| { + let src_validator = Address::Established(src_validator); + let dest_validator = Address::Established(dest_validator); + transaction::pos::Redelegation { + src_validator, + dest_validator, + owner, + amount, + } + }, + ) + } +} diff --git a/wasm/wasm_source/src/tx_unbond.rs b/wasm/wasm_source/src/tx_unbond.rs index 7ca5f46ebf..713d2d42a8 100644 --- a/wasm/wasm_source/src/tx_unbond.rs +++ b/wasm/wasm_source/src/tx_unbond.rs @@ -10,15 +10,22 @@ fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { let unbond = transaction::pos::Unbond::try_from_slice(&data[..]) .wrap_err("failed to decode Unbond")?; - ctx.unbond_tokens(unbond.source.as_ref(), &unbond.validator, unbond.amount) + ctx.unbond_tokens( + unbond.source.as_ref(), + &unbond.validator, + unbond.amount, + )?; + // TODO: would using debug_log! be useful? + + Ok(()) } #[cfg(test)] mod tests { use std::collections::BTreeSet; - use namada::ledger::pos::{GenesisValidator, PosParams, PosVP}; - use namada::proof_of_stake::types::WeightedValidator; + use namada::ledger::pos::{PosParams, PosVP}; + use namada::proof_of_stake::types::{GenesisValidator, WeightedValidator}; use namada::proof_of_stake::{ bond_handle, read_consensus_validator_set_addresses_with_stake, read_total_stake, read_validator_stake, unbond_handle, @@ -67,7 +74,7 @@ mod tests { ) -> TxResult { // Remove the validator stake threshold for simplicity let pos_params = PosParams { - validator_stake_threshold: token::Amount::default(), + validator_stake_threshold: token::Amount::zero(), ..pos_params }; @@ -86,7 +93,7 @@ mod tests { tokens: if is_delegation { // If we're unbonding a delegation, we'll give the initial stake // to the delegation instead of the validator - token::Amount::default() + token::Amount::zero() } else { initial_stake }, @@ -114,8 +121,9 @@ mod tests { native_token }); - // Initialize the delegation if it is the case - unlike genesis - // validator's self-bond, this happens at pipeline offset + // If delegation, initialize the bond with a delegation from the unbond + // source, which will become active at pipeline offset. If a self-bond, + // the bond is already active from genesis. if is_delegation { ctx().bond_tokens( unbond.source.as_ref(), @@ -137,11 +145,8 @@ mod tests { .source .clone() .unwrap_or_else(|| unbond.validator.clone()); - // let unbond_id = BondId { - // validator: unbond.validator.clone(), - // source: unbond_src.clone(), - // }; + // Check that PoS balance is the same as the initial validator stake let pos_balance_key = token::balance_key( &native_token, &Address::Internal(InternalAddress::PoS), @@ -159,26 +164,20 @@ mod tests { let mut epoched_validator_set_pre: Vec> = Vec::new(); - for epoch in 0..=pos_params.unbonding_len { + for epoch in 0..=pos_params.withdrawable_epoch_offset() { epoched_total_stake_pre.push(read_total_stake( ctx(), &pos_params, Epoch(epoch), )?); - epoched_validator_stake_pre.push( - read_validator_stake( - ctx(), - &pos_params, - &unbond.validator, - Epoch(epoch), - )? - .unwrap(), - ); - epoched_bonds_pre.push( - bond_handle - .get_delta_val(ctx(), Epoch(epoch), &pos_params)? - .map(token::Amount::from_change), - ); + epoched_validator_stake_pre.push(read_validator_stake( + ctx(), + &pos_params, + &unbond.validator, + Epoch(epoch), + )?); + epoched_bonds_pre + .push(bond_handle.get_delta_val(ctx(), Epoch(epoch))?); epoched_validator_set_pre.push( read_consensus_validator_set_addresses_with_stake( ctx(), @@ -186,31 +185,25 @@ mod tests { )?, ); } - // dbg!(&epoched_bonds_pre); // Apply the unbond tx apply_tx(ctx(), signed_tx)?; - // Read the data after the tx is executed. + // Read the data after the unbond tx is executed. // The following storage keys should be updated: - // - `#{PoS}/validator/#{validator}/deltas` // - `#{PoS}/total_deltas` // - `#{PoS}/validator_set` - let mut epoched_bonds_post: Vec> = Vec::new(); + let mut epoched_bonds_post: Vec> = Vec::new(); for epoch in 0..=pos_params.unbonding_len { - epoched_bonds_post.push( - bond_handle - .get_delta_val(ctx(), Epoch(epoch), &pos_params)? - .map(token::Amount::from_change), - ); + epoched_bonds_post + .push(bond_handle.get_delta_val(ctx(), Epoch(epoch))?); } - // dbg!(&epoched_bonds_post); let expected_amount_before_pipeline = if is_delegation { // When this is a delegation, there will be no bond until pipeline - token::Amount::default() + token::Amount::zero() } else { // Before pipeline offset, there can only be self-bond initial_stake @@ -227,7 +220,7 @@ mod tests { &unbond.validator, Epoch(epoch) )?, - Some(expected_amount_before_pipeline), + expected_amount_before_pipeline, "The validator deltas before the pipeline offset must not \ change - checking in epoch: {epoch}" ); @@ -250,7 +243,9 @@ mod tests { // At and after pipeline offset, there can be either delegation or // self-bond, both of which are initialized to the same `initial_stake` - for epoch in pos_params.pipeline_len..pos_params.unbonding_len { + for epoch in + pos_params.pipeline_len..=pos_params.withdrawable_epoch_offset() + { assert_eq!( read_validator_stake( ctx(), @@ -258,16 +253,17 @@ mod tests { &unbond.validator, Epoch(epoch) )?, - Some(initial_stake - unbond.amount), - "The validator deltas at and after the pipeline offset must \ + initial_stake - unbond.amount, + "The validator stake at and after the pipeline offset must \ have changed - checking in epoch: {epoch}" ); assert_eq!( read_total_stake(ctx(), &pos_params, Epoch(epoch))?, (initial_stake - unbond.amount), - "The total deltas at and after the pipeline offset must have \ + "The total stake at and after the pipeline offset must have \ changed - checking in epoch: {epoch}" ); + // Only at pipeline because the read won't return anything after if epoch == pos_params.pipeline_len { assert_ne!( epoched_validator_set_pre[epoch as usize], @@ -281,59 +277,16 @@ mod tests { } } - { - let epoch = pos_params.unbonding_len + 1; - let expected_stake = - initial_stake.change() - unbond.amount.change(); - assert_eq!( - read_validator_stake( - ctx(), - &pos_params, - &unbond.validator, - Epoch(epoch) - )? - .map(|v| v.change()), - Some(expected_stake), - "The total deltas at after the unbonding offset epoch must be \ - decremented by the unbonded amount - checking in epoch: \ - {epoch}" - ); - assert_eq!( - read_total_stake(ctx(), &pos_params, Epoch(epoch))?.change(), - expected_stake, - "The total deltas at after the unbonding offset epoch must be \ - decremented by the unbonded amount - checking in epoch: \ - {epoch}" - ); - } - - // - `#{staking_token}/balance/#{PoS}` // Check that PoS account balance is unchanged by unbond let pos_balance_post: token::Amount = ctx().read(&pos_balance_key)?.unwrap(); assert_eq!( pos_balance_pre, pos_balance_post, - "Unbonding doesn't affect PoS system balance" + "Unbonding should not affect PoS system balance" ); - // - `#{PoS}/unbond/#{owner}/#{validator}` // Check that the unbond doesn't exist until unbonding offset - - // Outer epoch is end (withdrawable), inner epoch is beginning of let unbond_handle = unbond_handle(&unbond_src, &unbond.validator); - - // let unbonds_post = ctx().read_unbond(&unbond_id)?.unwrap(); - // let bonds_post = ctx().read_bond(&unbond_id)?.unwrap(); - - for epoch in 0..(pos_params.pipeline_len + pos_params.unbonding_len) { - let unbond = unbond_handle.at(&Epoch(epoch)); - - assert!( - unbond.is_empty(ctx())?, - "There should be no unbond until unbonding offset - checking \ - epoch {epoch}" - ); - } let start_epoch = if is_delegation { // This bond was a delegation Epoch::from(pos_params.pipeline_len) @@ -341,62 +294,42 @@ mod tests { // This bond was a genesis validator self-bond Epoch::default() }; - // let end_epoch = Epoch::from(pos_params.unbonding_len - 1); - - // let expected_unbond = if unbond.amount == token::Amount::default() { - // HashMap::new() - // } else { - // HashMap::from_iter([((start_epoch, end_epoch), unbond.amount)]) - // }; + let withdrawable_epoch = pos_params.withdrawable_epoch_offset(); + for epoch in 0..withdrawable_epoch { + assert!( + unbond_handle + .at(&start_epoch) + .get(ctx(), &Epoch(epoch))? + .is_none(), + "There should be no unbond until the withdrawable offset - \ + checking epoch {epoch}" + ); + } // Ensure that the unbond is structured as expected, withdrawable at // 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.cubic_slashing_window_length, - )) - .get(ctx(), &start_epoch)?; + .at(&start_epoch) + .get(ctx(), &Epoch(withdrawable_epoch))?; assert_eq!( actual_unbond_amount, Some(unbond.amount), - "Delegation at pipeline + unbonding offset should be equal to the \ - unbonded amount" + "Delegation at pipeline + unbonding + cubic window offset should \ + be equal to the unbonded amount" ); - for epoch in start_epoch.0 - ..(pos_params.pipeline_len - + pos_params.unbonding_len - + pos_params.cubic_slashing_window_length) - { + for epoch in start_epoch.0..pos_params.withdrawable_epoch_offset() { let bond_amount = bond_handle.get_sum(ctx(), Epoch(epoch), &pos_params)?; let expected_amount = initial_stake - unbond.amount; assert_eq!( bond_amount, - Some(expected_amount.change()), + Some(expected_amount), "After the tx is applied, the bond should be changed in \ place, checking epoch {epoch}" ); } - // { - // let epoch = pos_params.unbonding_len + 1; - // let bond: Bond = bonds_post.get(epoch).unwrap(); - // let expected_bond = - // HashMap::from_iter([(start_epoch, initial_stake)]); - // assert_eq!( - // bond.pos_deltas, expected_bond, - // "At unbonding offset, the pos deltas should not change, \ - // checking epoch {epoch}" - // ); - // assert_eq!( - // bond.neg_deltas, unbond.amount, - // "At unbonding offset, the unbonded amount should have been \ - // deducted, checking epoch {epoch}" - // ) - // } // Use the tx_env to run PoS VP let tx_env = tx_host_env::take(); @@ -411,6 +344,8 @@ mod tests { Ok(()) } + /// Generates an initial validator stake and a unbond, while making sure + /// that the `initial_stake >= unbond.amount`. fn arb_initial_stake_and_unbond() -> impl Strategy { // Generate initial stake @@ -425,8 +360,7 @@ mod tests { ) } - /// Generates an initial validator stake and a unbond, while making sure - /// that the `initial_stake >= unbond.amount`. + /// Generates an arbitrary unbond, with the amount constrained from above. fn arb_unbond( max_amount: u64, ) -> impl Strategy { diff --git a/wasm/wasm_source/src/tx_withdraw.rs b/wasm/wasm_source/src/tx_withdraw.rs index d9f47dba23..807d0b530a 100644 --- a/wasm/wasm_source/src/tx_withdraw.rs +++ b/wasm/wasm_source/src/tx_withdraw.rs @@ -12,7 +12,7 @@ fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { let slashed = ctx.withdraw_tokens(withdraw.source.as_ref(), &withdraw.validator)?; - if slashed != token::Amount::default() { + if !slashed.is_zero() { debug_log!("New withdrawal slashed for {}", slashed.to_string_native()); } Ok(()) @@ -20,7 +20,8 @@ fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { #[cfg(test)] mod tests { - use namada::ledger::pos::{GenesisValidator, PosParams, PosVP}; + use namada::ledger::pos::{PosParams, PosVP}; + use namada::proof_of_stake::types::GenesisValidator; use namada::proof_of_stake::unbond_handle; use namada::types::dec::Dec; use namada::types::storage::Epoch; @@ -72,7 +73,7 @@ mod tests { ) -> TxResult { // Remove the validator stake threshold for simplicity let pos_params = PosParams { - validator_stake_threshold: token::Amount::default(), + validator_stake_threshold: token::Amount::zero(), ..pos_params }; @@ -90,7 +91,7 @@ mod tests { // If we're withdrawing a delegation, we'll give the initial // stake to the delegation instead of the // validator - token::Amount::default() + token::Amount::zero() } else { initial_stake }, @@ -194,7 +195,7 @@ mod tests { let handle = unbond_handle(&unbond_src, &withdraw.validator); let unbond_pre = - handle.at(&withdraw_epoch).get(ctx(), &bond_epoch).unwrap(); + handle.at(&bond_epoch).get(ctx(), &withdraw_epoch).unwrap(); assert_eq!(unbond_pre, Some(unbonded_amount));