Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix nominator_bond_less deletes bottom nomination bug #748

Merged
merged 26 commits into from
Aug 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d43780b
in progress
4meta5 Aug 28, 2021
a4c5852
update top nominators list upon changes and do not remove any bottom …
4meta5 Aug 28, 2021
de70ef5
test fix
4meta5 Aug 28, 2021
a05b143
init migration to start to fix inconsistent state
4meta5 Aug 28, 2021
87f2c49
more progress on migration
4meta5 Aug 28, 2021
3685c8d
improve nomination increase decrease events
4meta5 Aug 29, 2021
abef07b
fmt
4meta5 Aug 29, 2021
51e95b2
review and comment all uses of pop
4meta5 Aug 29, 2021
0660760
add two different errors for each source of inconsistency and init mi…
4meta5 Aug 29, 2021
baa7807
Merge branch 'master' into patch-staking-bond-less-deletes-bottom-nom…
4meta5 Aug 29, 2021
d67c683
add unreserve stakers extrinsic gated by root to unreserve the reserv…
4meta5 Aug 29, 2021
0e3e071
in middle of testing migration and found bug wherein nominator can ha…
4meta5 Aug 29, 2021
2e428bf
fix migration, still needs weights
4meta5 Aug 29, 2021
e09dd1b
rename root unreserve function and finish migration test
4meta5 Aug 29, 2021
946bcf1
last few changes to public visibility to keep the test without exposi…
4meta5 Aug 29, 2021
dbd58d7
fix comment
4meta5 Aug 29, 2021
3cdf928
Fix max nominators per collator upgrade bug (#751)
4meta5 Aug 30, 2021
0648529
fix total staked in hotfix_unreserve_nomination as well
4meta5 Aug 30, 2021
76b3238
accept review comments
4meta5 Aug 30, 2021
f8f341a
fix weight returned in on runtime upgrade
4meta5 Aug 30, 2021
88c06e4
remove accounts due unreserved balance storage item and use btreemap …
4meta5 Aug 30, 2021
fc4a3bb
try fix
4meta5 Aug 30, 2021
0aec5a9
no filtering condition on first migration loop
4meta5 Aug 30, 2021
9b42168
merge migration functions into one function
4meta5 Aug 30, 2021
c6ec390
conservative weight estimate
4meta5 Aug 30, 2021
462347e
Update pallets/parachain-staking/src/lib.rs
4meta5 Aug 30, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 185 additions & 26 deletions pallets/parachain-staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ pub mod pallet {
new_total: self.total_counted,
})
} else {
// >pop requires push to reset in case isn't pushed to bottom
let last_nomination_in_top = self
.top_nominators
.pop()
Expand All @@ -274,7 +275,7 @@ pub mod pallet {
new_total: self.total_counted,
})
} else {
// push previously popped last nomination into top_nominators
// >required push to previously popped last nomination into top_nominators
self.top_nominators.push(last_nomination_in_top);
self.add_bottom_nominator(Bond { owner: acc, amount });
Ok(NominatorAdded::AddedToBottom)
Expand All @@ -286,7 +287,10 @@ pub mod pallet {
&mut self,
nominator: A,
) -> Result<(bool, B), DispatchError> {
ensure!(self.nominators.remove(&nominator), Error::<T>::NominatorDNE);
ensure!(
self.nominators.remove(&nominator),
Error::<T>::NominatorDNEInNominatorSet
);
let mut nominator_stake: Option<B> = None;
self.top_nominators = self
.top_nominators
Expand All @@ -301,17 +305,20 @@ pub mod pallet {
}
})
.collect();
// item removed from the top => highest bottom is popped from bottom and pushed to top
if let Some(s) = nominator_stake {
// last element has largest amount as per ordering
if let Some(last) = self.bottom_nominators.pop() {
self.total_counted -= s - last.amount;
self.add_top_nominator(last);
} else {
// no item in bottom nominators so no item from bottom to pop and push up
self.total_counted -= s;
}
self.total_backing -= s;
return Ok((true, s));
}
// else (no item removed from the top)
self.bottom_nominators = self
.bottom_nominators
.clone()
Expand All @@ -325,7 +332,8 @@ pub mod pallet {
}
})
.collect();
let stake = nominator_stake.ok_or(Error::<T>::NominatorDNE)?;
// if err, no item with account exists in top || bottom
let stake = nominator_stake.ok_or(Error::<T>::NominatorDNEinTopNorBottom)?;
self.total_backing -= stake;
Ok((false, stake))
}
Expand All @@ -342,10 +350,13 @@ pub mod pallet {
break;
}
}
// if nominator was increased in top nominators
if in_top {
self.sort_top_nominators();
return true;
}
// else nominator to increase must exist in bottom
// >pop requires push later on to reset in case it isn't used
let lowest_top = self
.top_nominators
.pop()
Expand All @@ -367,7 +378,7 @@ pub mod pallet {
self.add_bottom_nominator(lowest_top);
true
} else {
// reset top_nominators from earlier pop
// >required push to reset top_nominators from earlier pop
self.top_nominators.push(lowest_top);
self.sort_bottom_nominators();
false
Expand All @@ -376,15 +387,19 @@ pub mod pallet {
/// Return true if in_top after call
pub fn dec_nominator(&mut self, nominator: A, less: B) -> bool {
let mut in_top = false;
let mut new_top: Option<Bond<A, B>> = None;
let mut new_lowest_top: Option<Bond<A, B>> = None;
for x in &mut self.top_nominators {
if x.owner == nominator {
x.amount -= less;
self.total_counted -= less;
self.total_backing -= less;
if let Some(top_bottom) = self.bottom_nominators.pop() {
if top_bottom.amount > x.amount {
new_top = Some(top_bottom);
// if there is at least 1 nominator in bottom nominators, compare it to check
// if it should be swapped with lowest top nomination and put in top
// >pop requires push later on to reset in case it isn't used
if let Some(highest_bottom) = self.bottom_nominators.pop() {
if highest_bottom.amount > x.amount {
new_lowest_top = Some(highest_bottom);
} else {
// >required push to reset self.bottom_nominators
self.bottom_nominators.push(highest_bottom);
}
}
in_top = true;
Expand All @@ -393,14 +408,22 @@ pub mod pallet {
}
if in_top {
self.sort_top_nominators();
if let Some(new) = new_top {
let lowest_top = self.top_nominators.pop().expect("just updated => exists");
self.total_counted -= lowest_top.amount;
self.total_counted += new.amount;
self.add_top_nominator(new);
if let Some(highest_bottom) = new_lowest_top {
// pop last in top to swap it with top bottom
let lowest_top = self
.top_nominators
.pop()
.expect("must have >1 item to update, assign in_top = true");
self.total_counted -= lowest_top.amount + less;
self.total_counted += highest_bottom.amount;
self.total_backing -= less;
self.add_top_nominator(highest_bottom);
self.add_bottom_nominator(lowest_top);
return false;
} else {
// no existing bottom nominators so update both counters the same magnitude
self.total_counted -= less;
self.total_backing -= less;
return true;
}
}
Expand Down Expand Up @@ -443,7 +466,7 @@ pub mod pallet {
Leaving(RoundIndex),
}

#[derive(Encode, Decode, RuntimeDebug)]
#[derive(Clone, Encode, Decode, RuntimeDebug)]
/// Nominator state
pub struct Nominator2<AccountId, Balance> {
/// All current nominations
Expand Down Expand Up @@ -745,8 +768,9 @@ pub mod pallet {

#[pallet::error]
pub enum Error<T> {
// Nominator Does Not Exist
NominatorDNE,
NominatorDNEinTopNorBottom,
NominatorDNEInNominatorSet,
CandidateDNE,
NominationDNE,
NominatorExists,
Expand Down Expand Up @@ -792,17 +816,17 @@ pub mod pallet {
CollatorScheduledExit(RoundIndex, T::AccountId, RoundIndex),
/// Account, Amount Unlocked, New Total Amt Locked
CollatorLeft(T::AccountId, BalanceOf<T>, BalanceOf<T>),
// Nominator, Collator, Old Nomination, Counted in Top, New Nomination
NominationIncreased(T::AccountId, T::AccountId, BalanceOf<T>, bool, BalanceOf<T>),
// Nominator, Collator, Old Nomination, Counted in Top, New Nomination
NominationDecreased(T::AccountId, T::AccountId, BalanceOf<T>, bool, BalanceOf<T>),
// Nominator, Collator, Amount, If in top nominations for collator after increase
NominationIncreased(T::AccountId, T::AccountId, BalanceOf<T>, bool),
// Nominator, Collator, Amount, If in top nominations for collator after decrease
NominationDecreased(T::AccountId, T::AccountId, BalanceOf<T>, bool),
/// Round, Nominator, Scheduled Exit
NominatorExitScheduled(RoundIndex, T::AccountId, RoundIndex),
/// Round, Nominator, Collator, Scheduled Exit
NominationRevocationScheduled(RoundIndex, T::AccountId, T::AccountId, RoundIndex),
/// Nominator, Amount Unstaked
NominatorLeft(T::AccountId, BalanceOf<T>),
/// Nominator, Amount Locked, Collator, Nominator Position with New Total Backing if in Top
/// Nominator, Amount Locked, Collator, Nominator Position with New Total Counted if in Top
Nomination(
T::AccountId,
BalanceOf<T>,
Expand Down Expand Up @@ -837,10 +861,119 @@ pub mod pallet {
Perbill,
Perbill,
),
/// Account, Amount Unreserved by Democracy
HotfixUnreservedNomination(T::AccountId, BalanceOf<T>),
}

pub(crate) fn correct_bond_less_removes_bottom_nomination_inconsistencies<T: Config>(
) -> (u64, u64) {
let (mut reads, mut writes) = (0u64, 0u64);
let mut map: BTreeMap<(T::AccountId, T::AccountId), ()> = BTreeMap::new();
let top_n = T::MaxNominatorsPerCollator::get() as usize;
// 1. for collator state, check if there is a nominator not in top or bottom
for (account, state) in <CollatorState2<T>>::iter() {
reads += 1u64;
// remove all accounts not in self.top_nominators && self.bottom_nominators
let mut nominator_set = Vec::new();
for Bond { owner, .. } in &state.top_nominators {
nominator_set.push(owner.clone());
}
for Bond { owner, .. } in &state.bottom_nominators {
nominator_set.push(owner.clone());
}
for nominator in &state.nominators.0 {
if !nominator_set.contains(nominator) {
// these accounts were removed without being unreserved so we track it
// with this map which will hold the due amount
map.insert((account.clone(), nominator.clone()), ());
}
}
// RESET incorrect storage state
let mut all_nominators = state.top_nominators.clone();
let mut starting_bottom_nominators = state.bottom_nominators.clone();
all_nominators.append(&mut starting_bottom_nominators);
// sort all nominators from greatest to least
all_nominators.sort_unstable_by(|a, b| b.amount.cmp(&a.amount));
// 2. split them into top and bottom using the T::MaxNominatorsPerCollator
let top_nominators: Vec<Bond<T::AccountId, BalanceOf<T>>> =
all_nominators.clone().into_iter().take(top_n).collect();
let bottom_nominators = if all_nominators.len() > top_n {
let rest = all_nominators.len() - top_n;
let bottom: Vec<Bond<T::AccountId, BalanceOf<T>>> = all_nominators
.clone()
.into_iter()
.rev()
.take(rest)
.collect();
bottom
} else {
// empty, all nominations are in top
Vec::new()
};
let (mut total_counted, mut total_backing): (BalanceOf<T>, BalanceOf<T>) =
(0u32.into(), 0u32.into());
for Bond { amount, .. } in &top_nominators {
total_counted += *amount;
total_backing += *amount;
}
for Bond { amount, .. } in &bottom_nominators {
total_backing += *amount;
}
// update candidate pool with new total counted if it changed
if state.total_counted != total_counted && state.is_active() {
reads += 1u64;
writes += 1u64;
<Pallet<T>>::update_active(account.clone(), total_counted);
}
let new_state = Collator2 {
nominators: OrderedSet::from(nominator_set),
top_nominators,
bottom_nominators,
total_counted,
total_backing,
..state
};
<CollatorState2<T>>::insert(&account, new_state);
writes += 1u64;
log::warn!("CORRECTED INCONSISTENT COLLATOR STATE FOR {:?}", account);
}
// 2. for nominator state, check if there are nominations that were inadvertently bumped
// -> this allows us to recover the due unreserved balances for cases of (1) that
// did not have the nominator state removed (it does not account for when it was removed)
for (account, mut state) in <NominatorState2<T>>::iter() {
reads += 1u64;
for Bond { owner, .. } in state.nominations.0.clone() {
if map.get(&(owner.clone(), account.clone())).is_some() {
if state.rm_nomination(owner).is_some() {
if state.nominations.0.is_empty() {
<NominatorState2<T>>::remove(&account);
} else {
<NominatorState2<T>>::insert(&account, state.clone());
}
writes += 1u64;
}
}
}
}
(reads, writes)
}

#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_runtime_upgrade() -> Weight {
let weight = T::DbWeight::get();
if !<FixBondLessMigrationExecuted<T>>::get() {
let (mut reads, mut writes) =
correct_bond_less_removes_bottom_nomination_inconsistencies::<T>();
reads += 1u64;
writes += 1u64;
<FixBondLessMigrationExecuted<T>>::put(true);
// 50% of the max block weight as safety margin for computation
weight.reads(reads) + weight.writes(writes) + 250_000_000_000
} else {
weight.reads(1u64)
}
}
fn on_initialize(n: T::BlockNumber) -> Weight {
let mut round = <Round<T>>::get();
if round.should_update(n) {
Expand Down Expand Up @@ -872,6 +1005,11 @@ pub mod pallet {
}
}

#[pallet::storage]
#[pallet::getter(fn fix_bond_less_migration_executed)]
/// Temporary to check if migration has run
pub(crate) type FixBondLessMigrationExecuted<T: Config> = StorageValue<_, bool, ValueQuery>;

#[pallet::storage]
#[pallet::getter(fn collator_commission)]
/// Commission percent taken off of rewards for all collators
Expand Down Expand Up @@ -907,7 +1045,7 @@ pub mod pallet {
#[pallet::storage]
#[pallet::getter(fn collator_state2)]
/// Get collator state associated with an account if account is collating else None
type CollatorState2<T: Config> = StorageMap<
pub(crate) type CollatorState2<T: Config> = StorageMap<
_,
Twox64Concat,
T::AccountId,
Expand Down Expand Up @@ -1085,6 +1223,27 @@ pub mod pallet {

#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::weight(1_000_000_000)]
/// Temporary root function to return nominations
/// - charges uniform 0.2% of block weight as fee per call
pub fn hotfix_unreserve_nomination(
origin: OriginFor<T>,
stakers: Vec<(T::AccountId, BalanceOf<T>)>,
) -> DispatchResultWithPostInfo {
frame_system::ensure_root(origin)?;
let mut sum_unreserved: BalanceOf<T> = 0u32.into();
4meta5 marked this conversation as resolved.
Show resolved Hide resolved
for (due_account, due_unreserve) in stakers {
T::Currency::unreserve(&due_account, due_unreserve);
Self::deposit_event(Event::HotfixUnreservedNomination(
due_account,
due_unreserve,
));
sum_unreserved += due_unreserve;
}
let new_total = <Total<T>>::get().saturating_sub(sum_unreserved);
<Total<T>>::put(new_total);
Ok(().into())
}
/// Set the expectations for total staked. These expectations determine the issuance for
/// the round according to logic in `fn compute_issuance`
#[pallet::weight(<T as Config>::WeightInfo::set_staking_expectations())]
Expand Down Expand Up @@ -1573,7 +1732,7 @@ pub mod pallet {
let new_total_staked = <Total<T>>::get().saturating_add(more);
<Total<T>>::put(new_total_staked);
Self::deposit_event(Event::NominationIncreased(
nominator, candidate, before, in_top, after,
nominator, candidate, more, in_top,
));
Ok(().into())
}
Expand Down Expand Up @@ -1618,7 +1777,7 @@ pub mod pallet {
let new_total_staked = <Total<T>>::get().saturating_sub(less);
<Total<T>>::put(new_total_staked);
Self::deposit_event(Event::NominationDecreased(
nominator, candidate, before, in_top, after,
nominator, candidate, less, in_top,
));
Ok(().into())
}
Expand Down Expand Up @@ -1657,7 +1816,7 @@ pub mod pallet {
round_issuance.ideal
}
}
fn nominator_leaves_collator(
pub(crate) fn nominator_leaves_collator(
nominator: T::AccountId,
collator: T::AccountId,
) -> DispatchResultWithPostInfo {
Expand Down
2 changes: 1 addition & 1 deletion pallets/parachain-staking/src/set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use sp_std::prelude::*;

/// An ordered set backed by `Vec`
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
#[derive(RuntimeDebug, PartialEq, Eq, Encode, Decode, Default)]
#[derive(RuntimeDebug, PartialEq, Eq, Encode, Decode, Default, Clone)]
pub struct OrderedSet<T>(pub Vec<T>);

impl<T: Ord> OrderedSet<T> {
Expand Down
Loading