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}