- https://github.com/paritytech/substrate/issues/12968
- https://github.com/paritytech/substrate/pull/12970
**Summary of changes**:
- Instead of a fixed `MAX_NOMINATIONS` that defines the maximum number of votes per voter, implement a curve where the max nominations is pegged to the nominator's balance. Any implementor of `trait NominationsQuota` can be used as a curve.
- Refactor the elections and data provider bounds.
- Implement election bounds over two independent axis: count and size (in bytes) of both the voters and candidates in the election snapshot.
## Snapshot bounds tracker
As the election data provider is preparing the results of [electing voters](https://paritytech.github.io/substrate/master/frame_election_provider_support/trait.ElectionDataProvider.html#tymethod.electing_voters), it keeps track of how much size (in bytes) the returned result will be. If either the size of the struct or the count of elements is exhausted, it returns earlier and do not iterate the remaining voters. In summary, we make sure that both size and count bounds provided by the caller of the data provider when requesting the electing voters is respected.
The data provider needs to keep track of the size and count as it is preparing the returned results. The data provider instantiates and updates an `ElectionSizeTracker` for that effect. This way, the data provider can keep track of both the size and count state of the current result without having to recalculate it at every iteration which may be expensive, especially to keep track of the size in bytes of the current result.
### Keeping track of the results size (in bytes)
The snapshot returned by the election data provided will be stored in the [EPM pallet](https://paritytech.github.io/substrate/master/pallet_election_provider_multi_phase/index.html) runtime storage. We want to make to control the bounds of the snapshot stored in the runtime storage. The runtime storage stores [SCALE encoded](https://docs.rs/parity-scale-codec/latest/parity_scale_codec/) byte streams. Note that the size of the snapshot in memory is different than the size of the same snapshot stored in the runtime storage. We are interested in controlling the bounds of the latter so that it is easier to reason about the bounds from a runtime developer perspective.
Optimally, we would want to use the method `fn encoded_size` required by `trait Encode` to calculate the size of the snapshot. However, calling `fn encoded_size` over all elements of the snapshot will be expensive and we're performing unecessary encoding. The question then is, how do we estimate and/or calculate the encoded size of the snapshot without having to encode/decode at every iteration?
**Option 1.** Calculate the `fn encoded_size` of every single selected voter**
- As the electing voters are iterated, we call `fn encoded_size` on the voter to add to the snapshot.
- The issue with this approach is that `fn encoded_size` requires allocation to encode the type and return the size. Too expensive.
**Option 2.** Use `sp_std::mem::size_of` to calculate the size (in bytes) of the tracker on the fly.
- We cannot guarantee that the size that it takes to allocate a type in the stack is the same as the SCALE encoded size.
**Option 3.** Use `size_hint` to estimate the size of the voter's snapshot without having to call `fn encode` at every iteration.
- With `fn size_hint` we can achieve the best of both option 1 and option 2: no need to perform allocation to calculate the accurate/estimate of the type when SCALE encoded and stored in the runtime storage.
The goal is to:
```rust=
let mut tracker = pallet_staking::ElectionSizeTracker::<Staking>::default();
let snapshot_voters = ElectionProviderMultiPhase::snapshot().unwrap().voters;
for voter in snapshot_voters {
assert!(tracker.try_register_voter(&voter, &DataProviderBounds::new_unbounded()).is_ok());
}
assert_eq!(
snapshot_voters.encoded_size(),
ElectionSizeTracker::<Staking>::final_byte_size_of(tracker.num_voters, tracker.size),
);
```
#### `test-staking-e2e` test (should add?)
```rust=
#[test]
fn snapshot_bounds() {
ExtBuilder::default()
.build_and_execute(|| {
assert!(ElectionProviderMultiPhase::snapshot().is_none());
roll_to_epm_unsigned();
let snapshot_voters = ElectionProviderMultiPhase::snapshot().unwrap().voters;
let mut tracker = pallet_staking::Tracker::<Staking>::default();
let encoded_voters_size = snapshot_voters.encoded_size();
// voter: (AccountId, VoteWeight, Vec<votes>)
for voter in snapshot_voters {
tracker.try_register_voter(&voter, &DataProviderBounds::new_unbounded()).unwrap();
}
let tracker_size = pallet_staking::Tracker::<Staking>::final_byte_size_of(tracker.num_voters, tracker.size);
assert_eq!(encoded_voters_size, tracker_size);
})
}
```