pallet_block_production_log/lib.rs
1//! A Substrate pallet maintaining a consumable log of block production information.
2//!
3//! ## Purpose of this pallet
4//!
5//! This pallet keeps a log containing block producer IDs along with times of blocks produced by them.
6//! This log is updated every block and is meant to consumed by other features.
7//! The intended use of this pallet within the Partner Chains SDK is to expose block production data for consumption
8//! by the Block Participation feature implemented by the `sp_block_participation` and `pallet_block_participation`
9//! crates.
10//!
11//! ## Usage - PC Builder
12//!
13//! This pallet requires inherent data provided by the inherent data provider defined by `sp_block_production_log`
14//! crate. Consult the crate's documentation for instruction on how to wire it into the node correctly.
15//!
16//! ### Adding to the runtime
17//!
18//! The pallet requires a minimal configuration. Consult the documentation for [pallet::Config] for details.
19//!
20//! An example configuration for a runtime using Aura consensus might look like this:
21//!
22//! ```rust,ignore
23//! impl pallet_block_production_log::Config for Runtime {
24//! type BlockProducerId = BlockAuthor;
25//! type WeightInfo = pallet_block_production_log::weights::SubstrateWeight<Runtime>;
26//!
27//! type Moment = u64;
28//!
29//! #[cfg(feature = "runtime-benchmarks")]
30//! type BenchmarkHelper = PalletBlockProductionLogBenchmarkHelper;
31//! }
32//! ```
33//!
34//! #### Defining block producer ID
35//!
36//! The pallet expects the Partner Chain to provide a type representing its block producers.
37//! This type can be as simple as an Aura public key but can also be a more complex type if block producers
38//! are not a homogenous group. For example, in the context of a Partner Chain using Ariadne committee selection,
39//! it's typical to have two kinds of block producers: permissioned producers provided by the governance authority
40//! and registered candidates recruited from among Cardano stake pool operators. In this instance an example
41//! author type could be:
42//! ```rust
43//! use sidechain_domain::*;
44//!
45//! pub enum BlockAuthor {
46//! Incentivized(CrossChainPublicKey, StakePoolPublicKey),
47//! ProBono(CrossChainPublicKey),
48//! }
49//! ```
50//!
51//! Keep in mind that other Partner Chains SDK components put their own constraints on the block author type
52//! that need to be adhered to for a Partner Chain to integrated them.
53//!
54//! #### Support for adding to a running chain
55//!
56//! The pallet and its inherent data provider defined in [sp_block_production_log] are written in a way that allows for
57//! adding it to an already live chain. The pallet allows for an initial period where inherent data is unavailable
58//! and considers its inherent extrinsic required only after the first block where inherent data is provided.
59//! Conversely, the inherent data provider is active only when the pallet and its runtime API is present for it to call.
60//!
61//! ### Consuming the log
62//!
63//! **Important**: Consuming the log is a destructive operation. Multiple features should not consume the log data
64//! unless they are coordinated to read and clear the same log prefix.
65//!
66//! The pallet exposes three functions that allow other pallets to consume its data: `take_prefix`, `peek_prefix`
67//! and `drop_prefix`. Any feature using the log should be able to identify the time up to which it should
68//! process the log data and either:
69//! - call `take_prefix` from some pallet's logic and process the returned data within the same block
70//! - call `peek_prefix` inside an inherent data provider and use `drop_prefix` from the corresponding pallet
71//! to clear the previously peeked data within the same block
72//!
73//! It is critically important to drop exactly the prefix processed to avoid either skipping or double-counting some blocks.
74//!
75//! ## Usage - PC user
76//!
77//! This pallet does not expose any user-facing functionalities.
78//!
79
80#![cfg_attr(not(feature = "std"), no_std)]
81#![deny(missing_docs)]
82
83pub mod benchmarking;
84pub mod weights;
85
86#[cfg(test)]
87mod mock;
88
89#[cfg(test)]
90mod test;
91
92pub use pallet::*;
93pub use weights::WeightInfo;
94
95#[frame_support::pallet]
96pub mod pallet {
97 use super::*;
98 use frame_support::pallet_prelude::*;
99 use frame_system::pallet_prelude::*;
100 use sp_block_production_log::*;
101 use sp_runtime::traits::Member;
102 use sp_std::vec::Vec;
103
104 #[pallet::pallet]
105 pub struct Pallet<T>(_);
106
107 #[pallet::config]
108 pub trait Config: frame_system::Config {
109 /// ID type that can represent any block producer in the network.
110 /// This type should be defined by the Partner Chain Depending on its consensus mechanism and possible block producer types.
111 type BlockProducerId: Member + Parameter + MaxEncodedLen;
112
113 /// Weight information on extrinsic in the pallet. For convenience weights in [weights] module can be used.
114 type WeightInfo: WeightInfo;
115
116 /// Type used to identify the moment in time when the block was produced, eg. a timestamp or slot number.
117 type Moment: Member + Parameter + MaxEncodedLen + PartialOrd + Ord + PartialEq + Eq;
118
119 #[cfg(feature = "runtime-benchmarks")]
120 /// Benchmark helper type used for running benchmarks
121 type BenchmarkHelper: benchmarking::BenchmarkHelper<Self::BlockProducerId>;
122 }
123
124 #[pallet::storage]
125 #[pallet::unbounded]
126 pub type Log<T: Config> = StorageValue<_, Vec<(T::Moment, T::BlockProducerId)>, ValueQuery>;
127
128 /// Temporary storage of the current block's producer, to be appended to the log on block finalization.
129 #[pallet::storage]
130 pub type CurrentProducer<T: Config> =
131 StorageValue<_, (T::Moment, T::BlockProducerId), OptionQuery>;
132
133 /// This storage is used to prevent calling `append` multiple times for the same block or for past blocks.
134 #[pallet::storage]
135 pub type LatestBlock<T: Config> = StorageValue<_, BlockNumberFor<T>, OptionQuery>;
136
137 #[pallet::inherent]
138 impl<T: Config> ProvideInherent for Pallet<T> {
139 type Call = Call<T>;
140 type Error = InherentError;
141 const INHERENT_IDENTIFIER: InherentIdentifier = INHERENT_IDENTIFIER;
142
143 fn create_inherent(data: &InherentData) -> Option<Self::Call> {
144 Self::decode_inherent_data(data).unwrap().map(|data| Call::append {
145 moment: data.moment,
146 block_producer_id: data.block_producer_id,
147 })
148 }
149
150 fn is_inherent(call: &Self::Call) -> bool {
151 matches!(call, Call::append { .. })
152 }
153
154 fn is_inherent_required(data: &InherentData) -> Result<Option<Self::Error>, Self::Error> {
155 let has_data = Self::decode_inherent_data(data)?.is_some();
156 if has_data || LatestBlock::<T>::get().is_some() {
157 Ok(Some(Self::Error::InherentRequired))
158 } else {
159 Ok(None)
160 }
161 }
162 }
163
164 #[pallet::error]
165 pub enum Error<T> {
166 /// Call is not allowed, because the log has been already written for a block with same or higher number.
167 BlockNumberNotIncreased,
168 }
169
170 #[pallet::call]
171 impl<T: Config> Pallet<T> {
172 /// Schedules an entry to be appended to the log. Log has to be ordered by a moment and writing the same moment twice is forbidden.
173 #[pallet::call_index(0)]
174 #[pallet::weight((T::WeightInfo::append(), DispatchClass::Mandatory))]
175 pub fn append(
176 origin: OriginFor<T>,
177 moment: T::Moment,
178 block_producer_id: T::BlockProducerId,
179 ) -> DispatchResult {
180 ensure_none(origin)?;
181
182 let current_block = <frame_system::Pallet<T>>::block_number();
183 match LatestBlock::<T>::get() {
184 Some(b) if b >= current_block => Err(Error::<T>::BlockNumberNotIncreased),
185 _ => Ok(()),
186 }?;
187 LatestBlock::<T>::put(current_block);
188
189 Ok(CurrentProducer::<T>::put((moment, block_producer_id)))
190 }
191 }
192
193 #[pallet::hooks]
194 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
195 /// A dummy `on_initialize` to return the amount of weight that `on_finalize` requires to
196 /// execute.
197 fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
198 T::WeightInfo::on_finalize()
199 }
200
201 fn on_finalize(block: BlockNumberFor<T>) {
202 if let Some((moment, block_producer_id)) = CurrentProducer::<T>::take() {
203 log::info!("👷 Block {block:?} producer is {block_producer_id:?}");
204 Log::<T>::append((moment, block_producer_id));
205 } else {
206 log::warn!(
207 "👷 Block {block:?} producer not set. This should occur only at the beginning of the production log pallet's lifetime."
208 )
209 }
210 }
211 }
212
213 impl<T: Config> Pallet<T> {
214 fn decode_inherent_data(
215 data: &InherentData,
216 ) -> Result<
217 Option<BlockProductionInherentDataV1<T::Moment, T::BlockProducerId>>,
218 InherentError,
219 > {
220 data.get_data::<BlockProductionInherentDataV1<T::Moment, T::BlockProducerId>>(
221 &Self::INHERENT_IDENTIFIER,
222 )
223 .map_err(|_| InherentError::InvalidInherentData)
224 }
225
226 /// Returns all entries up to `moment` (inclusive) and removes them from the log
227 pub fn take_prefix(moment: &T::Moment) -> Vec<(T::Moment, T::BlockProducerId)> {
228 let removed_prefix = Log::<T>::mutate(|log| {
229 let pos = log.partition_point(|(s, _)| s <= moment);
230 log.drain(..pos).collect()
231 });
232 removed_prefix
233 }
234
235 /// Returns all entries up to `moment` (inclusive) from the log
236 pub fn peek_prefix(
237 moment: &T::Moment,
238 ) -> impl Iterator<Item = (T::Moment, T::BlockProducerId)> {
239 Log::<T>::get().into_iter().take_while(move |(s, _)| s <= moment)
240 }
241
242 /// Removes all entries up to `moment` (inclusive) from the log
243 pub fn drop_prefix(moment: &T::Moment) {
244 Log::<T>::mutate(|log| {
245 let position = log.partition_point(|(s, _)| s <= moment);
246 log.drain(..position);
247 });
248 }
249 }
250}
251
252#[cfg(feature = "block-participation")]
253mod block_participation {
254 use pallet_block_participation::BlockParticipationProvider;
255
256 impl<T: crate::Config> BlockParticipationProvider<T::Moment, T::BlockProducerId>
257 for crate::Pallet<T>
258 {
259 fn blocks_to_process(
260 moment: &T::Moment,
261 ) -> impl Iterator<Item = (T::Moment, T::BlockProducerId)> {
262 Self::peek_prefix(moment)
263 }
264
265 fn discard_processed_blocks(moment: &T::Moment) {
266 Self::drop_prefix(moment)
267 }
268 }
269}