Skip to main content

topsoil_core/system/
limits.rs

1// This file is part of Soil.
2
3// Copyright (C) Soil contributors.
4// Copyright (C) Parity Technologies (UK) Ltd.
5// SPDX-License-Identifier: Apache-2.0 OR GPL-3.0-or-later WITH Classpath-exception-2.0
6
7//! Block resource limits configuration structures.
8//!
9//! FRAME defines two resources that are limited within a block:
10//! - Weight (execution cost/time)
11//! - Length (block size)
12//!
13//! `topsoil_system` tracks consumption of each of these resources separately for each
14//! `DispatchClass`. This module contains configuration object for both resources,
15//! which should be passed to `topsoil_system` configuration when runtime is being set up.
16
17use scale_info::TypeInfo;
18use subsoil::runtime::{traits::Bounded, Perbill};
19use topsoil_core::{
20	dispatch::{DispatchClass, OneOrMany, PerDispatchClass},
21	weights::{constants, Weight},
22};
23
24/// Block length limit configuration.
25#[derive(Debug, Clone, codec::Encode, codec::Decode, TypeInfo)]
26pub struct BlockLength {
27	/// Maximal total length in bytes for each extrinsic class.
28	///
29	/// In the worst case, the total block length is going to be:
30	/// `MAX(max)`
31	pub max: PerDispatchClass<u32>,
32	/// Optional maximum header size in bytes.
33	///
34	/// It is still possible that a header goes above this limit, if the runtime deposits too many
35	/// digests in the header. However, it is assumed that the runtime restricts the access for
36	/// depositing digests in the header.
37	///
38	/// If None, defaults to 20% of max block length.
39	pub max_header_size: Option<u32>,
40}
41
42impl Default for BlockLength {
43	fn default() -> Self {
44		let mut max = PerDispatchClass::new(|_| 5 * 1024 * 1024);
45		*max.get_mut(DispatchClass::Normal) =
46			DEFAULT_NORMAL_RATIO * *max.get_mut(DispatchClass::Normal);
47
48		Self { max, max_header_size: None }
49	}
50}
51
52impl BlockLength {
53	/// Create new `BlockLength` with `max` for every class.
54	#[deprecated(
55		note = "Use `BlockLength::builder().max(value).build()` instead. Will be removed after July 2026."
56	)]
57	pub fn max(max: u32) -> Self {
58		Self { max: PerDispatchClass::new(|_| max), max_header_size: None }
59	}
60
61	/// Create new `BlockLength` with `max` for `Operational` & `Mandatory`
62	/// and `normal * max` for `Normal`.
63	#[deprecated(
64		note = "Use `BlockLength::builder().normal_ratio(value, ratio).build()` instead. Will be removed after July 2026."
65	)]
66	pub fn max_with_normal_ratio(max: u32, normal: Perbill) -> Self {
67		Self {
68			max: PerDispatchClass::new(|class| {
69				if class == DispatchClass::Normal {
70					normal * max
71				} else {
72					max
73				}
74			}),
75			max_header_size: None,
76		}
77	}
78
79	/// Returns a builder to build a [`BlockLength`].
80	pub fn builder() -> BlockLengthBuilder {
81		BlockLengthBuilder { length: Default::default() }
82	}
83
84	/// Returns the maximum header size.
85	pub fn max_header_size(&self) -> u32 {
86		self.max_header_size.unwrap_or_else(|| {
87			let max_block_len = *self
88				.max
89				.get(DispatchClass::Normal)
90				.max(self.max.get(DispatchClass::Operational))
91				.max(self.max.get(DispatchClass::Mandatory));
92			max_block_len / 5
93		})
94	}
95}
96
97/// Builder for [`BlockLength`].
98pub struct BlockLengthBuilder {
99	length: BlockLength,
100}
101
102impl BlockLengthBuilder {
103	/// Set max block length for all classes.
104	pub fn max_length(mut self, max: u32) -> Self {
105		self.length.max = PerDispatchClass::new(|_| max);
106		self
107	}
108
109	/// Modify max block length for the given [`DispatchClass`].
110	pub fn modify_max_length_for_class(
111		mut self,
112		class: DispatchClass,
113		modify: impl Fn(&mut u32),
114	) -> Self {
115		modify(self.length.max.get_mut(class));
116		self
117	}
118
119	/// Set the max header size.
120	pub fn max_header_size(mut self, size: u32) -> Self {
121		self.length.max_header_size = Some(size);
122		self
123	}
124
125	/// Build the final [`BlockLength`].
126	pub fn build(self) -> BlockLength {
127		self.length
128	}
129}
130
131#[derive(Default, Debug)]
132pub struct ValidationErrors {
133	pub has_errors: bool,
134	#[cfg(feature = "std")]
135	pub errors: Vec<String>,
136}
137
138macro_rules! error_assert {
139	($cond : expr, $err : expr, $format : expr $(, $params: expr )*$(,)*) => {
140		if !$cond {
141			$err.has_errors = true;
142			#[cfg(feature = "std")]
143			{ $err.errors.push(format!($format $(, &$params )*)); }
144		}
145	}
146}
147
148/// A result of validating `BlockWeights` correctness.
149pub type ValidationResult = Result<BlockWeights, ValidationErrors>;
150
151/// A ratio of `Normal` dispatch class within block, used as default value for
152/// `BlockWeight` and `BlockLength`. The `Default` impls are provided mostly for convenience
153/// to use in tests.
154const DEFAULT_NORMAL_RATIO: Perbill = Perbill::from_percent(75);
155
156/// `DispatchClass`-specific weight configuration.
157#[derive(Debug, Clone, codec::Encode, codec::Decode, TypeInfo)]
158pub struct WeightsPerClass {
159	/// Base weight of single extrinsic of given class.
160	pub base_extrinsic: Weight,
161	/// Maximal weight of single extrinsic. Should NOT include `base_extrinsic` cost.
162	///
163	/// `None` indicates that this class of extrinsics doesn't have a limit.
164	pub max_extrinsic: Option<Weight>,
165	/// Block maximal total weight for all extrinsics of given class.
166	///
167	/// `None` indicates that weight sum of this class of extrinsics is not
168	/// restricted. Use this value carefully, since it might produce heavily oversized
169	/// blocks.
170	///
171	/// In the worst case, the total weight consumed by the class is going to be:
172	/// `MAX(max_total) + MAX(reserved)`.
173	pub max_total: Option<Weight>,
174	/// Block reserved allowance for all extrinsics of a particular class.
175	///
176	/// Setting to `None` indicates that extrinsics of that class are allowed
177	/// to go over total block weight (but at most `max_total` for that class).
178	/// Setting to `Some(x)` guarantees that at least `x` weight of particular class
179	/// is processed in every block.
180	pub reserved: Option<Weight>,
181}
182
183/// Block weight limits & base values configuration.
184///
185/// This object is responsible for defining weight limits and base weight values tracked
186/// during extrinsic execution.
187///
188/// Each block starts with `base_block` weight being consumed right away. Next up the
189/// `on_initialize` pallet callbacks are invoked and their cost is added before any extrinsic
190/// is executed. This cost is tracked as `Mandatory` dispatch class.
191///
192/// ```text,ignore
193/// |   | `max_block`    |   |
194/// |   |                |   |
195/// |   |                |   |
196/// |   |                |   |
197/// |   |                |  #| `on_initialize`
198/// |  #| `base_block`   |  #|
199/// |NOM|                |NOM|
200///  ||\_ Mandatory
201///  |\__ Operational
202///  \___ Normal
203/// ```
204///
205/// The remaining capacity can be used to dispatch extrinsics. Note that each dispatch class
206/// is being tracked separately, but the sum can't exceed `max_block` (except for `reserved`).
207/// Below you can see a picture representing full block with 3 extrinsics (two `Operational` and
208/// one `Normal`). Each class has it's own limit `max_total`, but also the sum cannot exceed
209/// `max_block` value.
210///
211/// ```text,ignore
212///                          -- `Mandatory` limit (unlimited)
213/// | # |                 |   |
214/// | # | `Ext3`          | - - `Operational` limit
215/// |#  | `Ext2`          |-  - `Normal` limit
216/// | # | `Ext1`          | # |
217/// |  #| `on_initialize` | ##|
218/// |  #| `base_block`    |###|
219/// |NOM|                 |NOM|
220/// ```
221///
222/// It should be obvious now that it's possible for one class to reach it's limit (say `Normal`),
223/// while the block has still capacity to process more transactions (`max_block` not reached,
224/// `Operational` transactions can still go in). Setting `max_total` to `None` disables the
225/// per-class limit. This is generally highly recommended for `Mandatory` dispatch class, while it
226/// can be dangerous for `Normal` class and should only be done with extra care and consideration.
227///
228/// Often it's desirable for some class of transactions to be added to the block despite it being
229/// full. For instance one might want to prevent high-priority `Normal` transactions from pushing
230/// out lower-priority `Operational` transactions. In such cases you might add a `reserved` capacity
231/// for given class.
232///
233/// ```test,ignore
234///              _
235///   #           \
236///   #   `Ext8`   - `reserved`
237///   #          _/
238/// | # | `Ext7                 | - - `Operational` limit
239/// |#  | `Ext6`                |   |
240/// |#  | `Ext5`                |-# - `Normal` limit
241/// |#  | `Ext4`                |## |
242/// |  #| `on_initialize`       |###|
243/// |  #| `base_block`          |###|
244/// |NOM|                       |NOM|
245/// ```
246///
247/// In the above example, `Ext4-6` fill up the block almost up to `max_block`. `Ext7` would not fit
248/// if there wasn't the extra `reserved` space for `Operational` transactions. Note that `max_total`
249/// limit applies to `reserved` space as well (i.e. the sum of weights of `Ext7` & `Ext8` mustn't
250/// exceed it). Setting `reserved` to `None` allows the extrinsics to always get into the block up
251/// to their `max_total` limit. If `max_total` is set to `None` as well, all extrinsics witch
252/// dispatchables of given class will always end up in the block (recommended for `Mandatory`
253/// dispatch class).
254///
255/// As a consequence of `reserved` space, total consumed block weight might exceed `max_block`
256/// value, so this parameter should rather be thought of as "target block weight" than a hard limit.
257#[derive(Debug, Clone, codec::Encode, codec::Decode, TypeInfo)]
258pub struct BlockWeights {
259	/// Base weight of block execution.
260	pub base_block: Weight,
261	/// Maximal total weight consumed by all kinds of extrinsics (without `reserved` space).
262	pub max_block: Weight,
263	/// Weight limits for extrinsics of given dispatch class.
264	pub per_class: PerDispatchClass<WeightsPerClass>,
265}
266
267impl Default for BlockWeights {
268	fn default() -> Self {
269		Self::with_sensible_defaults(
270			Weight::from_parts(constants::WEIGHT_REF_TIME_PER_SECOND, u64::MAX),
271			DEFAULT_NORMAL_RATIO,
272		)
273	}
274}
275
276impl BlockWeights {
277	/// Get per-class weight settings.
278	pub fn get(&self, class: DispatchClass) -> &WeightsPerClass {
279		self.per_class.get(class)
280	}
281
282	/// Verifies correctness of this `BlockWeights` object.
283	pub fn validate(self) -> ValidationResult {
284		fn or_max(w: Option<Weight>) -> Weight {
285			w.unwrap_or_else(Weight::max_value)
286		}
287		let mut error = ValidationErrors::default();
288
289		for class in DispatchClass::all() {
290			let weights = self.per_class.get(*class);
291			let max_for_class = or_max(weights.max_total);
292			let base_for_class = weights.base_extrinsic;
293			let reserved = or_max(weights.reserved);
294			// Make sure that if total is set it's greater than base_block &&
295			// base_for_class
296			error_assert!(
297				(max_for_class.all_gt(self.base_block) && max_for_class.all_gt(base_for_class))
298				|| max_for_class == Weight::zero(),
299				&mut error,
300				"[{:?}] {:?} (total) has to be greater than {:?} (base block) & {:?} (base extrinsic)",
301				class, max_for_class, self.base_block, base_for_class,
302			);
303			// Max extrinsic can't be greater than max_for_class.
304			error_assert!(
305				weights
306					.max_extrinsic
307					.unwrap_or(Weight::zero())
308					.all_lte(max_for_class.saturating_sub(base_for_class)),
309				&mut error,
310				"[{:?}] {:?} (max_extrinsic) can't be greater than {:?} (max for class)",
311				class,
312				weights.max_extrinsic,
313				max_for_class.saturating_sub(base_for_class),
314			);
315			// Max extrinsic should not be 0
316			error_assert!(
317				weights.max_extrinsic.unwrap_or_else(Weight::max_value).all_gt(Weight::zero()),
318				&mut error,
319				"[{:?}] {:?} (max_extrinsic) must not be 0. Check base cost and average initialization cost.",
320				class, weights.max_extrinsic,
321			);
322			// Make sure that if reserved is set it's greater than base_for_class.
323			error_assert!(
324				reserved.all_gt(base_for_class) || reserved == Weight::zero(),
325				&mut error,
326				"[{:?}] {:?} (reserved) has to be greater than {:?} (base extrinsic) if set",
327				class,
328				reserved,
329				base_for_class,
330			);
331			// Make sure max block is greater than max_total if it's set.
332			error_assert!(
333				self.max_block.all_gte(weights.max_total.unwrap_or(Weight::zero())),
334				&mut error,
335				"[{:?}] {:?} (max block) has to be greater than {:?} (max for class)",
336				class,
337				self.max_block,
338				weights.max_total,
339			);
340			// Make sure we can fit at least one extrinsic.
341			error_assert!(
342				self.max_block.all_gt(base_for_class + self.base_block),
343				&mut error,
344				"[{:?}] {:?} (max block) must fit at least one extrinsic {:?} (base weight)",
345				class,
346				self.max_block,
347				base_for_class + self.base_block,
348			);
349		}
350
351		if error.has_errors {
352			Err(error)
353		} else {
354			Ok(self)
355		}
356	}
357
358	/// Create new weights definition, with both `Normal` and `Operational`
359	/// classes limited to given weight.
360	///
361	/// Note there is no reservation for `Operational` class, so this constructor
362	/// is not suitable for production deployments.
363	pub fn simple_max(block_weight: Weight) -> Self {
364		Self::builder()
365			.base_block(Weight::zero())
366			.for_class(DispatchClass::all(), |weights| {
367				weights.base_extrinsic = Weight::zero();
368			})
369			.for_class(DispatchClass::non_mandatory(), |weights| {
370				weights.max_total = block_weight.into();
371			})
372			.build()
373			.expect("We only specify max_total and leave base values as defaults; qed")
374	}
375
376	/// Create a sensible default weights system given only expected maximal block weight and the
377	/// ratio that `Normal` extrinsics should occupy.
378	///
379	/// Assumptions:
380	///  - Average block initialization is assumed to be `10%`.
381	///  - `Operational` transactions have reserved allowance (`1.0 - normal_ratio`)
382	pub fn with_sensible_defaults(expected_block_weight: Weight, normal_ratio: Perbill) -> Self {
383		let normal_weight = normal_ratio * expected_block_weight;
384		Self::builder()
385			.for_class(DispatchClass::Normal, |weights| {
386				weights.max_total = normal_weight.into();
387			})
388			.for_class(DispatchClass::Operational, |weights| {
389				weights.max_total = expected_block_weight.into();
390				weights.reserved = (expected_block_weight - normal_weight).into();
391			})
392			.avg_block_initialization(Perbill::from_percent(10))
393			.build()
394			.expect("Sensible defaults are tested to be valid; qed")
395	}
396
397	/// Start constructing new `BlockWeights` object.
398	///
399	/// By default all kinds except of `Mandatory` extrinsics are disallowed.
400	pub fn builder() -> BlockWeightsBuilder {
401		BlockWeightsBuilder {
402			weights: BlockWeights {
403				base_block: constants::BlockExecutionWeight::get(),
404				max_block: Weight::zero(),
405				per_class: PerDispatchClass::new(|class| {
406					let initial =
407						if class == DispatchClass::Mandatory { None } else { Some(Weight::zero()) };
408					WeightsPerClass {
409						base_extrinsic: constants::ExtrinsicBaseWeight::get(),
410						max_extrinsic: None,
411						max_total: initial,
412						reserved: initial,
413					}
414				}),
415			},
416			init_cost: None,
417		}
418	}
419}
420
421/// An opinionated builder for `Weights` object.
422pub struct BlockWeightsBuilder {
423	weights: BlockWeights,
424	init_cost: Option<Perbill>,
425}
426
427impl BlockWeightsBuilder {
428	/// Set base block weight.
429	pub fn base_block(mut self, base_block: Weight) -> Self {
430		self.weights.base_block = base_block;
431		self
432	}
433
434	/// Average block initialization weight cost.
435	///
436	/// This value is used to derive maximal allowed extrinsic weight for each
437	/// class, based on the allowance.
438	///
439	/// This is to make sure that extrinsics don't stay forever in the pool,
440	/// because they could seemingly fit the block (since they are below `max_block`),
441	/// but the cost of calling `on_initialize` always prevents them from being included.
442	pub fn avg_block_initialization(mut self, init_cost: Perbill) -> Self {
443		self.init_cost = Some(init_cost);
444		self
445	}
446
447	/// Set parameters for particular class.
448	///
449	/// Note: `None` values of `max_extrinsic` will be overwritten in `build` in case
450	/// `avg_block_initialization` rate is set to a non-zero value.
451	pub fn for_class(
452		mut self,
453		class: impl OneOrMany<DispatchClass>,
454		action: impl Fn(&mut WeightsPerClass),
455	) -> Self {
456		for class in class.into_iter() {
457			action(self.weights.per_class.get_mut(class));
458		}
459		self
460	}
461
462	/// Construct the `BlockWeights` object.
463	pub fn build(self) -> ValidationResult {
464		// compute max extrinsic size
465		let Self { mut weights, init_cost } = self;
466
467		// compute max block size.
468		for class in DispatchClass::all() {
469			weights.max_block = match weights.per_class.get(*class).max_total {
470				Some(max) => max.max(weights.max_block),
471				_ => weights.max_block,
472			};
473		}
474		// compute max size of single extrinsic
475		if let Some(init_weight) = init_cost.map(|rate| rate * weights.max_block) {
476			for class in DispatchClass::all() {
477				let per_class = weights.per_class.get_mut(*class);
478				if per_class.max_extrinsic.is_none() && init_cost.is_some() {
479					per_class.max_extrinsic = per_class
480						.max_total
481						.map(|x| x.saturating_sub(init_weight))
482						.map(|x| x.saturating_sub(per_class.base_extrinsic));
483				}
484			}
485		}
486
487		// Validate the result
488		weights.validate()
489	}
490
491	/// Construct the `BlockWeights` object or panic if it's invalid.
492	///
493	/// This is a convenience method to be called whenever you construct a runtime.
494	pub fn build_or_panic(self) -> BlockWeights {
495		self.build().expect(
496			"Builder finished with `build_or_panic`; The panic is expected if runtime weights are not correct"
497		)
498	}
499}
500
501#[cfg(test)]
502mod tests {
503	use super::*;
504
505	#[test]
506	fn default_weights_are_valid() {
507		BlockWeights::default().validate().unwrap();
508	}
509}