referrerpolicy=no-referrer-when-downgrade

pallet_bags_list/list/
mod.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Implementation of a "bags list": a semi-sorted list where ordering granularity is dictated by
19//! configurable thresholds that delineate the boundaries of bags. It uses a pattern of composite
20//! data structures, where multiple storage items are masked by one outer API. See
21//! [`crate::ListNodes`], [`crate::ListBags`] for more information.
22//!
23//! The outer API of this module is the [`List`] struct. It wraps all acceptable operations on top
24//! of the aggregate linked list. All operations with the bags list should happen through this
25//! interface.
26
27use crate::Config;
28use alloc::{
29	boxed::Box,
30	collections::{btree_map::BTreeMap, btree_set::BTreeSet},
31};
32use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
33use core::{iter, marker::PhantomData};
34use frame_election_provider_support::ScoreProvider;
35use frame_support::{
36	defensive, ensure,
37	traits::{Defensive, DefensiveOption, Get},
38	CloneNoBound, DefaultNoBound, EqNoBound, PalletError, PartialEqNoBound, RuntimeDebugNoBound,
39};
40use scale_info::TypeInfo;
41use sp_runtime::traits::{Bounded, Zero};
42
43#[cfg(any(
44	test,
45	feature = "try-runtime",
46	feature = "fuzz",
47	feature = "std",
48	feature = "runtime-benchmarks"
49))]
50use alloc::vec::Vec;
51#[cfg(any(test, feature = "try-runtime", feature = "fuzz"))]
52use sp_runtime::TryRuntimeError;
53
54#[derive(
55	Debug,
56	PartialEq,
57	Eq,
58	Encode,
59	Decode,
60	DecodeWithMemTracking,
61	MaxEncodedLen,
62	TypeInfo,
63	PalletError,
64)]
65pub enum ListError {
66	/// A duplicate id has been detected.
67	Duplicate,
68	/// An Id does not have a greater score than another Id.
69	NotHeavier,
70	/// Attempted to place node in front of a node in another bag.
71	NotInSameBag,
72	/// Given node id was not found.
73	NodeNotFound,
74	/// The List is locked, therefore updates cannot happen now.
75	Locked,
76}
77
78#[cfg(test)]
79mod tests;
80
81/// Given a certain score, to which bag does it belong to?
82///
83/// Bags are identified by their upper threshold; the value returned by this function is guaranteed
84/// to be a member of `T::BagThresholds`.
85///
86/// Note that even if the thresholds list does not have `T::Score::max_value()` as its final member,
87/// this function behaves as if it does.
88pub fn notional_bag_for<T: Config<I>, I: 'static>(score: T::Score) -> T::Score {
89	let thresholds = T::BagThresholds::get();
90	let idx = thresholds.partition_point(|&threshold| score > threshold);
91	thresholds.get(idx).copied().unwrap_or_else(T::Score::max_value)
92}
93
94/// The **ONLY** entry point of this module. All operations to the bags-list should happen through
95/// this interface. It is forbidden to access other module members directly.
96//
97// Data structure providing efficient mostly-accurate selection of the top N id by `Score`.
98//
99// It's implemented as a set of linked lists. Each linked list comprises a bag of ids of
100// arbitrary and unbounded length, all having a score within a particular constant range.
101// This structure means that ids can be added and removed in `O(1)` time.
102//
103// Iteration is accomplished by chaining the iteration of each bag, from greatest to least. While
104// the users within any particular bag are sorted in an entirely arbitrary order, the overall score
105// decreases as successive bags are reached. This means that it is valid to truncate
106// iteration at any desired point; only those ids in the lowest bag can be excluded. This
107// satisfies both the desire for fairness and the requirement for efficiency.
108pub struct List<T: Config<I>, I: 'static = ()>(PhantomData<(T, I)>);
109
110impl<T: Config<I>, I: 'static> List<T, I> {
111	/// Remove all data associated with the list from storage.
112	///
113	/// ## WARNING
114	///
115	/// this function should generally not be used in production as it could lead to a very large
116	/// number of storage accesses.
117	pub(crate) fn unsafe_clear() {
118		#[allow(deprecated)]
119		crate::ListBags::<T, I>::remove_all(None);
120		#[allow(deprecated)]
121		crate::ListNodes::<T, I>::remove_all();
122	}
123
124	/// Regenerate all of the data from the given ids.
125	///
126	/// WARNING: this is expensive and should only ever be performed when the list needs to be
127	/// generated from scratch. Care needs to be taken to ensure
128	///
129	/// This may or may not need to be called at genesis as well, based on the configuration of the
130	/// pallet using this `List`.
131	///
132	/// Returns the number of ids migrated.
133	pub fn unsafe_regenerate(
134		all: impl IntoIterator<Item = T::AccountId>,
135		score_of: Box<dyn Fn(&T::AccountId) -> Option<T::Score>>,
136	) -> u32 {
137		// NOTE: This call is unsafe for the same reason as SortedListProvider::unsafe_regenerate.
138		// I.e. because it can lead to many storage accesses.
139		// So it is ok to call it as caller must ensure the conditions.
140		Self::unsafe_clear();
141		Self::insert_many(all, score_of)
142	}
143
144	/// Migrate the list from one set of thresholds to another.
145	///
146	/// This should only be called as part of an intentional migration; it's fairly expensive.
147	///
148	/// Returns the number of accounts affected.
149	///
150	/// Preconditions:
151	///
152	/// - `old_thresholds` is the previous list of thresholds.
153	/// - All `bag_upper` currently in storage are members of `old_thresholds`.
154	/// - `T::BagThresholds` has already been updated and is the new set of thresholds.
155	///
156	/// Postconditions:
157	///
158	/// - All `bag_upper` currently in storage are members of `T::BagThresholds`.
159	/// - No id is changed unless required to by the difference between the old threshold list and
160	///   the new.
161	/// - ids whose bags change at all are implicitly rebagged into the appropriate bag in the new
162	///   threshold set.
163	#[allow(dead_code)]
164	pub fn migrate(old_thresholds: &[T::Score]) -> u32 {
165		let new_thresholds = T::BagThresholds::get();
166		if new_thresholds == old_thresholds {
167			return 0
168		}
169
170		// we can't check all preconditions, but we can check one
171		debug_assert!(
172			crate::ListBags::<T, I>::iter()
173				.all(|(threshold, _)| old_thresholds.contains(&threshold)),
174			"not all `bag_upper` currently in storage are members of `old_thresholds`",
175		);
176		debug_assert!(
177			crate::ListNodes::<T, I>::iter()
178				.all(|(_, node)| old_thresholds.contains(&node.bag_upper)),
179			"not all `node.bag_upper` currently in storage are members of `old_thresholds`",
180		);
181
182		let old_set: BTreeSet<_> = old_thresholds.iter().copied().collect();
183		let new_set: BTreeSet<_> = new_thresholds.iter().copied().collect();
184
185		// accounts that need to be rebagged
186		let mut affected_accounts = BTreeSet::new();
187		// track affected old bags to make sure we only iterate them once
188		let mut affected_old_bags = BTreeSet::new();
189
190		let new_bags = new_set.difference(&old_set).copied();
191		// a new bag means that all accounts previously using the old bag's threshold must now
192		// be rebagged
193		for inserted_bag in new_bags {
194			let affected_bag = {
195				// this recreates `notional_bag_for` logic, but with the old thresholds.
196				let idx = old_thresholds.partition_point(|&threshold| inserted_bag > threshold);
197				old_thresholds.get(idx).copied().unwrap_or_else(T::Score::max_value)
198			};
199			if !affected_old_bags.insert(affected_bag) {
200				// If the previous threshold list was [10, 20], and we insert [3, 5], then there's
201				// no point iterating through bag 10 twice.
202				continue
203			}
204
205			if let Some(bag) = Bag::<T, I>::get(affected_bag) {
206				affected_accounts.extend(bag.iter().map(|node| node.id));
207			}
208		}
209
210		let removed_bags = old_set.difference(&new_set).copied();
211		// a removed bag means that all members of that bag must be rebagged
212		for removed_bag in removed_bags.clone() {
213			if !affected_old_bags.insert(removed_bag) {
214				continue
215			}
216
217			if let Some(bag) = Bag::<T, I>::get(removed_bag) {
218				affected_accounts.extend(bag.iter().map(|node| node.id));
219			}
220		}
221
222		// migrate the voters whose bag has changed
223		let num_affected = affected_accounts.len() as u32;
224		let score_of = T::ScoreProvider::score;
225		let _removed = Self::remove_many(&affected_accounts);
226		debug_assert_eq!(_removed, num_affected);
227		let _inserted = Self::insert_many(affected_accounts.into_iter(), score_of);
228		debug_assert_eq!(_inserted, num_affected);
229
230		// we couldn't previously remove the old bags because both insertion and removal assume that
231		// it's always safe to add a bag if it's not present. Now that that's sorted, we can get rid
232		// of them.
233		//
234		// it's pretty cheap to iterate this again, because both sets are in-memory and require no
235		// lookups.
236		for removed_bag in removed_bags {
237			debug_assert!(
238				!crate::ListNodes::<T, I>::iter().any(|(_, node)| node.bag_upper == removed_bag),
239				"no id should be present in a removed bag",
240			);
241			crate::ListBags::<T, I>::remove(removed_bag);
242		}
243
244		num_affected
245	}
246
247	/// Returns `true` if the list contains `id`, otherwise returns `false`.
248	pub(crate) fn contains(id: &T::AccountId) -> bool {
249		crate::ListNodes::<T, I>::contains_key(id)
250	}
251
252	/// Get the score of the given node,
253	pub fn get_score(id: &T::AccountId) -> Result<T::Score, ListError> {
254		Node::<T, I>::get(id).map(|node| node.score()).ok_or(ListError::NodeNotFound)
255	}
256
257	/// Iterate over all nodes in all bags in the list.
258	///
259	/// Full iteration can be expensive; it's recommended to limit the number of items with
260	/// `.take(n)`, or call `.next()` one by one.
261	pub(crate) fn iter() -> impl Iterator<Item = Node<T, I>> {
262		// We need a touch of special handling here: because we permit `T::BagThresholds` to
263		// omit the final bound, we need to ensure that we explicitly include that threshold in the
264		// list.
265		//
266		// It's important to retain the ability to omit the final bound because it makes tests much
267		// easier; they can just configure `type BagThresholds = ()`.
268		let thresholds = T::BagThresholds::get();
269		let iter = thresholds.iter().copied();
270		let iter: Box<dyn Iterator<Item = T::Score>> = if thresholds.last() ==
271			Some(&T::Score::max_value())
272		{
273			// in the event that they included it, we can just pass the iterator through unchanged.
274			Box::new(iter.rev())
275		} else {
276			// otherwise, insert it here.
277			Box::new(iter.chain(iter::once(T::Score::max_value())).rev())
278		};
279
280		iter.filter_map(Bag::get).flat_map(|bag| bag.iter())
281	}
282
283	/// Same as `iter`, but we start from a specific node.
284	///
285	/// All items after this node are returned, excluding `start` itself.
286	pub(crate) fn iter_from(
287		start: &T::AccountId,
288	) -> Result<impl Iterator<Item = Node<T, I>>, ListError> {
289		// We chain two iterators:
290		// 1. from the given `start` till the end of the bag
291		// 2. all the bags that come after `start`'s bag.
292
293		let start_node = Node::<T, I>::get(start).ok_or(ListError::NodeNotFound)?;
294		let start_node_upper = start_node.bag_upper;
295		let start_bag = core::iter::successors(start_node.next(), |prev| prev.next());
296
297		let thresholds = T::BagThresholds::get();
298		let idx = thresholds.partition_point(|&threshold| start_node_upper > threshold);
299		let leftover_bags = thresholds
300			.into_iter()
301			.take(idx)
302			.copied()
303			.rev()
304			.filter_map(Bag::get)
305			.flat_map(|bag| bag.iter());
306
307		crate::log!(
308			debug,
309			"starting to iterate from {:?}, who's bag is {:?}, and there are {:?} leftover bags",
310			&start,
311			start_node_upper,
312			idx
313		);
314		Ok(start_bag.chain(leftover_bags))
315	}
316
317	/// Insert several ids into the appropriate bags in the list. Continues with insertions
318	/// if duplicates are detected.
319	///
320	/// Returns the final count of number of ids inserted.
321	fn insert_many(
322		ids: impl IntoIterator<Item = T::AccountId>,
323		score_of: impl Fn(&T::AccountId) -> Option<T::Score>,
324	) -> u32 {
325		let mut count = 0;
326		ids.into_iter().for_each(|v| {
327			if let Some(score) = score_of(&v) {
328				if Self::insert(v, score).is_ok() {
329					count += 1;
330				}
331			} else {
332				// nada
333			}
334		});
335
336		count
337	}
338
339	/// Insert a new id into the appropriate bag in the list.
340	///
341	/// Returns an error if the list already contains `id`.
342	pub(crate) fn insert(id: T::AccountId, score: T::Score) -> Result<(), ListError> {
343		if Self::contains(&id) {
344			return Err(ListError::Duplicate)
345		}
346
347		let bag_score = notional_bag_for::<T, I>(score);
348		let mut bag = Bag::<T, I>::get_or_make(bag_score);
349		// unchecked insertion is okay; we just got the correct `notional_bag_for`.
350		bag.insert_unchecked(id.clone(), score);
351
352		// new inserts are always the tail, so we must write the bag.
353		bag.put();
354
355		crate::log!(
356			trace,
357			"inserted {:?} with score {:?} into bag {:?}, new count is {}",
358			id,
359			score,
360			bag_score,
361			crate::ListNodes::<T, I>::count(),
362		);
363
364		Ok(())
365	}
366
367	/// Remove an id from the list, returning an error if `id` does not exists.
368	pub(crate) fn remove(id: &T::AccountId) -> Result<(), ListError> {
369		if !Self::contains(id) {
370			return Err(ListError::NodeNotFound)
371		}
372		let _ = Self::remove_many(core::iter::once(id));
373		Ok(())
374	}
375
376	/// Remove many ids from the list.
377	///
378	/// This is more efficient than repeated calls to `Self::remove`.
379	///
380	/// Returns the final count of number of ids removed.
381	fn remove_many<'a>(ids: impl IntoIterator<Item = &'a T::AccountId>) -> u32 {
382		let mut bags = BTreeMap::new();
383		let mut count = 0;
384
385		for id in ids.into_iter() {
386			let node = match Node::<T, I>::get(id) {
387				Some(node) => node,
388				None => continue,
389			};
390			count += 1;
391
392			if !node.is_terminal() {
393				// this node is not a head or a tail and thus the bag does not need to be updated
394				node.excise()
395			} else {
396				// this node is a head or tail, so the bag needs to be updated
397				let bag = bags
398					.entry(node.bag_upper)
399					.or_insert_with(|| Bag::<T, I>::get_or_make(node.bag_upper));
400				// node.bag_upper must be correct, therefore this bag will contain this node.
401				bag.remove_node_unchecked(&node);
402			}
403
404			// now get rid of the node itself
405			node.remove_from_storage_unchecked()
406		}
407
408		for (_, bag) in bags {
409			bag.put();
410		}
411
412		count
413	}
414
415	/// Update a node's position in the list.
416	///
417	/// If the node was in the correct bag, no effect. If the node was in the incorrect bag, they
418	/// are moved into the correct bag.
419	///
420	/// Returns `Some((old_idx, new_idx))` if the node moved, otherwise `None`. In both cases, the
421	/// node's score is written to the `score` field. Thus, this is not a noop, even if `None`.
422	///
423	/// This operation is somewhat more efficient than simply calling [`self.remove`] followed by
424	/// [`self.insert`]. However, given large quantities of nodes to move, it may be more efficient
425	/// to call [`self.remove_many`] followed by [`self.insert_many`].
426	pub(crate) fn update_position_for(
427		mut node: Node<T, I>,
428		new_score: T::Score,
429	) -> Option<(T::Score, T::Score)> {
430		node.score = new_score;
431		if node.is_misplaced(new_score) {
432			let old_bag_upper = node.bag_upper;
433
434			if !node.is_terminal() {
435				// this node is not a head or a tail, so we can just cut it out of the list. update
436				// and put the prev and next of this node, we do `node.put` inside `insert_note`.
437				node.excise();
438			} else if let Some(mut bag) = Bag::<T, I>::get(node.bag_upper) {
439				// this is a head or tail, so the bag must be updated.
440				bag.remove_node_unchecked(&node);
441				bag.put();
442			} else {
443				frame_support::defensive!(
444					"Node did not have a bag; BagsList is in an inconsistent state"
445				);
446			}
447
448			// put the node into the appropriate new bag.
449			let new_bag_upper = notional_bag_for::<T, I>(new_score);
450			let mut bag = Bag::<T, I>::get_or_make(new_bag_upper);
451			// prev, next, and bag_upper of the node are updated inside `insert_node`, also
452			// `node.put` is in there.
453			bag.insert_node_unchecked(node);
454			bag.put();
455
456			Some((old_bag_upper, new_bag_upper))
457		} else {
458			// just write the new score.
459			node.put();
460			None
461		}
462	}
463
464	/// Put `heavier_id` to the position directly in front of `lighter_id`. Both ids must be in the
465	/// same bag and the `score_of` `lighter_id` must be less than that of `heavier_id`.
466	pub(crate) fn put_in_front_of(
467		lighter_id: &T::AccountId,
468		heavier_id: &T::AccountId,
469	) -> Result<(), ListError> {
470		let lighter_node = Node::<T, I>::get(&lighter_id).ok_or(ListError::NodeNotFound)?;
471		let heavier_node = Node::<T, I>::get(&heavier_id).ok_or(ListError::NodeNotFound)?;
472
473		ensure!(lighter_node.bag_upper == heavier_node.bag_upper, ListError::NotInSameBag);
474
475		// this is the most expensive check, so we do it last.
476		ensure!(
477			T::ScoreProvider::score(&heavier_id) > T::ScoreProvider::score(&lighter_id),
478			ListError::NotHeavier
479		);
480
481		// remove the heavier node from this list. Note that this removes the node from storage and
482		// decrements the node counter.
483		let _ =
484			Self::remove(&heavier_id).defensive_proof("both nodes have been checked to exist; qed");
485
486		// re-fetch `lighter_node` from storage since it may have been updated when `heavier_node`
487		// was removed.
488		let lighter_node =
489			Node::<T, I>::get(lighter_id).defensive_ok_or_else(|| ListError::NodeNotFound)?;
490
491		// insert `heavier_node` directly in front of `lighter_node`. This will update both nodes
492		// in storage and update the node counter.
493		Self::insert_at_unchecked(lighter_node, heavier_node);
494
495		Ok(())
496	}
497
498	/// Insert `node` directly in front of `at`.
499	///
500	/// WARNINGS:
501	/// - this is a naive function in that it does not check if `node` belongs to the same bag as
502	/// `at`. It is expected that the call site will check preconditions.
503	/// - this will panic if `at.bag_upper` is not a bag that already exists in storage.
504	fn insert_at_unchecked(mut at: Node<T, I>, mut node: Node<T, I>) {
505		// connect `node` to its new `prev`.
506		node.prev = at.prev.clone();
507		if let Some(mut prev) = at.prev() {
508			prev.next = Some(node.id().clone());
509			prev.put()
510		}
511
512		// connect `node` and `at`.
513		node.next = Some(at.id().clone());
514		at.prev = Some(node.id().clone());
515
516		if node.is_terminal() {
517			// `node` is the new head, so we make sure the bag is updated. Note,
518			// since `node` is always in front of `at` we know that 1) there is always at least 2
519			// nodes in the bag, and 2) only `node` could be the head and only `at` could be the
520			// tail.
521			let mut bag = Bag::<T, I>::get(at.bag_upper)
522				.expect("given nodes must always have a valid bag. qed.");
523
524			if node.prev == None {
525				bag.head = Some(node.id().clone())
526			}
527
528			bag.put()
529		};
530
531		// write the updated nodes to storage.
532		at.put();
533		node.put();
534	}
535
536	/// Check the internal state of the list.
537	///
538	/// This should be called from the call-site, whenever one of the mutating apis (e.g. `insert`)
539	/// is being used, after all other staking data (such as counter) has been updated. It checks:
540	///
541	/// * there are no duplicate ids,
542	/// * length of this list is in sync with `ListNodes::count()`,
543	/// * and sanity-checks all bags and nodes. This will cascade down all the checks and makes sure
544	/// all bags and nodes are checked per *any* update to `List`.
545	#[cfg(any(test, feature = "try-runtime", feature = "fuzz"))]
546	pub(crate) fn do_try_state() -> Result<(), TryRuntimeError> {
547		let mut seen_in_list = BTreeSet::new();
548		ensure!(
549			Self::iter().map(|node| node.id).all(|id| seen_in_list.insert(id)),
550			"duplicate identified"
551		);
552
553		let iter_count = Self::iter().count() as u32;
554		let stored_count = crate::ListNodes::<T, I>::count();
555		let nodes_count = crate::ListNodes::<T, I>::iter().count() as u32;
556		ensure!(iter_count == stored_count, "iter_count != stored_count");
557		ensure!(stored_count == nodes_count, "stored_count != nodes_count");
558
559		crate::log!(trace, "count of nodes: {}", stored_count);
560
561		let active_bags = {
562			let thresholds = T::BagThresholds::get().iter().copied();
563			let thresholds: Vec<T::Score> =
564				if thresholds.clone().last() == Some(T::Score::max_value()) {
565					// in the event that they included it, we don't need to make any changes
566					thresholds.collect()
567				} else {
568					// otherwise, insert it here.
569					thresholds.chain(iter::once(T::Score::max_value())).collect()
570				};
571			thresholds.into_iter().filter_map(|t| Bag::<T, I>::get(t))
572		};
573
574		// build map of bags and the corresponding nodes to avoid multiple lookups
575		let mut bags_map = BTreeMap::<T::Score, Vec<T::AccountId>>::new();
576
577		active_bags.clone().try_for_each(|b| {
578			bags_map.insert(
579				b.bag_upper,
580				b.iter().map(|n: Node<T, I>| n.id().clone()).collect::<Vec<_>>(),
581			);
582			b.do_try_state()
583		})?;
584
585		let nodes_in_bags_count =
586			active_bags.clone().fold(0u32, |acc, cur| acc + cur.iter().count() as u32);
587		ensure!(nodes_count == nodes_in_bags_count, "stored_count != nodes_in_bags_count");
588
589		crate::log!(trace, "count of active bags {}", active_bags.count());
590
591		// check that all nodes are sane. We check the `ListNodes` storage item directly in case we
592		// have some "stale" nodes that are not in a bag.
593		for (_id, node) in crate::ListNodes::<T, I>::iter() {
594			// check that the node is in the correct bag
595			let expected_bag = bags_map
596				.get(&node.bag_upper)
597				.ok_or("bag not found for the node in active bags")?;
598			frame_support::ensure!(expected_bag.contains(node.id()), "node not found in the bag");
599
600			// verify node state
601			node.do_try_state()?
602		}
603
604		Ok(())
605	}
606
607	/// Returns the nodes of all non-empty bags. For testing and benchmarks.
608	#[cfg(any(feature = "std", feature = "runtime-benchmarks"))]
609	#[allow(dead_code)]
610	pub(crate) fn get_bags() -> Vec<(T::Score, Vec<T::AccountId>)> {
611		use frame_support::traits::Get as _;
612
613		let thresholds = T::BagThresholds::get();
614		let iter = thresholds.iter().copied();
615		let iter: Box<dyn Iterator<Item = T::Score>> = if thresholds.last() ==
616			Some(&T::Score::max_value())
617		{
618			// in the event that they included it, we can just pass the iterator through unchanged.
619			Box::new(iter)
620		} else {
621			// otherwise, insert it here.
622			Box::new(iter.chain(core::iter::once(T::Score::max_value())))
623		};
624
625		iter.filter_map(|t| {
626			Bag::<T, I>::get(t)
627				.map(|bag| (t, bag.iter().map(|n| n.id().clone()).collect::<Vec<_>>()))
628		})
629		.collect::<Vec<_>>()
630	}
631}
632
633/// A Bag is a doubly-linked list of ids, where each id is mapped to a [`Node`].
634///
635/// Note that we maintain both head and tail pointers. While it would be possible to get away with
636/// maintaining only a head pointer and cons-ing elements onto the front of the list, it's more
637/// desirable to ensure that there is some element of first-come, first-serve to the list's
638/// iteration so that there's no incentive to churn ids positioning to improve the chances of
639/// appearing within the ids set.
640#[derive(
641	DefaultNoBound,
642	Encode,
643	Decode,
644	MaxEncodedLen,
645	TypeInfo,
646	RuntimeDebugNoBound,
647	CloneNoBound,
648	PartialEqNoBound,
649	EqNoBound,
650)]
651#[codec(mel_bound())]
652#[scale_info(skip_type_params(T, I))]
653pub struct Bag<T: Config<I>, I: 'static = ()> {
654	pub head: Option<T::AccountId>,
655	pub tail: Option<T::AccountId>,
656
657	#[codec(skip)]
658	pub bag_upper: T::Score,
659	#[codec(skip)]
660	pub _phantom: PhantomData<I>,
661}
662
663impl<T: Config<I>, I: 'static> Bag<T, I> {
664	#[cfg(test)]
665	pub(crate) fn new(
666		head: Option<T::AccountId>,
667		tail: Option<T::AccountId>,
668		bag_upper: T::Score,
669	) -> Self {
670		Self { head, tail, bag_upper, _phantom: PhantomData }
671	}
672
673	/// Get a bag by its upper score.
674	pub(crate) fn get(bag_upper: T::Score) -> Option<Bag<T, I>> {
675		crate::ListBags::<T, I>::try_get(bag_upper).ok().map(|mut bag| {
676			bag.bag_upper = bag_upper;
677			bag
678		})
679	}
680
681	/// Get a bag by its upper score or make it, appropriately initialized. Does not check if
682	/// if `bag_upper` is a valid threshold.
683	fn get_or_make(bag_upper: T::Score) -> Bag<T, I> {
684		Self::get(bag_upper).unwrap_or(Bag { bag_upper, ..Default::default() })
685	}
686
687	/// `True` if self is empty.
688	fn is_empty(&self) -> bool {
689		self.head.is_none() && self.tail.is_none()
690	}
691
692	/// Put the bag back into storage.
693	fn put(self) {
694		if self.is_empty() {
695			crate::ListBags::<T, I>::remove(self.bag_upper);
696		} else {
697			crate::ListBags::<T, I>::insert(self.bag_upper, self);
698		}
699	}
700
701	/// Get the head node in this bag.
702	fn head(&self) -> Option<Node<T, I>> {
703		self.head.as_ref().and_then(|id| Node::get(id))
704	}
705
706	/// Get the tail node in this bag.
707	fn tail(&self) -> Option<Node<T, I>> {
708		self.tail.as_ref().and_then(|id| Node::get(id))
709	}
710
711	/// Iterate over the nodes in this bag.
712	pub(crate) fn iter(&self) -> impl Iterator<Item = Node<T, I>> {
713		core::iter::successors(self.head(), |prev| prev.next())
714	}
715
716	/// Insert a new id into this bag.
717	///
718	/// This is private on purpose because it's naive: it doesn't check whether this is the
719	/// appropriate bag for this id at all. Generally, use [`List::insert`] instead.
720	///
721	/// Storage note: this modifies storage, but only for the nodes. You still need to call
722	/// `self.put()` after use.
723	fn insert_unchecked(&mut self, id: T::AccountId, score: T::Score) {
724		// insert_node will overwrite `prev`, `next` and `bag_upper` to the proper values. As long
725		// as this bag is the correct one, we're good. All calls to this must come after getting the
726		// correct [`notional_bag_for`].
727		self.insert_node_unchecked(Node::<T, I> {
728			id,
729			prev: None,
730			next: None,
731			bag_upper: Zero::zero(),
732			score,
733			_phantom: PhantomData,
734		});
735	}
736
737	/// Insert a node into this bag.
738	///
739	/// This is private on purpose because it's naive; it doesn't check whether this is the
740	/// appropriate bag for this node at all. Generally, use [`List::insert`] instead.
741	///
742	/// Storage note: this modifies storage, but only for the node. You still need to call
743	/// `self.put()` after use.
744	fn insert_node_unchecked(&mut self, mut node: Node<T, I>) {
745		if let Some(tail) = &self.tail {
746			if *tail == node.id {
747				// this should never happen, but this check prevents one path to a worst case
748				// infinite loop.
749				defensive!("system logic error: inserting a node who has the id of tail");
750				return
751			};
752		}
753
754		// re-set the `bag_upper`. Regardless of whatever the node had previously, now it is going
755		// to be `self.bag_upper`.
756		node.bag_upper = self.bag_upper;
757
758		let id = node.id.clone();
759		// update this node now, treating it as the new tail.
760		node.prev = self.tail.clone();
761		node.next = None;
762		node.put();
763
764		// update the previous tail.
765		if let Some(mut old_tail) = self.tail() {
766			old_tail.next = Some(id.clone());
767			old_tail.put();
768		}
769		self.tail = Some(id.clone());
770
771		// ensure head exist. This is only set when the length of the bag is just 1, i.e. if this is
772		// the first insertion into the bag. In this case, both head and tail should point to the
773		// same node.
774		if self.head.is_none() {
775			self.head = Some(id);
776			debug_assert!(self.iter().count() == 1);
777		}
778	}
779
780	/// Remove a node from this bag.
781	///
782	/// This is private on purpose because it doesn't check whether this bag contains the node in
783	/// the first place. Generally, use [`List::remove`] instead, similar to `insert_unchecked`.
784	///
785	/// Storage note: this modifies storage, but only for adjacent nodes. You still need to call
786	/// `self.put()` and `ListNodes::remove(id)` to update storage for the bag and `node`.
787	fn remove_node_unchecked(&mut self, node: &Node<T, I>) {
788		// reassign neighboring nodes.
789		node.excise();
790
791		// clear the bag head/tail pointers as necessary.
792		if self.tail.as_ref() == Some(&node.id) {
793			self.tail = node.prev.clone();
794		}
795		if self.head.as_ref() == Some(&node.id) {
796			self.head = node.next.clone();
797		}
798	}
799
800	/// Check the internal state of the bag.
801	///
802	/// Should be called by the call-site, after any mutating operation on a bag. The call site of
803	/// this struct is always `List`.
804	///
805	/// * Ensures head has no prev.
806	/// * Ensures tail has no next.
807	/// * Ensures there are no loops, traversal from head to tail is correct.
808	#[cfg(any(test, feature = "try-runtime", feature = "fuzz"))]
809	fn do_try_state(&self) -> Result<(), TryRuntimeError> {
810		frame_support::ensure!(
811			self.head()
812				.map(|head| head.prev().is_none())
813				// if there is no head, then there must not be a tail, meaning that the bag is
814				// empty.
815				.unwrap_or_else(|| self.tail.is_none()),
816			"head has a prev"
817		);
818
819		frame_support::ensure!(
820			self.tail()
821				.map(|tail| tail.next().is_none())
822				// if there is no tail, then there must not be a head, meaning that the bag is
823				// empty.
824				.unwrap_or_else(|| self.head.is_none()),
825			"tail has a next"
826		);
827
828		let mut seen_in_bag = BTreeSet::new();
829		frame_support::ensure!(
830			self.iter()
831				.map(|node| node.id)
832				// each voter is only seen once, thus there is no cycle within a bag
833				.all(|voter| seen_in_bag.insert(voter)),
834			"duplicate found in bag"
835		);
836
837		Ok(())
838	}
839
840	/// Iterate over the nodes in this bag (public for tests).
841	#[cfg(feature = "std")]
842	#[allow(dead_code)]
843	pub fn std_iter(&self) -> impl Iterator<Item = Node<T, I>> {
844		core::iter::successors(self.head(), |prev| prev.next())
845	}
846}
847
848/// A Node is the fundamental element comprising the doubly-linked list described by `Bag`.
849#[derive(
850	Encode,
851	Decode,
852	MaxEncodedLen,
853	TypeInfo,
854	CloneNoBound,
855	PartialEqNoBound,
856	EqNoBound,
857	RuntimeDebugNoBound,
858)]
859#[codec(mel_bound())]
860#[scale_info(skip_type_params(T, I))]
861pub struct Node<T: Config<I>, I: 'static = ()> {
862	pub id: T::AccountId,
863	pub prev: Option<T::AccountId>,
864	pub next: Option<T::AccountId>,
865	pub bag_upper: T::Score,
866	pub score: T::Score,
867	#[codec(skip)]
868	pub _phantom: PhantomData<I>,
869}
870
871impl<T: Config<I>, I: 'static> Node<T, I> {
872	/// Get a node by id.
873	pub fn get(id: &T::AccountId) -> Option<Node<T, I>> {
874		crate::ListNodes::<T, I>::try_get(id).ok()
875	}
876
877	/// Put the node back into storage.
878	fn put(self) {
879		crate::ListNodes::<T, I>::insert(self.id.clone(), self);
880	}
881
882	/// Update neighboring nodes to point to reach other.
883	///
884	/// Only updates storage for adjacent nodes, but not `self`; so the user may need to call
885	/// `self.put`.
886	fn excise(&self) {
887		// Update previous node.
888		if let Some(mut prev) = self.prev() {
889			prev.next = self.next.clone();
890			prev.put();
891		}
892		// Update next self.
893		if let Some(mut next) = self.next() {
894			next.prev = self.prev.clone();
895			next.put();
896		}
897	}
898
899	/// This is a naive function that removes a node from the `ListNodes` storage item.
900	///
901	/// It is naive because it does not check if the node has first been removed from its bag.
902	fn remove_from_storage_unchecked(&self) {
903		crate::ListNodes::<T, I>::remove(&self.id)
904	}
905
906	/// Get the previous node in the bag.
907	fn prev(&self) -> Option<Node<T, I>> {
908		self.prev.as_ref().and_then(|id| Node::get(id))
909	}
910
911	/// Get the next node in the bag.
912	fn next(&self) -> Option<Node<T, I>> {
913		self.next.as_ref().and_then(|id| Node::get(id))
914	}
915
916	/// `true` when this voter is in the wrong bag.
917	pub fn is_misplaced(&self, current_score: T::Score) -> bool {
918		notional_bag_for::<T, I>(current_score) != self.bag_upper
919	}
920
921	/// `true` when this voter is a bag head or tail.
922	fn is_terminal(&self) -> bool {
923		self.prev.is_none() || self.next.is_none()
924	}
925
926	/// Get the underlying voter.
927	pub(crate) fn id(&self) -> &T::AccountId {
928		&self.id
929	}
930
931	/// Get the current vote weight of the node.
932	pub(crate) fn score(&self) -> T::Score {
933		self.score
934	}
935
936	/// Get the underlying voter (public fo tests).
937	#[cfg(feature = "std")]
938	#[allow(dead_code)]
939	pub fn std_id(&self) -> &T::AccountId {
940		&self.id
941	}
942
943	#[cfg(any(feature = "runtime-benchmarks", feature = "fuzz", test))]
944	pub fn set_score(&mut self, s: T::Score) {
945		self.score = s
946	}
947
948	/// The bag this nodes belongs to (public for benchmarks).
949	#[cfg(feature = "runtime-benchmarks")]
950	#[allow(dead_code)]
951	pub fn bag_upper(&self) -> T::Score {
952		self.bag_upper
953	}
954
955	#[cfg(any(test, feature = "try-runtime", feature = "fuzz"))]
956	fn do_try_state(&self) -> Result<(), TryRuntimeError> {
957		let expected_bag = Bag::<T, I>::get(self.bag_upper).ok_or("bag not found for node")?;
958		let id = self.id();
959
960		let non_terminal_check = !self.is_terminal() &&
961			expected_bag.head.as_ref() != Some(id) &&
962			expected_bag.tail.as_ref() != Some(id);
963		let terminal_check =
964			expected_bag.head.as_ref() == Some(id) || expected_bag.tail.as_ref() == Some(id);
965		frame_support::ensure!(
966			non_terminal_check || terminal_check,
967			"a terminal node is neither its bag head or tail"
968		);
969
970		Ok(())
971	}
972}