referrerpolicy=no-referrer-when-downgrade

cumulus_pallet_weight_reclaim/
lib.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//! Pallet and transaction extensions to reclaim PoV proof size weight after an extrinsic has been
18//! applied.
19//!
20//! This crate provides:
21//! * [`StorageWeightReclaim`] transaction extension: it must wrap the whole transaction extension
22//!   pipeline.
23//! * The pallet required for the transaction extensions weight information and benchmarks.
24
25#![cfg_attr(not(feature = "std"), no_std)]
26
27extern crate alloc;
28#[cfg(not(feature = "std"))]
29use alloc::vec::Vec;
30use codec::{Decode, DecodeWithMemTracking, Encode};
31use cumulus_primitives_storage_weight_reclaim::get_proof_size;
32use derive_where::derive_where;
33use frame_support::{
34	dispatch::{DispatchInfo, PostDispatchInfo},
35	pallet_prelude::Weight,
36	traits::Defensive,
37};
38use scale_info::TypeInfo;
39use sp_runtime::{
40	traits::{DispatchInfoOf, Dispatchable, Implication, PostDispatchInfoOf, TransactionExtension},
41	transaction_validity::{TransactionSource, TransactionValidityError, ValidTransaction},
42	DispatchResult,
43};
44
45#[cfg(feature = "runtime-benchmarks")]
46pub mod benchmarks;
47#[cfg(test)]
48mod tests;
49mod weights;
50
51pub use pallet::*;
52pub use weights::WeightInfo;
53
54const LOG_TARGET: &'static str = "runtime::storage_reclaim_pallet";
55
56/// Pallet to use alongside the transaction extension [`StorageWeightReclaim`], the pallet provides
57/// weight information and benchmarks.
58#[frame_support::pallet]
59pub mod pallet {
60	use super::*;
61
62	#[pallet::pallet]
63	pub struct Pallet<T>(_);
64
65	#[pallet::config]
66	pub trait Config: frame_system::Config {
67		type WeightInfo: WeightInfo;
68	}
69}
70
71/// Storage weight reclaim mechanism.
72///
73/// This extension must wrap all the transaction extensions:
74#[doc = docify::embed!("./src/tests.rs", Tx)]
75///
76/// This extension checks the size of the node-side storage proof before and after executing a given
77/// extrinsic using the proof size host function. The difference between benchmarked and used weight
78/// is reclaimed.
79///
80/// If the benchmark was underestimating the proof size, then it is added to the block weight.
81///
82/// For the time part of the weight, it does same as system `WeightReclaim` extension, it
83/// calculates the unused weight using the post information and reclaim the unused weight.
84/// So this extension can be used as a drop-in replacement for `WeightReclaim` extension for
85/// parachains.
86#[derive(Encode, Decode, DecodeWithMemTracking, TypeInfo)]
87#[derive_where(Clone, Eq, PartialEq, Default; S)]
88#[scale_info(skip_type_params(T))]
89pub struct StorageWeightReclaim<T, S>(pub S, core::marker::PhantomData<T>);
90
91impl<T, S> StorageWeightReclaim<T, S> {
92	/// Create a new `StorageWeightReclaim` instance.
93	pub fn new(s: S) -> Self {
94		Self(s, Default::default())
95	}
96}
97
98impl<T, S> From<S> for StorageWeightReclaim<T, S> {
99	fn from(s: S) -> Self {
100		Self::new(s)
101	}
102}
103
104impl<T, S: core::fmt::Debug> core::fmt::Debug for StorageWeightReclaim<T, S> {
105	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
106		#[cfg(feature = "std")]
107		let _ = write!(f, "StorageWeightReclaim<{:?}>", self.0);
108
109		#[cfg(not(feature = "std"))]
110		let _ = write!(f, "StorageWeightReclaim<wasm-stripped>");
111
112		Ok(())
113	}
114}
115
116impl<T: Config + Send + Sync, S: TransactionExtension<T::RuntimeCall>>
117	TransactionExtension<T::RuntimeCall> for StorageWeightReclaim<T, S>
118where
119	T::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
120{
121	const IDENTIFIER: &'static str = "StorageWeightReclaim<Use `metadata()`!>";
122
123	type Implicit = S::Implicit;
124
125	// Initial proof size and inner extension value.
126	type Val = (Option<u64>, S::Val);
127
128	// Initial proof size and inner extension pre.
129	type Pre = (Option<u64>, S::Pre);
130
131	fn implicit(&self) -> Result<Self::Implicit, TransactionValidityError> {
132		self.0.implicit()
133	}
134
135	fn metadata() -> Vec<sp_runtime::traits::TransactionExtensionMetadata> {
136		let mut inner = S::metadata();
137		inner.push(sp_runtime::traits::TransactionExtensionMetadata {
138			identifier: "StorageWeightReclaim",
139			ty: scale_info::meta_type::<()>(),
140			implicit: scale_info::meta_type::<()>(),
141		});
142		inner
143	}
144
145	fn weight(&self, call: &T::RuntimeCall) -> Weight {
146		T::WeightInfo::storage_weight_reclaim().saturating_add(self.0.weight(call))
147	}
148
149	fn validate(
150		&self,
151		origin: T::RuntimeOrigin,
152		call: &T::RuntimeCall,
153		info: &DispatchInfoOf<T::RuntimeCall>,
154		len: usize,
155		self_implicit: Self::Implicit,
156		inherited_implication: &impl Implication,
157		source: TransactionSource,
158	) -> Result<(ValidTransaction, Self::Val, T::RuntimeOrigin), TransactionValidityError> {
159		let proof_size = get_proof_size();
160
161		self.0
162			.validate(origin, call, info, len, self_implicit, inherited_implication, source)
163			.map(|(validity, val, origin)| (validity, (proof_size, val), origin))
164	}
165
166	fn prepare(
167		self,
168		val: Self::Val,
169		origin: &T::RuntimeOrigin,
170		call: &T::RuntimeCall,
171		info: &DispatchInfoOf<T::RuntimeCall>,
172		len: usize,
173	) -> Result<Self::Pre, TransactionValidityError> {
174		let (proof_size, inner_val) = val;
175		self.0.prepare(inner_val, origin, call, info, len).map(|pre| (proof_size, pre))
176	}
177
178	fn post_dispatch_details(
179		pre: Self::Pre,
180		info: &DispatchInfoOf<T::RuntimeCall>,
181		post_info: &PostDispatchInfoOf<T::RuntimeCall>,
182		len: usize,
183		result: &DispatchResult,
184	) -> Result<Weight, TransactionValidityError> {
185		let (proof_size_before_dispatch, inner_pre) = pre;
186
187		let mut post_info_with_inner = *post_info;
188		S::post_dispatch(inner_pre, info, &mut post_info_with_inner, len, result)?;
189
190		let inner_refund = if let (Some(before_weight), Some(after_weight)) =
191			(post_info.actual_weight, post_info_with_inner.actual_weight)
192		{
193			before_weight.saturating_sub(after_weight)
194		} else {
195			Weight::zero()
196		};
197
198		let Some(proof_size_before_dispatch) = proof_size_before_dispatch else {
199			// We have no proof size information, there is nothing we can do.
200			return Ok(inner_refund);
201		};
202
203		let Some(proof_size_after_dispatch) = get_proof_size().defensive_proof(
204			"Proof recording enabled during prepare, now disabled. This should not happen.",
205		) else {
206			return Ok(inner_refund)
207		};
208
209		// The consumed proof size as measured by the host.
210		let measured_proof_size =
211			proof_size_after_dispatch.saturating_sub(proof_size_before_dispatch);
212
213		// The consumed weight as benchmarked. Calculated from post info and info.
214		// NOTE: `calc_actual_weight` will take the minimum of `post_info` and `info` weights.
215		// This means any underestimation of compute time in the pre dispatch info will not be
216		// taken into account.
217		let benchmarked_actual_weight = post_info_with_inner.calc_actual_weight(info);
218
219		let benchmarked_actual_proof_size = benchmarked_actual_weight.proof_size();
220		if benchmarked_actual_proof_size < measured_proof_size {
221			log::error!(
222				target: LOG_TARGET,
223				"Benchmarked storage weight smaller than consumed storage weight. \
224				benchmarked: {benchmarked_actual_proof_size} consumed: {measured_proof_size}"
225			);
226		} else {
227			log::trace!(
228				target: LOG_TARGET,
229				"Reclaiming storage weight. benchmarked: {benchmarked_actual_proof_size},
230				consumed: {measured_proof_size}"
231			);
232		}
233
234		let accurate_weight = benchmarked_actual_weight.set_proof_size(measured_proof_size);
235
236		let pov_size_missing_from_node = frame_system::BlockWeight::<T>::mutate(|current_weight| {
237			let already_reclaimed = frame_system::ExtrinsicWeightReclaimed::<T>::get();
238			current_weight.accrue(already_reclaimed, info.class);
239			current_weight.reduce(info.total_weight(), info.class);
240			current_weight.accrue(accurate_weight, info.class);
241
242			// If we encounter a situation where the node-side proof size is already higher than
243			// what we have in the runtime bookkeeping, we add the difference to the `BlockWeight`.
244			// This prevents that the proof size grows faster than the runtime proof size.
245			let extrinsic_len = frame_system::AllExtrinsicsLen::<T>::get().unwrap_or(0);
246			let node_side_pov_size = proof_size_after_dispatch.saturating_add(extrinsic_len.into());
247			let block_weight_proof_size = current_weight.total().proof_size();
248			let pov_size_missing_from_node =
249				node_side_pov_size.saturating_sub(block_weight_proof_size);
250			if pov_size_missing_from_node > 0 {
251				log::warn!(
252					target: LOG_TARGET,
253					"Node-side PoV size higher than runtime proof size weight. node-side: \
254					{node_side_pov_size} extrinsic_len: {extrinsic_len} runtime: \
255					{block_weight_proof_size}, missing: {pov_size_missing_from_node}. Setting to \
256					node-side proof size."
257				);
258				current_weight
259					.accrue(Weight::from_parts(0, pov_size_missing_from_node), info.class);
260			}
261
262			pov_size_missing_from_node
263		});
264
265		// The saturation will happen if the pre-dispatch weight is underestimating the proof
266		// size or if the node-side proof size is higher than expected.
267		// In this case the extrinsic proof size weight reclaimed is 0 and not a negative reclaim.
268		let accurate_unspent = info
269			.total_weight()
270			.saturating_sub(accurate_weight)
271			.saturating_sub(Weight::from_parts(0, pov_size_missing_from_node));
272		frame_system::ExtrinsicWeightReclaimed::<T>::put(accurate_unspent);
273
274		// Call have already returned their unspent amount.
275		// (also transaction extension prior in the pipeline, but there shouldn't be any.)
276		let already_unspent_in_tx_ext_pipeline = post_info.calc_unspent(info);
277		Ok(accurate_unspent.saturating_sub(already_unspent_in_tx_ext_pipeline))
278	}
279
280	fn bare_validate(
281		call: &T::RuntimeCall,
282		info: &DispatchInfoOf<T::RuntimeCall>,
283		len: usize,
284	) -> frame_support::pallet_prelude::TransactionValidity {
285		S::bare_validate(call, info, len)
286	}
287
288	fn bare_validate_and_prepare(
289		call: &T::RuntimeCall,
290		info: &DispatchInfoOf<T::RuntimeCall>,
291		len: usize,
292	) -> Result<(), TransactionValidityError> {
293		S::bare_validate_and_prepare(call, info, len)
294	}
295
296	fn bare_post_dispatch(
297		info: &DispatchInfoOf<T::RuntimeCall>,
298		post_info: &mut PostDispatchInfoOf<T::RuntimeCall>,
299		len: usize,
300		result: &DispatchResult,
301	) -> Result<(), TransactionValidityError> {
302		S::bare_post_dispatch(info, post_info, len, result)?;
303
304		frame_system::Pallet::<T>::reclaim_weight(info, post_info)
305	}
306}