referrerpolicy=no-referrer-when-downgrade

cumulus_pallet_parachain_system/block_weight/
mod.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Cumulus.
3// SPDX-License-Identifier: Apache-2.0
4
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// 	http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17//! Provides functionality to dynamically calculate the block weight for a parachain.
18//!
19//! With block bundling, parachains are relatively free to choose whatever block interval they want.
20//! The block interval is the time between individual blocks. The available resources per block (max
21//! block weight) depend on the number of cores allocated to the parachain on the relay chain. Each
22//! relay chain cores provides an execution time of `2s` and a storage size of `10MiB`. Depending on
23//! the desired number of blocks to produce, the resources need to be divided between the individual
24//! blocks. With small blocks that do not have that many resources available, a problem may arises
25//! for bigger transactions not fitting into blocks anymore, e.g. a runtime upgrade. For these cases
26//! the weight of a block can be increased to use the weight of a full core. Only the first block of
27//! a core is allowed to increase its weight to use the full core weight. In the case of the first
28//! block using the full core weight, there will be no further block build on the same core. This is
29//! signaled to the node by setting the [`CumulusDigestItem::UseFullCore`] digest item.`
30//!
31//! The [`MaxParachainBlockWeight`] provides a [`Get`] implementation that will return the max block
32//! weight as determined by the [`DynamicMaxBlockWeight`] transaction extension.
33//!
34//! [`DynamicMaxBlockWeightHooks`] needs to be registered as a pre-inherent hook. It is used to
35//! handle the weight consumption of `on_initialize` and change the block weight mode based on the
36//! consumed weight.
37//!
38//! # Setup
39//!
40//! Setup the transaction extension:
41#![doc = docify::embed!("src/block_weight/mock.rs", tx_extension_setup)]
42//! Setting up `MaximumBlockWeight`:
43#![doc = docify::embed!("src/block_weight/mock.rs", max_block_weight_setup)]
44//! Registering of the `PreInherents` hook:
45#![doc = docify::embed!("src/block_weight/mock.rs", pre_inherents_setup)]
46//! # Weight per context
47//!
48//! Depending on the context, [`MaxParachainBlockWeight`] may return a different max weight. The
49//! max weight is only allowed to change in the first block of a core. Otherwise, all blocks need to
50//! follow the target block weight determined based on the number of cores and the target block
51//! rate. In the case of a first block, the following contexts may allow to access the full core
52//! weight:
53//!
54//! - `on_initialize`: All logic that runs in this context up to the execution of `inherents` will
55//!   get access to the full core weight.
56//! - `inherents`: Inherents also have access to the full core weight.
57//! - `on_poll`: Only gets access to the target block weight.
58//! - `transactions`: May get access to the full core weight, depends if they enable the access to
59//!   the full core weight based on the logic of [`DynamicMaxBlockWeight`].
60//! - `on_finalize`/`on_idle`: Only gets access to the target block weight.
61//!
62//! If any context that allows to use the full core weight, pushes the used block weight above the
63//! target block weight, all other contexts will get access to the full core weight.
64
65use crate::{Config, PreviousCoreCount};
66use codec::{Decode, Encode};
67use core::marker::PhantomData;
68use cumulus_primitives_core::CumulusDigestItem;
69use frame_support::{
70	weights::{constants::WEIGHT_REF_TIME_PER_SECOND, Weight},
71	CloneNoBound, DebugNoBound,
72};
73use frame_system::pallet_prelude::BlockNumberFor;
74use polkadot_primitives::{executor_params::DEFAULT_BACKING_EXECUTION_TIMEOUT, MAX_POV_SIZE};
75use scale_info::TypeInfo;
76use sp_core::Get;
77use sp_runtime::Digest;
78
79#[cfg(test)]
80mod mock;
81pub mod pre_inherents_hook;
82#[cfg(test)]
83mod tests;
84pub mod transaction_extension;
85
86pub use pre_inherents_hook::DynamicMaxBlockWeightHooks;
87pub use transaction_extension::DynamicMaxBlockWeight;
88
89const LOG_TARGET: &str = "runtime::parachain-system::block-weight";
90
91/// Maximum ref time per core
92const MAX_REF_TIME_PER_CORE_NS: u64 =
93	DEFAULT_BACKING_EXECUTION_TIMEOUT.as_secs() * WEIGHT_REF_TIME_PER_SECOND;
94
95/// The available weight per core on the relay chain.
96pub(crate) const FULL_CORE_WEIGHT: Weight =
97	Weight::from_parts(MAX_REF_TIME_PER_CORE_NS, MAX_POV_SIZE as u64);
98
99// Is set to `true` when we are currently inside of `pre_validate_extrinsic`.
100//
101// Forces `MaxParachainBlockWeight::get()` to return fractional weight, enabling detection of
102// transactions that exceed the fractional target limit.
103environmental::environmental!(inside_pre_validate: bool);
104
105/// The current block weight mode.
106///
107/// Based on this mode [`MaxParachainBlockWeight`] determines the current allowed block weight.
108#[derive(DebugNoBound, Encode, Decode, CloneNoBound, TypeInfo, PartialEq)]
109#[scale_info(skip_type_params(T))]
110pub enum BlockWeightMode<T: Config> {
111	/// The block is allowed to use the weight of a full core.
112	FullCore {
113		/// The block in which this mode was set. Is used to determine if this is maybe stale mode
114		/// setting, e.g. when running `validate_block`.
115		context: BlockNumberFor<T>,
116	},
117	/// The current active transaction is allowed to use the weight of a full core.
118	PotentialFullCore {
119		/// The block in which this mode was set. Is used to determine if this is maybe stale mode
120		/// setting, e.g. when running `validate_block`.
121		context: BlockNumberFor<T>,
122		/// The index of the first transaction.
123		///
124		/// Stays `None` for all inherents until there is the first transaction.
125		first_transaction_index: Option<u32>,
126		/// The target weight that was used to determine that the extrinsic is above this limit.
127		target_weight: Weight,
128	},
129	/// The block is only allowed to consume its fraction of the core.
130	///
131	/// How much each block is allowed to consume, depends on the target number of blocks and the
132	/// available cores on the relay chain.
133	FractionOfCore {
134		/// The block in which this mode was set. Is used to determine if this is maybe stale mode
135		/// setting, e.g. when running `validate_block`.
136		context: BlockNumberFor<T>,
137		/// The index of the first transaction.
138		///
139		/// Stays `None` for all inherents until there is the first transaction.
140		first_transaction_index: Option<u32>,
141	},
142}
143
144impl<T: Config> BlockWeightMode<T> {
145	/// Check if this mode is stale, aka was set in a previous block.
146	fn is_stale(&self) -> bool {
147		let context = self.context();
148
149		context < frame_system::Pallet::<T>::block_number()
150	}
151
152	/// Returns the context (block) in which this mode was set.
153	fn context(&self) -> BlockNumberFor<T> {
154		match self {
155			Self::FullCore { context } |
156			Self::PotentialFullCore { context, .. } |
157			Self::FractionOfCore { context, .. } => *context,
158		}
159	}
160
161	/// Create a new instance of `Self::FullCore`.
162	pub(crate) fn full_core() -> Self {
163		Self::FullCore { context: frame_system::Pallet::<T>::block_number() }
164	}
165
166	/// Create new instance of `Self::FractionOfCore`.
167	pub(crate) fn fraction_of_core(first_transaction_index: Option<u32>) -> Self {
168		Self::FractionOfCore {
169			context: frame_system::Pallet::<T>::block_number(),
170			first_transaction_index,
171		}
172	}
173
174	/// Create new instance of `Self::PotentialFullCore`.
175	pub(crate) fn potential_full_core(
176		first_transaction_index: Option<u32>,
177		target_weight: Weight,
178	) -> Self {
179		Self::PotentialFullCore {
180			context: frame_system::Pallet::<T>::block_number(),
181			first_transaction_index,
182			target_weight,
183		}
184	}
185}
186
187/// Calculates the maximum block weight for a parachain.
188///
189/// Based on the available cores and the number of desired blocks a block weight is calculated.
190///
191/// The max block weight is partly dynamic and controlled via the [`DynamicMaxBlockWeight`]
192/// transaction extension. The transaction extension is communicating the desired max block weight
193/// using the [`BlockWeightMode`].
194pub struct MaxParachainBlockWeight<Config, TargetBlockRate>(PhantomData<(Config, TargetBlockRate)>);
195
196impl<Config: crate::Config, TargetBlockRate: Get<u32>>
197	MaxParachainBlockWeight<Config, TargetBlockRate>
198{
199	/// Returns the target block weight for one block.
200	pub(crate) fn target_block_weight() -> Weight {
201		let digest = frame_system::Pallet::<Config>::digest();
202		Self::target_block_weight_with_digest(&digest)
203	}
204
205	/// Same as [`Self::target_block_weight`], but takes the `digests` directly.
206	fn target_block_weight_with_digest(digest: &Digest) -> Weight {
207		let number_of_cores = CumulusDigestItem::find_core_info(&digest).map_or_else(
208			|| PreviousCoreCount::<Config>::get().map_or(1, |pc| pc.0),
209			|ci| ci.number_of_cores.0,
210		) as u64;
211
212		let target_blocks = TargetBlockRate::get() as u64;
213
214		// Ensure we have at least one core and valid target blocks
215		if number_of_cores == 0 || target_blocks == 0 {
216			return FULL_CORE_WEIGHT;
217		}
218
219		let blocks_per_core = target_blocks.div_ceil(number_of_cores);
220
221		// At maximum we want to allow `6s` of ref time, because we don't want to overload nodes
222		// that are running with standard hardware. These nodes need to be able to import all the
223		// blocks in `6s`.
224		let ref_time_per_block = core::cmp::min(
225			MAX_REF_TIME_PER_CORE_NS / blocks_per_core, // Core allocation limit
226			(6 * WEIGHT_REF_TIME_PER_SECOND) / target_blocks, // Full node import limit
227		);
228
229		// PoV size we can use as much as we can get from the cores, but at maximum it is one block
230		// per core. Or in other words, one block can not span across multiple cores.
231		let proof_size_per_block = MAX_POV_SIZE as u64 / blocks_per_core;
232
233		Weight::from_parts(ref_time_per_block, proof_size_per_block)
234	}
235}
236
237impl<Config: crate::Config, TargetBlockRate: Get<u32>> Get<Weight>
238	for MaxParachainBlockWeight<Config, TargetBlockRate>
239{
240	fn get() -> Weight {
241		let digest = frame_system::Pallet::<Config>::digest();
242		let target_block_weight = Self::target_block_weight_with_digest(&digest);
243
244		let maybe_full_core_weight = if is_first_block_in_core_with_digest(&digest).unwrap_or(false)
245		{
246			FULL_CORE_WEIGHT
247		} else {
248			target_block_weight
249		};
250
251		// Check if we are inside `pre_validate_extrinsic` of the transaction extension.
252		//
253		// When `pre_validate_extrinsic` calls this code, it is interested to know the
254		// fractional `target_block_weight` which is then used to calculate the weight for each
255		// dispatch class. Fractional weight is returned to detect transactions exceeding the
256		// fractional target, enabling proper transition to `PotentialFullCore` mode.
257		//
258		// If `FullCore` mode is already enabled, the fractional target weight is not important
259		// anymore.
260		let in_pre_validate = inside_pre_validate::with(|v| *v).unwrap_or(false);
261
262		match crate::BlockWeightMode::<Config>::get().filter(|m| !m.is_stale()) {
263			// We allow the full core.
264			Some(
265				BlockWeightMode::<Config>::FullCore { .. } |
266				BlockWeightMode::<Config>::PotentialFullCore { .. },
267			) => FULL_CORE_WEIGHT,
268			// We are in `pre_validate`.
269			_ if in_pre_validate => target_block_weight,
270			// Only use the fraction of a core.
271			Some(BlockWeightMode::<Config>::FractionOfCore { first_transaction_index, .. }) => {
272				let is_phase_finalization = frame_system::Pallet::<Config>::execution_phase()
273					.map_or(false, |p| matches!(p, frame_system::Phase::Finalization));
274				let inherents_applied = frame_system::Pallet::<Config>::inherents_applied();
275
276				if first_transaction_index.is_none() && !is_phase_finalization && !inherents_applied
277				{
278					// We are running in the context of inherents, here we allow the
279					// full core weight.
280					maybe_full_core_weight
281				} else {
282					// If we are finalizing the block (e.g. `on_idle` is running and
283					// `finalize_block`), running `on_poll` or nothing required more than the target
284					// block weight, we only allow the target block weight.
285					target_block_weight
286				}
287			},
288			// We are in `on_initialize` or in an offchain context.
289			None => maybe_full_core_weight,
290		}
291	}
292}
293
294/// Is this the first block in a core?
295fn is_first_block_in_core<T: Config>() -> Option<bool> {
296	let digest = frame_system::Pallet::<T>::digest();
297	is_first_block_in_core_with_digest(&digest)
298}
299
300/// Is this the first block in a core? (takes digest as parameter)
301///
302/// Returns `None` if the [`CumulusDigestItem::BlockBundleInfo`] digest is not set.
303fn is_first_block_in_core_with_digest(digest: &Digest) -> Option<bool> {
304	CumulusDigestItem::find_block_bundle_info(digest).map(|bi| bi.index == 0)
305}
306
307/// Is the `BlockWeight` already above the target block weight?
308///
309/// Returns `None` if the [`CumulusDigestItem::BlockBundleInfo`] digest is not set.
310fn block_weight_over_target_block_weight<T: Config, TargetBlockRate: Get<u32>>() -> bool {
311	let target_block_weight = MaxParachainBlockWeight::<T, TargetBlockRate>::target_block_weight();
312
313	frame_system::Pallet::<T>::remaining_block_weight()
314		.consumed()
315		.any_gt(target_block_weight)
316}