referrerpolicy=no-referrer-when-downgrade

pallet_glutton/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! # WARNING
19//!
20//! **DO NOT USE ON VALUE-BEARING CHAINS. THIS PALLET IS ONLY INTENDED FOR TESTING USAGE.**
21//!
22//! # Glutton Pallet
23//!
24//! Pallet that consumes `ref_time` and `proof_size` of a block. Based on the `Compute` and
25//! `Storage` parameters the pallet consumes the adequate amount of weight.
26
27#![deny(missing_docs)]
28#![cfg_attr(not(feature = "std"), no_std)]
29
30#[cfg(feature = "runtime-benchmarks")]
31mod benchmarking;
32#[cfg(test)]
33mod mock;
34#[cfg(test)]
35mod tests;
36pub mod weights;
37
38extern crate alloc;
39
40use alloc::{vec, vec::Vec};
41use blake2::{Blake2b512, Digest};
42use frame_support::{pallet_prelude::*, weights::WeightMeter, DefaultNoBound};
43use frame_system::pallet_prelude::*;
44use sp_io::hashing::twox_256;
45use sp_runtime::{traits::Zero, FixedPointNumber, FixedU64};
46
47pub use pallet::*;
48pub use weights::WeightInfo;
49
50const LOG_TARGET: &str = "runtime::glutton";
51
52/// The size of each value in the `TrashData` storage in bytes.
53pub const VALUE_SIZE: usize = 1024;
54/// Max number of entries for the `TrashData` map.
55pub const MAX_TRASH_DATA_ENTRIES: u32 = 65_000;
56/// Hard limit for any other resource limit (in units).
57pub const RESOURCE_HARD_LIMIT: FixedU64 = FixedU64::from_u32(10);
58
59#[frame_support::pallet]
60pub mod pallet {
61	use super::*;
62
63	#[pallet::config]
64	pub trait Config: frame_system::Config {
65		/// The overarching event type.
66		#[allow(deprecated)]
67		type RuntimeEvent: From<Event> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
68
69		/// The admin origin that can set computational limits and initialize the pallet.
70		type AdminOrigin: EnsureOrigin<Self::RuntimeOrigin>;
71
72		/// Weight information for this pallet.
73		type WeightInfo: WeightInfo;
74	}
75
76	#[pallet::pallet]
77	pub struct Pallet<T>(_);
78
79	#[pallet::event]
80	#[pallet::generate_deposit(pub(super) fn deposit_event)]
81	pub enum Event {
82		/// The pallet has been (re)initialized.
83		PalletInitialized {
84			/// Whether the pallet has been re-initialized.
85			reinit: bool,
86		},
87		/// The computation limit has been updated.
88		ComputationLimitSet {
89			/// The computation limit.
90			compute: FixedU64,
91		},
92		/// The storage limit has been updated.
93		StorageLimitSet {
94			/// The storage limit.
95			storage: FixedU64,
96		},
97		/// The block length limit has been updated.
98		BlockLengthLimitSet {
99			/// The block length limit.
100			block_length: FixedU64,
101		},
102	}
103
104	#[pallet::error]
105	pub enum Error<T> {
106		/// The pallet was already initialized.
107		///
108		/// Set `witness_count` to `Some` to bypass this error.
109		AlreadyInitialized,
110
111		/// The limit was over [`crate::RESOURCE_HARD_LIMIT`].
112		InsaneLimit,
113	}
114
115	/// The proportion of the remaining `ref_time` to consume during `on_idle`.
116	///
117	/// `1.0` is mapped to `100%`. Must be at most [`crate::RESOURCE_HARD_LIMIT`]. Setting this to
118	/// over `1.0` could stall the chain.
119	#[pallet::storage]
120	pub(crate) type Compute<T: Config> = StorageValue<_, FixedU64, ValueQuery>;
121
122	/// The proportion of the remaining `proof_size` to consume during `on_idle`.
123	///
124	/// `1.0` is mapped to `100%`. Must be at most [`crate::RESOURCE_HARD_LIMIT`]. Setting this to
125	/// over `1.0` could stall the chain.
126	#[pallet::storage]
127	pub(crate) type Storage<T: Config> = StorageValue<_, FixedU64, ValueQuery>;
128
129	/// The proportion of the `block length` to consume on each block.
130	///
131	/// `1.0` is mapped to `100%`. Must be at most [`crate::RESOURCE_HARD_LIMIT`]. Setting this to
132	/// over `1.0` could stall the chain.
133	#[pallet::storage]
134	pub(crate) type Length<T: Config> = StorageValue<_, FixedU64, ValueQuery>;
135
136	/// Storage map used for wasting proof size.
137	///
138	/// It contains no meaningful data - hence the name "Trash". The maximal number of entries is
139	/// set to 65k, which is just below the next jump at 16^4. This is important to reduce the proof
140	/// size benchmarking overestimate. The assumption here is that we won't have more than 65k *
141	/// 1KiB = 65MiB of proof size wasting in practice. However, this limit is not enforced, so the
142	/// pallet would also work out of the box with more entries, but its benchmarked proof weight
143	/// would possibly be underestimated in that case.
144	#[pallet::storage]
145	pub(super) type TrashData<T: Config> = StorageMap<
146		Hasher = Twox64Concat,
147		Key = u32,
148		Value = [u8; VALUE_SIZE],
149		QueryKind = OptionQuery,
150		MaxValues = ConstU32<MAX_TRASH_DATA_ENTRIES>,
151	>;
152
153	/// The current number of entries in `TrashData`.
154	#[pallet::storage]
155	pub(crate) type TrashDataCount<T: Config> = StorageValue<_, u32, ValueQuery>;
156
157	#[pallet::genesis_config]
158	#[derive(DefaultNoBound)]
159	pub struct GenesisConfig<T: Config> {
160		/// The compute limit.
161		pub compute: FixedU64,
162		/// The storage limit.
163		pub storage: FixedU64,
164		/// The amount of trash data for wasting proof size.
165		pub trash_data_count: u32,
166		/// The block length limit.
167		pub block_length: FixedU64,
168		#[serde(skip)]
169		/// The required configuration field.
170		pub _config: core::marker::PhantomData<T>,
171	}
172
173	#[pallet::genesis_build]
174	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
175		fn build(&self) {
176			assert!(
177				self.trash_data_count <= MAX_TRASH_DATA_ENTRIES,
178				"number of TrashData entries cannot be bigger than {:?}",
179				MAX_TRASH_DATA_ENTRIES
180			);
181
182			(0..self.trash_data_count)
183				.for_each(|i| TrashData::<T>::insert(i, Pallet::<T>::gen_value(i)));
184
185			TrashDataCount::<T>::set(self.trash_data_count);
186
187			assert!(self.compute <= RESOURCE_HARD_LIMIT, "Compute limit is insane");
188			<Compute<T>>::put(self.compute);
189
190			assert!(self.storage <= RESOURCE_HARD_LIMIT, "Storage limit is insane");
191			<Storage<T>>::put(self.storage);
192
193			assert!(self.block_length <= RESOURCE_HARD_LIMIT, "Block length limit is insane");
194			<Length<T>>::put(self.block_length);
195		}
196	}
197
198	#[pallet::hooks]
199	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
200		fn integrity_test() {
201			assert!(
202				!T::WeightInfo::waste_ref_time_iter(1).ref_time().is_zero(),
203				"Weight zero; would get stuck in an infinite loop"
204			);
205			assert!(
206				!T::WeightInfo::waste_proof_size_some(1).proof_size().is_zero(),
207				"Weight zero; would get stuck in an infinite loop"
208			);
209		}
210
211		fn on_idle(_: BlockNumberFor<T>, remaining_weight: Weight) -> Weight {
212			log::debug!(target: LOG_TARGET, "Running `on_idle` with remaining weight: {remaining_weight:?}");
213
214			let mut meter = WeightMeter::with_limit(remaining_weight);
215			if meter.try_consume(T::WeightInfo::empty_on_idle()).is_err() {
216				return T::WeightInfo::empty_on_idle();
217			}
218
219			let proof_size_limit =
220				Storage::<T>::get().saturating_mul_int(meter.remaining().proof_size());
221			let computation_weight_limit =
222				Compute::<T>::get().saturating_mul_int(meter.remaining().ref_time());
223
224			log::debug!(target: LOG_TARGET, "Going to waste: proof_size {proof_size_limit:?}; compute {computation_weight_limit:?}");
225
226			let mut meter = WeightMeter::with_limit(Weight::from_parts(
227				computation_weight_limit,
228				proof_size_limit,
229			));
230
231			Self::waste_at_most_proof_size(&mut meter);
232			Self::waste_at_most_ref_time(&mut meter);
233
234			meter.consumed()
235		}
236	}
237
238	#[pallet::inherent]
239	impl<T: Config> ProvideInherent for Pallet<T> {
240		type Call = Call<T>;
241		type Error = sp_inherents::MakeFatalError<()>;
242
243		const INHERENT_IDENTIFIER: InherentIdentifier = *b"bloated0";
244
245		fn create_inherent(_data: &InherentData) -> Option<Self::Call> {
246			let max_block_length = *T::BlockLength::get().max.get(DispatchClass::Mandatory);
247			let bloat_size = Length::<T>::get().saturating_mul_int(max_block_length) as usize;
248			let amount_trash = bloat_size / VALUE_SIZE;
249			let garbage = TrashData::<T>::iter()
250				.map(|(_k, v)| v)
251				.collect::<Vec<_>>()
252				.into_iter()
253				.cycle()
254				.take(amount_trash)
255				.collect::<Vec<_>>();
256
257			Some(Call::bloat { garbage })
258		}
259
260		fn is_inherent(call: &Self::Call) -> bool {
261			matches!(call, Call::bloat { .. })
262		}
263
264		fn check_inherent(call: &Self::Call, _: &InherentData) -> Result<(), Self::Error> {
265			match call {
266				Call::bloat { .. } => Ok(()),
267				_ => unreachable!("other calls are not inherents"),
268			}
269		}
270	}
271
272	#[pallet::call(weight = T::WeightInfo)]
273	impl<T: Config> Pallet<T> {
274		/// Initialize the pallet. Should be called once, if no genesis state was provided.
275		///
276		/// `current_count` is the current number of elements in `TrashData`. This can be set to
277		/// `None` when the pallet is first initialized.
278		///
279		/// Only callable by Root or `AdminOrigin`. A good default for `new_count` is `5_000`.
280		#[pallet::call_index(0)]
281		#[pallet::weight(
282			T::WeightInfo::initialize_pallet_grow(witness_count.unwrap_or_default())
283				.max(T::WeightInfo::initialize_pallet_shrink(witness_count.unwrap_or_default()))
284		)]
285		pub fn initialize_pallet(
286			origin: OriginFor<T>,
287			new_count: u32,
288			witness_count: Option<u32>,
289		) -> DispatchResult {
290			T::AdminOrigin::ensure_origin_or_root(origin)?;
291
292			let current_count = TrashDataCount::<T>::get();
293			ensure!(
294				current_count == witness_count.unwrap_or_default(),
295				Error::<T>::AlreadyInitialized
296			);
297
298			if new_count > current_count {
299				(current_count..new_count)
300					.for_each(|i| TrashData::<T>::insert(i, Self::gen_value(i)));
301			} else {
302				(new_count..current_count).for_each(TrashData::<T>::remove);
303			}
304
305			Self::deposit_event(Event::PalletInitialized { reinit: witness_count.is_some() });
306			TrashDataCount::<T>::set(new_count);
307			Ok(())
308		}
309
310		/// Set how much of the remaining `ref_time` weight should be consumed by `on_idle`.
311		///
312		/// Only callable by Root or `AdminOrigin`.
313		#[pallet::call_index(1)]
314		pub fn set_compute(origin: OriginFor<T>, compute: FixedU64) -> DispatchResult {
315			T::AdminOrigin::ensure_origin_or_root(origin)?;
316
317			ensure!(compute <= RESOURCE_HARD_LIMIT, Error::<T>::InsaneLimit);
318			Compute::<T>::set(compute);
319
320			Self::deposit_event(Event::ComputationLimitSet { compute });
321			Ok(())
322		}
323
324		/// Set how much of the remaining `proof_size` weight should be consumed by `on_idle`.
325		///
326		/// `1.0` means that all remaining `proof_size` will be consumed. The PoV benchmarking
327		/// results that are used here are likely an over-estimation. 100% intended consumption will
328		/// therefore translate to less than 100% actual consumption.
329		///
330		/// Only callable by Root or `AdminOrigin`.
331		#[pallet::call_index(2)]
332		pub fn set_storage(origin: OriginFor<T>, storage: FixedU64) -> DispatchResult {
333			T::AdminOrigin::ensure_origin_or_root(origin)?;
334
335			ensure!(storage <= RESOURCE_HARD_LIMIT, Error::<T>::InsaneLimit);
336			Storage::<T>::set(storage);
337
338			Self::deposit_event(Event::StorageLimitSet { storage });
339			Ok(())
340		}
341
342		/// Increase the block size by including the specified garbage bytes.
343		#[pallet::call_index(3)]
344		#[pallet::weight((0, DispatchClass::Mandatory))]
345		pub fn bloat(_origin: OriginFor<T>, _garbage: Vec<[u8; VALUE_SIZE]>) -> DispatchResult {
346			Ok(())
347		}
348
349		/// Set how much of the block length should be filled with trash data on each block.
350		///
351		/// `1.0` means that all block should be filled. If set to `1.0`, storage proof size will
352		///  be close to zero.
353		///
354		/// Only callable by Root or `AdminOrigin`.
355		#[pallet::call_index(4)]
356		#[pallet::weight({1})]
357		pub fn set_block_length(origin: OriginFor<T>, block_length: FixedU64) -> DispatchResult {
358			T::AdminOrigin::ensure_origin_or_root(origin)?;
359
360			ensure!(block_length <= RESOURCE_HARD_LIMIT, Error::<T>::InsaneLimit);
361			Length::<T>::set(block_length);
362
363			Self::deposit_event(Event::BlockLengthLimitSet { block_length });
364			Ok(())
365		}
366	}
367
368	impl<T: Config> Pallet<T> {
369		/// Waste at most the remaining proof size of `meter`.
370		///
371		/// Tries to come as close to the limit as possible.
372		pub(crate) fn waste_at_most_proof_size(meter: &mut WeightMeter) {
373			let Ok(n) = Self::calculate_proof_size_iters(&meter) else { return };
374
375			meter.consume(T::WeightInfo::waste_proof_size_some(n));
376
377			(0..n).for_each(|i| {
378				TrashData::<T>::get(i);
379			});
380		}
381
382		/// Calculate how many times `waste_proof_size_some` should be called to fill up `meter`.
383		fn calculate_proof_size_iters(meter: &WeightMeter) -> Result<u32, ()> {
384			let base = T::WeightInfo::waste_proof_size_some(0);
385			let slope = T::WeightInfo::waste_proof_size_some(1).saturating_sub(base);
386
387			let remaining = meter.remaining().saturating_sub(base);
388			let iter_by_proof_size =
389				remaining.proof_size().checked_div(slope.proof_size()).ok_or(())?;
390			let iter_by_ref_time = remaining.ref_time().checked_div(slope.ref_time()).ok_or(())?;
391
392			if iter_by_proof_size > 0 && iter_by_proof_size <= iter_by_ref_time {
393				Ok(iter_by_proof_size as u32)
394			} else {
395				Err(())
396			}
397		}
398
399		/// Waste at most the remaining ref time weight of `meter`.
400		///
401		/// Tries to come as close to the limit as possible.
402		pub(crate) fn waste_at_most_ref_time(meter: &mut WeightMeter) {
403			let Ok(n) = Self::calculate_ref_time_iters(&meter) else { return };
404			meter.consume(T::WeightInfo::waste_ref_time_iter(n));
405
406			let clobber = Self::waste_ref_time_iter(vec![0u8; 64], n);
407
408			// By casting it into a vec we can hopefully prevent the compiler from optimizing it
409			// out. Note that `Blake2b512` produces 64 bytes, this is therefore impossible - but the
410			// compiler does not know that (hopefully).
411			debug_assert!(clobber.len() == 64);
412			if clobber.len() == 65 {
413				TrashData::<T>::insert(0, [clobber[0] as u8; VALUE_SIZE]);
414			}
415		}
416
417		/// Wastes some `ref_time`. Receives the previous result as an argument.
418		///
419		/// The ref_time of one iteration should be in the order of 1-10 ms.
420		pub(crate) fn waste_ref_time_iter(clobber: Vec<u8>, i: u32) -> Vec<u8> {
421			let mut hasher = Blake2b512::new();
422
423			// Blake2 has a very high speed of hashing so we make multiple hashes with it to
424			// waste more `ref_time` at once.
425			(0..i).for_each(|_| {
426				hasher.update(clobber.as_slice());
427			});
428
429			hasher.finalize().to_vec()
430		}
431
432		/// Calculate how many times `waste_ref_time_iter` should be called to fill up `meter`.
433		fn calculate_ref_time_iters(meter: &WeightMeter) -> Result<u32, ()> {
434			let base = T::WeightInfo::waste_ref_time_iter(0);
435			let slope = T::WeightInfo::waste_ref_time_iter(1).saturating_sub(base);
436			if !slope.proof_size().is_zero() || !base.proof_size().is_zero() {
437				return Err(());
438			}
439
440			match meter
441				.remaining()
442				.ref_time()
443				.saturating_sub(base.ref_time())
444				.checked_div(slope.ref_time())
445			{
446				Some(0) | None => Err(()),
447				Some(i) => Ok(i as u32),
448			}
449		}
450
451		/// Generate a pseudo-random deterministic value from a `seed`.
452		pub(crate) fn gen_value(seed: u32) -> [u8; VALUE_SIZE] {
453			let mut ret = [0u8; VALUE_SIZE];
454
455			for i in 0u32..(VALUE_SIZE as u32 / 32) {
456				let hash = (seed, i).using_encoded(twox_256);
457				ret[i as usize * 32..(i + 1) as usize * 32].copy_from_slice(&hash);
458			}
459
460			ret
461		}
462	}
463}