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}