diff --git a/utils/global-state-update-gen/src/generic.rs b/utils/global-state-update-gen/src/generic.rs index f758f33ce4..a406c2549a 100644 --- a/utils/global-state-update-gen/src/generic.rs +++ b/utils/global-state-update-gen/src/generic.rs @@ -120,6 +120,17 @@ fn update_auction_state( let validators_diff = validators_diff(&old_snapshot, &new_snapshot); + let bids = state.get_bids(); + if slash { + // zero the unbonds for the removed validators independently of set_bid; set_bid will take + // care of zeroing the delegators if necessary + for pub_key in &validators_diff.removed { + if let Some(bid) = bids.get(pub_key) { + state.remove_withdraws_and_unbonds_with_bonding_purse(bid.bonding_purse()); + } + } + } + add_and_remove_bids( state, &validators_diff, @@ -128,10 +139,6 @@ fn update_auction_state( slash, ); - if slash { - state.remove_withdraws_and_unbonds(&validators_diff.removed); - } - // We need to output the validators for the next era, which are contained in the first entry // in the snapshot. Some( diff --git a/utils/global-state-update-gen/src/generic/state_reader.rs b/utils/global-state-update-gen/src/generic/state_reader.rs index d784b4229f..c7e2235e11 100644 --- a/utils/global-state-update-gen/src/generic/state_reader.rs +++ b/utils/global-state-update-gen/src/generic/state_reader.rs @@ -2,7 +2,10 @@ use casper_engine_test_support::LmdbWasmTestBuilder; use casper_types::{ account::{Account, AccountHash}, system::{ - auction::{Bids, UnbondingPurses, WithdrawPurses, SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY}, + auction::{ + Bids, UnbondingPurses, WithdrawPurses, SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY, + UNBONDING_DELAY_KEY, + }, mint::TOTAL_SUPPLY_KEY, }, Key, StoredValue, @@ -22,6 +25,8 @@ pub trait StateReader { fn get_withdraws(&mut self) -> WithdrawPurses; fn get_unbonds(&mut self) -> UnbondingPurses; + + fn get_unbonding_delay(&mut self) -> u64; } impl<'a, T> StateReader for &'a mut T @@ -55,6 +60,10 @@ where fn get_unbonds(&mut self) -> UnbondingPurses { T::get_unbonds(self) } + + fn get_unbonding_delay(&mut self) -> u64 { + T::get_unbonding_delay(self) + } } impl StateReader for LmdbWasmTestBuilder { @@ -95,4 +104,22 @@ impl StateReader for LmdbWasmTestBuilder { fn get_unbonds(&mut self) -> UnbondingPurses { LmdbWasmTestBuilder::get_unbonds(self) } + + fn get_unbonding_delay(&mut self) -> u64 { + // Find the hash of the auction contract. + let auction_contract_hash = self.get_system_auction_hash(); + + let unbonding_delay_key = self + .get_contract(auction_contract_hash) + .expect("auction should exist") + .named_keys()[UNBONDING_DELAY_KEY]; + + self.query(unbonding_delay_key) + .expect("should query") + .as_cl_value() + .cloned() + .expect("should be cl value") + .into_t() + .expect("should be u64") + } } diff --git a/utils/global-state-update-gen/src/generic/state_tracker.rs b/utils/global-state-update-gen/src/generic/state_tracker.rs index 6f0f36058b..72a353eb07 100644 --- a/utils/global-state-update-gen/src/generic/state_tracker.rs +++ b/utils/global-state-update-gen/src/generic/state_tracker.rs @@ -1,6 +1,6 @@ use std::{ cmp::Ordering, - collections::{btree_map::Entry, BTreeMap, BTreeSet}, + collections::{btree_map::Entry, BTreeMap}, convert::TryFrom, }; @@ -8,7 +8,10 @@ use rand::Rng; use casper_types::{ account::{Account, AccountHash}, - system::auction::{Bid, Bids, SeigniorageRecipientsSnapshot, UnbondingPurse}, + system::auction::{ + Bid, Bids, SeigniorageRecipientsSnapshot, UnbondingPurse, UnbondingPurses, WithdrawPurse, + WithdrawPurses, + }, AccessRights, CLValue, Key, PublicKey, StoredValue, URef, U512, }; @@ -21,6 +24,7 @@ pub struct StateTracker { total_supply: U512, total_supply_key: Key, accounts_cache: BTreeMap, + withdraws_cache: BTreeMap>, unbonds_cache: BTreeMap>, purses_cache: BTreeMap, bids_cache: Option, @@ -46,6 +50,7 @@ impl StateTracker { total_supply_key, total_supply: total_supply.into_t().expect("should be U512"), accounts_cache: BTreeMap::new(), + withdraws_cache: BTreeMap::new(), unbonds_cache: BTreeMap::new(), purses_cache: BTreeMap::new(), bids_cache: None, @@ -222,7 +227,12 @@ impl StateTracker { pub fn set_bid(&mut self, public_key: PublicKey, bid: Bid, slash: bool) { let maybe_current_bid = self.get_bids().get(&public_key).cloned(); - let new_amount = *bid.staked_amount(); + let account_hash = public_key.to_account_hash(); + let already_unbonded = self.already_unbonding_amount(&account_hash, &public_key); + + // we need to put enough funds in the bonding purse to cover the staked amount and the + // amount that will be getting unbonded in the future + let new_amount = *bid.staked_amount() + already_unbonded; let old_amount = maybe_current_bid .as_ref() .map(|bid| self.get_purse_balance(*bid.bonding_purse())) @@ -234,8 +244,6 @@ impl StateTracker { .unwrap() .insert(public_key.clone(), bid.clone()); - let account_hash = public_key.to_account_hash(); - // Replace the bid (overwrite the previous bid, if any): self.write_entry(Key::Bid(account_hash), bid.clone().into()); @@ -246,6 +254,8 @@ impl StateTracker { if old_bid.bonding_purse() != bid.bonding_purse() { self.set_purse_balance(*old_bid.bonding_purse(), U512::zero()); self.set_purse_balance(*bid.bonding_purse(), old_amount); + // the old bonding purse gets zeroed - the unbonds will get invalid, anyway + self.remove_withdraws_and_unbonds_with_bonding_purse(old_bid.bonding_purse()); } for (delegator_pub_key, delegator) in old_bid @@ -271,6 +281,8 @@ impl StateTracker { self.set_purse_balance(*new_delegator.bonding_purse(), old_amount); } self.set_purse_balance(*delegator.bonding_purse(), U512::zero()); + // the old bonding purse gets zeroed - remove the delegator's unbonds + self.remove_withdraws_and_unbonds_with_bonding_purse(delegator.bonding_purse()); } else { let amount = self.get_purse_balance(*delegator.bonding_purse()); let already_unbonding = @@ -288,48 +300,78 @@ impl StateTracker { if (slash && new_amount != old_amount) || new_amount > old_amount { self.set_purse_balance(*bid.bonding_purse(), new_amount); } else if new_amount < old_amount { - let already_unbonded = self.already_unbonding_amount(&account_hash, &public_key); self.create_unbonding_purse( *bid.bonding_purse(), &public_key, &public_key, - old_amount - new_amount - already_unbonded, + old_amount - new_amount, ); } for (delegator_public_key, delegator) in bid.delegators() { let old_amount = self.get_purse_balance(*delegator.bonding_purse()); - let new_amount = *delegator.staked_amount(); + let already_unbonded = + self.already_unbonding_amount(&account_hash, delegator_public_key); + let new_amount = *delegator.staked_amount() + already_unbonded; if (slash && new_amount != old_amount) || new_amount > old_amount { - self.set_purse_balance(*delegator.bonding_purse(), *delegator.staked_amount()); + self.set_purse_balance(*delegator.bonding_purse(), new_amount); } else if new_amount < old_amount { - let already_unbonded = self.already_unbonding_amount(&account_hash, &public_key); self.create_unbonding_purse( *delegator.bonding_purse(), &public_key, delegator_public_key, - old_amount - new_amount - already_unbonded, + old_amount - new_amount, ); } } } + fn get_withdraws(&mut self) -> WithdrawPurses { + let mut result = self.reader.get_withdraws(); + for (acc, purses) in &self.withdraws_cache { + result.insert(*acc, purses.clone()); + } + result + } + + fn get_unbonds(&mut self) -> UnbondingPurses { + let mut result = self.reader.get_unbonds(); + for (acc, purses) in &self.unbonds_cache { + result.insert(*acc, purses.clone()); + } + result + } + + fn write_withdraw(&mut self, account_hash: AccountHash, withdraws: Vec) { + self.withdraws_cache.insert(account_hash, withdraws.clone()); + self.write_entry( + Key::Withdraw(account_hash), + StoredValue::Withdraw(withdraws), + ); + } + + fn write_unbond(&mut self, account_hash: AccountHash, unbonds: Vec) { + self.unbonds_cache.insert(account_hash, unbonds.clone()); + self.write_entry(Key::Unbond(account_hash), StoredValue::Unbonding(unbonds)); + } + /// Returns the sum of already unbonding purses for the given validator account & unbonder. fn already_unbonding_amount(&mut self, account: &AccountHash, unbonder: &PublicKey) -> U512 { - let existing_purses = self - .reader - .get_unbonds() - .get(account) - .cloned() - .unwrap_or_default(); + let current_era = *self.read_snapshot().1.keys().next().unwrap(); + let unbonding_delay = self.reader.get_unbonding_delay(); + let limit_era = current_era.saturating_sub(unbonding_delay); + + let existing_purses = self.get_unbonds().get(account).cloned().unwrap_or_default(); let existing_purses_legacy = self - .reader .get_withdraws() .get(account) .cloned() .unwrap_or_default() .into_iter() - .map(UnbondingPurse::from); + .map(UnbondingPurse::from) + // There may be some leftover old legacy purses that haven't been purged - this is to + // make sure that we don't accidentally take them into account. + .filter(|purse| purse.era_of_creation() >= limit_era); existing_purses_legacy .chain(existing_purses) @@ -338,26 +380,23 @@ impl StateTracker { .sum() } - /// Generates the writes to the global state that will remove the pending withdraws and unbonds - /// of all the old validators that will cease to be validators, and slashes their unbonding - /// purses. - pub fn remove_withdraws_and_unbonds(&mut self, removed: &BTreeSet) { - let withdraws = self.reader.get_withdraws(); - let unbonds = self.reader.get_unbonds(); - for removed_validator in removed { - let acc = removed_validator.to_account_hash(); - if let Some(withdraw_set) = withdraws.get(&acc) { - for withdraw in withdraw_set { - self.set_purse_balance(*withdraw.bonding_purse(), U512::zero()); - } - self.write_entry(Key::Withdraw(acc), StoredValue::Withdraw(vec![])); + pub fn remove_withdraws_and_unbonds_with_bonding_purse(&mut self, affected_purse: &URef) { + let withdraws = self.get_withdraws(); + let unbonds = self.get_unbonds(); + + for (acc, mut purses) in withdraws { + let old_len = purses.len(); + purses.retain(|purse| purse.bonding_purse().addr() != affected_purse.addr()); + if purses.len() != old_len { + self.write_withdraw(acc, purses); } + } - if let Some(unbond_set) = unbonds.get(&acc) { - for unbond in unbond_set { - self.set_purse_balance(*unbond.bonding_purse(), U512::zero()); - } - self.write_entry(Key::Unbond(acc), StoredValue::Unbonding(vec![])); + for (acc, mut purses) in unbonds { + let old_len = purses.len(); + purses.retain(|purse| purse.bonding_purse().addr() != affected_purse.addr()); + if purses.len() != old_len { + self.write_unbond(acc, purses); } } } diff --git a/utils/global-state-update-gen/src/generic/testing.rs b/utils/global-state-update-gen/src/generic/testing.rs index c822ba6934..3297f0ddd9 100644 --- a/utils/global-state-update-gen/src/generic/testing.rs +++ b/utils/global-state-update-gen/src/generic/testing.rs @@ -270,6 +270,10 @@ impl StateReader for MockStateReader { fn get_unbonds(&mut self) -> UnbondingPurses { self.unbonds.clone() } + + fn get_unbonding_delay(&mut self) -> u64 { + 7 + } } impl ValidatorInfo { @@ -1556,7 +1560,16 @@ fn should_slash_a_validator_and_delegator_with_enqueued_withdraws() { update.assert_validators(&[ValidatorInfo::new(&validator1, U512::from(114))]); update.assert_seigniorage_recipients_written(&mut reader); - update.assert_total_supply(&mut reader, 327); + // validator 1 balance = 101 + + // validator 1 stake = 101 + + // delegator 1 stake = 13 + + // validator 2 balance = 102 + + // past delegator 1 stake = 10 + + // past delegator 2 stake = 10 + // = 337 + // (slashed: validator 2 stake = 102) + // (slashed: delegator 2 stake = 14) + update.assert_total_supply(&mut reader, 337); // check validator2 slashed let old_bid2 = reader @@ -1574,17 +1587,6 @@ fn should_slash_a_validator_and_delegator_with_enqueued_withdraws() { .bonding_purse(), 0, ); - // check past_delegator2 slashed - update.assert_written_balance( - *reader - .withdraws - .get(&validator2.to_account_hash()) - .expect("should have withdraws for validator2") - .last() - .expect("should have withdraw purses") - .bonding_purse(), - 0, - ); // check validator1 and its delegators not slashed for withdraw in reader @@ -1595,21 +1597,17 @@ fn should_slash_a_validator_and_delegator_with_enqueued_withdraws() { update.assert_key_absent(&Key::Balance(withdraw.bonding_purse().addr())); } - // check the withdraws under validator 2 are cleared - update.assert_withdraws_empty(&validator2); - // check the withdraws under validator 1 are unchanged update.assert_key_absent(&Key::Withdraw(validator1.to_account_hash())); - // 7 keys should be written: + // 6 keys should be written: // - seigniorage recipients // - total supply // - bid for validator 2 // - bonding purse balance validator 2 // - bonding purse balance for delegator 2 - // - bonding purse balance for past delegator 2 - // - empty WithdrawPurses for validator 2 - assert_eq!(update.len(), 7); + // - WithdrawPurses for validator 2 + assert_eq!(update.len(), 6); } #[test] @@ -1681,7 +1679,16 @@ fn should_slash_a_validator_and_delegator_with_enqueued_unbonds() { update.assert_validators(&[ValidatorInfo::new(&validator1, U512::from(114))]); update.assert_seigniorage_recipients_written(&mut reader); - update.assert_total_supply(&mut reader, 327); + // validator 1 balance = 101 + + // validator 1 stake = 101 + + // delegator 1 stake = 13 + + // validator 2 balance = 102 + + // past delegator 1 stake = 10 + + // past delegator 2 stake = 10 + // = 337 + // (slashed: validator 2 stake = 102) + // (slashed: delegator 2 stake = 14) + update.assert_total_supply(&mut reader, 337); // check validator2 slashed let old_bid2 = reader @@ -1699,17 +1706,6 @@ fn should_slash_a_validator_and_delegator_with_enqueued_unbonds() { .bonding_purse(), 0, ); - // check past_delegator2 slashed - update.assert_written_balance( - *reader - .unbonds - .get(&validator2.to_account_hash()) - .expect("should have unbonds for validator2") - .last() - .expect("should have unbond purses") - .bonding_purse(), - 0, - ); // check validator1 and its delegators not slashed for unbond in reader @@ -1720,9 +1716,6 @@ fn should_slash_a_validator_and_delegator_with_enqueued_unbonds() { update.assert_key_absent(&Key::Balance(unbond.bonding_purse().addr())); } - // check the unbonds under validator 2 are cleared - update.assert_unbonds_empty(&validator2); - // check the withdraws under validator 1 are unchanged update.assert_key_absent(&Key::Unbond(validator1.to_account_hash())); @@ -1732,9 +1725,8 @@ fn should_slash_a_validator_and_delegator_with_enqueued_unbonds() { // - bid for validator 2 // - bonding purse balance validator 2 // - bonding purse balance for delegator 2 - // - bonding purse balance for past delegator 2 - // - empty UnbondingPurses for validator 2 - assert_eq!(update.len(), 7); + // - UnbondingPurses for validator 2 + assert_eq!(update.len(), 6); } #[test] diff --git a/utils/global-state-update-gen/src/generic/update.rs b/utils/global-state-update-gen/src/generic/update.rs index 53a0e89a2a..19e6246701 100644 --- a/utils/global-state-update-gen/src/generic/update.rs +++ b/utils/global-state-update-gen/src/generic/update.rs @@ -197,26 +197,4 @@ impl Update { pub(crate) fn assert_validators_unchanged(&self) { assert!(self.validators.is_none()); } - - #[track_caller] - pub(crate) fn assert_withdraws_empty(&self, validator_key: &PublicKey) { - let withdraws = self - .entries - .get(&Key::Withdraw(validator_key.to_account_hash())) - .expect("should have withdraw purses") - .as_withdraw() - .expect("should be vec of withdraws"); - assert!(withdraws.is_empty()); - } - - #[track_caller] - pub(crate) fn assert_unbonds_empty(&self, validator_key: &PublicKey) { - let unbonds = self - .entries - .get(&Key::Unbond(validator_key.to_account_hash())) - .expect("should have unbond purses") - .as_unbonding() - .expect("should be vec of unbonds"); - assert!(unbonds.is_empty()); - } }