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//! ### Adding to the runtime
14//!
15//! The feature requires two types to be defind by the chain builders in their code:
16//! - `BlockProducerId`: the type representing the block author
17//! - `Moment`: a moment in time when the block was produced, which carries enough information to
18//! calculate the block's author. Typcally, this type can be a timestamp, or a slot,
19//! depending on the consensus mechanism used, but can be a richer type if needed.
20//!
21//! In addition, implementations of [GetAuthor] and [GetMoment] must be provided that can be used to
22//! retrieve the current block's author and moment when it was produced.
23//!
24//! An example configuration for a runtime using Aura consensus and Partner Chain toolkit's session management
25//! pallet might look like this:
26//!
27//! ```rust,ignore
28//! impl pallet_block_production_log::Config for Runtime {
29//! type BlockProducerId = BlockAuthor;
30//!
31//! type Moment = Slot;
32//!
33//! type GetMoment = FromStorage<pallet_aura::CurrentSlot<Runtime>>;
34//! type GetAuthor = FromFindAuthorIndex<Runtime, Aura, u32>;
35//! }
36//! ```
37//!
38//! #### Defining block producer ID
39//!
40//! The pallet expects the Partner Chain to provide a type representing its block producers.
41//! This type can be as simple as an Aura public key but can also be a more complex type if block producers
42//! are not a homogenous group. For example, in the context of a Partner Chain using Ariadne committee selection,
43//! it's typical to have two kinds of block producers: permissioned producers provided by the governance authority
44//! and registered candidates recruited from among Cardano stake pool operators. In this instance an example
45//! author type could be:
46//! ```rust
47//! use sidechain_domain::*;
48//!
49//! pub enum BlockAuthor {
50//! Incentivized(CrossChainPublicKey, StakePoolPublicKey),
51//! ProBono(CrossChainPublicKey),
52//! }
53//! ```
54//!
55//! Keep in mind that other Partner Chains SDK components put their own constraints on the block author type
56//! that need to be adhered to for a Partner Chain to integrated them.
57//!
58//! #### Defining moment type
59//!
60//! The pallet abstracts away the notion of time when a block was produced and allows the chain builders to
61//! configure it according to their chain's needs by providing a `Moment` type. This type can be a timestamp,
62//! a slot or round number, depending on the consensus mechanism used.
63//!
64//! #### Support for adding to a running chain
65//!
66//! The pallet is written in a way that allows for adding it to an already live chain.
67//!
68//! ### Consuming the log
69//!
70//! **Important**: Consuming the log is a destructive operation. Multiple features should not consume the log data
71//! unless they are coordinated to read and clear the same log prefix.
72//!
73//! The pallet exposes three functions that allow other pallets to consume its data: `take_prefix`, `peek_prefix`
74//! and `drop_prefix`. Any feature using the log should be able to identify the time up to which it should
75//! process the log data and either:
76//! - call `take_prefix` from some pallet's logic and process the returned data within the same block
77//! - call `peek_prefix` inside an inherent data provider and use `drop_prefix` from the corresponding pallet
78//! to clear the previously peeked data within the same block
79//!
80//! It is critically important to drop exactly the prefix processed to avoid either skipping or double-counting some blocks.
81//!
82//! ## Usage - PC user
83//!
84//! This pallet does not expose any user-facing functionalities.
85//!
86
87#![cfg_attr(not(feature = "std"), no_std)]
88#![deny(missing_docs)]
89
90pub mod weights;
91
92#[cfg(test)]
93mod mock;
94
95#[cfg(test)]
96mod test;
97
98use core::marker::PhantomData;
99
100pub use pallet::*;
101pub use weights::WeightInfo;
102
103/// Source of the current block's author
104pub trait GetAuthor<BlockProducerId> {
105 /// Returns the current block's author
106 fn get_author() -> Option<BlockProducerId>;
107}
108
109/// [GetAuthor] implementation that uses a [FindAuthor] instance to get the current block's author index
110/// of type `I` and uses it to read the author from `pallet_session_validator_management`.
111pub struct FromFindAuthorIndex<T, FA, I>(PhantomData<(T, FA, I)>);
112
113/// Source of the current block's moment
114pub trait GetMoment<Moment> {
115 /// Returns the current block's moment
116 fn get_moment() -> Option<Moment>;
117}
118
119/// [GetMoment] implementation that fetches current block's `Moment` from storage `S`
120pub struct FromStorage<S>(PhantomData<S>);
121
122#[frame_support::pallet]
123pub mod pallet {
124 use super::*;
125 use frame_support::pallet_prelude::*;
126 use frame_system::pallet_prelude::*;
127 use sp_runtime::traits::Member;
128 use sp_std::vec::Vec;
129
130 #[pallet::pallet]
131 pub struct Pallet<T>(_);
132
133 #[pallet::config]
134 pub trait Config: frame_system::Config {
135 /// ID type that can represent any block producer in the network.
136 /// This type should be defined by the Partner Chain Depending on its consensus mechanism and possible block producer types.
137 type BlockProducerId: Member + Parameter + MaxEncodedLen;
138
139 /// Type used to identify the moment in time when the block was produced, eg. a timestamp or slot number.
140 type Moment: Member + Parameter + MaxEncodedLen + PartialOrd + Ord + PartialEq + Eq;
141
142 /// Source of current block's author
143 type GetAuthor: GetAuthor<Self::BlockProducerId>;
144
145 /// Source of current block's moment
146 type GetMoment: GetMoment<Self::Moment>;
147 }
148
149 #[pallet::storage]
150 #[pallet::unbounded]
151 pub type Log<T: Config> = StorageValue<_, Vec<(T::Moment, T::BlockProducerId)>, ValueQuery>;
152
153 #[pallet::hooks]
154 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
155 /// Block initialization hook that adds current block's author to the log
156 fn on_initialize(block: BlockNumberFor<T>) -> Weight {
157 let Some(author) = T::GetAuthor::get_author() else {
158 log::warn!(
159 "👷 Block production log update skipped - could not determine block {block:?} producer"
160 );
161 return T::DbWeight::get().reads(1);
162 };
163 let Some(moment) = T::GetMoment::get_moment() else {
164 log::warn!(
165 "👷 Block production log update skipped - could not determine block {block:?} time"
166 );
167 return T::DbWeight::get().reads(1);
168 };
169
170 log::info!("👷 Block {block:?} producer is {author:?}");
171 Log::<T>::append((moment, author));
172
173 T::DbWeight::get().reads_writes(2, 1)
174 }
175 }
176
177 impl<T: Config> Pallet<T> {
178 /// Returns all entries up to `moment` (inclusive) and removes them from the log
179 pub fn take_prefix(moment: &T::Moment) -> Vec<(T::Moment, T::BlockProducerId)> {
180 let removed_prefix = Log::<T>::mutate(|log| {
181 let pos = log.partition_point(|(s, _)| s <= moment);
182 log.drain(..pos).collect()
183 });
184 removed_prefix
185 }
186
187 /// Returns all entries up to `moment` (inclusive) from the log
188 pub fn peek_prefix(
189 moment: &T::Moment,
190 ) -> impl Iterator<Item = (T::Moment, T::BlockProducerId)> {
191 Log::<T>::get().into_iter().take_while(move |(s, _)| s <= moment)
192 }
193
194 /// Removes all entries up to `moment` (inclusive) from the log
195 pub fn drop_prefix(moment: &T::Moment) {
196 Log::<T>::mutate(|log| {
197 let position = log.partition_point(|(s, _)| s <= moment);
198 log.drain(..position);
199 });
200 }
201 }
202}
203
204#[cfg(feature = "block-participation")]
205mod block_participation {
206 use pallet_block_participation::BlockParticipationProvider;
207
208 impl<T: crate::Config> BlockParticipationProvider<T::Moment, T::BlockProducerId>
209 for crate::Pallet<T>
210 {
211 fn blocks_to_process(
212 moment: &T::Moment,
213 ) -> impl Iterator<Item = (T::Moment, T::BlockProducerId)> {
214 Self::peek_prefix(moment)
215 }
216
217 fn discard_processed_blocks(moment: &T::Moment) {
218 Self::drop_prefix(moment)
219 }
220 }
221}
222
223mod source_impls {
224 use super::*;
225 use frame_support::{
226 pallet_prelude::StorageValue,
227 storage::types::QueryKindTrait,
228 traits::{FindAuthor, StorageInstance},
229 };
230 use pallet_session_validator_management as psvm;
231 use parity_scale_codec::FullCodec;
232 use sp_runtime::traits::Get;
233
234 impl<BlockProducerId, I, FA, T> GetAuthor<BlockProducerId> for FromFindAuthorIndex<T, FA, I>
235 where
236 FA: FindAuthor<I>,
237 I: TryInto<usize>,
238 T: psvm::Config,
239 psvm::CommitteeMemberOf<T>: Into<BlockProducerId>,
240 {
241 fn get_author() -> Option<BlockProducerId> {
242 Some(psvm::Pallet::<T>::find_current_authority::<I, FA>()?.into())
243 }
244 }
245
246 impl<Prefix, Value, Moment, QueryKind, OnEmpty> GetMoment<Moment>
247 for FromStorage<StorageValue<Prefix, Value, QueryKind, OnEmpty>>
248 where
249 Prefix: StorageInstance,
250 Value: FullCodec,
251 QueryKind: QueryKindTrait<Value, OnEmpty>,
252 OnEmpty: Get<QueryKind::Query> + 'static,
253 Option<Moment>: From<<QueryKind as QueryKindTrait<Value, OnEmpty>>::Query>,
254 {
255 fn get_moment() -> Option<Moment> {
256 StorageValue::<Prefix, Value, QueryKind, OnEmpty>::get().into()
257 }
258 }
259}