Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Add extrinsic to improve position in a bag of bags-list #9829

Merged
merged 42 commits into from
Dec 8, 2021
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
c414583
pallet-bags-list: Add `put_in_front_of` extrinsic
emostov Sep 17, 2021
331df1b
Test & Benchmarks
emostov Sep 21, 2021
e7c016c
Merge remote-tracking branch 'origin' into zeke-change-bag-position
emostov Sep 21, 2021
2401b2b
Respect line width
emostov Sep 21, 2021
60743cc
Remove List::put_in_fron_of tests; Remove AlreadyHigher error
emostov Sep 21, 2021
6bb232f
The dispatch origin for this call must be ...
emostov Sep 21, 2021
45f4a73
Add some periods
emostov Sep 21, 2021
f04a64c
Add back test to list module: put_in_front_of_exits_early_if_bag_not_…
emostov Sep 21, 2021
ce0fcfb
Merge remote-tracking branch 'origin' into zeke-change-bag-position
emostov Sep 21, 2021
aa0aaeb
add test tests::pallet::heavier_is_head_lighter_is_not_terminal
emostov Sep 22, 2021
fa1a33d
Cater for edge case of heavier being head
emostov Sep 23, 2021
21aef98
Add ExtBuilder::add_aux_data; try to make some tests use simpler data
emostov Sep 23, 2021
7433cf6
Update frame/bags-list/src/list/tests.rs
emostov Sep 23, 2021
1760815
make insert_at_unchecked infallible
emostov Sep 23, 2021
fcc29d2
Merge branch 'zeke-change-bag-position' of https://github.com/parityt…
emostov Sep 23, 2021
afff2a9
Make it permissioned - only callable by heavier
emostov Sep 23, 2021
b75457f
Add test cases for insert_at_unchecked
emostov Sep 23, 2021
fd1daeb
Move counter update to insert_at; fix comments
emostov Sep 25, 2021
f5a854f
Try merge origin master
emostov Oct 31, 2021
a12f094
Merge remote-tracking branch 'origin' into zeke-change-bag-position
emostov Nov 6, 2021
d5f82ac
Merge remote-tracking branch 'origin' into zeke-change-bag-position
emostov Nov 8, 2021
f3f992c
Address some feedback
emostov Nov 8, 2021
927a383
Make voteweight constructed with parameter_types
emostov Nov 10, 2021
387ab0d
Always set vote weight for Ids in build
emostov Nov 10, 2021
7d5a1ca
Add skip_genesis_ids
emostov Nov 10, 2021
2add927
Do not pass weight fn to List put_in_front_of
emostov Nov 10, 2021
fe120d6
Merge remote-tracking branch 'origin' into zeke-change-bag-position
emostov Nov 17, 2021
d135b8f
Remove remants of CounterForListNodes
emostov Nov 17, 2021
7530d1a
fmt
emostov Nov 17, 2021
cd39dc9
Merge branch 'master' of https://github.com/paritytech/substrate into…
Nov 17, 2021
fd751fc
cargo run --quiet --release --features=runtime-benchmarks --manifest-…
Nov 17, 2021
f5b0224
Delete messed up weights file
emostov Nov 17, 2021
91317bd
Some place holder stuff so we can run bench in CI
emostov Nov 17, 2021
6af64ce
Fix
emostov Nov 18, 2021
3382286
Merge branch 'master' of https://github.com/paritytech/substrate into…
Nov 18, 2021
56ea8cc
cargo run --quiet --release --features=runtime-benchmarks --manifest-…
Nov 18, 2021
a96e495
Merge remote-tracking branch 'origin' into zeke-change-bag-position
emostov Dec 2, 2021
77fd777
Update frame/bags-list/src/list/mod.rs
emostov Dec 6, 2021
aa4d896
fmt
emostov Dec 7, 2021
944dc85
Log + debug assert when refetching an Id
emostov Dec 7, 2021
ae188ae
Merge remote-tracking branch 'origin' into zeke-change-bag-position
emostov Dec 7, 2021
2eb86e5
Merge remote-tracking branch 'origin' into zeke-change-bag-position
emostov Dec 7, 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
44 changes: 38 additions & 6 deletions frame/bags-list/src/benchmarks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

use super::*;
use crate::list::List;
use frame_benchmarking::{account, whitelisted_caller};
use frame_benchmarking::{account, whitelist_account, whitelisted_caller};
use frame_election_provider_support::VoteWeightProvider;
use frame_support::{assert_ok, traits::Get};
use frame_system::RawOrigin as SystemOrigin;
Expand Down Expand Up @@ -135,9 +135,41 @@ frame_benchmarking::benchmarks! {
);
}

impl_benchmark_test_suite!(
Pallet,
crate::mock::ExtBuilder::default().build(),
crate::mock::Runtime,
)
put_in_front_of {
// The most expensive case for `put_in_front_of`:
//
// - both heavier's `prev` and `next` are nodes that will need to be read and written.
// - `lighter` is the bag's `head`, so the bag will need to be read and written.
gui1117 marked this conversation as resolved.
Show resolved Hide resolved

let bag_thresh = T::BagThresholds::get()[0];

// insert the nodes in order
let lighter: T::AccountId = account("lighter", 0, 0);
assert_ok!(List::<T>::insert(lighter.clone(), bag_thresh));

let heavier_prev: T::AccountId = account("heavier_prev", 0, 0);
assert_ok!(List::<T>::insert(heavier_prev.clone(), bag_thresh));

let heavier: T::AccountId = account("heavier", 0, 0);
assert_ok!(List::<T>::insert(heavier.clone(), bag_thresh));

let heavier_next: T::AccountId = account("heavier_next", 0, 0);
assert_ok!(List::<T>::insert(heavier_next.clone(), bag_thresh));

T::VoteWeightProvider::set_vote_weight_of(&lighter, bag_thresh - 1);
T::VoteWeightProvider::set_vote_weight_of(&heavier, bag_thresh);

assert_eq!(
List::<T>::iter().map(|n| n.id().clone()).collect::<Vec<_>>(),
vec![lighter.clone(), heavier_prev.clone(), heavier.clone(), heavier_next.clone()]
);

whitelist_account!(heavier);
}: _(SystemOrigin::Signed(heavier.clone()), lighter.clone())
verify {
assert_eq!(
List::<T>::iter().map(|n| n.id().clone()).collect::<Vec<_>>(),
vec![heavier, lighter, heavier_prev, heavier_next]
)
}
}
25 changes: 25 additions & 0 deletions frame/bags-list/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,17 @@ pub mod pallet {
Rebagged { who: T::AccountId, from: VoteWeight, to: VoteWeight },
}

#[pallet::error]
#[cfg_attr(test, derive(PartialEq))]
pub enum Error<T> {
/// Attempted to place node in front of a node in another bag.
NotInSameBag,
/// Id not found in list.
IdNotFound,
/// An Id does not have a greater vote weight than another Id.
NotHeavier,
}

#[pallet::call]
impl<T: Config> Pallet<T> {
/// Declare that some `dislocated` account has, through rewards or penalties, sufficiently
Expand All @@ -190,6 +201,20 @@ pub mod pallet {
let _ = Pallet::<T>::do_rebag(&dislocated, current_weight);
Ok(())
}

/// Move the caller's Id directly in front of `lighter`.
///
/// The dispatch origin for this call must be _Signed_ and can only be called by the Id of
/// the account going in front of `lighter`.
///
/// Only works if
emostov marked this conversation as resolved.
Show resolved Hide resolved
/// - both nodes are within the same bag,
/// - and `origin` has a greater `VoteWeight` than `lighter`.
#[pallet::weight(T::WeightInfo::put_in_front_of())]
pub fn put_in_front_of(origin: OriginFor<T>, lighter: T::AccountId) -> DispatchResult {
let heavier = ensure_signed(origin)?;
List::<T>::put_in_front_of(&lighter, &heavier).map_err(Into::into)
}
}

#[pallet::hooks]
Expand Down
77 changes: 77 additions & 0 deletions frame/bags-list/src/list/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,83 @@ impl<T: Config> List<T> {
})
}

/// Put `heavier_id` to the position directly in front of `lighter_id`. Both ids must be in the
/// same bag and the `weight_of` `lighter_id` must be less than that of `heavier_id`.
pub(crate) fn put_in_front_of(
lighter_id: &T::AccountId,
heavier_id: &T::AccountId,
) -> Result<(), crate::pallet::Error<T>> {
use crate::pallet;
use frame_support::ensure;

let lighter_node = Node::<T>::get(&lighter_id).ok_or(pallet::Error::IdNotFound)?;
let heavier_node = Node::<T>::get(&heavier_id).ok_or(pallet::Error::IdNotFound)?;

ensure!(lighter_node.bag_upper == heavier_node.bag_upper, pallet::Error::NotInSameBag);

// this is the most expensive check, so we do it last.
ensure!(
T::VoteWeightProvider::vote_weight(&heavier_id) >
T::VoteWeightProvider::vote_weight(&lighter_id),
pallet::Error::NotHeavier
);

// remove the heavier node from this list. Note that this removes the node from storage and
// decrements the node counter.
Self::remove(&heavier_id);
emostov marked this conversation as resolved.
Show resolved Hide resolved

// re-fetch `lighter_node` from storage since it may have been updated when `heavier_node`
// was removed.
let lighter_node = Node::<T>::get(&lighter_id).ok_or(pallet::Error::IdNotFound)?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let lighter_node = Node::<T>::get(&lighter_id).ok_or(pallet::Error::IdNotFound)?;
let lighter_node = Node::<T>::get(&lighter_id).expect("....; qed");

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@gui1117 gui1117 Dec 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but it is still true that this cannot and mustn't fail no ? I don't see how your commit is resolving Kian suggestion.

Maybe we should have log and/or debug assert.

Copy link
Contributor Author

@emostov emostov Dec 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

944dc85 <-- adds in a log + debug assert in case we ever hit this case

// insert `heavier_node` directly in front of `lighter_node`. This will update both nodes
// in storage and update the node counter.
Self::insert_at_unchecked(lighter_node, heavier_node);

Ok(())
}

/// Insert `node` directly in front of `at`.
///
/// WARNINGS:
/// - this is a naive function in that it does not check if `node` belongs to the same bag as
/// `at`. It is expected that the call site will check preconditions.
/// - this will panic if `at.bag_upper` is not a bag that already exists in storage.
fn insert_at_unchecked(mut at: Node<T>, mut node: Node<T>) {
// connect `node` to its new `prev`.
node.prev = at.prev.clone();
if let Some(mut prev) = at.prev() {
prev.next = Some(node.id().clone());
prev.put()
}

// connect `node` and `at`.
node.next = Some(at.id().clone());
at.prev = Some(node.id().clone());

if at.is_terminal() || node.is_terminal() {
// `at` is the tail and/or `node` is the head, so we make sure the bag is updated. Note,
// since `node` is always in front of `at` we know that 1) there is always at least 2
// nodes in the bag, and 2) only `node` could be the head and only `at` could be the
// tail.
let mut bag = Bag::<T>::get(at.bag_upper)
.expect("given nodes must always have a valid bag. qed.");

if node.prev == None {
bag.head = Some(node.id().clone())
}

if at.next == None {
bag.tail = Some(at.id().clone())
}

bag.put()
};
emostov marked this conversation as resolved.
Show resolved Hide resolved

// write the updated nodes to storage.
at.put();
node.put();
}

/// Sanity check the list.
///
/// This should be called from the call-site, whenever one of the mutating apis (e.g. `insert`)
Expand Down
117 changes: 117 additions & 0 deletions frame/bags-list/src/list/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,123 @@ mod list {
assert!(non_existent_ids.iter().all(|id| !List::<Runtime>::contains(id)));
})
}

#[test]
#[should_panic = "given nodes must always have a valid bag. qed."]
fn put_in_front_of_panics_if_bag_not_found() {
ExtBuilder::default().skip_genesis_ids().build_and_execute_no_post_check(|| {
let node_10_no_bag = Node::<Runtime> { id: 10, prev: None, next: None, bag_upper: 15 };
let node_11_no_bag = Node::<Runtime> { id: 11, prev: None, next: None, bag_upper: 15 };

// given
ListNodes::<Runtime>::insert(10, node_10_no_bag);
ListNodes::<Runtime>::insert(11, node_11_no_bag);
StakingMock::set_vote_weight_of(&10, 14);
StakingMock::set_vote_weight_of(&11, 15);
assert!(!ListBags::<Runtime>::contains_key(15));
assert_eq!(List::<Runtime>::get_bags(), vec![]);

// then .. this panics
let _ = List::<Runtime>::put_in_front_of(&10, &11);
});
}

#[test]
fn insert_at_unchecked_at_is_only_node() {
// Note that this `insert_at_unchecked` test should fail post checks because node 42 does
// not get re-assigned the correct bagu pper. This is because `insert_at_unchecked` assumes
// both nodes are already in the same bag with the correct bag upper.
ExtBuilder::default().build_and_execute_no_post_check(|| {
// given
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]);

// implicitly also test that `node`'s `prev`/`next` are correctly re-assigned.
let node_42 =
Node::<Runtime> { id: 42, prev: Some(1), next: Some(2), bag_upper: 1_000 };
assert!(!crate::ListNodes::<Runtime>::contains_key(42));

let node_1 = crate::ListNodes::<Runtime>::get(&1).unwrap();

// when
List::<Runtime>::insert_at_unchecked(node_1, node_42);

// then
assert_eq!(
List::<Runtime>::get_bags(),
vec![(10, vec![42, 1]), (1_000, vec![2, 3, 4])]
);
})
}

#[test]
fn insert_at_unchecked_at_is_head() {
ExtBuilder::default().build_and_execute(|| {
// given
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]);

// implicitly also test that `node`'s `prev`/`next` are correctly re-assigned.
let node_42 = Node::<Runtime> { id: 42, prev: Some(4), next: None, bag_upper: 1_000 };
assert!(!crate::ListNodes::<Runtime>::contains_key(42));

let node_2 = crate::ListNodes::<Runtime>::get(&2).unwrap();

// when
List::<Runtime>::insert_at_unchecked(node_2, node_42);

// then
assert_eq!(
List::<Runtime>::get_bags(),
vec![(10, vec![1]), (1_000, vec![42, 2, 3, 4])]
);
})
}

#[test]
fn insert_at_unchecked_at_is_non_terminal() {
ExtBuilder::default().build_and_execute(|| {
// given
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]);

// implicitly also test that `node`'s `prev`/`next` are correctly re-assigned.
let node_42 = Node::<Runtime> { id: 42, prev: None, next: Some(2), bag_upper: 1_000 };
assert!(!crate::ListNodes::<Runtime>::contains_key(42));

let node_3 = crate::ListNodes::<Runtime>::get(&3).unwrap();

// when
List::<Runtime>::insert_at_unchecked(node_3, node_42);

// then
assert_eq!(
List::<Runtime>::get_bags(),
vec![(10, vec![1]), (1_000, vec![2, 42, 3, 4])]
);
})
}

#[test]
fn insert_at_unchecked_at_is_tail() {
emostov marked this conversation as resolved.
Show resolved Hide resolved
ExtBuilder::default().build_and_execute(|| {
// given
assert_eq!(List::<Runtime>::get_bags(), vec![(10, vec![1]), (1_000, vec![2, 3, 4])]);

// implicitly also test that `node`'s `prev`/`next` are correctly re-assigned.
let node_42 =
Node::<Runtime> { id: 42, prev: Some(42), next: Some(42), bag_upper: 1_000 };
assert!(!crate::ListNodes::<Runtime>::contains_key(42));

let node_4 = crate::ListNodes::<Runtime>::get(&4).unwrap();

// when
List::<Runtime>::insert_at_unchecked(node_4, node_42);

// then
assert_eq!(
List::<Runtime>::get_bags(),
vec![(10, vec![1]), (1_000, vec![2, 3, 42, 4])]
);
})
}
}

mod bags {
Expand Down
33 changes: 23 additions & 10 deletions frame/bags-list/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,26 @@ use super::*;
use crate::{self as bags_list};
use frame_election_provider_support::VoteWeight;
use frame_support::parameter_types;
use std::collections::HashMap;

pub type AccountId = u32;
pub type Balance = u32;

parameter_types! {
// Set the vote weight for any id who's weight has _not_ been set with `set_vote_weight_of`.
pub static NextVoteWeight: VoteWeight = 0;
pub static NextVoteWeightMap: HashMap<AccountId, VoteWeight> = Default::default();
}

pub struct StakingMock;
impl frame_election_provider_support::VoteWeightProvider<AccountId> for StakingMock {
fn vote_weight(id: &AccountId) -> VoteWeight {
match id {
710 => 15,
711 => 16,
712 => 2_000, // special cases used for migrate test
_ => NextVoteWeight::get(),
}
*NextVoteWeightMap::get().get(id).unwrap_or(&NextVoteWeight::get())
}

#[cfg(any(feature = "runtime-benchmarks", test))]
fn set_vote_weight_of(_: &AccountId, weight: VoteWeight) {
// we don't really keep a mapping, just set weight for everyone.
NextVoteWeight::set(weight)
fn set_vote_weight_of(id: &AccountId, weight: VoteWeight) {
NEXT_VOTE_WEIGHT_MAP.with(|m| m.borrow_mut().insert(id.clone(), weight));
emostov marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down Expand Up @@ -103,9 +101,17 @@ pub(crate) const GENESIS_IDS: [(AccountId, VoteWeight); 4] =
#[derive(Default)]
pub struct ExtBuilder {
ids: Vec<(AccountId, VoteWeight)>,
skip_genesis_ids: bool,
}

impl ExtBuilder {
/// Skip adding the default genesis ids to the list.
#[cfg(test)]
pub(crate) fn skip_genesis_ids(mut self) -> Self {
self.skip_genesis_ids = true;
self
}

/// Add some AccountIds to insert into `List`.
#[cfg(test)]
pub(crate) fn add_ids(mut self, ids: Vec<(AccountId, VoteWeight)>) -> Self {
Expand All @@ -117,10 +123,17 @@ impl ExtBuilder {
sp_tracing::try_init_simple();
let storage = frame_system::GenesisConfig::default().build_storage::<Runtime>().unwrap();

let ids_with_weight: Vec<_> = if self.skip_genesis_ids {
self.ids.iter().collect()
} else {
GENESIS_IDS.iter().chain(self.ids.iter()).collect()
};

let mut ext = sp_io::TestExternalities::from(storage);
ext.execute_with(|| {
for (id, weight) in GENESIS_IDS.iter().chain(self.ids.iter()) {
for (id, weight) in ids_with_weight {
frame_support::assert_ok!(List::<Runtime>::insert(*id, *weight));
StakingMock::set_vote_weight_of(id, *weight);
}
});

Expand Down
Loading