Newer
Older
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
fn check_bonded_consistency() -> Result<(), TryRuntimeError> {
use sp_std::collections::btree_set::BTreeSet;
let mut count_controller_double = 0;
let mut count_double = 0;
let mut count_none = 0;
// sanity check to ensure that each controller in Bonded storage is associated with only one
// ledger.
let mut controllers = BTreeSet::new();
for (stash, controller) in <Bonded<T>>::iter() {
if !controllers.insert(controller.clone()) {
count_controller_double += 1;
}
match (<Ledger<T>>::get(&stash), <Ledger<T>>::get(&controller)) {
(Some(_), Some(_)) =>
// if stash == controller, it means that the ledger has migrated to
// post-controller. If no migration happened, we expect that the (stash,
// controller) pair has only one associated ledger.
if stash != controller {
count_double += 1;
},
(None, None) => {
count_none += 1;
},
_ => {},
};
}
if count_controller_double != 0 {
log!(
warn,
"a controller is associated with more than one ledger ({} occurrences)",
count_controller_double
);
};
if count_double != 0 {
log!(warn, "single tuple of (stash, controller) pair bonds more than one ledger ({} occurrences)", count_double);
}
if count_none != 0 {
log!(warn, "inconsistent bonded state: (stash, controller) pair missing associated ledger ({} occurrences)", count_none);
}
Ok(())
}
/// Invariants:
/// * A bonded ledger should always have an assigned `Payee`.
/// * The number of entries in `Payee` and of bonded staking ledgers *must* match.
/// * The stash account in the ledger must match that of the bonded account.
fn check_payees() -> Result<(), TryRuntimeError> {
for (stash, _) in Bonded::<T>::iter() {
ensure!(Payee::<T>::get(&stash).is_some(), "bonded ledger does not have payee set");
}
ensure!(
(Ledger::<T>::iter().count() == Payee::<T>::iter().count()) &&
(Ledger::<T>::iter().count() == Bonded::<T>::iter().count()),
"number of entries in payee storage items does not match the number of bonded ledgers",
);
Ok(())
}
/// Invariants:
/// * Number of voters in `VoterList` match that of the number of Nominators and Validators in
/// the system (validator is both voter and target).
/// * Number of targets in `TargetList` matches the number of validators in the system.
/// * Current validator count is bounded by the election provider's max winners.
fn check_count() -> Result<(), TryRuntimeError> {
ensure!(
<T as Config>::VoterList::count() ==
Nominators::<T>::count() + Validators::<T>::count(),
"wrong external count"
);
ensure!(
<T as Config>::TargetList::count() == Validators::<T>::count(),
"wrong external count"
);
ensure!(
ValidatorCount::<T>::get() <=
<T::ElectionProvider as frame_election_provider_support::ElectionProviderBase>::MaxWinners::get(),
Error::<T>::TooManyValidators
Ok(())
}
/// Invariants:
/// * Stake consistency: ledger.total == ledger.active + sum(ledger.unlocking).
/// * The ledger's controller and stash matches the associated `Bonded` tuple.
/// * Staking locked funds for every bonded stash (non virtual stakers) should be the same as
/// its ledger's total.
/// * For virtual stakers, locked funds should be zero and payee should be non-stash account.
/// * Staking ledger and bond are not corrupted.
fn check_ledgers() -> Result<(), TryRuntimeError> {
Bonded::<T>::iter()
Gonçalo Pestana
committed
.map(|(stash, ctrl)| {
// ensure locks consistency.
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
if VirtualStakers::<T>::contains_key(stash.clone()) {
ensure!(
T::Currency::balance_locked(crate::STAKING_ID, &stash) == Zero::zero(),
"virtual stakers should not have any locked balance"
);
ensure!(
<Bonded<T>>::get(stash.clone()).unwrap() == stash.clone(),
"stash and controller should be same"
);
ensure!(
Ledger::<T>::get(stash.clone()).unwrap().stash == stash,
"ledger corrupted for virtual staker"
);
let reward_destination = <Payee<T>>::get(stash.clone()).unwrap();
if let RewardDestination::Account(payee) = reward_destination {
ensure!(
payee != stash.clone(),
"reward destination should not be same as stash for virtual staker"
);
} else {
return Err(DispatchError::Other(
"reward destination must be of account variant for virtual staker",
));
}
} else {
ensure!(
Self::inspect_bond_state(&stash) == Ok(LedgerIntegrityState::Ok),
"bond, ledger and/or staking lock inconsistent for a bonded stash."
);
}
Gonçalo Pestana
committed
// ensure ledger consistency.
Self::ensure_ledger_consistent(ctrl)
})
.collect::<Result<Vec<_>, _>>()?;
Ok(())
/// Invariants:
/// * For each era exposed validator, check if the exposure total is sane (exposure.total =
/// exposure.own + exposure.own).
fn check_exposures() -> Result<(), TryRuntimeError> {
let era = Self::active_era().unwrap().index;
ErasStakers::<T>::iter_prefix_values(era)
.map(|expo| {
ensure!(
expo.total ==
expo.own +
expo.others
.iter()
.map(|e| e.value)
.fold(Zero::zero(), |acc, x| acc + x),
"wrong total exposure.",
);
Ok(())
})
.collect::<Result<(), TryRuntimeError>>()
/// Invariants:
/// * For each paged era exposed validator, check if the exposure total is sane (exposure.total
/// = exposure.own + exposure.own).
/// * Paged exposures metadata (`ErasStakersOverview`) matches the paged exposures state.
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
fn check_paged_exposures() -> Result<(), TryRuntimeError> {
use sp_staking::PagedExposureMetadata;
use sp_std::collections::btree_map::BTreeMap;
// Sanity check for the paged exposure of the active era.
let mut exposures: BTreeMap<T::AccountId, PagedExposureMetadata<BalanceOf<T>>> =
BTreeMap::new();
let era = Self::active_era().unwrap().index;
let accumulator_default = PagedExposureMetadata {
total: Zero::zero(),
own: Zero::zero(),
nominator_count: 0,
page_count: 0,
};
ErasStakersPaged::<T>::iter_prefix((era,))
.map(|((validator, _page), expo)| {
ensure!(
expo.page_total ==
expo.others.iter().map(|e| e.value).fold(Zero::zero(), |acc, x| acc + x),
"wrong total exposure for the page.",
);
let metadata = exposures.get(&validator).unwrap_or(&accumulator_default);
exposures.insert(
validator,
PagedExposureMetadata {
total: metadata.total + expo.page_total,
own: metadata.own,
nominator_count: metadata.nominator_count + expo.others.len() as u32,
page_count: metadata.page_count + 1,
},
);
Ok(())
})
.collect::<Result<(), TryRuntimeError>>()?;
exposures
.iter()
.map(|(validator, metadata)| {
let actual_overview = ErasStakersOverview::<T>::get(era, validator);
ensure!(actual_overview.is_some(), "No overview found for a paged exposure");
let actual_overview = actual_overview.unwrap();
ensure!(
actual_overview.total == metadata.total + actual_overview.own,
"Exposure metadata does not have correct total exposed stake."
);
ensure!(
actual_overview.nominator_count == metadata.nominator_count,
"Exposure metadata does not have correct count of nominators."
);
ensure!(
actual_overview.page_count == metadata.page_count,
"Exposure metadata does not have correct count of pages."
);
Ok(())
})
.collect::<Result<(), TryRuntimeError>>()
}
/// Invariants:
/// * Checks that each nominator has its entire stake correctly distributed.
fn check_nominators() -> Result<(), TryRuntimeError> {
// a check per nominator to ensure their entire stake is correctly distributed. Will only
// kick-in if the nomination was submitted before the current era.
let era = Self::active_era().unwrap().index;
// cache era exposures to avoid too many db reads.
let era_exposures = T::SessionInterface::validators()
.iter()
.map(|v| Self::eras_stakers(era, v))
.collect::<Vec<_>>();
<Nominators<T>>::iter()
.filter_map(
|(nominator, nomination)| {
if nomination.submitted_in < era {
Some(nominator)
} else {
None
}
},
)
.map(|nominator| -> Result<(), TryRuntimeError> {
// must be bonded.
Self::ensure_is_stash(&nominator)?;
let mut sum = BalanceOf::<T>::zero();
.map(|e| -> Result<(), TryRuntimeError> {
let individual =
e.others.iter().filter(|e| e.who == nominator).collect::<Vec<_>>();
let len = individual.len();
match len {
0 => { /* not supporting this validator at all. */ },
1 => sum += individual[0].value,
_ =>
return Err(
"nominator cannot back a validator more than once.".into()
),
};
Ok(())
})
.collect::<Result<Vec<_>, _>>()?;
// We take total instead of active as the nominator might have requested to unbond
// some of their stake that is still exposed in the current era.
if sum <= Self::ledger(Stash(nominator.clone()))?.total {
// This can happen when there is a slash in the current era so we only warn.
log!(warn, "nominator stake exceeds what is bonded.");
}
.collect::<Result<Vec<_>, _>>()?;
Ok(())
}
fn ensure_is_stash(who: &T::AccountId) -> Result<(), &'static str> {
ensure!(Self::bonded(who).is_some(), "Not a stash.");
Ok(())
}
fn ensure_ledger_consistent(ctrl: T::AccountId) -> Result<(), TryRuntimeError> {
// ensures ledger.total == ledger.active + sum(ledger.unlocking).
let ledger = Self::ledger(StakingAccount::Controller(ctrl.clone()))?;
let real_total: BalanceOf<T> =
ledger.unlocking.iter().fold(ledger.active, |a, c| a + c.value);
ensure!(real_total == ledger.total, "ledger.total corrupt");
Ok(())
}
}