referrerpolicy=no-referrer-when-downgrade

generate_bags/
lib.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//! Support code to ease the process of generating bag thresholds.
19//!
20//! NOTE: this assumes the runtime implements either `pallet_staking::Config`
21//! or `pallet_staking_async::Config`, as it requires an implementation of
22//! the traits [`frame_support::traits::Currency`] and `CurrencyToVote`.
23//!
24//! The process of adding bags to a runtime requires only four steps.
25//!
26//! 1. Update the runtime definition.
27//!
28//!    ```ignore
29//!    parameter_types!{
30//!         pub const BagThresholds: &'static [u64] = &[];
31//!    }
32//!
33//!    impl pallet_bags_list::Config for Runtime {
34//!         // <snip>
35//!         type BagThresholds = BagThresholds;
36//!    }
37//!    ```
38//!
39//! 2. Write a little program to generate the definitions. This program exists only to hook together
40//! the runtime definitions with the various calculations here. Take a look at
41//! _utils/frame/generate_bags/node-runtime_ for an example.
42//!
43//! 3. Run that program:
44//!
45//!    ```sh,notrust
46//!    $ cargo run -p node-runtime-generate-bags -- --total-issuance 1234 --minimum-balance 1
47//! output.rs    ```
48//!
49//! 4. Update the runtime definition.
50//!
51//!    ```diff,notrust
52//!    + mod output;
53//!    - pub const BagThresholds: &'static [u64] = &[];
54//!    + pub const BagThresholds: &'static [u64] = &output::THRESHOLDS;
55//!    ```
56
57use frame_election_provider_support::VoteWeight;
58use frame_support::traits::Get;
59use std::{
60	io::Write,
61	path::{Path, PathBuf},
62};
63
64/// Compute the existential weight for the specified configuration.
65///
66/// Note that this value depends on the current issuance, a quantity known to change over time.
67/// This makes the project of computing a static value suitable for inclusion in a static,
68/// generated file _excitingly unstable_.
69fn existential_weight<T: pallet_staking::Config>(
70	total_issuance: u128,
71	minimum_balance: u128,
72) -> VoteWeight {
73	use sp_staking::currency_to_vote::CurrencyToVote;
74
75	T::CurrencyToVote::to_vote(
76		minimum_balance
77			.try_into()
78			.map_err(|_| "failed to convert minimum_balance to type Balance")
79			.unwrap(),
80		total_issuance
81			.try_into()
82			.map_err(|_| "failed to convert total_issuance to type Balance")
83			.unwrap(),
84	)
85}
86
87fn existential_weight_async<T: pallet_staking_async::Config>(
88	total_issuance: u128,
89	minimum_balance: u128,
90) -> VoteWeight {
91	use sp_staking::currency_to_vote::CurrencyToVote;
92
93	T::CurrencyToVote::to_vote(
94		minimum_balance
95			.try_into()
96			.map_err(|_| "failed to convert minimum_balance to type Balance")
97			.unwrap(),
98		total_issuance
99			.try_into()
100			.map_err(|_| "failed to convert total_issuance to type Balance")
101			.unwrap(),
102	)
103}
104
105/// Return the path to a header file used in this repository if is exists.
106///
107/// Just searches the git working directory root for files matching certain patterns; it's
108/// pretty naive.
109fn path_to_header_file() -> Option<PathBuf> {
110	let mut workdir: &Path = &std::env::current_dir().ok()?;
111	while !workdir.join(".git").exists() {
112		workdir = workdir.parent()?;
113	}
114
115	for file_name in &["HEADER-APACHE2", "HEADER-GPL3", "HEADER", "file_header.txt"] {
116		let path = workdir.join(file_name);
117		if path.exists() {
118			return Some(path);
119		}
120	}
121	None
122}
123
124/// Create an underscore formatter: a formatter which inserts `_` every 3 digits of a number.
125fn underscore_formatter() -> num_format::CustomFormat {
126	num_format::CustomFormat::builder()
127		.grouping(num_format::Grouping::Standard)
128		.separator("_")
129		.build()
130		.expect("format described here meets all constraints")
131}
132
133/// Compute the constant ratio for the thresholds.
134///
135/// This ratio ensures that each bag, with the possible exceptions of certain small ones and the
136/// final one, is a constant multiple of the previous, while fully occupying the `VoteWeight`
137/// space.
138pub fn constant_ratio(existential_weight: VoteWeight, n_bags: usize) -> f64 {
139	((VoteWeight::MAX as f64 / existential_weight as f64).ln() / ((n_bags - 1) as f64)).exp()
140}
141
142/// Compute the list of bag thresholds.
143///
144/// Returns a list of exactly `n_bags` elements, except in the case of overflow.
145/// The first element is always `existential_weight`.
146/// The last element is always `VoteWeight::MAX`.
147///
148/// All other elements are computed from the previous according to the formula
149/// `threshold[k + 1] = (threshold[k] * ratio).max(threshold[k] + 1);`
150pub fn thresholds(
151	existential_weight: VoteWeight,
152	constant_ratio: f64,
153	n_bags: usize,
154) -> Vec<VoteWeight> {
155	const WEIGHT_LIMIT: f64 = VoteWeight::MAX as f64;
156
157	let mut thresholds = Vec::with_capacity(n_bags);
158
159	if n_bags > 1 {
160		thresholds.push(existential_weight);
161	}
162
163	while n_bags > 0 && thresholds.len() < n_bags - 1 {
164		let last = thresholds.last().copied().unwrap_or(existential_weight);
165		let successor = (last as f64 * constant_ratio).round().max(last as f64 + 1.0);
166		if successor < WEIGHT_LIMIT {
167			thresholds.push(successor as VoteWeight);
168		} else {
169			eprintln!("unexpectedly exceeded weight limit; breaking threshold generation loop");
170			break;
171		}
172	}
173
174	thresholds.push(VoteWeight::MAX);
175
176	debug_assert_eq!(thresholds.len(), n_bags);
177	debug_assert!(n_bags == 0 || thresholds[0] == existential_weight);
178	debug_assert!(n_bags == 0 || thresholds[thresholds.len() - 1] == VoteWeight::MAX);
179
180	thresholds
181}
182
183/// Write a thresholds module to the path specified.
184///
185/// Parameters:
186/// - `n_bags` the number of bags to generate.
187/// - `output` the path to write to; should terminate with a Rust module name, i.e.
188///   `foo/bar/thresholds.rs`.
189/// - `total_issuance` the total amount of the currency in the network.
190/// - `minimum_balance` the minimum balance of the currency required for an account to exist (i.e.
191///   existential deposit).
192///
193/// This generated module contains, in order:
194///
195/// - The contents of the header file in this repository's root, if found.
196/// - Module documentation noting that this is autogenerated and when.
197/// - Some associated constants.
198/// - The constant array of thresholds.
199fn generate_thresholds_inner<T: frame_system::Config>(
200	n_bags: usize,
201	output: &Path,
202	total_issuance: u128,
203	minimum_balance: u128,
204	existential_weight: VoteWeight,
205) -> Result<(), std::io::Error> {
206	// ensure the file is accessible
207	if let Some(parent) = output.parent() {
208		if !parent.exists() {
209			std::fs::create_dir_all(parent)?;
210		}
211	}
212
213	// copy the header file
214	if let Some(header_path) = path_to_header_file() {
215		std::fs::copy(header_path, output)?;
216	}
217
218	// open an append buffer
219	let file = std::fs::OpenOptions::new().create(true).append(true).open(output)?;
220	let mut buf = std::io::BufWriter::new(file);
221
222	// create underscore formatter and format buffer
223	let mut num_buf = num_format::Buffer::new();
224	let format = underscore_formatter();
225
226	// module docs
227	let now = chrono::Utc::now();
228	writeln!(buf)?;
229	writeln!(buf, "//! Autogenerated bag thresholds.")?;
230	writeln!(buf, "//!")?;
231	writeln!(buf, "//! Generated on {}", now.to_rfc3339())?;
232	writeln!(buf, "//! Arguments")?;
233	writeln!(buf, "//! Total issuance: {}", &total_issuance)?;
234	writeln!(buf, "//! Minimum balance: {}", &minimum_balance)?;
235
236	writeln!(buf, "//! for the {} runtime.", T::Version::get().spec_name,)?;
237
238	num_buf.write_formatted(&existential_weight, &format);
239	writeln!(buf)?;
240	writeln!(buf, "/// Existential weight for this runtime.")?;
241	writeln!(buf, "#[cfg(any(test, feature = \"std\"))]")?;
242	writeln!(buf, "#[allow(unused)]")?;
243	writeln!(buf, "pub const EXISTENTIAL_WEIGHT: u64 = {};", num_buf.as_str())?;
244
245	// constant ratio
246	let constant_ratio = constant_ratio(existential_weight, n_bags);
247	writeln!(buf)?;
248	writeln!(buf, "/// Constant ratio between bags for this runtime.")?;
249	writeln!(buf, "#[cfg(any(test, feature = \"std\"))]")?;
250	writeln!(buf, "#[allow(unused)]")?;
251	writeln!(buf, "pub const CONSTANT_RATIO: f64 = {:.16};", constant_ratio)?;
252
253	// thresholds
254	let thresholds = thresholds(existential_weight, constant_ratio, n_bags);
255	writeln!(buf)?;
256	writeln!(buf, "/// Upper thresholds delimiting the bag list.")?;
257	writeln!(buf, "pub const THRESHOLDS: [u64; {}] = [", thresholds.len())?;
258	for threshold in &thresholds {
259		num_buf.write_formatted(threshold, &format);
260		// u64::MAX, with spacers every 3 digits, is 26 characters wide
261		writeln!(buf, "	{:>26},", num_buf.as_str())?;
262	}
263	writeln!(buf, "];")?;
264
265	// thresholds balance
266	writeln!(buf)?;
267	writeln!(buf, "/// Upper thresholds delimiting the bag list.")?;
268	writeln!(buf, "pub const THRESHOLDS_BALANCES: [u128; {}] = [", thresholds.len())?;
269	for threshold in thresholds {
270		num_buf.write_formatted(&threshold, &format);
271		// u64::MAX, with spacers every 3 digits, is 26 characters wide
272		writeln!(buf, "	{:>26},", num_buf.as_str())?;
273	}
274	writeln!(buf, "];")?;
275
276	Ok(())
277}
278
279/// Write a thresholds module for a runtime using `pallet_staking`.
280///
281/// See `generate_thresholds_inner` for parameter documentation.
282pub fn generate_thresholds<T: pallet_staking::Config>(
283	n_bags: usize,
284	output: &Path,
285	total_issuance: u128,
286	minimum_balance: u128,
287) -> Result<(), std::io::Error> {
288	let ew = existential_weight::<T>(total_issuance, minimum_balance);
289	generate_thresholds_inner::<T>(n_bags, output, total_issuance, minimum_balance, ew)
290}
291
292/// Write a thresholds module for a runtime using `pallet_staking_async`.
293///
294/// See `generate_thresholds_inner` for parameter documentation.
295pub fn generate_thresholds_async<T: pallet_staking_async::Config>(
296	n_bags: usize,
297	output: &Path,
298	total_issuance: u128,
299	minimum_balance: u128,
300) -> Result<(), std::io::Error> {
301	let ew = existential_weight_async::<T>(total_issuance, minimum_balance);
302	generate_thresholds_inner::<T>(n_bags, output, total_issuance, minimum_balance, ew)
303}