referrerpolicy=no-referrer-when-downgrade

sp_npos_elections/
balancing.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//! Balancing algorithm implementation.
19//!
20//! Given a committee `A` and an edge weight vector `w`, a balanced solution is one that
21//!
22//! 1. it maximizes the sum of member supports, i.e `Argmax { sum(support(c)) }`. for all `c` in
23//! `A`.
24//! 2. it minimizes the sum of supports squared, i.e `Argmin { sum(support(c).pow(2)) }` for all `c`
25//! in `A`.
26//!
27//! See [`balance`] for more information.
28
29use crate::{BalancingConfig, Edge, ExtendedBalance, IdentifierT, Voter};
30use alloc::vec::Vec;
31use sp_arithmetic::traits::Zero;
32
33/// Balance the weight distribution of a given `voters` at most `iterations` times, or up until the
34/// point where the biggest difference created per iteration of all stakes is `tolerance`. If this
35/// is called with `tolerance = 0`, then exactly `iterations` rounds will be executed, except if no
36/// change has been made (`difference = 0`). `tolerance` and `iterations` are part of the
37/// [`BalancingConfig`] struct.
38///
39/// In almost all cases, a balanced solution will have a better score than an unbalanced solution,
40/// yet this is not 100% guaranteed because the first element of a [`crate::ElectionScore`] does not
41/// directly relate to balancing.
42///
43/// Note that some reference implementation adopt an approach in which voters are balanced randomly
44/// per round. To advocate determinism, we don't do this. In each round, all voters are exactly
45/// balanced once, in the same order.
46///
47/// Also, note that due to re-distribution of weights, the outcome of this function might contain
48/// edges with weight zero. The call site should filter such weight if desirable. Moreover, the
49/// outcome might need balance re-normalization, see `Voter::try_normalize`.
50///
51/// ### References
52///
53/// - [A new approach to the maximum flow problem](https://dl.acm.org/doi/10.1145/48014.61051).
54/// - [Validator election in nominated proof-of-stake](https://arxiv.org/abs/2004.12990) (Appendix
55///   A.)
56/// - [Computing a balanced solution](https://research.web3.foundation/en/latest/polkadot/NPoS/3.%20Balancing.html),
57///   which contains the details of the algorithm implementation.
58pub fn balance<AccountId: IdentifierT>(
59	voters: &mut Vec<Voter<AccountId>>,
60	config: &BalancingConfig,
61) -> usize {
62	if config.iterations == 0 {
63		return 0
64	}
65
66	let mut iter = 0;
67	loop {
68		let mut max_diff = 0;
69		for voter in voters.iter_mut() {
70			let diff = balance_voter(voter, config.tolerance);
71			if diff > max_diff {
72				max_diff = diff;
73			}
74		}
75
76		iter += 1;
77		if max_diff <= config.tolerance || iter >= config.iterations {
78			break iter
79		}
80	}
81}
82
83/// Internal implementation of balancing for one voter.
84pub(crate) fn balance_voter<AccountId: IdentifierT>(
85	voter: &mut Voter<AccountId>,
86	tolerance: ExtendedBalance,
87) -> ExtendedBalance {
88	// create a shallow copy of the elected ones. The original one will not be used henceforth.
89	let mut elected_edges = voter
90		.edges
91		.iter_mut()
92		.filter(|e| e.candidate.borrow().elected)
93		.collect::<Vec<&mut Edge<AccountId>>>();
94
95	// Either empty, or a self vote. Not much to do in either case.
96	if elected_edges.len() <= 1 {
97		return Zero::zero()
98	}
99
100	// amount of stake from this voter that is used in edges.
101	let stake_used =
102		elected_edges.iter().fold(0, |a: ExtendedBalance, e| a.saturating_add(e.weight));
103
104	// backed stake of each of the elected edges.
105	let backed_stakes = elected_edges
106		.iter()
107		.map(|e| e.candidate.borrow().backed_stake)
108		.collect::<Vec<_>>();
109
110	// backed stake of all the edges for whom we've spent some stake.
111	let backing_backed_stake = elected_edges
112		.iter()
113		.filter_map(|e| if e.weight > 0 { Some(e.candidate.borrow().backed_stake) } else { None })
114		.collect::<Vec<_>>();
115
116	let difference = if backing_backed_stake.len() > 0 {
117		let max_stake = backing_backed_stake
118			.iter()
119			.max()
120			.expect("vector with positive length will have a max; qed");
121		let min_stake = backed_stakes
122			.iter()
123			.min()
124			.expect("iterator with positive length will have a min; qed");
125		let mut difference = max_stake.saturating_sub(*min_stake);
126		difference = difference.saturating_add(voter.budget.saturating_sub(stake_used));
127		if difference < tolerance {
128			return difference
129		}
130		difference
131	} else {
132		voter.budget
133	};
134
135	// remove all backings.
136	for edge in elected_edges.iter_mut() {
137		let mut candidate = edge.candidate.borrow_mut();
138		candidate.backed_stake = candidate.backed_stake.saturating_sub(edge.weight);
139		edge.weight = 0;
140	}
141
142	elected_edges.sort_by_key(|e| e.candidate.borrow().backed_stake);
143
144	let mut cumulative_backed_stake = Zero::zero();
145	let mut last_index = elected_edges.len() - 1;
146
147	for (index, edge) in elected_edges.iter().enumerate() {
148		let index = index as ExtendedBalance;
149		let backed_stake = edge.candidate.borrow().backed_stake;
150		let temp = backed_stake.saturating_mul(index);
151		if temp.saturating_sub(cumulative_backed_stake) > voter.budget {
152			// defensive only. length of elected_edges is checked to be above 1.
153			last_index = index.saturating_sub(1) as usize;
154			break
155		}
156		cumulative_backed_stake = cumulative_backed_stake.saturating_add(backed_stake);
157	}
158
159	let last_stake = elected_edges
160		.get(last_index)
161		.expect(
162			"length of elected_edges is greater than or equal 2; last_index index is at the \
163 			 minimum elected_edges.len() - 1; index is within range; qed",
164		)
165		.candidate
166		.borrow()
167		.backed_stake;
168	let ways_to_split = last_index + 1;
169	let excess = voter
170		.budget
171		.saturating_add(cumulative_backed_stake)
172		.saturating_sub(last_stake.saturating_mul(ways_to_split as ExtendedBalance));
173
174	// Do the final update.
175	for edge in elected_edges.into_iter().take(ways_to_split) {
176		// first, do one scoped borrow to get the previous candidate stake.
177		let candidate_backed_stake = {
178			let candidate = edge.candidate.borrow();
179			candidate.backed_stake
180		};
181
182		let new_edge_weight = (excess / ways_to_split as ExtendedBalance)
183			.saturating_add(last_stake)
184			.saturating_sub(candidate_backed_stake);
185
186		// write the new edge weight
187		edge.weight = new_edge_weight;
188
189		// write the new candidate stake
190		let mut candidate = edge.candidate.borrow_mut();
191		candidate.backed_stake = candidate.backed_stake.saturating_add(new_edge_weight);
192	}
193
194	// excess / ways_to_split can cause a small un-normalized voters to be created.
195	// We won't `expect` here because even a result which is not normalized is not corrupt;
196	let _ = voter.try_normalize_elected();
197
198	difference
199}