pallet_block_participation/
lib.rs

1//! Pallet providing configuration and supporting runtime logic for the block participation data feature of Partner Chains SDK.
2//!
3//! ## Purpose of this pallet
4//!
5//! This pallet provides the runtime-side logic supporting the block participation data feature of PC SDK.
6//! Unlike most pallets, this one is not meant to be interacted with either by the chain's users or other
7//! runtime components in the system. Instead, it only serves two purposes:
8//! - it provides all configuration required by the feature's inherent data provider defined in the primitives crate
9//! - it provides an inhrenent extrinsic that removes from the runtime storage data that has been already
10//!   processed by the inherent data provider
11//! The reason for that is that the feature's purpose is to produce inherent data containing block participation
12//! data for consumption by custom-written pallet provided by each Partner Chain itself.
13//!
14//! The pallet is expected to be used together with the `pallet_block_production_log` when deployed in the
15//! context of Partner Chains SDK.
16//!
17//! ## Usage
18//!
19//! ### Adding into the runtime
20//!
21//! The pallet's configuration can be divided into three groups by purpose:
22//! - `BlockAuthor` and `DelegatorId` types representing block authors and their dependant block beneficiaries
23//! - `should_release_data` function that controls when the inherent data provider is active
24//! - `blocks_produced_up_to_slot` and `blocks_produced_upd_to_slot` functions that provide bindings for consuming
25//!   (reading and clearing) block production data. Most easily these should come from `pallet_block_production_log`.
26//!
27//! Consult documentation of [pallet::Config] for details on each configuration field.
28//!
29//! Assuming that the runtime also contains the `pallet_block_production_log`, an example configuration of
30//! the pallet might look like the following:
31//! ```rust,ignore
32//! const RELEASE_PERIOD: u64 = 128;
33//!
34//! impl pallet_block_participation::Config for Runtime {
35//!     type WeightInfo = pallet_block_participation::weights::SubstrateWeight<Runtime>;
36//!     type BlockAuthor = BlockAuthor;
37//!     type DelegatorId = DelegatorKey;
38//!
39//!     // release data every `RELEASE_PERIOD` blocks, up to current slot
40//!     fn should_release_data(slot: sidechain_slots::Slot) -> Option<sidechain_slots::Slot> {
41//!         if System::block_number() % RELEASE_PERIOD == 0 {
42//!             Some(slot)
43//!         } else {
44//!             None
45//!         }
46//!     }
47//!
48//!     fn blocks_produced_up_to_slot(slot: Slot) -> impl Iterator<Item = (Slot, BlockAuthor)> {
49//!         BlockProductionLog::peek_prefix(slot)
50//!     }
51//!
52//!     fn discard_blocks_produced_up_to_slot(slot: Slot) {
53//!         BlockProductionLog::drop_prefix(&slot)
54//!     }
55//!
56//!     const TARGET_INHERENT_ID: InherentIdentifier = *b"_example";
57//! }
58//! ```
59#![cfg_attr(not(feature = "std"), no_std)]
60#![deny(missing_docs)]
61
62#[cfg(feature = "runtime-benchmarks")]
63mod benchmarking;
64#[cfg(test)]
65mod mock;
66#[cfg(test)]
67mod tests;
68pub mod weights;
69
70use frame_support::pallet_prelude::*;
71pub use pallet::*;
72use sp_block_participation::*;
73
74#[frame_support::pallet]
75pub mod pallet {
76	use super::*;
77	use frame_system::pallet_prelude::*;
78
79	#[pallet::pallet]
80	pub struct Pallet<T>(_);
81
82	#[pallet::config]
83	pub trait Config: frame_system::Config {
84		/// Weight info for this pallet's extrinsics
85		type WeightInfo: crate::weights::WeightInfo;
86
87		/// Type identifying the producer of a block on the Partner Chain
88		type BlockAuthor: Member + Parameter + MaxEncodedLen;
89
90		/// Type identifying indirect block production participants on the Partner Chain
91		/// This can be native stakers on Partner Chain, stakers on the main chain or other.
92		type DelegatorId: Member + Parameter + MaxEncodedLen;
93
94		/// Should return slot up to which block production data should be released or None.
95		fn should_release_data(slot: Slot) -> Option<Slot>;
96
97		/// Returns block authors since last processing up to `slot`
98		fn blocks_produced_up_to_slot(
99			slot: Slot,
100		) -> impl Iterator<Item = (Slot, Self::BlockAuthor)>;
101
102		/// Discards block production data at the source up to slot
103		/// This should remove exactly the same data as returned by `blocks_produced_up_to_slot`
104		fn discard_blocks_produced_up_to_slot(slot: Slot);
105
106		/// Inherent ID under which block participation data should be provided.
107		/// It should be set to the ID used by the pallet that will process participation data for
108		/// paying out block rewards or other purposes.
109		const TARGET_INHERENT_ID: InherentIdentifier;
110	}
111
112	#[pallet::error]
113	pub enum Error<T> {
114		/// Indicates an attempt to process block participation data for already processed slot
115		UpToSlotNotIncreased,
116	}
117
118	/// Stores the slot number up to which block participation has already been processed
119	#[pallet::storage]
120	pub type ProcessedUpToSlot<T: Config> = StorageValue<_, Slot, ValueQuery>;
121
122	#[pallet::inherent]
123	impl<T: Config> ProvideInherent for Pallet<T> {
124		type Call = Call<T>;
125		type Error = sp_block_participation::InherentError;
126		const INHERENT_IDENTIFIER: InherentIdentifier = sp_block_participation::INHERENT_IDENTIFIER;
127
128		fn create_inherent(data: &InherentData) -> Option<Self::Call> {
129			// we unwrap here because we can't continue proposing a block if inherent data is invalid for some reason
130			let up_to_slot = Self::decode_inherent_data(data).unwrap()?;
131
132			Some(Call::note_processing { up_to_slot })
133		}
134
135		fn check_inherent(call: &Self::Call, data: &InherentData) -> Result<(), Self::Error> {
136			let Some(expected_inherent_data) = Self::decode_inherent_data(data)? else {
137				return Err(Self::Error::UnexpectedInherent);
138			};
139
140			let Self::Call::note_processing { up_to_slot } = call else {
141				unreachable!("There should be no other extrinsic in the pallet")
142			};
143
144			ensure!(*up_to_slot == expected_inherent_data, Self::Error::IncorrectSlotBoundary);
145
146			Ok(())
147		}
148
149		fn is_inherent(call: &Self::Call) -> bool {
150			matches!(call, Call::note_processing { .. })
151		}
152
153		fn is_inherent_required(data: &InherentData) -> Result<Option<Self::Error>, Self::Error> {
154			if Self::decode_inherent_data(data)?.is_some() {
155				Ok(Some(Self::Error::InherentRequired))
156			} else {
157				Ok(None)
158			}
159		}
160	}
161
162	impl<T: Config> Pallet<T> {
163		fn decode_inherent_data(data: &InherentData) -> Result<Option<Slot>, InherentError> {
164			data.get_data(&Self::INHERENT_IDENTIFIER)
165				.map_err(|_| InherentError::InvalidInherentData)
166		}
167	}
168
169	#[pallet::call]
170	impl<T: Config> Pallet<T> {
171		/// Registers the fact that block participation data has been released for processing
172		/// and removes the handled data from block production log.
173		///
174		/// This inherent does not by itself process any data and only serves an operational function
175		/// by cleaning up data that has been already processed by other components.
176		///
177		/// # Arguments
178		/// - `up_to_slot`: inclusive upper bound for processed data to be cleaned. This inherent saves
179		///                 the value of `up_to_slot` in the pallet's storage and expects it to increase
180		///                 on each invocation.
181		#[pallet::call_index(0)]
182		#[pallet::weight((0, DispatchClass::Mandatory))]
183		pub fn note_processing(origin: OriginFor<T>, up_to_slot: Slot) -> DispatchResult {
184			ensure_none(origin)?;
185			if up_to_slot <= ProcessedUpToSlot::<T>::get() {
186				return Err(Error::<T>::UpToSlotNotIncreased.into());
187			}
188			log::info!("🧾 Processing block participation data up to slot {}.", *up_to_slot);
189			T::discard_blocks_produced_up_to_slot(up_to_slot);
190			ProcessedUpToSlot::<T>::put(up_to_slot);
191			Ok(())
192		}
193	}
194
195	impl<T: Config> Pallet<T> {
196		/// Returns slot up to which block production data should be released or [None].
197		pub fn should_release_data(slot: Slot) -> Option<Slot> {
198			<T as Config>::should_release_data(slot)
199		}
200	}
201}