Skip to content
This repository has been archived by the owner on Feb 21, 2024. It is now read-only.

Commit

Permalink
make BagsList::put_in_front_of be permissionless (paritytech#14714)
Browse files Browse the repository at this point in the history
* make BagsList::put_in_fron_of be permissionless

* Update frame/bags-list/src/lib.rs

Co-authored-by: Liam Aharon <liam.aharon@hotmail.com>

* improve docs as well

* update lock

* Update frame/bags-list/Cargo.toml

Co-authored-by: Sam Johnson <sam@durosoft.com>

* fix

* Update frame/bags-list/src/lib.rs

Co-authored-by: Michael Assaf <94772640+snowmead@users.noreply.github.com>

---------

Co-authored-by: Liam Aharon <liam.aharon@hotmail.com>
Co-authored-by: Sam Johnson <sam@durosoft.com>
Co-authored-by: Michael Assaf <94772640+snowmead@users.noreply.github.com>
  • Loading branch information
4 people authored Aug 12, 2023
1 parent 28b1f79 commit 48d4313
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 15 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions frame/bags-list/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ frame-election-provider-support = { version = "4.0.0-dev", default-features = fa

# third party
log = { version = "0.4.17", default-features = false }
docify = "0.2.1"
aquamarine = { version = "0.3.2" }

# Optional imports for benchmarking
frame-benchmarking = { version = "4.0.0-dev", path = "../benchmarking", optional = true, default-features = false }
Expand Down
117 changes: 103 additions & 14 deletions frame/bags-list/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,60 @@
// See the License for the specific language governing permissions and
// limitations under the License.

//! # Bags-List Pallet
//! > Made with *Substrate*, for *Polkadot*.
//!
//! A semi-sorted list, where items hold an `AccountId` based on some `Score`. The
//! `AccountId` (`id` for short) might be synonym to a `voter` or `nominator` in some context, and
//! `Score` signifies the chance of each id being included in the final
//! [`SortedListProvider::iter`].
//! [![github]](https://github.com/paritytech/substrate/frame/fast-unstake) -
//! [![polkadot]](https://polkadot.network)
//!
//! It implements [`frame_election_provider_support::SortedListProvider`] to provide a semi-sorted
//! list of accounts to another pallet. It needs some other pallet to give it some information about
//! the weights of accounts via [`frame_election_provider_support::ScoreProvider`].
//! [polkadot]:
//! https://img.shields.io/badge/polkadot-E6007A?style=for-the-badge&logo=polkadot&logoColor=white
//! [github]:
//! https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github
//!
//! This pallet is not configurable at genesis. Whoever uses it should call appropriate functions of
//! the `SortedListProvider` (e.g. `on_insert`, or `unsafe_regenerate`) at their genesis.
//! # Bags-List Pallet
//!
//! # Goals
//! An onchain implementation of a semi-sorted linked list, with permissionless sorting and update
//! operations.
//!
//! ## Pallet API
//!
//! See the [`pallet`] module for more information about the interfaces this pallet exposes,
//! including its configuration trait, dispatchables, storage items, events and errors.
//!
//! This pallet provides an implementation of
//! [`frame_election_provider_support::SortedListProvider`] and it can typically be used by another
//! pallet via this API.
//!
//! ## Overview
//!
//! This pallet splits `AccountId`s into different bags. Within a bag, these `AccountId`s are stored
//! as nodes in a linked-list manner. This pallet then provides iteration over all bags, which
//! basically allows an infinitely large list of items to be kept in a sorted manner.
//!
//! Each bags has a upper and lower range of scores, denoted by [`Config::BagThresholds`]. All nodes
//! within a bag must be within the range of the bag. If not, the permissionless [`Pallet::rebag`]
//! can be used to move any node to the right bag.
//!
//! Once a `rebag` happens, the order within a node is still not enforced. To move a node to the
//! optimal position in a bag, the [`Pallet::put_in_front_of`] or [`Pallet::put_in_front_of_other`]
//! can be used.
//!
//! Additional reading, about how this pallet is used in the context of Polkadot's staking system:
//! <https://polkadot.network/blog/staking-update-september-2021/#bags-list-in-depth>
//!
//! ## Examples
//!
//! See [`example`] for a diagram of `rebag` and `put_in_front_of` operations.
//!
//! ## Low Level / Implementation Details
//!
//! The data structure exposed by this pallet aims to be optimized for:
//!
//! - insertions and removals.
//! - iteration over the top* N items by score, where the precise ordering of items doesn't
//! particularly matter.
//!
//! # Details
//! ### Further Details
//!
//! - items are kept in bags, which are delineated by their range of score (See
//! [`Config::BagThresholds`]).
Expand All @@ -53,6 +84,44 @@

#![cfg_attr(not(feature = "std"), no_std)]

#[cfg(doc)]
#[cfg_attr(doc, aquamarine::aquamarine)]
///
/// In this example, assuming each node has an equal id and score (eg. node 21 has a score of 21),
/// the node 22 can be moved from bag 1 to bag 0 with the `rebag` operation.
///
/// Once the whole list is iterated, assuming the above above rebag happens, the order of iteration
/// would be: `25, 21, 22, 12, 22, 5, 7, 3`.
///
/// Moreover, in bag2, node 7 can be moved to the front of node 5 with the `put_in_front_of`, as it
/// has a higher score.
///
/// ```mermaid
/// graph LR
/// Bag0 --> Bag1 --> Bag2
///
/// subgraph Bag0[Bag 0: 21-30 DOT]
/// direction LR
/// 25 --> 21 --> 22X[22]
/// end
///
/// subgraph Bag1[Bag 1: 11-20 DOT]
/// direction LR
/// 12 --> 22
/// end
///
/// subgraph Bag2[Bag 2: 0-10 DOT]
/// direction LR
/// 5 --> 7 --> 3
/// end
///
/// style 22X stroke-dasharray: 5 5,opacity:50%
/// ```
///
/// The equivalent of this in code would be:
#[doc = docify::embed!("src/tests.rs", examples_work)]
pub mod example {}

use codec::FullCodec;
use frame_election_provider_support::{ScoreProvider, SortedListProvider};
use frame_system::ensure_signed;
Expand Down Expand Up @@ -240,9 +309,11 @@ pub mod pallet {
/// 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`.
/// the account going in front of `lighter`. Fee is payed by the origin under all
/// circumstances.
///
/// Only works if:
///
/// Only works if
/// - both nodes are within the same bag,
/// - and `origin` has a greater `Score` than `lighter`.
#[pallet::call_index(1)]
Expand All @@ -257,6 +328,24 @@ pub mod pallet {
.map_err::<Error<T, I>, _>(Into::into)
.map_err::<DispatchError, _>(Into::into)
}

/// Same as [`Pallet::put_in_front_of`], but it can be called by anyone.
///
/// Fee is paid by the origin under all circumstances.
#[pallet::call_index(2)]
#[pallet::weight(T::WeightInfo::put_in_front_of())]
pub fn put_in_front_of_other(
origin: OriginFor<T>,
heavier: AccountIdLookupOf<T>,
lighter: AccountIdLookupOf<T>,
) -> DispatchResult {
let _ = ensure_signed(origin)?;
let lighter = T::Lookup::lookup(lighter)?;
let heavier = T::Lookup::lookup(heavier)?;
List::<T, I>::put_in_front_of(&lighter, &heavier)
.map_err::<Error<T, I>, _>(Into::into)
.map_err::<DispatchError, _>(Into::into)
}
}

#[pallet::hooks]
Expand Down
77 changes: 76 additions & 1 deletion frame/bags-list/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,62 @@ use frame_election_provider_support::{SortedListProvider, VoteWeight};
use list::Bag;
use mock::{test_utils::*, *};

#[docify::export]
#[test]
fn examples_work() {
ExtBuilder::default()
.skip_genesis_ids()
// initially set the score of 11 for 22 to push it next to 12
.add_ids(vec![(25, 25), (21, 21), (12, 12), (22, 11), (5, 5), (7, 7), (3, 3)])
.build_and_execute(|| {
// initial bags
assert_eq!(
List::<Runtime>::get_bags(),
vec![
// bag 0 -> 10
(10, vec![5, 7, 3]),
// bag 10 -> 20
(20, vec![12, 22]),
// bag 20 -> 30
(30, vec![25, 21])
]
);

// set score of 22 to 22
StakingMock::set_score_of(&22, 22);

// now we rebag 22 to the first bag
assert_ok!(BagsList::rebag(RuntimeOrigin::signed(42), 22));

assert_eq!(
List::<Runtime>::get_bags(),
vec![
// bag 0 -> 10
(10, vec![5, 7, 3]),
// bag 10 -> 20
(20, vec![12]),
// bag 20 -> 30
(30, vec![25, 21, 22])
]
);

// now we put 7 at the front of bag 0
assert_ok!(BagsList::put_in_front_of(RuntimeOrigin::signed(7), 5));

assert_eq!(
List::<Runtime>::get_bags(),
vec![
// bag 0 -> 10
(10, vec![7, 5, 3]),
// bag 10 -> 20
(20, vec![12]),
// bag 20 -> 30
(30, vec![25, 21, 22])
]
);
})
}

mod pallet {
use super::*;

Expand Down Expand Up @@ -207,6 +263,25 @@ mod pallet {
})
}

#[test]
fn put_in_front_of_other_can_be_permissionless() {
ExtBuilder::default()
.skip_genesis_ids()
.add_ids(vec![(10, 15), (11, 16), (12, 19)])
.build_and_execute(|| {
// given
assert_eq!(List::<Runtime>::get_bags(), vec![(20, vec![10, 11, 12])]);
// 11 now has more weight than 10 and can be moved before it.
StakingMock::set_score_of(&11u32, 17);

// when
assert_ok!(BagsList::put_in_front_of_other(RuntimeOrigin::signed(42), 11u32, 10));

// then
assert_eq!(List::<Runtime>::get_bags(), vec![(20, vec![11, 10, 12])]);
});
}

#[test]
fn put_in_front_of_two_node_bag_heavier_is_tail() {
ExtBuilder::default()
Expand Down Expand Up @@ -368,7 +443,7 @@ mod pallet {
StakingMock::set_score_of(&4, 999);

// when
BagsList::put_in_front_of(RuntimeOrigin::signed(2), 4).unwrap();
assert_ok!(BagsList::put_in_front_of(RuntimeOrigin::signed(2), 4));

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

0 comments on commit 48d4313

Please sign in to comment.