-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Suggestions for #9081 (Store voters in unsorted bags) #9328
Changes from 8 commits
01a04cc
a37e59d
f047cec
84dfba2
f0e754f
aceb642
19eb44f
89f5319
65e3bb3
57b9004
2b5d9c8
33ba6fb
21f77eb
2e2fada
3dc2e52
e0dfed0
f1f25f4
90a04d0
c466c95
fd907c6
a626db0
2e9c6d9
8deedee
8c9c736
4645325
1f3c154
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,12 +21,8 @@ | |
| //! - It's efficient to iterate over the top* N voters by stake, where the precise ordering of | ||
| //! voters doesn't particularly matter. | ||
|
|
||
| use crate::{ | ||
| AccountIdOf, Config, Nominations, Nominators, Pallet, Validators, VoteWeight, VoterBagFor, | ||
| VotingDataOf, slashing::SlashingSpans, | ||
| }; | ||
| use codec::{Encode, Decode}; | ||
| use frame_support::{DefaultNoBound, traits::Get}; | ||
| use codec::{Decode, Encode}; | ||
| use frame_support::{traits::Get, DefaultNoBound}; | ||
| use sp_runtime::SaturatedConversion; | ||
| use sp_std::{ | ||
| boxed::Box, | ||
|
|
@@ -35,6 +31,11 @@ use sp_std::{ | |
| marker::PhantomData, | ||
| }; | ||
|
|
||
| use crate::{ | ||
| slashing::SlashingSpans, AccountIdOf, Config, Nominations, Nominators, Pallet, Validators, | ||
| VoteWeight, VoterBagFor, VotingDataOf, | ||
| }; | ||
|
|
||
| /// [`Voter`] parametrized by [`Config`] instead of by `AccountId`. | ||
| pub type VoterOf<T> = Voter<AccountIdOf<T>>; | ||
|
|
||
|
|
@@ -438,9 +439,9 @@ impl<T: Config> Bag<T> { | |
| node.put(); | ||
|
|
||
| // update the previous tail | ||
| if let Some(mut tail) = self.tail() { | ||
| tail.next = Some(id.clone()); | ||
| tail.put(); | ||
| if let Some(mut old_tail) = self.tail() { | ||
| old_tail.next = Some(id.clone()); | ||
| old_tail.put(); | ||
| } | ||
|
|
||
| // update the internal bag links | ||
|
|
@@ -458,19 +459,56 @@ impl<T: Config> Bag<T> { | |
| /// the first place. Generally, use [`VoterList::remove`] instead. | ||
| /// | ||
| /// Storage note: this modifies storage, but only for adjacent nodes. You still need to call | ||
| /// `self.put()` and `node.put()` after use. | ||
| /// `self.put()`, `VoterNodes::remove(voter_id)` and `VoterBagFor::remove(voter_id)` | ||
| /// to update storage for the bag and `node`. | ||
| fn remove_node(&mut self, node: &Node<T>) { | ||
| // TODO: we could merge this function here. | ||
| // node.excise(); | ||
| // Excise `node`. | ||
| if let Some(mut prev) = node.prev() { | ||
emostov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| prev.next = self.next.clone(); | ||
| debug_assert!( | ||
| self.head.as_ref() != Some(&node.voter.id), | ||
| "node is the head, but has Some prev" | ||
| ); | ||
| debug_assert!( | ||
| prev.prev().is_some() || self.head.as_ref() == Some(&prev.voter.id), | ||
| "node.prev.prev should be Some OR node.prev should be the head" | ||
| ); | ||
|
||
|
|
||
| prev.next = node.next.clone(); | ||
| debug_assert!( | ||
| prev.next().and_then(|prev_next| | ||
| // prev.next.prev should point at node prior to being reassigned | ||
| Some(prev_next.prev().unwrap().voter.id == node.voter.id) | ||
| ) | ||
| // unless prev.next is None, in which case node has to be the tail | ||
| .unwrap_or(self.tail.as_ref() == Some(&node.voter.id)), | ||
| "prev.next.prev should point at node prior to being reassigned OR node should be the tail" | ||
| ); | ||
|
|
||
| prev.put(); | ||
| } | ||
| if let Some(mut next) = node.next() { | ||
emostov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| next.prev = self.prev.clone(); | ||
| debug_assert!( | ||
| self.tail.as_ref() != Some(&node.voter.id), | ||
| "node is the tail, but has Some next" | ||
| ); | ||
| debug_assert!( | ||
| next.next().is_some() || self.tail.as_ref() == Some(&next.voter.id), | ||
| "node.next.next should be Some OR node.next should be the head" | ||
| ); | ||
emostov marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| next.prev = node.prev.clone(); | ||
| debug_assert!( | ||
| next.prev().and_then(|next_prev| | ||
| // next.prev.next should point at next after being reassigned | ||
| Some(next_prev.next().unwrap().voter.id == next.voter.id) | ||
| ) | ||
| // unless next.prev is None, in which case node has to be the head | ||
| .unwrap_or_else(|| self.head.as_ref() == Some(&node.voter.id)), | ||
| "next.prev.next should point at next after being reassigned OR node should be the head" | ||
| ); | ||
|
|
||
| next.put(); | ||
| } | ||
| // IDEA: debug_assert! prev.next.prev == self | ||
|
|
||
| // clear the bag head/tail pointers as necessary | ||
| if self.head.as_ref() == Some(&node.voter.id) { | ||
|
|
@@ -498,7 +536,10 @@ pub struct Node<T: Config> { | |
| impl<T: Config> Node<T> { | ||
| /// Get a node by bag idx and account id. | ||
| pub fn get(bag_upper: VoteWeight, account_id: &AccountIdOf<T>) -> Option<Node<T>> { | ||
| // debug_assert!(bag_upper is in Threshold) | ||
| // debug_assert!( // TODO: figure out why this breaks test take_works | ||
|
||
| // T::VoterBagThresholds::get().contains(&bag_upper) || bag_upper == VoteWeight::MAX, | ||
| // "it is a logic error to attempt to get a bag which is not in the thresholds list" | ||
| // ); | ||
| crate::VoterNodes::<T>::try_get(account_id).ok().map(|mut node| { | ||
| node.bag_upper = bag_upper; | ||
| node | ||
|
|
@@ -566,6 +607,7 @@ impl<T: Config> Node<T> { | |
| /// Remove this node from the linked list. | ||
| /// | ||
| /// Modifies storage, but only modifies the adjacent nodes. Does not modify `self` or any bag. | ||
| #[allow(dead_code)]// TODO: do we keep? (equivalent code in `fn remove_node`) | ||
emostov marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| fn excise(&self) { | ||
| if let Some(mut prev) = self.prev() { | ||
| prev.next = self.next.clone(); | ||
|
|
@@ -855,10 +897,11 @@ pub mod make_bags { | |
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use crate::mock::{ExtBuilder, Staking, Test}; | ||
| use frame_support::traits::Currency; | ||
| use substrate_test_utils::assert_eq_uvec; | ||
|
|
||
| use super::*; | ||
| use crate::mock::*; | ||
|
|
||
| const GENESIS_VOTER_IDS: [u64; 5] = [11, 21, 31, 41, 101]; | ||
|
|
||
|
|
@@ -890,6 +933,9 @@ mod tests { | |
| // initialize the voters' deposits | ||
| let existential_deposit = <Test as Config>::Currency::minimum_balance(); | ||
| let mut balance = existential_deposit + 1; | ||
| assert_eq!(VoterBagThresholds::get()[1] as u128, balance); | ||
| assert_eq!(balance, 2); | ||
|
|
||
| for voter_id in voters.iter().rev() { | ||
| <Test as Config>::Currency::make_free_balance_be(voter_id, balance); | ||
| let controller = Staking::bonded(voter_id).unwrap(); | ||
|
|
@@ -906,4 +952,58 @@ mod tests { | |
| assert_eq!(voters, have_voters); | ||
| }); | ||
| } | ||
|
|
||
| /// This tests that we can `take` x voters, even if that quantity ends midway through a list. | ||
| #[test] | ||
| fn take_works() { | ||
| ExtBuilder::default().validator_pool(true).build_and_execute(|| { | ||
| // initialize the voters' deposits | ||
| let existential_deposit = <Test as Config>::Currency::minimum_balance(); | ||
| let mut balance = existential_deposit + 1; | ||
| assert_eq!(VoterBagThresholds::get()[1] as u128, balance); | ||
| assert_eq!(balance, 2); | ||
|
|
||
| for (idx, voter_id) in GENESIS_VOTER_IDS.iter().enumerate() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why are we doing this?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am thinking its easier to read the test when you can see up front how they are distributed in the bags. I need to double check how the genesis voters are distributed, but the scheme here guarantees that the last node
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see your point, but when you have 100+ tests, it is quite verbose to setup things manually for each tests. Instead, it is more reasonable to assume some fixed genesis stakers (and the programmer should simply have this in their mind) and write all tests preferably based on that. |
||
| if idx % 3 == 0 { | ||
| // This increases the balance by a constant factor of 2, which is | ||
| // is the factor used to generate the mock bags. Thus this will | ||
| // increase the balance by 1 bag. | ||
| // | ||
| // This will create 2 bags, the lower threshold bag having | ||
| // 3 voters with balance 4, and the higher threshold bag having | ||
| // 2 voters with balance 8. | ||
| balance *= 2; | ||
| } | ||
|
|
||
| <Test as Config>::Currency::make_free_balance_be(voter_id, balance); | ||
| let controller = Staking::bonded(voter_id).unwrap(); | ||
| let mut ledger = Staking::ledger(&controller).unwrap(); | ||
| ledger.total = balance; | ||
| ledger.active = balance; | ||
| Staking::update_ledger(&controller, &ledger); | ||
| Staking::do_rebag(voter_id); | ||
| } | ||
|
|
||
| let bag_thresh4 = <Staking as crate::Store>::VoterBags::get(&4).unwrap().iter() | ||
| .map(|node| node.voter.id).collect::<Vec<_>>(); | ||
| assert_eq!(bag_thresh4, vec![11, 21, 31]); | ||
|
|
||
| let bag_thresh8 = <Staking as crate::Store>::VoterBags::get(&8).unwrap().iter() | ||
| .map(|node| node.voter.id).collect::<Vec<_>>(); | ||
| assert_eq!(bag_thresh8, vec![41, 101]); | ||
|
|
||
|
|
||
| let voters: Vec<_> = VoterList::<Test>::iter() | ||
| // take 4/5 from [41, 101],[11, 21, 31], demonstrating that we can do a | ||
| // take that stops mid bag. | ||
| .take(4) | ||
| .map(|node| node.voter.id) | ||
| .collect(); | ||
|
|
||
| assert_eq!(voters, vec![41, 101, 11, 21]); | ||
| }); | ||
| } | ||
|
|
||
| // TODO: | ||
| // - storage is cleaned up when a voter is removed | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.