referrerpolicy=no-referrer-when-downgrade

cumulus_pallet_parachain_system/block_weight/
transaction_extension.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
17use super::{
18	block_weight_over_target_block_weight, inside_pre_validate, is_first_block_in_core_with_digest,
19	BlockWeightMode, MaxParachainBlockWeight, FULL_CORE_WEIGHT, LOG_TARGET,
20};
21use crate::WeightInfo;
22use alloc::vec::Vec;
23use codec::{Decode, DecodeWithMemTracking, Encode};
24use cumulus_primitives_core::CumulusDigestItem;
25use frame_support::{
26	dispatch::{DispatchClass, DispatchInfo, PostDispatchInfo},
27	pallet_prelude::{
28		InvalidTransaction, TransactionSource, TransactionValidityError, ValidTransaction,
29	},
30	weights::Weight,
31};
32use scale_info::TypeInfo;
33use sp_core::Get;
34use sp_runtime::{
35	traits::{DispatchInfoOf, Dispatchable, Implication, PostDispatchInfoOf, TransactionExtension},
36	DispatchResult,
37};
38
39/// Transaction extension that dynamically changes the max block weight.
40///
41/// With block bundling, parachains are running with block weights that may not allow certain
42/// transactions to be applied, e.g. a runtime upgrade. To ensure that these transactions can still
43/// be applied, this transaction extension can change the max block weight as required. There are
44/// multiple requirements for it to change the block weight:
45///
46/// 1. Only the first block of a core is allowed to change its block weight.
47///
48/// 2. Any `inherent` or any transaction up to `MAX_TRANSACTION_TO_CONSIDER` requires more block
49///    weight than the target extrinsic weight. Target extrinsic weight is the max weight for the
50///    respective extrinsic class. The priority to determine the target e weight is the following,
51///    we start checking if
52///    [`WeightsPerClass::max_extrinsic`](frame_system::limits::WeightsPerClass::max_extrinsic) is
53///    set, after this
54///    [`WeightsPerClass::max_total`](frame_system::limits::WeightsPerClass::max_total) and if both
55///    of these are `None` we fall back to the actual target block weight.
56///
57/// Because the node is tracking the wall clock time while building a block to abort block
58/// production if it takes too long, we do not allow any block to change the block weight. The node
59/// knows that the first block of a core may runs longer. So, the node allows this block to take up
60/// to `2s` of wall clock time. `2s` is the time each `PoV` gets on the relay chain for its
61/// validation or in other words the maximum core execution time. The extension sets the
62/// [`CumulusDigestItem::UseFullCore`] digest when the block should occupy the entire core.
63///
64/// Before dispatching an extrinsic the extension will check the requirements and set the
65/// appropriate [`BlockWeightMode`]. After the extrinsic has finished, the checks from before
66/// dispatching the extrinsic are repeated with the post dispatch weights. The [`BlockWeightMode`]
67/// is changed properly.
68///
69/// # Note
70///
71/// The extension requires that any of the inner extensions sets the
72/// [`BlockWeight`](frame_system::BlockWeight). Otherwise the weight tracking is not working
73/// properly. Normally this is done by [`CheckWeight`](frame_system::CheckWeight).
74///
75/// # Generic parameters
76///
77/// - `Config`: The [`Config`](crate::Config) trait of this pallet.
78///
79/// - `Inner`: The inner transaction extensions aka the other transaction extensions to be used by
80///   the runtime.
81///
82/// - `TargetBlockRate`: The target block rate the parachain should be running with. Or in other
83///   words, the number of blocks the parachain should produce in `6s`(relay chain slot duration).
84///
85/// - `MAX_TRANSACTION`: The maximum number of transactions to consider before giving up to change
86///   the max block weight.
87///
88/// - `ALLOW_NORMAL`: Should transactions with a dispatch class `Normal` be allowed to change the
89///   max block weight?
90#[derive(Encode, Decode, DecodeWithMemTracking, TypeInfo)]
91#[derive_where::derive_where(Clone, Eq, PartialEq, Default; Inner)]
92#[scale_info(skip_type_params(Config, TargetBlockRate))]
93pub struct DynamicMaxBlockWeight<
94	Config,
95	Inner,
96	TargetBlockRate,
97	const MAX_TRANSACTION_TO_CONSIDER: u32 = 10,
98	const ALLOW_NORMAL: bool = true,
99>(pub Inner, core::marker::PhantomData<(Config, TargetBlockRate)>);
100
101impl<T, S, TargetBlockRate, const MAX_TRANSACTION_TO_CONSIDER: u32, const ALLOW_NORMAL: bool>
102	DynamicMaxBlockWeight<T, S, TargetBlockRate, MAX_TRANSACTION_TO_CONSIDER, ALLOW_NORMAL>
103{
104	/// Create a new [`DynamicMaxBlockWeight`] instance.
105	pub fn new(s: S) -> Self {
106		Self(s, Default::default())
107	}
108}
109
110impl<
111		Config,
112		Inner,
113		TargetBlockRate,
114		const MAX_TRANSACTION_TO_CONSIDER: u32,
115		const ALLOW_NORMAL: bool,
116	> DynamicMaxBlockWeight<Config, Inner, TargetBlockRate, MAX_TRANSACTION_TO_CONSIDER, ALLOW_NORMAL>
117where
118	Config: crate::Config,
119	TargetBlockRate: Get<u32>,
120{
121	/// Should be executed before `validate` is called for any inner extension.
122	fn pre_validate_extrinsic(
123		info: &DispatchInfo,
124		len: usize,
125	) -> Result<(), TransactionValidityError> {
126		let is_not_inherent = frame_system::Pallet::<Config>::inherents_applied();
127		let extrinsic_index = frame_system::Pallet::<Config>::extrinsic_index().unwrap_or_default();
128		let transaction_index = is_not_inherent.then(|| extrinsic_index);
129
130		crate::BlockWeightMode::<Config>::mutate(|mode| {
131			let current_mode = mode.get_or_insert_with(|| BlockWeightMode::<Config>::fraction_of_core(transaction_index));
132
133			// If the mode is stale (from previous block), we reset it.
134			//
135			// This happens for example when running in an offchain context.
136			if current_mode.is_stale() {
137				*current_mode = BlockWeightMode::fraction_of_core(transaction_index);
138			}
139
140			log::trace!(
141				target: LOG_TARGET,
142				"About to pre-validate an extrinsic. current_mode={current_mode:?}, transaction_index={transaction_index:?}"
143			);
144
145			let is_potential =
146				matches!(current_mode, &mut BlockWeightMode::PotentialFullCore { .. });
147
148			match current_mode {
149				// We are already allowing the full core, not that much more to do here.
150				BlockWeightMode::<Config>::FullCore { .. } => {},
151				BlockWeightMode::<Config>::PotentialFullCore { first_transaction_index, .. } |
152				BlockWeightMode::<Config>::FractionOfCore { first_transaction_index, .. } => {
153					debug_assert!(
154						!is_potential,
155						"`PotentialFullCore` should resolve to `FullCore` or `FractionOfCore` after applying a transaction.",
156					);
157
158					let digest = frame_system::Pallet::<Config>::digest();
159					let block_weight_over_limit = extrinsic_index == 0
160						&& block_weight_over_target_block_weight::<Config, TargetBlockRate>();
161
162					// If `BlockWeights` is configured correctly, it will internally call `MaxParachainBlockWeight::get()`
163					// and by setting this variable to `true`, we tell it the context. This is important as we want to get
164					// the fractional `target_block_weight` and not the full core weight. Otherwise, we will here get a too huge weight
165					// and do not set the `PotentialFullCore` weight, leading to `CheckWeight` rejecting the extrinsic.
166					//
167					// All of this is only important for extrinsics that will enable the `PotentialFullCore` mode.
168					let block_weights = inside_pre_validate::using(&mut true, || Config::BlockWeights::get());
169					let class_weights = block_weights.get(info.class);
170					let target_block_weight =
171						MaxParachainBlockWeight::<Config, TargetBlockRate>::target_block_weight_with_digest(&digest)
172							.saturating_sub(block_weights.base_block);
173
174					// `max_extrinsic` determines the maximum weight allowed for one transaction.
175					// If that isn't set, we fall back to `max_total` which represents the total allowed weight for
176					// this dispatch class. If all previous weights are `None`, we fall back to the target block weight.
177					let target_weight = class_weights
178						.max_extrinsic
179						.or(class_weights.max_total)
180						.unwrap_or(target_block_weight);
181
182					// Protection against a misconfiguration as this should be detected by the pre-inherent hook.
183					if block_weight_over_limit {
184						*mode = Some(BlockWeightMode::<Config>::full_core());
185
186						// Inform the node that this block uses the full core.
187						frame_system::Pallet::<Config>::deposit_log(
188							CumulusDigestItem::UseFullCore.to_digest_item(),
189						);
190
191						if !is_first_block_in_core_with_digest(&digest).unwrap_or(false) {
192							// We are already above the allowed maximum and do not want to accept any more
193							// extrinsics.
194							frame_system::Pallet::<Config>::register_extra_weight_unchecked(
195								FULL_CORE_WEIGHT,
196								DispatchClass::Mandatory,
197							);
198						}
199
200						log::error!(
201							target: LOG_TARGET,
202							"Inherent block logic took longer than the target block weight, \
203							`DynamicMaxBlockWeightHooks` not registered as `PreInherents` hook!",
204						);
205					} else if info
206						.total_weight()
207						// The extrinsic lengths counts towards the POV size
208						.saturating_add(Weight::from_parts(0, len as u64))
209						.any_gt(target_weight)
210					{
211						// When `ALLOW_NORMAL` is `true`, we want to allow all classes of transactions. Inherents are always allowed.
212						let class_allowed = ALLOW_NORMAL || matches!(info.class, DispatchClass::Operational | DispatchClass::Mandatory);
213
214						// If the `BundleInfo` digest is not set (function returns `None`), it means we are in some offchain
215						// call like `validate_transaction`. In this case we assume this is the first block, otherwise these big
216						// transactions will never be able to enter the tx pool.
217						let is_first_block = is_first_block_in_core_with_digest(&digest).unwrap_or(true);
218
219						if transaction_index.unwrap_or_default().saturating_sub(first_transaction_index.unwrap_or_default()) < MAX_TRANSACTION_TO_CONSIDER
220							&& is_first_block && class_allowed {
221							log::trace!(
222								target: LOG_TARGET,
223								"Enabling `PotentialFullCore` mode for extrinsic",
224							);
225
226							*mode = Some(BlockWeightMode::<Config>::potential_full_core(
227								// While applying inherents `extrinsic_index` and `first_transaction_index` will be `None`.
228								// When the first transaction is applied, we want to store the index.
229								first_transaction_index.or(transaction_index),
230								target_weight,
231							));
232						} else {
233							log::trace!(
234								target: LOG_TARGET,
235								"Transaction is over the block limit, but is either outside of the allowed window or the dispatch class is not allowed.",
236							);
237
238							return Err(InvalidTransaction::ExhaustsResources)
239						}
240					} else {
241						if is_potential {
242							log::trace!(
243								target: LOG_TARGET,
244								"Resetting back to `FractionOfCore`"
245							);
246						} else {
247							log::trace!(
248								target: LOG_TARGET,
249								"Not changing block weight mode"
250							);
251						}
252
253						*mode =
254							Some(BlockWeightMode::<Config>::fraction_of_core(first_transaction_index.or(transaction_index)));
255					}
256				},
257			};
258
259			Ok(())
260		}).map_err(Into::into)
261	}
262
263	/// Should be called after all inner extensions have finished executing their post dispatch
264	/// handling.
265	///
266	/// Returns the weight to refund. Aka the weight that wasn't used by this extension.
267	fn post_dispatch_extrinsic(info: &DispatchInfo) -> Weight {
268		crate::BlockWeightMode::<Config>::mutate(|weight_mode| {
269			let Some(mode) = weight_mode else { return Weight::zero() };
270
271			match mode {
272				// If the previous mode was already `FullCore`, we are fine.
273				BlockWeightMode::<Config>::FullCore { .. } => {
274					Config::WeightInfo::block_weight_tx_extension_max_weight()
275						.saturating_sub(Config::WeightInfo::block_weight_tx_extension_full_core())
276				},
277				BlockWeightMode::<Config>::FractionOfCore { .. } => {
278					let digest = frame_system::Pallet::<Config>::digest();
279					let is_above_limit =
280						block_weight_over_target_block_weight::<Config, TargetBlockRate>();
281
282					// If we are above the limit, it means the transaction used more weight than
283					// what it had announced, which should not happen.
284					if is_above_limit {
285						log::error!(
286							target: LOG_TARGET,
287							"Extrinsic ({}) used more weight than what it had announced and pushed the \
288							block above the allowed weight limit!",
289							frame_system::Pallet::<Config>::extrinsic_index().unwrap_or_default()
290						);
291
292						// If this isn't the first block in a core, we register the full core weight
293						// to ensure that we don't include any other transactions. Because we don't
294						// know how many weight of the core was already used by the blocks before.
295						if !is_first_block_in_core_with_digest(&digest).unwrap_or(false) {
296							log::error!(
297								target: LOG_TARGET,
298								"Registering `FULL_CORE_WEIGHT` to ensure no other transaction is included \
299								in this block, because this isn't the first block in the core!",
300							);
301
302							frame_system::Pallet::<Config>::register_extra_weight_unchecked(
303								FULL_CORE_WEIGHT,
304								DispatchClass::Mandatory,
305							);
306						}
307
308						*weight_mode = Some(BlockWeightMode::<Config>::full_core());
309
310						// Inform the node that this block uses the full core.
311						frame_system::Pallet::<Config>::deposit_log(
312							CumulusDigestItem::UseFullCore.to_digest_item(),
313						);
314					}
315
316					Config::WeightInfo::block_weight_tx_extension_max_weight().saturating_sub(
317						Config::WeightInfo::block_weight_tx_extension_stays_fraction_of_core(),
318					)
319				},
320				// Now we check if the transaction required more weight than the target weight.
321				BlockWeightMode::<Config>::PotentialFullCore {
322					first_transaction_index,
323					target_weight,
324					..
325				} => {
326					let block_weight = frame_system::BlockWeight::<Config>::get();
327					let extrinsic_class_weight = block_weight.get(info.class);
328
329					// The transaction weight after execution is may not above the target weight,
330					// but the full block weight is maybe now above the target weight.
331					if extrinsic_class_weight.any_gt(*target_weight) ||
332						block_weight_over_target_block_weight::<Config, TargetBlockRate>()
333					{
334						log::trace!(
335							target: LOG_TARGET,
336							"Extrinsic class weight {extrinsic_class_weight:?} above target weight {target_weight:?}, enabling `FullCore` mode."
337						);
338
339						*weight_mode = Some(BlockWeightMode::<Config>::full_core());
340
341						// Inform the node that this block uses the full core.
342						frame_system::Pallet::<Config>::deposit_log(
343							CumulusDigestItem::UseFullCore.to_digest_item(),
344						);
345					} else {
346						log::trace!(
347							target: LOG_TARGET,
348							"Extrinsic class weight {extrinsic_class_weight:?} not above target \
349							weight {target_weight:?}, going back to `FractionOfCore` mode."
350						);
351
352						*weight_mode = Some(BlockWeightMode::<Config>::fraction_of_core(
353							*first_transaction_index,
354						));
355					}
356
357					// We run into the worst case, so no refund :)
358					Weight::zero()
359				},
360			}
361		})
362	}
363}
364
365impl<
366		Config,
367		Inner,
368		TargetBlockRate,
369		const MAX_TRANSACTION_TO_CONSIDER: u32,
370		const ALLOW_NORMAL: bool,
371	> From<Inner>
372	for DynamicMaxBlockWeight<
373		Config,
374		Inner,
375		TargetBlockRate,
376		MAX_TRANSACTION_TO_CONSIDER,
377		ALLOW_NORMAL,
378	>
379{
380	fn from(s: Inner) -> Self {
381		Self::new(s)
382	}
383}
384
385impl<
386		Config,
387		Inner: core::fmt::Debug,
388		TargetBlockRate,
389		const MAX_TRANSACTION_TO_CONSIDER: u32,
390		const ALLOW_NORMAL: bool,
391	> core::fmt::Debug
392	for DynamicMaxBlockWeight<
393		Config,
394		Inner,
395		TargetBlockRate,
396		MAX_TRANSACTION_TO_CONSIDER,
397		ALLOW_NORMAL,
398	>
399{
400	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
401		write!(f, "DynamicMaxBlockWeight<{:?}>", self.0)
402	}
403}
404
405impl<
406		Config: crate::Config + Send + Sync,
407		Inner: TransactionExtension<Config::RuntimeCall>,
408		TargetBlockRate: Get<u32> + Send + Sync + 'static,
409		const MAX_TRANSACTION_TO_CONSIDER: u32,
410		const ALLOW_NORMAL: bool,
411	> TransactionExtension<Config::RuntimeCall>
412	for DynamicMaxBlockWeight<
413		Config,
414		Inner,
415		TargetBlockRate,
416		MAX_TRANSACTION_TO_CONSIDER,
417		ALLOW_NORMAL,
418	>
419where
420	Config::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
421{
422	const IDENTIFIER: &'static str = "DynamicMaxBlockWeight<Use `metadata()`!>";
423
424	type Implicit = Inner::Implicit;
425
426	type Val = Inner::Val;
427
428	type Pre = Inner::Pre;
429
430	fn implicit(&self) -> Result<Self::Implicit, TransactionValidityError> {
431		self.0.implicit()
432	}
433
434	fn metadata() -> Vec<sp_runtime::traits::TransactionExtensionMetadata> {
435		let mut inner = Inner::metadata();
436		inner.push(sp_runtime::traits::TransactionExtensionMetadata {
437			identifier: "DynamicMaxBlockWeight",
438			ty: scale_info::meta_type::<()>(),
439			implicit: scale_info::meta_type::<()>(),
440		});
441		inner
442	}
443
444	fn weight(&self, call: &Config::RuntimeCall) -> Weight {
445		Config::WeightInfo::block_weight_tx_extension_max_weight()
446			.saturating_add(self.0.weight(call))
447	}
448
449	fn validate(
450		&self,
451		origin: Config::RuntimeOrigin,
452		call: &Config::RuntimeCall,
453		info: &DispatchInfoOf<Config::RuntimeCall>,
454		len: usize,
455		self_implicit: Self::Implicit,
456		inherited_implication: &impl Implication,
457		source: TransactionSource,
458	) -> Result<(ValidTransaction, Self::Val, Config::RuntimeOrigin), TransactionValidityError> {
459		Self::pre_validate_extrinsic(info, len)?;
460
461		self.0
462			.validate(origin, call, info, len, self_implicit, inherited_implication, source)
463	}
464
465	fn prepare(
466		self,
467		val: Self::Val,
468		origin: &Config::RuntimeOrigin,
469		call: &Config::RuntimeCall,
470		info: &DispatchInfoOf<Config::RuntimeCall>,
471		len: usize,
472	) -> Result<Self::Pre, TransactionValidityError> {
473		self.0.prepare(val, origin, call, info, len)
474	}
475
476	fn post_dispatch_details(
477		pre: Self::Pre,
478		info: &DispatchInfoOf<Config::RuntimeCall>,
479		post_info: &PostDispatchInfo,
480		len: usize,
481		result: &DispatchResult,
482	) -> Result<Weight, TransactionValidityError> {
483		let weight_refund = Inner::post_dispatch_details(pre, info, post_info, len, result)?;
484
485		let extra_refund = Self::post_dispatch_extrinsic(info);
486
487		Ok(weight_refund.saturating_add(extra_refund))
488	}
489
490	fn bare_validate(
491		call: &Config::RuntimeCall,
492		info: &DispatchInfoOf<Config::RuntimeCall>,
493		len: usize,
494	) -> frame_support::pallet_prelude::TransactionValidity {
495		Self::pre_validate_extrinsic(info, len)?;
496
497		Inner::bare_validate(call, info, len)
498	}
499
500	fn bare_validate_and_prepare(
501		call: &Config::RuntimeCall,
502		info: &DispatchInfoOf<Config::RuntimeCall>,
503		len: usize,
504	) -> Result<(), TransactionValidityError> {
505		Self::pre_validate_extrinsic(info, len)?;
506
507		Inner::bare_validate_and_prepare(call, info, len)
508	}
509
510	fn bare_post_dispatch(
511		info: &DispatchInfoOf<Config::RuntimeCall>,
512		post_info: &mut PostDispatchInfoOf<Config::RuntimeCall>,
513		len: usize,
514		result: &DispatchResult,
515	) -> Result<(), TransactionValidityError> {
516		Inner::bare_post_dispatch(info, post_info, len, result)?;
517
518		Self::post_dispatch_extrinsic(info);
519
520		Ok(())
521	}
522}