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 slot numbers 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//!     fn current_slot() -> sp_consensus_slots::Slot {
28//!         let slot: u64 = pallet_aura::CurrentSlot::<Runtime>::get().into();
29//!         sp_consensus_slots::Slot::from(slot)
30//!     }
31//!
32//!     #[cfg(feature = "runtime-benchmarks")]
33//!     type BenchmarkHelper = PalletBlockProductionLogBenchmarkHelper;
34//! }
35//! ```
36//!
37//! #### Defining block producer ID
38//!
39//! The pallet expects the Partner Chain to provide a type representing its block producers.
40//! This type can be as simple as an Aura public key but can also be a more complex type if block producers
41//! are not a homogenous group. For example, in the context of a Partner Chain using Ariadne committee selection,
42//! it's typical to have two kinds of block producers: permissioned producers provided by the governance authority
43//! and registered candidates recruited from among Cardano stake pool operators. In this instance an example
44//! author type could be:
45//! ```rust
46//! use sidechain_domain::*;
47//!
48//! pub enum BlockAuthor {
49//!     Incentivized(CrossChainPublicKey, StakePoolPublicKey),
50//!     ProBono(CrossChainPublicKey),
51//! }
52//! ```
53//!
54//! Keep in mind that other Partner Chains SDK components put their own constraints on the block author type
55//! that need to be adhered to for a Partner Chain to integrated them.
56//!
57//! #### Support for adding to a running chain
58//!
59//! The pallet and its inherent data provider defined in [sp_block_production_log] are written in a way that allows for
60//! adding it to an already live chain. The pallet allows for an initial period where inherent data is unavailable
61//! and considers its inherent extrinsic required only after the first block where inherent data is provided.
62//! Conversely, the inherent data provider is active only when the pallet and its runtime API is present for it to call.
63//!
64//! ### Consuming the log
65//!
66//! **Important**: Consuming the log is a destructive operation. Multiple features should not consume the log data
67//!                unless they are coordinated to read and clear the same log prefix.
68//!
69//! The pallet exposes three functions that allow other pallets to consume its data: `take_prefix`, `peek_prefix`
70//! and `drop_prefix`. Any feature using the log should be able to identify the slot number up to which it should
71//! process the log data and either:
72//! - call `take_prefix` from some pallet's logic and process the returned data within the same block
73//! - call `peek_prefix` inside an inherent data provider and use `drop_prefix` from the corresponding pallet
74//!   to clear the previously peeked data within the same block
75//!
76//! It is critically important to drop exactly the prefix processed to avoid either skipping or double-counting some blocks.
77//!
78//! ## Usage - PC user
79//!
80//! This pallet does not expose any user-facing functionalities.
81//!
82
83#![cfg_attr(not(feature = "std"), no_std)]
84#![deny(missing_docs)]
85
86pub mod benchmarking;
87pub mod weights;
88
89#[cfg(test)]
90mod mock;
91
92#[cfg(test)]
93mod test;
94
95pub use pallet::*;
96pub use weights::WeightInfo;
97
98#[frame_support::pallet]
99pub mod pallet {
100	use super::*;
101	use frame_support::pallet_prelude::*;
102	use frame_system::pallet_prelude::*;
103	use sp_block_production_log::*;
104	use sp_consensus_slots::Slot;
105	use sp_std::vec::Vec;
106
107	#[pallet::pallet]
108	pub struct Pallet<T>(_);
109
110	#[pallet::config]
111	pub trait Config: frame_system::Config {
112		/// ID type that can represent any block producer in the network.
113		/// This type should be defined by the Partner Chain Depending on its consensus mechanism and possible block producer types.
114		type BlockProducerId: Member + Parameter + MaxEncodedLen;
115
116		/// Weight information on extrinsic in the pallet. For convenience weights in [weights] module can be used.
117		type WeightInfo: WeightInfo;
118
119		/// The slot number of current block.
120		fn current_slot() -> Slot;
121
122		#[cfg(feature = "runtime-benchmarks")]
123		/// Benchmark helper type used for running benchmarks
124		type BenchmarkHelper: benchmarking::BenchmarkHelper<Self::BlockProducerId>;
125	}
126
127	#[pallet::storage]
128	#[pallet::unbounded]
129	pub type Log<T: Config> = StorageValue<_, Vec<(Slot, T::BlockProducerId)>, ValueQuery>;
130
131	/// Temporary storage of the current block's producer, to be appended to the log on block finalization.
132	#[pallet::storage]
133	pub type CurrentProducer<T: Config> = StorageValue<_, T::BlockProducerId, OptionQuery>;
134
135	/// This storage is used to prevent calling `append` multiple times for the same block or for past blocks.
136	#[pallet::storage]
137	pub type LatestBlock<T: Config> = StorageValue<_, BlockNumberFor<T>, OptionQuery>;
138
139	#[pallet::inherent]
140	impl<T: Config> ProvideInherent for Pallet<T> {
141		type Call = Call<T>;
142		type Error = InherentError;
143		const INHERENT_IDENTIFIER: InherentIdentifier = INHERENT_IDENTIFIER;
144
145		fn create_inherent(data: &InherentData) -> Option<Self::Call> {
146			Self::decode_inherent_data(data)
147				.unwrap()
148				.map(|block_producer_id| Call::append { block_producer_id })
149		}
150
151		fn is_inherent(call: &Self::Call) -> bool {
152			matches!(call, Call::append { .. })
153		}
154
155		fn is_inherent_required(data: &InherentData) -> Result<Option<Self::Error>, Self::Error> {
156			let has_data = Self::decode_inherent_data(data)?.is_some();
157			if has_data || LatestBlock::<T>::get().is_some() {
158				Ok(Some(Self::Error::InherentRequired))
159			} else {
160				Ok(None)
161			}
162		}
163	}
164
165	#[pallet::error]
166	pub enum Error<T> {
167		/// Call is not allowed, because the log has been already written for a block with same or higher number.
168		BlockNumberNotIncreased,
169	}
170
171	#[pallet::call]
172	impl<T: Config> Pallet<T> {
173		/// Schedules an entry to be appended to the log. Log has to be ordered by slots and writing the same slot twice is forbidden.
174		#[pallet::call_index(0)]
175		#[pallet::weight((T::WeightInfo::append(), DispatchClass::Mandatory))]
176		pub fn append(
177			origin: OriginFor<T>,
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(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(block_producer_id) = CurrentProducer::<T>::take() {
203				log::info!("👷 Block {block:?} producer is {block_producer_id:?}");
204				Log::<T>::append((T::current_slot(), 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<Option<T::BlockProducerId>, InherentError> {
217			data.get_data::<T::BlockProducerId>(&Self::INHERENT_IDENTIFIER)
218				.map_err(|_| InherentError::InvalidInherentData)
219		}
220
221		/// Returns all entries up to `slot` (inclusive) and removes them from the log
222		pub fn take_prefix(slot: &Slot) -> Vec<(Slot, T::BlockProducerId)> {
223			let removed_prefix = Log::<T>::mutate(|log| {
224				let pos = log.partition_point(|(s, _)| s <= slot);
225				log.drain(..pos).collect()
226			});
227			removed_prefix
228		}
229
230		/// Returns all entries up to `slot` (inclusive) from the log
231		pub fn peek_prefix(slot: Slot) -> impl Iterator<Item = (Slot, T::BlockProducerId)> {
232			Log::<T>::get().into_iter().take_while(move |(s, _)| s <= &slot)
233		}
234
235		/// Removes all entries up to `slot` (inclusive) from the log
236		pub fn drop_prefix(slot: &Slot) {
237			Log::<T>::mutate(|log| {
238				let position = log.partition_point(|(s, _)| s <= slot);
239				log.drain(..position);
240			});
241		}
242	}
243}