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}