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}