[PR#12889](https://github.com/paritytech/substrate/pull/12889) has introduced a helper for UI/UXs to query the minimum active stake by storing `MinimumActiveStake` in storage. This value is updated everytime the snapshot is created and the election data provider iterates over the `T::VotersList` (bags-list) to select the potential active nominators for the next era.
It has been reported that `MinimumActiveStake` in Polkadot and Kusama has zero in some eras. There are a few conditions when this may happen, namely:
**1. `all_voters` vec is empty**
```rust=
let min_active_stake: T::CurrencyBalance =
if all_voters.len() == 0 { 0u64.into() } else { min_active_stake.into() };
MinimumActiveStake::<T>::put(min_active_stake);
```
which basically means that if the selected voters vec size is 0 (i.e. there is not stakers for this era), the `MinimumActiveStake` will be 0 as well. This case effectively would result on an election with low score and the snapshot metadata would reflect it.
Given that the election is progressing as expected, the snapshot and snapshot metadata look OK (e.g. `snapshot_metadata: {voters: 12,338, targets: 1,969}`) at the time when `MinimumActiveStake` is 0, we can conclude this case is not happening.
**2. `StakingLedger` corrupt state/decoding issues**
In the current implementation, if there is a decoding error when calculating the `voter_weight` in `ElectionDataProvider::electing_voters`, the vote weight for the selected nominator is 0. `fn weight_of` implementation relies on `fn slashable_balance_of` to convert the nomination backing from `BalanceOf<T>` to `u64` and may return 0 in case of decoding issues:
```rust=
/// The total balance that can be slashed from a stash account as of right now.
pub fn slashable_balance_of(stash: &T::AccountId) -> BalanceOf<T> {
// Weight note: consider making the stake accessible through stash.
Self::bonded(stash).and_then(Self::ledger).map(|l| l.active).unwrap_or_default()
}
```
Given the way currently `MinimumActiveStake` is calculated, the final value will be 0 if at least one decoding error happened when iterating over the npos voters.
**3. Nominators with `ledger.active = 0` balance in `VoterList`**
Nominators in `StakingLedger` may have their active/total balance below the `MinNominatorBond` and the active balance may even be zero. This can happen in a couple different cases:
1. The `MinNominatorBond` has increased after the nomination;
2. The stash has unbounded some/all of their funds and some/all of the unlocking chunks are still queued.
In the case 2., it may be possible to call `chill_other`. However, this requires a few conditions to me met:
- the caller is the nominator stash/controller;
- `ChillThreshold` `MaxNominatorCount` and `MaxValidatorCount` are set and the nominator/validator count is larger than the `ChillThreshold * MaxCount`
➡️ In Kusama, the `ChillThreshold` is not set, so it is not possible to chill the lingering stashes with zero `ledger.active` to clean the ledger after.
### Actionable items
- [x] **Check potential decoding error in Kusama** ➡️ No decoding issues
`Staking.Ledger` entries are decodable, i.e. vecs `Ledger.Unlocking` and `Ledger.ClaimedRewards` have elements within the expected bounds (32 and 84, respectively).
```javascript
(await api.query.staking.ledger.entries())
.map(([_, l]) => {
let unlocking = l.unwrap().unlocking.length;
let claim = l.unwrap().claimedRewards.length;
if (unlocking > max_unlocking) {
max_unlocking = unlocking;
}
if (unlocking < min_unlocking) {
min_unlocking = unlocking;
}
if (claim > max_claim) {
max_claim = claim;
}
if (claim < min_claim) {
min_claim = claim;
}
});
console.log(`unlocking: [${min_unlocking}, ${max_unlocking}]`);
console.log(`claim: [${min_claim}, ${max_claim}]`);
// unlocking: [0, 32]
// claim: [0, 84]
```
- [x] **Ensure that `MinimumActiveStake` is never below `MinNominatorBond`**
- [x] Add tests to ensurer if this happens, `MinNominatorBond` is returned
- https://github.com/paritytech/substrate/pull/13946
- [x] **Understand why there are `voter_weight == 0` and `voter_weight < min_nominator_bond` in the election snapshot**
For account `HhnW2Yv4mR8N4nT2CoxntoBaKhPm3BxPmBAGgw6GN8xY5jF`, the extrinsic path for the stake to be 0 and still be part of the staking ledger was the following:
- @block `#2374226`, `bound(10KSM)` (~1065 days ago)
- @block `#6324235`, `unbound(10.524KSM)` (~786 days ago)
- @block `6538443`, `unbound(0.011KSM)` (~771 days ago)
Current state of the ledger:
```
staking.ledger: Option<PalletStakingStakingLedger>
{
stash: HhnW2Yv4mR8N4nT2CoxntoBaKhPm3BxPmBAGgw6GN8xY5jF
total: 11,761,211,430
active: 0
unlocking: [
{
value: 11,761,211,430
era: 2,015
}
]
claimedRewards: [ .... ]
}
```
Conclusion: for `Call::unbound` to be successful with a final `ledger.active = 0` the `MinNominatorBond/MinValidatorBond` *cannot* be set.
- ➡️ `Call::reap_stash` and `Call::chill_other` don't work.
- `Call::reap_stash`: `ledger.total` > ED (unlocking chunks);
- `Call::chill_other`: can't be called due to `ChillThreshold` not defined in Kusama
- `Call::withdraw_unbonded` must be called by the stash's controller
- ➡️ old accounts with unlocking chunks and `ledger.active` set in the past that cannot be `chill_other` since in Kusama there is no `ChillingThreshold` set.
**Solution**: i) use governance to clean the storage from all stashes with `ledger.active = 0` (?); ii) other?
#### @ block `#17534861` in Kusama:
Checking the snapshot state and metadata to ensure that the returned `voters` from `ElectionDataProvider::electing_voters` has len > 0 (and thus `min active stake` should be > 0, provided that there is no decoding issues).
❓ are there any voter with `voter_weight = 0` and `voter_weight < min_nominator_bond` in the snapshot?
yep:
```
- snapshot voters count: 12349
- total 0 balance accounts processed: 41
- total below min nominator bond accounts processed: 1681
```
Accounts in the snapshot with `ledger.active == 0`:
```
Account HhnW2Yv4mR8N4nT2CoxntoBaKhPm3BxPmBAGgw6GN8xY5jF has voter_weight 0
Account HS2t9kSqPSEyy72bdubsChH4pyHqbtiRHdH9Ua9rTXH5cnZ has voter_weight 0
Account HRtysNYP7Dcde1LjoQKvc55vyb1rZCcYD5AyQ9RXm1Hg79t has voter_weight 0
Account G4NPqUdpZ6LjHU1nAFwrwXeaQJQ8gESHgG6PurNd6pEJNNV has voter_weight 0
Account GD4eLY4LHDuKqygEUGY2b2VbeF3VUY8srF3MvT783ZwxoQc has voter_weight 0
Account EyyPnX95cE58Pexg4nfq7bkXz3FgNbyDdAfgKosMBJ73Ezs has voter_weight 0
Account DXUA9j8aaCfJ3r7ZQEY9BPd2Y8Q4Z3gqyfJZVAYXC4TTpsA has voter_weight 0
Account FmnWjFoYXvDbeM4MsTMZoSrNWqZeGdLzQtn8YGM1RpkhZnu has voter_weight 0
Account CahiA4dAnpi9zXw1vLbt6S8WdECvsy2FJ3W7yb21f14cVbZ has voter_weight 0
Account GAfmJsurdXoduDSWR632VSGKLtS7XsGoaddRwEfxyJKof2g has voter_weight 0
Account FozTcYuTvezCbTfZk88vRBA2jSdvH8wEj2hh4emg7M5ZExE has voter_weight 0
Account Cx5qYMJDA7fuH8MAeRf5o4Xya4Zz9zAqqvSz1joXKoectda has voter_weight 0
Account Ez8hKspXTFRyeBjBaKVhBASGpS74siR7HG7pZFLUKkUTu4Z has voter_weight 0
Account ENiDkMCgnBJu83ePh5ZQ99KU65YxB2N9VsGXAP3qSx4sYo6 has voter_weight 0
Account J2FwSPKX2MAWmqwRrxTHLrmwNyo3hXoPgNyc7ruwak7VwbZ has voter_weight 0
Account FVD7T8h5zGqfeXzWHVNAb5AabDGkAaMHEDEYqVcCeeaLZkV has voter_weight 0
Account FQ43rZFPc4aiPUySAcudHPT564fAAXVdhk1yna5Dto8MQfX has voter_weight 0
Account J9MFENZ5anqhf2ycma9TfKkt68gFPvRVMAKRCJrta5UD3jz has voter_weight 0
Account EM4w1ycbRYf3G54iPX9HTZuoDyCBEq72whB1jqtQmfrrMdc has voter_weight 0
Account FvTDoY3dKdHaXA7sk9yco6KBAuRMteATrA647bbZgbQXwCT has voter_weight 0
Account Dz9bBTBtFj8sUu7t6Rdj8Qq8TzRF7ts1uLmJgtLgc4zaFgL has voter_weight 0
Account DNok6X7kcekdDUHEjG5Pf71kdAXM3JntKo9h7fnb3n1WA7T has voter_weight 0
Account HnQi8QZxiy5CMTqe2QHEbKogAvvTdeyj64tPXHKPT2AmbyG has voter_weight 0
Account EX6HfRaJ4PLC4BsvxeYNcs1QuNFVPCP4Vb49eQ6Swwk1Btz has voter_weight 0
Account DKKCit6WyimEjZ8NT5jncCNiMEmRWsf9SrXzpkSgurz8VCe has voter_weight 0
Account HEyLyrhaTLE6WsEU93baEhtmgXfNfeMr5G5iwzb83qYFH8S has voter_weight 0
Account E7iqGR2Y7QExQ8KdEP8q2o15WvPq5XmuRmGaLC8RPdNvpGr has voter_weight 0
Account EMmEryAxfFfytHmafdvXGKdLwHSm1fsRsr1p9tpUkKJhGjC has voter_weight 0
Account DiTHzcYRdG8na8RWvFS8UhGbFRqTdmNiTW4XMtNF78qyXPX has voter_weight 0
Account DiWdbs57aTEpsvGWTKCKP3AeVMuC9erUkg1g1QopB8J7cD6 has voter_weight 0
Account FwBfvqpJZPrcnBLx1rvfDhScmsvumKhGWrTFS5NwTakvg4R has voter_weight 0
Account EVVfSZo1qjPnEkR8hPXzfNVKDpZEjiy5W8GVzuKbywvwJT9 has voter_weight 0
Account HHrEN83F6aTB7iZ9Bu2rBVomZNXjejm1wZXnW8pftRhPaWd has voter_weight 0
Account D5KPoy8HZ2reM4PWUFKxr9CwdGFHAdbMFD4UXcPRPcwwdNG has voter_weight 0
Account DcqV9NPaf9TC8w2BmLxmaGSy9ZoKu5f5Ass7J1mqgLyzGwX has voter_weight 0
Account Cmksma7nuY4ptQcrJNGfbmf1Hm4Q7eXPGrAxSjD6YvhEvqM has voter_weight 0
Account HKsMa8PovwAaSAogro9m5HB7rutSYJ2ZCADtxrFa3JsAxyC has voter_weight 0
Account E5mdCLugfUcMPiTXTERDi9x6DrTg8bUcoAMDsiA4qEym64H has voter_weight 0
Account EFVdUigKuTZved95fmLexDAXM9VmiVCpUB8yRtQ4S6wAuz4 has voter_weight 0
Account GA1WBfVMBReXjWKGnXneC682ZZYustFoc8aTsqXv5fFvi2e has voter_weight 0
Account J7ANomHeVgSC6EU6sqVRHoyCrM2RYWSGDJte33YpUHgWn6n has voter_weight 0
```
---
### Notes/drafts:
---
**EPM/data provider flow reminder**
1. (EPM) `Phase::Off && deadline || Phase::Signed && !snapshot` -> call `create_snapthot`
2. `create_snapshot_external` is called to fetch `(targets, voters, desired_targets)` from election data provider (ie. staking).
- if `fn create_snapshot_external` fails, there is a `ElectionError` thrown
3. `create_snapshot_external` fetches voters from election data provider
- Maximum requested voters from data provider is `T::MaxElectingVoters (== 12_500);`
4. `T::DataProvider::electing_voters()` is called
- iterates over the `T::VoterList` until some limits
- if voter is validator, push self-nominating vote into `all_voters`
- if voter is validator, push vote into `all_voters`
There are 2 cases where the `T::VoterList` iterator progresses but the `all_voters` is not updated. Note that `MinimumActiveStake` is 0 IIF `all_voters` lenght is zero. Those 2 cases are:
- all upper sorted nominator votes have 0 targets (should not happen)
- AND no self-nomination votes in the electing voters (not probable)
- OR nominators may not be decodable since they have more nominations than `T::MaxNominations`.
- OR any other bug in the voter list (bags) implementation
**Note**: In Kusama, current snapshot metadata is the following (`Phase::Off`). The snapshot exists as expected, so we can assume that the call to `T::DataProvider::electing_voters` worked as expected and the `MinimumActiveStake` should not be 0 🤔
```
electionProviderMultiPhase.snapshotMetadata: Option<PalletElectionProviderMultiPhaseSolutionOrSnapshotSize>
{
voters: 12,338
targets: 1,969
}
```
- [ ] ❓ should the minimum active stake be 0 at any time? If (for some reason) it may be zero, it should be the min between `MinNominatorBond` and `MinValidatorBond` (perhaps?).
- [ ] Minimum `MinimumActiveStake` should not be lower than `T::MinNominatorBond`
**Current state in Polkadot**:
```
staking.minValidatorBond: (u128) 0
staking.minNominatorBond: (u128) 2,500,000,000,000
```
**Current state in Kusama**:
```
staking.minValidatorBond: (u128) 0
staking.minNominatorBond: (u128) 100,000,000,000
```